Skip to content

@carrot/logging โ€‹

ts

Structured logging abstraction with pluggable sinks. Mirrors the shape of .NET's Carrot.Logging - fluent configuration, severity filtering per sink, contextual property scoping, and correlation ids.

Installation โ€‹

ts
import { Log, consoleSink } from '@carrot/logging';

Zero dependencies. Browser and Node compatible.

Architecture โ€‹

Fluent Configuration โ€‹

Follows the .NET builder pattern - Log.configure() returns a LogConfiguration that collects sinks and settings, then build() locks it and produces a Log instance:

ts
const log = Log.configure()
    .minLevel('debug')
    .addSink(consoleSink(), 'info')
    .addSink(appInsightsSink({ connectionString: '...' }), 'warn')
    .if(isDev, c => c.addSink(consoleSink({ colour: true }), 'debug'))
    .build();

Each sink gets its own minimum severity gate, independent of the logger's global minimum.

Log Modules โ€‹

Every severity level is a LogModule - a callable function that doubles as an object:

ts
log.info('message');                        // callable
log.info('message', { userId: 42 });        // with properties
log.info.write({ message: '...', error });  // full options
log.info.enabled = false;                   // toggle at runtime

Custom modules add a label tag so sinks can distinguish [Audit] from a plain info:

ts
const audit = log.addModule('audit', 'info');
audit.write('User changed password');

Context Stack โ€‹

LogContext manages a synchronous stack of property bags. Every active scope's properties are merged into each LogEvent at emit time:

ts
const scope = log.context.push({ requestId: 'abc-123' });
log.info('Processing');  // event.properties includes requestId
scope.dispose();

Supports using via Symbol.dispose and callback-scoped variants (run, runAsync).

Correlation IDs โ€‹

Every write() call returns a unique id (YYYYMMDDHHmmss + 8 hex). Nested writes can reference a parent id for tracing call chains:

ts
const id = log.info('Started batch');
log.debug.writeNested(id, 'Processing item 1');

Sink Interface โ€‹

Sinks are intentionally minimal:

ts
interface LogSink {
    emit(event: LogEvent): void;
    flush?(): void | Promise<void>;
}

The emit method receives a fully-resolved LogEvent with merged context properties, timestamp, and correlation id. Sinks that buffer (e.g. HTTP batch sinks) implement flush().

Sink errors are swallowed - a sink must never bring down the app.

File Structure โ€‹

src/
โ”œโ”€โ”€ index.ts            # Public API barrel
โ”œโ”€โ”€ types.ts            # LogLevel, LogEvent, LogWriteInput
โ”œโ”€โ”€ log.ts              # Log class - owns sinks, context, modules
โ”œโ”€โ”€ configuration.ts    # LogConfiguration fluent builder
โ”œโ”€โ”€ context.ts          # LogContext property scope stack
โ”œโ”€โ”€ module.ts           # LogModule factory and emit logic
โ”œโ”€โ”€ id.ts               # Correlation ID generation
โ”œโ”€โ”€ sink.ts             # LogSink interface + SinkRegistration
โ””โ”€โ”€ sinks/
    โ””โ”€โ”€ console.ts      # Built-in console sink

Dependencies โ€‹

None.

Build โ€‹

bash
npm run build

Compiles TypeScript to ESM via tsc. Output lands in dist/.


Using @carrot/logging โ€‹

Structured logging with pluggable sinks, scoped context, and per-sink severity filtering.

Import โ€‹

ts
import { Log, consoleSink } from '@carrot/logging';

Common Patterns โ€‹

1. Quick console logger โ€‹

ts
const log = Log.console();           // info+ to console
const log = Log.console('debug');    // debug+ to console

2. Multi-sink setup โ€‹

ts
import { appInsightsSink } from '@carrot/logging-applicationinsights';

const log = Log.configure()
    .minLevel('debug')
    .addSink(consoleSink(), 'debug')
    .addSink(appInsightsSink({ connectionString: AI_CONN }), 'warn')
    .build();

Each sink filters independently - console gets everything from debug up, App Insights only gets warnings and above.

3. Writing log entries โ€‹

ts
log.info('User signed in');
log.warn('Retry attempt', { attempt: 3, endpoint: '/api/data' });
log.error.write({ message: 'Request failed', error: err, properties: { url } });

Every level module (verbose, debug, info, warn, error, fatal) is both callable and has a .write() method.

4. Scoped context โ€‹

Push properties that automatically appear on every event while the scope is active:

ts
const scope = log.context.push({ requestId: 'abc-123', userId: 42 });

log.info('Processing request');   // includes requestId + userId
log.debug('Fetching data');       // includes requestId + userId

scope.dispose();                  // properties removed

With using (explicit resource management):

ts
{
    using scope = log.context.push({ requestId: 'abc-123' });
    log.info('Scoped log entry');
}
// scope auto-disposed

Callback-scoped:

ts
log.context.run({ batchId: 'xyz' }, () => {
    log.info('Inside batch');     // includes batchId
});

5. Correlation IDs โ€‹

Every write returns a unique id. Use it to link related entries:

ts
const batchId = log.info('Batch started');

for (const item of items) {
    log.debug.writeNested(batchId, `Processing ${item.name}`);
}

6. Custom modules โ€‹

Tag log entries with a label for filtering in sinks:

ts
const audit = log.addModule('audit', 'info');
audit('User changed password');      // tagged [audit] at info severity

const perf = log.addModule('perf', 'debug');
perf('Render took 12ms');            // tagged [perf] at debug severity

7. Conditional configuration โ€‹

ts
const log = Log.configure()
    .minLevel('info')
    .addSink(appInsightsSink({ connectionString: AI_CONN }))
    .if(import.meta.env.DEV, c => c.addSink(consoleSink(), 'debug'))
    .build();

8. Toggling modules at runtime โ€‹

ts
log.verbose.enabled = false;  // suppress verbose output
log.debug.enabled = false;    // suppress debug output

9. Flushing before exit โ€‹

ts
await log.flush();  // waits for all sinks that buffer

Console Sink Options โ€‹

ts
consoleSink({
    timestamp: true,    // [HH:mm:ss] prefix
    properties: true,   // structured properties in output
    colour: true,       // use console.info/warn/error for colour
});

Tips โ€‹

  • Start with Log.console() during development, wire up real sinks later.
  • Use context scopes for request/session tracing - avoids passing ids through every call.
  • Set per-sink levels to reduce noise in production sinks while keeping verbose output locally.
  • Always flush() before process exit if you have buffered sinks (e.g. App Insights).

Package Contents โ€‹

@carrot/logging-applicationinsights โ€‹

Import: import { ... } from '@carrot/logging-applicationinsights';

Factory โ€‹

appInsightsSink(options) - creates a LogSink & AppInsightsSinkAccessor

Options โ€‹

FieldTypeDescription
connectionStringstring?AI connection string. Required unless instance is provided
instanceApplicationInsights?Bring-your-own AI instance - skips creation
configPartial<IConfiguration & IConfig>?Additional AI SDK config (merged with defaults)
basePropertiesRecord<string, unknown>?Static properties attached to every telemetry item

Accessor โ€‹

AppInsightsSinkAccessor - { readonly appInsights: ApplicationInsights }

Exposes the underlying AI instance for custom events, metrics, or page views without creating a second instance.

Re-exports โ€‹

ApplicationInsights ยท SeverityLevel - re-exported types from @microsoft/applicationinsights-web


@carrot/logging-vue โ€‹

Import: import { ... } from '@carrot/logging-vue';

Plugin โ€‹

carrotLogging - Vue 3 plugin (app.use(carrotLogging, options))

CarrotLoggingPluginOptions:

FieldTypeDefaultDescription
logLogrequiredThe logger instance to provide
routerRouter?-If provided, binds navigation logging
routerOptionsRouterBindingOptions?-Options forwarded to bindLogToRouter
captureErrorsbooleantrueWire app.config.errorHandler to log unhandled errors
captureWarningsbooleantrueWire app.config.warnHandler to log Vue warnings

Composable โ€‹

useLog() - returns the Log instance from injection context. Throws if the plugin isn't installed.

LogKey - InjectionKey<Log> symbol for manual provide/inject.

Router Binding โ€‹

bindLogToRouter(log, router, options?) - logs an info entry on every navigation. Returns a teardown function.

RouterBindingOptions:

FieldTypeDefaultDescription
includeQuerybooleanfalseInclude route query params (disabled by default to avoid leaking sensitive data)
includeParamsbooleantrueInclude route params
includeMetabooleantrueInclude route meta

Carrot