@softize/opus 9.0.9 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +2 -2
- package/docs/adr/0001-vendor-neutral-observability-context.md +96 -0
- package/docs/protocol.md +101 -12
- package/package.json +16 -1
- package/src/audit/drivers/pg.ts +60 -8
- package/src/core/actions.ts +3 -1
- package/src/core/index.ts +4 -0
- package/src/core/runtime.ts +230 -26
- package/src/core/trace.ts +34 -0
- package/src/core/types.ts +44 -0
- package/src/observability/drivers/opentelemetry.ts +165 -0
- package/src/observability/index.ts +8 -0
- package/src/queue/drivers/bullmq.ts +47 -5
- package/src/server/drivers/fastify.ts +16 -1
- package/src/server/drivers/node.ts +23 -3
- package/src/server/index.ts +57 -0
- package/src/testing/index.ts +3 -0
- package/src/ui/docs/content/audit.md +17 -0
- package/src/ui/docs/content/observability.md +71 -0
- package/src/ui/docs/content/queue.md +29 -3
- package/src/ui/docs/content/runtime.md +18 -1
package/src/core/runtime.ts
CHANGED
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { isReaction } from './reactions.ts'
|
|
15
|
+
import { isBackgroundAction } from './actions.ts'
|
|
15
16
|
import { isSchedule } from './schedules.ts'
|
|
16
17
|
import { collectDomainWarnings, flattenDomain, isDomainConfig } from './domain.ts'
|
|
17
18
|
import type { DomainConfig } from './domain.ts'
|
|
18
19
|
import { AuditEmitter } from './audit.ts'
|
|
19
20
|
import { error, isActionError, normalizeError } from './errors.ts'
|
|
21
|
+
import { normalizeTraceContext } from './trace.ts'
|
|
20
22
|
import { evalExpression } from '../dsl/eval.ts'
|
|
21
23
|
import { parseExpression } from '../dsl/parser.ts'
|
|
22
24
|
import { parseLoad, resolveLoadArgs } from '../dsl/loads.ts'
|
|
@@ -40,12 +42,16 @@ import type {
|
|
|
40
42
|
EventBusAdapter,
|
|
41
43
|
HealthStatus,
|
|
42
44
|
I18nRef,
|
|
45
|
+
JobSpec,
|
|
43
46
|
LoaderFn,
|
|
44
47
|
LoaderResolver,
|
|
45
48
|
LoadsSpec,
|
|
46
49
|
Logger,
|
|
47
50
|
LoggerAdapter,
|
|
51
|
+
ObservabilityAdapter,
|
|
52
|
+
ObservabilityOperation,
|
|
48
53
|
Provenance,
|
|
54
|
+
ProgressReporter,
|
|
49
55
|
QueueAdapter,
|
|
50
56
|
ReactionContext,
|
|
51
57
|
ReactionDef,
|
|
@@ -62,6 +68,7 @@ import type {
|
|
|
62
68
|
AiTool,
|
|
63
69
|
AIConfig,
|
|
64
70
|
User,
|
|
71
|
+
TraceContext,
|
|
65
72
|
} from './types.ts'
|
|
66
73
|
|
|
67
74
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
@@ -82,6 +89,7 @@ export interface RuntimeSetup {
|
|
|
82
89
|
data?: DataAdapter
|
|
83
90
|
auth?: AuthAdapter
|
|
84
91
|
logger?: LoggerAdapter
|
|
92
|
+
observability?: ObservabilityAdapter
|
|
85
93
|
audit?: AuditSink | AuditSink[]
|
|
86
94
|
queue?: QueueAdapter
|
|
87
95
|
eventBus?: EventBusAdapter
|
|
@@ -229,6 +237,7 @@ export class Runtime {
|
|
|
229
237
|
private readonly server: ServerAdapter | undefined
|
|
230
238
|
private readonly data: DataAdapter | undefined
|
|
231
239
|
private readonly logger: LoggerAdapter | undefined
|
|
240
|
+
private readonly observability: ObservabilityAdapter | undefined
|
|
232
241
|
private readonly queue: QueueAdapter | undefined
|
|
233
242
|
private readonly eventBus: EventBusAdapter | undefined
|
|
234
243
|
private readonly scheduler: SchedulerAdapter | undefined
|
|
@@ -267,6 +276,7 @@ export class Runtime {
|
|
|
267
276
|
constructor(setup: RuntimeSetup) {
|
|
268
277
|
this.config = applyConfigDefaults(setup.config)
|
|
269
278
|
this.logger = setup.logger
|
|
279
|
+
this.observability = setup.observability
|
|
270
280
|
this.log = setup.logger ?? new ConsoleLogger({ runtime: 'tbdlib' })
|
|
271
281
|
|
|
272
282
|
this.audit = new AuditEmitter({ mode: this.config.auditMode, log: this.log })
|
|
@@ -488,10 +498,7 @@ export class Runtime {
|
|
|
488
498
|
return { ok, adapters }
|
|
489
499
|
}
|
|
490
500
|
|
|
491
|
-
/**
|
|
492
|
-
* Executa uma action de forma síncrona. Background actions usam
|
|
493
|
-
* `executeBackground` (não implementado em v0; queue adapter dispara).
|
|
494
|
-
*/
|
|
501
|
+
/** Executa uma action; declarations background são validadas e enfileiradas. */
|
|
495
502
|
async execute(
|
|
496
503
|
actionName: string,
|
|
497
504
|
input: unknown,
|
|
@@ -506,11 +513,81 @@ export class Runtime {
|
|
|
506
513
|
}), 0, ctxBase.requestId)
|
|
507
514
|
}
|
|
508
515
|
|
|
516
|
+
return this.executeAction(action, input, ctxBase, 'request')
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Executa no worker um envelope criado por `execute()`. O consumer fornece
|
|
521
|
+
* auth/can reidratados; identidade, provenance e trace vêm do envelope.
|
|
522
|
+
*/
|
|
523
|
+
async executeJob(
|
|
524
|
+
spec: JobSpec,
|
|
525
|
+
ctxBase: ContextBase,
|
|
526
|
+
progress?: ProgressReporter,
|
|
527
|
+
): Promise<ActionResult<unknown>> {
|
|
528
|
+
const action = this.actions.get(spec.action)
|
|
529
|
+
if (action === undefined) {
|
|
530
|
+
return this.errorResult(spec.action, error({
|
|
531
|
+
code: 'runtime.action_not_found',
|
|
532
|
+
category: 'not_found',
|
|
533
|
+
message: `Action "${spec.action}" is not registered`,
|
|
534
|
+
}), 0, spec.ctx.requestId)
|
|
535
|
+
}
|
|
536
|
+
if (!isBackgroundAction(action)) {
|
|
537
|
+
return this.errorResult(spec.action, error({
|
|
538
|
+
code: 'runtime.action_not_background',
|
|
539
|
+
category: 'validation',
|
|
540
|
+
message: `Action "${spec.action}" is not declared as background`,
|
|
541
|
+
}), 0, spec.ctx.requestId)
|
|
542
|
+
}
|
|
543
|
+
if ((ctxBase.user?.id ?? null) !== spec.ctx.userId) {
|
|
544
|
+
return this.errorResult(spec.action, error({
|
|
545
|
+
code: 'runtime.job_actor_mismatch',
|
|
546
|
+
category: 'authentication',
|
|
547
|
+
message: 'Worker context actor does not match the job envelope',
|
|
548
|
+
}), 0, spec.ctx.requestId)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const trace = normalizeTraceContext(spec.ctx.trace)
|
|
552
|
+
return this.executeAction(action, spec.input, {
|
|
553
|
+
...ctxBase,
|
|
554
|
+
tenantId: spec.ctx.tenantId,
|
|
555
|
+
...(spec.ctx.requestId !== undefined ? { requestId: spec.ctx.requestId } : {}),
|
|
556
|
+
...(trace !== undefined ? { trace } : {}),
|
|
557
|
+
provenance: {
|
|
558
|
+
kind: 'background',
|
|
559
|
+
queueName: spec.config.queue ?? 'default',
|
|
560
|
+
jobId: spec.jobId,
|
|
561
|
+
...(spec.ctx.originalProvenance !== undefined
|
|
562
|
+
? { originalProvenance: spec.ctx.originalProvenance }
|
|
563
|
+
: {}),
|
|
564
|
+
},
|
|
565
|
+
}, 'job', progress)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private async executeAction(
|
|
569
|
+
action: ActionDef,
|
|
570
|
+
input: unknown,
|
|
571
|
+
ctxBase: ContextBase,
|
|
572
|
+
execution: 'request' | 'job',
|
|
573
|
+
progress?: ProgressReporter,
|
|
574
|
+
): Promise<ActionResult<unknown>> {
|
|
575
|
+
|
|
509
576
|
const startedAt = performance.now()
|
|
510
577
|
const actionId = generateId()
|
|
511
|
-
|
|
578
|
+
return this.runObserved(
|
|
579
|
+
{
|
|
580
|
+
name: action.name,
|
|
581
|
+
kind: 'action',
|
|
582
|
+
resultKind: 'action-result',
|
|
583
|
+
...(ctxBase.trace !== undefined ? { parent: ctxBase.trace } : {}),
|
|
584
|
+
attributes: { 'opus.action.id': actionId, 'opus.action.kind': action.kind },
|
|
585
|
+
},
|
|
586
|
+
ctxBase.trace,
|
|
587
|
+
async (trace) => {
|
|
588
|
+
const ctx = this.buildActionContext(action, actionId, ctxBase, trace)
|
|
512
589
|
|
|
513
|
-
|
|
590
|
+
try {
|
|
514
591
|
// — 1. Validate input ————————————————————————————————————————————————
|
|
515
592
|
const validatedInput = await this.validate(action.input, input, 'input')
|
|
516
593
|
|
|
@@ -540,6 +617,60 @@ export class Runtime {
|
|
|
540
617
|
if (decision !== true) throw decision
|
|
541
618
|
}
|
|
542
619
|
|
|
620
|
+
if (execution === 'request' && isBackgroundAction(action)) {
|
|
621
|
+
if (this.queue === undefined) {
|
|
622
|
+
throw error({
|
|
623
|
+
code: 'runtime.queue_required',
|
|
624
|
+
category: 'internal',
|
|
625
|
+
message: `Background action "${action.name}" requires a QueueAdapter`,
|
|
626
|
+
})
|
|
627
|
+
}
|
|
628
|
+
const background = action.background
|
|
629
|
+
if (background === undefined) {
|
|
630
|
+
throw error({
|
|
631
|
+
code: 'runtime.invalid_background_config',
|
|
632
|
+
category: 'internal',
|
|
633
|
+
message: `Background action "${action.name}" has no background config`,
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
const spec: JobSpec = {
|
|
637
|
+
jobId: generateId(),
|
|
638
|
+
action: action.name,
|
|
639
|
+
input: validatedInput,
|
|
640
|
+
ctx: {
|
|
641
|
+
userId: ctx.user?.id ?? null,
|
|
642
|
+
tenantId: ctx.tenantId,
|
|
643
|
+
...(ctxBase.requestId !== undefined ? { requestId: ctxBase.requestId } : {}),
|
|
644
|
+
...(trace !== undefined ? { trace } : {}),
|
|
645
|
+
originalProvenance: ctx.provenance,
|
|
646
|
+
},
|
|
647
|
+
config: {
|
|
648
|
+
...(background.queue !== undefined ? { queue: background.queue } : {}),
|
|
649
|
+
...(background.priority !== undefined ? { priority: background.priority } : {}),
|
|
650
|
+
retry: background.retry ?? this.config.defaultRetry,
|
|
651
|
+
...(background.timeout !== undefined ? { timeout: background.timeout } : {}),
|
|
652
|
+
},
|
|
653
|
+
}
|
|
654
|
+
const queuedHandle = await this.queue.enqueue(spec)
|
|
655
|
+
const handle = trace !== undefined ? { ...queuedHandle, trace } : queuedHandle
|
|
656
|
+
const durationMs = performance.now() - startedAt
|
|
657
|
+
await this.audit.emit(this.buildAuditRecord({
|
|
658
|
+
actionId,
|
|
659
|
+
action: action.name,
|
|
660
|
+
actionKind: action.kind,
|
|
661
|
+
outcome: 'success',
|
|
662
|
+
durationMs,
|
|
663
|
+
ctx,
|
|
664
|
+
input: validatedInput,
|
|
665
|
+
output: handle,
|
|
666
|
+
}), action.audit)
|
|
667
|
+
return {
|
|
668
|
+
ok: true,
|
|
669
|
+
data: handle,
|
|
670
|
+
meta: this.buildMeta(actionId, action.name, durationMs, ctxBase.requestId, trace),
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
543
674
|
// — 5. Handler ————————————————————————————————————————————————————————
|
|
544
675
|
// Modo design (`OPUS_MODE=design`): tenta mockHandler primeiro, cai
|
|
545
676
|
// no handler real se não houver. Em qualquer outro modo, handler real
|
|
@@ -550,7 +681,14 @@ export class Runtime {
|
|
|
550
681
|
isDesignMode && action.mockHandler !== undefined
|
|
551
682
|
? action.mockHandler
|
|
552
683
|
: action.handler
|
|
553
|
-
const output = await handlerToRun(
|
|
684
|
+
const output = await handlerToRun(
|
|
685
|
+
ctx,
|
|
686
|
+
validatedInput,
|
|
687
|
+
loaded,
|
|
688
|
+
execution === 'job' && isBackgroundAction(action) && action.background?.progress === true
|
|
689
|
+
? progress
|
|
690
|
+
: undefined,
|
|
691
|
+
)
|
|
554
692
|
const usedMock = handlerToRun === action.mockHandler
|
|
555
693
|
|
|
556
694
|
// — 6. Validate output (dev only) —————————————————————————————————————
|
|
@@ -583,9 +721,9 @@ export class Runtime {
|
|
|
583
721
|
return {
|
|
584
722
|
ok: true,
|
|
585
723
|
data: validatedOutput,
|
|
586
|
-
meta: this.buildMeta(actionId, action.name, durationMs, ctxBase.requestId),
|
|
724
|
+
meta: this.buildMeta(actionId, action.name, durationMs, ctxBase.requestId, trace),
|
|
587
725
|
}
|
|
588
|
-
|
|
726
|
+
} catch (thrown) {
|
|
589
727
|
const actionError = normalizeError(thrown)
|
|
590
728
|
const durationMs = performance.now() - startedAt
|
|
591
729
|
await this.audit.emit(
|
|
@@ -604,9 +742,11 @@ export class Runtime {
|
|
|
604
742
|
return {
|
|
605
743
|
ok: false,
|
|
606
744
|
error: actionError,
|
|
607
|
-
meta: this.buildMeta(actionId, action.name, durationMs, ctxBase.requestId),
|
|
745
|
+
meta: this.buildMeta(actionId, action.name, durationMs, ctxBase.requestId, trace),
|
|
608
746
|
}
|
|
609
|
-
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
)
|
|
610
750
|
}
|
|
611
751
|
|
|
612
752
|
// ===========================================================================
|
|
@@ -615,6 +755,7 @@ export class Runtime {
|
|
|
615
755
|
|
|
616
756
|
private *allAdapters(): IterableIterator<Adapter> {
|
|
617
757
|
if (this.logger !== undefined) yield this.logger
|
|
758
|
+
if (this.observability !== undefined) yield this.observability
|
|
618
759
|
if (this.data !== undefined) yield this.data
|
|
619
760
|
if (this.auth !== undefined) yield this.auth
|
|
620
761
|
if (this.server !== undefined) yield this.server
|
|
@@ -698,6 +839,7 @@ export class Runtime {
|
|
|
698
839
|
action: ActionDef,
|
|
699
840
|
actionId: string,
|
|
700
841
|
base: ContextBase,
|
|
842
|
+
trace: TraceContext | undefined,
|
|
701
843
|
): ActionContext {
|
|
702
844
|
const dbExt = this.data?.contextExtension() ?? { db: undefined }
|
|
703
845
|
const provenance = resolveProvenance(base)
|
|
@@ -716,10 +858,11 @@ export class Runtime {
|
|
|
716
858
|
can: base.can,
|
|
717
859
|
db: dbExt.db,
|
|
718
860
|
log,
|
|
719
|
-
emit: this.buildEmit(action, actionId, base),
|
|
861
|
+
emit: this.buildEmit(action, actionId, base, trace),
|
|
720
862
|
storage: this.storage ?? null,
|
|
721
|
-
ai: this.bindAi(base),
|
|
863
|
+
ai: this.bindAi({ ...base, ...(trace !== undefined ? { trace } : {}) }),
|
|
722
864
|
provenance,
|
|
865
|
+
...(trace !== undefined ? { trace } : {}),
|
|
723
866
|
meta: base.meta ?? {},
|
|
724
867
|
}
|
|
725
868
|
}
|
|
@@ -728,6 +871,7 @@ export class Runtime {
|
|
|
728
871
|
action: ActionDef,
|
|
729
872
|
actionId: string,
|
|
730
873
|
base: ContextBase,
|
|
874
|
+
trace: TraceContext | undefined,
|
|
731
875
|
): ActionContext['emit'] {
|
|
732
876
|
return async (event, data, meta) => {
|
|
733
877
|
if (
|
|
@@ -769,6 +913,7 @@ export class Runtime {
|
|
|
769
913
|
actionId,
|
|
770
914
|
...(meta?.correlation !== undefined ? { correlation: meta.correlation } : {}),
|
|
771
915
|
},
|
|
916
|
+
...(trace !== undefined ? { trace } : {}),
|
|
772
917
|
...(meta !== undefined ? { meta: meta as Record<string, unknown> } : {}),
|
|
773
918
|
}
|
|
774
919
|
|
|
@@ -933,6 +1078,7 @@ export class Runtime {
|
|
|
933
1078
|
tenant: args.ctx.tenantId,
|
|
934
1079
|
input: args.input,
|
|
935
1080
|
severity: args.outcome === 'success' ? 'info' : severityForError(args.error),
|
|
1081
|
+
...(args.ctx.trace !== undefined ? { traceContext: args.ctx.trace } : {}),
|
|
936
1082
|
}
|
|
937
1083
|
if (args.output !== undefined) record.output = args.output
|
|
938
1084
|
if (args.error !== undefined) record.error = args.error
|
|
@@ -945,6 +1091,7 @@ export class Runtime {
|
|
|
945
1091
|
actionName: string,
|
|
946
1092
|
durationMs: number,
|
|
947
1093
|
requestId?: string,
|
|
1094
|
+
trace?: TraceContext,
|
|
948
1095
|
): ResultMeta {
|
|
949
1096
|
const meta: ResultMeta = {
|
|
950
1097
|
actionId,
|
|
@@ -952,6 +1099,7 @@ export class Runtime {
|
|
|
952
1099
|
durationMs,
|
|
953
1100
|
}
|
|
954
1101
|
if (requestId !== undefined) meta.requestId = requestId
|
|
1102
|
+
if (trace !== undefined) meta.trace = trace
|
|
955
1103
|
return meta
|
|
956
1104
|
}
|
|
957
1105
|
|
|
@@ -1021,15 +1169,27 @@ export class Runtime {
|
|
|
1021
1169
|
sourceActionId: event.source.actionId,
|
|
1022
1170
|
eventType: event.type,
|
|
1023
1171
|
}
|
|
1024
|
-
const
|
|
1172
|
+
const reactionLog = this.log.child({
|
|
1173
|
+
reaction: reaction.name,
|
|
1174
|
+
eventType: event.type,
|
|
1175
|
+
provenance: 'reaction',
|
|
1176
|
+
})
|
|
1177
|
+
try {
|
|
1178
|
+
await this.runObserved(
|
|
1179
|
+
{
|
|
1180
|
+
name: reaction.name,
|
|
1181
|
+
kind: 'reaction',
|
|
1182
|
+
resultKind: 'void',
|
|
1183
|
+
...(event.trace !== undefined ? { parent: event.trace } : {}),
|
|
1184
|
+
attributes: { 'opus.event.type': event.type, 'opus.source.action_id': event.source.actionId },
|
|
1185
|
+
},
|
|
1186
|
+
event.trace,
|
|
1187
|
+
async (trace) => {
|
|
1188
|
+
const ctx: ReactionContext = {
|
|
1025
1189
|
user: event.actor.id !== null ? ({ id: event.actor.id } as User) : null,
|
|
1026
1190
|
tenantId: event.tenant ?? null,
|
|
1027
1191
|
db: this.data?.contextExtension().db,
|
|
1028
|
-
log:
|
|
1029
|
-
reaction: reaction.name,
|
|
1030
|
-
eventType: event.type,
|
|
1031
|
-
provenance: 'reaction',
|
|
1032
|
-
}),
|
|
1192
|
+
log: reactionLog,
|
|
1033
1193
|
emit: noopEmit,
|
|
1034
1194
|
storage: this.storage ?? null,
|
|
1035
1195
|
// Reação = contexto de sistema; o agente só alcança actions públicas (can nega por
|
|
@@ -1039,31 +1199,75 @@ export class Runtime {
|
|
|
1039
1199
|
tenantId: event.tenant ?? null,
|
|
1040
1200
|
can: async () => false,
|
|
1041
1201
|
meta: {},
|
|
1202
|
+
...(trace !== undefined ? { trace } : {}),
|
|
1042
1203
|
}),
|
|
1043
1204
|
provenance,
|
|
1205
|
+
...(trace !== undefined ? { trace } : {}),
|
|
1044
1206
|
meta: {},
|
|
1045
1207
|
}
|
|
1046
1208
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1209
|
+
if (reaction.authorize !== undefined) {
|
|
1210
|
+
const allowed = await reaction.authorize(ctx, event)
|
|
1211
|
+
if (!allowed) return
|
|
1212
|
+
}
|
|
1213
|
+
await reaction.handler(ctx, event)
|
|
1214
|
+
},
|
|
1215
|
+
)
|
|
1053
1216
|
} catch (err) {
|
|
1054
1217
|
const actionError = isActionError(err) ? err : normalizeError(err)
|
|
1055
|
-
|
|
1218
|
+
reactionLog.error('reaction failed', {
|
|
1056
1219
|
code: actionError.code,
|
|
1057
1220
|
message: actionError.message,
|
|
1058
1221
|
})
|
|
1059
1222
|
}
|
|
1060
1223
|
}
|
|
1224
|
+
|
|
1225
|
+
/** Observabilidade nunca repete nem substitui o resultado do callback. */
|
|
1226
|
+
private async runObserved<T>(
|
|
1227
|
+
operation: ObservabilityOperation,
|
|
1228
|
+
fallbackTrace: TraceContext | undefined,
|
|
1229
|
+
run: (trace: TraceContext | undefined) => Promise<T>,
|
|
1230
|
+
): Promise<T> {
|
|
1231
|
+
if (this.observability === undefined) return run(fallbackTrace)
|
|
1232
|
+
|
|
1233
|
+
let execution: Promise<T> | undefined
|
|
1234
|
+
const executeOnce = (trace: TraceContext | undefined): Promise<T> => {
|
|
1235
|
+
if (execution !== undefined) return execution
|
|
1236
|
+
// Atribui a Promise antes de ceder controle: chamadas concorrentes, tardias ou
|
|
1237
|
+
// malcomportadas do adapter observam a mesma execução e nunca repetem efeitos.
|
|
1238
|
+
execution = Promise.resolve().then(() => run(trace))
|
|
1239
|
+
return execution
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
try {
|
|
1243
|
+
await this.observability.runInSpan(operation, executeOnce)
|
|
1244
|
+
} catch (err) {
|
|
1245
|
+
// Rejeição da própria execução pertence à action/reaction e será devolvida abaixo;
|
|
1246
|
+
// só uma falha antes/depois do callback é falha de instrumentação.
|
|
1247
|
+
if (execution === undefined || await settledSuccessfully(execution)) {
|
|
1248
|
+
this.log.warn('observability adapter failed (lenient)', {
|
|
1249
|
+
operation: operation.name,
|
|
1250
|
+
error: String(err),
|
|
1251
|
+
})
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return executeOnce(fallbackTrace)
|
|
1255
|
+
}
|
|
1061
1256
|
}
|
|
1062
1257
|
|
|
1063
1258
|
// =============================================================================
|
|
1064
1259
|
// Helpers
|
|
1065
1260
|
// =============================================================================
|
|
1066
1261
|
|
|
1262
|
+
async function settledSuccessfully<T>(promise: Promise<T>): Promise<boolean> {
|
|
1263
|
+
try {
|
|
1264
|
+
await promise
|
|
1265
|
+
return true
|
|
1266
|
+
} catch {
|
|
1267
|
+
return false
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1067
1271
|
// ContextBase moveu pra types.ts (compartilhado entre core e adapters).
|
|
1068
1272
|
|
|
1069
1273
|
/**
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { TraceContext } from './types.ts'
|
|
2
|
+
|
|
3
|
+
/** Valida carriers desserializados sem aceitar campos extras ou payload arbitrário. */
|
|
4
|
+
export function normalizeTraceContext(value: unknown): TraceContext | undefined {
|
|
5
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined
|
|
6
|
+
const record = value as Record<string, unknown>
|
|
7
|
+
const allowed = new Set(['traceId', 'spanId', 'traceFlags', 'traceState'])
|
|
8
|
+
if (Object.keys(record).some((key) => !allowed.has(key))) return undefined
|
|
9
|
+
if (
|
|
10
|
+
typeof record['traceId'] !== 'string' || record['traceId'].length === 0 ||
|
|
11
|
+
record['traceId'].length > 128
|
|
12
|
+
) return undefined
|
|
13
|
+
if (
|
|
14
|
+
record['spanId'] !== undefined &&
|
|
15
|
+
(typeof record['spanId'] !== 'string' || record['spanId'].length === 0 ||
|
|
16
|
+
record['spanId'].length > 64)
|
|
17
|
+
) return undefined
|
|
18
|
+
if (
|
|
19
|
+
record['traceFlags'] !== undefined &&
|
|
20
|
+
(!Number.isInteger(record['traceFlags']) || (record['traceFlags'] as number) < 0 ||
|
|
21
|
+
(record['traceFlags'] as number) > 255)
|
|
22
|
+
) return undefined
|
|
23
|
+
if (
|
|
24
|
+
record['traceState'] !== undefined &&
|
|
25
|
+
(typeof record['traceState'] !== 'string' || record['traceState'].length > 512 ||
|
|
26
|
+
/[^\x20-\x7e]/.test(record['traceState']))
|
|
27
|
+
) return undefined
|
|
28
|
+
return {
|
|
29
|
+
traceId: record['traceId'],
|
|
30
|
+
...(record['spanId'] !== undefined ? { spanId: record['spanId'] as string } : {}),
|
|
31
|
+
...(record['traceFlags'] !== undefined ? { traceFlags: record['traceFlags'] as number } : {}),
|
|
32
|
+
...(record['traceState'] !== undefined ? { traceState: record['traceState'] as string } : {}),
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/core/types.ts
CHANGED
|
@@ -38,6 +38,14 @@ export type I18nRef = string | { key: string; default: string }
|
|
|
38
38
|
*/
|
|
39
39
|
export type Unsubscribe = () => void
|
|
40
40
|
|
|
41
|
+
/** Contexto de propagação distribuída, independente de SDK/vendor. */
|
|
42
|
+
export interface TraceContext {
|
|
43
|
+
traceId: string
|
|
44
|
+
spanId?: string
|
|
45
|
+
traceFlags?: number
|
|
46
|
+
traceState?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
/**
|
|
42
50
|
* Logger genérico que mora no contexto. Implementado por `LoggerAdapter`.
|
|
43
51
|
* Detalhes: §8.
|
|
@@ -169,6 +177,7 @@ export interface ResultMeta {
|
|
|
169
177
|
action: string
|
|
170
178
|
durationMs: number
|
|
171
179
|
requestId?: string
|
|
180
|
+
trace?: TraceContext
|
|
172
181
|
cached?: boolean
|
|
173
182
|
}
|
|
174
183
|
|
|
@@ -270,6 +279,7 @@ export interface ActionContext {
|
|
|
270
279
|
storage: StorageAdapter | null
|
|
271
280
|
ai: BoundAi | null
|
|
272
281
|
provenance: Provenance
|
|
282
|
+
trace?: TraceContext
|
|
273
283
|
meta: Record<string, unknown>
|
|
274
284
|
}
|
|
275
285
|
|
|
@@ -286,6 +296,7 @@ export interface ReactionContext {
|
|
|
286
296
|
storage: StorageAdapter | null
|
|
287
297
|
ai: BoundAi | null
|
|
288
298
|
provenance: Provenance
|
|
299
|
+
trace?: TraceContext
|
|
289
300
|
meta: Record<string, unknown>
|
|
290
301
|
}
|
|
291
302
|
|
|
@@ -334,10 +345,13 @@ export interface AuditRecord {
|
|
|
334
345
|
error?: ActionError
|
|
335
346
|
|
|
336
347
|
severity: 'info' | 'warning' | 'error'
|
|
348
|
+
/** Campo legado preservado para compatibilidade com sinks/consumers existentes. */
|
|
337
349
|
trace?: {
|
|
338
350
|
requestId?: string
|
|
339
351
|
parentActionId?: string
|
|
340
352
|
}
|
|
353
|
+
/** Contexto de trace vendor-neutral; aditivo ao campo `trace` legado. */
|
|
354
|
+
traceContext?: TraceContext
|
|
341
355
|
meta?: Record<string, unknown>
|
|
342
356
|
}
|
|
343
357
|
|
|
@@ -373,6 +387,7 @@ export interface DomainEvent<T = unknown> {
|
|
|
373
387
|
actionId: string
|
|
374
388
|
correlation?: string
|
|
375
389
|
}
|
|
390
|
+
trace?: TraceContext
|
|
376
391
|
meta?: Record<string, unknown>
|
|
377
392
|
}
|
|
378
393
|
|
|
@@ -397,6 +412,7 @@ export interface JobHandle<T = unknown> {
|
|
|
397
412
|
startedAt?: string
|
|
398
413
|
finishedAt?: string
|
|
399
414
|
attempts: number
|
|
415
|
+
trace?: TraceContext
|
|
400
416
|
}
|
|
401
417
|
|
|
402
418
|
/**
|
|
@@ -411,6 +427,8 @@ export interface JobSpec {
|
|
|
411
427
|
userId: string | null
|
|
412
428
|
tenantId: string | null
|
|
413
429
|
requestId?: string
|
|
430
|
+
trace?: TraceContext
|
|
431
|
+
originalProvenance?: Provenance
|
|
414
432
|
}
|
|
415
433
|
config: {
|
|
416
434
|
queue?: string
|
|
@@ -926,6 +944,7 @@ export type AdapterKind =
|
|
|
926
944
|
| 'ai'
|
|
927
945
|
| 'ui'
|
|
928
946
|
| 'schema'
|
|
947
|
+
| 'observability'
|
|
929
948
|
|
|
930
949
|
/**
|
|
931
950
|
* Resultado de health check de um adapter (ou do runtime agregado).
|
|
@@ -964,6 +983,8 @@ export interface ContextBase {
|
|
|
964
983
|
provenance?: Provenance
|
|
965
984
|
meta?: Record<string, unknown>
|
|
966
985
|
requestId?: string
|
|
986
|
+
/** Trace pai recebido do transporte, evento ou job. */
|
|
987
|
+
trace?: TraceContext
|
|
967
988
|
}
|
|
968
989
|
|
|
969
990
|
/**
|
|
@@ -982,6 +1003,11 @@ export interface RuntimeRef {
|
|
|
982
1003
|
input: unknown,
|
|
983
1004
|
ctx: ContextBase,
|
|
984
1005
|
): Promise<ActionResult<unknown>>
|
|
1006
|
+
executeJob(
|
|
1007
|
+
spec: JobSpec,
|
|
1008
|
+
ctx: ContextBase,
|
|
1009
|
+
progress?: ProgressReporter,
|
|
1010
|
+
): Promise<ActionResult<unknown>>
|
|
985
1011
|
healthCheck(): Promise<{
|
|
986
1012
|
ok: boolean
|
|
987
1013
|
adapters: Record<string, HealthStatus>
|
|
@@ -1076,6 +1102,24 @@ export interface LoggerAdapter extends Adapter, Logger {
|
|
|
1076
1102
|
kind: 'logger'
|
|
1077
1103
|
}
|
|
1078
1104
|
|
|
1105
|
+
export interface ObservabilityOperation {
|
|
1106
|
+
name: string
|
|
1107
|
+
kind: 'action' | 'reaction'
|
|
1108
|
+
/** Como o driver determina outcome: action inspeciona `ActionResult.ok`; reaction usa reject. */
|
|
1109
|
+
resultKind: 'action-result' | 'void'
|
|
1110
|
+
parent?: TraceContext
|
|
1111
|
+
attributes?: Record<string, string | number | boolean>
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/** Porta de contexto ativo; drivers podem usar OTel, ALS ou outro mecanismo. */
|
|
1115
|
+
export interface ObservabilityAdapter extends Adapter {
|
|
1116
|
+
kind: 'observability'
|
|
1117
|
+
runInSpan<T>(
|
|
1118
|
+
operation: ObservabilityOperation,
|
|
1119
|
+
run: (trace: TraceContext) => Promise<T>,
|
|
1120
|
+
): Promise<T>
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1079
1123
|
export interface QueueAdapter extends Adapter {
|
|
1080
1124
|
kind: 'queue'
|
|
1081
1125
|
enqueue(spec: JobSpec): Promise<JobHandle>
|