@graphorin/observability 0.6.0 → 0.7.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 +42 -0
- package/README.md +29 -5
- package/dist/cost/cost-tracker.d.ts.map +1 -1
- package/dist/cost/cost-tracker.js +34 -0
- package/dist/cost/cost-tracker.js.map +1 -1
- package/dist/cost/delegate.d.ts +40 -0
- package/dist/cost/delegate.d.ts.map +1 -0
- package/dist/cost/delegate.js +31 -0
- package/dist/cost/delegate.js.map +1 -0
- package/dist/cost/index.d.ts +2 -1
- package/dist/cost/index.js +2 -1
- package/dist/cost/types.d.ts +39 -0
- package/dist/cost/types.d.ts.map +1 -1
- package/dist/exporters/otlp-http.d.ts.map +1 -1
- package/dist/exporters/otlp-http.js +4 -2
- package/dist/exporters/otlp-http.js.map +1 -1
- package/dist/exporters/types.d.ts +7 -0
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/exporters/types.js.map +1 -1
- package/dist/exporters/with-validation.d.ts.map +1 -1
- package/dist/exporters/with-validation.js +5 -4
- package/dist/exporters/with-validation.js.map +1 -1
- package/dist/gen-ai/emit.js +9 -1
- package/dist/gen-ai/emit.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -10
- package/dist/index.js.map +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/dist/redaction/imperative-patterns.d.ts +3 -3
- package/dist/redaction/imperative-patterns.d.ts.map +1 -1
- package/dist/redaction/imperative-patterns.js +7 -0
- package/dist/redaction/imperative-patterns.js.map +1 -1
- package/dist/redaction/patterns.d.ts.map +1 -1
- package/dist/redaction/patterns.js +2 -2
- package/dist/redaction/patterns.js.map +1 -1
- package/dist/tracer/sampling.d.ts +6 -1
- package/dist/tracer/sampling.d.ts.map +1 -1
- package/dist/tracer/sampling.js +7 -1
- package/dist/tracer/sampling.js.map +1 -1
- package/dist/tracer/span.d.ts.map +1 -1
- package/dist/tracer/span.js +12 -3
- package/dist/tracer/span.js.map +1 -1
- package/package.json +18 -35
- package/src/cost/cost-tracker.ts +315 -0
- package/src/cost/delegate.ts +79 -0
- package/src/cost/index.ts +23 -0
- package/src/cost/types.ts +151 -0
- package/src/eval/index.ts +16 -0
- package/src/eval/runner.ts +146 -0
- package/src/eval/types.ts +122 -0
- package/src/exporters/console.ts +75 -0
- package/src/exporters/index.ts +36 -0
- package/src/exporters/jsonl.ts +167 -0
- package/src/exporters/otlp-http.ts +178 -0
- package/src/exporters/types.ts +85 -0
- package/src/exporters/with-validation.ts +176 -0
- package/src/gen-ai/emit.ts +157 -0
- package/src/gen-ai/index.ts +26 -0
- package/src/gen-ai/operation-mapping.ts +89 -0
- package/src/gen-ai/system-derivation.ts +84 -0
- package/src/gen-ai/types.ts +143 -0
- package/src/index.ts +36 -0
- package/src/logger/index.ts +13 -0
- package/src/logger/logger.ts +178 -0
- package/src/openinference/index.ts +133 -0
- package/src/redaction/config.ts +50 -0
- package/src/redaction/errors.ts +53 -0
- package/src/redaction/imperative-patterns.ts +268 -0
- package/src/redaction/index.ts +42 -0
- package/src/redaction/patterns.ts +263 -0
- package/src/redaction/types.ts +116 -0
- package/src/redaction/validator.ts +302 -0
- package/src/replay/config.ts +58 -0
- package/src/replay/index.ts +17 -0
- package/src/replay/log.ts +89 -0
- package/src/replay/replay.ts +196 -0
- package/src/replay/types.ts +112 -0
- package/src/telemetry/index.ts +94 -0
- package/src/tracer/ids.ts +27 -0
- package/src/tracer/index.ts +22 -0
- package/src/tracer/sampling.ts +170 -0
- package/src/tracer/span-names.ts +52 -0
- package/src/tracer/span.ts +175 -0
- package/src/tracer/tracer.ts +309 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mapping table from Graphorin `SpanType` values to canonical
|
|
3
|
+
* `gen_ai.operation.name` enum values.
|
|
4
|
+
*
|
|
5
|
+
* The table mirrors the canonical-mapping reference table published
|
|
6
|
+
* in the architecture documentation and is the authoritative source
|
|
7
|
+
* for the `emitGenAIAttributes` helper.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { SpanType } from '@graphorin/core';
|
|
13
|
+
|
|
14
|
+
import type { GenAIOperationName } from './types.js';
|
|
15
|
+
|
|
16
|
+
const TABLE: ReadonlyArray<readonly [SpanType, GenAIOperationName]> = Object.freeze([
|
|
17
|
+
// E8: match what the runtime actually emits (agent factory stamps
|
|
18
|
+
// `gen_ai.operation.name: 'invoke_agent'` on both run + step spans) and the
|
|
19
|
+
// OTel GenAI operation enum, which has `invoke_agent` and no step concept.
|
|
20
|
+
['agent.run', 'invoke_agent'],
|
|
21
|
+
['agent.step', 'invoke_agent'],
|
|
22
|
+
['agent.handoff', 'agent.handoff'],
|
|
23
|
+
['agent.suspend', 'agent.suspend'],
|
|
24
|
+
['agent.resume', 'agent.resume'],
|
|
25
|
+
|
|
26
|
+
['provider.generate', 'chat'],
|
|
27
|
+
['provider.stream', 'chat'],
|
|
28
|
+
|
|
29
|
+
['tool.execute', 'execute_tool'],
|
|
30
|
+
['tool.approval', 'execute_tool'],
|
|
31
|
+
|
|
32
|
+
['memory.read.working', 'memory.read'],
|
|
33
|
+
['memory.read.session', 'memory.read'],
|
|
34
|
+
['memory.read.episodic', 'memory.read'],
|
|
35
|
+
['memory.read.semantic', 'memory.read'],
|
|
36
|
+
['memory.read.procedural', 'memory.read'],
|
|
37
|
+
['memory.read.shared', 'memory.read'],
|
|
38
|
+
['memory.write.working', 'memory.write'],
|
|
39
|
+
['memory.write.session', 'memory.write'],
|
|
40
|
+
['memory.write.episodic', 'memory.write'],
|
|
41
|
+
['memory.write.semantic', 'memory.write'],
|
|
42
|
+
['memory.write.procedural', 'memory.write'],
|
|
43
|
+
['memory.write.shared', 'memory.write'],
|
|
44
|
+
['memory.search.working', 'memory.search'],
|
|
45
|
+
['memory.search.session', 'memory.search'],
|
|
46
|
+
['memory.search.episodic', 'memory.search'],
|
|
47
|
+
['memory.search.semantic', 'memory.search'],
|
|
48
|
+
['memory.search.procedural', 'memory.search'],
|
|
49
|
+
['memory.search.shared', 'memory.search'],
|
|
50
|
+
['memory.consolidate.light', 'memory.consolidate'],
|
|
51
|
+
['memory.consolidate.standard', 'memory.consolidate'],
|
|
52
|
+
['memory.consolidate.deep', 'memory.consolidate'],
|
|
53
|
+
['memory.conflict', 'memory.conflict'],
|
|
54
|
+
['memory.embed', 'embedding'],
|
|
55
|
+
|
|
56
|
+
['workflow.run', 'workflow.run'],
|
|
57
|
+
['workflow.step', 'workflow.step'],
|
|
58
|
+
['workflow.task', 'workflow.task'],
|
|
59
|
+
['workflow.checkpoint', 'workflow.checkpoint'],
|
|
60
|
+
|
|
61
|
+
['skill.activate', 'skill.activate'],
|
|
62
|
+
['skill.load', 'skill.load'],
|
|
63
|
+
|
|
64
|
+
['mcp.connect', 'mcp.connect'],
|
|
65
|
+
['mcp.call', 'mcp.call'],
|
|
66
|
+
['mcp.list-tools', 'mcp.list-tools'],
|
|
67
|
+
] as const);
|
|
68
|
+
|
|
69
|
+
const LOOKUP = new Map<SpanType, GenAIOperationName>(TABLE);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the canonical `gen_ai.operation.name` value for a Graphorin
|
|
73
|
+
* span type. Returns `undefined` if no mapping exists (e.g. for
|
|
74
|
+
* `replay.*` markers, which are emitted via a separate code path).
|
|
75
|
+
*
|
|
76
|
+
* @stable
|
|
77
|
+
*/
|
|
78
|
+
export function operationNameFor(type: SpanType): GenAIOperationName | undefined {
|
|
79
|
+
return LOOKUP.get(type);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Full canonical span-to-operation table - exposed for tooling
|
|
84
|
+
* (documentation generators, fixture tests) that need to introspect
|
|
85
|
+
* the mapping.
|
|
86
|
+
*
|
|
87
|
+
* @stable
|
|
88
|
+
*/
|
|
89
|
+
export const OPERATION_NAME_TABLE: ReadonlyArray<readonly [SpanType, GenAIOperationName]> = TABLE;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-derivation lookup table for the `gen_ai.system` attribute.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { GenAISystem } from './types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Canonical mapping from a provider class name (or substring) to the
|
|
11
|
+
* `gen_ai.system` enum value. The table is an export so consumers
|
|
12
|
+
* (e.g. provider adapters in Phase 06) can introspect or extend it.
|
|
13
|
+
*
|
|
14
|
+
* @stable
|
|
15
|
+
*/
|
|
16
|
+
export const PROVIDER_CLASS_TO_GEN_AI_SYSTEM: ReadonlyArray<readonly [RegExp, GenAISystem]> =
|
|
17
|
+
Object.freeze([
|
|
18
|
+
[/anthropic/i, 'anthropic'],
|
|
19
|
+
[/openai(?!compat)/i, 'openai'],
|
|
20
|
+
[/google|gemini/i, 'google'],
|
|
21
|
+
[/mistral/i, 'mistral'],
|
|
22
|
+
[/ollama/i, 'ollama'],
|
|
23
|
+
[/openrouter/i, 'openrouter'],
|
|
24
|
+
[/azure/i, 'azure_ai_inference'],
|
|
25
|
+
[/bedrock|aws/i, 'aws.bedrock'],
|
|
26
|
+
[/cohere/i, 'cohere'],
|
|
27
|
+
[/vertex/i, 'vertex_ai'],
|
|
28
|
+
[/llamacpp|llama-cpp|llama_cpp/i, 'graphorin-llamacpp'],
|
|
29
|
+
[/openaicompatible|openai-compatible/i, 'graphorin-openai-compatible'],
|
|
30
|
+
] as const);
|
|
31
|
+
|
|
32
|
+
const WARNED_CLASSES = new Set<string>();
|
|
33
|
+
let WARN_SINK: (line: string) => void = (line) => console.warn(line);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Override the WARN-once sink used for unrecognised provider class
|
|
37
|
+
* names. Useful in tests so the suite does not pollute stderr.
|
|
38
|
+
*
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
export function setGenAISystemWarnSink(sink: (line: string) => void): void {
|
|
42
|
+
WARN_SINK = sink;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reset the WARN-once cache. Test-only.
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
export function _resetGenAISystemWarningsForTesting(): void {
|
|
51
|
+
WARNED_CLASSES.clear();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Derive the canonical `gen_ai.system` value from a provider class
|
|
56
|
+
* name. Returns `null` when the name does not match any known
|
|
57
|
+
* pattern; callers should declare `Provider.genAiSystem` explicitly
|
|
58
|
+
* in that case.
|
|
59
|
+
*
|
|
60
|
+
* The first time an unknown class name is seen, the function emits
|
|
61
|
+
* one structured WARN line to the configured sink so operators
|
|
62
|
+
* notice the gap. Subsequent lookups for the same class are silent.
|
|
63
|
+
*
|
|
64
|
+
* @stable
|
|
65
|
+
*/
|
|
66
|
+
export function deriveGenAISystem(providerClassName: string): GenAISystem | null {
|
|
67
|
+
for (const [regex, system] of PROVIDER_CLASS_TO_GEN_AI_SYSTEM) {
|
|
68
|
+
if (regex.test(providerClassName)) return system;
|
|
69
|
+
}
|
|
70
|
+
warnOnce(providerClassName);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function warnOnce(providerClassName: string): void {
|
|
75
|
+
if (WARNED_CLASSES.has(providerClassName)) return;
|
|
76
|
+
WARNED_CLASSES.add(providerClassName);
|
|
77
|
+
WARN_SINK(
|
|
78
|
+
`[graphorin/observability] WARN: provider class "${providerClassName}" is not in ` +
|
|
79
|
+
'the auto-derivation table for `gen_ai.system`. Declare `Provider.genAiSystem` ' +
|
|
80
|
+
'explicitly on the adapter so OpenTelemetry GenAI dashboards attribute the spans ' +
|
|
81
|
+
'correctly. See `@graphorin/observability/gen-ai` and the OpenTelemetry GenAI ' +
|
|
82
|
+
'semantic conventions for the canonical vendor enum.',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the OpenTelemetry GenAI semantic-conventions
|
|
3
|
+
* conformance helpers.
|
|
4
|
+
*
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SpanType } from '@graphorin/core';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Canonical OpenTelemetry semantic-conventions vendor enum used as the
|
|
12
|
+
* value of the `gen_ai.system` attribute.
|
|
13
|
+
*
|
|
14
|
+
* @stable
|
|
15
|
+
*/
|
|
16
|
+
export type GenAISystem =
|
|
17
|
+
| 'anthropic'
|
|
18
|
+
| 'openai'
|
|
19
|
+
| 'google'
|
|
20
|
+
| 'mistral'
|
|
21
|
+
| 'ollama'
|
|
22
|
+
| 'openrouter'
|
|
23
|
+
| 'azure_ai_inference'
|
|
24
|
+
| 'aws.bedrock'
|
|
25
|
+
| 'cohere'
|
|
26
|
+
| 'vertex_ai'
|
|
27
|
+
| 'graphorin-llamacpp'
|
|
28
|
+
| 'graphorin-openai-compatible'
|
|
29
|
+
| (string & { readonly __genAiSystem?: never });
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `gen_ai.tool.type` enum value. Defaults to `'function'` for
|
|
33
|
+
* user-defined and MCP-derived tools without explicit declaration.
|
|
34
|
+
*
|
|
35
|
+
* @stable
|
|
36
|
+
*/
|
|
37
|
+
export type GenAIToolType =
|
|
38
|
+
| 'function'
|
|
39
|
+
| 'web_search'
|
|
40
|
+
| 'database'
|
|
41
|
+
| 'code_interpreter'
|
|
42
|
+
| 'image_generation'
|
|
43
|
+
| 'file_search';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Canonical `gen_ai.operation.name` enum used per the OpenTelemetry
|
|
47
|
+
* GenAI semantic conventions for AI agent + framework spans.
|
|
48
|
+
*
|
|
49
|
+
* @stable
|
|
50
|
+
*/
|
|
51
|
+
export type GenAIOperationName =
|
|
52
|
+
// E8: `invoke_agent` is the canonical OTel GenAI agent operation and what
|
|
53
|
+
// the runtime emits; the legacy `agent.run`/`agent.step` members remain for
|
|
54
|
+
// compatibility with older recorded traces.
|
|
55
|
+
| 'invoke_agent'
|
|
56
|
+
| 'agent.run'
|
|
57
|
+
| 'agent.step'
|
|
58
|
+
| 'agent.handoff'
|
|
59
|
+
| 'agent.suspend'
|
|
60
|
+
| 'agent.resume'
|
|
61
|
+
| 'agent.fanout.spawned'
|
|
62
|
+
| 'agent.fanout.merged'
|
|
63
|
+
| 'agent.evaluator.iteration'
|
|
64
|
+
| 'chat'
|
|
65
|
+
| 'embedding'
|
|
66
|
+
| 'execute_tool'
|
|
67
|
+
| 'memory.read'
|
|
68
|
+
| 'memory.write'
|
|
69
|
+
| 'memory.search'
|
|
70
|
+
| 'memory.consolidate'
|
|
71
|
+
| 'memory.conflict'
|
|
72
|
+
| 'workflow.run'
|
|
73
|
+
| 'workflow.step'
|
|
74
|
+
| 'workflow.task'
|
|
75
|
+
| 'workflow.checkpoint'
|
|
76
|
+
| 'mcp.connect'
|
|
77
|
+
| 'mcp.call'
|
|
78
|
+
| 'mcp.list-tools'
|
|
79
|
+
| 'skill.activate'
|
|
80
|
+
| 'skill.load'
|
|
81
|
+
| 'replay.run'
|
|
82
|
+
| 'replay.skipped';
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Per-message event type used by {@link emitGenAIMessageEvents}.
|
|
86
|
+
*
|
|
87
|
+
* @stable
|
|
88
|
+
*/
|
|
89
|
+
export type GenAIMessageRole = 'system' | 'user' | 'assistant' | 'tool';
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Single per-message record passed to {@link emitGenAIMessageEvents}.
|
|
93
|
+
*
|
|
94
|
+
* @stable
|
|
95
|
+
*/
|
|
96
|
+
export interface GenAIMessage {
|
|
97
|
+
readonly role: GenAIMessageRole;
|
|
98
|
+
readonly content: string;
|
|
99
|
+
/** Optional model-specific metadata (tool calls, names, …). */
|
|
100
|
+
readonly toolCalls?: ReadonlyArray<{
|
|
101
|
+
readonly id: string;
|
|
102
|
+
readonly name: string;
|
|
103
|
+
readonly arguments: string;
|
|
104
|
+
}>;
|
|
105
|
+
readonly toolCallId?: string;
|
|
106
|
+
readonly name?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Per-span attribute payload expected by {@link emitGenAIAttributes}.
|
|
111
|
+
* The fields mirror the OpenTelemetry GenAI semantic conventions and
|
|
112
|
+
* are merged with the existing Graphorin-prefixed attributes - the
|
|
113
|
+
* `gen_ai.*` family is additive, never replacing.
|
|
114
|
+
*
|
|
115
|
+
* @stable
|
|
116
|
+
*/
|
|
117
|
+
export interface GenAIAttributes {
|
|
118
|
+
readonly system?: GenAISystem;
|
|
119
|
+
readonly requestModel?: string;
|
|
120
|
+
readonly responseModel?: string;
|
|
121
|
+
readonly responseId?: string;
|
|
122
|
+
readonly inputTokens?: number;
|
|
123
|
+
readonly outputTokens?: number;
|
|
124
|
+
readonly finishReasons?: ReadonlyArray<string>;
|
|
125
|
+
readonly operation?: GenAIOperationName;
|
|
126
|
+
readonly agentId?: string;
|
|
127
|
+
readonly agentName?: string;
|
|
128
|
+
readonly sessionId?: string;
|
|
129
|
+
readonly toolName?: string;
|
|
130
|
+
readonly toolType?: GenAIToolType;
|
|
131
|
+
readonly toolCallId?: string;
|
|
132
|
+
readonly toolDescription?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Mapping from a Graphorin `SpanType` to the canonical
|
|
137
|
+
* `gen_ai.operation.name` value. Returns `undefined` for span types
|
|
138
|
+
* that do not have a canonical operation enum (`replay.*` is recorded
|
|
139
|
+
* as `'replay.run'` / `'replay.skipped'` per the doc table).
|
|
140
|
+
*
|
|
141
|
+
* @stable
|
|
142
|
+
*/
|
|
143
|
+
export type SpanTypeToOperationName = (type: SpanType) => GenAIOperationName | undefined;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @graphorin/observability - observability primitives for the
|
|
3
|
+
* Graphorin framework. Ships:
|
|
4
|
+
*
|
|
5
|
+
* - the typed `AISpan<T>` tracer over OpenTelemetry,
|
|
6
|
+
* - the **mandatory** `withValidation()` wrapper for every exporter,
|
|
7
|
+
* - the sensitivity-aware `RedactionValidator` with 14 built-in PII /
|
|
8
|
+
* secret detection patterns,
|
|
9
|
+
* - OpenTelemetry GenAI semantic-conventions conformance helpers,
|
|
10
|
+
* - OpenInference span-kind emission,
|
|
11
|
+
* - the structured `Logger` with span correlation,
|
|
12
|
+
* - the append-only `JSONLExporter` for replay,
|
|
13
|
+
* - sanitized-by-default `Replay` primitives,
|
|
14
|
+
* - the hierarchical `CostTracker`,
|
|
15
|
+
* - and a minimal inline eval runner.
|
|
16
|
+
*
|
|
17
|
+
* The full documentation lives in the package `README.md`.
|
|
18
|
+
*
|
|
19
|
+
* @packageDocumentation
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Canonical version constant, derived from `package.json` at build time. */
|
|
23
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
24
|
+
|
|
25
|
+
export const VERSION: string = pkg.version;
|
|
26
|
+
|
|
27
|
+
export * from './cost/index.js';
|
|
28
|
+
export * from './eval/index.js';
|
|
29
|
+
export * from './exporters/index.js';
|
|
30
|
+
export * from './gen-ai/index.js';
|
|
31
|
+
export * from './logger/index.js';
|
|
32
|
+
export * from './openinference/index.js';
|
|
33
|
+
export * from './redaction/index.js';
|
|
34
|
+
export * from './replay/index.js';
|
|
35
|
+
export * from './telemetry/index.js';
|
|
36
|
+
export * from './tracer/index.js';
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createLogger(...)` - structured logger with optional span correlation.
|
|
3
|
+
*
|
|
4
|
+
* The logger is a thin wrapper around `console.{log,info,warn,error}`
|
|
5
|
+
* that adds:
|
|
6
|
+
*
|
|
7
|
+
* - structured fields,
|
|
8
|
+
* - JSON or pretty rendering,
|
|
9
|
+
* - automatic correlation with the current Graphorin span (the
|
|
10
|
+
* {@link CurrentSpanContext} helper carries the live trace + span ids),
|
|
11
|
+
* - sensitivity-aware redaction via the supplied
|
|
12
|
+
* {@link RedactionValidatorInstance}.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { AISpan, LogFields, Logger, LogLevel, Sensitivity } from '@graphorin/core';
|
|
18
|
+
|
|
19
|
+
import { createAsyncContext } from '@graphorin/core';
|
|
20
|
+
|
|
21
|
+
import type { RedactionValidatorInstance } from '../redaction/types.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @stable
|
|
25
|
+
*/
|
|
26
|
+
export type LoggerFormat = 'json' | 'pretty';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Configuration shape for {@link createLogger}.
|
|
30
|
+
*
|
|
31
|
+
* @stable
|
|
32
|
+
*/
|
|
33
|
+
export interface LoggerOptions {
|
|
34
|
+
readonly level?: LogLevel;
|
|
35
|
+
readonly format?: LoggerFormat;
|
|
36
|
+
/** Optional validator. Logger fields flow through `validate(...)`. */
|
|
37
|
+
readonly redaction?: RedactionValidatorInstance;
|
|
38
|
+
/** Default sensitivity tier for fields. Defaults to `'internal'`. */
|
|
39
|
+
readonly defaultTier?: Sensitivity;
|
|
40
|
+
/**
|
|
41
|
+
* Sink that receives the rendered line. Defaults to writing to the
|
|
42
|
+
* appropriate `console.*` method.
|
|
43
|
+
*/
|
|
44
|
+
readonly sink?: (level: LogLevel, line: string) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const LEVELS: ReadonlyArray<LogLevel> = ['trace', 'debug', 'info', 'warn', 'error'] as const;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @internal - context used to thread the current span id through nested
|
|
51
|
+
* async calls. Exposed via {@link withCurrentSpan} below.
|
|
52
|
+
*/
|
|
53
|
+
const SPAN_CONTEXT = createAsyncContext<{ traceId: string; spanId: string }>(
|
|
54
|
+
'graphorin.observability.current-span',
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Run `fn` with the supplied span as the "current" log-correlation
|
|
59
|
+
* span. The logger picks up the trace + span ids automatically.
|
|
60
|
+
*
|
|
61
|
+
* @stable
|
|
62
|
+
*/
|
|
63
|
+
export function withCurrentSpan<R>(
|
|
64
|
+
span: AISpan | undefined,
|
|
65
|
+
fn: () => R | Promise<R>,
|
|
66
|
+
): R | Promise<R> {
|
|
67
|
+
if (span === undefined) return fn();
|
|
68
|
+
return SPAN_CONTEXT.runAsync({ traceId: span.traceId, spanId: span.id }, async () => fn());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Read the current span context (if any). Useful for callers that
|
|
73
|
+
* want to attach span metadata to bespoke records.
|
|
74
|
+
*
|
|
75
|
+
* @stable
|
|
76
|
+
*/
|
|
77
|
+
export function getCurrentSpanContext():
|
|
78
|
+
| { readonly traceId: string; readonly spanId: string }
|
|
79
|
+
| undefined {
|
|
80
|
+
return SPAN_CONTEXT.get();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build a {@link Logger} configured against the supplied options.
|
|
85
|
+
*
|
|
86
|
+
* @stable
|
|
87
|
+
*/
|
|
88
|
+
export function createLogger(opts: LoggerOptions = {}): Logger {
|
|
89
|
+
const minLevel = LEVELS.indexOf(opts.level ?? 'info');
|
|
90
|
+
const format = opts.format ?? 'json';
|
|
91
|
+
const redaction = opts.redaction;
|
|
92
|
+
const defaultTier: Sensitivity = opts.defaultTier ?? 'internal';
|
|
93
|
+
const sink = opts.sink ?? defaultSink;
|
|
94
|
+
|
|
95
|
+
function emit(level: LogLevel, message: string, fields?: LogFields): void {
|
|
96
|
+
const idx = LEVELS.indexOf(level);
|
|
97
|
+
if (idx < minLevel) return;
|
|
98
|
+
|
|
99
|
+
const span = SPAN_CONTEXT.get();
|
|
100
|
+
const safeFields = sanitizeFields(fields, redaction, defaultTier);
|
|
101
|
+
|
|
102
|
+
const payload: Record<string, unknown> = {
|
|
103
|
+
level,
|
|
104
|
+
time: new Date().toISOString(),
|
|
105
|
+
message,
|
|
106
|
+
...safeFields,
|
|
107
|
+
};
|
|
108
|
+
if (span !== undefined) {
|
|
109
|
+
payload.traceId = span.traceId;
|
|
110
|
+
payload.spanId = span.spanId;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const line = format === 'json' ? JSON.stringify(payload) : prettyRender(payload);
|
|
114
|
+
sink(level, line);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
trace: (message, fields) => emit('trace', message, fields),
|
|
119
|
+
debug: (message, fields) => emit('debug', message, fields),
|
|
120
|
+
info: (message, fields) => emit('info', message, fields),
|
|
121
|
+
warn: (message, fields) => emit('warn', message, fields),
|
|
122
|
+
error: (message, fields) => emit('error', message, fields),
|
|
123
|
+
child(_bindings: LogFields): Logger {
|
|
124
|
+
const inherited = sanitizeFields(_bindings, redaction, defaultTier);
|
|
125
|
+
const parent = createLogger(opts);
|
|
126
|
+
return {
|
|
127
|
+
trace: (m, f) => parent.trace(m, { ...inherited, ...f }),
|
|
128
|
+
debug: (m, f) => parent.debug(m, { ...inherited, ...f }),
|
|
129
|
+
info: (m, f) => parent.info(m, { ...inherited, ...f }),
|
|
130
|
+
warn: (m, f) => parent.warn(m, { ...inherited, ...f }),
|
|
131
|
+
error: (m, f) => parent.error(m, { ...inherited, ...f }),
|
|
132
|
+
child(b: LogFields): Logger {
|
|
133
|
+
return parent.child({ ...inherited, ...b });
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function sanitizeFields(
|
|
141
|
+
fields: LogFields | undefined,
|
|
142
|
+
validator: RedactionValidatorInstance | undefined,
|
|
143
|
+
tier: Sensitivity,
|
|
144
|
+
): LogFields {
|
|
145
|
+
if (fields === undefined) return {};
|
|
146
|
+
if (validator === undefined) return { ...fields };
|
|
147
|
+
const out: Record<string, unknown> = {};
|
|
148
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
149
|
+
const result = validator.validate({ value: v, tier, context: { attribute: k } });
|
|
150
|
+
if (result === null) continue;
|
|
151
|
+
out[k] = result.value;
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function defaultSink(level: LogLevel, line: string): void {
|
|
157
|
+
switch (level) {
|
|
158
|
+
case 'trace':
|
|
159
|
+
case 'debug':
|
|
160
|
+
console.debug(line);
|
|
161
|
+
return;
|
|
162
|
+
case 'info':
|
|
163
|
+
console.info(line);
|
|
164
|
+
return;
|
|
165
|
+
case 'warn':
|
|
166
|
+
console.warn(line);
|
|
167
|
+
return;
|
|
168
|
+
case 'error':
|
|
169
|
+
console.error(line);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function prettyRender(payload: Record<string, unknown>): string {
|
|
175
|
+
const { level, time, message, ...rest } = payload;
|
|
176
|
+
const fields = Object.keys(rest).length === 0 ? '' : ` ${JSON.stringify(rest)}`;
|
|
177
|
+
return `[${String(time)}] ${String(level).toUpperCase()} ${String(message)}${fields}`;
|
|
178
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenInference span-kind emission. Adds the
|
|
3
|
+
* `openinference.span.kind` attribute (one of `AGENT`, `EVALUATOR`,
|
|
4
|
+
* `LLM`, `TOOL`, `RETRIEVER`, `EMBEDDING`, `CHAIN`) to applicable
|
|
5
|
+
* Graphorin spans.
|
|
6
|
+
*
|
|
7
|
+
* The mapping is the canonical table published in the architecture
|
|
8
|
+
* documentation. Span types without a clean OpenInference equivalent
|
|
9
|
+
* (`skill.*`, `mcp.connect`, `mcp.list-tools`, `replay.*`) are NOT
|
|
10
|
+
* emitted - the caller can introspect via {@link openInferenceKindFor}
|
|
11
|
+
* and decide whether to log a fallback attribute.
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { AISpan, SpanType } from '@graphorin/core';
|
|
17
|
+
|
|
18
|
+
import { asGraphorinSpan } from '../tracer/tracer.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Canonical OpenInference span-kind enum.
|
|
22
|
+
*
|
|
23
|
+
* @stable
|
|
24
|
+
*/
|
|
25
|
+
export type OpenInferenceSpanKind =
|
|
26
|
+
| 'AGENT'
|
|
27
|
+
| 'EVALUATOR'
|
|
28
|
+
| 'LLM'
|
|
29
|
+
| 'TOOL'
|
|
30
|
+
| 'RETRIEVER'
|
|
31
|
+
| 'EMBEDDING'
|
|
32
|
+
| 'CHAIN'
|
|
33
|
+
| 'RERANKER';
|
|
34
|
+
|
|
35
|
+
const KIND_TABLE: ReadonlyArray<readonly [SpanType, OpenInferenceSpanKind]> = Object.freeze([
|
|
36
|
+
['agent.run', 'AGENT'],
|
|
37
|
+
['agent.step', 'AGENT'],
|
|
38
|
+
['agent.handoff', 'AGENT'],
|
|
39
|
+
['agent.suspend', 'AGENT'],
|
|
40
|
+
['agent.resume', 'AGENT'],
|
|
41
|
+
|
|
42
|
+
['provider.generate', 'LLM'],
|
|
43
|
+
['provider.stream', 'LLM'],
|
|
44
|
+
|
|
45
|
+
['tool.execute', 'TOOL'],
|
|
46
|
+
['tool.approval', 'TOOL'],
|
|
47
|
+
|
|
48
|
+
['memory.read.working', 'RETRIEVER'],
|
|
49
|
+
['memory.read.session', 'RETRIEVER'],
|
|
50
|
+
['memory.read.episodic', 'RETRIEVER'],
|
|
51
|
+
['memory.read.semantic', 'RETRIEVER'],
|
|
52
|
+
['memory.read.procedural', 'RETRIEVER'],
|
|
53
|
+
['memory.read.shared', 'RETRIEVER'],
|
|
54
|
+
['memory.write.working', 'RETRIEVER'],
|
|
55
|
+
['memory.write.session', 'RETRIEVER'],
|
|
56
|
+
['memory.write.episodic', 'RETRIEVER'],
|
|
57
|
+
['memory.write.semantic', 'RETRIEVER'],
|
|
58
|
+
['memory.write.procedural', 'RETRIEVER'],
|
|
59
|
+
['memory.write.shared', 'RETRIEVER'],
|
|
60
|
+
['memory.search.working', 'RETRIEVER'],
|
|
61
|
+
['memory.search.session', 'RETRIEVER'],
|
|
62
|
+
['memory.search.episodic', 'RETRIEVER'],
|
|
63
|
+
['memory.search.semantic', 'RETRIEVER'],
|
|
64
|
+
['memory.search.procedural', 'RETRIEVER'],
|
|
65
|
+
['memory.search.shared', 'RETRIEVER'],
|
|
66
|
+
|
|
67
|
+
['memory.embed', 'EMBEDDING'],
|
|
68
|
+
|
|
69
|
+
['memory.consolidate.light', 'CHAIN'],
|
|
70
|
+
['memory.consolidate.standard', 'CHAIN'],
|
|
71
|
+
['memory.consolidate.deep', 'CHAIN'],
|
|
72
|
+
['memory.conflict', 'CHAIN'],
|
|
73
|
+
['workflow.run', 'CHAIN'],
|
|
74
|
+
['workflow.step', 'CHAIN'],
|
|
75
|
+
['workflow.task', 'CHAIN'],
|
|
76
|
+
['workflow.checkpoint', 'CHAIN'],
|
|
77
|
+
|
|
78
|
+
['mcp.call', 'TOOL'],
|
|
79
|
+
] as const);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Span types intentionally excluded from OpenInference span-kind
|
|
83
|
+
* emission per the canonical table - `skill.*`, `mcp.connect`,
|
|
84
|
+
* `mcp.list-tools`, and `replay.*` markers do not have a clean
|
|
85
|
+
* OpenInference equivalent.
|
|
86
|
+
*
|
|
87
|
+
* @stable
|
|
88
|
+
*/
|
|
89
|
+
export const OPEN_INFERENCE_EXCLUDED_TYPES: ReadonlyArray<SpanType> = Object.freeze([
|
|
90
|
+
'skill.activate',
|
|
91
|
+
'skill.load',
|
|
92
|
+
'mcp.connect',
|
|
93
|
+
'mcp.list-tools',
|
|
94
|
+
] as const);
|
|
95
|
+
|
|
96
|
+
const KIND_LOOKUP = new Map<SpanType, OpenInferenceSpanKind>(KIND_TABLE);
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve the OpenInference span kind for a Graphorin span type.
|
|
100
|
+
* Returns `null` for types intentionally excluded from emission.
|
|
101
|
+
*
|
|
102
|
+
* @stable
|
|
103
|
+
*/
|
|
104
|
+
export function openInferenceKindFor<T extends SpanType>(type: T): OpenInferenceSpanKind | null {
|
|
105
|
+
return KIND_LOOKUP.get(type) ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Attach the `openinference.span.kind` attribute to a span. No-op for
|
|
110
|
+
* span types that lack a clean OpenInference equivalent. The attribute
|
|
111
|
+
* is tagged `'public'` because the enum value is bounded and contains
|
|
112
|
+
* no PII.
|
|
113
|
+
*
|
|
114
|
+
* @stable
|
|
115
|
+
*/
|
|
116
|
+
export function emitOpenInferenceKind<T extends SpanType>(span: AISpan<T>): void {
|
|
117
|
+
const kind = KIND_LOOKUP.get(span.type);
|
|
118
|
+
if (kind === undefined) return;
|
|
119
|
+
const gs = asGraphorinSpan(span);
|
|
120
|
+
if (gs !== null) {
|
|
121
|
+
gs.setAttribute('openinference.span.kind', kind, { sensitivity: 'public' });
|
|
122
|
+
} else {
|
|
123
|
+
span.setAttributes({ 'openinference.span.kind': kind });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Full canonical span-to-kind table - exposed for tooling and tests
|
|
129
|
+
* that need to introspect the mapping.
|
|
130
|
+
*
|
|
131
|
+
* @stable
|
|
132
|
+
*/
|
|
133
|
+
export const OPEN_INFERENCE_KIND_TABLE = KIND_TABLE;
|