@theholocron/logger 3.53.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 +123 -0
- package/dist/index.d.mts +128 -0
- package/dist/index.mjs +235 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Newton Koumantzelis
|
|
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,123 @@
|
|
|
1
|
+
# `@theholocron/logger`
|
|
2
|
+
|
|
3
|
+
Structured logging for the Holocron CLI and its plugins. A `Logger` adapter interface backed by [Pino](https://getpino.io), with `pino-pretty` for local dev, plain JSON for CI, and an [Axiom](https://axiom.co) transport for log aggregation.
|
|
4
|
+
|
|
5
|
+
Call sites depend on the `Logger` interface — never on Pino directly. Swapping the logging library is a one-file change.
|
|
6
|
+
|
|
7
|
+
> `logger` is the operational-output channel (internal state, debug traces, errors, structured context). It is **not** `print` — the user-facing UX output surface — and does not wrap or replace it. The two are parallel concerns.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pnpm add @theholocron/logger
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createLogger } from "@theholocron/logger";
|
|
19
|
+
|
|
20
|
+
const { logger, runId } = createLogger({
|
|
21
|
+
level: "info",
|
|
22
|
+
axiom: process.env.HOLOCRON_AXIOM_TOKEN
|
|
23
|
+
? {
|
|
24
|
+
dataset: process.env.HOLOCRON_AXIOM_DATASET!,
|
|
25
|
+
token: process.env.HOLOCRON_AXIOM_TOKEN,
|
|
26
|
+
}
|
|
27
|
+
: undefined,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const log = logger.child({ command: "sync-github", repo: "theholocron/configs" });
|
|
31
|
+
log.info({ branch: "main" }, "opening PR");
|
|
32
|
+
// → { level, time, runId, env, command, repo, branch, msg: "opening PR" }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Pass child loggers down into modules so every line carries its context automatically:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
export async function runSyncGithub(input: { logger: Logger }) {
|
|
39
|
+
const log = input.logger.child({ module: "sync-github" });
|
|
40
|
+
log.debug("resolving remote");
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## `Logger` interface
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
interface Logger {
|
|
48
|
+
debug(obj: Record<string, unknown>, msg?: string): void;
|
|
49
|
+
debug(msg: string): void;
|
|
50
|
+
info(obj: Record<string, unknown>, msg?: string): void;
|
|
51
|
+
info(msg: string): void;
|
|
52
|
+
warn(obj: Record<string, unknown>, msg?: string): void;
|
|
53
|
+
warn(msg: string): void;
|
|
54
|
+
error(obj: Record<string, unknown>, msg?: string): void;
|
|
55
|
+
error(msg: string): void;
|
|
56
|
+
child(bindings: Record<string, unknown>): Logger;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The overloads mirror Pino: object-first for structured lines, bare string for simple ones.
|
|
61
|
+
|
|
62
|
+
## `createLogger(config?)`
|
|
63
|
+
|
|
64
|
+
Returns `{ logger, runId }`. `runId` is a UUID bound to every line the logger and its children emit — surface it via `print` when `--debug` or `--verbose` is set so a whole run can be pulled back out of Axiom.
|
|
65
|
+
|
|
66
|
+
| `config` field | Type | Notes |
|
|
67
|
+
| --------------- | ---------------------------------------- | ---------------------------------------------------------- |
|
|
68
|
+
| `level` | `"debug" \| "info" \| "warn" \| "error"` | Highest-priority level source. Optional. |
|
|
69
|
+
| `axiom.dataset` | `string` | Axiom dataset name. Supply from env vars only. |
|
|
70
|
+
| `axiom.token` | `string` | Axiom API token. Supply from env vars only — never config. |
|
|
71
|
+
|
|
72
|
+
### Level resolution
|
|
73
|
+
|
|
74
|
+
1. `config.level` — the resolved `--verbose` (`debug`) / `--quiet` (`error`) CLI flag, or `holocron.config` `log.level`, passed by the command layer
|
|
75
|
+
2. `HOLOCRON_LOG_LEVEL` env var
|
|
76
|
+
3. `"info"` (default)
|
|
77
|
+
|
|
78
|
+
An unrecognised value at any tier is ignored and resolution falls through. The resolved level applies to **every** transport — there is no separate Axiom gate.
|
|
79
|
+
|
|
80
|
+
## Transports
|
|
81
|
+
|
|
82
|
+
| Environment | Output |
|
|
83
|
+
| ----------------------- | --------------------------------------------- |
|
|
84
|
+
| Local, TTY | `pino-pretty` — colourised, human-readable |
|
|
85
|
+
| CI (`CI` truthy) | Newline-delimited JSON to stdout |
|
|
86
|
+
| `config.axiom` supplied | Axiom, in a Pino worker thread (non-blocking) |
|
|
87
|
+
|
|
88
|
+
The Axiom transport is added only when `config.axiom` is supplied **and** `HOLOCRON_TELEMETRY` is not `"false"`. When Axiom is the only non-console transport, console JSON is kept alongside it so CI logs stay readable.
|
|
89
|
+
|
|
90
|
+
### Axiom datasets
|
|
91
|
+
|
|
92
|
+
Selected by `HOLOCRON_AXIOM_DATASET` — one env var, one value per environment:
|
|
93
|
+
|
|
94
|
+
| Value | When |
|
|
95
|
+
| ---------------- | ---------------- |
|
|
96
|
+
| `holocron-ci` | CI (org secret) |
|
|
97
|
+
| `holocron-local` | local (optional) |
|
|
98
|
+
|
|
99
|
+
If `HOLOCRON_AXIOM_DATASET` is unset locally, local runs write only to `pino-pretty` — no Axiom connection is attempted.
|
|
100
|
+
|
|
101
|
+
## Redaction
|
|
102
|
+
|
|
103
|
+
Sensitive field paths are stripped in Pino's serialisation layer, before any transport — pino-pretty, CI stdout, and Axiom alike. Redacted values become `[Redacted]`.
|
|
104
|
+
|
|
105
|
+
`token`, `secret`, `password`, `apiKey`, `secrets[*].value`, `headers.authorization`, `headers["x-api-key"]`, and the same keys one level down (`*.token`, `*.secret`, …). The full list is exported as `REDACTED_PATHS`.
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
| Script | Description |
|
|
110
|
+
| -------------------- | ----------------------- |
|
|
111
|
+
| `pnpm build` | Bundle with tsdown |
|
|
112
|
+
| `pnpm test` | Run the vitest suite |
|
|
113
|
+
| `pnpm test:coverage` | Run tests with coverage |
|
|
114
|
+
| `pnpm typecheck` | `tsc --noEmit` |
|
|
115
|
+
| `pnpm lint` | ESLint |
|
|
116
|
+
|
|
117
|
+
## Releases
|
|
118
|
+
|
|
119
|
+
Automated via semantic-release. See [CHANGELOG.md](../../CHANGELOG.md).
|
|
120
|
+
|
|
121
|
+
## Documentation
|
|
122
|
+
|
|
123
|
+
<https://theholocron.github.io/holocron/logging/>
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import "pino";
|
|
2
|
+
//#region src/interface.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The logging surface every Holocron command and plugin depends on.
|
|
5
|
+
*
|
|
6
|
+
* Call sites import `Logger` — never Pino directly. The concrete
|
|
7
|
+
* implementation ({@link PinoLogger}) is hidden behind this interface so the
|
|
8
|
+
* logging library can be swapped without touching a single call site.
|
|
9
|
+
*
|
|
10
|
+
* The overloads mirror Pino's native API: pass a context object first for
|
|
11
|
+
* structured logging (`log.info({ repo }, "sync complete")`) or a bare string
|
|
12
|
+
* for a simple line (`log.info("sync complete")`).
|
|
13
|
+
*/
|
|
14
|
+
interface Logger {
|
|
15
|
+
debug(obj: Record<string, unknown>, msg?: string): void;
|
|
16
|
+
debug(msg: string): void;
|
|
17
|
+
info(obj: Record<string, unknown>, msg?: string): void;
|
|
18
|
+
info(msg: string): void;
|
|
19
|
+
warn(obj: Record<string, unknown>, msg?: string): void;
|
|
20
|
+
warn(msg: string): void;
|
|
21
|
+
error(obj: Record<string, unknown>, msg?: string): void;
|
|
22
|
+
error(msg: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Derive a child logger that inherits every binding from its parent and
|
|
25
|
+
* adds its own. Used to attach per-module context (`module`, `repo`, …)
|
|
26
|
+
* without threading extra fields through every call.
|
|
27
|
+
*/
|
|
28
|
+
child(bindings: Record<string, unknown>): Logger;
|
|
29
|
+
}
|
|
30
|
+
/** Log levels, ordered least to most severe. */
|
|
31
|
+
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
32
|
+
/** The four levels, in ascending severity — useful for validation. */
|
|
33
|
+
declare const LOG_LEVELS: readonly LogLevel[];
|
|
34
|
+
/** Runtime environment a logger was constructed in. */
|
|
35
|
+
type LogEnv = "ci" | "local";
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/transports.d.ts
|
|
38
|
+
/** Axiom credentials — never read from `holocron.config`, only from env vars. */
|
|
39
|
+
interface AxiomTransportConfig {
|
|
40
|
+
dataset: string;
|
|
41
|
+
token: string;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/context.d.ts
|
|
45
|
+
/**
|
|
46
|
+
* A fresh correlation id for one command invocation. Every log line from a
|
|
47
|
+
* single `holocron` run carries this `runId`, so a whole run can be pulled
|
|
48
|
+
* back out of Axiom with one query.
|
|
49
|
+
*/
|
|
50
|
+
declare function generateRunId(): string;
|
|
51
|
+
/**
|
|
52
|
+
* True when running inside CI. GitHub Actions (and most other providers) set
|
|
53
|
+
* `CI=true`; we also accept any other non-empty, non-`false` value.
|
|
54
|
+
*/
|
|
55
|
+
declare function isCI(env?: NodeJS.ProcessEnv): boolean;
|
|
56
|
+
/** `"ci"` inside CI, otherwise `"local"` — bound on every root logger. */
|
|
57
|
+
declare function detectEnv(env?: NodeJS.ProcessEnv): LogEnv;
|
|
58
|
+
/** Narrow an arbitrary string to a {@link LogLevel}, or `undefined`. */
|
|
59
|
+
declare function parseLogLevel(value: string | undefined): LogLevel | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the effective log level in priority order:
|
|
62
|
+
*
|
|
63
|
+
* 1. `explicit` — already-resolved CLI flag (`--verbose` → `debug`,
|
|
64
|
+
* `--quiet` → `error`) or `holocron.config` `log.level`, passed by the
|
|
65
|
+
* command layer.
|
|
66
|
+
* 2. `HOLOCRON_LOG_LEVEL` env var.
|
|
67
|
+
* 3. `"info"` default.
|
|
68
|
+
*
|
|
69
|
+
* An unrecognised value at any tier is ignored and resolution falls through.
|
|
70
|
+
*/
|
|
71
|
+
declare function resolveLevel(explicit?: LogLevel, env?: NodeJS.ProcessEnv): LogLevel;
|
|
72
|
+
/** True when the Axiom transport must be suppressed via `HOLOCRON_TELEMETRY=false`. */
|
|
73
|
+
declare function isTelemetryDisabled(env?: NodeJS.ProcessEnv): boolean;
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/redact.d.ts
|
|
76
|
+
/**
|
|
77
|
+
* Sensitive field paths stripped from every log line before any transport —
|
|
78
|
+
* pino-pretty, CI stdout, and Axiom alike. Redaction happens in Pino's
|
|
79
|
+
* serialisation layer, so a raw token never leaves the process.
|
|
80
|
+
*
|
|
81
|
+
* Paths use Pino's redaction syntax: dotted access, `[*]` wildcards for
|
|
82
|
+
* arrays, and bracket-quoted keys for names that are not valid identifiers.
|
|
83
|
+
*
|
|
84
|
+
* @see https://getpino.io/#/docs/redaction
|
|
85
|
+
*/
|
|
86
|
+
declare const REDACTED_PATHS: readonly string[];
|
|
87
|
+
/** Replacement string Pino writes in place of a redacted value. */
|
|
88
|
+
declare const REDACT_CENSOR = "[Redacted]";
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/index.d.ts
|
|
91
|
+
interface LoggerConfig {
|
|
92
|
+
/**
|
|
93
|
+
* Explicit level — the highest-priority source. The command layer passes
|
|
94
|
+
* the resolved `--verbose` / `--quiet` flag or `holocron.config`
|
|
95
|
+
* `log.level` here. Falls through to `HOLOCRON_LOG_LEVEL`, then `"info"`.
|
|
96
|
+
*/
|
|
97
|
+
level?: LogLevel;
|
|
98
|
+
/**
|
|
99
|
+
* Axiom credentials. Supply only from env vars (`HOLOCRON_AXIOM_TOKEN` /
|
|
100
|
+
* `HOLOCRON_AXIOM_DATASET`) — never from config files. Omit to skip the
|
|
101
|
+
* Axiom transport. Also skipped when `HOLOCRON_TELEMETRY=false`.
|
|
102
|
+
*/
|
|
103
|
+
axiom?: AxiomTransportConfig;
|
|
104
|
+
}
|
|
105
|
+
interface CreateLoggerResult {
|
|
106
|
+
/** The logger to thread through commands and modules via `child()`. */
|
|
107
|
+
logger: Logger;
|
|
108
|
+
/**
|
|
109
|
+
* Correlation id bound to every line this logger (and its children) emit.
|
|
110
|
+
* Surface it to the user via `print` when `--debug` or `--verbose` is set
|
|
111
|
+
* so they can pull the run out of Axiom.
|
|
112
|
+
*/
|
|
113
|
+
runId: string;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The single entry point for building a logger. Generates a `runId`, detects
|
|
117
|
+
* the environment, resolves the level, wires the right transports, and
|
|
118
|
+
* returns a ready {@link Logger}.
|
|
119
|
+
*
|
|
120
|
+
* ```ts
|
|
121
|
+
* const { logger, runId } = createLogger({ level, axiom });
|
|
122
|
+
* const log = logger.child({ command: "sync-github", repo });
|
|
123
|
+
* log.info({ branch }, "opening PR");
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
declare function createLogger(config?: LoggerConfig): CreateLoggerResult;
|
|
127
|
+
//#endregion
|
|
128
|
+
export { type AxiomTransportConfig, CreateLoggerResult, LOG_LEVELS, type LogEnv, type LogLevel, type Logger, LoggerConfig, REDACTED_PATHS, REDACT_CENSOR, createLogger, detectEnv, generateRunId, isCI, isTelemetryDisabled, parseLogLevel, resolveLevel };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import pino from "pino";
|
|
3
|
+
//#region src/interface.ts
|
|
4
|
+
/** The four levels, in ascending severity — useful for validation. */
|
|
5
|
+
const LOG_LEVELS = [
|
|
6
|
+
"debug",
|
|
7
|
+
"info",
|
|
8
|
+
"warn",
|
|
9
|
+
"error"
|
|
10
|
+
];
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/context.ts
|
|
13
|
+
/**
|
|
14
|
+
* A fresh correlation id for one command invocation. Every log line from a
|
|
15
|
+
* single `holocron` run carries this `runId`, so a whole run can be pulled
|
|
16
|
+
* back out of Axiom with one query.
|
|
17
|
+
*/
|
|
18
|
+
function generateRunId() {
|
|
19
|
+
return randomUUID();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* True when running inside CI. GitHub Actions (and most other providers) set
|
|
23
|
+
* `CI=true`; we also accept any other non-empty, non-`false` value.
|
|
24
|
+
*/
|
|
25
|
+
function isCI(env = process.env) {
|
|
26
|
+
const ci = env.CI;
|
|
27
|
+
if (!ci) return false;
|
|
28
|
+
return ci !== "false" && ci !== "0";
|
|
29
|
+
}
|
|
30
|
+
/** `"ci"` inside CI, otherwise `"local"` — bound on every root logger. */
|
|
31
|
+
function detectEnv(env = process.env) {
|
|
32
|
+
return isCI(env) ? "ci" : "local";
|
|
33
|
+
}
|
|
34
|
+
/** Narrow an arbitrary string to a {@link LogLevel}, or `undefined`. */
|
|
35
|
+
function parseLogLevel(value) {
|
|
36
|
+
return value && LOG_LEVELS.includes(value) ? value : void 0;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the effective log level in priority order:
|
|
40
|
+
*
|
|
41
|
+
* 1. `explicit` — already-resolved CLI flag (`--verbose` → `debug`,
|
|
42
|
+
* `--quiet` → `error`) or `holocron.config` `log.level`, passed by the
|
|
43
|
+
* command layer.
|
|
44
|
+
* 2. `HOLOCRON_LOG_LEVEL` env var.
|
|
45
|
+
* 3. `"info"` default.
|
|
46
|
+
*
|
|
47
|
+
* An unrecognised value at any tier is ignored and resolution falls through.
|
|
48
|
+
*/
|
|
49
|
+
function resolveLevel(explicit, env = process.env) {
|
|
50
|
+
return explicit ?? parseLogLevel(env.HOLOCRON_LOG_LEVEL) ?? "info";
|
|
51
|
+
}
|
|
52
|
+
/** True when the Axiom transport must be suppressed via `HOLOCRON_TELEMETRY=false`. */
|
|
53
|
+
function isTelemetryDisabled(env = process.env) {
|
|
54
|
+
return env.HOLOCRON_TELEMETRY === "false";
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/redact.ts
|
|
58
|
+
/**
|
|
59
|
+
* Sensitive field paths stripped from every log line before any transport —
|
|
60
|
+
* pino-pretty, CI stdout, and Axiom alike. Redaction happens in Pino's
|
|
61
|
+
* serialisation layer, so a raw token never leaves the process.
|
|
62
|
+
*
|
|
63
|
+
* Paths use Pino's redaction syntax: dotted access, `[*]` wildcards for
|
|
64
|
+
* arrays, and bracket-quoted keys for names that are not valid identifiers.
|
|
65
|
+
*
|
|
66
|
+
* @see https://getpino.io/#/docs/redaction
|
|
67
|
+
*/
|
|
68
|
+
const REDACTED_PATHS = [
|
|
69
|
+
"token",
|
|
70
|
+
"secret",
|
|
71
|
+
"password",
|
|
72
|
+
"apiKey",
|
|
73
|
+
"secrets[*].value",
|
|
74
|
+
"headers.authorization",
|
|
75
|
+
"headers[\"x-api-key\"]",
|
|
76
|
+
"*.token",
|
|
77
|
+
"*.secret",
|
|
78
|
+
"*.password",
|
|
79
|
+
"*.apiKey"
|
|
80
|
+
];
|
|
81
|
+
/** Replacement string Pino writes in place of a redacted value. */
|
|
82
|
+
const REDACT_CENSOR = "[Redacted]";
|
|
83
|
+
/** Ready-to-use Pino `redact` option. */
|
|
84
|
+
const redactOptions = {
|
|
85
|
+
paths: [...REDACTED_PATHS],
|
|
86
|
+
censor: REDACT_CENSOR
|
|
87
|
+
};
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/transports.ts
|
|
90
|
+
const prettyTarget = (level) => ({
|
|
91
|
+
target: "pino-pretty",
|
|
92
|
+
level,
|
|
93
|
+
options: {
|
|
94
|
+
colorize: true,
|
|
95
|
+
translateTime: "SYS:HH:MM:ss",
|
|
96
|
+
ignore: "pid,hostname"
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const stdoutJsonTarget = (level) => ({
|
|
100
|
+
target: "pino/file",
|
|
101
|
+
level,
|
|
102
|
+
options: { destination: 1 }
|
|
103
|
+
});
|
|
104
|
+
const axiomTarget = (level, axiom) => ({
|
|
105
|
+
target: "@axiomhq/pino",
|
|
106
|
+
level,
|
|
107
|
+
options: {
|
|
108
|
+
dataset: axiom.dataset,
|
|
109
|
+
token: axiom.token
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
/**
|
|
113
|
+
* Build the Pino `transport` option for the current environment.
|
|
114
|
+
*
|
|
115
|
+
* | Environment | Output |
|
|
116
|
+
* | -------------------------- | --------------------------------------------------- |
|
|
117
|
+
* | Local, TTY, no Axiom | `pino-pretty` only |
|
|
118
|
+
* | CI, no Axiom | `undefined` — Pino's default NDJSON to stdout |
|
|
119
|
+
* | Local, TTY, with Axiom | `pino-pretty` + Axiom |
|
|
120
|
+
* | CI, with Axiom | NDJSON to stdout + Axiom |
|
|
121
|
+
*
|
|
122
|
+
* The Axiom transport is dropped entirely when `telemetryDisabled` is set,
|
|
123
|
+
* regardless of whether credentials were supplied. Every transport runs on
|
|
124
|
+
* the same resolved `level` — there is no Axiom-only gate.
|
|
125
|
+
*
|
|
126
|
+
* Returning `undefined` lets `createLogger` fall back to a plain
|
|
127
|
+
* `pino({ ... })` with no worker thread — the cheapest path, and exactly
|
|
128
|
+
* what CI wants.
|
|
129
|
+
*/
|
|
130
|
+
function buildTransport(input) {
|
|
131
|
+
const { level, axiom, ci, tty, telemetryDisabled } = input;
|
|
132
|
+
const pretty = !ci && tty;
|
|
133
|
+
const targets = [];
|
|
134
|
+
if (pretty) targets.push(prettyTarget(level));
|
|
135
|
+
if (axiom && !telemetryDisabled) targets.push(axiomTarget(level, axiom));
|
|
136
|
+
if (targets.length === 0) return void 0;
|
|
137
|
+
if (!pretty) targets.unshift(stdoutJsonTarget(level));
|
|
138
|
+
if (targets.length === 1) return targets[0];
|
|
139
|
+
return { targets };
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/pino.ts
|
|
143
|
+
/**
|
|
144
|
+
* The one concrete {@link Logger}. Nothing outside this package should name
|
|
145
|
+
* it — construct loggers with `createLogger`. It is a thin pass-through to a
|
|
146
|
+
* Pino instance; the interface overloads mirror Pino's own, so each method is
|
|
147
|
+
* a single delegated call.
|
|
148
|
+
*/
|
|
149
|
+
var PinoLogger = class PinoLogger {
|
|
150
|
+
#pino;
|
|
151
|
+
constructor(instance) {
|
|
152
|
+
this.#pino = instance;
|
|
153
|
+
}
|
|
154
|
+
debug(objOrMsg, msg) {
|
|
155
|
+
this.#emit("debug", objOrMsg, msg);
|
|
156
|
+
}
|
|
157
|
+
info(objOrMsg, msg) {
|
|
158
|
+
this.#emit("info", objOrMsg, msg);
|
|
159
|
+
}
|
|
160
|
+
warn(objOrMsg, msg) {
|
|
161
|
+
this.#emit("warn", objOrMsg, msg);
|
|
162
|
+
}
|
|
163
|
+
error(objOrMsg, msg) {
|
|
164
|
+
this.#emit("error", objOrMsg, msg);
|
|
165
|
+
}
|
|
166
|
+
child(bindings) {
|
|
167
|
+
return new PinoLogger(this.#pino.child(bindings));
|
|
168
|
+
}
|
|
169
|
+
#emit(level, objOrMsg, msg) {
|
|
170
|
+
if (typeof objOrMsg === "string") this.#pino[level](objOrMsg);
|
|
171
|
+
else this.#pino[level](objOrMsg, msg);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Assemble the Pino `LoggerOptions`: resolved level, `runId` / `env` on every
|
|
176
|
+
* line, ISO timestamps, sensitive-field redaction, and — unless a capture
|
|
177
|
+
* `destination` is in play — the environment-appropriate transport from
|
|
178
|
+
* {@link buildTransport}.
|
|
179
|
+
*/
|
|
180
|
+
function buildPinoOptions(input) {
|
|
181
|
+
const options = {
|
|
182
|
+
level: input.level,
|
|
183
|
+
base: input.base,
|
|
184
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
185
|
+
redact: {
|
|
186
|
+
paths: [...redactOptions.paths],
|
|
187
|
+
censor: redactOptions.censor
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
if (input.destination) return options;
|
|
191
|
+
const transport = buildTransport(input);
|
|
192
|
+
if (transport) options.transport = transport;
|
|
193
|
+
return options;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Construct the underlying Pino instance. Writes to `destination` when one is
|
|
197
|
+
* supplied (tests), otherwise to the transports {@link buildPinoOptions} wired.
|
|
198
|
+
*/
|
|
199
|
+
function createPinoInstance(input) {
|
|
200
|
+
const options = buildPinoOptions(input);
|
|
201
|
+
return input.destination ? pino(options, input.destination) : pino(options);
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/index.ts
|
|
205
|
+
/**
|
|
206
|
+
* The single entry point for building a logger. Generates a `runId`, detects
|
|
207
|
+
* the environment, resolves the level, wires the right transports, and
|
|
208
|
+
* returns a ready {@link Logger}.
|
|
209
|
+
*
|
|
210
|
+
* ```ts
|
|
211
|
+
* const { logger, runId } = createLogger({ level, axiom });
|
|
212
|
+
* const log = logger.child({ command: "sync-github", repo });
|
|
213
|
+
* log.info({ branch }, "opening PR");
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
function createLogger(config = {}) {
|
|
217
|
+
const runId = generateRunId();
|
|
218
|
+
const env = detectEnv();
|
|
219
|
+
return {
|
|
220
|
+
logger: new PinoLogger(createPinoInstance({
|
|
221
|
+
level: resolveLevel(config.level),
|
|
222
|
+
axiom: config.axiom,
|
|
223
|
+
ci: isCI(),
|
|
224
|
+
tty: Boolean(process.stdout.isTTY),
|
|
225
|
+
telemetryDisabled: isTelemetryDisabled(),
|
|
226
|
+
base: {
|
|
227
|
+
runId,
|
|
228
|
+
env
|
|
229
|
+
}
|
|
230
|
+
})),
|
|
231
|
+
runId
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
export { LOG_LEVELS, REDACTED_PATHS, REDACT_CENSOR, createLogger, detectEnv, generateRunId, isCI, isTelemetryDisabled, parseLogLevel, resolveLevel };
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@theholocron/logger",
|
|
3
|
+
"version": "3.53.0",
|
|
4
|
+
"description": "Structured logging for Holocron — a Pino-backed Logger adapter interface with pino-pretty, CI JSON, and Axiom transports.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"axiom",
|
|
7
|
+
"holocron",
|
|
8
|
+
"logging",
|
|
9
|
+
"logger",
|
|
10
|
+
"pino",
|
|
11
|
+
"structured-logging",
|
|
12
|
+
"theholocron"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/theholocron/holocron/tree/main/packages/logger#readme",
|
|
15
|
+
"bugs": "https://github.com/theholocron/holocron/issues",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/theholocron/holocron.git",
|
|
19
|
+
"directory": "packages/logger"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Newton Koumantzelis",
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.mts",
|
|
28
|
+
"import": "./dist/index.mjs",
|
|
29
|
+
"default": "./dist/index.mjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@axiomhq/pino": "^2.0.0",
|
|
37
|
+
"pino": "^10.3.1",
|
|
38
|
+
"pino-pretty": "^13.1.3"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@theholocron/eslint-config": "^7.32.1",
|
|
42
|
+
"@theholocron/tsconfig": "^7.32.1",
|
|
43
|
+
"@theholocron/tsdown-config": "^7.32.1",
|
|
44
|
+
"@theholocron/vitest-config": "^7.32.1",
|
|
45
|
+
"@types/node": "^26",
|
|
46
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
47
|
+
"@vitest/eslint-plugin": "^1.6.27",
|
|
48
|
+
"eslint": "^10.8.1",
|
|
49
|
+
"eslint-plugin-n": "^18.3.0",
|
|
50
|
+
"globals": "^17.11.0",
|
|
51
|
+
"tsdown": "^0.22.14",
|
|
52
|
+
"typescript": "^5.9.3",
|
|
53
|
+
"vitest": "^4.1.11"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=22"
|
|
57
|
+
},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "tsdown",
|
|
63
|
+
"lint": "eslint .",
|
|
64
|
+
"typecheck": "tsc --noEmit",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"test:watch": "vitest",
|
|
67
|
+
"test:coverage": "vitest run --coverage"
|
|
68
|
+
}
|
|
69
|
+
}
|