@outfitter/logging 0.3.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 +106 -0
- package/dist/index.d.ts +72 -3
- package/dist/index.js +259 -30
- package/package.json +7 -3
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:
|
|
@@ -310,6 +323,8 @@ process.exit(0);
|
|
|
310
323
|
|
|
311
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.
|
|
312
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
|
+
|
|
313
328
|
**Precedence** (highest wins):
|
|
314
329
|
1. `OUTFITTER_LOG_LEVEL` environment variable
|
|
315
330
|
2. Explicit `level` parameter
|
|
@@ -328,10 +343,97 @@ const logger = createLogger({
|
|
|
328
343
|
// With OUTFITTER_ENV=development → "debug"
|
|
329
344
|
// With OUTFITTER_LOG_LEVEL=error → "error" (overrides everything)
|
|
330
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
|
+
});
|
|
331
354
|
```
|
|
332
355
|
|
|
333
356
|
MCP-style level names are mapped automatically: `warning` to `warn`, `emergency`/`critical`/`alert` to `fatal`, `notice` to `info`.
|
|
334
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
|
+
|
|
335
437
|
## API Reference
|
|
336
438
|
|
|
337
439
|
### Functions
|
|
@@ -363,6 +465,10 @@ MCP-style level names are mapped automatically: `warning` to `warn`, `emergency`
|
|
|
363
465
|
| `PrettyFormatterOptions` | Options for human-readable formatter |
|
|
364
466
|
| `FileSinkOptions` | Options for file sink configuration |
|
|
365
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
|
+
|
|
366
472
|
## License
|
|
367
473
|
|
|
368
474
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Logger as ContractLogger, LoggerAdapter as ContractLoggerAdapter, LoggerFactory as ContractLoggerFactory } from "@outfitter/contracts";
|
|
1
2
|
/**
|
|
2
3
|
* Log levels supported by the logger, ordered from lowest to highest severity.
|
|
3
4
|
*
|
|
@@ -185,6 +186,32 @@ interface LoggerConfig {
|
|
|
185
186
|
redaction?: RedactionConfig;
|
|
186
187
|
}
|
|
187
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
|
+
/**
|
|
188
215
|
* Logger instance with methods for each log level.
|
|
189
216
|
*
|
|
190
217
|
* Provides methods for logging at each severity level, plus utilities for
|
|
@@ -201,7 +228,7 @@ interface LoggerConfig {
|
|
|
201
228
|
* logger.debug("Debug info", { details: "..." });
|
|
202
229
|
* ```
|
|
203
230
|
*/
|
|
204
|
-
interface LoggerInstance {
|
|
231
|
+
interface LoggerInstance extends ContractLogger {
|
|
205
232
|
/**
|
|
206
233
|
* Log at trace level (most verbose, for detailed debugging).
|
|
207
234
|
* @param message - Human-readable log message
|
|
@@ -333,6 +360,17 @@ interface ConsoleSinkOptions {
|
|
|
333
360
|
* - `false`: Never use colors
|
|
334
361
|
*/
|
|
335
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;
|
|
336
374
|
}
|
|
337
375
|
/**
|
|
338
376
|
* Options for the file sink.
|
|
@@ -568,5 +606,36 @@ declare function flush(): Promise<void>;
|
|
|
568
606
|
* // With nothing set → "info"
|
|
569
607
|
* ```
|
|
570
608
|
*/
|
|
571
|
-
declare function resolveLogLevel(level?: LogLevel): LogLevel;
|
|
572
|
-
|
|
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,8 +1,16 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
configureSync,
|
|
4
|
+
getConfig,
|
|
5
|
+
getLogger
|
|
6
|
+
} from "@logtape/logtape";
|
|
2
7
|
import {
|
|
3
8
|
getEnvironment as _getEnvironment,
|
|
4
9
|
getEnvironmentDefaults as _getEnvironmentDefaults
|
|
5
10
|
} from "@outfitter/config";
|
|
11
|
+
import {
|
|
12
|
+
createLoggerFactory as createContractLoggerFactory
|
|
13
|
+
} from "@outfitter/contracts";
|
|
6
14
|
var LEVEL_PRIORITY = {
|
|
7
15
|
trace: 0,
|
|
8
16
|
debug: 1,
|
|
@@ -27,6 +35,13 @@ var globalRedactionConfig = {
|
|
|
27
35
|
keys: []
|
|
28
36
|
};
|
|
29
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";
|
|
30
45
|
function shouldLog(level, minLevel) {
|
|
31
46
|
if (minLevel === "silent")
|
|
32
47
|
return false;
|
|
@@ -139,6 +154,154 @@ function processNestedForErrors(obj) {
|
|
|
139
154
|
}
|
|
140
155
|
return result;
|
|
141
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
|
+
}
|
|
142
305
|
function createLoggerFromState(state) {
|
|
143
306
|
const log = (level, message, metadata) => {
|
|
144
307
|
if (!shouldLog(level, state.level))
|
|
@@ -157,22 +320,21 @@ function createLoggerFromState(state) {
|
|
|
157
320
|
processedMessage = applyPatterns(message, allPatterns, replacement);
|
|
158
321
|
}
|
|
159
322
|
}
|
|
160
|
-
|
|
323
|
+
if (logtapeBridgeEnabled) {
|
|
324
|
+
emitViaLogtape(state.backendLogger, level, processedMessage, {
|
|
325
|
+
...processedMetadata ?? {},
|
|
326
|
+
[LOGGER_ID_PROPERTY]: state.loggerId
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const directRecord = {
|
|
161
331
|
timestamp: Date.now(),
|
|
162
332
|
level,
|
|
163
333
|
category: state.name,
|
|
164
334
|
message: processedMessage,
|
|
165
|
-
...processedMetadata
|
|
335
|
+
...processedMetadata ? { metadata: processedMetadata } : {}
|
|
166
336
|
};
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
let formatted;
|
|
170
|
-
if (sink.formatter) {
|
|
171
|
-
formatted = sink.formatter.format(record);
|
|
172
|
-
}
|
|
173
|
-
sink.write(record, formatted);
|
|
174
|
-
} catch {}
|
|
175
|
-
}
|
|
337
|
+
dispatchRecordToSinks(state.sinks, directRecord);
|
|
176
338
|
};
|
|
177
339
|
return {
|
|
178
340
|
trace: (message, metadata) => log("trace", message, metadata),
|
|
@@ -188,9 +350,12 @@ function createLoggerFromState(state) {
|
|
|
188
350
|
addSink: (sink) => {
|
|
189
351
|
state.sinks.push(sink);
|
|
190
352
|
registeredSinks.add(sink);
|
|
353
|
+
registerLoggerSink(state.loggerId, sink);
|
|
191
354
|
},
|
|
192
355
|
child: (context) => {
|
|
193
356
|
const childState = {
|
|
357
|
+
loggerId: state.loggerId,
|
|
358
|
+
backendLogger: state.backendLogger,
|
|
194
359
|
name: state.name,
|
|
195
360
|
level: state.level,
|
|
196
361
|
context: { ...state.context, ...context },
|
|
@@ -202,7 +367,11 @@ function createLoggerFromState(state) {
|
|
|
202
367
|
};
|
|
203
368
|
}
|
|
204
369
|
function createLogger(config) {
|
|
370
|
+
ensureLogtapeBackendConfigured();
|
|
371
|
+
const loggerId = `logger-${++loggerIdCounter}`;
|
|
205
372
|
const state = {
|
|
373
|
+
loggerId,
|
|
374
|
+
backendLogger: getLogger([LOGTAPE_BRIDGE_CATEGORY, config.name]),
|
|
206
375
|
name: config.name,
|
|
207
376
|
level: config.level ?? "info",
|
|
208
377
|
context: config.context ?? {},
|
|
@@ -211,6 +380,7 @@ function createLogger(config) {
|
|
|
211
380
|
};
|
|
212
381
|
for (const sink of state.sinks) {
|
|
213
382
|
registeredSinks.add(sink);
|
|
383
|
+
registerLoggerSink(loggerId, sink);
|
|
214
384
|
}
|
|
215
385
|
return createLoggerFromState(state);
|
|
216
386
|
}
|
|
@@ -309,7 +479,7 @@ function createPrettyFormatter(options) {
|
|
|
309
479
|
}
|
|
310
480
|
function createConsoleSink(options) {
|
|
311
481
|
const useColors = options?.colors ?? (typeof process !== "undefined" ? Boolean(process.stdout?.isTTY) : false);
|
|
312
|
-
const formatter = createPrettyFormatter({ colors: useColors });
|
|
482
|
+
const formatter = options?.formatter ?? createPrettyFormatter({ colors: useColors });
|
|
313
483
|
const sink = {
|
|
314
484
|
formatter,
|
|
315
485
|
write(record, formatted) {
|
|
@@ -382,15 +552,24 @@ function configureRedaction(config) {
|
|
|
382
552
|
];
|
|
383
553
|
}
|
|
384
554
|
}
|
|
385
|
-
async function
|
|
555
|
+
async function flushSinks(sinks) {
|
|
386
556
|
const flushPromises = [];
|
|
387
|
-
for (const sink of
|
|
557
|
+
for (const sink of sinks) {
|
|
388
558
|
if (sink.flush) {
|
|
389
|
-
flushPromises.push(sink.flush());
|
|
559
|
+
flushPromises.push(sink.flush().catch(() => {}));
|
|
390
560
|
}
|
|
391
561
|
}
|
|
392
562
|
await Promise.all(flushPromises);
|
|
393
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
|
+
}
|
|
394
573
|
var ENV_LEVEL_MAP = {
|
|
395
574
|
trace: "trace",
|
|
396
575
|
debug: "debug",
|
|
@@ -406,30 +585,80 @@ var ENV_LEVEL_MAP = {
|
|
|
406
585
|
silent: "silent"
|
|
407
586
|
};
|
|
408
587
|
function resolveLogLevel(level) {
|
|
409
|
-
const envLogLevel =
|
|
410
|
-
if (envLogLevel !== undefined) {
|
|
411
|
-
|
|
412
|
-
if (mapped !== undefined) {
|
|
413
|
-
return mapped;
|
|
414
|
-
}
|
|
588
|
+
const envLogLevel = safeGetEnv("OUTFITTER_LOG_LEVEL");
|
|
589
|
+
if (envLogLevel !== undefined && Object.hasOwn(ENV_LEVEL_MAP, envLogLevel)) {
|
|
590
|
+
return ENV_LEVEL_MAP[envLogLevel];
|
|
415
591
|
}
|
|
416
|
-
if (level !== undefined) {
|
|
417
|
-
return level;
|
|
592
|
+
if (level !== undefined && Object.hasOwn(ENV_LEVEL_MAP, level)) {
|
|
593
|
+
return ENV_LEVEL_MAP[level];
|
|
418
594
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return mapped;
|
|
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];
|
|
425
600
|
}
|
|
426
|
-
}
|
|
601
|
+
} catch {}
|
|
427
602
|
return "info";
|
|
428
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
|
+
}
|
|
429
655
|
export {
|
|
656
|
+
resolveOutfitterLogLevel,
|
|
430
657
|
resolveLogLevel,
|
|
431
658
|
flush,
|
|
432
659
|
createPrettyFormatter,
|
|
660
|
+
createOutfitterLoggerFactory,
|
|
661
|
+
createOutfitterLoggerAdapter,
|
|
433
662
|
createLogger,
|
|
434
663
|
createJsonFormatter,
|
|
435
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,11 +27,15 @@
|
|
|
27
27
|
"clean": "rm -rf dist"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@outfitter/config": "0.3.0",
|
|
31
|
-
"@outfitter/contracts": "0.2.0",
|
|
32
30
|
"@logtape/logtape": "^2.0.0"
|
|
33
31
|
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@outfitter/config": ">=0.3.0",
|
|
34
|
+
"@outfitter/contracts": ">=0.2.0"
|
|
35
|
+
},
|
|
34
36
|
"devDependencies": {
|
|
37
|
+
"@outfitter/config": "0.3.1",
|
|
38
|
+
"@outfitter/contracts": "0.3.0",
|
|
35
39
|
"@types/bun": "latest",
|
|
36
40
|
"typescript": "^5.8.0"
|
|
37
41
|
},
|