@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 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
- export { flush, createPrettyFormatter, createLogger, createJsonFormatter, createFileSink, createConsoleSink, createChildLogger, configureRedaction, Sink, RedactionConfig, PrettyFormatterOptions, LoggerInstance, LoggerConfig, LogRecord, LogLevel, GlobalRedactionConfig, Formatter, FileSinkOptions, DEFAULT_PATTERNS, ConsoleSinkOptions };
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
- const record = {
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 !== undefined ? { metadata: processedMetadata } : {}
335
+ ...processedMetadata ? { metadata: processedMetadata } : {}
162
336
  };
163
- for (const sink of state.sinks) {
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 flush() {
555
+ async function flushSinks(sinks) {
382
556
  const flushPromises = [];
383
- for (const sink of registeredSinks) {
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.2.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
  },