feat(tess): add safe runtime observability (#726)
This commit was merged in pull request #726.
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import { ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { AgentRuntimeProviderRegistry } from '@mosaicstack/agent';
|
||||
import {
|
||||
createRuntimeAuditLogEntry,
|
||||
type LogService,
|
||||
type RuntimeAuditErrorCode,
|
||||
} from '@mosaicstack/log';
|
||||
import type {
|
||||
AgentRuntimeProvider,
|
||||
RuntimeAttachHandle,
|
||||
@@ -14,6 +19,7 @@ import type {
|
||||
RuntimeStreamEvent,
|
||||
} from '@mosaicstack/types';
|
||||
import type { ActorTenantScope } from '../auth/session-scope.js';
|
||||
import { LOG_SERVICE } from '../log/log.tokens.js';
|
||||
|
||||
export const AGENT_RUNTIME_PROVIDER_REGISTRY = Symbol('AGENT_RUNTIME_PROVIDER_REGISTRY');
|
||||
export const RUNTIME_PROVIDER_AUDIT_SINK = Symbol('RUNTIME_PROVIDER_AUDIT_SINK');
|
||||
@@ -42,6 +48,8 @@ export interface RuntimeAuditEvent {
|
||||
channelId: string;
|
||||
correlationId: string;
|
||||
resourceId?: string;
|
||||
durationMs?: number;
|
||||
errorCode?: RuntimeAuditErrorCode;
|
||||
}
|
||||
|
||||
export interface RuntimeAuditSink {
|
||||
@@ -87,8 +95,12 @@ export class DenyRuntimeApprovalVerifier implements RuntimeApprovalVerifier {
|
||||
export class RuntimeProviderAuditService implements RuntimeAuditSink {
|
||||
private readonly logger = new Logger(RuntimeProviderAuditService.name);
|
||||
|
||||
constructor(@Inject(LOG_SERVICE) private readonly logService: LogService) {}
|
||||
|
||||
async record(event: RuntimeAuditEvent): Promise<void> {
|
||||
this.logger.log(JSON.stringify(event));
|
||||
const entry = createRuntimeAuditLogEntry(event);
|
||||
await this.logService.logs.ingest(entry);
|
||||
this.logger.log(JSON.stringify({ event: entry.content, metadata: entry.metadata }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +279,7 @@ export class RuntimeProviderService {
|
||||
invoke: (provider: AgentRuntimeProvider, scope: RuntimeScope) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const scope = this.deriveScope(context);
|
||||
const startedAt = Date.now();
|
||||
await this.record(providerId, operation, 'requested', scope, resourceId);
|
||||
let invocationStarted = false;
|
||||
try {
|
||||
@@ -276,13 +289,22 @@ export class RuntimeProviderService {
|
||||
}
|
||||
invocationStarted = true;
|
||||
const result = await invoke(provider, scope);
|
||||
await this.recordCompletion(providerId, operation, scope, resourceId);
|
||||
await this.recordCompletion(providerId, operation, scope, resourceId, Date.now() - startedAt);
|
||||
return result;
|
||||
} catch (error: unknown) {
|
||||
const durationMs = Date.now() - startedAt;
|
||||
if (invocationStarted && !(error instanceof RuntimeApprovalDeniedError)) {
|
||||
await this.recordFailure(providerId, operation, scope, resourceId);
|
||||
await this.recordFailure(providerId, operation, scope, resourceId, durationMs);
|
||||
} else {
|
||||
await this.record(providerId, operation, 'denied', scope, resourceId);
|
||||
await this.record(
|
||||
providerId,
|
||||
operation,
|
||||
'denied',
|
||||
scope,
|
||||
resourceId,
|
||||
durationMs,
|
||||
'policy_denied',
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -300,6 +322,7 @@ export class RuntimeProviderService {
|
||||
) => AsyncIterable<RuntimeStreamEvent>,
|
||||
): AsyncIterable<RuntimeStreamEvent> {
|
||||
const scope = this.deriveScope(context);
|
||||
const startedAt = Date.now();
|
||||
await this.record(providerId, operation, 'requested', scope, resourceId);
|
||||
let invocationStarted = false;
|
||||
try {
|
||||
@@ -309,12 +332,21 @@ export class RuntimeProviderService {
|
||||
for await (const event of invoke(provider, scope)) {
|
||||
yield event;
|
||||
}
|
||||
await this.recordCompletion(providerId, operation, scope, resourceId);
|
||||
await this.recordCompletion(providerId, operation, scope, resourceId, Date.now() - startedAt);
|
||||
} catch (error: unknown) {
|
||||
const durationMs = Date.now() - startedAt;
|
||||
if (invocationStarted) {
|
||||
await this.recordFailure(providerId, operation, scope, resourceId);
|
||||
await this.recordFailure(providerId, operation, scope, resourceId, durationMs);
|
||||
} else {
|
||||
await this.record(providerId, operation, 'denied', scope, resourceId);
|
||||
await this.record(
|
||||
providerId,
|
||||
operation,
|
||||
'denied',
|
||||
scope,
|
||||
resourceId,
|
||||
durationMs,
|
||||
'policy_denied',
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -358,9 +390,18 @@ export class RuntimeProviderService {
|
||||
operation: RuntimeProviderOperation,
|
||||
scope: RuntimeScope,
|
||||
resourceId: string | undefined,
|
||||
durationMs: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.record(providerId, operation, 'failed', scope, resourceId);
|
||||
await this.record(
|
||||
providerId,
|
||||
operation,
|
||||
'failed',
|
||||
scope,
|
||||
resourceId,
|
||||
durationMs,
|
||||
'provider_error',
|
||||
);
|
||||
} catch {
|
||||
this.logger.error(
|
||||
`Runtime provider failure audit failed provider=${providerId} operation=${operation} correlation=${scope.correlationId}`,
|
||||
@@ -373,9 +414,10 @@ export class RuntimeProviderService {
|
||||
operation: RuntimeProviderOperation,
|
||||
scope: RuntimeScope,
|
||||
resourceId: string | undefined,
|
||||
durationMs: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.record(providerId, operation, 'succeeded', scope, resourceId);
|
||||
await this.record(providerId, operation, 'succeeded', scope, resourceId, durationMs);
|
||||
} catch {
|
||||
this.logger.error(
|
||||
`Runtime provider completion audit failed provider=${providerId} operation=${operation} correlation=${scope.correlationId}`,
|
||||
@@ -389,6 +431,8 @@ export class RuntimeProviderService {
|
||||
outcome: RuntimeProviderAuditOutcome,
|
||||
scope: RuntimeScope,
|
||||
resourceId: string | undefined,
|
||||
durationMs?: number,
|
||||
errorCode?: RuntimeAuditErrorCode,
|
||||
): Promise<void> {
|
||||
await this.audit.record({
|
||||
providerId,
|
||||
@@ -399,6 +443,8 @@ export class RuntimeProviderService {
|
||||
channelId: scope.channelId,
|
||||
correlationId: scope.correlationId,
|
||||
...(resourceId ? { resourceId } : {}),
|
||||
...(durationMs !== undefined ? { durationMs } : {}),
|
||||
...(errorCode ? { errorCode } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user