You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

170 lines
5.2 KiB

import type {
Transport,
ConsoleTransportOptions,
HttpTransportOptions,
LogEntry
} from './types.ts';
import { LogLevel } from './types.ts';
import { logger } from './consts.ts';
// ============================================================================
// CONSOLE TRANSPORT
// ============================================================================
/**
* Emit log entries to the browser/Node console using the method that matches
* the level: DEBUG → console.debug, INFO → console.info, WARN → console.warn,
* ERROR → console.error. TRACE maps to `console.debug`, FATAL to `console.error`
* since consoles don't expose those levels natively.
*
* @example
* consoleTransport()
* consoleTransport({ timestamp: false, prefix: false })
* consoleTransport({ minLevel: LogLevel.INFO })
*/
export function consoleTransport(options: ConsoleTransportOptions = {}): Transport {
const {
timestamp: showTimestamp = true,
prefix: showPrefix = true,
source: showSource = true,
minLevel
} = options;
return {
name: 'console',
minLevel,
write(entry: LogEntry): void {
const args: unknown[] = [];
if (showTimestamp) args.push(entry.timestamp.toISOString());
if (showPrefix) args.push(`[${entry.category}]`);
if (showSource && entry.source?.file) {
const file = entry.source.file;
const afterSlash = file.slice(file.lastIndexOf('/') + 1);
const clean = afterSlash.split('?')[0];
args.push(`(${clean}:${entry.source.line})`);
}
args.push(entry.message);
// Structured payload — only appended when it has content.
const meta: Record<string, unknown> = {};
if (entry.context) meta.context = entry.context;
if (entry.tags?.length) meta.tags = entry.tags;
if (entry.traceId) meta.traceId = entry.traceId;
if (entry.durationMs !== undefined) meta.durationMs = entry.durationMs;
if (entry.error) meta.error = entry.error;
if (Object.keys(meta).length > 0) args.push(meta);
// Browser/Node console has no trace/fatal native —
// TRACE → debug, FATAL → error.
switch (entry.level) {
case LogLevel.TRACE:
case LogLevel.DEBUG:
console.debug(...(args as [unknown, ...unknown[]]));
break;
case LogLevel.INFO:
console.info(...(args as [unknown, ...unknown[]]));
break;
case LogLevel.WARN:
console.warn(...(args as [unknown, ...unknown[]]));
break;
case LogLevel.ERROR:
case LogLevel.FATAL:
console.error(...(args as [unknown, ...unknown[]]));
break;
}
}
};
}
// ============================================================================
// HTTP TRANSPORT
// ============================================================================
/**
* POST log entries to an HTTP endpoint. Fire-and-forget — network errors are
* reported via `console.error` but do not interrupt the application flow.
*
* No built-in retry or batching. For high-volume needs use a custom transport
* with a queue/retry layer.
*/
export function httpTransport(options: HttpTransportOptions): Transport {
function serialize(entry: LogEntry): Record<string, unknown> {
return {
timestamp: entry.timestamp.toISOString(),
level: entry.level,
category: entry.category,
message: entry.message,
context: entry.context,
error: entry.error,
tags: entry.tags,
traceId: entry.traceId,
durationMs: entry.durationMs,
source: entry.source
};
}
async function post(payload: unknown): Promise<void> {
const res = await fetch(options.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...options.headers
},
body: JSON.stringify(payload)
});
if (!res.ok) {
const body = await res.text().catch(() => '');
throw new Error(`http transport push failed: ${res.status} ${res.statusText} ${body}`);
}
}
return {
name: 'http',
minLevel: options.minLevel ?? LogLevel.ERROR,
async write(entry: LogEntry): Promise<void> {
await post(serialize(entry));
},
async writeBatch(entries: LogEntry[]): Promise<void> {
// Batched delivery — single request with an array of entries.
await post(entries.map(serialize));
}
};
}
// ============================================================================
// CALLBACK TRANSPORT
// ============================================================================
/**
* Transport that invokes a callback for each reaching entry. Structurally
* equivalent to `logger.subscribe(fn)` — it exists as a factory so it can be
* pre-registered via `LoggerOptions.transports` or composed with
* `minLevel`/`filter`/`name`.
*
* @example
* callbackTransport((entry) => captured.push(entry))
*
* @example
* // Sentry integration: ERROR+ only, using the caller-provided SDK
* callbackTransport(
* (entry) => {
* if (entry.error) Sentry.captureException(entry.error);
* else Sentry.captureMessage(entry.message);
* },
* { minLevel: LogLevel.ERROR, name: 'sentry' }
* )
*/
export function callbackTransport(
fn: (entry: LogEntry) => void,
options: { minLevel?: LogLevel; filter?: (entry: LogEntry) => boolean; name?: string } = {}
): Transport {
return {
write: fn,
minLevel: options.minLevel,
filter: options.filter,
name: options.name ?? 'callback'
};
}

Powered by TurnKey Linux.