@lupaflow/observability 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +33 -0
- package/src/index.ts +3 -0
- package/src/logger.ts +19 -0
- package/src/span.ts +69 -0
- package/src/tracker.ts +88 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lupaflow/observability",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"require": "./dist/index.js",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@lupaflow/core": "workspace:*",
|
|
25
|
+
"@lupaflow/types": "workspace:*",
|
|
26
|
+
"pino": "^9.5.0",
|
|
27
|
+
"pino-pretty": "^13.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"tsup": "^8.3.0",
|
|
31
|
+
"typescript": "^5.6.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/index.ts
ADDED
package/src/logger.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import pino from "pino"
|
|
2
|
+
import { globalEventBus } from "@lupaflow/core"
|
|
3
|
+
|
|
4
|
+
const logger = pino({
|
|
5
|
+
transport: {
|
|
6
|
+
target: "pino-pretty",
|
|
7
|
+
options: {
|
|
8
|
+
colorize: true,
|
|
9
|
+
translateTime: "HH:MM:ss",
|
|
10
|
+
ignore: "pid,hostname",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
globalEventBus.onAny((event) => {
|
|
16
|
+
logger.debug({ event: event.name, id: event.id }, `Event: ${event.name}`)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export { logger }
|
package/src/span.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { generateId } from "@lupaflow/core"
|
|
2
|
+
|
|
3
|
+
interface Span {
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
parentId?: string
|
|
7
|
+
startTime: number
|
|
8
|
+
endTime?: number
|
|
9
|
+
status: "open" | "closed" | "error"
|
|
10
|
+
metadata?: Record<string, unknown>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class Tracer {
|
|
14
|
+
private spans: Map<string, Span> = new Map()
|
|
15
|
+
private stack: string[] = []
|
|
16
|
+
|
|
17
|
+
start(name: string, metadata?: Record<string, unknown>): string {
|
|
18
|
+
const id = generateId()
|
|
19
|
+
const parentId = this.stack[this.stack.length - 1]
|
|
20
|
+
const span: Span = {
|
|
21
|
+
id,
|
|
22
|
+
name,
|
|
23
|
+
parentId,
|
|
24
|
+
startTime: Date.now(),
|
|
25
|
+
status: "open",
|
|
26
|
+
metadata,
|
|
27
|
+
}
|
|
28
|
+
this.spans.set(id, span)
|
|
29
|
+
this.stack.push(id)
|
|
30
|
+
return id
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
end(id: string, metadata?: Record<string, unknown>): void {
|
|
34
|
+
const span = this.spans.get(id)
|
|
35
|
+
if (!span) return
|
|
36
|
+
span.endTime = Date.now()
|
|
37
|
+
span.status = "closed"
|
|
38
|
+
span.metadata = { ...span.metadata, ...metadata }
|
|
39
|
+
this.stack = this.stack.filter((s) => s !== id)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
error(id: string, error: string): void {
|
|
43
|
+
const span = this.spans.get(id)
|
|
44
|
+
if (!span) return
|
|
45
|
+
span.endTime = Date.now()
|
|
46
|
+
span.status = "error"
|
|
47
|
+
span.metadata = { ...span.metadata, error }
|
|
48
|
+
this.stack = this.stack.filter((s) => s !== id)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getTrace(id: string): Span | undefined {
|
|
52
|
+
return this.spans.get(id)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getActiveSpans(): Span[] {
|
|
56
|
+
return Array.from(this.spans.values()).filter((s) => s.status === "open")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getAllSpans(): Span[] {
|
|
60
|
+
return Array.from(this.spans.values())
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
clear(): void {
|
|
64
|
+
this.spans.clear()
|
|
65
|
+
this.stack = []
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const globalTracer = new Tracer()
|
package/src/tracker.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { globalEventBus } from "@lupaflow/core"
|
|
2
|
+
import type { LupaEvent, ProviderCompleteEvent, LLMTokenEvent } from "@lupaflow/types"
|
|
3
|
+
|
|
4
|
+
interface UsageRecord {
|
|
5
|
+
provider: string
|
|
6
|
+
model: string
|
|
7
|
+
tokens: number
|
|
8
|
+
cost: number
|
|
9
|
+
latencyMs: number
|
|
10
|
+
timestamp: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const TOKEN_COST_PER_1K: Record<string, { input: number; output: number }> = {
|
|
14
|
+
"openai/gpt-4o-mini": { input: 0.00015, output: 0.0006 },
|
|
15
|
+
"openai/gpt-4o": { input: 0.0025, output: 0.01 },
|
|
16
|
+
"llama-3.1-70b-versatile": { input: 0.00059, output: 0.00079 },
|
|
17
|
+
"llama-3.1-8b-instant": { input: 0.00005, output: 0.00008 },
|
|
18
|
+
"gemini-1.5-flash": { input: 0.000075, output: 0.0003 },
|
|
19
|
+
"gemini-1.5-pro": { input: 0.00125, output: 0.005 },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function estimateCost(provider: string, model: string, tokens: number, isOutput = false): number {
|
|
23
|
+
const key = model
|
|
24
|
+
const rates = TOKEN_COST_PER_1K[key]
|
|
25
|
+
if (!rates) return 0
|
|
26
|
+
const rate = isOutput ? rates.output : rates.input
|
|
27
|
+
return (tokens / 1000) * rate
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class UsageTracker {
|
|
31
|
+
private records: UsageRecord[] = []
|
|
32
|
+
private totalTokens = 0
|
|
33
|
+
private totalCost = 0
|
|
34
|
+
|
|
35
|
+
constructor() {
|
|
36
|
+
globalEventBus.on("provider:complete", (event: LupaEvent) => {
|
|
37
|
+
const e = event as ProviderCompleteEvent
|
|
38
|
+
const isOutput = e.name === "provider:complete"
|
|
39
|
+
const cost = estimateCost(e.provider, e.model, e.tokens, isOutput)
|
|
40
|
+
|
|
41
|
+
this.records.push({
|
|
42
|
+
provider: e.provider,
|
|
43
|
+
model: e.model,
|
|
44
|
+
tokens: e.tokens,
|
|
45
|
+
cost,
|
|
46
|
+
latencyMs: e.latencyMs,
|
|
47
|
+
timestamp: e.timestamp,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
this.totalTokens += e.tokens
|
|
51
|
+
this.totalCost += cost
|
|
52
|
+
|
|
53
|
+
globalEventBus.emit("llm:token", {
|
|
54
|
+
tokens: e.tokens,
|
|
55
|
+
cost,
|
|
56
|
+
} as LLMTokenEvent)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getTotalTokens(): number {
|
|
61
|
+
return this.totalTokens
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getTotalCost(): number {
|
|
65
|
+
return this.totalCost
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getRecords(): UsageRecord[] {
|
|
69
|
+
return [...this.records]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getSummary(): { totalTokens: number; totalCost: number; totalCalls: number; avgLatencyMs: number } {
|
|
73
|
+
const calls = this.records.length
|
|
74
|
+
const avgLatency = calls > 0 ? this.records.reduce((s, r) => s + r.latencyMs, 0) / calls : 0
|
|
75
|
+
return {
|
|
76
|
+
totalTokens: this.totalTokens,
|
|
77
|
+
totalCost: this.totalCost,
|
|
78
|
+
totalCalls: calls,
|
|
79
|
+
avgLatencyMs: Math.round(avgLatency),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
reset(): void {
|
|
84
|
+
this.records = []
|
|
85
|
+
this.totalTokens = 0
|
|
86
|
+
this.totalCost = 0
|
|
87
|
+
}
|
|
88
|
+
}
|
package/tsconfig.json
ADDED