@elsium-ai/observe 0.2.0 → 0.2.2
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/README.md +1149 -16
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @elsium-ai/observe
|
|
2
2
|
|
|
3
|
-
Observability, tracing, cost tracking, and
|
|
3
|
+
Observability, tracing, cost intelligence, metrics, audit trails, provenance tracking, and OpenTelemetry compatibility for [ElsiumAI](https://github.com/elsium-ai/elsium-ai).
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@elsium-ai/observe)
|
|
6
6
|
[](https://github.com/elsium-ai/elsium-ai/blob/main/LICENSE)
|
|
@@ -13,31 +13,1164 @@ npm install @elsium-ai/observe @elsium-ai/core
|
|
|
13
13
|
|
|
14
14
|
## What's Inside
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
| Category | Exports | Description |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| **Spans** | `createSpan`, `Span`, `SpanData`, `SpanEvent`, `SpanKind`, `SpanStatus`, `SpanHandler` | Low-level span creation with nested context propagation |
|
|
19
|
+
| **Cost Engine** | `createCostEngine`, `registerModelTier`, `CostEngine`, `CostEngineConfig`, `BudgetConfig`, `LoopDetectionConfig`, `CostAlert`, `CostDimension`, `CostIntelligenceReport`, `ModelSuggestion`, `ModelTierEntry` | Budget enforcement, cost projections, loop detection, and model optimization suggestions |
|
|
20
|
+
| **Tracer** | `observe`, `Tracer`, `TracerConfig`, `TracerOutput`, `TracerExporter`, `CostReport` | High-level tracing with sampling, console output, and custom exporters |
|
|
21
|
+
| **Metrics** | `createMetrics`, `MetricsCollector`, `MetricEntry` | Counters, gauges, and histograms for application-level metrics |
|
|
22
|
+
| **Audit Trail** | `createAuditTrail`, `auditMiddleware`, `AuditEventType`, `AuditEvent`, `AuditStorageAdapter`, `AuditQueryFilter`, `AuditIntegrityResult`, `AuditTrailConfig`, `AuditTrail` | SHA-256 hash-chained audit events with tamper detection and middleware |
|
|
23
|
+
| **Provenance** | `createProvenanceTracker`, `ProvenanceRecord`, `ProvenanceTracker` | Full lineage tracking per output: prompt, model, config, input, output |
|
|
24
|
+
| **OpenTelemetry** | `toOTelSpan`, `toOTelExportRequest`, `toTraceparent`, `parseTraceparent`, `injectTraceContext`, `extractTraceContext`, `createOTLPExporter`, `OTelSpan`, `OTelSpanKind`, `OTelStatusCode`, `OTelAttribute`, `OTelAttributeValue`, `OTelEvent`, `OTelResource`, `OTelExportRequest`, `TraceContext`, `OTLPExporterConfig` | W3C Trace Context propagation, OTel span conversion, and OTLP JSON export |
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
---
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
## Spans
|
|
29
|
+
|
|
30
|
+
Low-level building blocks for distributed tracing. Each span records a named operation with timing, metadata, events, and parent-child relationships.
|
|
31
|
+
|
|
32
|
+
### `SpanKind`
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
type SpanKind = 'llm' | 'tool' | 'agent' | 'workflow' | 'custom'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### `SpanStatus`
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
type SpanStatus = 'running' | 'ok' | 'error'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### `SpanEvent`
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
interface SpanEvent {
|
|
48
|
+
name: string
|
|
49
|
+
timestamp: number
|
|
50
|
+
data?: Record<string, unknown>
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `SpanData`
|
|
55
|
+
|
|
56
|
+
The serializable representation of a span, returned by `span.toJSON()`.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
interface SpanData {
|
|
60
|
+
id: string
|
|
61
|
+
traceId: string
|
|
62
|
+
parentId?: string
|
|
63
|
+
name: string
|
|
64
|
+
kind: SpanKind
|
|
65
|
+
status: SpanStatus
|
|
66
|
+
startTime: number
|
|
67
|
+
endTime?: number
|
|
68
|
+
durationMs?: number
|
|
69
|
+
metadata: Record<string, unknown>
|
|
70
|
+
events: SpanEvent[]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `Span`
|
|
75
|
+
|
|
76
|
+
The live span interface used during operation execution.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
interface Span {
|
|
80
|
+
readonly id: string
|
|
81
|
+
readonly traceId: string
|
|
82
|
+
readonly name: string
|
|
83
|
+
readonly kind: SpanKind
|
|
84
|
+
|
|
85
|
+
addEvent(name: string, data?: Record<string, unknown>): void
|
|
86
|
+
setMetadata(key: string, value: unknown): void
|
|
87
|
+
end(result?: { status?: SpanStatus; metadata?: Record<string, unknown> }): void
|
|
88
|
+
child(name: string, kind?: SpanKind): Span
|
|
89
|
+
toJSON(): SpanData
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `SpanHandler`
|
|
94
|
+
|
|
95
|
+
Callback invoked when a span ends.
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
type SpanHandler = (span: SpanData) => void
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### `createSpan()`
|
|
102
|
+
|
|
103
|
+
Creates a new span for tracing an operation.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
function createSpan(
|
|
107
|
+
name: string,
|
|
108
|
+
options?: {
|
|
109
|
+
traceId?: string
|
|
110
|
+
parentId?: string
|
|
111
|
+
kind?: SpanKind
|
|
112
|
+
onEnd?: SpanHandler
|
|
113
|
+
},
|
|
114
|
+
): Span
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
| Parameter | Type | Default | Description |
|
|
118
|
+
|---|---|---|---|
|
|
119
|
+
| `name` | `string` | -- | Human-readable name for the operation |
|
|
120
|
+
| `options.traceId` | `string` | auto-generated | Trace ID to group related spans |
|
|
121
|
+
| `options.parentId` | `string` | `undefined` | Parent span ID for nesting |
|
|
122
|
+
| `options.kind` | `SpanKind` | `'custom'` | Category of work being performed |
|
|
123
|
+
| `options.onEnd` | `SpanHandler` | `undefined` | Callback fired when `span.end()` is called |
|
|
124
|
+
|
|
125
|
+
**Returns:** `Span`
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { createSpan } from '@elsium-ai/observe'
|
|
129
|
+
|
|
130
|
+
const span = createSpan('generate-summary', { kind: 'llm' })
|
|
131
|
+
span.setMetadata('model', 'gpt-4o')
|
|
132
|
+
span.addEvent('prompt-sent', { tokens: 120 })
|
|
133
|
+
|
|
134
|
+
// Create a child span for a sub-operation
|
|
135
|
+
const child = span.child('embed-context', 'tool')
|
|
136
|
+
child.end()
|
|
137
|
+
|
|
138
|
+
span.end({ status: 'ok', metadata: { outputTokens: 350 } })
|
|
139
|
+
|
|
140
|
+
console.log(span.toJSON())
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Cost Engine
|
|
146
|
+
|
|
147
|
+
Budget enforcement and cost intelligence for LLM usage. Tracks spend across models, agents, users, and features. Detects runaway loops and suggests cheaper model alternatives.
|
|
148
|
+
|
|
149
|
+
### `BudgetConfig`
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
interface BudgetConfig {
|
|
153
|
+
totalBudget?: number
|
|
154
|
+
dailyBudget?: number
|
|
155
|
+
perUser?: number
|
|
156
|
+
perFeature?: number
|
|
157
|
+
perAgent?: number
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `LoopDetectionConfig`
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
interface LoopDetectionConfig {
|
|
165
|
+
maxCallsPerMinute?: number
|
|
166
|
+
maxCostPerMinute?: number
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### `CostAlert`
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
interface CostAlert {
|
|
174
|
+
type: 'threshold' | 'loop_detected' | 'budget_exceeded' | 'projection_warning'
|
|
175
|
+
dimension: string
|
|
176
|
+
currentValue: number
|
|
177
|
+
limit: number
|
|
178
|
+
message: string
|
|
179
|
+
timestamp: number
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### `CostDimension`
|
|
184
|
+
|
|
185
|
+
Aggregated cost data for a single dimension (model, agent, user, or feature).
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
interface CostDimension {
|
|
189
|
+
totalCost: number
|
|
190
|
+
totalTokens: number
|
|
191
|
+
callCount: number
|
|
192
|
+
firstCallAt: number
|
|
193
|
+
lastCallAt: number
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### `CostIntelligenceReport`
|
|
198
|
+
|
|
199
|
+
Full cost intelligence report with projections and recommendations.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
interface CostIntelligenceReport {
|
|
203
|
+
totalSpend: number
|
|
204
|
+
totalTokens: number
|
|
205
|
+
totalCalls: number
|
|
206
|
+
projectedDailySpend: number
|
|
207
|
+
projectedMonthlySpend: number
|
|
208
|
+
byModel: Record<string, CostDimension>
|
|
209
|
+
byAgent: Record<string, CostDimension>
|
|
210
|
+
byUser: Record<string, CostDimension>
|
|
211
|
+
byFeature: Record<string, CostDimension>
|
|
212
|
+
recommendations: string[]
|
|
213
|
+
alerts: CostAlert[]
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### `ModelSuggestion`
|
|
218
|
+
|
|
219
|
+
A recommendation to switch to a cheaper model.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
interface ModelSuggestion {
|
|
223
|
+
currentModel: string
|
|
224
|
+
suggestedModel: string
|
|
225
|
+
estimatedSavings: number
|
|
226
|
+
reason: string
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### `ModelTierEntry`
|
|
231
|
+
|
|
232
|
+
Defines the pricing tier for a model.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
interface ModelTierEntry {
|
|
236
|
+
tier: 'low' | 'mid' | 'high'
|
|
237
|
+
costPerMToken: number
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### `CostEngineConfig`
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
interface CostEngineConfig {
|
|
245
|
+
totalBudget?: number
|
|
246
|
+
dailyBudget?: number
|
|
247
|
+
perUser?: number
|
|
248
|
+
perFeature?: number
|
|
249
|
+
perAgent?: number
|
|
250
|
+
loopDetection?: LoopDetectionConfig
|
|
251
|
+
onAlert?: (alert: CostAlert) => void
|
|
252
|
+
alertThresholds?: number[]
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### `CostEngine`
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
interface CostEngine {
|
|
260
|
+
middleware(): Middleware
|
|
261
|
+
getReport(): CostIntelligenceReport
|
|
262
|
+
suggestModel(currentModel: string, inputTokens: number): ModelSuggestion | null
|
|
263
|
+
trackCall(
|
|
264
|
+
response: LLMResponse,
|
|
265
|
+
dimensions?: { agent?: string; user?: string; feature?: string },
|
|
266
|
+
): void
|
|
267
|
+
reset(): void
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### `createCostEngine()`
|
|
272
|
+
|
|
273
|
+
Creates a cost engine with budget enforcement, alerting, and cost intelligence.
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
function createCostEngine(config?: CostEngineConfig): CostEngine
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
| Parameter | Type | Default | Description |
|
|
280
|
+
|---|---|---|---|
|
|
281
|
+
| `config.totalBudget` | `number` | `undefined` | Maximum total spend before calls are rejected |
|
|
282
|
+
| `config.dailyBudget` | `number` | `undefined` | Maximum daily spend rate before alerts fire |
|
|
283
|
+
| `config.perUser` | `number` | `undefined` | Per-user budget limit |
|
|
284
|
+
| `config.perFeature` | `number` | `undefined` | Per-feature budget limit |
|
|
285
|
+
| `config.perAgent` | `number` | `undefined` | Per-agent budget limit |
|
|
286
|
+
| `config.loopDetection` | `LoopDetectionConfig` | `undefined` | Thresholds for detecting runaway loops |
|
|
287
|
+
| `config.onAlert` | `(alert: CostAlert) => void` | `undefined` | Callback for every alert |
|
|
288
|
+
| `config.alertThresholds` | `number[]` | `undefined` | Budget percentage thresholds that trigger alerts (e.g. `[0.5, 0.8, 0.95]`) |
|
|
289
|
+
|
|
290
|
+
**Returns:** `CostEngine`
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
import { createCostEngine } from '@elsium-ai/observe'
|
|
294
|
+
|
|
295
|
+
const engine = createCostEngine({
|
|
296
|
+
totalBudget: 50.0,
|
|
297
|
+
dailyBudget: 5.0,
|
|
298
|
+
perAgent: 10.0,
|
|
299
|
+
loopDetection: { maxCallsPerMinute: 60, maxCostPerMinute: 1.0 },
|
|
300
|
+
alertThresholds: [0.5, 0.8, 0.95],
|
|
301
|
+
onAlert: (alert) => console.warn(alert.message),
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
// Use as middleware in an ElsiumAI gateway
|
|
305
|
+
// gateway.use(engine.middleware())
|
|
306
|
+
|
|
307
|
+
// Or track calls manually
|
|
308
|
+
// engine.trackCall(response, { agent: 'summarizer', user: 'user-123' })
|
|
309
|
+
|
|
310
|
+
// Get a full intelligence report
|
|
311
|
+
const report = engine.getReport()
|
|
312
|
+
console.log('Projected monthly spend:', report.projectedMonthlySpend)
|
|
313
|
+
console.log('Recommendations:', report.recommendations)
|
|
314
|
+
|
|
315
|
+
// Get model optimization suggestions
|
|
316
|
+
const suggestion = engine.suggestModel('claude-opus-4-6', 200)
|
|
317
|
+
if (suggestion) {
|
|
318
|
+
console.log(`Switch to ${suggestion.suggestedModel} for ${suggestion.estimatedSavings.toFixed(0)}% savings`)
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### `registerModelTier()`
|
|
323
|
+
|
|
324
|
+
Registers a custom model with its pricing tier so the cost engine can track it.
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
function registerModelTier(model: string, entry: ModelTierEntry): void
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
| Parameter | Type | Description |
|
|
331
|
+
|---|---|---|
|
|
332
|
+
| `model` | `string` | The model identifier |
|
|
333
|
+
| `entry` | `ModelTierEntry` | The tier classification and cost per million tokens |
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
import { registerModelTier } from '@elsium-ai/observe'
|
|
337
|
+
|
|
338
|
+
registerModelTier('my-custom-model', { tier: 'mid', costPerMToken: 1.5 })
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
The cost engine ships with built-in tiers for common models including GPT-4o, GPT-4.1, GPT-5, Claude Sonnet 4.6, Claude Opus 4.6, Claude Haiku 4.5, Gemini 2.0 Flash, Gemini 2.5 Pro, o1, o3, o4-mini, and more.
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Tracer
|
|
346
|
+
|
|
347
|
+
High-level tracing API that wraps spans with sampling, console output, cost tracking, and pluggable exporters.
|
|
348
|
+
|
|
349
|
+
### `TracerOutput`
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
type TracerOutput = 'console' | 'json-file' | TracerExporter
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### `TracerExporter`
|
|
356
|
+
|
|
357
|
+
Interface for custom span exporters.
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
interface TracerExporter {
|
|
361
|
+
name: string
|
|
362
|
+
export(spans: SpanData[]): void | Promise<void>
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### `TracerConfig`
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
interface TracerConfig {
|
|
370
|
+
output?: TracerOutput[]
|
|
371
|
+
costTracking?: boolean
|
|
372
|
+
samplingRate?: number
|
|
373
|
+
maxSpans?: number
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### `CostReport`
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
interface CostReport {
|
|
381
|
+
totalCost: number
|
|
382
|
+
totalTokens: number
|
|
383
|
+
totalInputTokens: number
|
|
384
|
+
totalOutputTokens: number
|
|
385
|
+
callCount: number
|
|
386
|
+
byModel: Record<
|
|
387
|
+
string,
|
|
388
|
+
{
|
|
389
|
+
cost: number
|
|
390
|
+
tokens: number
|
|
391
|
+
calls: number
|
|
392
|
+
}
|
|
393
|
+
>
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### `Tracer`
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
interface Tracer {
|
|
401
|
+
startSpan(name: string, kind?: SpanKind): Span
|
|
402
|
+
getSpans(): SpanData[]
|
|
403
|
+
getCostReport(): CostReport
|
|
404
|
+
trackLLMCall(data: {
|
|
405
|
+
model: string
|
|
406
|
+
inputTokens: number
|
|
407
|
+
outputTokens: number
|
|
408
|
+
cost: number
|
|
409
|
+
latencyMs: number
|
|
410
|
+
}): void
|
|
411
|
+
reset(): void
|
|
412
|
+
flush(): Promise<void>
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### `observe()`
|
|
417
|
+
|
|
418
|
+
Creates a tracer instance for recording spans and LLM call costs.
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
function observe(config?: TracerConfig): Tracer
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
| Parameter | Type | Default | Description |
|
|
425
|
+
|---|---|---|---|
|
|
426
|
+
| `config.output` | `TracerOutput[]` | `['console']` | Where to send completed spans |
|
|
427
|
+
| `config.costTracking` | `boolean` | `true` | Whether `trackLLMCall` records data |
|
|
428
|
+
| `config.samplingRate` | `number` | `1.0` | Fraction of spans to sample (0.0 to 1.0) |
|
|
429
|
+
| `config.maxSpans` | `number` | `10000` | Maximum spans held in memory before oldest are evicted |
|
|
430
|
+
|
|
431
|
+
**Returns:** `Tracer`
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
import { observe } from '@elsium-ai/observe'
|
|
435
|
+
|
|
436
|
+
const tracer = observe({
|
|
437
|
+
output: ['console'],
|
|
438
|
+
samplingRate: 1.0,
|
|
439
|
+
costTracking: true,
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
// Start a span
|
|
443
|
+
const span = tracer.startSpan('chat-completion', 'llm')
|
|
444
|
+
span.setMetadata('model', 'gpt-4o')
|
|
445
|
+
span.end()
|
|
446
|
+
|
|
447
|
+
// Track an LLM call for cost reporting
|
|
448
|
+
tracer.trackLLMCall({
|
|
449
|
+
model: 'gpt-4o',
|
|
450
|
+
inputTokens: 500,
|
|
451
|
+
outputTokens: 200,
|
|
452
|
+
cost: 0.0035,
|
|
453
|
+
latencyMs: 1200,
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
// Get cost report
|
|
457
|
+
const report = tracer.getCostReport()
|
|
458
|
+
console.log('Total cost:', report.totalCost)
|
|
459
|
+
|
|
460
|
+
// Flush spans to all exporters
|
|
461
|
+
await tracer.flush()
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
You can provide a custom exporter:
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
import { observe } from '@elsium-ai/observe'
|
|
468
|
+
import type { TracerExporter } from '@elsium-ai/observe'
|
|
469
|
+
|
|
470
|
+
const myExporter: TracerExporter = {
|
|
471
|
+
name: 'my-backend',
|
|
472
|
+
async export(spans) {
|
|
473
|
+
await fetch('https://my-telemetry.example.com/spans', {
|
|
474
|
+
method: 'POST',
|
|
475
|
+
body: JSON.stringify(spans),
|
|
476
|
+
})
|
|
477
|
+
},
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const tracer = observe({ output: [myExporter] })
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
## Metrics
|
|
486
|
+
|
|
487
|
+
General-purpose metrics collection with counters, gauges, and histograms.
|
|
488
|
+
|
|
489
|
+
### `MetricEntry`
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
interface MetricEntry {
|
|
493
|
+
name: string
|
|
494
|
+
type: 'counter' | 'gauge' | 'histogram'
|
|
495
|
+
value: number
|
|
496
|
+
tags: Record<string, string>
|
|
497
|
+
timestamp: number
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### `MetricsCollector`
|
|
502
|
+
|
|
503
|
+
```ts
|
|
504
|
+
interface MetricsCollector {
|
|
505
|
+
increment(name: string, value?: number, tags?: Record<string, string>): void
|
|
506
|
+
gauge(name: string, value: number, tags?: Record<string, string>): void
|
|
507
|
+
histogram(name: string, value: number, tags?: Record<string, string>): void
|
|
508
|
+
getMetrics(): MetricEntry[]
|
|
509
|
+
reset(): void
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### `createMetrics()`
|
|
514
|
+
|
|
515
|
+
Creates a metrics collector for counters, gauges, and histograms.
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
function createMetrics(options?: { maxEntries?: number }): MetricsCollector
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
| Parameter | Type | Default | Description |
|
|
522
|
+
|---|---|---|---|
|
|
523
|
+
| `options.maxEntries` | `number` | `50000` | Maximum metric entries held in memory |
|
|
524
|
+
|
|
525
|
+
**Returns:** `MetricsCollector`
|
|
526
|
+
|
|
527
|
+
```ts
|
|
528
|
+
import { createMetrics } from '@elsium-ai/observe'
|
|
529
|
+
|
|
530
|
+
const metrics = createMetrics()
|
|
531
|
+
|
|
532
|
+
// Increment a counter (default increment is 1)
|
|
533
|
+
metrics.increment('llm.calls', 1, { model: 'gpt-4o' })
|
|
534
|
+
|
|
535
|
+
// Set a gauge to a current value
|
|
536
|
+
metrics.gauge('queue.depth', 42, { queue: 'embeddings' })
|
|
537
|
+
|
|
538
|
+
// Record a histogram observation
|
|
539
|
+
metrics.histogram('llm.latency_ms', 1200, { model: 'gpt-4o' })
|
|
540
|
+
|
|
541
|
+
// Retrieve all recorded entries
|
|
542
|
+
const entries = metrics.getMetrics()
|
|
543
|
+
console.log(`Recorded ${entries.length} metric entries`)
|
|
544
|
+
|
|
545
|
+
// Reset all state
|
|
546
|
+
metrics.reset()
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## Audit Trail
|
|
552
|
+
|
|
553
|
+
Tamper-evident audit logging with SHA-256 hash-chaining. Every event is linked to the previous one via its hash, enabling integrity verification of the full event history.
|
|
554
|
+
|
|
555
|
+
### `AuditEventType`
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
type AuditEventType =
|
|
559
|
+
| 'llm_call'
|
|
560
|
+
| 'tool_execution'
|
|
561
|
+
| 'security_violation'
|
|
562
|
+
| 'budget_alert'
|
|
563
|
+
| 'policy_violation'
|
|
564
|
+
| 'auth_event'
|
|
565
|
+
| 'approval_request'
|
|
566
|
+
| 'approval_decision'
|
|
567
|
+
| 'config_change'
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### `AuditEvent`
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
interface AuditEvent {
|
|
574
|
+
id: string
|
|
575
|
+
sequenceId: number
|
|
576
|
+
type: AuditEventType
|
|
577
|
+
timestamp: number
|
|
578
|
+
actor?: string
|
|
579
|
+
traceId?: string
|
|
580
|
+
data: Record<string, unknown>
|
|
581
|
+
hash: string
|
|
582
|
+
previousHash: string
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### `AuditStorageAdapter`
|
|
587
|
+
|
|
588
|
+
Interface for custom audit storage backends (database, file system, etc.).
|
|
589
|
+
|
|
590
|
+
```ts
|
|
591
|
+
interface AuditStorageAdapter {
|
|
592
|
+
append(event: AuditEvent): void | Promise<void>
|
|
593
|
+
query(filter: AuditQueryFilter): AuditEvent[] | Promise<AuditEvent[]>
|
|
594
|
+
count(): number | Promise<number>
|
|
595
|
+
verifyIntegrity(): AuditIntegrityResult | Promise<AuditIntegrityResult>
|
|
596
|
+
getLastHash?(): string | Promise<string>
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### `AuditQueryFilter`
|
|
601
|
+
|
|
602
|
+
```ts
|
|
603
|
+
interface AuditQueryFilter {
|
|
604
|
+
type?: AuditEventType | AuditEventType[]
|
|
605
|
+
actor?: string
|
|
606
|
+
traceId?: string
|
|
607
|
+
fromTimestamp?: number
|
|
608
|
+
toTimestamp?: number
|
|
609
|
+
limit?: number
|
|
610
|
+
offset?: number
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### `AuditIntegrityResult`
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
interface AuditIntegrityResult {
|
|
618
|
+
valid: boolean
|
|
619
|
+
totalEvents: number
|
|
620
|
+
brokenAt?: number
|
|
621
|
+
chainComplete?: boolean
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### `AuditTrailConfig`
|
|
626
|
+
|
|
627
|
+
```ts
|
|
628
|
+
interface AuditTrailConfig {
|
|
629
|
+
storage?: AuditStorageAdapter | 'memory'
|
|
630
|
+
hashChain?: boolean
|
|
631
|
+
maxEvents?: number
|
|
632
|
+
onError?: (error: unknown) => void
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### `AuditTrail`
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
interface AuditTrail {
|
|
640
|
+
log(
|
|
641
|
+
type: AuditEventType,
|
|
642
|
+
data: Record<string, unknown>,
|
|
643
|
+
options?: { actor?: string; traceId?: string },
|
|
644
|
+
): void
|
|
645
|
+
query(filter: AuditQueryFilter): Promise<AuditEvent[]>
|
|
646
|
+
verifyIntegrity(): Promise<AuditIntegrityResult>
|
|
647
|
+
readonly count: number
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### `createAuditTrail()`
|
|
652
|
+
|
|
653
|
+
Creates a hash-chained audit trail for tamper-evident event logging.
|
|
654
|
+
|
|
655
|
+
```ts
|
|
656
|
+
function createAuditTrail(config?: AuditTrailConfig): AuditTrail
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
| Parameter | Type | Default | Description |
|
|
660
|
+
|---|---|---|---|
|
|
661
|
+
| `config.storage` | `AuditStorageAdapter \| 'memory'` | `'memory'` | Storage backend for audit events |
|
|
662
|
+
| `config.hashChain` | `boolean` | `true` | Enable SHA-256 hash chaining for tamper detection |
|
|
663
|
+
| `config.maxEvents` | `number` | `10000` | Maximum events retained in memory storage |
|
|
664
|
+
| `config.onError` | `(error: unknown) => void` | `undefined` | Error handler for async storage failures |
|
|
665
|
+
|
|
666
|
+
**Returns:** `AuditTrail`
|
|
667
|
+
|
|
668
|
+
```ts
|
|
669
|
+
import { createAuditTrail } from '@elsium-ai/observe'
|
|
670
|
+
|
|
671
|
+
const audit = createAuditTrail({ hashChain: true })
|
|
672
|
+
|
|
673
|
+
// Log events
|
|
674
|
+
audit.log('llm_call', {
|
|
675
|
+
model: 'gpt-4o',
|
|
676
|
+
inputTokens: 500,
|
|
677
|
+
outputTokens: 200,
|
|
678
|
+
cost: 0.0035,
|
|
679
|
+
}, { actor: 'user-123', traceId: 'trc_abc' })
|
|
680
|
+
|
|
681
|
+
audit.log('tool_execution', {
|
|
682
|
+
tool: 'web-search',
|
|
683
|
+
query: 'latest news',
|
|
684
|
+
resultCount: 10,
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
// Query events
|
|
688
|
+
const llmEvents = await audit.query({ type: 'llm_call', limit: 50 })
|
|
689
|
+
console.log(`Found ${llmEvents.length} LLM call events`)
|
|
690
|
+
|
|
691
|
+
// Verify the hash chain has not been tampered with
|
|
692
|
+
const integrity = await audit.verifyIntegrity()
|
|
693
|
+
console.log('Audit chain valid:', integrity.valid)
|
|
694
|
+
console.log('Total events:', integrity.totalEvents)
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
### `auditMiddleware()`
|
|
698
|
+
|
|
699
|
+
Creates an ElsiumAI middleware that automatically logs every LLM call (success or failure) to the given audit trail.
|
|
700
|
+
|
|
701
|
+
```ts
|
|
702
|
+
function auditMiddleware(auditTrail: AuditTrail): Middleware
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
| Parameter | Type | Description |
|
|
706
|
+
|---|---|---|
|
|
707
|
+
| `auditTrail` | `AuditTrail` | The audit trail instance to log events to |
|
|
708
|
+
|
|
709
|
+
**Returns:** `Middleware` (from `@elsium-ai/core`)
|
|
710
|
+
|
|
711
|
+
```ts
|
|
712
|
+
import { createAuditTrail, auditMiddleware } from '@elsium-ai/observe'
|
|
26
713
|
|
|
27
|
-
// Hash-chained audit trail
|
|
28
714
|
const audit = createAuditTrail({ hashChain: true })
|
|
715
|
+
const middleware = auditMiddleware(audit)
|
|
716
|
+
|
|
717
|
+
// Use with an ElsiumAI gateway
|
|
718
|
+
// gateway.use(middleware)
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
The middleware automatically records `llm_call` events containing `provider`, `model`, `inputTokens`, `outputTokens`, `totalTokens`, `cost`, `latencyMs`, and `stopReason`. On errors, it records the error message and `success: false`.
|
|
722
|
+
|
|
723
|
+
---
|
|
724
|
+
|
|
725
|
+
## Provenance
|
|
726
|
+
|
|
727
|
+
Tracks the full lineage of every AI-generated output by hashing the prompt, model, config, input, and output. Enables reproducibility audits and output-to-source tracing.
|
|
728
|
+
|
|
729
|
+
### `ProvenanceRecord`
|
|
730
|
+
|
|
731
|
+
```ts
|
|
732
|
+
interface ProvenanceRecord {
|
|
733
|
+
id: string
|
|
734
|
+
outputHash: string
|
|
735
|
+
promptVersion: string
|
|
736
|
+
modelVersion: string
|
|
737
|
+
configHash: string
|
|
738
|
+
inputHash: string
|
|
739
|
+
timestamp: number
|
|
740
|
+
traceId?: string
|
|
741
|
+
metadata?: Record<string, unknown>
|
|
742
|
+
}
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
All hash fields (`outputHash`, `promptVersion`, `modelVersion`, `configHash`, `inputHash`) are SHA-256 hex digests of their respective inputs.
|
|
746
|
+
|
|
747
|
+
### `ProvenanceTracker`
|
|
748
|
+
|
|
749
|
+
```ts
|
|
750
|
+
interface ProvenanceTracker {
|
|
751
|
+
record(data: {
|
|
752
|
+
prompt: string
|
|
753
|
+
model: string
|
|
754
|
+
config: Record<string, unknown>
|
|
755
|
+
input: string
|
|
756
|
+
output: string
|
|
757
|
+
traceId?: string
|
|
758
|
+
metadata?: Record<string, unknown>
|
|
759
|
+
}): ProvenanceRecord
|
|
760
|
+
query(filter: {
|
|
761
|
+
outputHash?: string
|
|
762
|
+
promptVersion?: string
|
|
763
|
+
modelVersion?: string
|
|
764
|
+
traceId?: string
|
|
765
|
+
}): ProvenanceRecord[]
|
|
766
|
+
getLineage(outputHash: string): ProvenanceRecord[]
|
|
767
|
+
readonly count: number
|
|
768
|
+
clear(): void
|
|
769
|
+
}
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
### `createProvenanceTracker()`
|
|
773
|
+
|
|
774
|
+
Creates a provenance tracker for recording and querying output lineage.
|
|
775
|
+
|
|
776
|
+
```ts
|
|
777
|
+
function createProvenanceTracker(options?: {
|
|
778
|
+
maxRecords?: number
|
|
779
|
+
}): ProvenanceTracker
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
| Parameter | Type | Default | Description |
|
|
783
|
+
|---|---|---|---|
|
|
784
|
+
| `options.maxRecords` | `number` | `10000` | Maximum records held in memory |
|
|
785
|
+
|
|
786
|
+
**Returns:** `ProvenanceTracker`
|
|
787
|
+
|
|
788
|
+
```ts
|
|
789
|
+
import { createProvenanceTracker } from '@elsium-ai/observe'
|
|
29
790
|
|
|
30
|
-
// Provenance tracking
|
|
31
791
|
const provenance = createProvenanceTracker()
|
|
32
|
-
provenance.record({ prompt, model, config, input, output, traceId })
|
|
33
792
|
|
|
34
|
-
//
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
793
|
+
// Record a generation
|
|
794
|
+
const record = provenance.record({
|
|
795
|
+
prompt: 'Summarize the following article...',
|
|
796
|
+
model: 'gpt-4o',
|
|
797
|
+
config: { temperature: 0.7, maxTokens: 500 },
|
|
798
|
+
input: 'The article text here...',
|
|
799
|
+
output: 'A concise summary of the article.',
|
|
800
|
+
traceId: 'trc_abc',
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
console.log('Output hash:', record.outputHash)
|
|
804
|
+
|
|
805
|
+
// Query records by output hash
|
|
806
|
+
const matches = provenance.query({ outputHash: record.outputHash })
|
|
807
|
+
|
|
808
|
+
// Get the full lineage for a given output (all records sharing the same traceId)
|
|
809
|
+
const lineage = provenance.getLineage(record.outputHash)
|
|
810
|
+
console.log(`Lineage has ${lineage.length} steps`)
|
|
811
|
+
|
|
812
|
+
// Check count and clear
|
|
813
|
+
console.log('Total records:', provenance.count)
|
|
814
|
+
provenance.clear()
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
---
|
|
818
|
+
|
|
819
|
+
## OpenTelemetry
|
|
820
|
+
|
|
821
|
+
Compatibility layer for converting ElsiumAI spans to the OpenTelemetry format, propagating W3C Trace Context headers, and exporting via OTLP JSON to any OTel-compatible backend (Jaeger, Grafana Tempo, Datadog, Honeycomb, etc.).
|
|
822
|
+
|
|
823
|
+
### `OTelSpanKind`
|
|
824
|
+
|
|
825
|
+
```ts
|
|
826
|
+
type OTelSpanKind = 0 | 1 | 2 | 3 | 4 | 5
|
|
827
|
+
// 0 = UNSPECIFIED, 1 = INTERNAL, 2 = SERVER, 3 = CLIENT, 4 = PRODUCER, 5 = CONSUMER
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
### `OTelStatusCode`
|
|
831
|
+
|
|
832
|
+
```ts
|
|
833
|
+
type OTelStatusCode = 0 | 1 | 2
|
|
834
|
+
// 0 = UNSET, 1 = OK, 2 = ERROR
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
### `OTelAttributeValue`
|
|
838
|
+
|
|
839
|
+
```ts
|
|
840
|
+
interface OTelAttributeValue {
|
|
841
|
+
stringValue?: string
|
|
842
|
+
intValue?: number
|
|
843
|
+
doubleValue?: number
|
|
844
|
+
boolValue?: boolean
|
|
845
|
+
arrayValue?: { values: OTelAttributeValue[] }
|
|
846
|
+
}
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
### `OTelAttribute`
|
|
850
|
+
|
|
851
|
+
```ts
|
|
852
|
+
interface OTelAttribute {
|
|
853
|
+
key: string
|
|
854
|
+
value: OTelAttributeValue
|
|
855
|
+
}
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
### `OTelEvent`
|
|
859
|
+
|
|
860
|
+
```ts
|
|
861
|
+
interface OTelEvent {
|
|
862
|
+
name: string
|
|
863
|
+
timeUnixNano: string
|
|
864
|
+
attributes: OTelAttribute[]
|
|
865
|
+
}
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
### `OTelSpan`
|
|
869
|
+
|
|
870
|
+
The OpenTelemetry-compatible span representation.
|
|
871
|
+
|
|
872
|
+
```ts
|
|
873
|
+
interface OTelSpan {
|
|
874
|
+
traceId: string
|
|
875
|
+
spanId: string
|
|
876
|
+
parentSpanId?: string
|
|
877
|
+
name: string
|
|
878
|
+
kind: OTelSpanKind
|
|
879
|
+
startTimeUnixNano: string
|
|
880
|
+
endTimeUnixNano: string
|
|
881
|
+
attributes: OTelAttribute[]
|
|
882
|
+
events: OTelEvent[]
|
|
883
|
+
status: {
|
|
884
|
+
code: OTelStatusCode
|
|
885
|
+
message?: string
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
### `OTelResource`
|
|
891
|
+
|
|
892
|
+
```ts
|
|
893
|
+
interface OTelResource {
|
|
894
|
+
attributes: OTelAttribute[]
|
|
895
|
+
}
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
### `OTelExportRequest`
|
|
899
|
+
|
|
900
|
+
The full OTLP JSON export payload structure.
|
|
901
|
+
|
|
902
|
+
```ts
|
|
903
|
+
interface OTelExportRequest {
|
|
904
|
+
resourceSpans: Array<{
|
|
905
|
+
resource: OTelResource
|
|
906
|
+
scopeSpans: Array<{
|
|
907
|
+
scope: {
|
|
908
|
+
name: string
|
|
909
|
+
version: string
|
|
910
|
+
}
|
|
911
|
+
spans: OTelSpan[]
|
|
912
|
+
}>
|
|
913
|
+
}>
|
|
914
|
+
}
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
### `TraceContext`
|
|
918
|
+
|
|
919
|
+
Parsed W3C Trace Context.
|
|
920
|
+
|
|
921
|
+
```ts
|
|
922
|
+
interface TraceContext {
|
|
923
|
+
traceId: string
|
|
924
|
+
spanId: string
|
|
925
|
+
traceFlags: number
|
|
926
|
+
traceState?: string
|
|
927
|
+
}
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
### `OTLPExporterConfig`
|
|
931
|
+
|
|
932
|
+
```ts
|
|
933
|
+
interface OTLPExporterConfig {
|
|
934
|
+
/** OTLP endpoint URL (e.g. http://localhost:4318/v1/traces) */
|
|
935
|
+
endpoint: string
|
|
936
|
+
/** Optional headers (e.g. for auth) */
|
|
937
|
+
headers?: Record<string, string>
|
|
938
|
+
/** Service name for resource attributes */
|
|
939
|
+
serviceName?: string
|
|
940
|
+
/** Service version */
|
|
941
|
+
serviceVersion?: string
|
|
942
|
+
/** Batch size before sending */
|
|
943
|
+
batchSize?: number
|
|
944
|
+
/** Flush interval in ms */
|
|
945
|
+
flushIntervalMs?: number
|
|
946
|
+
}
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
### `toOTelSpan()`
|
|
950
|
+
|
|
951
|
+
Converts an ElsiumAI `SpanData` to an OpenTelemetry-compatible span.
|
|
952
|
+
|
|
953
|
+
```ts
|
|
954
|
+
function toOTelSpan(span: SpanData): OTelSpan
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
| Parameter | Type | Description |
|
|
958
|
+
|---|---|---|
|
|
959
|
+
| `span` | `SpanData` | The ElsiumAI span to convert |
|
|
960
|
+
|
|
961
|
+
**Returns:** `OTelSpan`
|
|
962
|
+
|
|
963
|
+
```ts
|
|
964
|
+
import { createSpan, toOTelSpan } from '@elsium-ai/observe'
|
|
965
|
+
|
|
966
|
+
const span = createSpan('my-operation', { kind: 'llm' })
|
|
967
|
+
span.end()
|
|
968
|
+
|
|
969
|
+
const otelSpan = toOTelSpan(span.toJSON())
|
|
970
|
+
console.log(otelSpan.traceId, otelSpan.spanId)
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
ElsiumAI span kinds are mapped as follows: `llm` to CLIENT (3), `tool`/`agent`/`workflow` to INTERNAL (1), `custom` to UNSPECIFIED (0).
|
|
974
|
+
|
|
975
|
+
### `toOTelExportRequest()`
|
|
976
|
+
|
|
977
|
+
Builds a full OTLP JSON export request from a batch of spans.
|
|
978
|
+
|
|
979
|
+
```ts
|
|
980
|
+
function toOTelExportRequest(
|
|
981
|
+
spans: SpanData[],
|
|
982
|
+
options?: {
|
|
983
|
+
serviceName?: string
|
|
984
|
+
serviceVersion?: string
|
|
985
|
+
},
|
|
986
|
+
): OTelExportRequest
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
| Parameter | Type | Default | Description |
|
|
990
|
+
|---|---|---|---|
|
|
991
|
+
| `spans` | `SpanData[]` | -- | Batch of spans to export |
|
|
992
|
+
| `options.serviceName` | `string` | `'elsium-ai'` | Service name in resource attributes |
|
|
993
|
+
| `options.serviceVersion` | `string` | `'0.1.0'` | Service version in resource attributes |
|
|
994
|
+
|
|
995
|
+
**Returns:** `OTelExportRequest`
|
|
996
|
+
|
|
997
|
+
```ts
|
|
998
|
+
import { observe, toOTelExportRequest } from '@elsium-ai/observe'
|
|
999
|
+
|
|
1000
|
+
const tracer = observe({ output: [] })
|
|
1001
|
+
const span = tracer.startSpan('process-request', 'agent')
|
|
1002
|
+
span.end()
|
|
1003
|
+
|
|
1004
|
+
const payload = toOTelExportRequest(tracer.getSpans(), {
|
|
1005
|
+
serviceName: 'my-ai-service',
|
|
1006
|
+
serviceVersion: '1.0.0',
|
|
1007
|
+
})
|
|
1008
|
+
|
|
1009
|
+
// Send to any OTLP-compatible endpoint
|
|
1010
|
+
await fetch('http://localhost:4318/v1/traces', {
|
|
1011
|
+
method: 'POST',
|
|
1012
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1013
|
+
body: JSON.stringify(payload),
|
|
1014
|
+
})
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
### `toTraceparent()`
|
|
1018
|
+
|
|
1019
|
+
Creates a W3C `traceparent` header value from a span.
|
|
1020
|
+
|
|
1021
|
+
```ts
|
|
1022
|
+
function toTraceparent(span: SpanData): string
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
| Parameter | Type | Description |
|
|
1026
|
+
|---|---|---|
|
|
1027
|
+
| `span` | `SpanData` | The span to derive the traceparent from |
|
|
1028
|
+
|
|
1029
|
+
**Returns:** `string` -- Format: `00-{traceId}-{spanId}-01`
|
|
1030
|
+
|
|
1031
|
+
```ts
|
|
1032
|
+
import { createSpan, toTraceparent } from '@elsium-ai/observe'
|
|
1033
|
+
|
|
1034
|
+
const span = createSpan('outgoing-call', { kind: 'llm' })
|
|
1035
|
+
span.end()
|
|
1036
|
+
|
|
1037
|
+
const header = toTraceparent(span.toJSON())
|
|
1038
|
+
// e.g. "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
### `parseTraceparent()`
|
|
1042
|
+
|
|
1043
|
+
Parses a W3C `traceparent` header string into a `TraceContext`.
|
|
1044
|
+
|
|
1045
|
+
```ts
|
|
1046
|
+
function parseTraceparent(header: string): TraceContext | null
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
| Parameter | Type | Description |
|
|
1050
|
+
|---|---|---|
|
|
1051
|
+
| `header` | `string` | The traceparent header value |
|
|
1052
|
+
|
|
1053
|
+
**Returns:** `TraceContext | null` -- Returns `null` if the header is malformed or uses an unsupported version.
|
|
1054
|
+
|
|
1055
|
+
```ts
|
|
1056
|
+
import { parseTraceparent } from '@elsium-ai/observe'
|
|
1057
|
+
|
|
1058
|
+
const ctx = parseTraceparent('00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01')
|
|
1059
|
+
if (ctx) {
|
|
1060
|
+
console.log('Trace ID:', ctx.traceId)
|
|
1061
|
+
console.log('Span ID:', ctx.spanId)
|
|
1062
|
+
console.log('Sampled:', ctx.traceFlags === 1)
|
|
1063
|
+
}
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
### `injectTraceContext()`
|
|
1067
|
+
|
|
1068
|
+
Injects the `traceparent` header into an outgoing HTTP headers object for distributed trace propagation.
|
|
1069
|
+
|
|
1070
|
+
```ts
|
|
1071
|
+
function injectTraceContext(
|
|
1072
|
+
span: SpanData,
|
|
1073
|
+
headers?: Record<string, string>,
|
|
1074
|
+
): Record<string, string>
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
| Parameter | Type | Default | Description |
|
|
1078
|
+
|---|---|---|---|
|
|
1079
|
+
| `span` | `SpanData` | -- | The span whose trace context to inject |
|
|
1080
|
+
| `headers` | `Record<string, string>` | `{}` | Existing headers to merge with |
|
|
1081
|
+
|
|
1082
|
+
**Returns:** `Record<string, string>` -- A new headers object with `traceparent` set.
|
|
1083
|
+
|
|
1084
|
+
```ts
|
|
1085
|
+
import { createSpan, injectTraceContext } from '@elsium-ai/observe'
|
|
1086
|
+
|
|
1087
|
+
const span = createSpan('api-call', { kind: 'llm' })
|
|
1088
|
+
const headers = injectTraceContext(span.toJSON(), {
|
|
1089
|
+
'Content-Type': 'application/json',
|
|
1090
|
+
Authorization: 'Bearer token',
|
|
1091
|
+
})
|
|
1092
|
+
// headers now includes { traceparent: '00-...-...-01', ... }
|
|
1093
|
+
|
|
1094
|
+
await fetch('https://api.example.com/generate', { headers })
|
|
1095
|
+
span.end()
|
|
1096
|
+
```
|
|
1097
|
+
|
|
1098
|
+
### `extractTraceContext()`
|
|
1099
|
+
|
|
1100
|
+
Extracts a `TraceContext` from incoming HTTP headers. Checks both `traceparent` and `Traceparent` keys.
|
|
1101
|
+
|
|
1102
|
+
```ts
|
|
1103
|
+
function extractTraceContext(
|
|
1104
|
+
headers: Record<string, string | undefined>,
|
|
1105
|
+
): TraceContext | null
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
| Parameter | Type | Description |
|
|
1109
|
+
|---|---|---|
|
|
1110
|
+
| `headers` | `Record<string, string \| undefined>` | Incoming HTTP headers |
|
|
1111
|
+
|
|
1112
|
+
**Returns:** `TraceContext | null`
|
|
1113
|
+
|
|
1114
|
+
```ts
|
|
1115
|
+
import { extractTraceContext, createSpan } from '@elsium-ai/observe'
|
|
1116
|
+
|
|
1117
|
+
// In an HTTP handler
|
|
1118
|
+
function handleRequest(req: { headers: Record<string, string> }) {
|
|
1119
|
+
const parentCtx = extractTraceContext(req.headers)
|
|
1120
|
+
|
|
1121
|
+
const span = createSpan('handle-request', {
|
|
1122
|
+
traceId: parentCtx?.traceId,
|
|
1123
|
+
parentId: parentCtx?.spanId,
|
|
1124
|
+
kind: 'agent',
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
// ... process request ...
|
|
1128
|
+
span.end()
|
|
1129
|
+
}
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
### `createOTLPExporter()`
|
|
1133
|
+
|
|
1134
|
+
Creates an OTLP JSON exporter that sends spans to any OTel-compatible backend. Supports batching and automatic periodic flushing.
|
|
1135
|
+
|
|
1136
|
+
```ts
|
|
1137
|
+
function createOTLPExporter(config: OTLPExporterConfig): TracerExporter
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1140
|
+
| Parameter | Type | Default | Description |
|
|
1141
|
+
|---|---|---|---|
|
|
1142
|
+
| `config.endpoint` | `string` | -- | OTLP endpoint URL (e.g. `http://localhost:4318/v1/traces`) |
|
|
1143
|
+
| `config.headers` | `Record<string, string>` | `{}` | Optional HTTP headers (e.g. for authentication) |
|
|
1144
|
+
| `config.serviceName` | `string` | `undefined` | Service name for resource attributes |
|
|
1145
|
+
| `config.serviceVersion` | `string` | `undefined` | Service version for resource attributes |
|
|
1146
|
+
| `config.batchSize` | `number` | `100` | Number of spans to buffer before sending a batch |
|
|
1147
|
+
| `config.flushIntervalMs` | `number` | `5000` | Automatic flush interval in milliseconds |
|
|
1148
|
+
|
|
1149
|
+
**Returns:** `TracerExporter`
|
|
1150
|
+
|
|
1151
|
+
```ts
|
|
1152
|
+
import { observe, createOTLPExporter } from '@elsium-ai/observe'
|
|
1153
|
+
|
|
1154
|
+
const exporter = createOTLPExporter({
|
|
1155
|
+
endpoint: 'http://localhost:4318/v1/traces',
|
|
1156
|
+
serviceName: 'my-ai-service',
|
|
1157
|
+
serviceVersion: '1.0.0',
|
|
1158
|
+
headers: { Authorization: 'Bearer my-token' },
|
|
1159
|
+
batchSize: 50,
|
|
1160
|
+
flushIntervalMs: 10000,
|
|
1161
|
+
})
|
|
1162
|
+
|
|
1163
|
+
const tracer = observe({ output: [exporter] })
|
|
1164
|
+
|
|
1165
|
+
const span = tracer.startSpan('generate', 'llm')
|
|
38
1166
|
span.end()
|
|
1167
|
+
|
|
1168
|
+
// Spans are batched and sent automatically, or flush manually:
|
|
1169
|
+
await tracer.flush()
|
|
39
1170
|
```
|
|
40
1171
|
|
|
1172
|
+
---
|
|
1173
|
+
|
|
41
1174
|
## Part of ElsiumAI
|
|
42
1175
|
|
|
43
1176
|
This package is the observability layer of the [ElsiumAI](https://github.com/elsium-ai/elsium-ai) framework. See the [full documentation](https://github.com/elsium-ai/elsium-ai) for guides and examples.
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elsium-ai/observe",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Observability, tracing, and cost tracking for ElsiumAI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Eric Utrera <ebutrera9103@gmail.com>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/elsium-ai/elsium-ai",
|
|
9
|
+
"url": "git+https://github.com/elsium-ai/elsium-ai.git",
|
|
10
10
|
"directory": "packages/observe"
|
|
11
11
|
},
|
|
12
12
|
"type": "module",
|
|
@@ -26,9 +26,13 @@
|
|
|
26
26
|
"dev": "bun --watch src/index.ts"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@elsium-ai/core": "
|
|
29
|
+
"@elsium-ai/core": "^0.2.2"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"typescript": "^5.7.0"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"registry": "https://registry.npmjs.org",
|
|
36
|
+
"access": "public"
|
|
33
37
|
}
|
|
34
38
|
}
|