@btravstack/observability 0.3.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/LICENSE +21 -0
- package/README.md +164 -0
- package/dist/index.cjs +213 -0
- package/dist/index.d.cts +81 -0
- package/dist/index.d.mts +81 -0
- package/dist/index.mjs +207 -0
- package/dist/logger-B1-_76ej.d.cts +35 -0
- package/dist/logger-B1-_76ej.d.mts +35 -0
- package/dist/otel.cjs +90 -0
- package/dist/otel.d.cts +38 -0
- package/dist/otel.d.mts +38 -0
- package/dist/otel.mjs +87 -0
- package/dist/pino.cjs +34 -0
- package/dist/pino.d.cts +27 -0
- package/dist/pino.d.mts +27 -0
- package/dist/pino.mjs +33 -0
- package/package.json +114 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Benoit TRAVERS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<!-- doctest: prelude
|
|
2
|
+
import { HttpModule } from "@btravstack/http-server";
|
|
3
|
+
import { Port } from "@btravstack/di";
|
|
4
|
+
import type { AsyncResult } from "unthrown";
|
|
5
|
+
import { orderRouter } from "../../module.js";
|
|
6
|
+
import { otel } from "@btravstack/observability/otel";
|
|
7
|
+
import { cache } from "@btravstack/cache";
|
|
8
|
+
import { redisCache } from "@btravstack/cache/redis";
|
|
9
|
+
import { CustomersSlice } from "../../slices/customers/module.js";
|
|
10
|
+
import { OrdersSlice } from "../../slices/orders/module.js";
|
|
11
|
+
class PlaceOrder extends Port("PlaceOrder")<{
|
|
12
|
+
readonly execute: (id: string, quantity: number) => AsyncResult<void, never>;
|
|
13
|
+
}> {}
|
|
14
|
+
class OrderRepository extends Port("OrderRepository")<{
|
|
15
|
+
readonly save: (order: {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly quantity: number;
|
|
18
|
+
}) => AsyncResult<void, never>;
|
|
19
|
+
}> {}
|
|
20
|
+
-->
|
|
21
|
+
|
|
22
|
+
# @btravstack/observability
|
|
23
|
+
|
|
24
|
+
> Observability for [`@btravstack/core`](../core), starting with logging: a
|
|
25
|
+
> **strict** `Logger` port โ no `any`, no printf, no mutable context, no static
|
|
26
|
+
> instance โ a default implementation that stamps every line with the ambient
|
|
27
|
+
> unit's trace id, a dependency-free JSON sink, pino behind a subpath, and the
|
|
28
|
+
> kernel's nine lifecycle events as log lines in the same stream.
|
|
29
|
+
|
|
30
|
+
๐ **[Documentation](https://btravstack.github.io/btravstack/reference/observability)** ยท
|
|
31
|
+
[How-to](https://btravstack.github.io/btravstack/how-to/log-and-correlate) ยท
|
|
32
|
+
[API Reference](https://btravstack.github.io/btravstack/api/observability/)
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
pnpm add @btravstack/observability @btravstack/core @btravstack/config @btravstack/di unthrown
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Those four are peers. `pino` is an **optional** peer, needed only if you import
|
|
39
|
+
`@btravstack/observability/pino`. Node `>=20`.
|
|
40
|
+
|
|
41
|
+
## A worked example
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { runMain, Logger } from "@btravstack/core";
|
|
45
|
+
import {
|
|
46
|
+
createLogger,
|
|
47
|
+
jsonSink,
|
|
48
|
+
kernelEvents,
|
|
49
|
+
observability,
|
|
50
|
+
} from "@btravstack/observability";
|
|
51
|
+
import { Module, Provider } from "@btravstack/di";
|
|
52
|
+
|
|
53
|
+
// The application depends on the port, like any other service.
|
|
54
|
+
const placeOrder = Provider(PlaceOrder)(
|
|
55
|
+
{ orders: OrderRepository, logger: Logger },
|
|
56
|
+
{
|
|
57
|
+
sync: ({ orders, logger }) => ({
|
|
58
|
+
execute: (id, quantity) =>
|
|
59
|
+
orders
|
|
60
|
+
.save({ id, quantity })
|
|
61
|
+
.tap(() => logger.info("order placed", { id, quantity })),
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// The starter provides it. `LOG_LEVEL` is read inside the graph and validated
|
|
67
|
+
// once โ `verbose` is a startup failure naming the variable, not a silent
|
|
68
|
+
// fallback to `info`.
|
|
69
|
+
const OrderApi = HttpModule("OrderApi")({
|
|
70
|
+
router: orderRouter,
|
|
71
|
+
imports: [
|
|
72
|
+
OrdersSlice,
|
|
73
|
+
CustomersSlice,
|
|
74
|
+
cache({ adapter: redisCache() }),
|
|
75
|
+
observability(),
|
|
76
|
+
otel(),
|
|
77
|
+
],
|
|
78
|
+
exports: [Logger],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// The kernel's own events, in the same stream and the same shape. The logger
|
|
82
|
+
// here is built by hand because `building` is emitted while the graph still
|
|
83
|
+
// is: the sink cannot come from the context it is watching.
|
|
84
|
+
await runMain(OrderApi, { onEvent: kernelEvents(createLogger(jsonSink())) });
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Every line written inside a unit carries that unit's `traceId`, `unitId` and
|
|
88
|
+
`tenantId` โ the logger reads `currentUnit()` **per call**, so one
|
|
89
|
+
application-scope logger is correct for every request without a single
|
|
90
|
+
argument threaded through the call stack.
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"orderId": "0199a1e0-0000-7000-8000-000000000001",
|
|
95
|
+
"time": "2026-08-16T09:41:02.113Z",
|
|
96
|
+
"level": "info",
|
|
97
|
+
"message": "order placed",
|
|
98
|
+
"unitId": "u-7",
|
|
99
|
+
"traceId": "3f9cโฆ"
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## What makes the interface strict
|
|
104
|
+
|
|
105
|
+
| Decision | Why |
|
|
106
|
+
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
107
|
+
| A **port**, never a class or a static | A test provides its own; there is no global to reach past DI with |
|
|
108
|
+
| `with(attributes)` returns a logger | Nothing mutates: two scopes cannot interleave each other's context |
|
|
109
|
+
| `Attributes` is a flat record of scalars | The shape a log backend indexes; no `any`, no printf, no stringifying whatever it is handed |
|
|
110
|
+
| A failure goes in `cause` | An `Error`'s `message` and `stack` are non-enumerable โ `JSON.stringify` alone drops exactly the part worth keeping |
|
|
111
|
+
| It cannot throw | A broken sink is swallowed: an observability fault must not become an outage |
|
|
112
|
+
| Six levels, fixed | `LOG_LEVEL` is validated at startup, and `isEnabled` is a comparison |
|
|
113
|
+
|
|
114
|
+
## Swapping the implementation
|
|
115
|
+
|
|
116
|
+
`observability({ sink })` replaces the destination; providing `Logger`
|
|
117
|
+
yourself replaces everything. For a deployment that wants pino's throughput:
|
|
118
|
+
|
|
119
|
+
<!-- doctest: skip โ needs `pino`, which no example workspace installs; held by packages/observability/src/pino.spec.ts instead -->
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import pino from "pino";
|
|
123
|
+
import { pinoSink } from "@btravstack/observability/pino";
|
|
124
|
+
|
|
125
|
+
observability({ sink: pinoSink(pino()) });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The level filter stays this package's โ `LOG_LEVEL`, validated once โ so there
|
|
129
|
+
is one filter in the process rather than two that can disagree.
|
|
130
|
+
|
|
131
|
+
## Traces and metrics
|
|
132
|
+
|
|
133
|
+
The other half of the package's name, behind an optional-peer subpath the way
|
|
134
|
+
`pino` is:
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
pnpm add @opentelemetry/api @opentelemetry/sdk-node
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
<!-- doctest: skip โ needs `@opentelemetry/*`, which no example workspace installs; held by packages/observability/src/otel.spec.ts instead -->
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { observability } from "@btravstack/observability";
|
|
144
|
+
import { UnitSpanModule, otel } from "@btravstack/observability/otel";
|
|
145
|
+
|
|
146
|
+
// Beside observability() in the root's imports; the SDK is a resource of the
|
|
147
|
+
// graph โ started with the scope, flushed by release on every exit path.
|
|
148
|
+
// Configuration is the OTEL_* environment conventions, read by the SDK itself.
|
|
149
|
+
const AppImports = [observability(), otel()];
|
|
150
|
+
|
|
151
|
+
// A span per kernel unit, correlated with the same ids the logger stamps:
|
|
152
|
+
// runMain(App, { unit: UnitSpanModule })
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Inbound W3C `traceparent` feeds the unit's trace id in `@btravstack/http-server` and
|
|
156
|
+
`@btravstack/amqp-worker`. Auto-instrumentation must be preloaded
|
|
157
|
+
(`node --import @opentelemetry/auto-instrumentations-node/register`) โ it
|
|
158
|
+
cannot be a provider, and the package does not pretend otherwise. Full
|
|
159
|
+
semantics on
|
|
160
|
+
[the reference page](https://btravstack.github.io/btravstack/reference/observability).
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
[MIT](./LICENSE) ยฉ Benoit TRAVERS
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _btravstack_config = require("@btravstack/config");
|
|
3
|
+
let _btravstack_core = require("@btravstack/core");
|
|
4
|
+
let unthrown = require("unthrown");
|
|
5
|
+
let _btravstack_di = require("@btravstack/di");
|
|
6
|
+
//#region src/config.ts
|
|
7
|
+
/**
|
|
8
|
+
* `LOG_LEVEL`, as a `ConfigField` of the six levels and nothing else.
|
|
9
|
+
*
|
|
10
|
+
* A value outside the set is a `ConfigInvalid` naming the variable โ exit
|
|
11
|
+
* `78` under `runMain`, before a line is written โ rather than a silent
|
|
12
|
+
* fallback to `info`: a deployment that meant `debug` and typed `verbose`
|
|
13
|
+
* should be told, not quietly under-logged for a week. Built on
|
|
14
|
+
* `Config.string`, so it inherits the semantics every other variable has: an
|
|
15
|
+
* unset variable takes the default, a set-but-blank one is an error.
|
|
16
|
+
*/
|
|
17
|
+
const logLevel = (options = {}) => {
|
|
18
|
+
const text = _btravstack_config.Config.string("LOG_LEVEL", { default: options.default ?? "info" });
|
|
19
|
+
return {
|
|
20
|
+
variable: text.variable,
|
|
21
|
+
parse: (raw) => text.parse(raw).flatMap((value) => _btravstack_core.LEVELS.includes(value) ? (0, unthrown.Ok)(value) : (0, unthrown.Err)(new _btravstack_config.ConfigFieldInvalid({ reason: `must be one of ${_btravstack_core.LEVELS.join(", ")}, got ${JSON.stringify(value)}` })))
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
/** The schema `observability()` binds `LoggerConfig` through โ one field today, and the place a second one lands. */
|
|
25
|
+
const loggerSchema = (level) => _btravstack_config.Config.object({ level: _btravstack_config.Config.pinned(level, logLevel()) });
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/json-sink.ts
|
|
28
|
+
/**
|
|
29
|
+
* An `Error`'s `message` and `stack` are **non-enumerable**, so
|
|
30
|
+
* `JSON.stringify` renders a thrown one as `{}` โ the line that exists to carry
|
|
31
|
+
* a failure would carry nothing. Walks `cause` chains so a wrapped failure keeps
|
|
32
|
+
* its origin.
|
|
33
|
+
*/
|
|
34
|
+
const renderCause = (cause, depth = 0) => {
|
|
35
|
+
if (!(cause instanceof Error)) return cause;
|
|
36
|
+
return {
|
|
37
|
+
name: cause.name,
|
|
38
|
+
message: cause.message,
|
|
39
|
+
stack: cause.stack,
|
|
40
|
+
...cause.cause === void 0 || depth >= 4 ? {} : { cause: renderCause(cause.cause, depth + 1) }
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* One JSON object per line on `stream`.
|
|
45
|
+
*
|
|
46
|
+
* The caller's attributes are spread FIRST and the line's own fields after them,
|
|
47
|
+
* and that order is the precedence: an `attributes: { level: "info" }` cannot
|
|
48
|
+
* rewrite an `error` line's severity, nor its `traceId`.
|
|
49
|
+
*
|
|
50
|
+
* The unit's ids are spread at the top level rather than nested under `unit`,
|
|
51
|
+
* because `traceId` is the field an operator searches.
|
|
52
|
+
*/
|
|
53
|
+
const jsonSink = (stream = process.stdout) => (line) => {
|
|
54
|
+
const { level, message, attributes, cause, time, unit } = line;
|
|
55
|
+
const rendered = {
|
|
56
|
+
...attributes,
|
|
57
|
+
time: new Date(time).toISOString(),
|
|
58
|
+
level,
|
|
59
|
+
message,
|
|
60
|
+
...unit === void 0 ? {} : unit,
|
|
61
|
+
...cause === void 0 ? {} : { cause: renderCause(cause) }
|
|
62
|
+
};
|
|
63
|
+
stream.write(`${safeStringify(rendered)}\n`);
|
|
64
|
+
};
|
|
65
|
+
const safeStringify = (rendered) => {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.stringify(rendered);
|
|
68
|
+
} catch {
|
|
69
|
+
return JSON.stringify({
|
|
70
|
+
time: rendered["time"],
|
|
71
|
+
level: rendered["level"],
|
|
72
|
+
message: rendered["message"],
|
|
73
|
+
cause: "[unserialisable]"
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/logger.ts
|
|
79
|
+
const severity = (level) => _btravstack_core.LEVELS.indexOf(level);
|
|
80
|
+
/**
|
|
81
|
+
* A logger over `sink`, filtered at `level` and correlated with the ambient
|
|
82
|
+
* unit.
|
|
83
|
+
*
|
|
84
|
+
* The correlation is read **per call**, not captured: one logger is built per
|
|
85
|
+
* scope and every unit the kernel opens has its own record, so a logger that
|
|
86
|
+
* captured it at construction would stamp the wrong trace id on every line but
|
|
87
|
+
* the first. `with` layers attributes and nothing else, so a child costs one
|
|
88
|
+
* object and shares the sink.
|
|
89
|
+
*
|
|
90
|
+
* Every path is wrapped: a sink that throws is swallowed here, because a
|
|
91
|
+
* logger that takes the process down is worse than a line nobody sees.
|
|
92
|
+
*/
|
|
93
|
+
const createLogger = (sink, level = "info") => {
|
|
94
|
+
const floor = severity(level);
|
|
95
|
+
const build = (base) => {
|
|
96
|
+
const write = (lineLevel, message, attributes, cause) => {
|
|
97
|
+
if (severity(lineLevel) < floor) return;
|
|
98
|
+
const unit = (0, _btravstack_core.currentUnit)();
|
|
99
|
+
try {
|
|
100
|
+
sink({
|
|
101
|
+
level: lineLevel,
|
|
102
|
+
message,
|
|
103
|
+
attributes: attributes === void 0 ? base : {
|
|
104
|
+
...base,
|
|
105
|
+
...attributes
|
|
106
|
+
},
|
|
107
|
+
cause,
|
|
108
|
+
time: Date.now(),
|
|
109
|
+
unit: unit === void 0 ? void 0 : {
|
|
110
|
+
unitId: unit.unitId,
|
|
111
|
+
traceId: unit.traceId,
|
|
112
|
+
...unit.tenantId === void 0 ? {} : { tenantId: unit.tenantId }
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
} catch {}
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
log: (lineLevel, message, attributes, cause) => write(lineLevel, message, attributes, cause),
|
|
119
|
+
trace: (message, attributes, cause) => write("trace", message, attributes, cause),
|
|
120
|
+
debug: (message, attributes, cause) => write("debug", message, attributes, cause),
|
|
121
|
+
info: (message, attributes, cause) => write("info", message, attributes, cause),
|
|
122
|
+
warn: (message, attributes, cause) => write("warn", message, attributes, cause),
|
|
123
|
+
error: (message, attributes, cause) => write("error", message, attributes, cause),
|
|
124
|
+
fatal: (message, attributes, cause) => write("fatal", message, attributes, cause),
|
|
125
|
+
with: (attributes) => build({
|
|
126
|
+
...base,
|
|
127
|
+
...attributes
|
|
128
|
+
}),
|
|
129
|
+
isEnabled: (lineLevel) => severity(lineLevel) >= floor
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
return build({});
|
|
133
|
+
};
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/observability.ts
|
|
136
|
+
/**
|
|
137
|
+
* What the graph bound from the environment: the level every logger in it
|
|
138
|
+
* filters at. A port of its own, like a starter's `HttpConfig`, so anything
|
|
139
|
+
* that wants to know reads it rather than re-deriving it.
|
|
140
|
+
*/
|
|
141
|
+
var LoggerConfig = class extends (0, _btravstack_di.Port)("LoggerConfig") {};
|
|
142
|
+
/**
|
|
143
|
+
* The observability starter: a module providing the application's `Logger` and
|
|
144
|
+
* the `LoggerConfig` it was built from. Import it next to the application and
|
|
145
|
+
* export `Logger`.
|
|
146
|
+
*
|
|
147
|
+
* Every line carries the ambient unit's `traceId`, because the logger reads
|
|
148
|
+
* `currentUnit()` per call โ so a request's lines are attributable with no
|
|
149
|
+
* argument threaded through the call stack.
|
|
150
|
+
*/
|
|
151
|
+
const observability = (options = {}) => (0, _btravstack_di.Module)("Observability")({
|
|
152
|
+
needs: [_btravstack_config.Env],
|
|
153
|
+
provides: [_btravstack_config.Config.provider(LoggerConfig)(loggerSchema(options.level)), (0, _btravstack_di.Provider)(_btravstack_core.Logger)({ config: LoggerConfig }, { sync: ({ config }) => createLogger(options.sink ?? jsonSink(), config.level) })],
|
|
154
|
+
exports: [_btravstack_core.Logger, LoggerConfig]
|
|
155
|
+
});
|
|
156
|
+
/**
|
|
157
|
+
* The kernel's nine lifecycle events, as log lines on `logger` โ the adapter
|
|
158
|
+
* between `StartOptions.onEvent` and an application's own stream.
|
|
159
|
+
*
|
|
160
|
+
* The mapping is deliberate rather than mechanical: `startFailed` and `uncaught`
|
|
161
|
+
* are `error`, `teardownError` is `warn` (the application is already stopping
|
|
162
|
+
* and the exit code says so), everything else `info`. Each event's own fields
|
|
163
|
+
* become attributes, so a drain is queryable by field.
|
|
164
|
+
*
|
|
165
|
+
* The logger is a PARAMETER, not resolved: `building` is emitted while the graph
|
|
166
|
+
* is still being built, so the sink cannot come from the context it watches.
|
|
167
|
+
*/
|
|
168
|
+
const kernelEvents = (logger) => (event) => {
|
|
169
|
+
switch (event.type) {
|
|
170
|
+
case "startFailed":
|
|
171
|
+
logger.error("the application failed to start", { event: event.type }, event.cause);
|
|
172
|
+
return;
|
|
173
|
+
case "uncaught":
|
|
174
|
+
logger.error("an uncaught exception stopped the application", { event: event.type }, event.cause);
|
|
175
|
+
return;
|
|
176
|
+
case "teardownError":
|
|
177
|
+
logger.warn("a finaliser failed while the application was stopping", {
|
|
178
|
+
event: event.type,
|
|
179
|
+
port: event.port
|
|
180
|
+
}, event.cause);
|
|
181
|
+
return;
|
|
182
|
+
case "serving":
|
|
183
|
+
logger.info("serving", {
|
|
184
|
+
event: event.type,
|
|
185
|
+
runtime: event.runtime,
|
|
186
|
+
probePort: event.probePort,
|
|
187
|
+
...typeof event.info === "object" && event.info !== null && !Array.isArray(event.info) ? event.info : {}
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
case "draining":
|
|
191
|
+
logger.info("draining", {
|
|
192
|
+
event: event.type,
|
|
193
|
+
inFlight: event.inFlight
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
case "drained":
|
|
197
|
+
logger.info("drained", {
|
|
198
|
+
event: event.type,
|
|
199
|
+
inFlightAtStart: event.report.inFlightAtStart,
|
|
200
|
+
completed: event.report.completed,
|
|
201
|
+
abandoned: event.report.abandoned
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
default: logger.info(event.type, { event: event.type });
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
//#endregion
|
|
208
|
+
exports.LoggerConfig = LoggerConfig;
|
|
209
|
+
exports.createLogger = createLogger;
|
|
210
|
+
exports.jsonSink = jsonSink;
|
|
211
|
+
exports.kernelEvents = kernelEvents;
|
|
212
|
+
exports.logLevel = logLevel;
|
|
213
|
+
exports.observability = observability;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { n as Sink, r as createLogger, t as Line } from "./logger-B1-_76ej.cjs";
|
|
2
|
+
import { ConfigField, ConfigInvalid, Env } from "@btravstack/config";
|
|
3
|
+
import { EventSink, Level, Logger, LoggerService } from "@btravstack/core";
|
|
4
|
+
import { Module } from "@btravstack/di";
|
|
5
|
+
//#region src/config.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* `LOG_LEVEL`, as a `ConfigField` of the six levels and nothing else.
|
|
8
|
+
*
|
|
9
|
+
* A value outside the set is a `ConfigInvalid` naming the variable โ exit
|
|
10
|
+
* `78` under `runMain`, before a line is written โ rather than a silent
|
|
11
|
+
* fallback to `info`: a deployment that meant `debug` and typed `verbose`
|
|
12
|
+
* should be told, not quietly under-logged for a week. Built on
|
|
13
|
+
* `Config.string`, so it inherits the semantics every other variable has: an
|
|
14
|
+
* unset variable takes the default, a set-but-blank one is an error.
|
|
15
|
+
*/
|
|
16
|
+
declare const logLevel: (options?: {
|
|
17
|
+
readonly default?: Level;
|
|
18
|
+
}) => ConfigField<Level>;
|
|
19
|
+
/** What `observability()` binds from the environment. */
|
|
20
|
+
type LoggerSettings = {
|
|
21
|
+
readonly level: Level;
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/json-sink.d.ts
|
|
25
|
+
/**
|
|
26
|
+
* One JSON object per line on `stream`.
|
|
27
|
+
*
|
|
28
|
+
* The caller's attributes are spread FIRST and the line's own fields after them,
|
|
29
|
+
* and that order is the precedence: an `attributes: { level: "info" }` cannot
|
|
30
|
+
* rewrite an `error` line's severity, nor its `traceId`.
|
|
31
|
+
*
|
|
32
|
+
* The unit's ids are spread at the top level rather than nested under `unit`,
|
|
33
|
+
* because `traceId` is the field an operator searches.
|
|
34
|
+
*/
|
|
35
|
+
declare const jsonSink: (stream?: {
|
|
36
|
+
readonly write: (chunk: string) => unknown;
|
|
37
|
+
}) => Sink;
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/observability.d.ts
|
|
40
|
+
declare const LoggerConfig_base: import("@btravstack/di").PortClass<"LoggerConfig">;
|
|
41
|
+
/**
|
|
42
|
+
* What the graph bound from the environment: the level every logger in it
|
|
43
|
+
* filters at. A port of its own, like a starter's `HttpConfig`, so anything
|
|
44
|
+
* that wants to know reads it rather than re-deriving it.
|
|
45
|
+
*/
|
|
46
|
+
declare class LoggerConfig extends LoggerConfig_base<LoggerSettings> {}
|
|
47
|
+
type ObservabilityOptions = {
|
|
48
|
+
/**
|
|
49
|
+
* Where lines go. Default: one JSON object per line on `stdout`. The
|
|
50
|
+
* `@btravstack/observability/pino` subpath is the same seam for a deployment
|
|
51
|
+
* that wants pino's throughput.
|
|
52
|
+
*/
|
|
53
|
+
readonly sink?: Sink;
|
|
54
|
+
/** Pins the level instead of reading `LOG_LEVEL` โ a test's `"fatal"`, a CLI's `"debug"`. */
|
|
55
|
+
readonly level?: Level;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The observability starter: a module providing the application's `Logger` and
|
|
59
|
+
* the `LoggerConfig` it was built from. Import it next to the application and
|
|
60
|
+
* export `Logger`.
|
|
61
|
+
*
|
|
62
|
+
* Every line carries the ambient unit's `traceId`, because the logger reads
|
|
63
|
+
* `currentUnit()` per call โ so a request's lines are attributable with no
|
|
64
|
+
* argument threaded through the call stack.
|
|
65
|
+
*/
|
|
66
|
+
declare const observability: (options?: ObservabilityOptions) => Module<Logger | LoggerConfig, ConfigInvalid, Env>;
|
|
67
|
+
/**
|
|
68
|
+
* The kernel's nine lifecycle events, as log lines on `logger` โ the adapter
|
|
69
|
+
* between `StartOptions.onEvent` and an application's own stream.
|
|
70
|
+
*
|
|
71
|
+
* The mapping is deliberate rather than mechanical: `startFailed` and `uncaught`
|
|
72
|
+
* are `error`, `teardownError` is `warn` (the application is already stopping
|
|
73
|
+
* and the exit code says so), everything else `info`. Each event's own fields
|
|
74
|
+
* become attributes, so a drain is queryable by field.
|
|
75
|
+
*
|
|
76
|
+
* The logger is a PARAMETER, not resolved: `building` is emitted while the graph
|
|
77
|
+
* is still being built, so the sink cannot come from the context it watches.
|
|
78
|
+
*/
|
|
79
|
+
declare const kernelEvents: (logger: LoggerService) => EventSink;
|
|
80
|
+
//#endregion
|
|
81
|
+
export { type Line, LoggerConfig, type LoggerSettings, type ObservabilityOptions, type Sink, createLogger, jsonSink, kernelEvents, logLevel, observability };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { n as Sink, r as createLogger, t as Line } from "./logger-B1-_76ej.mjs";
|
|
2
|
+
import { ConfigField, ConfigInvalid, Env } from "@btravstack/config";
|
|
3
|
+
import { EventSink, Level, Logger, LoggerService } from "@btravstack/core";
|
|
4
|
+
import { Module } from "@btravstack/di";
|
|
5
|
+
//#region src/config.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* `LOG_LEVEL`, as a `ConfigField` of the six levels and nothing else.
|
|
8
|
+
*
|
|
9
|
+
* A value outside the set is a `ConfigInvalid` naming the variable โ exit
|
|
10
|
+
* `78` under `runMain`, before a line is written โ rather than a silent
|
|
11
|
+
* fallback to `info`: a deployment that meant `debug` and typed `verbose`
|
|
12
|
+
* should be told, not quietly under-logged for a week. Built on
|
|
13
|
+
* `Config.string`, so it inherits the semantics every other variable has: an
|
|
14
|
+
* unset variable takes the default, a set-but-blank one is an error.
|
|
15
|
+
*/
|
|
16
|
+
declare const logLevel: (options?: {
|
|
17
|
+
readonly default?: Level;
|
|
18
|
+
}) => ConfigField<Level>;
|
|
19
|
+
/** What `observability()` binds from the environment. */
|
|
20
|
+
type LoggerSettings = {
|
|
21
|
+
readonly level: Level;
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/json-sink.d.ts
|
|
25
|
+
/**
|
|
26
|
+
* One JSON object per line on `stream`.
|
|
27
|
+
*
|
|
28
|
+
* The caller's attributes are spread FIRST and the line's own fields after them,
|
|
29
|
+
* and that order is the precedence: an `attributes: { level: "info" }` cannot
|
|
30
|
+
* rewrite an `error` line's severity, nor its `traceId`.
|
|
31
|
+
*
|
|
32
|
+
* The unit's ids are spread at the top level rather than nested under `unit`,
|
|
33
|
+
* because `traceId` is the field an operator searches.
|
|
34
|
+
*/
|
|
35
|
+
declare const jsonSink: (stream?: {
|
|
36
|
+
readonly write: (chunk: string) => unknown;
|
|
37
|
+
}) => Sink;
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/observability.d.ts
|
|
40
|
+
declare const LoggerConfig_base: import("@btravstack/di").PortClass<"LoggerConfig">;
|
|
41
|
+
/**
|
|
42
|
+
* What the graph bound from the environment: the level every logger in it
|
|
43
|
+
* filters at. A port of its own, like a starter's `HttpConfig`, so anything
|
|
44
|
+
* that wants to know reads it rather than re-deriving it.
|
|
45
|
+
*/
|
|
46
|
+
declare class LoggerConfig extends LoggerConfig_base<LoggerSettings> {}
|
|
47
|
+
type ObservabilityOptions = {
|
|
48
|
+
/**
|
|
49
|
+
* Where lines go. Default: one JSON object per line on `stdout`. The
|
|
50
|
+
* `@btravstack/observability/pino` subpath is the same seam for a deployment
|
|
51
|
+
* that wants pino's throughput.
|
|
52
|
+
*/
|
|
53
|
+
readonly sink?: Sink;
|
|
54
|
+
/** Pins the level instead of reading `LOG_LEVEL` โ a test's `"fatal"`, a CLI's `"debug"`. */
|
|
55
|
+
readonly level?: Level;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The observability starter: a module providing the application's `Logger` and
|
|
59
|
+
* the `LoggerConfig` it was built from. Import it next to the application and
|
|
60
|
+
* export `Logger`.
|
|
61
|
+
*
|
|
62
|
+
* Every line carries the ambient unit's `traceId`, because the logger reads
|
|
63
|
+
* `currentUnit()` per call โ so a request's lines are attributable with no
|
|
64
|
+
* argument threaded through the call stack.
|
|
65
|
+
*/
|
|
66
|
+
declare const observability: (options?: ObservabilityOptions) => Module<Logger | LoggerConfig, ConfigInvalid, Env>;
|
|
67
|
+
/**
|
|
68
|
+
* The kernel's nine lifecycle events, as log lines on `logger` โ the adapter
|
|
69
|
+
* between `StartOptions.onEvent` and an application's own stream.
|
|
70
|
+
*
|
|
71
|
+
* The mapping is deliberate rather than mechanical: `startFailed` and `uncaught`
|
|
72
|
+
* are `error`, `teardownError` is `warn` (the application is already stopping
|
|
73
|
+
* and the exit code says so), everything else `info`. Each event's own fields
|
|
74
|
+
* become attributes, so a drain is queryable by field.
|
|
75
|
+
*
|
|
76
|
+
* The logger is a PARAMETER, not resolved: `building` is emitted while the graph
|
|
77
|
+
* is still being built, so the sink cannot come from the context it watches.
|
|
78
|
+
*/
|
|
79
|
+
declare const kernelEvents: (logger: LoggerService) => EventSink;
|
|
80
|
+
//#endregion
|
|
81
|
+
export { type Line, LoggerConfig, type LoggerSettings, type ObservabilityOptions, type Sink, createLogger, jsonSink, kernelEvents, logLevel, observability };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Config, ConfigFieldInvalid, Env } from "@btravstack/config";
|
|
2
|
+
import { LEVELS, Logger, currentUnit } from "@btravstack/core";
|
|
3
|
+
import { Err, Ok } from "unthrown";
|
|
4
|
+
import { Module, Port, Provider } from "@btravstack/di";
|
|
5
|
+
//#region src/config.ts
|
|
6
|
+
/**
|
|
7
|
+
* `LOG_LEVEL`, as a `ConfigField` of the six levels and nothing else.
|
|
8
|
+
*
|
|
9
|
+
* A value outside the set is a `ConfigInvalid` naming the variable โ exit
|
|
10
|
+
* `78` under `runMain`, before a line is written โ rather than a silent
|
|
11
|
+
* fallback to `info`: a deployment that meant `debug` and typed `verbose`
|
|
12
|
+
* should be told, not quietly under-logged for a week. Built on
|
|
13
|
+
* `Config.string`, so it inherits the semantics every other variable has: an
|
|
14
|
+
* unset variable takes the default, a set-but-blank one is an error.
|
|
15
|
+
*/
|
|
16
|
+
const logLevel = (options = {}) => {
|
|
17
|
+
const text = Config.string("LOG_LEVEL", { default: options.default ?? "info" });
|
|
18
|
+
return {
|
|
19
|
+
variable: text.variable,
|
|
20
|
+
parse: (raw) => text.parse(raw).flatMap((value) => LEVELS.includes(value) ? Ok(value) : Err(new ConfigFieldInvalid({ reason: `must be one of ${LEVELS.join(", ")}, got ${JSON.stringify(value)}` })))
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
/** The schema `observability()` binds `LoggerConfig` through โ one field today, and the place a second one lands. */
|
|
24
|
+
const loggerSchema = (level) => Config.object({ level: Config.pinned(level, logLevel()) });
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/json-sink.ts
|
|
27
|
+
/**
|
|
28
|
+
* An `Error`'s `message` and `stack` are **non-enumerable**, so
|
|
29
|
+
* `JSON.stringify` renders a thrown one as `{}` โ the line that exists to carry
|
|
30
|
+
* a failure would carry nothing. Walks `cause` chains so a wrapped failure keeps
|
|
31
|
+
* its origin.
|
|
32
|
+
*/
|
|
33
|
+
const renderCause = (cause, depth = 0) => {
|
|
34
|
+
if (!(cause instanceof Error)) return cause;
|
|
35
|
+
return {
|
|
36
|
+
name: cause.name,
|
|
37
|
+
message: cause.message,
|
|
38
|
+
stack: cause.stack,
|
|
39
|
+
...cause.cause === void 0 || depth >= 4 ? {} : { cause: renderCause(cause.cause, depth + 1) }
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* One JSON object per line on `stream`.
|
|
44
|
+
*
|
|
45
|
+
* The caller's attributes are spread FIRST and the line's own fields after them,
|
|
46
|
+
* and that order is the precedence: an `attributes: { level: "info" }` cannot
|
|
47
|
+
* rewrite an `error` line's severity, nor its `traceId`.
|
|
48
|
+
*
|
|
49
|
+
* The unit's ids are spread at the top level rather than nested under `unit`,
|
|
50
|
+
* because `traceId` is the field an operator searches.
|
|
51
|
+
*/
|
|
52
|
+
const jsonSink = (stream = process.stdout) => (line) => {
|
|
53
|
+
const { level, message, attributes, cause, time, unit } = line;
|
|
54
|
+
const rendered = {
|
|
55
|
+
...attributes,
|
|
56
|
+
time: new Date(time).toISOString(),
|
|
57
|
+
level,
|
|
58
|
+
message,
|
|
59
|
+
...unit === void 0 ? {} : unit,
|
|
60
|
+
...cause === void 0 ? {} : { cause: renderCause(cause) }
|
|
61
|
+
};
|
|
62
|
+
stream.write(`${safeStringify(rendered)}\n`);
|
|
63
|
+
};
|
|
64
|
+
const safeStringify = (rendered) => {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.stringify(rendered);
|
|
67
|
+
} catch {
|
|
68
|
+
return JSON.stringify({
|
|
69
|
+
time: rendered["time"],
|
|
70
|
+
level: rendered["level"],
|
|
71
|
+
message: rendered["message"],
|
|
72
|
+
cause: "[unserialisable]"
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/logger.ts
|
|
78
|
+
const severity = (level) => LEVELS.indexOf(level);
|
|
79
|
+
/**
|
|
80
|
+
* A logger over `sink`, filtered at `level` and correlated with the ambient
|
|
81
|
+
* unit.
|
|
82
|
+
*
|
|
83
|
+
* The correlation is read **per call**, not captured: one logger is built per
|
|
84
|
+
* scope and every unit the kernel opens has its own record, so a logger that
|
|
85
|
+
* captured it at construction would stamp the wrong trace id on every line but
|
|
86
|
+
* the first. `with` layers attributes and nothing else, so a child costs one
|
|
87
|
+
* object and shares the sink.
|
|
88
|
+
*
|
|
89
|
+
* Every path is wrapped: a sink that throws is swallowed here, because a
|
|
90
|
+
* logger that takes the process down is worse than a line nobody sees.
|
|
91
|
+
*/
|
|
92
|
+
const createLogger = (sink, level = "info") => {
|
|
93
|
+
const floor = severity(level);
|
|
94
|
+
const build = (base) => {
|
|
95
|
+
const write = (lineLevel, message, attributes, cause) => {
|
|
96
|
+
if (severity(lineLevel) < floor) return;
|
|
97
|
+
const unit = currentUnit();
|
|
98
|
+
try {
|
|
99
|
+
sink({
|
|
100
|
+
level: lineLevel,
|
|
101
|
+
message,
|
|
102
|
+
attributes: attributes === void 0 ? base : {
|
|
103
|
+
...base,
|
|
104
|
+
...attributes
|
|
105
|
+
},
|
|
106
|
+
cause,
|
|
107
|
+
time: Date.now(),
|
|
108
|
+
unit: unit === void 0 ? void 0 : {
|
|
109
|
+
unitId: unit.unitId,
|
|
110
|
+
traceId: unit.traceId,
|
|
111
|
+
...unit.tenantId === void 0 ? {} : { tenantId: unit.tenantId }
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
} catch {}
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
log: (lineLevel, message, attributes, cause) => write(lineLevel, message, attributes, cause),
|
|
118
|
+
trace: (message, attributes, cause) => write("trace", message, attributes, cause),
|
|
119
|
+
debug: (message, attributes, cause) => write("debug", message, attributes, cause),
|
|
120
|
+
info: (message, attributes, cause) => write("info", message, attributes, cause),
|
|
121
|
+
warn: (message, attributes, cause) => write("warn", message, attributes, cause),
|
|
122
|
+
error: (message, attributes, cause) => write("error", message, attributes, cause),
|
|
123
|
+
fatal: (message, attributes, cause) => write("fatal", message, attributes, cause),
|
|
124
|
+
with: (attributes) => build({
|
|
125
|
+
...base,
|
|
126
|
+
...attributes
|
|
127
|
+
}),
|
|
128
|
+
isEnabled: (lineLevel) => severity(lineLevel) >= floor
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
return build({});
|
|
132
|
+
};
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/observability.ts
|
|
135
|
+
/**
|
|
136
|
+
* What the graph bound from the environment: the level every logger in it
|
|
137
|
+
* filters at. A port of its own, like a starter's `HttpConfig`, so anything
|
|
138
|
+
* that wants to know reads it rather than re-deriving it.
|
|
139
|
+
*/
|
|
140
|
+
var LoggerConfig = class extends Port("LoggerConfig") {};
|
|
141
|
+
/**
|
|
142
|
+
* The observability starter: a module providing the application's `Logger` and
|
|
143
|
+
* the `LoggerConfig` it was built from. Import it next to the application and
|
|
144
|
+
* export `Logger`.
|
|
145
|
+
*
|
|
146
|
+
* Every line carries the ambient unit's `traceId`, because the logger reads
|
|
147
|
+
* `currentUnit()` per call โ so a request's lines are attributable with no
|
|
148
|
+
* argument threaded through the call stack.
|
|
149
|
+
*/
|
|
150
|
+
const observability = (options = {}) => Module("Observability")({
|
|
151
|
+
needs: [Env],
|
|
152
|
+
provides: [Config.provider(LoggerConfig)(loggerSchema(options.level)), Provider(Logger)({ config: LoggerConfig }, { sync: ({ config }) => createLogger(options.sink ?? jsonSink(), config.level) })],
|
|
153
|
+
exports: [Logger, LoggerConfig]
|
|
154
|
+
});
|
|
155
|
+
/**
|
|
156
|
+
* The kernel's nine lifecycle events, as log lines on `logger` โ the adapter
|
|
157
|
+
* between `StartOptions.onEvent` and an application's own stream.
|
|
158
|
+
*
|
|
159
|
+
* The mapping is deliberate rather than mechanical: `startFailed` and `uncaught`
|
|
160
|
+
* are `error`, `teardownError` is `warn` (the application is already stopping
|
|
161
|
+
* and the exit code says so), everything else `info`. Each event's own fields
|
|
162
|
+
* become attributes, so a drain is queryable by field.
|
|
163
|
+
*
|
|
164
|
+
* The logger is a PARAMETER, not resolved: `building` is emitted while the graph
|
|
165
|
+
* is still being built, so the sink cannot come from the context it watches.
|
|
166
|
+
*/
|
|
167
|
+
const kernelEvents = (logger) => (event) => {
|
|
168
|
+
switch (event.type) {
|
|
169
|
+
case "startFailed":
|
|
170
|
+
logger.error("the application failed to start", { event: event.type }, event.cause);
|
|
171
|
+
return;
|
|
172
|
+
case "uncaught":
|
|
173
|
+
logger.error("an uncaught exception stopped the application", { event: event.type }, event.cause);
|
|
174
|
+
return;
|
|
175
|
+
case "teardownError":
|
|
176
|
+
logger.warn("a finaliser failed while the application was stopping", {
|
|
177
|
+
event: event.type,
|
|
178
|
+
port: event.port
|
|
179
|
+
}, event.cause);
|
|
180
|
+
return;
|
|
181
|
+
case "serving":
|
|
182
|
+
logger.info("serving", {
|
|
183
|
+
event: event.type,
|
|
184
|
+
runtime: event.runtime,
|
|
185
|
+
probePort: event.probePort,
|
|
186
|
+
...typeof event.info === "object" && event.info !== null && !Array.isArray(event.info) ? event.info : {}
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
case "draining":
|
|
190
|
+
logger.info("draining", {
|
|
191
|
+
event: event.type,
|
|
192
|
+
inFlight: event.inFlight
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
case "drained":
|
|
196
|
+
logger.info("drained", {
|
|
197
|
+
event: event.type,
|
|
198
|
+
inFlightAtStart: event.report.inFlightAtStart,
|
|
199
|
+
completed: event.report.completed,
|
|
200
|
+
abandoned: event.report.abandoned
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
default: logger.info(event.type, { event: event.type });
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
//#endregion
|
|
207
|
+
export { LoggerConfig, createLogger, jsonSink, kernelEvents, logLevel, observability };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Attributes, Level, LoggerService } from "@btravstack/core";
|
|
2
|
+
//#region src/logger.d.ts
|
|
3
|
+
/** One line, as the implementation hands it to a {@link Sink}: the message, its severity, and everything known about it. */
|
|
4
|
+
type Line = {
|
|
5
|
+
readonly level: Level;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
readonly attributes: Attributes;
|
|
8
|
+
readonly cause: unknown;
|
|
9
|
+
/** Milliseconds since the epoch, stamped when the line was written. */
|
|
10
|
+
readonly time: number;
|
|
11
|
+
/** What `currentUnit()` carried, or `undefined` outside a unit. */
|
|
12
|
+
readonly unit: {
|
|
13
|
+
readonly unitId: string;
|
|
14
|
+
readonly traceId: string;
|
|
15
|
+
readonly tenantId?: string;
|
|
16
|
+
} | undefined;
|
|
17
|
+
};
|
|
18
|
+
/** Where a line goes. Given a {@link Line}, writes it โ and never throws, which `createLogger` guarantees on its behalf. */
|
|
19
|
+
type Sink = (line: Line) => void;
|
|
20
|
+
/**
|
|
21
|
+
* A logger over `sink`, filtered at `level` and correlated with the ambient
|
|
22
|
+
* unit.
|
|
23
|
+
*
|
|
24
|
+
* The correlation is read **per call**, not captured: one logger is built per
|
|
25
|
+
* scope and every unit the kernel opens has its own record, so a logger that
|
|
26
|
+
* captured it at construction would stamp the wrong trace id on every line but
|
|
27
|
+
* the first. `with` layers attributes and nothing else, so a child costs one
|
|
28
|
+
* object and shares the sink.
|
|
29
|
+
*
|
|
30
|
+
* Every path is wrapped: a sink that throws is swallowed here, because a
|
|
31
|
+
* logger that takes the process down is worse than a line nobody sees.
|
|
32
|
+
*/
|
|
33
|
+
declare const createLogger: (sink: Sink, level?: Level) => LoggerService;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { Sink as n, createLogger as r, Line as t };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Attributes, Level, LoggerService } from "@btravstack/core";
|
|
2
|
+
//#region src/logger.d.ts
|
|
3
|
+
/** One line, as the implementation hands it to a {@link Sink}: the message, its severity, and everything known about it. */
|
|
4
|
+
type Line = {
|
|
5
|
+
readonly level: Level;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
readonly attributes: Attributes;
|
|
8
|
+
readonly cause: unknown;
|
|
9
|
+
/** Milliseconds since the epoch, stamped when the line was written. */
|
|
10
|
+
readonly time: number;
|
|
11
|
+
/** What `currentUnit()` carried, or `undefined` outside a unit. */
|
|
12
|
+
readonly unit: {
|
|
13
|
+
readonly unitId: string;
|
|
14
|
+
readonly traceId: string;
|
|
15
|
+
readonly tenantId?: string;
|
|
16
|
+
} | undefined;
|
|
17
|
+
};
|
|
18
|
+
/** Where a line goes. Given a {@link Line}, writes it โ and never throws, which `createLogger` guarantees on its behalf. */
|
|
19
|
+
type Sink = (line: Line) => void;
|
|
20
|
+
/**
|
|
21
|
+
* A logger over `sink`, filtered at `level` and correlated with the ambient
|
|
22
|
+
* unit.
|
|
23
|
+
*
|
|
24
|
+
* The correlation is read **per call**, not captured: one logger is built per
|
|
25
|
+
* scope and every unit the kernel opens has its own record, so a logger that
|
|
26
|
+
* captured it at construction would stamp the wrong trace id on every line but
|
|
27
|
+
* the first. `with` layers attributes and nothing else, so a child costs one
|
|
28
|
+
* object and shares the sink.
|
|
29
|
+
*
|
|
30
|
+
* Every path is wrapped: a sink that throws is swallowed here, because a
|
|
31
|
+
* logger that takes the process down is worse than a line nobody sees.
|
|
32
|
+
*/
|
|
33
|
+
declare const createLogger: (sink: Sink, level?: Level) => LoggerService;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { Sink as n, createLogger as r, Line as t };
|
package/dist/otel.cjs
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _btravstack_core = require("@btravstack/core");
|
|
3
|
+
let unthrown = require("unthrown");
|
|
4
|
+
let _btravstack_di = require("@btravstack/di");
|
|
5
|
+
let _opentelemetry_api = require("@opentelemetry/api");
|
|
6
|
+
let _opentelemetry_sdk_node = require("@opentelemetry/sdk-node");
|
|
7
|
+
//#region src/otel.ts
|
|
8
|
+
/**
|
|
9
|
+
* The tracing half of observability โ the ADAPTER, not the contract. `Tracer`
|
|
10
|
+
* and `Meter` are the kernel's ports, and OTel's presence stops at this subpath,
|
|
11
|
+
* behind an optional peer exactly like `pino`.
|
|
12
|
+
*
|
|
13
|
+
* The ports are narrowings of OTel's own shapes, so what the SDK hands back
|
|
14
|
+
* satisfies them with no translation in between.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The SDK itself, module-private: providing it is what starts it, and `release`
|
|
18
|
+
* is what flushes it. It rides the graph as a RESOURCE so the kernel closes it
|
|
19
|
+
* on every exit path โ a span lost in `shutdown()` becomes a `teardownError` and
|
|
20
|
+
* exit `2`, never silence.
|
|
21
|
+
*/
|
|
22
|
+
var OtelSdk = class extends (0, _btravstack_di.Port)("OtelSdk") {};
|
|
23
|
+
/**
|
|
24
|
+
* The OTel starter: a module providing `Tracer` and `Meter` over a `NodeSDK`
|
|
25
|
+
* opened with the scope and flushed on close. Compose it beside
|
|
26
|
+
* `observability()` in a root's `imports`.
|
|
27
|
+
*
|
|
28
|
+
* **No config slice, deliberately**: the SDK reads the `OTEL_*` environment
|
|
29
|
+
* conventions itself, and re-binding them through `Config` would be a second
|
|
30
|
+
* spelling of names operators already know. Programmatic overrides go through
|
|
31
|
+
* `options`, the SDK's own configuration type.
|
|
32
|
+
*
|
|
33
|
+
* **Auto-instrumentation cannot live here**: the register hook must be
|
|
34
|
+
* preloaded before the instrumented libraries are imported, which no DI
|
|
35
|
+
* provider can promise. Preloading is the deployment's line.
|
|
36
|
+
*/
|
|
37
|
+
const otel = (options) => (0, _btravstack_di.Module)("Otel")({
|
|
38
|
+
provides: [
|
|
39
|
+
(0, _btravstack_di.Provider)(OtelSdk)({
|
|
40
|
+
acquire: () => {
|
|
41
|
+
const sdk = new _opentelemetry_sdk_node.NodeSDK(options);
|
|
42
|
+
sdk.start();
|
|
43
|
+
return (0, unthrown.Ok)(sdk);
|
|
44
|
+
},
|
|
45
|
+
release: (sdk) => sdk.shutdown()
|
|
46
|
+
}),
|
|
47
|
+
(0, _btravstack_di.Provider)(_btravstack_core.Tracer)({ sdk: OtelSdk }, { sync: () => {
|
|
48
|
+
const tracer = _opentelemetry_api.trace.getTracer("@btravstack/observability");
|
|
49
|
+
return { startSpan: (name) => tracer.startSpan(name) };
|
|
50
|
+
} }),
|
|
51
|
+
(0, _btravstack_di.Provider)(_btravstack_core.Meter)({ sdk: OtelSdk }, { sync: () => _opentelemetry_api.metrics.getMeter("@btravstack/observability") })
|
|
52
|
+
],
|
|
53
|
+
exports: [_btravstack_core.Tracer, _btravstack_core.Meter]
|
|
54
|
+
});
|
|
55
|
+
/** The span a unit rides, ended when the unit's fork closes. */
|
|
56
|
+
var UnitSpan = class extends (0, _btravstack_di.Port)("UnitSpan") {};
|
|
57
|
+
/**
|
|
58
|
+
* A span per kernel unit, as a `StartOptions.unit` module: the kernel forks it
|
|
59
|
+
* per unit and tears it down inside the unit's ambient record, so the span opens
|
|
60
|
+
* when the unit does and `onStop` ends it on every path out.
|
|
61
|
+
*
|
|
62
|
+
* The correlation is the ambient record's own, carried as attributes so a span
|
|
63
|
+
* joins the same query the logger's lines answer. The remote PARENT is
|
|
64
|
+
* deliberately not reconstructed: `UnitMeta.traceId` carries the inbound trace
|
|
65
|
+
* id alone, so this correlates by attribute rather than pretending to a W3C
|
|
66
|
+
* parent-child edge it cannot prove.
|
|
67
|
+
*
|
|
68
|
+
* Compose it as `start`'s `unit`; the root must export `Tracer`.
|
|
69
|
+
*/
|
|
70
|
+
const UnitSpanModule = (0, _btravstack_di.Module)("UnitSpan")({
|
|
71
|
+
needs: [_btravstack_core.Tracer],
|
|
72
|
+
provides: [(0, _btravstack_di.Provider)(UnitSpan)({ tracer: _btravstack_core.Tracer }, {
|
|
73
|
+
sync: ({ tracer }) => {
|
|
74
|
+
const unit = (0, _btravstack_core.currentUnit)();
|
|
75
|
+
const span = tracer.startSpan("unit");
|
|
76
|
+
if (unit !== void 0) span.setAttributes({
|
|
77
|
+
"btravstack.unit_id": unit.unitId,
|
|
78
|
+
"btravstack.trace_id": unit.traceId,
|
|
79
|
+
...unit.tenantId === void 0 ? {} : { "btravstack.tenant_id": unit.tenantId }
|
|
80
|
+
});
|
|
81
|
+
return span;
|
|
82
|
+
},
|
|
83
|
+
onStop: (span) => span.end()
|
|
84
|
+
})],
|
|
85
|
+
exports: [UnitSpan]
|
|
86
|
+
});
|
|
87
|
+
//#endregion
|
|
88
|
+
exports.UnitSpan = UnitSpan;
|
|
89
|
+
exports.UnitSpanModule = UnitSpanModule;
|
|
90
|
+
exports.otel = otel;
|
package/dist/otel.d.cts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Meter, Span, Tracer } from "@btravstack/core";
|
|
2
|
+
import { Module, Scope } from "@btravstack/di";
|
|
3
|
+
import { NodeSDKConfiguration } from "@opentelemetry/sdk-node";
|
|
4
|
+
//#region src/otel.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The OTel starter: a module providing `Tracer` and `Meter` over a `NodeSDK`
|
|
7
|
+
* opened with the scope and flushed on close. Compose it beside
|
|
8
|
+
* `observability()` in a root's `imports`.
|
|
9
|
+
*
|
|
10
|
+
* **No config slice, deliberately**: the SDK reads the `OTEL_*` environment
|
|
11
|
+
* conventions itself, and re-binding them through `Config` would be a second
|
|
12
|
+
* spelling of names operators already know. Programmatic overrides go through
|
|
13
|
+
* `options`, the SDK's own configuration type.
|
|
14
|
+
*
|
|
15
|
+
* **Auto-instrumentation cannot live here**: the register hook must be
|
|
16
|
+
* preloaded before the instrumented libraries are imported, which no DI
|
|
17
|
+
* provider can promise. Preloading is the deployment's line.
|
|
18
|
+
*/
|
|
19
|
+
declare const otel: (options?: Partial<NodeSDKConfiguration>) => Module<Tracer | Meter, never, Scope>;
|
|
20
|
+
declare const UnitSpan_base: import("@btravstack/di").PortClass<"UnitSpan">;
|
|
21
|
+
/** The span a unit rides, ended when the unit's fork closes. */
|
|
22
|
+
declare class UnitSpan extends UnitSpan_base<Span> {}
|
|
23
|
+
/**
|
|
24
|
+
* A span per kernel unit, as a `StartOptions.unit` module: the kernel forks it
|
|
25
|
+
* per unit and tears it down inside the unit's ambient record, so the span opens
|
|
26
|
+
* when the unit does and `onStop` ends it on every path out.
|
|
27
|
+
*
|
|
28
|
+
* The correlation is the ambient record's own, carried as attributes so a span
|
|
29
|
+
* joins the same query the logger's lines answer. The remote PARENT is
|
|
30
|
+
* deliberately not reconstructed: `UnitMeta.traceId` carries the inbound trace
|
|
31
|
+
* id alone, so this correlates by attribute rather than pretending to a W3C
|
|
32
|
+
* parent-child edge it cannot prove.
|
|
33
|
+
*
|
|
34
|
+
* Compose it as `start`'s `unit`; the root must export `Tracer`.
|
|
35
|
+
*/
|
|
36
|
+
declare const UnitSpanModule: Module<UnitSpan, never, Scope | Tracer>;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { UnitSpan, UnitSpanModule, otel };
|
package/dist/otel.d.mts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Meter, Span, Tracer } from "@btravstack/core";
|
|
2
|
+
import { Module, Scope } from "@btravstack/di";
|
|
3
|
+
import { NodeSDKConfiguration } from "@opentelemetry/sdk-node";
|
|
4
|
+
//#region src/otel.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The OTel starter: a module providing `Tracer` and `Meter` over a `NodeSDK`
|
|
7
|
+
* opened with the scope and flushed on close. Compose it beside
|
|
8
|
+
* `observability()` in a root's `imports`.
|
|
9
|
+
*
|
|
10
|
+
* **No config slice, deliberately**: the SDK reads the `OTEL_*` environment
|
|
11
|
+
* conventions itself, and re-binding them through `Config` would be a second
|
|
12
|
+
* spelling of names operators already know. Programmatic overrides go through
|
|
13
|
+
* `options`, the SDK's own configuration type.
|
|
14
|
+
*
|
|
15
|
+
* **Auto-instrumentation cannot live here**: the register hook must be
|
|
16
|
+
* preloaded before the instrumented libraries are imported, which no DI
|
|
17
|
+
* provider can promise. Preloading is the deployment's line.
|
|
18
|
+
*/
|
|
19
|
+
declare const otel: (options?: Partial<NodeSDKConfiguration>) => Module<Tracer | Meter, never, Scope>;
|
|
20
|
+
declare const UnitSpan_base: import("@btravstack/di").PortClass<"UnitSpan">;
|
|
21
|
+
/** The span a unit rides, ended when the unit's fork closes. */
|
|
22
|
+
declare class UnitSpan extends UnitSpan_base<Span> {}
|
|
23
|
+
/**
|
|
24
|
+
* A span per kernel unit, as a `StartOptions.unit` module: the kernel forks it
|
|
25
|
+
* per unit and tears it down inside the unit's ambient record, so the span opens
|
|
26
|
+
* when the unit does and `onStop` ends it on every path out.
|
|
27
|
+
*
|
|
28
|
+
* The correlation is the ambient record's own, carried as attributes so a span
|
|
29
|
+
* joins the same query the logger's lines answer. The remote PARENT is
|
|
30
|
+
* deliberately not reconstructed: `UnitMeta.traceId` carries the inbound trace
|
|
31
|
+
* id alone, so this correlates by attribute rather than pretending to a W3C
|
|
32
|
+
* parent-child edge it cannot prove.
|
|
33
|
+
*
|
|
34
|
+
* Compose it as `start`'s `unit`; the root must export `Tracer`.
|
|
35
|
+
*/
|
|
36
|
+
declare const UnitSpanModule: Module<UnitSpan, never, Scope | Tracer>;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { UnitSpan, UnitSpanModule, otel };
|
package/dist/otel.mjs
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Meter, Tracer, currentUnit } from "@btravstack/core";
|
|
2
|
+
import { Ok } from "unthrown";
|
|
3
|
+
import { Module, Port, Provider } from "@btravstack/di";
|
|
4
|
+
import { metrics, trace } from "@opentelemetry/api";
|
|
5
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
6
|
+
//#region src/otel.ts
|
|
7
|
+
/**
|
|
8
|
+
* The tracing half of observability โ the ADAPTER, not the contract. `Tracer`
|
|
9
|
+
* and `Meter` are the kernel's ports, and OTel's presence stops at this subpath,
|
|
10
|
+
* behind an optional peer exactly like `pino`.
|
|
11
|
+
*
|
|
12
|
+
* The ports are narrowings of OTel's own shapes, so what the SDK hands back
|
|
13
|
+
* satisfies them with no translation in between.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The SDK itself, module-private: providing it is what starts it, and `release`
|
|
17
|
+
* is what flushes it. It rides the graph as a RESOURCE so the kernel closes it
|
|
18
|
+
* on every exit path โ a span lost in `shutdown()` becomes a `teardownError` and
|
|
19
|
+
* exit `2`, never silence.
|
|
20
|
+
*/
|
|
21
|
+
var OtelSdk = class extends Port("OtelSdk") {};
|
|
22
|
+
/**
|
|
23
|
+
* The OTel starter: a module providing `Tracer` and `Meter` over a `NodeSDK`
|
|
24
|
+
* opened with the scope and flushed on close. Compose it beside
|
|
25
|
+
* `observability()` in a root's `imports`.
|
|
26
|
+
*
|
|
27
|
+
* **No config slice, deliberately**: the SDK reads the `OTEL_*` environment
|
|
28
|
+
* conventions itself, and re-binding them through `Config` would be a second
|
|
29
|
+
* spelling of names operators already know. Programmatic overrides go through
|
|
30
|
+
* `options`, the SDK's own configuration type.
|
|
31
|
+
*
|
|
32
|
+
* **Auto-instrumentation cannot live here**: the register hook must be
|
|
33
|
+
* preloaded before the instrumented libraries are imported, which no DI
|
|
34
|
+
* provider can promise. Preloading is the deployment's line.
|
|
35
|
+
*/
|
|
36
|
+
const otel = (options) => Module("Otel")({
|
|
37
|
+
provides: [
|
|
38
|
+
Provider(OtelSdk)({
|
|
39
|
+
acquire: () => {
|
|
40
|
+
const sdk = new NodeSDK(options);
|
|
41
|
+
sdk.start();
|
|
42
|
+
return Ok(sdk);
|
|
43
|
+
},
|
|
44
|
+
release: (sdk) => sdk.shutdown()
|
|
45
|
+
}),
|
|
46
|
+
Provider(Tracer)({ sdk: OtelSdk }, { sync: () => {
|
|
47
|
+
const tracer = trace.getTracer("@btravstack/observability");
|
|
48
|
+
return { startSpan: (name) => tracer.startSpan(name) };
|
|
49
|
+
} }),
|
|
50
|
+
Provider(Meter)({ sdk: OtelSdk }, { sync: () => metrics.getMeter("@btravstack/observability") })
|
|
51
|
+
],
|
|
52
|
+
exports: [Tracer, Meter]
|
|
53
|
+
});
|
|
54
|
+
/** The span a unit rides, ended when the unit's fork closes. */
|
|
55
|
+
var UnitSpan = class extends Port("UnitSpan") {};
|
|
56
|
+
/**
|
|
57
|
+
* A span per kernel unit, as a `StartOptions.unit` module: the kernel forks it
|
|
58
|
+
* per unit and tears it down inside the unit's ambient record, so the span opens
|
|
59
|
+
* when the unit does and `onStop` ends it on every path out.
|
|
60
|
+
*
|
|
61
|
+
* The correlation is the ambient record's own, carried as attributes so a span
|
|
62
|
+
* joins the same query the logger's lines answer. The remote PARENT is
|
|
63
|
+
* deliberately not reconstructed: `UnitMeta.traceId` carries the inbound trace
|
|
64
|
+
* id alone, so this correlates by attribute rather than pretending to a W3C
|
|
65
|
+
* parent-child edge it cannot prove.
|
|
66
|
+
*
|
|
67
|
+
* Compose it as `start`'s `unit`; the root must export `Tracer`.
|
|
68
|
+
*/
|
|
69
|
+
const UnitSpanModule = Module("UnitSpan")({
|
|
70
|
+
needs: [Tracer],
|
|
71
|
+
provides: [Provider(UnitSpan)({ tracer: Tracer }, {
|
|
72
|
+
sync: ({ tracer }) => {
|
|
73
|
+
const unit = currentUnit();
|
|
74
|
+
const span = tracer.startSpan("unit");
|
|
75
|
+
if (unit !== void 0) span.setAttributes({
|
|
76
|
+
"btravstack.unit_id": unit.unitId,
|
|
77
|
+
"btravstack.trace_id": unit.traceId,
|
|
78
|
+
...unit.tenantId === void 0 ? {} : { "btravstack.tenant_id": unit.tenantId }
|
|
79
|
+
});
|
|
80
|
+
return span;
|
|
81
|
+
},
|
|
82
|
+
onStop: (span) => span.end()
|
|
83
|
+
})],
|
|
84
|
+
exports: [UnitSpan]
|
|
85
|
+
});
|
|
86
|
+
//#endregion
|
|
87
|
+
export { UnitSpan, UnitSpanModule, otel };
|
package/dist/pino.cjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/pino.ts
|
|
3
|
+
/**
|
|
4
|
+
* A {@link Sink} over a pino logger โ the subpath a deployment reaches for
|
|
5
|
+
* when the default JSON sink's `JSON.stringify` per line is the thing showing
|
|
6
|
+
* up in a profile. `pino` is an **optional** peer: install it, import
|
|
7
|
+
* `@btravstack/observability/pino`, and pass the sink; a consumer that never
|
|
8
|
+
* imports this file never needs it.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import pino from "pino";
|
|
12
|
+
* import { observability } from "@btravstack/observability";
|
|
13
|
+
* import { pinoSink } from "@btravstack/observability/pino";
|
|
14
|
+
*
|
|
15
|
+
* observability({ sink: pinoSink(pino()) });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* The level filter stays **ours**: `createLogger` has already decided the
|
|
19
|
+
* line is worth writing by the time a sink sees it, so pino is configured at
|
|
20
|
+
* `trace` here โ one filter in the process, and it is the one `LOG_LEVEL`
|
|
21
|
+
* validated at startup. `fatal` and `trace` map to pino's own; the ambient
|
|
22
|
+
* unit's ids ride as fields, not as a message prefix, so they stay indexable.
|
|
23
|
+
*/
|
|
24
|
+
const pinoSink = (logger) => (line) => {
|
|
25
|
+
const { level, message, attributes, cause, unit } = line;
|
|
26
|
+
const fields = {
|
|
27
|
+
...attributes,
|
|
28
|
+
...unit === void 0 ? {} : unit,
|
|
29
|
+
...cause === void 0 ? {} : { err: cause }
|
|
30
|
+
};
|
|
31
|
+
logger[level](fields, message);
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
exports.pinoSink = pinoSink;
|
package/dist/pino.d.cts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { n as Sink } from "./logger-B1-_76ej.cjs";
|
|
2
|
+
import { Logger } from "pino";
|
|
3
|
+
//#region src/pino.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A {@link Sink} over a pino logger โ the subpath a deployment reaches for
|
|
6
|
+
* when the default JSON sink's `JSON.stringify` per line is the thing showing
|
|
7
|
+
* up in a profile. `pino` is an **optional** peer: install it, import
|
|
8
|
+
* `@btravstack/observability/pino`, and pass the sink; a consumer that never
|
|
9
|
+
* imports this file never needs it.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import pino from "pino";
|
|
13
|
+
* import { observability } from "@btravstack/observability";
|
|
14
|
+
* import { pinoSink } from "@btravstack/observability/pino";
|
|
15
|
+
*
|
|
16
|
+
* observability({ sink: pinoSink(pino()) });
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* The level filter stays **ours**: `createLogger` has already decided the
|
|
20
|
+
* line is worth writing by the time a sink sees it, so pino is configured at
|
|
21
|
+
* `trace` here โ one filter in the process, and it is the one `LOG_LEVEL`
|
|
22
|
+
* validated at startup. `fatal` and `trace` map to pino's own; the ambient
|
|
23
|
+
* unit's ids ride as fields, not as a message prefix, so they stay indexable.
|
|
24
|
+
*/
|
|
25
|
+
declare const pinoSink: (logger: Logger) => Sink;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { pinoSink };
|
package/dist/pino.d.mts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { n as Sink } from "./logger-B1-_76ej.mjs";
|
|
2
|
+
import { Logger } from "pino";
|
|
3
|
+
//#region src/pino.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A {@link Sink} over a pino logger โ the subpath a deployment reaches for
|
|
6
|
+
* when the default JSON sink's `JSON.stringify` per line is the thing showing
|
|
7
|
+
* up in a profile. `pino` is an **optional** peer: install it, import
|
|
8
|
+
* `@btravstack/observability/pino`, and pass the sink; a consumer that never
|
|
9
|
+
* imports this file never needs it.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import pino from "pino";
|
|
13
|
+
* import { observability } from "@btravstack/observability";
|
|
14
|
+
* import { pinoSink } from "@btravstack/observability/pino";
|
|
15
|
+
*
|
|
16
|
+
* observability({ sink: pinoSink(pino()) });
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* The level filter stays **ours**: `createLogger` has already decided the
|
|
20
|
+
* line is worth writing by the time a sink sees it, so pino is configured at
|
|
21
|
+
* `trace` here โ one filter in the process, and it is the one `LOG_LEVEL`
|
|
22
|
+
* validated at startup. `fatal` and `trace` map to pino's own; the ambient
|
|
23
|
+
* unit's ids ride as fields, not as a message prefix, so they stay indexable.
|
|
24
|
+
*/
|
|
25
|
+
declare const pinoSink: (logger: Logger) => Sink;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { pinoSink };
|
package/dist/pino.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/pino.ts
|
|
2
|
+
/**
|
|
3
|
+
* A {@link Sink} over a pino logger โ the subpath a deployment reaches for
|
|
4
|
+
* when the default JSON sink's `JSON.stringify` per line is the thing showing
|
|
5
|
+
* up in a profile. `pino` is an **optional** peer: install it, import
|
|
6
|
+
* `@btravstack/observability/pino`, and pass the sink; a consumer that never
|
|
7
|
+
* imports this file never needs it.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import pino from "pino";
|
|
11
|
+
* import { observability } from "@btravstack/observability";
|
|
12
|
+
* import { pinoSink } from "@btravstack/observability/pino";
|
|
13
|
+
*
|
|
14
|
+
* observability({ sink: pinoSink(pino()) });
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* The level filter stays **ours**: `createLogger` has already decided the
|
|
18
|
+
* line is worth writing by the time a sink sees it, so pino is configured at
|
|
19
|
+
* `trace` here โ one filter in the process, and it is the one `LOG_LEVEL`
|
|
20
|
+
* validated at startup. `fatal` and `trace` map to pino's own; the ambient
|
|
21
|
+
* unit's ids ride as fields, not as a message prefix, so they stay indexable.
|
|
22
|
+
*/
|
|
23
|
+
const pinoSink = (logger) => (line) => {
|
|
24
|
+
const { level, message, attributes, cause, unit } = line;
|
|
25
|
+
const fields = {
|
|
26
|
+
...attributes,
|
|
27
|
+
...unit === void 0 ? {} : unit,
|
|
28
|
+
...cause === void 0 ? {} : { err: cause }
|
|
29
|
+
};
|
|
30
|
+
logger[level](fields, message);
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
export { pinoSink };
|
package/package.json
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@btravstack/observability",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Observability for @btravstack/core: a strict Logger port correlated with the kernel's units, a dependency-free JSON sink, and the lifecycle events as log lines",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dependency-injection",
|
|
7
|
+
"logger",
|
|
8
|
+
"logging",
|
|
9
|
+
"observability",
|
|
10
|
+
"opentelemetry",
|
|
11
|
+
"structured-logging",
|
|
12
|
+
"typescript",
|
|
13
|
+
"unthrown"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/btravstack/btravstack#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/btravstack/btravstack/issues"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Benoit TRAVERS <benoit.travers.fr@gmail.com>",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/btravstack/btravstack.git",
|
|
24
|
+
"directory": "packages/observability"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"main": "./dist/index.cjs",
|
|
32
|
+
"module": "./dist/index.mjs",
|
|
33
|
+
"types": "./dist/index.d.cts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"import": {
|
|
37
|
+
"types": "./dist/index.d.mts",
|
|
38
|
+
"default": "./dist/index.mjs"
|
|
39
|
+
},
|
|
40
|
+
"require": {
|
|
41
|
+
"types": "./dist/index.d.cts",
|
|
42
|
+
"default": "./dist/index.cjs"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"./pino": {
|
|
46
|
+
"import": {
|
|
47
|
+
"types": "./dist/pino.d.mts",
|
|
48
|
+
"default": "./dist/pino.mjs"
|
|
49
|
+
},
|
|
50
|
+
"require": {
|
|
51
|
+
"types": "./dist/pino.d.cts",
|
|
52
|
+
"default": "./dist/pino.cjs"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"./otel": {
|
|
56
|
+
"import": {
|
|
57
|
+
"types": "./dist/otel.d.mts",
|
|
58
|
+
"default": "./dist/otel.mjs"
|
|
59
|
+
},
|
|
60
|
+
"require": {
|
|
61
|
+
"types": "./dist/otel.d.cts",
|
|
62
|
+
"default": "./dist/otel.cjs"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"./package.json": "./package.json"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@btravstack/tsconfig": "0.2.0",
|
|
69
|
+
"@opentelemetry/api": "1.9.1",
|
|
70
|
+
"@opentelemetry/sdk-node": "0.221.0",
|
|
71
|
+
"@opentelemetry/sdk-trace": "2.10.0",
|
|
72
|
+
"@types/node": "26.2.0",
|
|
73
|
+
"@unthrown/vitest": "5.7.0",
|
|
74
|
+
"@vitest/coverage-v8": "4.1.11",
|
|
75
|
+
"pino": "10.3.1",
|
|
76
|
+
"tsdown": "0.22.14",
|
|
77
|
+
"typescript": "7.0.2",
|
|
78
|
+
"unthrown": "5.7.0",
|
|
79
|
+
"vitest": "4.1.11",
|
|
80
|
+
"@btravstack/config": "0.3.0",
|
|
81
|
+
"@btravstack/di": "0.3.0",
|
|
82
|
+
"@btravstack/testing": "0.3.0",
|
|
83
|
+
"@btravstack/core": "0.3.0"
|
|
84
|
+
},
|
|
85
|
+
"peerDependencies": {
|
|
86
|
+
"@opentelemetry/api": "^1.9.0",
|
|
87
|
+
"@opentelemetry/sdk-node": "^0.221.0",
|
|
88
|
+
"pino": "^10",
|
|
89
|
+
"unthrown": "^5.0.0",
|
|
90
|
+
"@btravstack/config": "^0.3.0",
|
|
91
|
+
"@btravstack/core": "^0.3.0",
|
|
92
|
+
"@btravstack/di": "^0.3.0"
|
|
93
|
+
},
|
|
94
|
+
"peerDependenciesMeta": {
|
|
95
|
+
"@opentelemetry/api": {
|
|
96
|
+
"optional": true
|
|
97
|
+
},
|
|
98
|
+
"@opentelemetry/sdk-node": {
|
|
99
|
+
"optional": true
|
|
100
|
+
},
|
|
101
|
+
"pino": {
|
|
102
|
+
"optional": true
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"engines": {
|
|
106
|
+
"node": ">=20"
|
|
107
|
+
},
|
|
108
|
+
"scripts": {
|
|
109
|
+
"build": "tsdown src/index.ts src/pino.ts src/otel.ts --format cjs,esm --dts --clean",
|
|
110
|
+
"dev": "tsdown src/index.ts src/pino.ts --format cjs,esm --dts --watch",
|
|
111
|
+
"test": "vitest run --coverage",
|
|
112
|
+
"typecheck": "tsc --noEmit"
|
|
113
|
+
}
|
|
114
|
+
}
|