@outfitter/logging 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -0
- package/dist/index.d.ts +99 -10
- package/dist/index.js +284 -15
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -193,8 +193,21 @@ const logger = createLogger({
|
|
|
193
193
|
name: "app",
|
|
194
194
|
sinks: [createConsoleSink()],
|
|
195
195
|
});
|
|
196
|
+
|
|
197
|
+
// Use JSON formatting instead of the default pretty formatter
|
|
198
|
+
const jsonLogger = createLogger({
|
|
199
|
+
name: "app",
|
|
200
|
+
sinks: [createConsoleSink({ formatter: createJsonFormatter() })],
|
|
201
|
+
});
|
|
196
202
|
```
|
|
197
203
|
|
|
204
|
+
**Options:**
|
|
205
|
+
|
|
206
|
+
| Option | Type | Default | Description |
|
|
207
|
+
|--------|------|---------|-------------|
|
|
208
|
+
| `colors` | `boolean` | auto-detect | Enable ANSI colors |
|
|
209
|
+
| `formatter` | `Formatter` | `createPrettyFormatter()` | Custom log formatter |
|
|
210
|
+
|
|
198
211
|
### File Sink
|
|
199
212
|
|
|
200
213
|
Buffered writes to a file path:
|
|
@@ -304,6 +317,123 @@ await flush();
|
|
|
304
317
|
process.exit(0);
|
|
305
318
|
```
|
|
306
319
|
|
|
320
|
+
## Environment-Aware Log Level
|
|
321
|
+
|
|
322
|
+
### `resolveLogLevel(level?)`
|
|
323
|
+
|
|
324
|
+
Resolve the log level from environment configuration. Use this instead of hardcoding levels so your app responds to `OUTFITTER_ENV` and `OUTFITTER_LOG_LEVEL` automatically.
|
|
325
|
+
|
|
326
|
+
Accepts `LogLevel` or a plain `string` — useful when forwarding CLI flags or MCP values without casting. Invalid strings are ignored and fall through to the next precedence level.
|
|
327
|
+
|
|
328
|
+
**Precedence** (highest wins):
|
|
329
|
+
1. `OUTFITTER_LOG_LEVEL` environment variable
|
|
330
|
+
2. Explicit `level` parameter
|
|
331
|
+
3. `OUTFITTER_ENV` profile defaults (`"debug"` in development)
|
|
332
|
+
4. `"info"` (default)
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
import { createLogger, resolveLogLevel } from "@outfitter/logging";
|
|
336
|
+
|
|
337
|
+
const logger = createLogger({
|
|
338
|
+
name: "my-app",
|
|
339
|
+
level: resolveLogLevel(),
|
|
340
|
+
sinks: [createConsoleSink()],
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// With OUTFITTER_ENV=development → "debug"
|
|
344
|
+
// With OUTFITTER_LOG_LEVEL=error → "error" (overrides everything)
|
|
345
|
+
// With nothing set → "info"
|
|
346
|
+
|
|
347
|
+
// Forward a CLI string without casting
|
|
348
|
+
const level = flags.logLevel; // string from commander
|
|
349
|
+
const logger2 = createLogger({
|
|
350
|
+
name: "cli",
|
|
351
|
+
level: resolveLogLevel(level),
|
|
352
|
+
sinks: [createConsoleSink()],
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
MCP-style level names are mapped automatically: `warning` to `warn`, `emergency`/`critical`/`alert` to `fatal`, `notice` to `info`.
|
|
357
|
+
|
|
358
|
+
### Edge Runtime Compatibility
|
|
359
|
+
|
|
360
|
+
`resolveLogLevel` and `createConsoleSink` are safe to use in environments where `process` is unavailable (V8 isolates, Cloudflare Workers, edge runtimes). Environment variable reads are guarded and environment profile resolution falls back gracefully to defaults.
|
|
361
|
+
|
|
362
|
+
## Logger Factory + BYO Backends
|
|
363
|
+
|
|
364
|
+
Use the Outfitter logger factory when wiring runtimes (CLI, MCP, daemons). It applies Outfitter defaults for log level resolution and redaction.
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
import { createOutfitterLoggerFactory } from "@outfitter/logging";
|
|
368
|
+
|
|
369
|
+
const factory = createOutfitterLoggerFactory();
|
|
370
|
+
const logger = factory.createLogger({
|
|
371
|
+
name: "mcp",
|
|
372
|
+
context: { surface: "mcp" },
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
logger.info("Tool invoked", { tool: "search" });
|
|
376
|
+
await factory.flush();
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
If you need a different backend, you can use the shared contracts factory with your own adapter and still satisfy the same `Logger` interface expected by Outfitter packages.
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
import {
|
|
383
|
+
createLoggerFactory,
|
|
384
|
+
type Logger,
|
|
385
|
+
type LoggerAdapter,
|
|
386
|
+
} from "@outfitter/contracts";
|
|
387
|
+
|
|
388
|
+
type BackendOptions = { write: (line: string) => void };
|
|
389
|
+
|
|
390
|
+
const adapter: LoggerAdapter<BackendOptions> = {
|
|
391
|
+
createLogger(config) {
|
|
392
|
+
const write = config.backend?.write ?? (() => {});
|
|
393
|
+
const createMethod = (level: string): Logger["info"] =>
|
|
394
|
+
((message: string) => {
|
|
395
|
+
write(`[${level}] ${message}`);
|
|
396
|
+
}) as Logger["info"];
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
trace: createMethod("trace"),
|
|
400
|
+
debug: createMethod("debug"),
|
|
401
|
+
info: createMethod("info"),
|
|
402
|
+
warn: createMethod("warn"),
|
|
403
|
+
error: createMethod("error"),
|
|
404
|
+
fatal: createMethod("fatal"),
|
|
405
|
+
child: () => adapter.createLogger(config),
|
|
406
|
+
};
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const customFactory = createLoggerFactory(adapter);
|
|
411
|
+
const customLogger = customFactory.createLogger({
|
|
412
|
+
name: "custom-runtime",
|
|
413
|
+
backend: { write: (line) => console.log(line) },
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
customLogger.info("Hello from custom backend");
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Runtime Compatibility
|
|
420
|
+
|
|
421
|
+
| Export | Node.js | Bun | Edge/V8 Isolates | Notes |
|
|
422
|
+
|---|---|---|---|---|
|
|
423
|
+
| `createLogger` | Yes | Yes | Yes | Universal |
|
|
424
|
+
| `createConsoleSink` | Yes | Yes | Yes | Falls back to `console.*` when `process` unavailable |
|
|
425
|
+
| `createFileSink` | No | Yes | No | Requires `Bun.file` / `Bun.write` |
|
|
426
|
+
| `createJsonFormatter` | Yes | Yes | Yes | Universal |
|
|
427
|
+
| `createPrettyFormatter` | Yes | Yes | Yes | Universal |
|
|
428
|
+
| `resolveLogLevel` | Yes | Yes | Yes | Guards `process.env` access |
|
|
429
|
+
| `resolveOutfitterLogLevel` | Yes | Yes | Yes | Guards `process.env` access |
|
|
430
|
+
| `configureRedaction` | Yes | Yes | Yes | Universal |
|
|
431
|
+
| `flush` | Yes | Yes | Yes | Universal |
|
|
432
|
+
|
|
433
|
+
Edge-runtime notes:
|
|
434
|
+
- `resolveLogLevel()` safely returns defaults when `process` is undefined
|
|
435
|
+
- `createConsoleSink()` auto-detects TTY via `process.stdout?.isTTY` with graceful fallback
|
|
436
|
+
|
|
307
437
|
## API Reference
|
|
308
438
|
|
|
309
439
|
### Functions
|
|
@@ -312,6 +442,7 @@ process.exit(0);
|
|
|
312
442
|
| ----------------------- | --------------------------------------------------- |
|
|
313
443
|
| `createLogger` | Create a configured logger instance |
|
|
314
444
|
| `createChildLogger` | Create a child logger with merged context |
|
|
445
|
+
| `resolveLogLevel` | Resolve log level from env vars and profile |
|
|
315
446
|
| `configureRedaction` | Configure global redaction patterns and keys |
|
|
316
447
|
| `flush` | Flush all pending log writes across all sinks |
|
|
317
448
|
| `createJsonFormatter` | Create a JSON formatter for structured output |
|
|
@@ -334,6 +465,10 @@ process.exit(0);
|
|
|
334
465
|
| `PrettyFormatterOptions` | Options for human-readable formatter |
|
|
335
466
|
| `FileSinkOptions` | Options for file sink configuration |
|
|
336
467
|
|
|
468
|
+
## Upgrading
|
|
469
|
+
|
|
470
|
+
Run `outfitter update --guide` for version-specific migration instructions, or check the [migration docs](https://github.com/outfitter-dev/outfitter/tree/main/plugins/outfitter/shared/migrations) for detailed upgrade steps.
|
|
471
|
+
|
|
337
472
|
## License
|
|
338
473
|
|
|
339
474
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* @outfitter/logging
|
|
3
|
-
*
|
|
4
|
-
* Structured logging via logtape with automatic sensitive data redaction.
|
|
5
|
-
* Provides consistent log formatting across CLI, MCP, and server contexts.
|
|
6
|
-
*
|
|
7
|
-
* @packageDocumentation
|
|
8
|
-
*/
|
|
1
|
+
import { Logger as ContractLogger, LoggerAdapter as ContractLoggerAdapter, LoggerFactory as ContractLoggerFactory } from "@outfitter/contracts";
|
|
9
2
|
/**
|
|
10
3
|
* Log levels supported by the logger, ordered from lowest to highest severity.
|
|
11
4
|
*
|
|
@@ -193,6 +186,32 @@ interface LoggerConfig {
|
|
|
193
186
|
redaction?: RedactionConfig;
|
|
194
187
|
}
|
|
195
188
|
/**
|
|
189
|
+
* Backend options accepted by the Outfitter logger factory.
|
|
190
|
+
*
|
|
191
|
+
* These options are passed via `LoggerFactoryConfig.backend`.
|
|
192
|
+
*/
|
|
193
|
+
interface OutfitterLoggerBackendOptions {
|
|
194
|
+
/** Sinks for this specific logger instance */
|
|
195
|
+
sinks?: Sink[];
|
|
196
|
+
/** Redaction overrides for this specific logger instance */
|
|
197
|
+
redaction?: RedactionConfig;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Default options applied by the Outfitter logger factory.
|
|
201
|
+
*/
|
|
202
|
+
interface OutfitterLoggerDefaults {
|
|
203
|
+
/** Default sinks used when a logger does not provide backend sinks */
|
|
204
|
+
sinks?: Sink[];
|
|
205
|
+
/** Default redaction policy merged with logger-specific redaction */
|
|
206
|
+
redaction?: RedactionConfig;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Options for creating the Outfitter logger factory.
|
|
210
|
+
*/
|
|
211
|
+
interface OutfitterLoggerFactoryOptions {
|
|
212
|
+
defaults?: OutfitterLoggerDefaults;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
196
215
|
* Logger instance with methods for each log level.
|
|
197
216
|
*
|
|
198
217
|
* Provides methods for logging at each severity level, plus utilities for
|
|
@@ -209,7 +228,7 @@ interface LoggerConfig {
|
|
|
209
228
|
* logger.debug("Debug info", { details: "..." });
|
|
210
229
|
* ```
|
|
211
230
|
*/
|
|
212
|
-
interface LoggerInstance {
|
|
231
|
+
interface LoggerInstance extends ContractLogger {
|
|
213
232
|
/**
|
|
214
233
|
* Log at trace level (most verbose, for detailed debugging).
|
|
215
234
|
* @param message - Human-readable log message
|
|
@@ -341,6 +360,17 @@ interface ConsoleSinkOptions {
|
|
|
341
360
|
* - `false`: Never use colors
|
|
342
361
|
*/
|
|
343
362
|
colors?: boolean;
|
|
363
|
+
/**
|
|
364
|
+
* Custom formatter for log output.
|
|
365
|
+
* When provided, overrides the default pretty formatter.
|
|
366
|
+
* Use `createJsonFormatter()` for structured output.
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* ```typescript
|
|
370
|
+
* const sink = createConsoleSink({ formatter: createJsonFormatter() });
|
|
371
|
+
* ```
|
|
372
|
+
*/
|
|
373
|
+
formatter?: Formatter;
|
|
344
374
|
}
|
|
345
375
|
/**
|
|
346
376
|
* Options for the file sink.
|
|
@@ -549,4 +579,63 @@ declare function configureRedaction(config: GlobalRedactionConfig): void;
|
|
|
549
579
|
* ```
|
|
550
580
|
*/
|
|
551
581
|
declare function flush(): Promise<void>;
|
|
552
|
-
|
|
582
|
+
/**
|
|
583
|
+
* Resolve the log level from environment configuration.
|
|
584
|
+
*
|
|
585
|
+
* Precedence (highest wins):
|
|
586
|
+
* 1. `OUTFITTER_LOG_LEVEL` environment variable
|
|
587
|
+
* 2. Explicit `level` parameter
|
|
588
|
+
* 3. `OUTFITTER_ENV` environment profile defaults
|
|
589
|
+
* 4. `"info"` (default)
|
|
590
|
+
*
|
|
591
|
+
* @param level - Optional explicit log level (overridden by env var)
|
|
592
|
+
* @returns Resolved LogLevel
|
|
593
|
+
*
|
|
594
|
+
* @example
|
|
595
|
+
* ```typescript
|
|
596
|
+
* import { createLogger, resolveLogLevel } from "@outfitter/logging";
|
|
597
|
+
*
|
|
598
|
+
* // Auto-resolve from environment
|
|
599
|
+
* const logger = createLogger({
|
|
600
|
+
* name: "my-app",
|
|
601
|
+
* level: resolveLogLevel(),
|
|
602
|
+
* });
|
|
603
|
+
*
|
|
604
|
+
* // With OUTFITTER_ENV=development → "debug"
|
|
605
|
+
* // With OUTFITTER_LOG_LEVEL=error → "error" (overrides everything)
|
|
606
|
+
* // With nothing set → "info"
|
|
607
|
+
* ```
|
|
608
|
+
*/
|
|
609
|
+
declare function resolveLogLevel(level?: LogLevel | string): LogLevel;
|
|
610
|
+
/**
|
|
611
|
+
* Resolve log level using Outfitter runtime defaults.
|
|
612
|
+
*
|
|
613
|
+
* Precedence (highest wins):
|
|
614
|
+
* 1. `OUTFITTER_LOG_LEVEL` environment variable
|
|
615
|
+
* 2. Explicit `level` parameter
|
|
616
|
+
* 3. `OUTFITTER_ENV` profile defaults
|
|
617
|
+
* 4. `"silent"` (when profile default is null)
|
|
618
|
+
*/
|
|
619
|
+
declare function resolveOutfitterLogLevel(level?: LogLevel | string): LogLevel;
|
|
620
|
+
/**
|
|
621
|
+
* Outfitter logger adapter contract type.
|
|
622
|
+
*/
|
|
623
|
+
type OutfitterLoggerAdapter = ContractLoggerAdapter<OutfitterLoggerBackendOptions>;
|
|
624
|
+
/**
|
|
625
|
+
* Outfitter logger factory contract type.
|
|
626
|
+
*/
|
|
627
|
+
type OutfitterLoggerFactory = ContractLoggerFactory<OutfitterLoggerBackendOptions>;
|
|
628
|
+
/**
|
|
629
|
+
* Create an Outfitter logger adapter with environment defaults and redaction.
|
|
630
|
+
*
|
|
631
|
+
* Defaults:
|
|
632
|
+
* - log level resolution via `resolveOutfitterLogLevel()`
|
|
633
|
+
* - redaction enabled by default (`enabled: true`)
|
|
634
|
+
* - console sink when no explicit sinks are provided
|
|
635
|
+
*/
|
|
636
|
+
declare function createOutfitterLoggerAdapter(options?: OutfitterLoggerFactoryOptions): OutfitterLoggerAdapter;
|
|
637
|
+
/**
|
|
638
|
+
* Create an Outfitter logger factory over the contracts logger abstraction.
|
|
639
|
+
*/
|
|
640
|
+
declare function createOutfitterLoggerFactory(options?: OutfitterLoggerFactoryOptions): OutfitterLoggerFactory;
|
|
641
|
+
export { resolveOutfitterLogLevel, resolveLogLevel, flush, createPrettyFormatter, createOutfitterLoggerFactory, createOutfitterLoggerAdapter, createLogger, createJsonFormatter, createFileSink, createConsoleSink, createChildLogger, configureRedaction, Sink, RedactionConfig, PrettyFormatterOptions, OutfitterLoggerFactoryOptions, OutfitterLoggerFactory, OutfitterLoggerDefaults, OutfitterLoggerBackendOptions, OutfitterLoggerAdapter, LoggerInstance, LoggerConfig, LogRecord, LogLevel, GlobalRedactionConfig, Formatter, FileSinkOptions, DEFAULT_PATTERNS, ConsoleSinkOptions };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
configureSync,
|
|
4
|
+
getConfig,
|
|
5
|
+
getLogger
|
|
6
|
+
} from "@logtape/logtape";
|
|
7
|
+
import {
|
|
8
|
+
getEnvironment as _getEnvironment,
|
|
9
|
+
getEnvironmentDefaults as _getEnvironmentDefaults
|
|
10
|
+
} from "@outfitter/config";
|
|
11
|
+
import {
|
|
12
|
+
createLoggerFactory as createContractLoggerFactory
|
|
13
|
+
} from "@outfitter/contracts";
|
|
2
14
|
var LEVEL_PRIORITY = {
|
|
3
15
|
trace: 0,
|
|
4
16
|
debug: 1,
|
|
@@ -23,6 +35,13 @@ var globalRedactionConfig = {
|
|
|
23
35
|
keys: []
|
|
24
36
|
};
|
|
25
37
|
var registeredSinks = new Set;
|
|
38
|
+
var LOGGER_ID_PROPERTY = "__outfitter_internal_logger_id";
|
|
39
|
+
var loggerSinkRegistry = new Map;
|
|
40
|
+
var logtapeBackendConfigured = false;
|
|
41
|
+
var logtapeBridgeEnabled = true;
|
|
42
|
+
var loggerIdCounter = 0;
|
|
43
|
+
var LOGTAPE_BRIDGE_SINK = "__outfitter_bridge_sink";
|
|
44
|
+
var LOGTAPE_BRIDGE_CATEGORY = "__outfitter_bridge";
|
|
26
45
|
function shouldLog(level, minLevel) {
|
|
27
46
|
if (minLevel === "silent")
|
|
28
47
|
return false;
|
|
@@ -135,6 +154,154 @@ function processNestedForErrors(obj) {
|
|
|
135
154
|
}
|
|
136
155
|
return result;
|
|
137
156
|
}
|
|
157
|
+
function mergeRedactionConfig(base, override) {
|
|
158
|
+
if (!(base || override)) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const merged = {};
|
|
162
|
+
const keys = [...base?.keys ?? [], ...override?.keys ?? []];
|
|
163
|
+
const patterns = [...base?.patterns ?? [], ...override?.patterns ?? []];
|
|
164
|
+
if (keys.length > 0) {
|
|
165
|
+
merged.keys = keys;
|
|
166
|
+
}
|
|
167
|
+
if (patterns.length > 0) {
|
|
168
|
+
merged.patterns = patterns;
|
|
169
|
+
}
|
|
170
|
+
if (base?.enabled !== undefined) {
|
|
171
|
+
merged.enabled = base.enabled;
|
|
172
|
+
}
|
|
173
|
+
if (override?.enabled !== undefined) {
|
|
174
|
+
merged.enabled = override.enabled;
|
|
175
|
+
}
|
|
176
|
+
if (base?.replacement !== undefined) {
|
|
177
|
+
merged.replacement = base.replacement;
|
|
178
|
+
}
|
|
179
|
+
if (override?.replacement !== undefined) {
|
|
180
|
+
merged.replacement = override.replacement;
|
|
181
|
+
}
|
|
182
|
+
return merged;
|
|
183
|
+
}
|
|
184
|
+
function stringifyLogtapeMessage(parts) {
|
|
185
|
+
return parts.map((part) => String(part)).join("");
|
|
186
|
+
}
|
|
187
|
+
function fromLogtapeLevel(level) {
|
|
188
|
+
if (level === "warning") {
|
|
189
|
+
return "warn";
|
|
190
|
+
}
|
|
191
|
+
return level;
|
|
192
|
+
}
|
|
193
|
+
function registerLoggerSink(loggerId, sink) {
|
|
194
|
+
const sinks = loggerSinkRegistry.get(loggerId);
|
|
195
|
+
if (sinks) {
|
|
196
|
+
sinks.add(sink);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
loggerSinkRegistry.set(loggerId, new Set([sink]));
|
|
200
|
+
}
|
|
201
|
+
function dispatchRecordToSinks(sinks, record) {
|
|
202
|
+
for (const sink of sinks) {
|
|
203
|
+
try {
|
|
204
|
+
let formatted;
|
|
205
|
+
if (sink.formatter) {
|
|
206
|
+
formatted = sink.formatter.format(record);
|
|
207
|
+
}
|
|
208
|
+
sink.write(record, formatted);
|
|
209
|
+
} catch {}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function isLogtapeBridgeConfigured(config) {
|
|
213
|
+
if (!config) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if (!(LOGTAPE_BRIDGE_SINK in config.sinks)) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
return config.loggers.some((logger) => {
|
|
220
|
+
const categoryParts = Array.isArray(logger.category) ? logger.category : [logger.category];
|
|
221
|
+
if (categoryParts.length !== 1 || categoryParts[0] !== LOGTAPE_BRIDGE_CATEGORY) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
return (logger.sinks ?? []).includes(LOGTAPE_BRIDGE_SINK);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function emitViaLogtape(logger, level, message, metadata) {
|
|
228
|
+
const loggerWithMetadata = logger.with(metadata);
|
|
229
|
+
switch (level) {
|
|
230
|
+
case "trace":
|
|
231
|
+
loggerWithMetadata.trace`${message}`;
|
|
232
|
+
return;
|
|
233
|
+
case "debug":
|
|
234
|
+
loggerWithMetadata.debug`${message}`;
|
|
235
|
+
return;
|
|
236
|
+
case "info":
|
|
237
|
+
loggerWithMetadata.info`${message}`;
|
|
238
|
+
return;
|
|
239
|
+
case "warn":
|
|
240
|
+
loggerWithMetadata.warn`${message}`;
|
|
241
|
+
return;
|
|
242
|
+
case "error":
|
|
243
|
+
loggerWithMetadata.error`${message}`;
|
|
244
|
+
return;
|
|
245
|
+
case "fatal":
|
|
246
|
+
loggerWithMetadata.fatal`${message}`;
|
|
247
|
+
return;
|
|
248
|
+
default:
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function ensureLogtapeBackendConfigured() {
|
|
253
|
+
const currentConfig = getConfig();
|
|
254
|
+
if (logtapeBackendConfigured) {
|
|
255
|
+
logtapeBridgeEnabled = isLogtapeBridgeConfigured(currentConfig);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (currentConfig !== null) {
|
|
259
|
+
logtapeBridgeEnabled = isLogtapeBridgeConfigured(currentConfig);
|
|
260
|
+
logtapeBackendConfigured = true;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
configureSync({
|
|
265
|
+
sinks: {
|
|
266
|
+
[LOGTAPE_BRIDGE_SINK](record) {
|
|
267
|
+
const loggerId = record.properties[LOGGER_ID_PROPERTY];
|
|
268
|
+
if (typeof loggerId !== "string") {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const sinks = loggerSinkRegistry.get(loggerId);
|
|
272
|
+
if (!sinks || sinks.size === 0) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const metadata = { ...record.properties };
|
|
276
|
+
delete metadata[LOGGER_ID_PROPERTY];
|
|
277
|
+
const categoryParts = record.category[0] === LOGTAPE_BRIDGE_CATEGORY ? record.category.slice(1) : record.category;
|
|
278
|
+
const converted = {
|
|
279
|
+
timestamp: record.timestamp,
|
|
280
|
+
level: fromLogtapeLevel(record.level),
|
|
281
|
+
category: categoryParts.join("."),
|
|
282
|
+
message: stringifyLogtapeMessage(record.message),
|
|
283
|
+
...Object.keys(metadata).length > 0 ? { metadata } : {}
|
|
284
|
+
};
|
|
285
|
+
dispatchRecordToSinks(sinks, converted);
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
loggers: [
|
|
289
|
+
{
|
|
290
|
+
category: [LOGTAPE_BRIDGE_CATEGORY],
|
|
291
|
+
sinks: [LOGTAPE_BRIDGE_SINK],
|
|
292
|
+
lowestLevel: "trace"
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
category: ["logtape", "meta"],
|
|
296
|
+
lowestLevel: "error"
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
});
|
|
300
|
+
} catch {
|
|
301
|
+
logtapeBridgeEnabled = false;
|
|
302
|
+
}
|
|
303
|
+
logtapeBackendConfigured = true;
|
|
304
|
+
}
|
|
138
305
|
function createLoggerFromState(state) {
|
|
139
306
|
const log = (level, message, metadata) => {
|
|
140
307
|
if (!shouldLog(level, state.level))
|
|
@@ -153,22 +320,21 @@ function createLoggerFromState(state) {
|
|
|
153
320
|
processedMessage = applyPatterns(message, allPatterns, replacement);
|
|
154
321
|
}
|
|
155
322
|
}
|
|
156
|
-
|
|
323
|
+
if (logtapeBridgeEnabled) {
|
|
324
|
+
emitViaLogtape(state.backendLogger, level, processedMessage, {
|
|
325
|
+
...processedMetadata ?? {},
|
|
326
|
+
[LOGGER_ID_PROPERTY]: state.loggerId
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const directRecord = {
|
|
157
331
|
timestamp: Date.now(),
|
|
158
332
|
level,
|
|
159
333
|
category: state.name,
|
|
160
334
|
message: processedMessage,
|
|
161
|
-
...processedMetadata
|
|
335
|
+
...processedMetadata ? { metadata: processedMetadata } : {}
|
|
162
336
|
};
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
let formatted;
|
|
166
|
-
if (sink.formatter) {
|
|
167
|
-
formatted = sink.formatter.format(record);
|
|
168
|
-
}
|
|
169
|
-
sink.write(record, formatted);
|
|
170
|
-
} catch {}
|
|
171
|
-
}
|
|
337
|
+
dispatchRecordToSinks(state.sinks, directRecord);
|
|
172
338
|
};
|
|
173
339
|
return {
|
|
174
340
|
trace: (message, metadata) => log("trace", message, metadata),
|
|
@@ -184,9 +350,12 @@ function createLoggerFromState(state) {
|
|
|
184
350
|
addSink: (sink) => {
|
|
185
351
|
state.sinks.push(sink);
|
|
186
352
|
registeredSinks.add(sink);
|
|
353
|
+
registerLoggerSink(state.loggerId, sink);
|
|
187
354
|
},
|
|
188
355
|
child: (context) => {
|
|
189
356
|
const childState = {
|
|
357
|
+
loggerId: state.loggerId,
|
|
358
|
+
backendLogger: state.backendLogger,
|
|
190
359
|
name: state.name,
|
|
191
360
|
level: state.level,
|
|
192
361
|
context: { ...state.context, ...context },
|
|
@@ -198,7 +367,11 @@ function createLoggerFromState(state) {
|
|
|
198
367
|
};
|
|
199
368
|
}
|
|
200
369
|
function createLogger(config) {
|
|
370
|
+
ensureLogtapeBackendConfigured();
|
|
371
|
+
const loggerId = `logger-${++loggerIdCounter}`;
|
|
201
372
|
const state = {
|
|
373
|
+
loggerId,
|
|
374
|
+
backendLogger: getLogger([LOGTAPE_BRIDGE_CATEGORY, config.name]),
|
|
202
375
|
name: config.name,
|
|
203
376
|
level: config.level ?? "info",
|
|
204
377
|
context: config.context ?? {},
|
|
@@ -207,6 +380,7 @@ function createLogger(config) {
|
|
|
207
380
|
};
|
|
208
381
|
for (const sink of state.sinks) {
|
|
209
382
|
registeredSinks.add(sink);
|
|
383
|
+
registerLoggerSink(loggerId, sink);
|
|
210
384
|
}
|
|
211
385
|
return createLoggerFromState(state);
|
|
212
386
|
}
|
|
@@ -305,7 +479,7 @@ function createPrettyFormatter(options) {
|
|
|
305
479
|
}
|
|
306
480
|
function createConsoleSink(options) {
|
|
307
481
|
const useColors = options?.colors ?? (typeof process !== "undefined" ? Boolean(process.stdout?.isTTY) : false);
|
|
308
|
-
const formatter = createPrettyFormatter({ colors: useColors });
|
|
482
|
+
const formatter = options?.formatter ?? createPrettyFormatter({ colors: useColors });
|
|
309
483
|
const sink = {
|
|
310
484
|
formatter,
|
|
311
485
|
write(record, formatted) {
|
|
@@ -378,18 +552,113 @@ function configureRedaction(config) {
|
|
|
378
552
|
];
|
|
379
553
|
}
|
|
380
554
|
}
|
|
381
|
-
async function
|
|
555
|
+
async function flushSinks(sinks) {
|
|
382
556
|
const flushPromises = [];
|
|
383
|
-
for (const sink of
|
|
557
|
+
for (const sink of sinks) {
|
|
384
558
|
if (sink.flush) {
|
|
385
|
-
flushPromises.push(sink.flush());
|
|
559
|
+
flushPromises.push(sink.flush().catch(() => {}));
|
|
386
560
|
}
|
|
387
561
|
}
|
|
388
562
|
await Promise.all(flushPromises);
|
|
389
563
|
}
|
|
564
|
+
async function flush() {
|
|
565
|
+
await flushSinks(registeredSinks);
|
|
566
|
+
}
|
|
567
|
+
function safeGetEnv(key) {
|
|
568
|
+
if (typeof process !== "undefined") {
|
|
569
|
+
return process.env?.[key];
|
|
570
|
+
}
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
var ENV_LEVEL_MAP = {
|
|
574
|
+
trace: "trace",
|
|
575
|
+
debug: "debug",
|
|
576
|
+
info: "info",
|
|
577
|
+
notice: "info",
|
|
578
|
+
warn: "warn",
|
|
579
|
+
warning: "warn",
|
|
580
|
+
error: "error",
|
|
581
|
+
critical: "fatal",
|
|
582
|
+
alert: "fatal",
|
|
583
|
+
emergency: "fatal",
|
|
584
|
+
fatal: "fatal",
|
|
585
|
+
silent: "silent"
|
|
586
|
+
};
|
|
587
|
+
function resolveLogLevel(level) {
|
|
588
|
+
const envLogLevel = safeGetEnv("OUTFITTER_LOG_LEVEL");
|
|
589
|
+
if (envLogLevel !== undefined && Object.hasOwn(ENV_LEVEL_MAP, envLogLevel)) {
|
|
590
|
+
return ENV_LEVEL_MAP[envLogLevel];
|
|
591
|
+
}
|
|
592
|
+
if (level !== undefined && Object.hasOwn(ENV_LEVEL_MAP, level)) {
|
|
593
|
+
return ENV_LEVEL_MAP[level];
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
const env = _getEnvironment();
|
|
597
|
+
const defaults = _getEnvironmentDefaults(env);
|
|
598
|
+
if (defaults.logLevel !== null && Object.hasOwn(ENV_LEVEL_MAP, defaults.logLevel)) {
|
|
599
|
+
return ENV_LEVEL_MAP[defaults.logLevel];
|
|
600
|
+
}
|
|
601
|
+
} catch {}
|
|
602
|
+
return "info";
|
|
603
|
+
}
|
|
604
|
+
function resolveOutfitterLogLevel(level) {
|
|
605
|
+
const envLogLevel = safeGetEnv("OUTFITTER_LOG_LEVEL");
|
|
606
|
+
if (envLogLevel !== undefined && Object.hasOwn(ENV_LEVEL_MAP, envLogLevel)) {
|
|
607
|
+
return ENV_LEVEL_MAP[envLogLevel];
|
|
608
|
+
}
|
|
609
|
+
if (level !== undefined && Object.hasOwn(ENV_LEVEL_MAP, level)) {
|
|
610
|
+
return ENV_LEVEL_MAP[level];
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
const env = _getEnvironment();
|
|
614
|
+
const defaults = _getEnvironmentDefaults(env);
|
|
615
|
+
if (defaults.logLevel !== null && Object.hasOwn(ENV_LEVEL_MAP, defaults.logLevel)) {
|
|
616
|
+
return ENV_LEVEL_MAP[defaults.logLevel];
|
|
617
|
+
}
|
|
618
|
+
} catch {}
|
|
619
|
+
return "silent";
|
|
620
|
+
}
|
|
621
|
+
function createOutfitterLoggerAdapter(options) {
|
|
622
|
+
const factorySinks = new Set;
|
|
623
|
+
return {
|
|
624
|
+
createLogger(config) {
|
|
625
|
+
const backend = config.backend;
|
|
626
|
+
const sinks = backend?.sinks ?? options?.defaults?.sinks ?? [createConsoleSink()];
|
|
627
|
+
const defaultRedaction = mergeRedactionConfig({ enabled: true }, options?.defaults?.redaction);
|
|
628
|
+
const redaction = mergeRedactionConfig(defaultRedaction, backend?.redaction);
|
|
629
|
+
const loggerConfig = {
|
|
630
|
+
name: config.name,
|
|
631
|
+
level: resolveOutfitterLogLevel(config.level),
|
|
632
|
+
sinks,
|
|
633
|
+
...config.context !== undefined ? { context: config.context } : {},
|
|
634
|
+
...redaction !== undefined ? { redaction } : {}
|
|
635
|
+
};
|
|
636
|
+
for (const sink of sinks) {
|
|
637
|
+
factorySinks.add(sink);
|
|
638
|
+
}
|
|
639
|
+
const logger = createLogger(loggerConfig);
|
|
640
|
+
const originalAddSink = logger.addSink.bind(logger);
|
|
641
|
+
logger.addSink = (sink) => {
|
|
642
|
+
factorySinks.add(sink);
|
|
643
|
+
originalAddSink(sink);
|
|
644
|
+
};
|
|
645
|
+
return logger;
|
|
646
|
+
},
|
|
647
|
+
async flush() {
|
|
648
|
+
await flushSinks(factorySinks);
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function createOutfitterLoggerFactory(options) {
|
|
653
|
+
return createContractLoggerFactory(createOutfitterLoggerAdapter(options));
|
|
654
|
+
}
|
|
390
655
|
export {
|
|
656
|
+
resolveOutfitterLogLevel,
|
|
657
|
+
resolveLogLevel,
|
|
391
658
|
flush,
|
|
392
659
|
createPrettyFormatter,
|
|
660
|
+
createOutfitterLoggerFactory,
|
|
661
|
+
createOutfitterLoggerAdapter,
|
|
393
662
|
createLogger,
|
|
394
663
|
createJsonFormatter,
|
|
395
664
|
createFileSink,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outfitter/logging",
|
|
3
3
|
"description": "Structured logging via logtape with redaction support for Outfitter",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist"
|
|
@@ -27,10 +27,15 @@
|
|
|
27
27
|
"clean": "rm -rf dist"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@outfitter/contracts": "0.2.0",
|
|
31
30
|
"@logtape/logtape": "^2.0.0"
|
|
32
31
|
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@outfitter/config": ">=0.3.0",
|
|
34
|
+
"@outfitter/contracts": ">=0.2.0"
|
|
35
|
+
},
|
|
33
36
|
"devDependencies": {
|
|
37
|
+
"@outfitter/config": "0.3.1",
|
|
38
|
+
"@outfitter/contracts": "0.3.0",
|
|
34
39
|
"@types/bun": "latest",
|
|
35
40
|
"typescript": "^5.8.0"
|
|
36
41
|
},
|