JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • 0
  • Score
    100M100P100Q4774F
  • License ISC

Minimal injectable structured-logging and metrics seam. Cross-standard reusable; protocol-agnostic, no Workers runtime dependency.

Package Exports

  • @dwk/log

Readme

@dwk/log

Minimal, injectable structured-logging seam shared across the @dwk packages.

A cross-standard reusable lib (like @dwk/dpop and @dwk/rdf): protocol-agnostic, stateless, and unit-testable without a Workers runtime. It defines where the @dwk packages send signal, not how that signal is stored — the composed Worker wires a concrete logger to Workers structured logs / Logpush / Analytics Engine.

See spec/observability.md for the cross-cutting requirement and the event-taxonomy conventions.

Why

The @dwk packages handle untrusted, attacker-supplied input. Without a logging seam, security-relevant events — a blocked SSRF attempt, an auth rejection, a poison queue message — are silently swallowed and indistinguishable from a dead link or a timeout. This package is the seam those events flow through.

The seam

import type { Logger } from "@dwk/log";

export interface Logger {
  debug(event: string, fields?: Record<string, unknown>): void;
  info(event: string, fields?: Record<string, unknown>): void;
  warn(event: string, fields?: Record<string, unknown>): void;
  error(event: string, fields?: Record<string, unknown>): void;
}

Each call names a stable, dotted event (e.g. webmention.ssrf.blocked) plus a flat bag of structured fields, so operators query by event and field instead of grepping prose. Event-name taxonomies are owned by each consuming package.

Usage

A package accepts an optional logger in its config and defaults to noopLogger, so logging is strictly opt-in:

import { noopLogger, type Logger } from "@dwk/log";

function createThing(config: { logger?: Logger }) {
  const logger = config.logger ?? noopLogger;
  logger.warn("thing.blocked", { reason: "policy" });
}

The composed Worker wires a real logger once:

import { consoleLogger } from "@dwk/log";

const logger = consoleLogger({ minLevel: "info", base: { service: "wm" } });
const handler = createWebmention({ baseUrl, logger });

Exports

Export Purpose
Logger, LogLevel, LogFields The logging seam types.
noopLogger Discards everything; the default when no logger is configured.
consoleLogger(options?) Emits one JSON record per call to console (Workers logs).
withContext(logger, ctx) Binds request/pod-scoped fields onto every record.
hostFromUrl(raw) Redaction helper: a URL's host only, never its path/query.
Metrics The metrics seam type (count / observe).
noopMetrics Discards everything; the default when no metrics sink is set.
analyticsEngineMetrics(dataset, options?) Adapter to Cloudflare Workers Analytics Engine.

Metrics

The companion metrics seam answers "how often / how much?" for the same events the logger names, so an operator can chart "SSRF blocks/min" or "verification success rate" instead of scraping log lines. It is injected the same way — an optional metrics, defaulting to noopMetrics — and reuses the same event names and field bags as logs, so logs and counters share one vocabulary:

import { noopMetrics, type Metrics } from "@dwk/log";

function createThing(config: { metrics?: Metrics }) {
  const metrics = config.metrics ?? noopMetrics;
  metrics.count("thing.blocked", { reason: "policy" }); // a counter
  metrics.observe("thing.latency", 42, { host: "a.example" }); // an observation
}

The composed Worker wires the real adapter once, from a bound AnalyticsEngineDataset:

import { analyticsEngineMetrics } from "@dwk/log";

// env.WM_METRICS is an AnalyticsEngineDataset binding declared in wrangler.toml.
const metrics = analyticsEngineMetrics(env.WM_METRICS, {
  base: { service: "wm" },
});
const handler = createWebmention({ baseUrl, logger, metrics });

analyticsEngineMetrics maps each call onto writeDataPoint deterministically: the event becomes indexes[0] (the sampling key) and blobs[0]; string fields become further blobs, and numeric/boolean fields become doubles (with a lead 1 for count or the observed value for observe) — all in sorted key order so positions are stable per event. Cloudflare's Analytics Engine limits (1 index ≤ 96 B, ≤ 20 blobs ≤ 16 KB total, ≤ 20 doubles) are enforced, and like Logger a Metrics implementation never throws into the operation it measures. The binding type is declared structurally (AnalyticsEngineDatasetLike), so this package keeps no @cloudflare/workers-types dependency. The same redaction rules apply — never pass tokens, bodies, or full URLs as fields.

Redaction

Redaction is the caller's responsibility, but the seam helps. Never pass tokens, credentials, or full request/response bodies as fields. For URLs, prefer hostFromUrl(raw) so an attacker-supplied path or query string never lands in a log line.