UNPKG

@dudousxd/nestjs-telescope

Version:

Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.

99 lines 5.63 kB
import type { Watcher, WatcherContext } from '../nest/watcher.js'; import { type AxiosInterceptorLike, type CustomAxiosSource } from './axios-source.js'; export interface HttpClientWatcherOptions { /** Outbound calls at/above this many ms get a 'slow' tag. Default 1000. */ slowMs?: number; /** Time source; injectable for tests. Default wall clock. */ clock?: { now(): number; }; /** * An axios instance (or {@link CustomAxiosSource} for lazy resolution) to * capture alongside the global `fetch`. NestJS apps mostly call out through * `@nestjs/axios`'s `HttpService`, whose `axiosRef` is exactly an * {@link AxiosInterceptorLike}; in Node that traffic uses the http adapter and * is invisible to the `fetch` patch. Pass the instance (or a source that * resolves `HttpService` from `ctx.moduleRef`) to capture it via axios's * public interceptor API — no monkey-patching. */ axios?: AxiosInterceptorLike | CustomAxiosSource; } /** * Captures outbound HTTP calls made via the global `fetch` and, optionally, via * an axios instance (`@nestjs/axios`'s `HttpService.axiosRef` or a bare axios), * correlated to the request/job that made them. * * Both paths run inside the caller's async context, so the active ALS batch * (set by the request middleware) is live when we record — no extra wiring is * needed for correlation. Recording is guarded so a telescope failure can never * change the host's HTTP result, and the host's network error is always * re-thrown. The `fetch` patch is idempotent across instances via a symbol * marker; axios instrumentation is idempotent via a module-level `WeakSet` of * instrumented instances (so two watchers sharing one axios don't double-wrap). * * ## Why axios needs its own path * NestJS apps mostly call out through `@nestjs/axios`. In Node, axios uses the * http adapter (not `fetch`), so those calls bypass the `fetch` patch entirely * and are invisible without this. We attach via axios's PUBLIC interceptor API * (`interceptors.request/response.use`) — never monkey-patching — so we don't * fight axios internals or other interceptors the host installs. * * @remarks * Only the global `fetch` and an explicitly-provided axios instance are * instrumented. Clients that bypass both (a custom `http.request`, native * addons) are not captured. The global `fetch` is replaced for the process * lifetime (not restored on shutdown) — appropriate for an always-on * observability tool. The captured URL has userinfo stripped and sensitive * query-param values redacted; key-based content redaction does not otherwise * apply to the URL string. * * **Double-capture (edge case):** if an axios instance is explicitly configured * with a `fetch` adapter AND that `fetch` is our patched global, a single call * could record twice (once per path). Node's default is the http adapter, so * this only happens when a host opts into the fetch adapter; we don't build * detection machinery for it. */ export declare class HttpClientWatcher implements Watcher { readonly type: "http_client"; private readonly logger; private readonly slowMs; private readonly clock; private readonly axiosSource; /** Per-request start times, keyed by the axios config object. A `WeakMap` * (not a symbol stamped onto the config) keeps the host's object pristine and * lets entries be GC'd if a request never completes — no leak, no mutation. */ private readonly axiosStartedAt; constructor(options?: HttpClientWatcherOptions); register(ctx: WatcherContext): void; /** Patch the global `fetch` exactly as before — unchanged behavior. Idempotent * process-wide via the {@link PATCHED} marker. Pulled into its own method so * axios instrumentation can run independently (and still attach even when * `fetch` is missing or already patched). */ private patchFetch; /** Wire axios capture if an axios source was provided. The bare-instance form * attaches immediately; the {@link CustomAxiosSource} form hands the host an * `attach` callback so it can resolve `HttpService` from `ctx.moduleRef` * lazily (the instance often doesn't exist yet at construction time). */ private registerAxios; /** Install request/response interceptors on an axios instance via its public * API. Idempotent via the module-level {@link INSTRUMENTED_AXIOS} set: a * second call (re-registration, or two watchers sharing one instance) is a * no-op so no call double-records. * * Timing uses {@link axiosStartedAt} — a `WeakMap<config, number>` — rather * than stamping the config: axios passes the SAME config object from the * request interceptor through to the response/error interceptor, so the * config is a stable key, and a `WeakMap` neither mutates the host's object * nor leaks if a request never resolves. */ private attachAxios; /** Compute duration from the stored start time, describe the target, and hand * the entry to {@link safeRecord} — the same content/tags/family-hash shape as * the `fetch` path, so axios and fetch entries are indistinguishable. A * missing start time (interceptor order, or a config we never saw) records * `durationMs: 0` rather than dropping the entry. */ private recordAxios; /** Hand an entry to the Recorder, swallowing any failure so a telescope bug * can never alter the host's HTTP call. */ private safeRecord; } //# sourceMappingURL=http-client.watcher.d.ts.map