Pydantic Logfire — Uncomplicated Observability — JavaScript SDK
From the team behind Pydantic Validation, Pydantic Logfire is an observability platform built on the same belief as our open source library — that the most powerful tools can be easy to use.
Check the Github Repository README for more information on how to use the SDK.
Scoped manual clients
Use withTags() or withSettings() when several manual spans or logs share
stable defaults:
import * as logfire from 'logfire'
const payments = logfire.withTags('payments')
payments.info('Payment authorized {payment_id}', {
payment_id: 'pay_123',
})
await payments.span('Capture payment {payment_id}', {
attributes: { payment_id: 'pay_123' },
callback: async () => capturePayment('pay_123'),
})
Scoped clients do not mutate global defaults. Per-call tags are appended after
scoped tags and duplicates are removed while preserving order. withSettings()
currently supports reusable tags and a default level for calls such as
log() and spans whose options do not set a level.
Function instrumentation
Use instrument() to wrap a sync or async function in a Logfire span without
changing the function body:
import * as logfire from 'logfire'
const fetchCustomer = logfire.instrument(
async (customerId: string) => {
return loadCustomer(customerId)
},
{
message: 'Fetch customer {customer_id}',
extractArgs: ['customer_id'],
tags: ['customers'],
}
)
await fetchCustomer('cus_123')
Argument extraction is off by default. Prefer explicit names such as
extractArgs: ['customer_id']; extractArgs: true is best effort and can be
unreliable after bundling or minification. recordReturn: true records
successful return values as telemetry on a best-effort basis, but never makes a
successful function call fail because return serialization failed.
Scoped clients expose the same wrapper:
const customers = logfire.withTags('customers')
const syncCustomer = customers.instrument(syncCustomerImpl, {
message: 'Sync customer {customer_id}',
extractArgs: ['customer_id'],
})
TypeScript decorators are intentionally not part of this first pass.
Minimum level filtering
Use configureLogfireApi({ minLevel }) to suppress low-severity manual Logfire
telemetry before spans are created. This is separate from console-output
configuration:
import * as logfire from 'logfire'
logfire.configureLogfireApi({
minLevel: 'warning',
})
minLevel accepts trace, debug, info, notice, warning, error, or
fatal, or numeric values from logfire.Level. Set minLevel: null to clear a
previous setting. Log helpers and reportError() are filtered by their level;
span(), startSpan(), startPendingSpan(), and instrument() are filtered
only when the call or scoped client sets an explicit level.
Filtered span() callbacks still run with a no-op span. Thrown or rejected
errors propagate normally, but Logfire does not record them because the call was
filtered. Use reportError() or a span level at or above the minimum when
errors should always be reported.
Attribute serialization
Logfire serializes object and array attributes as JSON strings and adds
logfire.json_schema metadata so the backend can render them as structured
values. By default, schema metadata uses bounded best-effort inference for
ordinary JSON-like values such as objects, arrays, strings, numbers, booleans,
null, and dates.
Configure jsonSchema when you need a cheaper or quieter mode:
import * as logfire from 'logfire'
logfire.configureLogfireApi({
jsonSchema: 'basic',
})
Use jsonSchema: 'basic' for legacy broad top-level object/array schemas,
or jsonSchema: false to omit logfire.json_schema entirely. This only
controls schema metadata; object and array attributes are still serialized as
JSON strings. Values that cannot be serialized are recorded as
"[unserializable]".
Error reporting
Use reportError() from explicit catch blocks. The caught value can be
unknown, matching modern TypeScript catch behavior:
try {
await syncCustomer()
} catch (error) {
logfire.reportError('Customer sync failed', error, { customer_id: 'cus_123' }, { tags: ['customers'] })
}
The third argument is always structured attributes. Use the optional fourth
argument for report options such as tags or parentSpan. The JavaScript API
does not include a Python-style exception() helper in this first pass.
Baggage span attributes
Configure an explicit baggage allowlist when stable OpenTelemetry baggage values should be copied onto Logfire manual spans and logs:
import { configureLogfireApi } from 'logfire'
configureLogfireApi({
baggage: {
spanAttributes: ['tenant', 'region'],
},
})
The Node and browser runtime packages expose the same shape through
logfire.configure(). Projection is disabled by default and affects Logfire
manual spans/logs, including span(), startSpan(), startPendingSpan(), log
helpers, reportError(), and instrument().
Configured keys are emitted with a baggage. prefix, such as
baggage.tenant. User-provided attributes win if they already set the same
attribute key. Missing baggage keys are ignored, baggage metadata is ignored,
and values are kept as strings truncated to 1000 characters.
Baggage propagates across service boundaries. Do not store secrets, credentials, session cookies, raw emails, or other sensitive user data in baggage.
Use OpenTelemetry propagation APIs directly when you need to move context through queues or background-job metadata:
import { context, propagation } from '@opentelemetry/api'
import * as logfire from 'logfire'
const carrier: Record<string, string> = {}
propagation.inject(context.active(), carrier)
const extractedContext = propagation.extract(context.active(), carrier)
await context.with(extractedContext, async () => {
await logfire.span('process job', {
callback: async () => processJob(),
})
})
The carrier is a serializable object such as headers or queue metadata.
OpenTelemetry Context is runtime-local and is not serializable. Logfire JS
does not add generic getContext() / attachContext() wrappers in this first
pass; use the OpenTelemetry APIs directly for those cases.
Manual pending spans
Use startPendingSpan() when you want to show a long-running operation as
pending immediately, without enabling automatic pending spans for every span in
the runtime:
import { startPendingSpan } from 'logfire'
const span = startPendingSpan('Load dashboard', { route: '/dashboard' })
try {
await loadDashboard()
} finally {
span.end()
}
The helper returns the real span for you to end and emits one
logfire.span_type = "pending_span" placeholder at start time. Runtimes that
also install automatic pending-span processing, such as Node.js, suppress the
automatic placeholder for this one real span so the manual placeholder is not
duplicated. The suppression marker is internal to logfire and is shared by
the hardcoded OpenTelemetry context key.
Evaluations
logfire/evals exports the JavaScript evaluation API. It mirrors the
Python pydantic-evals model:
offline Dataset experiments, built-in and custom case evaluators,
report-level analyses, YAML/JSON dataset files, and withOnlineEvaluation for
sampled live monitoring. The emitted OpenTelemetry span/log format and dataset
file format are compatible with Logfire's evaluations UI.
Use offline evaluation for curated checks before deployment:
import { Case, Dataset, EqualsExpected, Evaluator, IsInstance, renderReport, type EvaluatorContext } from 'logfire/evals'
interface ClassifyInputs {
text: string
}
class ConfidenceScore extends Evaluator<ClassifyInputs, string> {
static evaluatorName = 'ConfidenceScore'
evaluate(ctx: EvaluatorContext<ClassifyInputs, string>): number {
return ctx.output === ctx.expectedOutput ? 1 : 0
}
}
const dataset = new Dataset<ClassifyInputs, string>({
cases: [
new Case({ expectedOutput: 'POSITIVE', inputs: { text: 'I love this!' }, name: 'positive-1' }),
new Case({ expectedOutput: 'NEGATIVE', inputs: { text: 'This failed' }, name: 'negative-1' }),
],
evaluators: [new IsInstance({ typeName: 'string' }), new EqualsExpected(), new ConfidenceScore()],
name: 'sentiment-classifier',
})
const report = await dataset.evaluate(async ({ text }) => {
const lower = text.toLowerCase()
if (lower.includes('love')) return 'POSITIVE'
if (lower.includes('fail')) return 'NEGATIVE'
return 'NEUTRAL'
})
console.log(renderReport(report, { includeInput: true, includeOutput: true }))
An evaluator may return a boolean assertion, number score, string label,
{ value, reason }, or a map of named results. Built-ins include
EqualsExpected, Equals, Contains, IsInstance, MaxDuration,
HasMatchingSpan, and LLMJudge. Report evaluators include
ConfusionMatrixEvaluator, PrecisionRecallEvaluator, ROCAUCEvaluator, and
KolmogorovSmirnovEvaluator.
Use setEvalAttribute() and incrementEvalMetric() inside the task to add
per-case data to the report, and use HasMatchingSpan when the task must emit
a particular OpenTelemetry span. Use LLMJudge for rubric-based checks by
providing a judge callback or a process-wide setDefaultJudge() function.
Datasets can be saved and loaded in Python-compatible YAML/JSON:
await dataset.toFile('sentiment.yaml', { schemaPath: 'sentiment.schema.json' })
const restored = await Dataset.fromFile<ClassifyInputs, string>('sentiment.yaml', {
customEvaluators: [ConfidenceScore],
})
Dataset files use Python-compatible field names such as expected_output,
report_evaluators, predicted_from, and snake_case SpanQuery keys.
Dataset.toFile / Dataset.fromFile are available in Node, Bun, and Deno;
browser and Cloudflare Worker runtimes can use in-memory datasets and online
evaluation, but not filesystem-backed helpers.
Use online evaluation to monitor live async functions without blocking callers:
import { Contains, Evaluator, OnlineEvaluator, waitForEvaluations, withOnlineEvaluation, type EvaluatorContext } from 'logfire/evals'
class NonEmpty extends Evaluator {
static evaluatorName = 'NonEmpty'
evaluate(ctx: EvaluatorContext): boolean {
return String(ctx.output ?? '').length > 0
}
}
const monitored = withOnlineEvaluation(async (text: string) => `summary: ${text}`, {
evaluators: [
new NonEmpty(),
new OnlineEvaluator({
evaluator: new Contains({ asStrings: true, caseSensitive: false, value: 'summary' }),
maxConcurrency: 5,
sampleRate: 0.1,
}),
],
extractArgs: ['text'],
target: 'summarizer',
})
await monitored('hello')
await waitForEvaluations()
For online evaluation, JavaScript parameter-name extraction is best effort; use
extractArgs: ['argName'] for stable context.inputs keys in bundled or
minified builds, or extractArgs: false to keep positional input values.
logfire.configure() from @pydantic/logfire-node installs the evals
span-tree processor automatically; custom OpenTelemetry setups can add
getEvalsSpanProcessor() from logfire/evals.
References and examples:
- Pydantic Evals overview
- Evaluator overview
- Dataset management
- Report evaluators
- Online evaluation
examples/node/evals.tsexamples/node/demo_evals.tsexamples/node/demo_online_evals.ts
Hosted Datasets
logfire/datasets exports a trusted-runtime API client for managing hosted
evaluation datasets through the Logfire platform API. Pass an API key with
dataset scopes explicitly when using the core package:
import { LogfireAPIClient } from 'logfire/datasets'
const client = new LogfireAPIClient({ apiKey: process.env.LOGFIRE_API_KEY ?? '' })
const dataset = await client.createDataset({ name: 'sentiment-classifier' })
await client.addCases(dataset.id, [{ expectedOutput: 'POSITIVE', inputs: { text: 'I love this!' } }])
Use @pydantic/logfire-node/datasets in Node.js when you want the helper to
read LOGFIRE_API_KEY and LOGFIRE_BASE_URL from the environment.
Managed Variables
logfire/vars exports managed variables for runtime configuration controlled
by local config or the Logfire Variables API. Use defineVar, or import the
Python-parity var export with an alias because var is a JavaScript keyword.
import { configureVariables, defineVar } from 'logfire/vars'
configureVariables({
config: {
variables: {
feature_enabled: {
labels: { on: { serialized_value: 'true', version: 1 } },
name: 'feature_enabled',
overrides: [],
rollout: { labels: { on: 1 } },
},
},
},
})
const featureEnabled = defineVar('feature_enabled', { default: false })
const resolved = await featureEnabled.get({ targetingKey: 'user-123' })
Variables can compose other variables with @{name}@ and render runtime
Handlebars placeholders with defineTemplateVar. Provider values compose
strictly and fall back to code defaults on reference errors; code defaults
compose leniently and render unresolved non-fatal references as empty strings.
import { defineTemplateVar } from 'logfire/vars'
const prompt = defineTemplateVar<string, { name: string }>('prompt', {
default: 'Hello {{name}}',
templateInputsSchema: {
properties: { name: { type: 'string' } },
type: 'object',
},
})
const resolvedPrompt = await prompt.get({ name: 'Ada' })
Use templateMismatchPolicy to control what happens when a composed template
references a {{field}} path that is not declared in templateInputsSchema.
variablesValidate() reports referenceErrors, referenceCycles, and
templateFieldIssues; variablesPush() defaults to warning-and-apply behavior
and returns a blocked result instead of throwing for strict validation blockers.
Remote variables require a Logfire API key and should be used from trusted server-side runtimes. Do not expose API keys in browser bundles.