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
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'
|
|
};
|
|
}
|