@michalszymanski-ai/otel-helpers
v0.1.0
Published
Tiny TypeScript helpers for native OpenTelemetry spans and GenAI semantic conventions
Downloads
47
Maintainers
Readme
@michalszymanski-ai/otel-helpers
Tiny TypeScript helpers for native OpenTelemetry.
- Add spans with minimal diff impact
- Record LLM costs
npm i @michalszymanski-ai/otel-helpers @opentelemetry/apiwithSpan
withSpan allows you to create a span around a function in a way that is easy to review.
Here is the same handleCheckout instrumented two ways.
Original:
async function handleCheckout(orderId: string) {
const order = await loadOrder(orderId);
await chargeCard(order);
return order;
}Vanilla OpenTelemetry SDK:
--- original.ts
+++ manual.ts
@@ -1,5 +1,20 @@
+import { trace, SpanStatusCode } from "@opentelemetry/api";
+
+const tracer = trace.getTracer("checkout");
+
async function handleCheckout(orderId: string) {
- const order = await loadOrder(orderId);
- await chargeCard(order);
- return order;
+ return tracer.startActiveSpan("api.checkout", async (span) => {
+ try {
+ span.setAttribute("order.id", orderId);
+ const order = await loadOrder(orderId);
+ await chargeCard(order);
+ return order;
+ } catch (err) {
+ span.recordException(err as Error);
+ span.setStatus({ code: SpanStatusCode.ERROR });
+ throw err;
+ } finally {
+ span.end();
+ }
+ });
}Notice that:
- The entire function is indented, creating a long diff hunk.
- The reviewer needs to visually compare versions to analyze changes.
- The try/catch block creates code changes around the key logic of the function.
Here's the diff produced by withSpan:
--- original.ts
+++ helper.ts
@@ -1,5 +1,8 @@
+import { withSpan } from "@michalszymanski-ai/otel-helpers";
+
+const handleCheckout = withSpan("api.checkout", async (span, orderId: string) => {
+ span.setAttribute("order.id", orderId);
const order = await loadOrder(orderId);
await chargeCard(order);
return order;
-}
+});Notice that:
- The diff is now purely additive
- It is easy to identify the scope of the changes and their potential impact.
GenAI spans
- Creates spans and metrics for LLM calls
- Respects OTel Semantic conventions
import { withGenAiSpan, recordGenAiUsage, anthropicUsage } from "@michalszymanski-ai/otel-helpers";
const response = await withGenAiSpan(
{
operation: "chat",
provider: "anthropic",
requestModel: MODEL,
useCase: "stylist",
callSite: "initial",
},
async (span) => {
const res = await client.messages.create({
model: MODEL,
max_tokens: 2048,
system: SYSTEM_PROMPT,
tools,
messages,
});
recordGenAiUsage(span, anthropicUsage(res));
return res;
},
);The package emits current OpenTelemetry GenAI semantic convention attributes:
gen_ai.operation.namegen_ai.provider.namegen_ai.request.modelgen_ai.response.modelgen_ai.usage.input_tokensgen_ai.usage.output_tokensgen_ai.usage.cache_read.input_tokensgen_ai.usage.cache_creation.input_tokensgen_ai.usage.reasoning.output_tokensgen_ai.response.finish_reasons
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.*.
API reference
All exports live in src/index.ts. The full surface:
Spans (src/span.ts)
| Export | Signature | Notes |
| --- | --- | --- |
| withSpan | withSpan(name, fn, opts?) | Wraps fn in a span; ends it in finally. Records exception + ERROR status on throw. |
| recordSpanError | recordSpanError(span, err) | Adds the exception event + ERROR status + error.type attribute. |
| spanErrorAttributes | spanErrorAttributes(err) | Returns { "error.type": "..." } derived from Error.name, err.code, or the value's type name. Useful when you want to attach error info to a parent span without ending it. |
GenAI helpers (src/gen-ai.ts)
| Export | Signature | Notes |
| --- | --- | --- |
| withGenAiSpan | withGenAiSpan(config, fn) | withSpan + GenAI semantic convention attributes (gen_ai.*). Span kind is CLIENT. |
| recordGenAiUsage | recordGenAiUsage(span, usage) | Sets the token-count attributes on an existing span. |
| createGenAiMetrics | createGenAiMetrics(meter?) | Returns the two histogram instruments (gen_ai.client.token.usage, gen_ai.client.operation.duration). |
| recordGenAiMetrics | recordGenAiMetrics(metrics, attrs, usage, durationMs) | Records token + duration histograms. |
| anthropicUsage | anthropicUsage(response) | Parses an Anthropic Messages SDK response into a GenAiUsage. |
| openAiUsage | openAiUsage(response) | Parses an OpenAI Chat Completions response into a GenAiUsage. |
| estimateGenAiCostUsd | estimateGenAiCostUsd(usage, pricing) | Returns (input_tokens × pricing.inputUsdPer1M + output_tokens × pricing.outputUsdPer1M) / 1_000_000. |
| genAiSpanName | genAiSpanName(config) | "operation model" or just "operation" if no model set. Useful when you create the span manually. |
| genAiAttributes | genAiAttributes(config, usage?) | Pure helper — returns the OTel attribute map. |
All functions accept an optional tracer / meter argument so you can scope
instrumentation to a custom OTel SDK. When omitted they use
trace.getTracer("@michalszymanski-ai/otel-helpers").
How this fits with Superlog
This package is consumed by the
superlog-skills
onboarding skills (otel-nextjs-style, otel-expo-style,
otel-supabase-edge-style) so an AI agent instrumenting a customer's
TypeScript app emits OTel data that the Superlog Evidence Spine and MCP
server understand. If you want to query the resulting telemetry from an
agent later, see
superlog/docs/users/mcp-integration.md.
The package is published under
@michalszymanski-ai/otel-helpersuntil the@superlognpm scope is coordinated. The skill SKILL.md files still reference the historical@superlog/otel-helpersname in a few places — tracked insuperlog/docs/operators/production-readiness.md.
Publishing a new version
Tag a release. GitHub Actions publishes to npm with provenance:
npm version patch # or minor / major
git push origin main --tagsLicense
MIT
