@spinajs/log 2.0.481 → 2.0.482
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 +744 -0
- package/lib/cjs/archive/LogArchiveService.d.ts +75 -0
- package/lib/cjs/archive/LogArchiveService.d.ts.map +1 -0
- package/lib/cjs/archive/LogArchiveService.js +189 -0
- package/lib/cjs/archive/LogArchiveService.js.map +1 -0
- package/lib/cjs/archive/archive-strategy.d.ts +32 -0
- package/lib/cjs/archive/archive-strategy.d.ts.map +1 -0
- package/lib/cjs/archive/archive-strategy.js +43 -0
- package/lib/cjs/archive/archive-strategy.js.map +1 -0
- package/lib/cjs/archive/context.d.ts +50 -0
- package/lib/cjs/archive/context.d.ts.map +1 -0
- package/lib/cjs/archive/context.js +3 -0
- package/lib/cjs/archive/context.js.map +1 -0
- package/lib/cjs/archive/index.d.ts +10 -0
- package/lib/cjs/archive/index.d.ts.map +1 -0
- package/lib/cjs/archive/index.js +26 -0
- package/lib/cjs/archive/index.js.map +1 -0
- package/lib/cjs/archive/retention-strategy.d.ts +26 -0
- package/lib/cjs/archive/retention-strategy.d.ts.map +1 -0
- package/lib/cjs/archive/retention-strategy.js +42 -0
- package/lib/cjs/archive/retention-strategy.js.map +1 -0
- package/lib/cjs/archive/strategies/AgeLogRetentionStrategy.d.ts +10 -0
- package/lib/cjs/archive/strategies/AgeLogRetentionStrategy.d.ts.map +1 -0
- package/lib/cjs/archive/strategies/AgeLogRetentionStrategy.js +46 -0
- package/lib/cjs/archive/strategies/AgeLogRetentionStrategy.js.map +1 -0
- package/lib/cjs/archive/strategies/CountLogRetentionStrategy.d.ts +10 -0
- package/lib/cjs/archive/strategies/CountLogRetentionStrategy.d.ts.map +1 -0
- package/lib/cjs/archive/strategies/CountLogRetentionStrategy.js +42 -0
- package/lib/cjs/archive/strategies/CountLogRetentionStrategy.js.map +1 -0
- package/lib/cjs/archive/strategies/CronLogArchiveStrategy.d.ts +15 -0
- package/lib/cjs/archive/strategies/CronLogArchiveStrategy.d.ts.map +1 -0
- package/lib/cjs/archive/strategies/CronLogArchiveStrategy.js +52 -0
- package/lib/cjs/archive/strategies/CronLogArchiveStrategy.js.map +1 -0
- package/lib/cjs/archive/strategies/SizeLogArchiveStrategy.d.ts +18 -0
- package/lib/cjs/archive/strategies/SizeLogArchiveStrategy.d.ts.map +1 -0
- package/lib/cjs/archive/strategies/SizeLogArchiveStrategy.js +63 -0
- package/lib/cjs/archive/strategies/SizeLogArchiveStrategy.js.map +1 -0
- package/lib/cjs/archive/strategies/util.d.ts +28 -0
- package/lib/cjs/archive/strategies/util.d.ts.map +1 -0
- package/lib/cjs/archive/strategies/util.js +59 -0
- package/lib/cjs/archive/strategies/util.js.map +1 -0
- package/lib/cjs/bootstrap.d.ts.map +1 -1
- package/lib/cjs/bootstrap.js +24 -1
- package/lib/cjs/bootstrap.js.map +1 -1
- package/lib/cjs/callsite.d.ts +26 -0
- package/lib/cjs/callsite.d.ts.map +1 -0
- package/lib/cjs/callsite.js +60 -0
- package/lib/cjs/callsite.js.map +1 -0
- package/lib/cjs/config/log.d.ts +96 -0
- package/lib/cjs/config/log.d.ts.map +1 -0
- package/lib/cjs/config/log.js +75 -0
- package/lib/cjs/config/log.js.map +1 -0
- package/lib/cjs/context.browser.d.ts +46 -0
- package/lib/cjs/context.browser.d.ts.map +1 -0
- package/lib/cjs/context.browser.js +85 -0
- package/lib/cjs/context.browser.js.map +1 -0
- package/lib/cjs/context.d.ts +61 -0
- package/lib/cjs/context.d.ts.map +1 -0
- package/lib/cjs/context.js +92 -0
- package/lib/cjs/context.js.map +1 -0
- package/lib/cjs/filters/LevelFilter.d.ts +24 -0
- package/lib/cjs/filters/LevelFilter.d.ts.map +1 -0
- package/lib/cjs/filters/LevelFilter.js +39 -0
- package/lib/cjs/filters/LevelFilter.js.map +1 -0
- package/lib/cjs/filters/MatchFilter.d.ts +30 -0
- package/lib/cjs/filters/MatchFilter.d.ts.map +1 -0
- package/lib/cjs/filters/MatchFilter.js +65 -0
- package/lib/cjs/filters/MatchFilter.js.map +1 -0
- package/lib/cjs/filters/RateLimitFilter.d.ts +37 -0
- package/lib/cjs/filters/RateLimitFilter.d.ts.map +1 -0
- package/lib/cjs/filters/RateLimitFilter.js +70 -0
- package/lib/cjs/filters/RateLimitFilter.js.map +1 -0
- package/lib/cjs/filters/index.d.ts +4 -0
- package/lib/cjs/filters/index.d.ts.map +1 -0
- package/lib/cjs/filters/index.js +20 -0
- package/lib/cjs/filters/index.js.map +1 -0
- package/lib/cjs/index.browser.d.ts +14 -0
- package/lib/cjs/index.browser.d.ts.map +1 -0
- package/lib/cjs/index.browser.js +46 -0
- package/lib/cjs/index.browser.js.map +1 -0
- package/lib/cjs/index.d.ts +8 -0
- package/lib/cjs/index.d.ts.map +1 -1
- package/lib/cjs/index.js +8 -0
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/log.d.ts +60 -1
- package/lib/cjs/log.d.ts.map +1 -1
- package/lib/cjs/log.js +294 -32
- package/lib/cjs/log.js.map +1 -1
- package/lib/cjs/perf/LogMetricSink.d.ts +28 -0
- package/lib/cjs/perf/LogMetricSink.d.ts.map +1 -0
- package/lib/cjs/perf/LogMetricSink.js +120 -0
- package/lib/cjs/perf/LogMetricSink.js.map +1 -0
- package/lib/cjs/perf/measure.d.ts +9 -0
- package/lib/cjs/perf/measure.d.ts.map +1 -0
- package/lib/cjs/perf/measure.js +21 -0
- package/lib/cjs/perf/measure.js.map +1 -0
- package/lib/cjs/perf.d.ts +9 -0
- package/lib/cjs/perf.d.ts.map +1 -0
- package/lib/cjs/perf.js +18 -0
- package/lib/cjs/perf.js.map +1 -0
- package/lib/cjs/schemas/log.configuration.d.ts +151 -18
- package/lib/cjs/schemas/log.configuration.d.ts.map +1 -1
- package/lib/cjs/schemas/log.configuration.js +154 -21
- package/lib/cjs/schemas/log.configuration.js.map +1 -1
- package/lib/cjs/targets/BrowserConsoleTarget.d.ts +48 -0
- package/lib/cjs/targets/BrowserConsoleTarget.d.ts.map +1 -0
- package/lib/cjs/targets/BrowserConsoleTarget.js +48 -0
- package/lib/cjs/targets/BrowserConsoleTarget.js.map +1 -0
- package/lib/cjs/targets/ColoredConsoleTarget.d.ts.map +1 -1
- package/lib/cjs/targets/ColoredConsoleTarget.js +21 -12
- package/lib/cjs/targets/ColoredConsoleTarget.js.map +1 -1
- package/lib/cjs/targets/FileTarget.d.ts +96 -20
- package/lib/cjs/targets/FileTarget.d.ts.map +1 -1
- package/lib/cjs/targets/FileTarget.js +188 -225
- package/lib/cjs/targets/FileTarget.js.map +1 -1
- package/lib/cjs/targets/JsonFileTarget.d.ts +19 -0
- package/lib/cjs/targets/JsonFileTarget.d.ts.map +1 -0
- package/lib/cjs/targets/JsonFileTarget.js +44 -0
- package/lib/cjs/targets/JsonFileTarget.js.map +1 -0
- package/lib/cjs/targets/JsonTarget.d.ts +27 -0
- package/lib/cjs/targets/JsonTarget.d.ts.map +1 -0
- package/lib/cjs/targets/JsonTarget.js +46 -0
- package/lib/cjs/targets/JsonTarget.js.map +1 -0
- package/lib/cjs/targets/MemoryTarget.d.ts +33 -0
- package/lib/cjs/targets/MemoryTarget.d.ts.map +1 -0
- package/lib/cjs/targets/MemoryTarget.js +63 -0
- package/lib/cjs/targets/MemoryTarget.js.map +1 -0
- package/lib/cjs/targets/index.d.ts +7 -0
- package/lib/cjs/targets/index.d.ts.map +1 -1
- package/lib/cjs/targets/index.js +7 -0
- package/lib/cjs/targets/index.js.map +1 -1
- package/lib/cjs/targets/wrappers/AutoFlushTarget.d.ts +37 -0
- package/lib/cjs/targets/wrappers/AutoFlushTarget.d.ts.map +1 -0
- package/lib/cjs/targets/wrappers/AutoFlushTarget.js +48 -0
- package/lib/cjs/targets/wrappers/AutoFlushTarget.js.map +1 -0
- package/lib/cjs/targets/wrappers/FallbackGroupTarget.d.ts +45 -0
- package/lib/cjs/targets/wrappers/FallbackGroupTarget.d.ts.map +1 -0
- package/lib/cjs/targets/wrappers/FallbackGroupTarget.js +94 -0
- package/lib/cjs/targets/wrappers/FallbackGroupTarget.js.map +1 -0
- package/lib/cjs/targets/wrappers/RetryingTarget.d.ts +44 -0
- package/lib/cjs/targets/wrappers/RetryingTarget.d.ts.map +1 -0
- package/lib/cjs/targets/wrappers/RetryingTarget.js +61 -0
- package/lib/cjs/targets/wrappers/RetryingTarget.js.map +1 -0
- package/lib/cjs/targets/wrappers/SplitGroupTarget.d.ts +30 -0
- package/lib/cjs/targets/wrappers/SplitGroupTarget.d.ts.map +1 -0
- package/lib/cjs/targets/wrappers/SplitGroupTarget.js +52 -0
- package/lib/cjs/targets/wrappers/SplitGroupTarget.js.map +1 -0
- package/lib/cjs/traceparent.d.ts +63 -0
- package/lib/cjs/traceparent.d.ts.map +1 -0
- package/lib/cjs/traceparent.js +107 -0
- package/lib/cjs/traceparent.js.map +1 -0
- package/lib/mjs/archive/LogArchiveService.d.ts +75 -0
- package/lib/mjs/archive/LogArchiveService.d.ts.map +1 -0
- package/lib/mjs/archive/LogArchiveService.js +186 -0
- package/lib/mjs/archive/LogArchiveService.js.map +1 -0
- package/lib/mjs/archive/archive-strategy.d.ts +32 -0
- package/lib/mjs/archive/archive-strategy.d.ts.map +1 -0
- package/lib/mjs/archive/archive-strategy.js +39 -0
- package/lib/mjs/archive/archive-strategy.js.map +1 -0
- package/lib/mjs/archive/context.d.ts +50 -0
- package/lib/mjs/archive/context.d.ts.map +1 -0
- package/lib/mjs/archive/context.js +2 -0
- package/lib/mjs/archive/context.js.map +1 -0
- package/lib/mjs/archive/index.d.ts +10 -0
- package/lib/mjs/archive/index.d.ts.map +1 -0
- package/lib/mjs/archive/index.js +10 -0
- package/lib/mjs/archive/index.js.map +1 -0
- package/lib/mjs/archive/retention-strategy.d.ts +26 -0
- package/lib/mjs/archive/retention-strategy.d.ts.map +1 -0
- package/lib/mjs/archive/retention-strategy.js +38 -0
- package/lib/mjs/archive/retention-strategy.js.map +1 -0
- package/lib/mjs/archive/strategies/AgeLogRetentionStrategy.d.ts +10 -0
- package/lib/mjs/archive/strategies/AgeLogRetentionStrategy.d.ts.map +1 -0
- package/lib/mjs/archive/strategies/AgeLogRetentionStrategy.js +43 -0
- package/lib/mjs/archive/strategies/AgeLogRetentionStrategy.js.map +1 -0
- package/lib/mjs/archive/strategies/CountLogRetentionStrategy.d.ts +10 -0
- package/lib/mjs/archive/strategies/CountLogRetentionStrategy.d.ts.map +1 -0
- package/lib/mjs/archive/strategies/CountLogRetentionStrategy.js +39 -0
- package/lib/mjs/archive/strategies/CountLogRetentionStrategy.js.map +1 -0
- package/lib/mjs/archive/strategies/CronLogArchiveStrategy.d.ts +15 -0
- package/lib/mjs/archive/strategies/CronLogArchiveStrategy.d.ts.map +1 -0
- package/lib/mjs/archive/strategies/CronLogArchiveStrategy.js +49 -0
- package/lib/mjs/archive/strategies/CronLogArchiveStrategy.js.map +1 -0
- package/lib/mjs/archive/strategies/SizeLogArchiveStrategy.d.ts +18 -0
- package/lib/mjs/archive/strategies/SizeLogArchiveStrategy.d.ts.map +1 -0
- package/lib/mjs/archive/strategies/SizeLogArchiveStrategy.js +60 -0
- package/lib/mjs/archive/strategies/SizeLogArchiveStrategy.js.map +1 -0
- package/lib/mjs/archive/strategies/util.d.ts +28 -0
- package/lib/mjs/archive/strategies/util.d.ts.map +1 -0
- package/lib/mjs/archive/strategies/util.js +54 -0
- package/lib/mjs/archive/strategies/util.js.map +1 -0
- package/lib/mjs/bootstrap.d.ts.map +1 -1
- package/lib/mjs/bootstrap.js +25 -2
- package/lib/mjs/bootstrap.js.map +1 -1
- package/lib/mjs/callsite.d.ts +26 -0
- package/lib/mjs/callsite.d.ts.map +1 -0
- package/lib/mjs/callsite.js +57 -0
- package/lib/mjs/callsite.js.map +1 -0
- package/lib/mjs/config/log.d.ts +96 -0
- package/lib/mjs/config/log.d.ts.map +1 -0
- package/lib/mjs/config/log.js +72 -0
- package/lib/mjs/config/log.js.map +1 -0
- package/lib/mjs/context.browser.d.ts +46 -0
- package/lib/mjs/context.browser.d.ts.map +1 -0
- package/lib/mjs/context.browser.js +81 -0
- package/lib/mjs/context.browser.js.map +1 -0
- package/lib/mjs/context.d.ts +61 -0
- package/lib/mjs/context.d.ts.map +1 -0
- package/lib/mjs/context.js +88 -0
- package/lib/mjs/context.js.map +1 -0
- package/lib/mjs/filters/LevelFilter.d.ts +24 -0
- package/lib/mjs/filters/LevelFilter.d.ts.map +1 -0
- package/lib/mjs/filters/LevelFilter.js +36 -0
- package/lib/mjs/filters/LevelFilter.js.map +1 -0
- package/lib/mjs/filters/MatchFilter.d.ts +30 -0
- package/lib/mjs/filters/MatchFilter.d.ts.map +1 -0
- package/lib/mjs/filters/MatchFilter.js +62 -0
- package/lib/mjs/filters/MatchFilter.js.map +1 -0
- package/lib/mjs/filters/RateLimitFilter.d.ts +37 -0
- package/lib/mjs/filters/RateLimitFilter.d.ts.map +1 -0
- package/lib/mjs/filters/RateLimitFilter.js +67 -0
- package/lib/mjs/filters/RateLimitFilter.js.map +1 -0
- package/lib/mjs/filters/index.d.ts +4 -0
- package/lib/mjs/filters/index.d.ts.map +1 -0
- package/lib/mjs/filters/index.js +4 -0
- package/lib/mjs/filters/index.js.map +1 -0
- package/lib/mjs/index.browser.d.ts +14 -0
- package/lib/mjs/index.browser.d.ts.map +1 -0
- package/lib/mjs/index.browser.js +30 -0
- package/lib/mjs/index.browser.js.map +1 -0
- package/lib/mjs/index.d.ts +8 -0
- package/lib/mjs/index.d.ts.map +1 -1
- package/lib/mjs/index.js +8 -0
- package/lib/mjs/index.js.map +1 -1
- package/lib/mjs/log.d.ts +60 -1
- package/lib/mjs/log.d.ts.map +1 -1
- package/lib/mjs/log.js +295 -33
- package/lib/mjs/log.js.map +1 -1
- package/lib/mjs/perf/LogMetricSink.d.ts +28 -0
- package/lib/mjs/perf/LogMetricSink.d.ts.map +1 -0
- package/lib/mjs/perf/LogMetricSink.js +117 -0
- package/lib/mjs/perf/LogMetricSink.js.map +1 -0
- package/lib/mjs/perf/measure.d.ts +9 -0
- package/lib/mjs/perf/measure.d.ts.map +1 -0
- package/lib/mjs/perf/measure.js +18 -0
- package/lib/mjs/perf/measure.js.map +1 -0
- package/lib/mjs/perf.d.ts +9 -0
- package/lib/mjs/perf.d.ts.map +1 -0
- package/lib/mjs/perf.js +15 -0
- package/lib/mjs/perf.js.map +1 -0
- package/lib/mjs/schemas/log.configuration.d.ts +151 -18
- package/lib/mjs/schemas/log.configuration.d.ts.map +1 -1
- package/lib/mjs/schemas/log.configuration.js +154 -21
- package/lib/mjs/schemas/log.configuration.js.map +1 -1
- package/lib/mjs/targets/BrowserConsoleTarget.d.ts +48 -0
- package/lib/mjs/targets/BrowserConsoleTarget.d.ts.map +1 -0
- package/lib/mjs/targets/BrowserConsoleTarget.js +45 -0
- package/lib/mjs/targets/BrowserConsoleTarget.js.map +1 -0
- package/lib/mjs/targets/ColoredConsoleTarget.d.ts.map +1 -1
- package/lib/mjs/targets/ColoredConsoleTarget.js +21 -9
- package/lib/mjs/targets/ColoredConsoleTarget.js.map +1 -1
- package/lib/mjs/targets/FileTarget.d.ts +96 -20
- package/lib/mjs/targets/FileTarget.d.ts.map +1 -1
- package/lib/mjs/targets/FileTarget.js +191 -192
- package/lib/mjs/targets/FileTarget.js.map +1 -1
- package/lib/mjs/targets/JsonFileTarget.d.ts +19 -0
- package/lib/mjs/targets/JsonFileTarget.d.ts.map +1 -0
- package/lib/mjs/targets/JsonFileTarget.js +41 -0
- package/lib/mjs/targets/JsonFileTarget.js.map +1 -0
- package/lib/mjs/targets/JsonTarget.d.ts +27 -0
- package/lib/mjs/targets/JsonTarget.d.ts.map +1 -0
- package/lib/mjs/targets/JsonTarget.js +43 -0
- package/lib/mjs/targets/JsonTarget.js.map +1 -0
- package/lib/mjs/targets/MemoryTarget.d.ts +33 -0
- package/lib/mjs/targets/MemoryTarget.d.ts.map +1 -0
- package/lib/mjs/targets/MemoryTarget.js +60 -0
- package/lib/mjs/targets/MemoryTarget.js.map +1 -0
- package/lib/mjs/targets/index.d.ts +7 -0
- package/lib/mjs/targets/index.d.ts.map +1 -1
- package/lib/mjs/targets/index.js +7 -0
- package/lib/mjs/targets/index.js.map +1 -1
- package/lib/mjs/targets/wrappers/AutoFlushTarget.d.ts +37 -0
- package/lib/mjs/targets/wrappers/AutoFlushTarget.d.ts.map +1 -0
- package/lib/mjs/targets/wrappers/AutoFlushTarget.js +45 -0
- package/lib/mjs/targets/wrappers/AutoFlushTarget.js.map +1 -0
- package/lib/mjs/targets/wrappers/FallbackGroupTarget.d.ts +45 -0
- package/lib/mjs/targets/wrappers/FallbackGroupTarget.d.ts.map +1 -0
- package/lib/mjs/targets/wrappers/FallbackGroupTarget.js +91 -0
- package/lib/mjs/targets/wrappers/FallbackGroupTarget.js.map +1 -0
- package/lib/mjs/targets/wrappers/RetryingTarget.d.ts +44 -0
- package/lib/mjs/targets/wrappers/RetryingTarget.d.ts.map +1 -0
- package/lib/mjs/targets/wrappers/RetryingTarget.js +58 -0
- package/lib/mjs/targets/wrappers/RetryingTarget.js.map +1 -0
- package/lib/mjs/targets/wrappers/SplitGroupTarget.d.ts +30 -0
- package/lib/mjs/targets/wrappers/SplitGroupTarget.d.ts.map +1 -0
- package/lib/mjs/targets/wrappers/SplitGroupTarget.js +49 -0
- package/lib/mjs/targets/wrappers/SplitGroupTarget.js.map +1 -0
- package/lib/mjs/traceparent.d.ts +63 -0
- package/lib/mjs/traceparent.d.ts.map +1 -0
- package/lib/mjs/traceparent.js +100 -0
- package/lib/mjs/traceparent.js.map +1 -0
- package/lib/tsconfig.cjs.tsbuildinfo +1 -1
- package/lib/tsconfig.mjs.tsbuildinfo +1 -1
- package/package.json +25 -15
package/README.md
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
# `@spinajs/log`
|
|
2
|
+
|
|
3
|
+
The logging system for SpinaJS. You resolve a `Log` service, call level methods
|
|
4
|
+
(`info`, `error`, …), and a **rules + targets** configuration decides which
|
|
5
|
+
logger writes **where** and in **what format**. It supports plain-text layouts
|
|
6
|
+
and structured JSON, buffered file logging with rotation/retention, network
|
|
7
|
+
sinks (Grafana Loki, OTLP), composable wrapper targets, a filter pipeline,
|
|
8
|
+
ambient request-scoped context with trace correlation, and runtime level
|
|
9
|
+
control.
|
|
10
|
+
|
|
11
|
+
## Table of contents
|
|
12
|
+
|
|
13
|
+
- [Which package do I import?](#which-package-do-i-import)
|
|
14
|
+
- [Quick start](#quick-start)
|
|
15
|
+
- [Log levels](#log-levels)
|
|
16
|
+
- [Loggers](#loggers)
|
|
17
|
+
- [Logging API](#logging-api)
|
|
18
|
+
- [Layouts / templates](#layouts--templates)
|
|
19
|
+
- [Targets (sinks)](#targets-sinks)
|
|
20
|
+
- [Rules](#rules)
|
|
21
|
+
- [Filters](#filters)
|
|
22
|
+
- [Structured logging](#structured-logging)
|
|
23
|
+
- [Async context & correlation](#async-context--correlation)
|
|
24
|
+
- [File archiving](#file-archiving)
|
|
25
|
+
- [Runtime level control](#runtime-level-control)
|
|
26
|
+
- [Configuration reference](#configuration-reference)
|
|
27
|
+
- [Extending](#extending)
|
|
28
|
+
- [Package map](#package-map)
|
|
29
|
+
|
|
30
|
+
## Which package do I import?
|
|
31
|
+
|
|
32
|
+
| Package | Use it for |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `@spinajs/log` | **Application code.** Resolve `Log` and log. Ships the console/file/JSON/memory targets, the filter pipeline, `LogContext`, and runtime level control. |
|
|
35
|
+
| `@spinajs/log-common` | Only when writing a **custom target or filter** (abstract `LogTarget` / `LogFilter`, `ILogEntry`, `LogLevel`, `createLogMessageObject`, `serializeError`/`safeStringify`, `BatchQueue`). No implementation, so it avoids a circular dependency. |
|
|
36
|
+
| `@spinajs/internal-logger` | Only inside low-level packages (DI, configuration) that must log **before** the logger/config exists. Messages buffer and replay into the real logger once configuration resolves. |
|
|
37
|
+
| `@spinajs/log-source-graphana-loki` | The Grafana **Loki** target (`GraphanaLogTarget`). |
|
|
38
|
+
| `@spinajs/log-otlp` | The **OTLP/HTTP** export target (`OtlpLogTarget`) for any OpenTelemetry backend. |
|
|
39
|
+
| `@spinajs/telemetry` | Prometheus metrics + Apdex + an HTTP request-timing middleware. Metrics, not a log sink — see its own README. |
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { DI } from "@spinajs/di";
|
|
45
|
+
import { Log } from "@spinajs/log";
|
|
46
|
+
|
|
47
|
+
const log = await DI.resolve(Log, ["my-module"]); // logger named "my-module"
|
|
48
|
+
|
|
49
|
+
log.info("started");
|
|
50
|
+
log.info("user %s logged in", userId); // printf-style args
|
|
51
|
+
log.error(err, "could not connect to %s", host); // Error first, then message
|
|
52
|
+
log.info({ reqId, sku }, "checkout started"); // structured fields (merging object)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
With no configuration the logger writes every level to a colored console.
|
|
56
|
+
|
|
57
|
+
## Log levels
|
|
58
|
+
|
|
59
|
+
Eight levels, lowest to highest severity:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
trace < debug < info < success < warn < error < fatal < security
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`success` and `security` are SpinaJS extras (a positive/notice level and a
|
|
66
|
+
top-of-scale audit level). A rule's `level` is the **minimum**; lower-severity
|
|
67
|
+
messages for that logger are dropped.
|
|
68
|
+
|
|
69
|
+
Each level also maps to the OpenTelemetry **SeverityNumber** (1–24), emitted by
|
|
70
|
+
the JSON/OTLP targets so any observability backend can rank/filter by severity:
|
|
71
|
+
|
|
72
|
+
| Level | SeverityNumber |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| trace | 1 |
|
|
75
|
+
| debug | 5 |
|
|
76
|
+
| info | 9 |
|
|
77
|
+
| success | 11 |
|
|
78
|
+
| warn | 13 |
|
|
79
|
+
| error | 17 |
|
|
80
|
+
| fatal | 21 |
|
|
81
|
+
| security | 23 |
|
|
82
|
+
|
|
83
|
+
## Loggers
|
|
84
|
+
|
|
85
|
+
- **Named** — `DI.resolve(Log, ["name"])`. The same name returns the same logger.
|
|
86
|
+
- **`@Logger` decorator** — inject a logger into a class property:
|
|
87
|
+
```ts
|
|
88
|
+
import { Logger, Log } from "@spinajs/log";
|
|
89
|
+
class UserService {
|
|
90
|
+
@Logger("UserService") protected Log: Log;
|
|
91
|
+
save() { this.Log.info("saving"); }
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
- **Child loggers** — `const child = log.child("db", { pool: 1 });` creates a
|
|
95
|
+
logger named `parent.db` that inherits the parent's variables plus any extra.
|
|
96
|
+
- **Per-logger variables** — `log.addVariable("region", "eu");` makes `${region}`
|
|
97
|
+
available in that logger's layouts and structured records.
|
|
98
|
+
- **Timers** — `log.timeStart("q"); …; const ms = log.timeEnd("q");` returns the
|
|
99
|
+
elapsed milliseconds.
|
|
100
|
+
|
|
101
|
+
## Logging API
|
|
102
|
+
|
|
103
|
+
Every level method (`trace`/`debug`/`info`/`warn`/`error`/`fatal`/`security`/`success`)
|
|
104
|
+
accepts three call shapes, dispatched by the **first argument**:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
log.info("plain message");
|
|
108
|
+
log.info("formatted %s = %d", name, count); // printf args
|
|
109
|
+
log.error(new Error("boom"), "while saving %s", id); // Error first -> structured `error`
|
|
110
|
+
log.info({ reqId: "abc", userId: 42 }, "handled"); // plain object first -> merged fields
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- **Error first** → the error is serialized into a structured `error` field and
|
|
114
|
+
the message/args follow.
|
|
115
|
+
- **Plain object first** (not an `Error`/array) → its keys are merged into the
|
|
116
|
+
entry's variables (available as `${key}` in layouts and as fields in JSON),
|
|
117
|
+
the second argument is the format string.
|
|
118
|
+
- **String first** → it is the message / printf format string.
|
|
119
|
+
|
|
120
|
+
**printf specifiers** (in the message string):
|
|
121
|
+
|
|
122
|
+
| Spec | Meaning |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `%s` | string |
|
|
125
|
+
| `%d` | number |
|
|
126
|
+
| `%i` | `parseInt` |
|
|
127
|
+
| `%f` | `parseFloat` |
|
|
128
|
+
| `%j` / `%o` / `%O` | JSON / object |
|
|
129
|
+
| `%%` | literal `%` (consumes no argument) |
|
|
130
|
+
|
|
131
|
+
Leftover arguments are appended space-separated.
|
|
132
|
+
|
|
133
|
+
## Layouts / templates
|
|
134
|
+
|
|
135
|
+
Text targets render each entry through a **layout** string using `${…}`
|
|
136
|
+
placeholders. The default layout is:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
${datetime} ${level} ${message}${?error} Exception: ${error:message}${/error} (${logger})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Available constructs:
|
|
143
|
+
|
|
144
|
+
| Construct | Renders |
|
|
145
|
+
| --- | --- |
|
|
146
|
+
| `${datetime}` | current date-time |
|
|
147
|
+
| `${date:dd_MM_yyyy}` | formatted date (Luxon-style format) |
|
|
148
|
+
| `${level}` | upper-case level, e.g. `ERROR` |
|
|
149
|
+
| `${message}` | the formatted message |
|
|
150
|
+
| `${logger}` | the logger name |
|
|
151
|
+
| `${myVar}` | any per-logger / merged / ambient variable |
|
|
152
|
+
| `${error:message}` | sub-property access on a variable (here the structured error's `message`) |
|
|
153
|
+
| `${?error} … ${/error}` | conditional block — rendered only when `error` is truthy |
|
|
154
|
+
| `${callsite}` | the caller's `file:line` (opt-in — see below) |
|
|
155
|
+
|
|
156
|
+
Set a target's `layout` to override the default, e.g.
|
|
157
|
+
`"${datetime} ${level} ${message} @ ${callsite} (${logger})"`.
|
|
158
|
+
|
|
159
|
+
**`${callsite}`** is captured only when *some* target's layout references it, so
|
|
160
|
+
logging stays zero-cost otherwise (no stack is walked). It resolves to
|
|
161
|
+
`basename:line` and is best-effort (empty string if the runtime stack can't be
|
|
162
|
+
parsed).
|
|
163
|
+
|
|
164
|
+
## Targets (sinks)
|
|
165
|
+
|
|
166
|
+
A **target** is where messages go. Each configured target has a `name`
|
|
167
|
+
(referenced by rules) and a `type` (the DI key). Common options
|
|
168
|
+
(`ICommonTargetOptions`): `name`, `type`, `enabled` (default `true`), `layout`.
|
|
169
|
+
|
|
170
|
+
> **Per-target filters.** A target definition may carry its own `filters` list
|
|
171
|
+
> (same shape as `logger.filters`). These run **only** when writing to that
|
|
172
|
+
> target, **after** the logger-level pipeline, and drop/mutate the entry for
|
|
173
|
+
> **that target only** — the entry is cloned per target first, so a mutating
|
|
174
|
+
> filter (e.g. `WhenRepeatedFilter`'s `(xN)`) never bleeds into other targets:
|
|
175
|
+
>
|
|
176
|
+
> ```js
|
|
177
|
+
> targets: [
|
|
178
|
+
> { name: "Audit", type: "FileTarget",
|
|
179
|
+
> filters: [{ type: "MatchFilter", pattern: "secret", mode: "drop" }] },
|
|
180
|
+
> { name: "Console", type: "ConsoleTarget" }, // still sees everything
|
|
181
|
+
> ]
|
|
182
|
+
> ```
|
|
183
|
+
|
|
184
|
+
> **`enabled: false` targets are never instantiated.** A target definition
|
|
185
|
+
> marked `enabled: false` is skipped at resolve time — its class is never
|
|
186
|
+
> constructed, so a disabled `FileTarget` never opens its file, starts its flush
|
|
187
|
+
> timer, or spins up its archive service. A rule that references only disabled
|
|
188
|
+
> targets is silently skipped (treated as intentionally not routed); a rule that
|
|
189
|
+
> references a target **name that does not exist at all** still throws
|
|
190
|
+
> `InvalidOption`.
|
|
191
|
+
|
|
192
|
+
> **Config shape note:** some targets read their settings **flat** on the target
|
|
193
|
+
> definition (Console `theme`/`layout`, `MemoryTarget.limit`, `JsonTarget.stream`),
|
|
194
|
+
> while File/JSON-file and the wrapper targets read them **nested under an
|
|
195
|
+
> `options` object**. Loki and OTLP accept either. Each example below uses the
|
|
196
|
+
> form that target expects.
|
|
197
|
+
|
|
198
|
+
> **Runtime targets (attach/detach a sink).** Besides config, you can attach or
|
|
199
|
+
> detach a target on a specific logger **at runtime** (bunyan `addStream` style):
|
|
200
|
+
>
|
|
201
|
+
> ```js
|
|
202
|
+
> const log = DI.resolve(Log, ["my-logger"]);
|
|
203
|
+
>
|
|
204
|
+
> // attach – resolves the target ( honoring enabled:false ) and returns the instance
|
|
205
|
+
> const mem = log.addTarget(
|
|
206
|
+
> { name: "Live", type: "MemoryTarget" },
|
|
207
|
+
> { level: "warn" } // optional level window [level, maxLevel ?? security] + filters
|
|
208
|
+
> );
|
|
209
|
+
>
|
|
210
|
+
> // detach – flushes buffered entries, then removes it ( does NOT dispose it )
|
|
211
|
+
> await log.removeTarget("Live");
|
|
212
|
+
> ```
|
|
213
|
+
>
|
|
214
|
+
> `addTarget(def, opts?)` returns the resolved target instance (or `undefined`
|
|
215
|
+
> when `def.enabled === false`). `opts` accepts `level` / `maxLevel` (the level
|
|
216
|
+
> window) and `filters` (appended after `def.filters`). Adding a target whose
|
|
217
|
+
> `name` already exists **replaces** the previous one (flushed first), so a name
|
|
218
|
+
> is never duplicated, and the `MinLevel` / `${callsite}` gates are recomputed.
|
|
219
|
+
> `removeTarget(name)` force-flushes each matching target **before** detaching so
|
|
220
|
+
> no buffered entry is lost; it is a no-op when no target has that name.
|
|
221
|
+
|
|
222
|
+
| `type` | Class | Package | Writes to |
|
|
223
|
+
| --- | --- | --- | --- |
|
|
224
|
+
| `ConsoleTarget` | `ColoredConsoleTarget` / `BrowserConsoleTarget` | `@spinajs/log` | stdout/stderr (ANSI colors on Node; devtools styling in the browser) |
|
|
225
|
+
| `FileTarget` | `FileTarget` | `@spinajs/log` | a file via `@spinajs/fs`, buffered, with rotation/retention/zip |
|
|
226
|
+
| `JsonTarget` | `JsonTarget` | `@spinajs/log` | **stdout** as newline-delimited JSON (NDJSON) |
|
|
227
|
+
| `JsonFileTarget` | `JsonFileTarget` | `@spinajs/log` | a **file** as NDJSON (inherits FileTarget rotation) |
|
|
228
|
+
| `MemoryTarget` | `MemoryTarget` | `@spinajs/log` | an in-memory ring buffer (readable in-process) |
|
|
229
|
+
| `BlackHoleTarget` | `BlackHoleTarget` | `@spinajs/log` | nowhere (discards; useful in tests) |
|
|
230
|
+
| `SplitGroupTarget` | `SplitGroupTarget` | `@spinajs/log` | fans one target out to many |
|
|
231
|
+
| `AutoFlushTarget` | `AutoFlushTarget` | `@spinajs/log` | wraps a target; force-flushes it on high-severity entries |
|
|
232
|
+
| `RetryingTarget` | `RetryingTarget` | `@spinajs/log` | wraps a target; retries its `write` on rejection |
|
|
233
|
+
| `FallbackGroupTarget` | `FallbackGroupTarget` | `@spinajs/log` | ordered fallback across targets (+ drop-hook) |
|
|
234
|
+
| `GraphanaLogTarget` | `GraphanaLokiLogTarget` | `@spinajs/log-source-graphana-loki` | Grafana Loki over HTTP, batched |
|
|
235
|
+
| `OtlpLogTarget` | `OtlpLogTarget` | `@spinajs/log-otlp` | any OTLP/HTTP backend at `/v1/logs` |
|
|
236
|
+
|
|
237
|
+
### Console
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
{ name: "Console", type: "ConsoleTarget" }
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Node uses ANSI colors per level (override the palette with a `theme` map);
|
|
244
|
+
the browser build maps levels to `console.debug/log/warn/error`.
|
|
245
|
+
|
|
246
|
+
> **Browser caveat:** because output is formatted and dispatched through the
|
|
247
|
+
> logger, browser devtools attribute log lines to the console target, not your
|
|
248
|
+
> call site. Use `${callsite}` in the layout if you need the origin.
|
|
249
|
+
|
|
250
|
+
### File
|
|
251
|
+
|
|
252
|
+
Writes through the [`@spinajs/fs`](../fs) abstraction, so the active log and its
|
|
253
|
+
archives can live on any registered provider (local disk, S3, FTP, …). Options
|
|
254
|
+
live under `options`:
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
{
|
|
258
|
+
name: "File",
|
|
259
|
+
type: "FileTarget",
|
|
260
|
+
options: {
|
|
261
|
+
path: "logs/log_${logger}_${date:dd_MM_yyyy}.txt", // required; variables allowed
|
|
262
|
+
archivePath: "logs/archive",
|
|
263
|
+
maxSize: 1024 * 1024, // rotate past this many bytes
|
|
264
|
+
compress: true, // zip archived files
|
|
265
|
+
maxBufferSize: 100, // buffered messages before a flush
|
|
266
|
+
maxQueueSize: 100000, // hard in-memory cap; drops oldest if a sink is stuck
|
|
267
|
+
flushInterval: 1000, // ms; flush a partial buffer at least this often
|
|
268
|
+
archiveStrategy: "SizeLogArchiveStrategy",
|
|
269
|
+
retentionStrategies: ["CountLogRetentionStrategy"],
|
|
270
|
+
maxArchiveFiles: 5,
|
|
271
|
+
maxAge: 7 * 24 * 60 * 60, // seconds
|
|
272
|
+
archiveInterval: 60, // seconds between size checks
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
| option | default | meaning |
|
|
278
|
+
| --- | --- | --- |
|
|
279
|
+
| `path` | *required* | active log path, relative to the `fs` provider base path (variables allowed) |
|
|
280
|
+
| `archivePath` | log dir | archive directory, relative to the `archiveFs` provider |
|
|
281
|
+
| `fs` | `fs-log-default` | provider for the active log (`fs-log-default` is registered automatically at `process.cwd()`) |
|
|
282
|
+
| `archiveFs` | = `fs` | provider archives are moved to |
|
|
283
|
+
| `archiveStrategy` | `SizeLogArchiveStrategy` | rotation strategy class name |
|
|
284
|
+
| `retentionStrategies` | `["CountLogRetentionStrategy"]` | ordered retention strategy class names |
|
|
285
|
+
| `maxSize` | `1048576` | rotate when the active log exceeds this many bytes |
|
|
286
|
+
| `archiveInterval` | `60` | seconds between size checks |
|
|
287
|
+
| `rotate` | — | cron expression for `CronLogArchiveStrategy` (6-field, seconds supported) |
|
|
288
|
+
| `compress` | `false` | zip the archived file, then delete the raw copy |
|
|
289
|
+
| `maxBufferSize` | `100` | buffered messages before a flush |
|
|
290
|
+
| `maxQueueSize` | `100000` | hard cap; oldest buffered messages are dropped past it |
|
|
291
|
+
| `flushInterval` | `1000` | periodic flush tick in ms |
|
|
292
|
+
| `maxArchiveFiles` | `5` | archives to keep (`CountLogRetentionStrategy`) |
|
|
293
|
+
| `maxAge` | `604800` | max archive age in seconds (`AgeLogRetentionStrategy`) |
|
|
294
|
+
|
|
295
|
+
Writes are buffered and flushed as one batched `fs.append`, guarded by a
|
|
296
|
+
write-lock so a rotation never races an append; a failed append is retried
|
|
297
|
+
(never silently dropped, up to the `maxQueueSize` cap).
|
|
298
|
+
|
|
299
|
+
### JSON (stdout) and JSON file
|
|
300
|
+
|
|
301
|
+
`JsonTarget` emits one JSON object per line to stdout — ideal for container log
|
|
302
|
+
collectors (promtail/Loki, Filebeat/Elastic, Datadog, CloudWatch) that index
|
|
303
|
+
fields instead of parsing text:
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
{ name: "Json", type: "JsonTarget", stream: "stdout" } // or "stderr"
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
A record looks like:
|
|
310
|
+
|
|
311
|
+
```json
|
|
312
|
+
{"time":"2026-07-15T…","severityNumber":17,"level":"ERROR","logger":"checkout","message":"save failed","reqId":"abc","error":{"name":"Error","message":"save failed","stack":"…","code":"ECONNREFUSED"}}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`JsonFileTarget` writes the same NDJSON to a **file**, reusing all of
|
|
316
|
+
FileTarget's rotation/retention/zip (configure it exactly like `FileTarget`
|
|
317
|
+
under `options`, with `type: "JsonFileTarget"`). Both stamp `time` at log time
|
|
318
|
+
and serialize with a never-throwing, circular-safe stringifier.
|
|
319
|
+
|
|
320
|
+
### Memory (ring buffer)
|
|
321
|
+
|
|
322
|
+
Keeps the last `limit` entries in memory so a debug endpoint or a crash handler
|
|
323
|
+
can read recent context back in-process:
|
|
324
|
+
|
|
325
|
+
```js
|
|
326
|
+
{ name: "Memory", type: "MemoryTarget", limit: 200 } // default 100
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
const ring = DI.resolve<MemoryTarget>("MemoryTarget");
|
|
331
|
+
ring.getRecords(); // ILogEntry[] (newest last); ring.clear() to empty
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### BlackHole
|
|
335
|
+
|
|
336
|
+
```js
|
|
337
|
+
{ name: "Null", type: "BlackHoleTarget" } // discards everything
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Wrapper targets
|
|
341
|
+
|
|
342
|
+
Wrappers decorate inner target definitions (given under `options`).
|
|
343
|
+
|
|
344
|
+
**SplitGroup** — fan one logical target out to many sinks:
|
|
345
|
+
|
|
346
|
+
```js
|
|
347
|
+
{ name: "Multi", type: "SplitGroupTarget", options: { targets: [
|
|
348
|
+
{ name: "Console", type: "ConsoleTarget" },
|
|
349
|
+
{ name: "File", type: "FileTarget", options: { path: "logs/app.log" } },
|
|
350
|
+
]}}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**AutoFlush** — force-flush an inner (buffered) target when a high-severity entry
|
|
354
|
+
arrives, so a crash-level event is never left buffered:
|
|
355
|
+
|
|
356
|
+
```js
|
|
357
|
+
{ name: "SafeFile", type: "AutoFlushTarget", options: {
|
|
358
|
+
target: { name: "File", type: "FileTarget", options: { path: "logs/app.log" } },
|
|
359
|
+
flushLevel: "error", // default "error"
|
|
360
|
+
}}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**Retrying** — retry an inner target's `write` on rejection with exponential
|
|
364
|
+
backoff + jitter:
|
|
365
|
+
|
|
366
|
+
```js
|
|
367
|
+
{ name: "RetryOut", type: "RetryingTarget", options: {
|
|
368
|
+
target: { name: "Custom", type: "MyTarget" },
|
|
369
|
+
maxAttempts: 3, // default 3
|
|
370
|
+
delayMs: 100, // default 100
|
|
371
|
+
}}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**FallbackGroup** — an ordered list; write advances to the next target when the
|
|
375
|
+
primary **rejects** (write-rejection contract), *and* a drop-hook chains entries
|
|
376
|
+
a self-healing network target **gives up on** (buffer overflow or a non-retryable
|
|
377
|
+
delivery failure) to the next target — a durable fallback for a down sink, with
|
|
378
|
+
no duplicates:
|
|
379
|
+
|
|
380
|
+
```js
|
|
381
|
+
{ name: "Durable", type: "FallbackGroupTarget", options: { targets: [
|
|
382
|
+
{ name: "Otlp", type: "OtlpLogTarget", options: { endpoint: "http://collector:4318" } },
|
|
383
|
+
{ name: "Spill", type: "JsonFileTarget", options: { path: "logs/undelivered.ndjson" } },
|
|
384
|
+
]}}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Grafana Loki (`@spinajs/log-source-graphana-loki`)
|
|
388
|
+
|
|
389
|
+
```js
|
|
390
|
+
{ name: "Loki", type: "GraphanaLogTarget", options: {
|
|
391
|
+
host: "http://localhost:3100",
|
|
392
|
+
auth: { username: "admin", password: "admin" }, // optional (unauthenticated Loki allowed)
|
|
393
|
+
labels: { app: "my-app" },
|
|
394
|
+
interval: 3000, bufferSize: 10, maxBufferSize: 1000, timeout: 1000,
|
|
395
|
+
}}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Batched HTTP push with exponential-backoff + jitter retry (honoring
|
|
399
|
+
`Retry-After`, retrying only network errors and 429/502/503/504); non-retryable
|
|
400
|
+
errors surface instead of looping. The primary buffer is bounded.
|
|
401
|
+
|
|
402
|
+
### OTLP (`@spinajs/log-otlp`)
|
|
403
|
+
|
|
404
|
+
Export to any OpenTelemetry backend (OTel Collector, Grafana/Tempo, Datadog, …):
|
|
405
|
+
|
|
406
|
+
```js
|
|
407
|
+
{ name: "Otlp", type: "OtlpLogTarget", options: {
|
|
408
|
+
endpoint: "http://localhost:4318", // POSTs to /v1/logs
|
|
409
|
+
headers: { Authorization: "Bearer …" }, // optional
|
|
410
|
+
resource: { "service.name": "my-app" }, // resource attributes
|
|
411
|
+
scopeName: "@spinajs/log",
|
|
412
|
+
interval: 3000, bufferSize: 10, maxBufferSize: 1000, timeout: 5000,
|
|
413
|
+
}}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Maps each entry to the OTLP Logs model — `severityNumber`, `body`, resource +
|
|
417
|
+
record attributes, `traceId`/`spanId` from the request trace context, and a
|
|
418
|
+
structured `error` to `exception.type`/`exception.message`/`exception.stacktrace`
|
|
419
|
+
semantic attributes. Batched with the same resilience retry as Loki.
|
|
420
|
+
|
|
421
|
+
## Rules
|
|
422
|
+
|
|
423
|
+
A **rule** binds a logger-name pattern to a minimum `level` and one or more
|
|
424
|
+
`target` names:
|
|
425
|
+
|
|
426
|
+
```js
|
|
427
|
+
{ name: "http/*/controller", level: "info", target: ["Console", "File"] }
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Level windows (`maxLevel`)
|
|
431
|
+
|
|
432
|
+
`level` is the **lower** bound. Add an optional `maxLevel` to route only a level
|
|
433
|
+
**window** `[level, maxLevel]` (inclusive) — e.g. warn/error but **not**
|
|
434
|
+
fatal/security:
|
|
435
|
+
|
|
436
|
+
```js
|
|
437
|
+
{ name: "*", level: "warn", maxLevel: "error", target: "Ops" }
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Without `maxLevel` the upper bound defaults to the highest level (`security`), so
|
|
441
|
+
a plain min-only rule is unchanged.
|
|
442
|
+
|
|
443
|
+
Several rules may route to the **same** target with **different** windows; the
|
|
444
|
+
target then accepts the **union** of those windows. So two rules `info..info` and
|
|
445
|
+
`error..error` to one target deliver `info` and `error` but **not** a `warn`
|
|
446
|
+
between them.
|
|
447
|
+
|
|
448
|
+
> `maxLevel` does **not** lower the per-logger `MinLevel` fast-gate: a call above
|
|
449
|
+
> every window still builds the entry and is then filtered out per target — the
|
|
450
|
+
> gate only tracks the lowest `level` across rules.
|
|
451
|
+
|
|
452
|
+
Name matching uses glob semantics:
|
|
453
|
+
|
|
454
|
+
- `*` — any logger name.
|
|
455
|
+
- `prefix*` — names starting with `prefix`.
|
|
456
|
+
- `a.b.*` — dotted namespaces.
|
|
457
|
+
- an exact name matches only itself.
|
|
458
|
+
|
|
459
|
+
### Ordered, additive matching (`final`)
|
|
460
|
+
|
|
461
|
+
Rules are evaluated **in config order**, and matching is **additive** (NLog-style):
|
|
462
|
+
**every** rule whose pattern matches a logger applies, so a logger matched by both
|
|
463
|
+
`*` and a specific rule routes to **both** (targets are de-duped downstream, so a
|
|
464
|
+
target hit by two matching rules still receives each entry once).
|
|
465
|
+
|
|
466
|
+
A matched rule marked `final: true` **stops** evaluation of any *later* rules; that
|
|
467
|
+
final rule and all earlier matched rules still apply.
|
|
468
|
+
|
|
469
|
+
```js
|
|
470
|
+
rules: [
|
|
471
|
+
{ name: "db.pool", level: "trace", target: "PoolDebug", final: true }, // stops here
|
|
472
|
+
{ name: "*", level: "info", target: "Console" }, // skipped for db.pool
|
|
473
|
+
]
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
- `db.pool` matches the first rule, applies it, and stops — the later `*` is **not**
|
|
477
|
+
applied, so `db.pool` routes **only** to `PoolDebug`.
|
|
478
|
+
- any other logger doesn't match `db.pool`, falls through, and routes to `Console`.
|
|
479
|
+
|
|
480
|
+
> **Migration from the old behavior.** Previously a specific rule *dropped* the `*`
|
|
481
|
+
> catch-all, so adding a rule for one logger silently **excluded** it from the
|
|
482
|
+
> global console/file. Now the specific rule is **additive** — that logger reaches
|
|
483
|
+
> both its own target **and** the catch-all. To restore the old "this logger goes
|
|
484
|
+
> **only** here" behavior, mark its rule `final: true` and place it **before** the
|
|
485
|
+
> `*` catch-all (as above).
|
|
486
|
+
|
|
487
|
+
## Filters
|
|
488
|
+
|
|
489
|
+
Filters run in order per logger and can drop or modify entries. Configure a list
|
|
490
|
+
under `logger.filters`; each item's `type` is a DI-registered filter. A filter
|
|
491
|
+
returns the (possibly modified) entry to keep, or drops it.
|
|
492
|
+
|
|
493
|
+
```js
|
|
494
|
+
logger: {
|
|
495
|
+
filters: [
|
|
496
|
+
{ type: "LevelFilter", min: "warn" },
|
|
497
|
+
{ type: "MatchFilter", pattern: "healthcheck", mode: "drop" },
|
|
498
|
+
{ type: "RateLimitFilter", limit: 100, intervalSeconds: 10 },
|
|
499
|
+
{ type: "WhenRepeatedFilter", timeout: 10 },
|
|
500
|
+
],
|
|
501
|
+
// …targets, rules
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
| Filter | Options | Effect |
|
|
506
|
+
| --- | --- | --- |
|
|
507
|
+
| `WhenRepeatedFilter` | `timeout` (s, default 10), `maxKeys` (default 1024) | collapses identical repeated entries within the window into one, appending `(xN)` when logging resumes |
|
|
508
|
+
| `LevelFilter` | `min`, `max` (level names) | keeps only entries whose level is within `[min, max]` |
|
|
509
|
+
| `MatchFilter` | `pattern`, `field` (default `message`), `mode` (`keep`/`drop`, default `keep`), `flags` | regex-match a variable; keep on match (or drop, in `drop` mode); an invalid pattern is a no-op |
|
|
510
|
+
| `RateLimitFilter` | `limit`, `intervalSeconds`, `key` (optional variable) | fixed-window rate limit; drops overflow, per-key or global |
|
|
511
|
+
|
|
512
|
+
Filters run **after** the near-zero-cost level gate, so disabled levels never
|
|
513
|
+
reach them. The legacy `logger.whenRepeated` option still works (mapped to a
|
|
514
|
+
prepended `WhenRepeatedFilter`).
|
|
515
|
+
|
|
516
|
+
The same filter list can also be attached **per target** (`targets[].filters`) to
|
|
517
|
+
filter for one sink only — see [Targets](#targets-sinks). Per-target filters run
|
|
518
|
+
**after** the logger-level pipeline on a per-target clone, so a filter that mutates
|
|
519
|
+
the entry there never affects other targets.
|
|
520
|
+
|
|
521
|
+
## Structured logging
|
|
522
|
+
|
|
523
|
+
Use `JsonTarget`/`JsonFileTarget` (or Loki/OTLP) to emit machine-readable
|
|
524
|
+
records. The pieces:
|
|
525
|
+
|
|
526
|
+
- **Serializer registry** — registered field serializers run when an entry is
|
|
527
|
+
built. The default `error` serializer turns an `Error` into
|
|
528
|
+
`{ name, message, stack, code, signal }`, walking the `.cause` /
|
|
529
|
+
`AggregateError` chain into `stack`. Register your own:
|
|
530
|
+
```ts
|
|
531
|
+
import { registerSerializer } from "@spinajs/log-common";
|
|
532
|
+
registerSerializer("req", (r: any) => ({ method: r.method, url: r.url }));
|
|
533
|
+
// then: log.info({ req }, "handled")
|
|
534
|
+
```
|
|
535
|
+
A serializer that throws degrades to `{ serializerError }` — logging never
|
|
536
|
+
crashes the caller.
|
|
537
|
+
- **Merging-object fields** — `log.info({ reqId, sku }, "…")` adds `reqId`/`sku`
|
|
538
|
+
as first-class fields.
|
|
539
|
+
- **`safeStringify`** — the JSON targets serialize with a never-throwing,
|
|
540
|
+
`[Circular]`-safe stringifier, so a circular field can't break logging.
|
|
541
|
+
- **`severityNumber`** — the OTel severity number is included on JSON/OTLP
|
|
542
|
+
records for backend severity ranking.
|
|
543
|
+
|
|
544
|
+
## Async context & correlation
|
|
545
|
+
|
|
546
|
+
`LogContext` provides ambient, per-operation variables over an
|
|
547
|
+
`AsyncLocalStorage` shared with `@spinajs/http` — so anything logged inside a
|
|
548
|
+
request automatically carries its context with zero threading.
|
|
549
|
+
|
|
550
|
+
```ts
|
|
551
|
+
import { LogContext } from "@spinajs/log";
|
|
552
|
+
|
|
553
|
+
LogContext.with({ requestId: "abc", tenant: "acme" }, async () => {
|
|
554
|
+
// any logger, any depth, across awaits:
|
|
555
|
+
log.info("deep inside"); // entry carries requestId + tenant
|
|
556
|
+
});
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
- `LogContext.with(vars, fn)` — run `fn` with `vars` merged onto the current
|
|
560
|
+
context (copy-on-write; nesting accumulates).
|
|
561
|
+
- `LogContext.active()` — the current context (or `{}`).
|
|
562
|
+
- `LogContext.set(key, value)` — late-bind a value onto the active context.
|
|
563
|
+
- `LogContext.bind(fn)` — capture the context and re-attach it to a detached
|
|
564
|
+
callback / event handler.
|
|
565
|
+
|
|
566
|
+
Only **scalar** values (string/number/boolean/bigint) from the ambient context
|
|
567
|
+
are projected into log lines — objects/arrays/Dates are skipped as noise (pass
|
|
568
|
+
structured payloads explicitly per call). Inside an HTTP request the context is
|
|
569
|
+
`req.storage`, so logs automatically carry `requestId` and `realIp`.
|
|
570
|
+
|
|
571
|
+
**Trace correlation** — the http `RequestId` middleware continues an inbound W3C
|
|
572
|
+
`traceparent` (or starts a new trace) and seeds `traceId`/`spanId` into the
|
|
573
|
+
context, so every log line across services shares a trace id (and they surface as
|
|
574
|
+
top-level fields on OTLP records). Helpers `parseTraceparent`,
|
|
575
|
+
`formatTraceparent`, and `newTraceContext` are exported for custom propagation.
|
|
576
|
+
|
|
577
|
+
## File archiving
|
|
578
|
+
|
|
579
|
+
`FileTarget`/`JsonFileTarget` rotate and prune via strategies selected by class
|
|
580
|
+
name:
|
|
581
|
+
|
|
582
|
+
**Rotation** (when to archive) — one strategy:
|
|
583
|
+
|
|
584
|
+
- `SizeLogArchiveStrategy` — interval timer; rotates when the active log passes `maxSize`.
|
|
585
|
+
- `CronLogArchiveStrategy` — rotates on the `rotate` cron expression (6-field, seconds supported).
|
|
586
|
+
|
|
587
|
+
**Retention** (which archives to delete) — an ordered list, so policies compose:
|
|
588
|
+
|
|
589
|
+
- `CountLogRetentionStrategy` — keep the newest `maxArchiveFiles`.
|
|
590
|
+
- `AgeLogRetentionStrategy` — delete archives older than `maxAge` seconds.
|
|
591
|
+
|
|
592
|
+
```js
|
|
593
|
+
{ name: "File", type: "FileTarget", options: {
|
|
594
|
+
path: "logs/app.log",
|
|
595
|
+
rotate: "0 0 1 * * *", // 1am daily
|
|
596
|
+
archiveStrategy: "CronLogArchiveStrategy",
|
|
597
|
+
retentionStrategies: ["CountLogRetentionStrategy", "AgeLogRetentionStrategy"],
|
|
598
|
+
maxArchiveFiles: 5,
|
|
599
|
+
maxAge: 7 * 24 * 60 * 60,
|
|
600
|
+
compress: true,
|
|
601
|
+
}}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
Custom strategies extend `LogArchiveStrategy` / `LogRetentionStrategy`, register
|
|
605
|
+
in DI, and are named in the config. The browser build omits `FileTarget` and the
|
|
606
|
+
archive module (and never pulls in `@spinajs/fs`).
|
|
607
|
+
|
|
608
|
+
## Runtime level control
|
|
609
|
+
|
|
610
|
+
Every logger supports a runtime override on top of its rule-derived minimum
|
|
611
|
+
level, with a near-zero-cost disabled path (a disabled call returns before
|
|
612
|
+
building an entry):
|
|
613
|
+
|
|
614
|
+
```ts
|
|
615
|
+
log.getLevel(); // current effective LogLevel
|
|
616
|
+
log.setLevel("error"); // gate everything below error (persists in the browser)
|
|
617
|
+
log.setDefaultLevel("info"); // set only if nothing is already overridden/persisted
|
|
618
|
+
log.enableAll(); // = setLevel("trace")
|
|
619
|
+
log.disableAll(); // silence everything
|
|
620
|
+
log.resetLevel(); // back to the rule-derived level
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
In the browser the chosen level persists to `localStorage` (cookie fallback), so
|
|
624
|
+
it survives reloads; on Node persistence is a no-op. `setLevel` accepts a level
|
|
625
|
+
name or a `LogLevel` value (validated via `normalizeLevel`).
|
|
626
|
+
|
|
627
|
+
## Configuration reference
|
|
628
|
+
|
|
629
|
+
A complete `logger` configuration, validated against the schema in
|
|
630
|
+
`src/schemas/log.configuration.ts` (`targets` and `rules` are required non-empty
|
|
631
|
+
arrays; a target needs `name` + `type`; a rule needs `name` + `level` + `target`):
|
|
632
|
+
|
|
633
|
+
```js
|
|
634
|
+
module.exports = {
|
|
635
|
+
logger: {
|
|
636
|
+
variables: {},
|
|
637
|
+
targets: [
|
|
638
|
+
{ name: "Console", type: "ConsoleTarget" },
|
|
639
|
+
{ name: "Json", type: "JsonTarget", stream: "stdout" },
|
|
640
|
+
{ name: "File", type: "FileTarget", options: {
|
|
641
|
+
path: "logs/log_${logger}_${date:dd_MM_yyyy}.txt",
|
|
642
|
+
archivePath: "logs/archive",
|
|
643
|
+
maxSize: 1024 * 1024,
|
|
644
|
+
compress: true,
|
|
645
|
+
maxBufferSize: 8 * 1024,
|
|
646
|
+
retentionStrategies: ["CountLogRetentionStrategy", "AgeLogRetentionStrategy"],
|
|
647
|
+
maxArchiveFiles: 5,
|
|
648
|
+
maxAge: 7 * 24 * 60 * 60,
|
|
649
|
+
}},
|
|
650
|
+
],
|
|
651
|
+
filters: [
|
|
652
|
+
{ type: "WhenRepeatedFilter", timeout: 10 },
|
|
653
|
+
],
|
|
654
|
+
rules: [
|
|
655
|
+
{ name: "*", level: "info", target: "Console" }, // everything -> console
|
|
656
|
+
{ name: "audit*", level: "trace", target: ["Json", "File"] }, // audit loggers -> json + file
|
|
657
|
+
],
|
|
658
|
+
},
|
|
659
|
+
};
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
## Flushing & shutdown
|
|
663
|
+
|
|
664
|
+
Buffered targets (`FileTarget`, Loki, OTLP) hold entries in an in-memory
|
|
665
|
+
`BatchQueue` and drain them on their own tick. To force a drain explicitly:
|
|
666
|
+
|
|
667
|
+
- **`log.flush()`** — force-drains THIS logger's targets' buffers
|
|
668
|
+
(`Promise<void>`). It calls `forceFlush()` on each target; on a non-buffered
|
|
669
|
+
target that is a harmless no-op. `flush()` does **not** close or dispose the
|
|
670
|
+
target — handle and timer teardown remains the DI container's job.
|
|
671
|
+
- **`Log.flushAll()`** — flushes every registered logger (best-effort; never
|
|
672
|
+
rejects). Static.
|
|
673
|
+
- **`Log.clearLoggers()`** — flushes all loggers **before** disposing them, so
|
|
674
|
+
buffered entries are written out during teardown rather than relying on the DI
|
|
675
|
+
container disposing the target singletons.
|
|
676
|
+
|
|
677
|
+
On a **clean** process exit the log bootstrapper registers a Node-only
|
|
678
|
+
`beforeExit` hook that runs `Log.flushAll()`. This is best-effort: `beforeExit`
|
|
679
|
+
does not fire on hard exits (`process.exit`, signals, crashes), so call
|
|
680
|
+
`Log.flushAll()` / `Log.clearLoggers()` yourself in those paths.
|
|
681
|
+
|
|
682
|
+
## Extending
|
|
683
|
+
|
|
684
|
+
**Custom target** — extend `LogTarget`, register it under a `type`, implement
|
|
685
|
+
`write`. Optionally implement `forceFlush` (for buffered targets) and set
|
|
686
|
+
`OnDropped` semantics (see the fallback contract):
|
|
687
|
+
|
|
688
|
+
```ts
|
|
689
|
+
import { LogTarget, ICommonTargetOptions, ILogEntry } from "@spinajs/log-common";
|
|
690
|
+
import { Injectable, Singleton } from "@spinajs/di";
|
|
691
|
+
import { format } from "@spinajs/configuration-common";
|
|
692
|
+
|
|
693
|
+
@Singleton()
|
|
694
|
+
@Injectable("MyTarget")
|
|
695
|
+
export class MyTarget extends LogTarget<ICommonTargetOptions> {
|
|
696
|
+
public write(entry: ILogEntry): void {
|
|
697
|
+
if (!this.Options.enabled) return;
|
|
698
|
+
const line = format(entry.Variables, this.Options.layout);
|
|
699
|
+
// …deliver `line`… ; reject/throw to signal non-acceptance (Retry/Fallback act on it)
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
The `write()` contract: it **may reject** to signal the entry was not accepted —
|
|
705
|
+
`RetryingTarget`/`FallbackGroupTarget` act on that. Self-healing targets resolve
|
|
706
|
+
and call the optional `OnDropped(entry)` hook for entries they ultimately give
|
|
707
|
+
up on, which `FallbackGroupTarget` chains to a durable fallback.
|
|
708
|
+
|
|
709
|
+
**Custom filter** — extend `LogFilter`, register it under a `type`, implement
|
|
710
|
+
`apply` (return the entry to keep, or `null` to drop):
|
|
711
|
+
|
|
712
|
+
```ts
|
|
713
|
+
import { LogFilter, ILogEntry } from "@spinajs/log-common";
|
|
714
|
+
import { Injectable } from "@spinajs/di";
|
|
715
|
+
|
|
716
|
+
@Injectable("OnlyErrors")
|
|
717
|
+
export class OnlyErrors extends LogFilter {
|
|
718
|
+
public apply(entry: ILogEntry): ILogEntry | null {
|
|
719
|
+
return entry.Level >= 5 /* Error */ ? entry : null;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
## Package map
|
|
725
|
+
|
|
726
|
+
```
|
|
727
|
+
your code ──> @spinajs/log ( Log service, targets, filters, rules, LogContext )
|
|
728
|
+
▲
|
|
729
|
+
low-level pkgs ──> @spinajs/internal-logger (buffers until config is ready,
|
|
730
|
+
│ then replays into @spinajs/log)
|
|
731
|
+
▼
|
|
732
|
+
@spinajs/log-common (contracts: Log, LogTarget, LogFilter,
|
|
733
|
+
BatchQueue, serializers, layout variables)
|
|
734
|
+
|
|
735
|
+
network sinks: @spinajs/log-source-graphana-loki ( GraphanaLogTarget )
|
|
736
|
+
@spinajs/log-otlp ( OtlpLogTarget )
|
|
737
|
+
metrics: @spinajs/telemetry ( Prometheus + Apdex + timing )
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
`InternalLogger` exists so packages that load **before** configuration/logging
|
|
741
|
+
(DI, configuration) can still log. Those messages buffer and flush into the real
|
|
742
|
+
targets once `Configuration` resolves; on process exit any still-buffered
|
|
743
|
+
messages print to the console so nothing is lost. Do not use `InternalLogger` in
|
|
744
|
+
application code — resolve `Log` instead.
|