@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 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
- export { resolveLogLevel, flush, createPrettyFormatter, createLogger, createJsonFormatter, createFileSink, createConsoleSink, createChildLogger, configureRedaction, Sink, RedactionConfig, PrettyFormatterOptions, LoggerInstance, LoggerConfig, LogRecord, LogLevel, GlobalRedactionConfig, Formatter, FileSinkOptions, DEFAULT_PATTERNS, ConsoleSinkOptions };
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
- 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 = {
161
331
  timestamp: Date.now(),
162
332
  level,
163
333
  category: state.name,
164
334
  message: processedMessage,
165
- ...processedMetadata !== undefined ? { metadata: processedMetadata } : {}
335
+ ...processedMetadata ? { metadata: processedMetadata } : {}
166
336
  };
167
- for (const sink of state.sinks) {
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 flush() {
555
+ async function flushSinks(sinks) {
386
556
  const flushPromises = [];
387
- for (const sink of registeredSinks) {
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 = process.env["OUTFITTER_LOG_LEVEL"];
410
- if (envLogLevel !== undefined) {
411
- const mapped = ENV_LEVEL_MAP[envLogLevel];
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
- const env = _getEnvironment();
420
- const defaults = _getEnvironmentDefaults(env);
421
- if (defaults.logLevel !== null) {
422
- const mapped = ENV_LEVEL_MAP[defaults.logLevel];
423
- if (mapped !== undefined) {
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.3.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
  },