@elsium-ai/observe 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/README.md +61 -1
- package/dist/audit.d.ts +9 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +122 -32
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -565,6 +565,8 @@ type AuditEventType =
|
|
|
565
565
|
| 'approval_request'
|
|
566
566
|
| 'approval_decision'
|
|
567
567
|
| 'config_change'
|
|
568
|
+
| 'provider_failover'
|
|
569
|
+
| 'circuit_breaker_state_change'
|
|
568
570
|
```
|
|
569
571
|
|
|
570
572
|
### `AuditEvent`
|
|
@@ -622,6 +624,20 @@ interface AuditIntegrityResult {
|
|
|
622
624
|
}
|
|
623
625
|
```
|
|
624
626
|
|
|
627
|
+
### `AuditBatchConfig`
|
|
628
|
+
|
|
629
|
+
```ts
|
|
630
|
+
interface AuditBatchConfig {
|
|
631
|
+
size?: number
|
|
632
|
+
intervalMs?: number
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
| Field | Type | Default | Description |
|
|
637
|
+
|---|---|---|---|
|
|
638
|
+
| `size` | `number` | `100` | Flush the buffer when it reaches this many events. |
|
|
639
|
+
| `intervalMs` | `number` | `50` | Flush the buffer on this interval (ms), regardless of size. |
|
|
640
|
+
|
|
625
641
|
### `AuditTrailConfig`
|
|
626
642
|
|
|
627
643
|
```ts
|
|
@@ -629,6 +645,7 @@ interface AuditTrailConfig {
|
|
|
629
645
|
storage?: AuditStorageAdapter | 'memory'
|
|
630
646
|
hashChain?: boolean
|
|
631
647
|
maxEvents?: number
|
|
648
|
+
batch?: AuditBatchConfig
|
|
632
649
|
onError?: (error: unknown) => void
|
|
633
650
|
}
|
|
634
651
|
```
|
|
@@ -642,12 +659,27 @@ interface AuditTrail {
|
|
|
642
659
|
data: Record<string, unknown>,
|
|
643
660
|
options?: { actor?: string; traceId?: string },
|
|
644
661
|
): void
|
|
662
|
+
ready(): Promise<void>
|
|
645
663
|
query(filter: AuditQueryFilter): Promise<AuditEvent[]>
|
|
646
664
|
verifyIntegrity(): Promise<AuditIntegrityResult>
|
|
665
|
+
flush(): Promise<void>
|
|
666
|
+
dispose(): void
|
|
647
667
|
readonly count: number
|
|
668
|
+
readonly pending: number
|
|
648
669
|
}
|
|
649
670
|
```
|
|
650
671
|
|
|
672
|
+
| Member | Description |
|
|
673
|
+
|---|---|
|
|
674
|
+
| `log()` | Append an event. In batched mode, this buffers the event without hashing — zero CPU on the hot path. |
|
|
675
|
+
| `ready()` | Resolves once async initialization (e.g. `getLastHash`) has completed. |
|
|
676
|
+
| `query()` | Query events by type, actor, traceId, or timestamp range. Auto-flushes pending events in batched mode. |
|
|
677
|
+
| `verifyIntegrity()` | Verify the hash chain has not been tampered with. Auto-flushes pending events in batched mode. |
|
|
678
|
+
| `flush()` | Drain all pending events — computes hashes and writes to storage. No-op when not in batched mode. |
|
|
679
|
+
| `dispose()` | Stop the flush timer and drain remaining events. Call this on shutdown. |
|
|
680
|
+
| `count` | Total events (stored + pending). |
|
|
681
|
+
| `pending` | Number of buffered events not yet flushed (0 when not in batched mode). |
|
|
682
|
+
|
|
651
683
|
### `createAuditTrail()`
|
|
652
684
|
|
|
653
685
|
Creates a hash-chained audit trail for tamper-evident event logging.
|
|
@@ -660,7 +692,8 @@ function createAuditTrail(config?: AuditTrailConfig): AuditTrail
|
|
|
660
692
|
|---|---|---|---|
|
|
661
693
|
| `config.storage` | `AuditStorageAdapter \| 'memory'` | `'memory'` | Storage backend for audit events |
|
|
662
694
|
| `config.hashChain` | `boolean` | `true` | Enable SHA-256 hash chaining for tamper detection |
|
|
663
|
-
| `config.maxEvents` | `number` | `10000` | Maximum events retained
|
|
695
|
+
| `config.maxEvents` | `number` | `10000` | Maximum events retained (ring buffer — O(1) eviction) |
|
|
696
|
+
| `config.batch` | `AuditBatchConfig` | `undefined` | Enable batched mode for high-volume scenarios |
|
|
664
697
|
| `config.onError` | `(error: unknown) => void` | `undefined` | Error handler for async storage failures |
|
|
665
698
|
|
|
666
699
|
**Returns:** `AuditTrail`
|
|
@@ -694,6 +727,33 @@ console.log('Audit chain valid:', integrity.valid)
|
|
|
694
727
|
console.log('Total events:', integrity.totalEvents)
|
|
695
728
|
```
|
|
696
729
|
|
|
730
|
+
#### Batched mode (high-volume)
|
|
731
|
+
|
|
732
|
+
In high-volume scenarios, `log()` computes a SHA-256 hash on every call — blocking the hot path. Batched mode moves hashing off the critical path: `log()` buffers raw event data, and hashing + storage writes happen asynchronously on flush.
|
|
733
|
+
|
|
734
|
+
```ts
|
|
735
|
+
import { createAuditTrail } from '@elsium-ai/observe'
|
|
736
|
+
|
|
737
|
+
const audit = createAuditTrail({
|
|
738
|
+
batch: {
|
|
739
|
+
size: 500, // Flush after 500 events
|
|
740
|
+
intervalMs: 100, // Or every 100ms, whichever comes first
|
|
741
|
+
},
|
|
742
|
+
maxEvents: 100_000,
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
// log() is now near-zero cost — just pushes to an internal buffer
|
|
746
|
+
audit.log('llm_call', { model: 'gpt-4o', tokens: 100 })
|
|
747
|
+
|
|
748
|
+
// Force-flush before reading (query/verifyIntegrity auto-flush)
|
|
749
|
+
await audit.flush()
|
|
750
|
+
|
|
751
|
+
// Clean up on shutdown
|
|
752
|
+
process.on('SIGTERM', () => audit.dispose())
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
Hash chain integrity is fully preserved — events are hashed sequentially during flush, not during `log()`.
|
|
756
|
+
|
|
697
757
|
### `auditMiddleware()`
|
|
698
758
|
|
|
699
759
|
Creates an ElsiumAI middleware that automatically logs every LLM call (success or failure) to the given audit trail.
|
package/dist/audit.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Middleware } from '@elsium-ai/core';
|
|
2
|
-
export type AuditEventType = 'llm_call' | 'tool_execution' | 'security_violation' | 'budget_alert' | 'policy_violation' | 'auth_event' | 'approval_request' | 'approval_decision' | 'config_change';
|
|
2
|
+
export type AuditEventType = 'llm_call' | 'tool_execution' | 'security_violation' | 'budget_alert' | 'policy_violation' | 'auth_event' | 'approval_request' | 'approval_decision' | 'config_change' | 'provider_failover' | 'circuit_breaker_state_change';
|
|
3
3
|
export interface AuditEvent {
|
|
4
4
|
id: string;
|
|
5
5
|
sequenceId: number;
|
|
@@ -33,10 +33,15 @@ export interface AuditIntegrityResult {
|
|
|
33
33
|
brokenAt?: number;
|
|
34
34
|
chainComplete?: boolean;
|
|
35
35
|
}
|
|
36
|
+
export interface AuditBatchConfig {
|
|
37
|
+
size?: number;
|
|
38
|
+
intervalMs?: number;
|
|
39
|
+
}
|
|
36
40
|
export interface AuditTrailConfig {
|
|
37
41
|
storage?: AuditStorageAdapter | 'memory';
|
|
38
42
|
hashChain?: boolean;
|
|
39
43
|
maxEvents?: number;
|
|
44
|
+
batch?: AuditBatchConfig;
|
|
40
45
|
onError?: (error: unknown) => void;
|
|
41
46
|
}
|
|
42
47
|
export interface AuditTrail {
|
|
@@ -48,7 +53,10 @@ export interface AuditTrail {
|
|
|
48
53
|
ready(): Promise<void>;
|
|
49
54
|
query(filter: AuditQueryFilter): Promise<AuditEvent[]>;
|
|
50
55
|
verifyIntegrity(): Promise<AuditIntegrityResult>;
|
|
56
|
+
flush(): Promise<void>;
|
|
57
|
+
dispose(): void;
|
|
51
58
|
readonly count: number;
|
|
59
|
+
readonly pending: number;
|
|
52
60
|
}
|
|
53
61
|
export declare function createAuditTrail(config?: AuditTrailConfig): AuditTrail;
|
|
54
62
|
export declare function auditMiddleware(auditTrail: AuditTrail): Middleware;
|
package/dist/audit.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAqC,MAAM,iBAAiB,CAAA;AAEpF,MAAM,MAAM,cAAc,GACvB,UAAU,GACV,gBAAgB,GAChB,oBAAoB,GACpB,cAAc,GACd,kBAAkB,GAClB,YAAY,GACZ,kBAAkB,GAClB,mBAAmB,GACnB,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type { Tracer, TracerConfig, TracerOutput, TracerExporter, CostReport } f
|
|
|
7
7
|
export { createMetrics } from './metrics';
|
|
8
8
|
export type { MetricsCollector, MetricEntry } from './metrics';
|
|
9
9
|
export { createAuditTrail, auditMiddleware } from './audit';
|
|
10
|
-
export type { AuditEventType, AuditEvent, AuditStorageAdapter, AuditQueryFilter, AuditIntegrityResult, AuditTrailConfig, AuditTrail, } from './audit';
|
|
10
|
+
export type { AuditEventType, AuditEvent, AuditStorageAdapter, AuditQueryFilter, AuditIntegrityResult, AuditTrailConfig, AuditBatchConfig, AuditTrail, } from './audit';
|
|
11
11
|
export { createProvenanceTracker } from './provenance';
|
|
12
12
|
export type { ProvenanceRecord, ProvenanceTracker } from './provenance';
|
|
13
13
|
export { createExperiment, createFileExperimentStore } from './experiment';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACnC,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAA;AAG1F,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACnE,YAAY,EACX,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,mBAAmB,EACnB,SAAS,EACT,aAAa,EACb,sBAAsB,EACtB,eAAe,EACf,cAAc,GACd,MAAM,eAAe,CAAA;AAGtB,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAClC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAG9F,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACzC,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAG9D,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAC3D,YAAY,EACX,cAAc,EACd,UAAU,EACV,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,UAAU,GACV,MAAM,SAAS,CAAA;AAGhB,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AACtD,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAGvE,OAAO,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAA;AAC1E,YAAY,EACX,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GACf,MAAM,cAAc,CAAA;AAGrB,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAClE,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAGvD,OAAO,EACN,UAAU,EACV,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,GAClB,MAAM,QAAQ,CAAA;AACf,YAAY,EACX,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,GAClB,MAAM,QAAQ,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACnC,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAA;AAG1F,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACnE,YAAY,EACX,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,mBAAmB,EACnB,SAAS,EACT,aAAa,EACb,sBAAsB,EACtB,eAAe,EACf,cAAc,GACd,MAAM,eAAe,CAAA;AAGtB,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAClC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAG9F,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACzC,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAG9D,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAC3D,YAAY,EACX,cAAc,EACd,UAAU,EACV,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,gBAAgB,EAChB,UAAU,GACV,MAAM,SAAS,CAAA;AAGhB,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AACtD,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAGvE,OAAO,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAA;AAC1E,YAAY,EACX,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GACf,MAAM,cAAc,CAAA;AAGrB,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAClE,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAGvD,OAAO,EACN,UAAU,EACV,mBAAmB,EACnB,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,GAClB,MAAM,QAAQ,CAAA;AACf,YAAY,EACX,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,GAClB,MAAM,QAAQ,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -708,21 +708,53 @@ function computeEventHash(event, previousHash) {
|
|
|
708
708
|
});
|
|
709
709
|
return createHash("sha256").update(content).digest("hex");
|
|
710
710
|
}
|
|
711
|
+
var ZERO_HASH = "0".repeat(64);
|
|
712
|
+
|
|
713
|
+
class RingBuffer {
|
|
714
|
+
buffer;
|
|
715
|
+
head = 0;
|
|
716
|
+
size = 0;
|
|
717
|
+
capacity;
|
|
718
|
+
constructor(capacity) {
|
|
719
|
+
this.capacity = capacity;
|
|
720
|
+
this.buffer = new Array(capacity);
|
|
721
|
+
}
|
|
722
|
+
push(item) {
|
|
723
|
+
const index = (this.head + this.size) % this.capacity;
|
|
724
|
+
if (this.size === this.capacity) {
|
|
725
|
+
this.head = (this.head + 1) % this.capacity;
|
|
726
|
+
} else {
|
|
727
|
+
this.size++;
|
|
728
|
+
}
|
|
729
|
+
this.buffer[index] = item;
|
|
730
|
+
}
|
|
731
|
+
toArray() {
|
|
732
|
+
const result = new Array(this.size);
|
|
733
|
+
for (let i = 0;i < this.size; i++) {
|
|
734
|
+
result[i] = this.buffer[(this.head + i) % this.capacity];
|
|
735
|
+
}
|
|
736
|
+
return result;
|
|
737
|
+
}
|
|
738
|
+
get length() {
|
|
739
|
+
return this.size;
|
|
740
|
+
}
|
|
741
|
+
last() {
|
|
742
|
+
if (this.size === 0)
|
|
743
|
+
return;
|
|
744
|
+
return this.buffer[(this.head + this.size - 1) % this.capacity];
|
|
745
|
+
}
|
|
746
|
+
}
|
|
711
747
|
|
|
712
748
|
class InMemoryAuditStorage {
|
|
713
|
-
|
|
714
|
-
maxEvents;
|
|
749
|
+
ring;
|
|
715
750
|
constructor(maxEvents) {
|
|
716
|
-
this.
|
|
751
|
+
this.ring = new RingBuffer(maxEvents ?? 1e4);
|
|
717
752
|
}
|
|
718
753
|
append(event) {
|
|
719
|
-
this.
|
|
720
|
-
if (this.events.length > this.maxEvents) {
|
|
721
|
-
this.events = this.events.slice(-this.maxEvents);
|
|
722
|
-
}
|
|
754
|
+
this.ring.push(event);
|
|
723
755
|
}
|
|
724
756
|
query(filter) {
|
|
725
|
-
let results =
|
|
757
|
+
let results = this.ring.toArray();
|
|
726
758
|
if (filter.type) {
|
|
727
759
|
const types = Array.isArray(filter.type) ? filter.type : [filter.type];
|
|
728
760
|
results = results.filter((e) => types.includes(e.type));
|
|
@@ -746,29 +778,29 @@ class InMemoryAuditStorage {
|
|
|
746
778
|
return results.slice(offset, offset + limit);
|
|
747
779
|
}
|
|
748
780
|
count() {
|
|
749
|
-
return this.
|
|
781
|
+
return this.ring.length;
|
|
750
782
|
}
|
|
751
783
|
verifyIntegrity() {
|
|
752
|
-
|
|
784
|
+
const events = this.ring.toArray();
|
|
785
|
+
if (events.length === 0) {
|
|
753
786
|
return { valid: true, totalEvents: 0, chainComplete: true };
|
|
754
787
|
}
|
|
755
|
-
for (let i = 0;i <
|
|
756
|
-
const event =
|
|
788
|
+
for (let i = 0;i < events.length; i++) {
|
|
789
|
+
const event = events[i];
|
|
757
790
|
const expectedHash = computeEventHash(event, event.previousHash);
|
|
758
791
|
if (event.hash !== expectedHash) {
|
|
759
|
-
return { valid: false, totalEvents:
|
|
792
|
+
return { valid: false, totalEvents: events.length, brokenAt: i };
|
|
760
793
|
}
|
|
761
|
-
if (i > 0 && event.previousHash !==
|
|
762
|
-
return { valid: false, totalEvents:
|
|
794
|
+
if (i > 0 && event.previousHash !== events[i - 1].hash) {
|
|
795
|
+
return { valid: false, totalEvents: events.length, brokenAt: i };
|
|
763
796
|
}
|
|
764
797
|
}
|
|
765
|
-
const chainComplete =
|
|
766
|
-
return { valid: true, totalEvents:
|
|
798
|
+
const chainComplete = events[0].previousHash === ZERO_HASH;
|
|
799
|
+
return { valid: true, totalEvents: events.length, chainComplete };
|
|
767
800
|
}
|
|
768
801
|
getLastHash() {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
return this.events[this.events.length - 1].hash;
|
|
802
|
+
const last = this.ring.last();
|
|
803
|
+
return last ? last.hash : ZERO_HASH;
|
|
772
804
|
}
|
|
773
805
|
}
|
|
774
806
|
function createAuditTrail(config) {
|
|
@@ -776,7 +808,7 @@ function createAuditTrail(config) {
|
|
|
776
808
|
const storage = config?.storage && typeof config.storage !== "string" ? config.storage : new InMemoryAuditStorage(config?.maxEvents);
|
|
777
809
|
let sequenceId = 0;
|
|
778
810
|
let idCounter = 0;
|
|
779
|
-
let previousHash =
|
|
811
|
+
let previousHash = ZERO_HASH;
|
|
780
812
|
let isReady = true;
|
|
781
813
|
let readyPromise = Promise.resolve();
|
|
782
814
|
if (useHashChain && storage.getLastHash) {
|
|
@@ -792,18 +824,25 @@ function createAuditTrail(config) {
|
|
|
792
824
|
});
|
|
793
825
|
}
|
|
794
826
|
}
|
|
795
|
-
|
|
827
|
+
const batchConfig = config?.batch;
|
|
828
|
+
const batchSize = batchConfig?.size ?? 100;
|
|
829
|
+
const batchIntervalMs = batchConfig?.intervalMs ?? 50;
|
|
830
|
+
const isBatched = !!batchConfig;
|
|
831
|
+
const pendingBuffer = [];
|
|
832
|
+
let flushTimer = null;
|
|
833
|
+
const flushPromise = Promise.resolve();
|
|
834
|
+
function buildAndAppend(entry) {
|
|
796
835
|
sequenceId++;
|
|
797
836
|
idCounter++;
|
|
798
837
|
const event = {
|
|
799
|
-
id: `audit_${idCounter.toString(36)}_${
|
|
838
|
+
id: `audit_${idCounter.toString(36)}_${entry.timestamp.toString(36)}`,
|
|
800
839
|
sequenceId,
|
|
801
|
-
type,
|
|
802
|
-
timestamp:
|
|
803
|
-
actor:
|
|
804
|
-
traceId:
|
|
805
|
-
data,
|
|
806
|
-
previousHash: useHashChain ? previousHash :
|
|
840
|
+
type: entry.type,
|
|
841
|
+
timestamp: entry.timestamp,
|
|
842
|
+
actor: entry.actor,
|
|
843
|
+
traceId: entry.traceId,
|
|
844
|
+
data: entry.data,
|
|
845
|
+
previousHash: useHashChain ? previousHash : ZERO_HASH
|
|
807
846
|
};
|
|
808
847
|
const hash = useHashChain ? computeEventHash(event, event.previousHash) : createHash("sha256").update(JSON.stringify(event)).digest("hex");
|
|
809
848
|
const finalEvent = { ...event, hash };
|
|
@@ -815,26 +854,77 @@ function createAuditTrail(config) {
|
|
|
815
854
|
result.catch((err2) => config?.onError?.(err2));
|
|
816
855
|
}
|
|
817
856
|
}
|
|
857
|
+
function drainBuffer() {
|
|
858
|
+
let entry = pendingBuffer.shift();
|
|
859
|
+
while (entry) {
|
|
860
|
+
buildAndAppend(entry);
|
|
861
|
+
entry = pendingBuffer.shift();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (isBatched) {
|
|
865
|
+
flushTimer = setInterval(() => {
|
|
866
|
+
if (pendingBuffer.length > 0)
|
|
867
|
+
drainBuffer();
|
|
868
|
+
}, batchIntervalMs);
|
|
869
|
+
if (typeof flushTimer === "object" && "unref" in flushTimer) {
|
|
870
|
+
flushTimer.unref();
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
function logEntry(type, data, options) {
|
|
874
|
+
const entry = {
|
|
875
|
+
type,
|
|
876
|
+
data,
|
|
877
|
+
timestamp: Date.now(),
|
|
878
|
+
actor: options?.actor,
|
|
879
|
+
traceId: options?.traceId
|
|
880
|
+
};
|
|
881
|
+
if (isBatched) {
|
|
882
|
+
pendingBuffer.push(entry);
|
|
883
|
+
if (pendingBuffer.length >= batchSize)
|
|
884
|
+
drainBuffer();
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
buildAndAppend(entry);
|
|
888
|
+
}
|
|
818
889
|
return {
|
|
819
890
|
log(type, data, options) {
|
|
820
891
|
if (isReady) {
|
|
821
|
-
|
|
892
|
+
logEntry(type, data, options);
|
|
822
893
|
} else {
|
|
823
|
-
readyPromise = readyPromise.then(() =>
|
|
894
|
+
readyPromise = readyPromise.then(() => logEntry(type, data, options));
|
|
824
895
|
}
|
|
825
896
|
},
|
|
826
897
|
ready() {
|
|
827
898
|
return readyPromise;
|
|
828
899
|
},
|
|
900
|
+
async flush() {
|
|
901
|
+
await readyPromise;
|
|
902
|
+
drainBuffer();
|
|
903
|
+
await flushPromise;
|
|
904
|
+
},
|
|
905
|
+
dispose() {
|
|
906
|
+
if (flushTimer) {
|
|
907
|
+
clearInterval(flushTimer);
|
|
908
|
+
flushTimer = null;
|
|
909
|
+
}
|
|
910
|
+
drainBuffer();
|
|
911
|
+
},
|
|
829
912
|
async query(filter) {
|
|
913
|
+
if (isBatched)
|
|
914
|
+
drainBuffer();
|
|
830
915
|
return storage.query(filter);
|
|
831
916
|
},
|
|
832
917
|
async verifyIntegrity() {
|
|
918
|
+
if (isBatched)
|
|
919
|
+
drainBuffer();
|
|
833
920
|
return storage.verifyIntegrity();
|
|
834
921
|
},
|
|
835
922
|
get count() {
|
|
836
923
|
const result = storage.count();
|
|
837
|
-
return typeof result === "number" ? result : 0;
|
|
924
|
+
return (typeof result === "number" ? result : 0) + pendingBuffer.length;
|
|
925
|
+
},
|
|
926
|
+
get pending() {
|
|
927
|
+
return pendingBuffer.length;
|
|
838
928
|
}
|
|
839
929
|
};
|
|
840
930
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elsium-ai/observe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Observability, tracing, and cost tracking for ElsiumAI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Eric Utrera <ebutrera9103@gmail.com>",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"dev": "bun --watch src/index.ts"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@elsium-ai/core": "^0.
|
|
29
|
+
"@elsium-ai/core": "^0.7.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"typescript": "^5.7.0"
|