@grc-claw/observability 2.0.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.
@@ -0,0 +1,204 @@
1
+ export type SpanKind = 'INTERNAL' | 'CLIENT' | 'SERVER' | 'PRODUCER' | 'CONSUMER';
2
+ export type SpanStatus = 'OK' | 'ERROR' | 'UNSET';
3
+ export interface SpanAttributes {
4
+ 'agent.did'?: string;
5
+ 'agent.session_id'?: string;
6
+ 'agent.tenant_id'?: string;
7
+ 'tool.name'?: string;
8
+ 'tool.tier'?: string;
9
+ 'tool.namespace'?: string;
10
+ 'policy.result'?: string;
11
+ 'policy.reason'?: string;
12
+ 'compliance.framework'?: string;
13
+ 'compliance.control_id'?: string;
14
+ 'compliance.score'?: number;
15
+ 'llm.provider'?: string;
16
+ 'llm.model'?: string;
17
+ 'llm.tokens_in'?: number;
18
+ 'llm.tokens_out'?: number;
19
+ 'llm.cost_usd'?: number;
20
+ 'llm.latency_ms'?: number;
21
+ 'evidence.hash'?: string;
22
+ 'evidence.type'?: string;
23
+ 'soar.playbook_id'?: string;
24
+ 'soar.execution_id'?: string;
25
+ 'soar.step_id'?: string;
26
+ 'risk.score'?: number;
27
+ 'risk.factors'?: string;
28
+ 'error.type'?: string;
29
+ 'error.message'?: string;
30
+ [key: string]: string | number | boolean | undefined;
31
+ }
32
+ export interface SpanEvent {
33
+ name: string;
34
+ timestamp: string;
35
+ attributes: Record<string, string | number | boolean>;
36
+ }
37
+ export interface Span {
38
+ traceId: string;
39
+ spanId: string;
40
+ parentSpanId?: string;
41
+ name: string;
42
+ kind: SpanKind;
43
+ status: SpanStatus;
44
+ startTime: string;
45
+ endTime?: string;
46
+ durationMs?: number;
47
+ attributes: SpanAttributes;
48
+ events: SpanEvent[];
49
+ resource: {
50
+ 'service.name': string;
51
+ 'service.version': string;
52
+ 'deployment.environment': string;
53
+ };
54
+ }
55
+ export interface Metric {
56
+ name: string;
57
+ type: 'counter' | 'gauge' | 'histogram';
58
+ value: number;
59
+ unit: string;
60
+ labels: Record<string, string>;
61
+ timestamp: string;
62
+ }
63
+ export interface TraceContext {
64
+ traceId: string;
65
+ spanId: string;
66
+ traceFlags: number;
67
+ }
68
+ export declare class AgentTracer {
69
+ private spans;
70
+ private metrics;
71
+ private activeTraces;
72
+ private serviceName;
73
+ private serviceVersion;
74
+ private environment;
75
+ constructor(opts?: {
76
+ serviceName?: string;
77
+ version?: string;
78
+ environment?: string;
79
+ });
80
+ /** Start a new trace (root span) */
81
+ startTrace(name: string, attributes?: SpanAttributes): Span;
82
+ /** Start a new span within a trace */
83
+ startSpan(name: string, opts?: {
84
+ traceId?: string;
85
+ parentSpanId?: string;
86
+ kind?: SpanKind;
87
+ attributes?: SpanAttributes;
88
+ }): Span;
89
+ /** End a span */
90
+ endSpan(spanId: string, status?: SpanStatus, error?: string): void;
91
+ /** Add an event to a span */
92
+ addSpanEvent(spanId: string, name: string, attributes?: Record<string, string | number | boolean>): void;
93
+ /** Set span attributes */
94
+ setAttributes(spanId: string, attributes: Partial<SpanAttributes>): void;
95
+ /** Trace a tool invocation */
96
+ traceToolInvocation(opts: {
97
+ traceId: string;
98
+ parentSpanId?: string;
99
+ agentDid: string;
100
+ sessionId: string;
101
+ toolName: string;
102
+ toolTier: string;
103
+ policyResult: string;
104
+ policyReason?: string;
105
+ }): Span;
106
+ /** Trace an LLM call */
107
+ traceLLMCall(opts: {
108
+ traceId: string;
109
+ parentSpanId?: string;
110
+ agentDid: string;
111
+ provider: string;
112
+ model: string;
113
+ tokensIn: number;
114
+ tokensOut: number;
115
+ costUsd: number;
116
+ latencyMs: number;
117
+ }): Span;
118
+ /** Trace a compliance check */
119
+ traceComplianceCheck(opts: {
120
+ traceId: string;
121
+ parentSpanId?: string;
122
+ agentDid: string;
123
+ framework: string;
124
+ controlId: string;
125
+ score: number;
126
+ result: 'pass' | 'fail' | 'partial';
127
+ }): Span;
128
+ /** Trace a SOAR playbook execution */
129
+ traceSOARExecution(opts: {
130
+ traceId: string;
131
+ parentSpanId?: string;
132
+ playbookId: string;
133
+ executionId: string;
134
+ trigger: string;
135
+ severity: string;
136
+ }): Span;
137
+ /** Record a metric */
138
+ recordMetric(name: string, type: Metric['type'], value: number, unit: string, labels: Record<string, string>): void;
139
+ /** Get all metrics (Prometheus format) */
140
+ getPrometheusMetrics(): string;
141
+ /** Get all spans for a trace */
142
+ getTrace(traceId: string): Span[];
143
+ /** Export spans as OTLP JSON (for Datadog, Grafana, Jaeger, etc.) */
144
+ exportOTLP(): {
145
+ resourceSpans: {
146
+ resource: Span['resource'];
147
+ scopeSpans: {
148
+ scope: {
149
+ name: string;
150
+ version: string;
151
+ };
152
+ spans: Span[];
153
+ }[];
154
+ }[];
155
+ };
156
+ /** Get observability statistics */
157
+ getStats(): {
158
+ totalSpans: number;
159
+ totalTraces: number;
160
+ totalMetrics: number;
161
+ errorRate: number;
162
+ avgSpanDurationMs: number;
163
+ };
164
+ }
165
+ export interface AIBOMEntry {
166
+ component: string;
167
+ type: 'model' | 'tool' | 'data_source' | 'policy' | 'framework';
168
+ version?: string;
169
+ provider?: string;
170
+ license?: string;
171
+ riskLevel: 'low' | 'medium' | 'high' | 'critical';
172
+ properties: Record<string, unknown>;
173
+ }
174
+ export interface AIBOM {
175
+ specVersion: '1.0';
176
+ serialNumber: string;
177
+ version: number;
178
+ metadata: {
179
+ timestamp: string;
180
+ component: {
181
+ type: 'application';
182
+ name: string;
183
+ version: string;
184
+ };
185
+ authors: {
186
+ name: string;
187
+ }[];
188
+ };
189
+ components: AIBOMEntry[];
190
+ dependencies: {
191
+ ref: string;
192
+ dependsOn: string[];
193
+ }[];
194
+ vulnerabilities: {
195
+ id: string;
196
+ source: string;
197
+ description: string;
198
+ severity: string;
199
+ }[];
200
+ }
201
+ export declare class AIBOMGenerator {
202
+ /** Generate an AI Bill of Materials from trace data */
203
+ generateFromTraces(traces: Span[], agentName: string): AIBOM;
204
+ }
package/dist/index.js ADDED
@@ -0,0 +1,326 @@
1
+ /**
2
+ * @grc-claw/observability
3
+ * OpenTelemetry-native Agent Tracing and Compliance Observability
4
+ *
5
+ * Every agent step emits structured spans: tool invocations, LLM calls,
6
+ * policy checks, evidence generation. Supports distributed tracing across
7
+ * multi-agent swarms with custom compliance metrics.
8
+ *
9
+ * Export to any OTLP-compatible backend (Datadog, Grafana, Jaeger).
10
+ */
11
+ import * as crypto from 'crypto';
12
+ // ─── Agent Tracer ────────────────────────────────────────────────────
13
+ export class AgentTracer {
14
+ spans = new Map();
15
+ metrics = [];
16
+ activeTraces = new Map(); // traceId -> spanIds
17
+ serviceName;
18
+ serviceVersion;
19
+ environment;
20
+ constructor(opts) {
21
+ this.serviceName = opts?.serviceName ?? '@grc-claw/agent-runtime';
22
+ this.serviceVersion = opts?.version ?? '0.1.0';
23
+ this.environment = opts?.environment ?? 'development';
24
+ }
25
+ /** Start a new trace (root span) */
26
+ startTrace(name, attributes) {
27
+ const traceId = crypto.randomUUID().replace(/-/g, '');
28
+ return this.startSpan(name, { traceId, attributes });
29
+ }
30
+ /** Start a new span within a trace */
31
+ startSpan(name, opts) {
32
+ const traceId = opts?.traceId ?? crypto.randomUUID().replace(/-/g, '');
33
+ const spanId = crypto.randomUUID().replace(/-/g, '').substring(0, 16);
34
+ const span = {
35
+ traceId,
36
+ spanId,
37
+ parentSpanId: opts?.parentSpanId,
38
+ name,
39
+ kind: opts?.kind ?? 'INTERNAL',
40
+ status: 'UNSET',
41
+ startTime: new Date().toISOString(),
42
+ attributes: opts?.attributes ?? {},
43
+ events: [],
44
+ resource: {
45
+ 'service.name': this.serviceName,
46
+ 'service.version': this.serviceVersion,
47
+ 'deployment.environment': this.environment,
48
+ },
49
+ };
50
+ this.spans.set(spanId, span);
51
+ // Track in active traces
52
+ const traceSpans = this.activeTraces.get(traceId) ?? [];
53
+ traceSpans.push(spanId);
54
+ this.activeTraces.set(traceId, traceSpans);
55
+ return span;
56
+ }
57
+ /** End a span */
58
+ endSpan(spanId, status, error) {
59
+ const span = this.spans.get(spanId);
60
+ if (!span)
61
+ return;
62
+ span.endTime = new Date().toISOString();
63
+ span.status = status ?? 'OK';
64
+ span.durationMs = new Date(span.endTime).getTime() - new Date(span.startTime).getTime();
65
+ if (error) {
66
+ span.status = 'ERROR';
67
+ span.attributes['error.message'] = error;
68
+ span.events.push({
69
+ name: 'exception',
70
+ timestamp: new Date().toISOString(),
71
+ attributes: { 'exception.message': error },
72
+ });
73
+ }
74
+ }
75
+ /** Add an event to a span */
76
+ addSpanEvent(spanId, name, attributes) {
77
+ const span = this.spans.get(spanId);
78
+ if (!span)
79
+ return;
80
+ span.events.push({
81
+ name,
82
+ timestamp: new Date().toISOString(),
83
+ attributes: attributes ?? {},
84
+ });
85
+ }
86
+ /** Set span attributes */
87
+ setAttributes(spanId, attributes) {
88
+ const span = this.spans.get(spanId);
89
+ if (!span)
90
+ return;
91
+ Object.assign(span.attributes, attributes);
92
+ }
93
+ // ─── Convenience Methods for Agent Operations ──
94
+ /** Trace a tool invocation */
95
+ traceToolInvocation(opts) {
96
+ const span = this.startSpan(`agent.tool.${opts.toolName}`, {
97
+ traceId: opts.traceId,
98
+ parentSpanId: opts.parentSpanId,
99
+ kind: 'CLIENT',
100
+ attributes: {
101
+ 'agent.did': opts.agentDid,
102
+ 'agent.session_id': opts.sessionId,
103
+ 'tool.name': opts.toolName,
104
+ 'tool.tier': opts.toolTier,
105
+ 'policy.result': opts.policyResult,
106
+ 'policy.reason': opts.policyReason,
107
+ },
108
+ });
109
+ this.recordMetric('agent.tool.invocations', 'counter', 1, 'invocations', {
110
+ tool: opts.toolName,
111
+ tier: opts.toolTier,
112
+ result: opts.policyResult,
113
+ });
114
+ return span;
115
+ }
116
+ /** Trace an LLM call */
117
+ traceLLMCall(opts) {
118
+ const span = this.startSpan(`agent.llm.${opts.provider}`, {
119
+ traceId: opts.traceId,
120
+ parentSpanId: opts.parentSpanId,
121
+ kind: 'CLIENT',
122
+ attributes: {
123
+ 'agent.did': opts.agentDid,
124
+ 'llm.provider': opts.provider,
125
+ 'llm.model': opts.model,
126
+ 'llm.tokens_in': opts.tokensIn,
127
+ 'llm.tokens_out': opts.tokensOut,
128
+ 'llm.cost_usd': opts.costUsd,
129
+ 'llm.latency_ms': opts.latencyMs,
130
+ },
131
+ });
132
+ this.recordMetric('agent.llm.tokens', 'counter', opts.tokensIn + opts.tokensOut, 'tokens', {
133
+ provider: opts.provider,
134
+ model: opts.model,
135
+ });
136
+ this.recordMetric('agent.llm.cost', 'counter', opts.costUsd, 'usd', {
137
+ provider: opts.provider,
138
+ model: opts.model,
139
+ });
140
+ this.recordMetric('agent.llm.latency', 'histogram', opts.latencyMs, 'ms', {
141
+ provider: opts.provider,
142
+ model: opts.model,
143
+ });
144
+ return span;
145
+ }
146
+ /** Trace a compliance check */
147
+ traceComplianceCheck(opts) {
148
+ const span = this.startSpan(`agent.compliance.${opts.framework}`, {
149
+ traceId: opts.traceId,
150
+ parentSpanId: opts.parentSpanId,
151
+ kind: 'INTERNAL',
152
+ attributes: {
153
+ 'agent.did': opts.agentDid,
154
+ 'compliance.framework': opts.framework,
155
+ 'compliance.control_id': opts.controlId,
156
+ 'compliance.score': opts.score,
157
+ 'policy.result': opts.result,
158
+ },
159
+ });
160
+ this.recordMetric('agent.compliance.checks', 'counter', 1, 'checks', {
161
+ framework: opts.framework,
162
+ result: opts.result,
163
+ });
164
+ this.recordMetric('agent.compliance.score', 'gauge', opts.score, 'score', {
165
+ framework: opts.framework,
166
+ control: opts.controlId,
167
+ });
168
+ return span;
169
+ }
170
+ /** Trace a SOAR playbook execution */
171
+ traceSOARExecution(opts) {
172
+ return this.startSpan(`soar.playbook.${opts.playbookId}`, {
173
+ traceId: opts.traceId,
174
+ parentSpanId: opts.parentSpanId,
175
+ kind: 'INTERNAL',
176
+ attributes: {
177
+ 'soar.playbook_id': opts.playbookId,
178
+ 'soar.execution_id': opts.executionId,
179
+ 'policy.result': opts.trigger,
180
+ 'risk.factors': opts.severity,
181
+ },
182
+ });
183
+ }
184
+ // ─── Metrics ──
185
+ /** Record a metric */
186
+ recordMetric(name, type, value, unit, labels) {
187
+ this.metrics.push({
188
+ name,
189
+ type,
190
+ value,
191
+ unit,
192
+ labels,
193
+ timestamp: new Date().toISOString(),
194
+ });
195
+ }
196
+ /** Get all metrics (Prometheus format) */
197
+ getPrometheusMetrics() {
198
+ const grouped = new Map();
199
+ for (const m of this.metrics) {
200
+ const key = m.name;
201
+ const group = grouped.get(key) ?? [];
202
+ group.push(m);
203
+ grouped.set(key, group);
204
+ }
205
+ const lines = [];
206
+ for (const [name, metrics] of grouped) {
207
+ const type = metrics[0]?.type ?? 'counter';
208
+ lines.push(`# HELP ${name} Agent observability metric`);
209
+ lines.push(`# TYPE ${name} ${type}`);
210
+ for (const m of metrics) {
211
+ const labels = Object.entries(m.labels).map(([k, v]) => `${k}="${v}"`).join(',');
212
+ lines.push(`${name}{${labels}} ${m.value}`);
213
+ }
214
+ }
215
+ return lines.join('\n');
216
+ }
217
+ // ─── Export ──
218
+ /** Get all spans for a trace */
219
+ getTrace(traceId) {
220
+ const spanIds = this.activeTraces.get(traceId) ?? [];
221
+ return spanIds.map((id) => this.spans.get(id)).filter(Boolean);
222
+ }
223
+ /** Export spans as OTLP JSON (for Datadog, Grafana, Jaeger, etc.) */
224
+ exportOTLP() {
225
+ const allSpans = Array.from(this.spans.values());
226
+ return {
227
+ resourceSpans: [
228
+ {
229
+ resource: {
230
+ 'service.name': this.serviceName,
231
+ 'service.version': this.serviceVersion,
232
+ 'deployment.environment': this.environment,
233
+ },
234
+ scopeSpans: [
235
+ {
236
+ scope: { name: this.serviceName, version: this.serviceVersion },
237
+ spans: allSpans,
238
+ },
239
+ ],
240
+ },
241
+ ],
242
+ };
243
+ }
244
+ /** Get observability statistics */
245
+ getStats() {
246
+ const allSpans = Array.from(this.spans.values());
247
+ const completedSpans = allSpans.filter((s) => s.durationMs !== undefined);
248
+ const errorSpans = allSpans.filter((s) => s.status === 'ERROR');
249
+ const avgDuration = completedSpans.length > 0
250
+ ? completedSpans.reduce((sum, s) => sum + (s.durationMs ?? 0), 0) / completedSpans.length
251
+ : 0;
252
+ return {
253
+ totalSpans: allSpans.length,
254
+ totalTraces: this.activeTraces.size,
255
+ totalMetrics: this.metrics.length,
256
+ errorRate: allSpans.length > 0 ? errorSpans.length / allSpans.length : 0,
257
+ avgSpanDurationMs: Math.round(avgDuration * 100) / 100,
258
+ };
259
+ }
260
+ }
261
+ export class AIBOMGenerator {
262
+ /** Generate an AI Bill of Materials from trace data */
263
+ generateFromTraces(traces, agentName) {
264
+ const components = [];
265
+ const seenComponents = new Set();
266
+ for (const span of traces) {
267
+ // Extract models
268
+ if (span.attributes['llm.provider'] && span.attributes['llm.model']) {
269
+ const key = `model:${span.attributes['llm.provider']}:${span.attributes['llm.model']}`;
270
+ if (!seenComponents.has(key)) {
271
+ seenComponents.add(key);
272
+ components.push({
273
+ component: String(span.attributes['llm.model']),
274
+ type: 'model',
275
+ provider: String(span.attributes['llm.provider']),
276
+ riskLevel: 'medium',
277
+ properties: {
278
+ totalTokens: span.attributes['llm.tokens_in'] ?? 0 + (span.attributes['llm.tokens_out'] ?? 0),
279
+ costUsd: span.attributes['llm.cost_usd'],
280
+ },
281
+ });
282
+ }
283
+ }
284
+ // Extract tools
285
+ if (span.attributes['tool.name']) {
286
+ const key = `tool:${span.attributes['tool.name']}`;
287
+ if (!seenComponents.has(key)) {
288
+ seenComponents.add(key);
289
+ const tier = String(span.attributes['tool.tier'] ?? 'read');
290
+ components.push({
291
+ component: String(span.attributes['tool.name']),
292
+ type: 'tool',
293
+ riskLevel: tier === 'destructive' ? 'high' : tier === 'write' ? 'medium' : 'low',
294
+ properties: { tier },
295
+ });
296
+ }
297
+ }
298
+ // Extract frameworks
299
+ if (span.attributes['compliance.framework']) {
300
+ const key = `framework:${span.attributes['compliance.framework']}`;
301
+ if (!seenComponents.has(key)) {
302
+ seenComponents.add(key);
303
+ components.push({
304
+ component: String(span.attributes['compliance.framework']),
305
+ type: 'framework',
306
+ riskLevel: 'low',
307
+ properties: { controlId: span.attributes['compliance.control_id'] },
308
+ });
309
+ }
310
+ }
311
+ }
312
+ return {
313
+ specVersion: '1.0',
314
+ serialNumber: `urn:uuid:${crypto.randomUUID()}`,
315
+ version: 1,
316
+ metadata: {
317
+ timestamp: new Date().toISOString(),
318
+ component: { type: 'application', name: agentName, version: '0.1.0' },
319
+ authors: [{ name: 'GRC_Claw AI-BOM Generator' }],
320
+ },
321
+ components,
322
+ dependencies: [],
323
+ vulnerabilities: [],
324
+ };
325
+ }
326
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@grc-claw/observability",
3
+ "version": "2.0.0",
4
+ "description": "OpenTelemetry-native agent tracing and compliance observability",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "test": "node --test dist/**/*.test.js 2>/dev/null || true"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ }
25
+ }