Skip to content

TypeScript SDK

@opensearch-project/genai-observability-sdk-ts instruments JavaScript and TypeScript AI agent applications using standard OpenTelemetry. It configures the OTEL pipeline in one call, provides an observe() wrapper for tracing agents and tools, and enriches spans with GenAI semantic convention attributes. It is the TypeScript counterpart of the Python SDK.

Terminal window
npm install @opensearch-project/genai-observability-sdk-ts
# or: pnpm add @opensearch-project/genai-observability-sdk-ts
# or: yarn add @opensearch-project/genai-observability-sdk-ts

Some features rely on optional peer dependencies, installed only when you need them:

Terminal window
# OpenSearch trace retrieval (OpenSearchTraceRetriever)
npm install @opensearch-project/opensearch
# AWS SigV4 authentication (AWSSigV4OTLPExporter)
npm install @aws-sdk/credential-providers aws4

The SDK exports these functions and classes. This page covers the full API surface.

ExportPurposeDocs
register()Configure OTEL pipelineThis page
observe()Trace agents, tools, LLM callsThis page
withObserve()Block-level tracingThis page
OpOperation name constantsThis page
enrich()Set GenAI attributes on active spanThis page
score()Attach evaluation scores to tracesThis page
evaluate()Run a task against a dataset with scorersThis page
BenchmarkUpload pre-computed eval resultsThis page
OpenSearchTraceRetrieverQuery stored traces from OpenSearchThis page
AWSSigV4OTLPExporterSigV4-signed OTLP exporterThis page

For evaluation concepts (scoring traces, running experiments, comparing agent versions), see Evaluation & Scoring.

import { register, observe, Op, enrich, score } from "@opensearch-project/genai-observability-sdk-ts";
// Configure the OTEL pipeline once at startup (register is async)
await register({ endpoint: "http://localhost:4318/v1/traces", serviceName: "my-agent" });
const getWeather = observe(
{ name: "get_weather", op: Op.EXECUTE_TOOL },
(city: string) => ({ city, temp: 22, condition: "sunny" }),
);
const assistant = observe(
{ name: "assistant", op: Op.INVOKE_AGENT },
(query: string) => {
enrich({ model: "gpt-4o", provider: "openai" });
const data = getWeather("Paris");
return `${data.condition}, ${data.temp}C`;
},
);
const result = assistant("What's the weather?");

This produces the span tree:

invoke_agent assistant
└── execute_tool get_weather

Configures the OTEL tracing pipeline. Call once at startup before any tracing occurs. register() is async - await it (or chain .then()) so the exporter is ready before spans are created. It returns the configured TracerProvider.

import { register } from "@opensearch-project/genai-observability-sdk-ts";
await register({
endpoint: "http://localhost:4318/v1/traces",
serviceName: "my-app",
});
ParameterTypeDefaultDescription
endpointstringhttp://localhost:21890/opentelemetry/v1/tracesOTLP endpoint URL. Falls back to OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, then OTEL_EXPORTER_OTLP_ENDPOINT (with /v1/traces appended).
protocol"http" | "grpc"inferred from URLForce transport. grpc:// / grpcs:// -> gRPC, else HTTP.
serviceNamestring"unknown_service"Attached as service.name.
projectNamestringAlias for serviceName (used when serviceName is not set).
serviceVersionstringSets service.version.
batchbooleantruetrue = BatchSpanProcessor, false = SimpleSpanProcessor.
autoInstrumentbooleantrueDiscover and activate installed OTel instrumentor packages.
exporterSpanExporterCustom exporter. Overrides endpoint, protocol, headers.
setGlobalbooleantrueRegister as the global TracerProvider.
headersRecord<string, string>Additional headers for the OTLP exporter.

The endpoint is resolved in priority order:

  1. endpoint parameter - full URL, used as-is
  2. OTEL_EXPORTER_OTLP_TRACES_ENDPOINT - full URL, used as-is
  3. OTEL_EXPORTER_OTLP_ENDPOINT - base URL, /v1/traces appended automatically
  4. http://localhost:21890/opentelemetry/v1/traces - Data Prepper default
URL schemeTransport
http:// or https://OTLP HTTP (default)
grpc://OTLP gRPC, insecure
grpcs://OTLP gRPC with TLS

Protocol can also be forced with the protocol option, or via OTEL_EXPORTER_OTLP_TRACES_PROTOCOL / OTEL_EXPORTER_OTLP_PROTOCOL.

// Self-hosted with Data Prepper (uses the default endpoint)
await register({ serviceName: "my-agent" });
// OTel Collector on localhost
await register({ endpoint: "http://localhost:4318/v1/traces", serviceName: "my-agent" });
// gRPC
await register({ endpoint: "grpc://localhost:4317", serviceName: "my-agent" });
// AWS OpenSearch Ingestion with SigV4
import { AWSSigV4OTLPExporter } from "@opensearch-project/genai-observability-sdk-ts";
await register({
serviceName: "my-agent",
exporter: new AWSSigV4OTLPExporter({
endpoint: "https://pipeline.us-east-1.osis.amazonaws.com/v1/traces",
service: "osis",
region: "us-east-1",
}),
});

A function wrapper that creates a span around the wrapped function. Unlike Python’s @observe decorator, in TypeScript you wrap a function value. It supports three calling styles:

// 1. Bare - span name = function name, no op
const fn = observe(function myFunc(x: number) { return x * 2; });
// 2. Options + function
const agent = observe(
{ name: "planner", op: Op.INVOKE_AGENT },
(query: string) => callLlm(query),
);
// 3. Options only - returns a wrapper (decorator-factory pattern)
const withTracing = observe({ op: Op.INVOKE_AGENT });
const tracedAgent = withTracing(myAgentFunction);
ParameterTypeDefaultDescription
namestringfunction nameSpan entity name.
opstringSets gen_ai.operation.name. Span name becomes "{op} {name}" for well-known ops.
kindSpanKindINTERNALOTel SpanKind.
nameFromstringFunction parameter whose runtime value becomes the span name.
ConstantValueUse for
Op.INVOKE_AGENT"invoke_agent"Agent invocations and orchestration
Op.EXECUTE_TOOL"execute_tool"Tool/function calls
Op.CHAT"chat"LLM chat completions
Op.CREATE_AGENT"create_agent"Agent initialization
Op.RETRIEVAL"retrieval"RAG retrieval
Op.EMBEDDINGS"embeddings"Embedding generation
Op.GENERATE_CONTENT"generate_content"Content generation
Op.TEXT_COMPLETION"text_completion"Text completions

Any custom string also works for op. For well-known ops the span name is prefixed, e.g. "invoke_agent planner".

observe() automatically:

  • Captures input as gen_ai.input.messages (or gen_ai.tool.call.arguments for Op.EXECUTE_TOOL). Argument names are used as keys when available.
  • Captures output as gen_ai.output.messages (or gen_ai.tool.call.result for tools).
  • Records errors as span status ERROR with an exception event, then re-throws.
  • Sets entity attributes - gen_ai.agent.name for non-tool ops; gen_ai.tool.name + gen_ai.tool.type="function" for Op.EXECUTE_TOOL.

All captured values are truncated at 10,000 characters.

AttributeWhen set
gen_ai.operation.nameWhen op is provided
gen_ai.agent.nameAll ops except execute_tool
gen_ai.tool.nameWhen op=Op.EXECUTE_TOOL
gen_ai.tool.typeWhen op=Op.EXECUTE_TOOL (set to "function")
gen_ai.input.messages / gen_ai.output.messagesAll ops except execute_tool
gen_ai.tool.call.arguments / gen_ai.tool.call.resultWhen op=Op.EXECUTE_TOOL

When the tool name is only known at call time:

const dispatch = observe(
{ op: Op.EXECUTE_TOOL, nameFrom: "toolName" },
(toolName: string, args: Record<string, unknown>) => runTool(toolName, args),
);
dispatch("web_search", { q: "hello" });
// Produces span: "execute_tool web_search"

Sync, async, generators, and async generators are all supported. For generators, streamed values are collected and recorded as the output when iteration completes.

const asyncSearch = observe(
{ op: Op.EXECUTE_TOOL },
async (query: string) => await searchApi.query(query),
);

Block-level tracing - the TypeScript equivalent of Python’s with observe(...) context manager. It gives you direct access to the span.

import { withObserve, Op, enrich } from "@opensearch-project/genai-observability-sdk-ts";
// Sync, with options
const result = withObserve("thinking", { op: Op.CHAT }, (span) => {
span.setAttribute("custom.step", "reasoning");
enrich({ model: "gpt-4o", inputTokens: 1500 });
return callLlm(prompt);
});
// Async, name-only form
const data = await withObserve("fetch-data", async (span) => {
return await fetchFromApi();
});

Adds GenAI semantic convention attributes to the currently active span. Call it inside an observe()-wrapped function or a withObserve() block.

const callLlm = observe({ op: Op.CHAT, name: "llm_call" }, async (messages: unknown[]) => {
const response = await openai.chat.completions.create({ model: "gpt-4o", messages });
enrich({
model: "gpt-4o",
provider: "openai",
inputTokens: response.usage.prompt_tokens,
outputTokens: response.usage.completion_tokens,
finishReason: response.choices[0].finish_reason,
});
return response.choices[0].message.content;
});
ParameterOTel Attribute
modelgen_ai.request.model
providergen_ai.provider.name
inputTokensgen_ai.usage.input_tokens
outputTokensgen_ai.usage.output_tokens
totalTokensgen_ai.usage.total_tokens
responseIdgen_ai.response.id
finishReasongen_ai.response.finish_reasons (wrapped as an array)
temperaturegen_ai.request.temperature
maxTokensgen_ai.request.max_tokens
sessionIdgen_ai.conversation.id
agentIdgen_ai.agent.id
agentDescriptiongen_ai.agent.description
toolDefinitionsgen_ai.tool.definitions (JSON-serialized)
systemInstructionsgen_ai.system_instructions
inputMessagesgen_ai.input.messages (JSON-serialized)
outputMessagesgen_ai.output.messages (JSON-serialized)
any other keykey used as-is

All parameters are optional; only provided values are set. enrich() is a no-op when there is no active recording span.


When autoInstrument is true (the default), register() discovers and activates supported OTel instrumentor packages that are installed in your project - no code changes needed. Currently the SDK discovers the OpenAI instrumentation package:

Terminal window
npm install @opentelemetry/instrumentation-openai

Disable discovery entirely:

await register({ autoInstrument: false });

Attaches an evaluation score to a trace or span. Scores are emitted as OTEL spans through the same OTLP pipeline - no separate client or index needed. When traceId is provided, the score span is attached to that trace so it appears in the same trace waterfall.

import { score } from "@opensearch-project/genai-observability-sdk-ts";
// Span-level: score a specific span
score({
name: "accuracy",
value: 0.95,
traceId: "6ebb9835f43af1552f2cebb9f5165e39",
spanId: "89829115c2128845",
explanation: "Weather data matches ground truth",
});
// Trace-level: attaches to the trace's root span
score({
name: "relevance",
value: 0.92,
traceId: "6ebb9835f43af1552f2cebb9f5165e39",
explanation: "Response addresses the user's query",
});
// Standalone: no trace linkage
score({ name: "fluency", value: 0.88 });
ParameterTypeDescription
namestringMetric name, e.g. "relevance", "factuality".
valuenumberNumeric score.
traceIdstringHex trace ID to score. Omit for standalone scores.
spanIdstringHex span ID for span-level scoring. When omitted, attaches to the root span.
labelstringHuman-readable label, e.g. "pass".
explanationstringEvaluator rationale (truncated to 500 chars).
responseIdstringLLM completion ID for correlation.
attributesRecord<string, unknown>Additional span attributes (string/number/boolean values).

Runs a task against a dataset, scores each output, and records results as OTEL spans.

import { evaluate } from "@opensearch-project/genai-observability-sdk-ts";
import type { EvalScore } from "@opensearch-project/genai-observability-sdk-ts";
function accuracy(input: unknown, output: unknown, expected: unknown): EvalScore {
return { name: "accuracy", value: String(output).includes(String(expected)) ? 1.0 : 0.0 };
}
const result = evaluate({
name: "rag-agent",
task: (input) => callMyAgent(input),
data: [
{ input: "What is Python?", expected: "programming language" },
{ input: "What causes rain?", expected: "water vapor" },
],
scores: [accuracy],
metadata: { agentVersion: "v2" },
recordIo: true,
});
console.log(result.summary);

Produces:

test_suite_run rag-agent
├── test_case
└── test_case
ParameterTypeDescription
namestringBenchmark name (test.suite.name), stable across runs.
task(input: unknown) => unknownFunction that takes input and returns output.
dataArray<{ input, expected?, caseId?, caseName? }>Test cases.
scoresScorerFn[]Scorers: (input, output, expected) returning EvalScore, EvalScore[], or number.
metadataRecord<string, unknown>Attached to the root span (reserved keys are filtered).
recordIobooleanRecord input/output/expected as span attributes (default false).

Uploads pre-computed evaluation results from any framework as OTEL spans.

import { Benchmark } from "@opensearch-project/genai-observability-sdk-ts";
const bench = new Benchmark("nightly-eval", { metadata: { model: "gpt-4o" }, recordIo: true });
bench.log({ input: "What is Python?", output: "A language", scores: { accuracy: 1.0 } });
// Link to existing agent traces
bench.log({
input: "query",
output: "answer",
scores: { accuracy: 0.9 },
traceId: "6ebb9835f43af1552f2cebb9f5165e39",
spanId: "89829115c2128845",
});
const summary = bench.close();

For evaluation concepts and workflows, see Evaluation & Scoring.


Retrieves GenAI trace spans stored in OpenSearch. Requires the @opensearch-project/opensearch package.

import { OpenSearchTraceRetriever } from "@opensearch-project/genai-observability-sdk-ts";
const retriever = new OpenSearchTraceRetriever({
host: "https://localhost:9200",
auth: { username: "admin", password: "admin" },
verifyCerts: false,
});
// Retrieve all spans for a session or trace
const session = await retriever.getTraces("my-conversation-id");
for (const trace of session.traces) {
for (const span of trace.spans) {
console.log(`${span.operationName}: ${span.name} (${span.model})`);
}
}
// List recent root spans
const roots = await retriever.listRootSpans({ services: ["my-agent"], maxResults: 10 });
// Check which traces already have evaluation spans
const evaluated = await retriever.findEvaluatedTraceIds(["trace-id-1", "trace-id-2"]);
Constructor optionTypeDefaultDescription
hoststring"https://localhost:9200"OpenSearch endpoint.
indexstring"otel-v1-apm-span-*"Index pattern for span data.
auth{ username, password } | "awsSigV4"Authentication method.
verifyCertsbooleantrueVerify TLS certificates.

For AWS-hosted endpoints, use AWSSigV4OTLPExporter to sign OTLP requests with SigV4, and pass it to register():

import { register, AWSSigV4OTLPExporter } from "@opensearch-project/genai-observability-sdk-ts";
const exporter = new AWSSigV4OTLPExporter({
endpoint: "https://pipeline.us-east-1.osis.amazonaws.com/v1/traces",
service: "osis", // "osis" for OSIS pipelines, "es" for OpenSearch Service
region: "us-east-1", // or set AWS_DEFAULT_REGION / AWS_REGION
});
await register({ serviceName: "my-agent", exporter });

The exporter resolves credentials from the standard AWS provider chain (environment variables, shared credentials file, IAM role/IMDS) and requires the @aws-sdk/credential-providers and aws4 packages. A region is required - pass region, or set AWS_DEFAULT_REGION / AWS_REGION.


VariableDescriptionDefault
OTEL_EXPORTER_OTLP_TRACES_ENDPOINTFull OTLP traces endpoint URL (used as-is)
OTEL_EXPORTER_OTLP_ENDPOINTBase OTLP endpoint URL (/v1/traces appended)Data Prepper default
OTEL_EXPORTER_OTLP_TRACES_PROTOCOLProtocol for traces (http/protobuf, grpc)
OTEL_EXPORTER_OTLP_PROTOCOLProtocol for all signals (http/protobuf, grpc)
AWS_DEFAULT_REGIONAWS region for SigV4 signing
AWS_REGIONAWS region for SigV4 signing (fallback)