@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 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
+ [![npm](https://img.shields.io/npm/v/@superlog/otel-helpers?color=2E4BFF&label=npm)](https://www.npmjs.com/package/@superlog/otel-helpers)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@superlog/otel-helpers?color=2E4BFF)](https://www.npmjs.com/package/@superlog/otel-helpers)
5
+ [![CI](https://img.shields.io/github/actions/workflow/status/superloglabs/otel-helpers/ci.yml?branch=main&label=CI)](https://github.com/superloglabs/otel-helpers/actions)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-2E4BFF)](LICENSE)
7
+ [![Node >=18](https://img.shields.io/badge/node-%3E%3D18-2E4BFF)](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
@@ -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":"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"}
@@ -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
@@ -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
+ }