@dudousxd/nestjs-telescope
Version:
Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.
99 lines • 5.63 kB
TypeScript
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