Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Guidance for agents working on `@larvit/log`. Keep changes aligned with the prio

## What this is

Structured logging with a tiny API and first-class OTLP (logs + traces) over `fetch`, with no OpenTelemetry SDK dependency. Works as a plain stdout/stderr logger when OTLP is not configured.
Structured logging with a tiny API and first-class OTLP (logs + traces) over `fetch`, with no OpenTelemetry SDK dependency. Works as a plain stdout/stderr logger when OTLP is not configured. `log.fetch()` auto-instruments outgoing HTTP (client spans + W3C `traceparent` propagation); the `traceparent` option joins upstream traces.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Design priorities (in order)

Expand Down
85 changes: 84 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @larvit/log

Structured logging with a simple interface and support for OTLP.
Structured logging with a simple interface, OTLP export, and auto-instrumented HTTP tracing.

## Design priorities

Expand Down Expand Up @@ -68,6 +68,59 @@ async function myRequestHandler(req, res) {

```

### Trace outgoing HTTP calls

`log.fetch()` is a drop-in for `fetch()` that automatically creates an OpenTelemetry **client span**
for the call (nested under the log's span) and injects a W3C `traceparent` header, so the downstream
service continues the same trace:

```javascript
const reqLog = new Log({ otlpHttpBaseURI: "http://127.0.0.1:4318", parentLog: appLog });

const res = await reqLog.fetch("https://api.example.com/users", { method: "POST" });

await reqLog.end(); // flushes the spans (the fetch itself never waits on the OTLP export)
```

The span records the OTel HTTP semantic-convention attributes (`http.request.method`, `url.full`,
`url.scheme`, `server.address`/`server.port`, `http.response.status_code`, and `error.type` on
failure — its value is the error's `code`, else its `name`, else `"fetch_error"`). 4xx/5xx responses,
and thrown network errors (which are re-thrown unchanged), mark the span as errored. Instrumentation
never changes the call's result.

Notes:

- **The span is the only output** — `log.fetch` writes no log line. Without `otlpHttpBaseURI` it just
injects the `traceparent` header (still useful: downstream services continue the trace).
- **Delivery:** the span exports in the background, so the call returns as soon as the response is
ready. `await log.end()` delivers every span started by this log — including a fire-and-forget
`log.fetch()` you never awaited — so a short-lived process can safely exit after `end()`.
- **Errors:** fire-and-forget delivers the *span*, not error handling — exactly like plain `fetch`, a
`log.fetch()` you don't await surfaces a failed request as an unhandled promise rejection. Await it
(or attach a `.catch`) whenever the call can fail.
- **Inputs:** only `string` and `URL` are traced. A `Request` object, or a relative URL with no base
(e.g. in Node), passes straight through to a plain, untraced `fetch` (the call still works).

**Privacy:** the query string is **dropped** from `url.full` by default (it may carry tokens) and
userinfo is always stripped. Opt in with `captureQuery: true` (known-sensitive keys like `Signature`
are still redacted). Headers are captured only when allow-listed via `captureRequestHeaders` /
`captureResponseHeaders`; request/response bodies are never captured. These three are an
instance-wide policy (read at call time from the log); `clone()` to vary them.

### Continue a trace from an incoming request

To join a trace started upstream, pass the incoming `traceparent` header — this log adopts that trace
and nests under the caller's span. Read the current context back with `log.traceparent()` to propagate
it to a non-fetch client:

```javascript
const reqLog = new Log({ traceparent: req.headers.traceparent });
// ...later, calling some other client:
myClient.send({ headers: { traceparent: reqLog.traceparent() } });
```

A malformed `traceparent` is ignored (a fresh trace starts), so passing an untrusted header is safe.

### Configuration

**Log level only**
Expand All @@ -78,6 +131,17 @@ async function myRequestHandler(req, res) {
const log = new Log({
// All options is optional

// log.fetch only: include the URL query string on the span (sensitive keys still redacted).
// Default false — the query is dropped, as it may contain tokens.
// Added in 2.3.0
captureQuery: false,

// log.fetch only: header-name allow-lists, recorded as http.request.header.* /
// http.response.header.*. Default: none captured. Instance-wide; clone() to vary per call.
// Added in 2.3.0
captureRequestHeaders: ["x-request-id"],
captureResponseHeaders: ["x-request-id"],

// Context will be appended as metadata to all log entries
// Default is an empty context
context: {
Expand Down Expand Up @@ -128,6 +192,11 @@ const log = new Log({
// Added in 1.3.0
printTraceInfo: false,

// Incoming W3C traceparent to adopt: this log joins that trace and nests under that span.
// Ignored if malformed or if parentLog is set.
// Added in 2.3.0
traceparent: null,

// Use a specific span name. Any log using this log as a parent will be
// grouped under this span name.
// Defaults to be the same as the span id, that is internally generated for each span
Expand Down Expand Up @@ -196,6 +265,20 @@ To publish manually instead: `npm run build-and-publish`.

## Changelog

### v2.3.0

- **Auto-instrumented HTTP client.** New `log.fetch(input, init?)` — a drop-in for `fetch` that
creates an OTel **client span** for the call (nested under the log's span), injects a W3C
`traceparent` header, and records the HTTP semantic-convention attributes. The query string is
dropped from `url.full` by default (opt in with `captureQuery`); headers are captured only via the
`captureRequestHeaders`/`captureResponseHeaders` allow-lists; bodies are never captured. 4xx/5xx and
thrown errors (re-thrown unchanged) mark the span errored. Spans export in the background and flush
on `end()`, so the fetch never waits on the OTLP round-trip.
- **W3C trace-context propagation.** New `traceparent` option to adopt an incoming trace (join it and
nest under its span; malformed values are ignored) and `log.traceparent()` to emit the current
context for non-fetch clients. New exported helpers `parseTraceparent`/`formatTraceparent`.
- Still dependency-free and runtime-agnostic — built on global `fetch`/`Headers`/`URL`.

### v2.2.0

- OTLP can now export over **HTTP/protobuf**, not only HTTP/JSON. Opt in with
Expand Down
Loading