Release candidate — 1.0.0-rc.6
StitchAPI
GuidesObservability

Trace sinks

Tracing is off by default — opt in with trace: console, a fileSink, env vars, or your own TraceSink.

A trace sink is any consumer of a stitch's event stream — reach for one when you want every start → progress → drift → result → done teed somewhere durable. Tracing is off by default: a stitch makes its call and writes nothing, prints nothing (no side effects by default). You turn it on per stitch with the trace field, globally with an env var, or handle events yourself by iterating .stream().

Example

Opt in per stitch with the trace field — 'console' for the colored stderr stream, or fileSink(path) for JSONL on disk:

import { ,  } from 'stitchapi';

// Colored one-line-per-event stream to stderr.
const  = ({
    : 'https://api.example.com',
    : '/search',
    : 'console',
});

// JSONL appended to a file you name.
const  = ({
    : 'https://api.example.com',
    : '/report',
    : ('./runs/today.jsonl'),
});

Or wire it globally by environment variable — handy for turning tracing on without touching code:

# Colored stream to stderr.
STITCH_TRACE_CONSOLE=1 node run.js

# Append JSONL to a path (off unless set).
STITCH_TRACE_FILE=./runs/today.jsonl node run.js

# ALSO fan the same events to an OTLP collector.
STITCH_EXPORT=otlp node run.js

# Turn file tracing OFF: 0, false, or an empty value disable it (no file is written).
STITCH_TRACE_FILE=0 node run.js

# Capture full request/response bodies (the JSONL truncates them to 2048 chars by default).
STITCH_TRACE_MAX_BODY=full node run.js

A stitch-local trace wins over the environment: trace: false forces tracing off even when the env vars are set.

To consume events as your own sink, iterate the stream — that's the public seam for custom handling:

import {  } from 'stitchapi';

const  = ({ : 'https://api.example.com', : '/search' });

for await (const  of ({ : { : 'mango' } }).()) {
    if (. === 'drift')
        .(.., ..);
    if (. === 'result') .(.);
}

Options

The trace field accepts:

ValueEffect
(unset)Falls back to the env vars below — off unless one is set.
'console'The colored one-line-per-event stream to stderr.
fileSink(p)JSONL appended to p (defaults to ~/.stitch/runs/proto.jsonl).
a TraceSinkAny custom sink — e.g. from createTrace / multiplex / otlpSink.
falseForces tracing off, even when STITCH_TRACE_* is set.

When trace is unset, four env vars control the built-in sink: STITCH_TRACE_CONSOLE=1 turns on the colored stderr stream; STITCH_TRACE_FILE=<path> appends JSONL to that path (off when unset, 0, or false); STITCH_EXPORT=otlp adds OTLP as one more sink fed from the same events — see OTLP export; and STITCH_TRACE_MAX_BODY tunes body truncation (full to capture whole bodies, or a character cap).

To build sinks in code, consoleSink() and fileSink(path) return the single-purpose sinks, createTrace({ console, file }) returns one that does both, and multiplex(a, b, ...) fans one event stream out to several sinks at once. All are exported from stitchapi.

Logging to a host logger

Already running pino, winston, or just console? loggerSink(logger) bridges the event stream straight onto it — no JSONL file, no OTLP collector. It is logger-agnostic (anything with error / warn / info / debug methods satisfies the LoggerLike shape) and payload-free: it logs only metadata — name, method, scrubbed URL, status, attempt counts, drift path/level, timing — never event.input, event.data, or a streamed delta chunk.

import { ,  } from 'stitchapi';

// `console` already satisfies LoggerLike; swap in `pino()` / a winston logger / a
// Nest `Logger` and nothing else changes.
const  = ({
    : 'https://api.example.com',
    : '/search',
    : (),
});

Each StitchEvent maps to a level:

EventLevel
resultinfo
errorerror
driftthe finding's own level (error / warn / info)
start / progress / info / donedebug
deltanever logged — a streamed chunk is raw response data.

Override any per-type level with levels — for example, demote the happy-path result to debug so production logs stay quiet until something drifts or errors:

import { ,  } from 'stitchapi';

const  = ({
    : 'https://api.example.com',
    : '/search',
    : (, { : { : 'debug' } }),
});

This is the generic core counterpart to @stitchapi/nest's NestJS-specific nestLoggerSink, which forwards the same payload-free metadata to a Nest Logger — see NestJS integration.

Turning tracing off

There are two off switches, both of which disable the built-in sinks:

// Per-stitch: `trace: false` disables ALL built-in sinks for this stitch and wins
// over every STITCH_TRACE_* env var. (A `TraceSink` value replaces them instead.)
const  = ({
    : 'https://api.example.com',
    : '/health',
    : false,
});

Globally, set STITCH_TRACE_FILE to 0, false, or an empty string to turn file tracing off — these are treated as the off switch, not as a path, so no file named 0 or false is ever written to the working directory.

Default secret redaction

Before any event is written to the built-in JSONL (or console) sink, header values for a fixed denylist are replaced with the literal [REDACTED], matched case-insensitively wherever headers appear in the event payload (the start event's input headers, response headers, and so on):

  • authorization
  • proxy-authorization
  • cookie
  • set-cookie
  • x-api-key

So a call carrying authorization: 'Bearer …' traces as "authorization":"[REDACTED]" — the raw secret never reaches disk. Redaction happens at the sink boundary, so the live request still sends the real header; only the trace copy is scrubbed.

The same boundary scrubs URLs: a start event's resolved URL has any userinfo (https://user:pass@host) removed and the values of secret-bearing query params (api_key, access_token, signature, …) replaced with REDACTED — in both the JSONL url field and the OTLP url.full attribute. The matching keys in the start event's structured input.query are redacted the same way, so the two views can't disagree; benign params (page, sort) survive for observability.

Need to redact a non-standard auth header (say x-auth-token)? Widen the denylist — you can extend it but never shrink it — with redactHeaders:

import { ,  } from 'stitchapi';

const  = ({
    : 'https://api.example.com',
    : '/search',
    : ('./runs/today.jsonl', {
        : ['x-auth-token', 'x-session-id'],
    }),
});

A TraceSink is just { handle(event, ctx), flush?() } — implement those two and you have a custom consumer you can pass to trace or multiplex. The same shape backs the console, JSONL, and OTLP sinks, which is why all three come from one source without the call site knowing. See Reference → Helpers for the exported building blocks and Reference → Event types for the StitchEvent union a sink receives.

Anti-pattern: don't assume a custom TraceSink inherits the built-in [REDACTED] scrubbing and write the raw event payload straight to your destination — its headers still carry the live secret (authorization, cookie, x-api-key), so a handle that logs them verbatim leaks a credential the call resolves but the caller never sees; scrub the same header denylist inside your handle before you persist, or wrap the built-in sink via multiplex so redaction runs first. See Capability, not credential.

The CLI honors the same default: stitch run traces nothing until you pass --trace, which records the JSONL file (at ~/.stitch/runs/proto.jsonl, or wherever STITCH_TRACE_FILE points) so stitch trace has a log to summarize. Use --trace=console to stream to stderr instead.

Body truncation and full capture

To keep one large response from bloating the log — and to limit how much payload is persisted by default — the built-in JSONL sink truncates the request body (the start event) and the response value (the result event) once their JSON encoding passes a character cap. The oversized value is replaced with a compact marker:

{ "truncated": true, "bytes": 51234, "preview": "{\"items\":[{\"id\":1,…" }

preview is the first maxBodyBytes characters of the (already header-redacted) JSON, so you keep a readable head of the payload without storing all of it. The default cap is 2048 characters; 0 keeps only the marker, no preview.

Truncation bounds size — it is not field-level redaction, and a preview can still contain a body-embedded secret. Header and URL secrets are redacted (above) regardless of the cap.

Opt into full capture — the whole body, untruncated — when you need it, with maxBodyBytes: false on the file sink:

import { ,  } from 'stitchapi';

const  = ({
    : 'https://api.example.com',
    : '/report',
    // false = capture the whole body; a number sets a custom character cap.
    : ('./runs/today.jsonl', { : false }),
});

Or globally, without touching code, via the env knob the built-in sink reads:

# Whole bodies, no truncation.
STITCH_TRACE_MAX_BODY=full node run.js

# A custom character cap (here 8 KB).
STITCH_TRACE_MAX_BODY=8192 node run.js

Migration: earlier releases stored every request and response body in the JSONL trace in full. The built-in file sink now truncates them to 2048 characters by default, and resolved URLs have their userinfo and secret query values scrubbed. No API changed shape and nothing was removed — if a tool or stitch trace workflow relied on whole bodies on disk, restore the old behaviour with STITCH_TRACE_MAX_BODY=full (or fileSink(path, { maxBodyBytes: false })). A custom TraceSink is unaffected: it receives the live events and decides for itself.

See also

On this page