@superlog/otel-helpers 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/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/gen-ai.d.ts +59 -0
- package/dist/gen-ai.js +113 -0
- package/dist/gen-ai.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/span.d.ts +9 -0
- package/dist/span.js +44 -0
- package/dist/span.js.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Superlog
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# @superlog/otel-helpers
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@superlog/otel-helpers)
|
|
4
|
+
[](https://www.npmjs.com/package/@superlog/otel-helpers)
|
|
5
|
+
[](https://github.com/superloglabs/otel-helpers/actions)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](package.json)
|
|
8
|
+
|
|
9
|
+
Tiny TypeScript helpers for native OpenTelemetry.
|
|
10
|
+
|
|
11
|
+
- Add spans with minimal diff impact
|
|
12
|
+
- Record LLM costs
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm i @superlog/otel-helpers @opentelemetry/api
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## `withSpan`
|
|
19
|
+
|
|
20
|
+
`withSpan` allows you to create a span around a function in a way that is easy to review.
|
|
21
|
+
Here is the same `handleCheckout` instrumented two ways.
|
|
22
|
+
|
|
23
|
+
Original:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
async function handleCheckout(orderId: string) {
|
|
27
|
+
const order = await loadOrder(orderId);
|
|
28
|
+
await chargeCard(order);
|
|
29
|
+
return order;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Vanilla OpenTelemetry SDK:
|
|
34
|
+
|
|
35
|
+
```diff
|
|
36
|
+
--- original.ts
|
|
37
|
+
+++ manual.ts
|
|
38
|
+
@@ -1,5 +1,20 @@
|
|
39
|
+
+import { trace, SpanStatusCode } from "@opentelemetry/api";
|
|
40
|
+
+
|
|
41
|
+
+const tracer = trace.getTracer("checkout");
|
|
42
|
+
+
|
|
43
|
+
async function handleCheckout(orderId: string) {
|
|
44
|
+
- const order = await loadOrder(orderId);
|
|
45
|
+
- await chargeCard(order);
|
|
46
|
+
- return order;
|
|
47
|
+
+ return tracer.startActiveSpan("api.checkout", async (span) => {
|
|
48
|
+
+ try {
|
|
49
|
+
+ span.setAttribute("order.id", orderId);
|
|
50
|
+
+ const order = await loadOrder(orderId);
|
|
51
|
+
+ await chargeCard(order);
|
|
52
|
+
+ return order;
|
|
53
|
+
+ } catch (err) {
|
|
54
|
+
+ span.recordException(err as Error);
|
|
55
|
+
+ span.setStatus({ code: SpanStatusCode.ERROR });
|
|
56
|
+
+ throw err;
|
|
57
|
+
+ } finally {
|
|
58
|
+
+ span.end();
|
|
59
|
+
+ }
|
|
60
|
+
+ });
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Notice that:
|
|
65
|
+
- The entire function is indented, creating a long diff hunk.
|
|
66
|
+
- The reviewer needs to visually compare versions to analyze changes.
|
|
67
|
+
- The try/catch block creates code changes around the key logic of the function.
|
|
68
|
+
|
|
69
|
+
Here's the diff produced by `withSpan`:
|
|
70
|
+
|
|
71
|
+
```diff
|
|
72
|
+
--- original.ts
|
|
73
|
+
+++ helper.ts
|
|
74
|
+
@@ -1,5 +1,8 @@
|
|
75
|
+
-async function handleCheckout(orderId: string) {
|
|
76
|
+
+import { withSpan } from "@superlog/otel-helpers";
|
|
77
|
+
+
|
|
78
|
+
+const handleCheckout = withSpan("api.checkout", async (span, orderId: string) => {
|
|
79
|
+
+ span.setAttribute("order.id", orderId);
|
|
80
|
+
const order = await loadOrder(orderId);
|
|
81
|
+
await chargeCard(order);
|
|
82
|
+
return order;
|
|
83
|
+
-}
|
|
84
|
+
+});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- The diff is now purely additive
|
|
88
|
+
- It is easy to identify the scope of the changes and their potential impact.
|
|
89
|
+
|
|
90
|
+
## GenAI spans
|
|
91
|
+
|
|
92
|
+
- Creates spans and metrics for LLM calls
|
|
93
|
+
- Respects OTel Semantic conventions
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { withGenAiSpan, recordGenAiUsage, anthropicUsage } from "@superlog/otel-helpers";
|
|
97
|
+
|
|
98
|
+
const response = await withGenAiSpan(
|
|
99
|
+
{
|
|
100
|
+
operation: "chat",
|
|
101
|
+
provider: "anthropic",
|
|
102
|
+
requestModel: MODEL,
|
|
103
|
+
useCase: "stylist",
|
|
104
|
+
callSite: "initial",
|
|
105
|
+
},
|
|
106
|
+
async (span) => {
|
|
107
|
+
const res = await client.messages.create({
|
|
108
|
+
model: MODEL,
|
|
109
|
+
max_tokens: 2048,
|
|
110
|
+
system: SYSTEM_PROMPT,
|
|
111
|
+
tools,
|
|
112
|
+
messages,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
recordGenAiUsage(span, anthropicUsage(res));
|
|
116
|
+
return res;
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The package emits current OpenTelemetry GenAI semantic convention attributes:
|
|
122
|
+
|
|
123
|
+
- `gen_ai.operation.name`
|
|
124
|
+
- `gen_ai.provider.name`
|
|
125
|
+
- `gen_ai.request.model`
|
|
126
|
+
- `gen_ai.response.model`
|
|
127
|
+
- `gen_ai.usage.input_tokens`
|
|
128
|
+
- `gen_ai.usage.output_tokens`
|
|
129
|
+
- `gen_ai.response.finish_reasons`
|
|
130
|
+
|
|
131
|
+
The GenAI semantic conventions are currently marked Development by OpenTelemetry. This package follows the current names, and keeps product-specific dimensions under `app.gen_ai.*`.
|
|
132
|
+
|
|
133
|
+
## Publishing a new version
|
|
134
|
+
|
|
135
|
+
Tag a release. GitHub Actions publishes to npm with provenance:
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
pnpm version patch # or minor / major
|
|
139
|
+
git push origin main --tags
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT
|
package/dist/gen-ai.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Attributes, type Histogram, type Meter, type Span } from "@opentelemetry/api";
|
|
2
|
+
import { type WithSpanOptions } from "./span.js";
|
|
3
|
+
export type GenAiOperationName = "chat" | "create_agent" | "embeddings" | "execute_tool" | "generate_content" | "invoke_agent" | "invoke_workflow" | "retrieval" | "text_completion" | (string & {});
|
|
4
|
+
export type GenAiProviderName = "anthropic" | "aws.bedrock" | "azure.ai.inference" | "azure.ai.openai" | "cohere" | "deepseek" | "gcp.gemini" | "gcp.gen_ai" | "gcp.vertex_ai" | "groq" | "ibm.watsonx.ai" | "mistral_ai" | "openai" | "perplexity" | "x_ai" | (string & {});
|
|
5
|
+
export type GenAiOutputType = "image" | "json" | "speech" | "text" | (string & {});
|
|
6
|
+
export type GenAiTokenType = "input" | "output";
|
|
7
|
+
export type GenAiSpanConfig = {
|
|
8
|
+
operation: GenAiOperationName;
|
|
9
|
+
provider: GenAiProviderName;
|
|
10
|
+
requestModel?: string;
|
|
11
|
+
responseModel?: string;
|
|
12
|
+
conversationId?: string;
|
|
13
|
+
outputType?: GenAiOutputType;
|
|
14
|
+
useCase?: string;
|
|
15
|
+
callSite?: string;
|
|
16
|
+
attributes?: Attributes;
|
|
17
|
+
spanName?: string;
|
|
18
|
+
};
|
|
19
|
+
export type GenAiUsage = {
|
|
20
|
+
inputTokens?: number;
|
|
21
|
+
outputTokens?: number;
|
|
22
|
+
cacheReadInputTokens?: number;
|
|
23
|
+
cacheCreationInputTokens?: number;
|
|
24
|
+
reasoningOutputTokens?: number;
|
|
25
|
+
responseModel?: string;
|
|
26
|
+
finishReasons?: string[];
|
|
27
|
+
costUsd?: number;
|
|
28
|
+
};
|
|
29
|
+
export type GenAiCostInput = {
|
|
30
|
+
inputTokens?: number;
|
|
31
|
+
outputTokens?: number;
|
|
32
|
+
inputUsdPer1M: number;
|
|
33
|
+
outputUsdPer1M: number;
|
|
34
|
+
};
|
|
35
|
+
export type GenAiMetrics = {
|
|
36
|
+
tokenUsage: Histogram;
|
|
37
|
+
operationDuration: Histogram;
|
|
38
|
+
};
|
|
39
|
+
export type GenAiMetricRecorder = {
|
|
40
|
+
metrics: GenAiMetrics;
|
|
41
|
+
operation: GenAiOperationName;
|
|
42
|
+
provider: GenAiProviderName;
|
|
43
|
+
requestModel?: string;
|
|
44
|
+
responseModel?: string;
|
|
45
|
+
useCase?: string;
|
|
46
|
+
callSite?: string;
|
|
47
|
+
durationSeconds?: number;
|
|
48
|
+
inputTokens?: number;
|
|
49
|
+
outputTokens?: number;
|
|
50
|
+
};
|
|
51
|
+
export declare function genAiSpanName(config: Pick<GenAiSpanConfig, "operation" | "requestModel">): string;
|
|
52
|
+
export declare function genAiAttributes(config: GenAiSpanConfig & GenAiUsage): Attributes;
|
|
53
|
+
export declare function withGenAiSpan<TResult>(config: GenAiSpanConfig, fn: (span: Span) => TResult | Promise<TResult>, options?: WithSpanOptions): Promise<Awaited<TResult>>;
|
|
54
|
+
export declare function recordGenAiUsage(span: Span, usage: GenAiUsage): void;
|
|
55
|
+
export declare function createGenAiMetrics(meter?: Meter): GenAiMetrics;
|
|
56
|
+
export declare function recordGenAiMetrics(input: GenAiMetricRecorder): void;
|
|
57
|
+
export declare function estimateGenAiCostUsd(input: GenAiCostInput): number;
|
|
58
|
+
export declare function anthropicUsage(response: unknown): GenAiUsage;
|
|
59
|
+
export declare function openAiUsage(response: unknown): GenAiUsage;
|
package/dist/gen-ai.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { SpanKind, metrics, } from "@opentelemetry/api";
|
|
2
|
+
import { withSpan } from "./span.js";
|
|
3
|
+
const defaultMeter = metrics.getMeter("@superlog/otel-helpers");
|
|
4
|
+
export function genAiSpanName(config) {
|
|
5
|
+
return config.requestModel ? `${config.operation} ${config.requestModel}` : config.operation;
|
|
6
|
+
}
|
|
7
|
+
export function genAiAttributes(config) {
|
|
8
|
+
return compactAttributes({
|
|
9
|
+
"gen_ai.operation.name": config.operation,
|
|
10
|
+
"gen_ai.provider.name": config.provider,
|
|
11
|
+
"gen_ai.request.model": config.requestModel,
|
|
12
|
+
"gen_ai.response.model": config.responseModel,
|
|
13
|
+
"gen_ai.conversation.id": config.conversationId,
|
|
14
|
+
"gen_ai.output.type": config.outputType,
|
|
15
|
+
"gen_ai.usage.input_tokens": config.inputTokens,
|
|
16
|
+
"gen_ai.usage.output_tokens": config.outputTokens,
|
|
17
|
+
"gen_ai.usage.cache_read.input_tokens": config.cacheReadInputTokens,
|
|
18
|
+
"gen_ai.usage.cache_creation.input_tokens": config.cacheCreationInputTokens,
|
|
19
|
+
"gen_ai.usage.reasoning.output_tokens": config.reasoningOutputTokens,
|
|
20
|
+
"gen_ai.response.finish_reasons": config.finishReasons,
|
|
21
|
+
"app.gen_ai.use_case": config.useCase,
|
|
22
|
+
"app.gen_ai.call_site": config.callSite,
|
|
23
|
+
"app.gen_ai.cost_usd": config.costUsd,
|
|
24
|
+
...config.attributes,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export async function withGenAiSpan(config, fn, options = {}) {
|
|
28
|
+
return withSpan(config.spanName ?? genAiSpanName(config), fn, {
|
|
29
|
+
...options,
|
|
30
|
+
kind: options.kind ?? SpanKind.CLIENT,
|
|
31
|
+
attributes: {
|
|
32
|
+
...genAiAttributes(config),
|
|
33
|
+
...options.attributes,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function recordGenAiUsage(span, usage) {
|
|
38
|
+
span.setAttributes(genAiAttributes({ operation: "", provider: "", ...usage }));
|
|
39
|
+
}
|
|
40
|
+
export function createGenAiMetrics(meter = defaultMeter) {
|
|
41
|
+
return {
|
|
42
|
+
tokenUsage: meter.createHistogram("gen_ai.client.token.usage", {
|
|
43
|
+
description: "Measures number of input and output tokens used.",
|
|
44
|
+
unit: "{token}",
|
|
45
|
+
}),
|
|
46
|
+
operationDuration: meter.createHistogram("gen_ai.client.operation.duration", {
|
|
47
|
+
description: "GenAI operation duration.",
|
|
48
|
+
unit: "s",
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function recordGenAiMetrics(input) {
|
|
53
|
+
const attrs = compactAttributes({
|
|
54
|
+
"gen_ai.operation.name": input.operation,
|
|
55
|
+
"gen_ai.provider.name": input.provider,
|
|
56
|
+
"gen_ai.request.model": input.requestModel,
|
|
57
|
+
"gen_ai.response.model": input.responseModel,
|
|
58
|
+
"app.gen_ai.use_case": input.useCase,
|
|
59
|
+
"app.gen_ai.call_site": input.callSite,
|
|
60
|
+
});
|
|
61
|
+
if (input.inputTokens !== undefined) {
|
|
62
|
+
input.metrics.tokenUsage.record(input.inputTokens, {
|
|
63
|
+
...attrs,
|
|
64
|
+
"gen_ai.token.type": "input",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (input.outputTokens !== undefined) {
|
|
68
|
+
input.metrics.tokenUsage.record(input.outputTokens, {
|
|
69
|
+
...attrs,
|
|
70
|
+
"gen_ai.token.type": "output",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (input.durationSeconds !== undefined) {
|
|
74
|
+
input.metrics.operationDuration.record(input.durationSeconds, attrs);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function estimateGenAiCostUsd(input) {
|
|
78
|
+
return (((input.inputTokens ?? 0) * input.inputUsdPer1M) / 1_000_000 +
|
|
79
|
+
((input.outputTokens ?? 0) * input.outputUsdPer1M) / 1_000_000);
|
|
80
|
+
}
|
|
81
|
+
export function anthropicUsage(response) {
|
|
82
|
+
const usage = getObject(getObject(response).usage);
|
|
83
|
+
const stopReason = getObject(response).stop_reason;
|
|
84
|
+
return {
|
|
85
|
+
inputTokens: numberOrUndefined(usage.input_tokens),
|
|
86
|
+
outputTokens: numberOrUndefined(usage.output_tokens),
|
|
87
|
+
cacheReadInputTokens: numberOrUndefined(usage.cache_read_input_tokens),
|
|
88
|
+
cacheCreationInputTokens: numberOrUndefined(usage.cache_creation_input_tokens),
|
|
89
|
+
finishReasons: typeof stopReason === "string" ? [stopReason] : undefined,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function openAiUsage(response) {
|
|
93
|
+
const obj = getObject(response);
|
|
94
|
+
const usage = getObject(obj.usage);
|
|
95
|
+
const choice = Array.isArray(obj.choices) ? getObject(obj.choices[0]) : {};
|
|
96
|
+
const finishReason = choice.finish_reason;
|
|
97
|
+
return {
|
|
98
|
+
inputTokens: numberOrUndefined(usage.prompt_tokens),
|
|
99
|
+
outputTokens: numberOrUndefined(usage.completion_tokens),
|
|
100
|
+
responseModel: typeof obj.model === "string" ? obj.model : undefined,
|
|
101
|
+
finishReasons: typeof finishReason === "string" ? [finishReason] : undefined,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function compactAttributes(attrs) {
|
|
105
|
+
return Object.fromEntries(Object.entries(attrs).filter(([, value]) => value !== undefined && value !== null && value !== ""));
|
|
106
|
+
}
|
|
107
|
+
function numberOrUndefined(value) {
|
|
108
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
109
|
+
}
|
|
110
|
+
function getObject(value) {
|
|
111
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=gen-ai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gen-ai.js","sourceRoot":"","sources":["../src/gen-ai.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,OAAO,GAKR,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,QAAQ,EAAwB,MAAM,WAAW,CAAC;AAoF3D,MAAM,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAC,CAAC;AAEhE,MAAM,UAAU,aAAa,CAAC,MAA2D;IACvF,OAAO,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC;AAC/F,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAoC;IAClE,OAAO,iBAAiB,CAAC;QACvB,uBAAuB,EAAE,MAAM,CAAC,SAAS;QACzC,sBAAsB,EAAE,MAAM,CAAC,QAAQ;QACvC,sBAAsB,EAAE,MAAM,CAAC,YAAY;QAC3C,uBAAuB,EAAE,MAAM,CAAC,aAAa;QAC7C,wBAAwB,EAAE,MAAM,CAAC,cAAc;QAC/C,oBAAoB,EAAE,MAAM,CAAC,UAAU;QACvC,2BAA2B,EAAE,MAAM,CAAC,WAAW;QAC/C,4BAA4B,EAAE,MAAM,CAAC,YAAY;QACjD,sCAAsC,EAAE,MAAM,CAAC,oBAAoB;QACnE,0CAA0C,EAAE,MAAM,CAAC,wBAAwB;QAC3E,sCAAsC,EAAE,MAAM,CAAC,qBAAqB;QACpE,gCAAgC,EAAE,MAAM,CAAC,aAAa;QACtD,qBAAqB,EAAE,MAAM,CAAC,OAAO;QACrC,sBAAsB,EAAE,MAAM,CAAC,QAAQ;QACvC,qBAAqB,EAAE,MAAM,CAAC,OAAO;QACrC,GAAG,MAAM,CAAC,UAAU;KACrB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAAuB,EACvB,EAA8C,EAC9C,UAA2B,EAAE;IAE7B,OAAO,QAAQ,CAAC,MAAM,CAAC,QAAQ,IAAI,aAAa,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE;QAC5D,GAAG,OAAO;QACV,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,QAAQ,CAAC,MAAM;QACrC,UAAU,EAAE;YACV,GAAG,eAAe,CAAC,MAAM,CAAC;YAC1B,GAAG,OAAO,CAAC,UAAU;SACtB;KACF,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,IAAU,EAAE,KAAiB;IAC5D,IAAI,CAAC,aAAa,CAAC,eAAe,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC;AACjF,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,QAAe,YAAY;IAC5D,OAAO;QACL,UAAU,EAAE,KAAK,CAAC,eAAe,CAAC,2BAA2B,EAAE;YAC7D,WAAW,EAAE,kDAAkD;YAC/D,IAAI,EAAE,SAAS;SAChB,CAAC;QACF,iBAAiB,EAAE,KAAK,CAAC,eAAe,CAAC,kCAAkC,EAAE;YAC3E,WAAW,EAAE,2BAA2B;YACxC,IAAI,EAAE,GAAG;SACV,CAAC;KACH,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAA0B;IAC3D,MAAM,KAAK,GAAG,iBAAiB,CAAC;QAC9B,uBAAuB,EAAE,KAAK,CAAC,SAAS;QACxC,sBAAsB,EAAE,KAAK,CAAC,QAAQ;QACtC,sBAAsB,EAAE,KAAK,CAAC,YAAY;QAC1C,uBAAuB,EAAE,KAAK,CAAC,aAAa;QAC5C,qBAAqB,EAAE,KAAK,CAAC,OAAO;QACpC,sBAAsB,EAAE,KAAK,CAAC,QAAQ;KACvC,CAAC,CAAC;IAEH,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACpC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE;YACjD,GAAG,KAAK;YACR,mBAAmB,EAAE,OAAO;SAC7B,CAAC,CAAC;IACL,CAAC;IACD,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACrC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,EAAE;YAClD,GAAG,KAAK;YACR,mBAAmB,EAAE,QAAQ;SAC9B,CAAC,CAAC;IACL,CAAC;IACD,IAAI,KAAK,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;QACxC,KAAK,CAAC,OAAO,CAAC,iBAAiB,CAAC,MAAM,CAAC,KAAK,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC;IACvE,CAAC;AACH,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,KAAqB;IACxD,OAAO,CACL,CAAC,CAAC,KAAK,CAAC,WAAW,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,aAAa,CAAC,GAAG,SAAS;QAC5D,CAAC,CAAC,KAAK,CAAC,YAAY,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,cAAc,CAAC,GAAG,SAAS,CAC/D,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,QAAiB;IAC9C,MAAM,KAAK,GAAG,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC;IACnD,MAAM,UAAU,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC,WAAW,CAAC;IACnD,OAAO;QACL,WAAW,EAAE,iBAAiB,CAAC,KAAK,CAAC,YAAY,CAAC;QAClD,YAAY,EAAE,iBAAiB,CAAC,KAAK,CAAC,aAAa,CAAC;QACpD,oBAAoB,EAAE,iBAAiB,CAAC,KAAK,CAAC,uBAAuB,CAAC;QACtE,wBAAwB,EAAE,iBAAiB,CAAC,KAAK,CAAC,2BAA2B,CAAC;QAC9E,aAAa,EAAE,OAAO,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS;KACzE,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,QAAiB;IAC3C,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;IAChC,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3E,MAAM,YAAY,GAAG,MAAM,CAAC,aAAa,CAAC;IAC1C,OAAO;QACL,WAAW,EAAE,iBAAiB,CAAC,KAAK,CAAC,aAAa,CAAC;QACnD,YAAY,EAAE,iBAAiB,CAAC,KAAK,CAAC,iBAAiB,CAAC;QACxD,aAAa,EAAE,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;QACpE,aAAa,EAAE,OAAO,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,SAAS;KAC7E,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAiB;IAC1C,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC,CACnG,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAc;IACvC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACjF,CAAC;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAE,KAAiC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC/F,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { recordSpanError, spanErrorAttributes, withSpan, type WithSpanOptions, } from "./span.js";
|
|
2
|
+
export { anthropicUsage, createGenAiMetrics, estimateGenAiCostUsd, genAiAttributes, genAiSpanName, openAiUsage, recordGenAiMetrics, recordGenAiUsage, withGenAiSpan, type GenAiCostInput, type GenAiMetricRecorder, type GenAiMetrics, type GenAiOperationName, type GenAiOutputType, type GenAiProviderName, type GenAiSpanConfig, type GenAiTokenType, type GenAiUsage, } from "./gen-ai.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { recordSpanError, spanErrorAttributes, withSpan, } from "./span.js";
|
|
2
|
+
export { anthropicUsage, createGenAiMetrics, estimateGenAiCostUsd, genAiAttributes, genAiSpanName, openAiUsage, recordGenAiMetrics, recordGenAiUsage, withGenAiSpan, } from "./gen-ai.js";
|
|
3
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,QAAQ,GAET,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,eAAe,EACf,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,GAUd,MAAM,aAAa,CAAC"}
|
package/dist/span.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SpanKind, type Attributes, type Span, type SpanOptions, type Tracer } from "@opentelemetry/api";
|
|
2
|
+
export type WithSpanOptions = Omit<SpanOptions, "attributes" | "kind"> & {
|
|
3
|
+
attributes?: Attributes;
|
|
4
|
+
kind?: SpanKind;
|
|
5
|
+
tracer?: Tracer;
|
|
6
|
+
};
|
|
7
|
+
export declare function withSpan<TResult>(name: string, fn: (span: Span) => TResult | Promise<TResult>, options?: WithSpanOptions): Promise<Awaited<TResult>>;
|
|
8
|
+
export declare function recordSpanError(span: Span, err: unknown): void;
|
|
9
|
+
export declare function spanErrorAttributes(err: unknown): Attributes;
|
package/dist/span.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
|
|
2
|
+
const defaultTracer = trace.getTracer("@superlog/otel-helpers");
|
|
3
|
+
export async function withSpan(name, fn, options = {}) {
|
|
4
|
+
const { tracer = defaultTracer, attributes, kind = SpanKind.INTERNAL, ...spanOptions } = options;
|
|
5
|
+
const result = await tracer.startActiveSpan(name, { ...spanOptions, attributes, kind }, async (span) => {
|
|
6
|
+
try {
|
|
7
|
+
return await fn(span);
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
recordSpanError(span, err);
|
|
11
|
+
throw err;
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
span.end();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
export function recordSpanError(span, err) {
|
|
20
|
+
span.recordException(toException(err));
|
|
21
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
22
|
+
span.setAttributes(spanErrorAttributes(err));
|
|
23
|
+
}
|
|
24
|
+
export function spanErrorAttributes(err) {
|
|
25
|
+
return {
|
|
26
|
+
"error.type": errorType(err),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function toException(err) {
|
|
30
|
+
if (err instanceof Error)
|
|
31
|
+
return err;
|
|
32
|
+
return new Error(typeof err === "string" ? err : JSON.stringify(err));
|
|
33
|
+
}
|
|
34
|
+
function errorType(err) {
|
|
35
|
+
if (err instanceof Error)
|
|
36
|
+
return err.name || "Error";
|
|
37
|
+
if (typeof err === "object" && err !== null) {
|
|
38
|
+
const maybeCode = err.code;
|
|
39
|
+
if (typeof maybeCode === "string" && maybeCode.length > 0)
|
|
40
|
+
return maybeCode;
|
|
41
|
+
}
|
|
42
|
+
return typeof err;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=span.js.map
|
package/dist/span.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"span.js","sourceRoot":"","sources":["../src/span.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,cAAc,EACd,KAAK,GAKN,MAAM,oBAAoB,CAAC;AAE5B,MAAM,aAAa,GAAG,KAAK,CAAC,SAAS,CAAC,wBAAwB,CAAC,CAAC;AAQhE,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,IAAY,EACZ,EAA8C,EAC9C,UAA2B,EAAE;IAE7B,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,UAAU,EAAE,IAAI,GAAG,QAAQ,CAAC,QAAQ,EAAE,GAAG,WAAW,EAAE,GAAG,OAAO,CAAC;IAEjG,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,IAAI,EAAE,EAAE,GAAG,WAAW,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QACrG,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,eAAe,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAC3B,MAAM,GAAG,CAAC;QACZ,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,GAAG,EAAE,CAAC;QACb,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,MAA0B,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAU,EAAE,GAAY;IACtD,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;IACvC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,cAAc,CAAC,KAAK,EAAE,CAAC,CAAC;IAC/C,IAAI,CAAC,aAAa,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,GAAY;IAC9C,OAAO;QACL,YAAY,EAAE,SAAS,CAAC,GAAG,CAAC;KAC7B,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,GAAY;IAC/B,IAAI,GAAG,YAAY,KAAK;QAAE,OAAO,GAAG,CAAC;IACrC,OAAO,IAAI,KAAK,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;AACxE,CAAC;AAED,SAAS,SAAS,CAAC,GAAY;IAC7B,IAAI,GAAG,YAAY,KAAK;QAAE,OAAO,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC;IACrD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,SAAS,GAAI,GAA0B,CAAC,IAAI,CAAC;QACnD,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,SAAS,CAAC;IAC9E,CAAC;IACD,OAAO,OAAO,GAAG,CAAC;AACpB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@superlog/otel-helpers",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tiny TypeScript helpers for native OpenTelemetry spans and GenAI semantic conventions.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"homepage": "https://github.com/superloglabs/otel-helpers#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/superloglabs/otel-helpers.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/superloglabs/otel-helpers/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"opentelemetry",
|
|
18
|
+
"otel",
|
|
19
|
+
"tracing",
|
|
20
|
+
"observability",
|
|
21
|
+
"genai",
|
|
22
|
+
"llm",
|
|
23
|
+
"ai"
|
|
24
|
+
],
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@opentelemetry/api": "^1.9.0"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@opentelemetry/api": "^1.9.0",
|
|
43
|
+
"typescript": "^5.7.2",
|
|
44
|
+
"vitest": "^2.1.8"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc -p tsconfig.build.json",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"typecheck": "tsc --noEmit"
|
|
53
|
+
}
|
|
54
|
+
}
|