@svayam-opensource/svm-util-log 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/lib/cjs/config.js +9 -0
- package/lib/cjs/format.js +114 -0
- package/lib/cjs/index.js +39 -0
- package/lib/cjs/levels.js +39 -0
- package/lib/cjs/logger.js +112 -0
- package/lib/esm/config.d.ts +74 -0
- package/lib/esm/config.js +6 -0
- package/lib/esm/format.d.ts +39 -0
- package/lib/esm/format.js +104 -0
- package/lib/esm/index.d.ts +19 -0
- package/lib/esm/index.js +22 -0
- package/lib/esm/levels.d.ts +69 -0
- package/lib/esm/levels.js +35 -0
- package/lib/esm/logger.d.ts +22 -0
- package/lib/esm/logger.js +105 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# @svayam-opensource/svm-util-log
|
|
2
|
+
|
|
3
|
+
Svayam's shared logging framework — the logger from `svm-util`, carrying only the
|
|
4
|
+
dependencies a logger needs.
|
|
5
|
+
|
|
6
|
+
**Mandated by POL-423.** All Svayam-owned Node code uses this framework and leaves messages
|
|
7
|
+
at the various levels, so a run can be reviewed afterwards for issue and problem resolution.
|
|
8
|
+
|
|
9
|
+
## Why this package exists
|
|
10
|
+
|
|
11
|
+
`@svayam/svm-util` is one package: the logger, the helpers, database access and the API
|
|
12
|
+
server. Taking it whole installs **675 packages, 274 MB** — right for an API service, and the
|
|
13
|
+
whole cost for a command-line tool that wants one log line. `@svayam-opensource/gov` is
|
|
14
|
+
installed with `npm i -g` on adopters' laptops.
|
|
15
|
+
|
|
16
|
+
This package is **25 packages, 8.4 MB**.
|
|
17
|
+
|
|
18
|
+
It is also on the **public** scope, because `@svayam/svm-util` publishes to the internal
|
|
19
|
+
registry, which adopters of a public CLI cannot resolve. A logging mandate that makes the
|
|
20
|
+
governance CLI uninstallable is a mandate nothing can obey.
|
|
21
|
+
|
|
22
|
+
First package of the split — see [911#224](https://github.com/Svayamtech/911-SVM-LIB-SVC/issues/224).
|
|
23
|
+
|
|
24
|
+
## Use
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { createApplicationLogger, LogLevel } from "@svayam-opensource/svm-util-log";
|
|
28
|
+
|
|
29
|
+
const logger = createApplicationLogger({
|
|
30
|
+
appId: "gov-work",
|
|
31
|
+
logDir: "~/.gov/svm/logs",
|
|
32
|
+
methods: [
|
|
33
|
+
{ METHOD: "console", LOGLEVEL: LogLevel.warn },
|
|
34
|
+
{ METHOD: "file", LOGLEVEL: LogLevel.debug },
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
logger.log(LogLevel.debug, "chose the api-key route", "gov-work:cli:sign-in-choice", "captureAgentKey");
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### `pgm_cd` is the code path
|
|
42
|
+
|
|
43
|
+
`<package>:<dir>:<module>` — `gov-work:cli:sign-in-choice` is `src/cli/sign-in-choice.ts`.
|
|
44
|
+
The point is that a reader can get from a line back to the code without guessing (POL-426).
|
|
45
|
+
|
|
46
|
+
### Turning it on in the field
|
|
47
|
+
|
|
48
|
+
Errors and warnings always come through. Everything below them is opt-in:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
NODE_DEBUG=gov-work:cli:sign-in-choice gov work # one module
|
|
52
|
+
NODE_DEBUG=gov-work:* gov work # the package
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That is POL-425: **the person who needs the log is usually not the person who can reproduce
|
|
56
|
+
the fault.** They turn it on, reproduce, and send the file.
|
|
57
|
+
|
|
58
|
+
### Secrets
|
|
59
|
+
|
|
60
|
+
POL-427 is **C01**: no credential, key, token or PII reaches a log, at any level, in any
|
|
61
|
+
transport. `meta` is masked before rendering — by field name, case-insensitively, matching a
|
|
62
|
+
name that *contains* a masked word (`userPassword`, `db_pwd`, `authToken`). Extend
|
|
63
|
+
`DEFAULT_MASK`, never replace it.
|
|
64
|
+
|
|
65
|
+
## Transports
|
|
66
|
+
|
|
67
|
+
`console` and `file` (daily-rotate) ship here. `database` and `api` belong to the packages
|
|
68
|
+
carrying `mysql` and `axios`, and register themselves:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
registerTransport("database", (conf, config) => new MyDbTransport(conf, config));
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
A method declared in config with nothing registered for it is **ignored, not an error** — a
|
|
75
|
+
line that does not go somewhere, never a crash at startup.
|
|
76
|
+
|
|
77
|
+
## Two deliberate departures from `svm-util`
|
|
78
|
+
|
|
79
|
+
1. **Configuration is injected, never global.** `svm-util` reaches a module-level
|
|
80
|
+
`getSvayamConfiguration()`; a CLI has a first run where no configuration exists at all.
|
|
81
|
+
2. **Console output goes to STDERR.** `stdout` is a program's answer, and a caller piping it
|
|
82
|
+
should not receive the log as data.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_MASK = void 0;
|
|
4
|
+
/** What a `mask` covers before a consumer adds to it. Extend, do not replace. */
|
|
5
|
+
exports.DEFAULT_MASK = [
|
|
6
|
+
"password", "passwd", "pwd", "secret", "token", "apiKey", "api_key",
|
|
7
|
+
"authorization", "auth", "credential", "credentials", "privateKey", "private_key",
|
|
8
|
+
"sessionId", "session_id", "cookie", "jwt", "bearer",
|
|
9
|
+
];
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isLogEnabled = isLogEnabled;
|
|
7
|
+
exports.maskValues = maskValues;
|
|
8
|
+
exports.stringify = stringify;
|
|
9
|
+
exports.toLogEntry = toLogEntry;
|
|
10
|
+
exports.formatLogMessage = formatLogMessage;
|
|
11
|
+
/* -------------------------------------------------
|
|
12
|
+
* Copyright (c) 2026 - Svayam Infoware Pvt. Ltd.
|
|
13
|
+
* All rights reserved. This code may not be copied
|
|
14
|
+
* or duplicated in whole or part by any means
|
|
15
|
+
* without express prior agreement in writing.
|
|
16
|
+
* -----------------------------------------------*/
|
|
17
|
+
/**
|
|
18
|
+
* Turning a call into a line — masking, provenance, and the one-line layout.
|
|
19
|
+
*
|
|
20
|
+
* Extracted from `svm-util/src/svm-helper.ts` (`stringify`, `getLogEntry`, `formatLogMessage`,
|
|
21
|
+
* `isLogEnabled`), with the global configuration lookup removed: every function here takes
|
|
22
|
+
* what it needs. See `config.ts` for why.
|
|
23
|
+
*/
|
|
24
|
+
const node_util_1 = __importDefault(require("node:util"));
|
|
25
|
+
const config_js_1 = require("./config.js");
|
|
26
|
+
const COMPACT = {
|
|
27
|
+
depth: 4, compact: true, breakLength: Infinity, maxStringLength: 512, colors: false,
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Is logging switched on for this origin?
|
|
31
|
+
*
|
|
32
|
+
* `node:util.debuglog` is the switch, so `NODE_DEBUG=gov-work:cli:agent-install` enables one
|
|
33
|
+
* module and `NODE_DEBUG=gov-work:*` the package — POL-425's "switchable at run time by
|
|
34
|
+
* whoever is in front of the problem, with no rebuild and no release".
|
|
35
|
+
*
|
|
36
|
+
* NOT CACHED PER CALL BY ACCIDENT: `debuglog` itself is lazy and memoised, so asking it every
|
|
37
|
+
* time is cheap, and asking it every time is what lets a long-running process be told the
|
|
38
|
+
* truth if the environment is re-read.
|
|
39
|
+
*/
|
|
40
|
+
function isLogEnabled(pgm_cd, fn_cd, act_id) {
|
|
41
|
+
const on = (s) => (s ? node_util_1.default.debuglog(s).enabled : false);
|
|
42
|
+
return on(pgm_cd) || on(fn_cd) || on(act_id?.toString());
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Replace the value of any masked field, at any depth, before it can be rendered.
|
|
46
|
+
*
|
|
47
|
+
* POL-427 (C01). The comparison is case-insensitive and matches a field whose name CONTAINS a
|
|
48
|
+
* masked word, because the field that leaks is rarely named exactly `password` — it is
|
|
49
|
+
* `userPassword`, `db_pwd`, `authToken`. Over-masking costs a reader one lookup; under-masking
|
|
50
|
+
* costs a credential.
|
|
51
|
+
*/
|
|
52
|
+
function maskValues(value, mask = config_js_1.DEFAULT_MASK) {
|
|
53
|
+
const hidden = (key) => {
|
|
54
|
+
const k = key.toLowerCase();
|
|
55
|
+
return mask.some((m) => k.includes(m.toLowerCase()));
|
|
56
|
+
};
|
|
57
|
+
const walk = (v, depth) => {
|
|
58
|
+
if (depth > 8 || v === null || typeof v !== "object")
|
|
59
|
+
return v;
|
|
60
|
+
if (Array.isArray(v))
|
|
61
|
+
return v.map((x) => walk(x, depth + 1));
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const [k, val] of Object.entries(v)) {
|
|
64
|
+
out[k] = hidden(k) ? "***" : walk(val, depth + 1);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
};
|
|
68
|
+
return walk(value, 0);
|
|
69
|
+
}
|
|
70
|
+
/** Render anything for a log line: masked, depth-limited, and never throwing. */
|
|
71
|
+
function stringify(obj, config) {
|
|
72
|
+
if (obj === null)
|
|
73
|
+
return "null";
|
|
74
|
+
if (obj === undefined)
|
|
75
|
+
return "undefined";
|
|
76
|
+
if (typeof obj !== "object")
|
|
77
|
+
return String(obj);
|
|
78
|
+
try {
|
|
79
|
+
const masked = maskValues(obj, config?.mask ?? config_js_1.DEFAULT_MASK);
|
|
80
|
+
return node_util_1.default.inspect(masked, { ...COMPACT, ...(config?.inspect ?? {}) });
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
// A logger that throws while rendering takes the program down at the moment it was trying
|
|
84
|
+
// to explain itself. Say what went wrong with the value and keep the line.
|
|
85
|
+
return `<unrenderable: ${e?.message ?? "unknown"}>`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Give a call its provenance. Padding keeps a wall of lines readable in columns. */
|
|
89
|
+
function toLogEntry(appId, lvl, msg, pgm_cd, fn_cd, act_id, meta) {
|
|
90
|
+
return {
|
|
91
|
+
level: lvl,
|
|
92
|
+
message: msg,
|
|
93
|
+
app_cd: appId,
|
|
94
|
+
pgm_cd: pgm_cd ?? "n/a",
|
|
95
|
+
fn_cd: fn_cd ?? "n/a",
|
|
96
|
+
act_id: act_id ?? 0,
|
|
97
|
+
meta,
|
|
98
|
+
logTime: new Date().toISOString(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The single line a transport writes.
|
|
103
|
+
*
|
|
104
|
+
* The origin comes FIRST and at a fixed width, because the common act is scanning a column
|
|
105
|
+
* for one module rather than reading prose — which is what a log is for when it is being read
|
|
106
|
+
* at all.
|
|
107
|
+
*/
|
|
108
|
+
function formatLogMessage(e, config) {
|
|
109
|
+
const pgm = (e.pgm_cd ?? "n/a").padEnd(34);
|
|
110
|
+
const fn = (e.fn_cd ?? "n/a").padEnd(22);
|
|
111
|
+
const act = String(e.act_id ?? 0).padEnd(4);
|
|
112
|
+
const meta = e.meta === undefined ? "" : ` meta=${stringify(e.meta, config)}`;
|
|
113
|
+
return `${e.logTime ?? ""} ${e.level.padEnd(7)} ${pgm} ${fn} ${act} ${e.message}${meta}`;
|
|
114
|
+
}
|
package/lib/cjs/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerTransport = exports.createApplicationLogger = exports.toLogEntry = exports.stringify = exports.maskValues = exports.isLogEnabled = exports.formatLogMessage = exports.DEFAULT_MASK = exports.withinLevel = exports.logLevelIndex = exports.LogMethod = exports.LogLevelIndex = exports.LogLevelColors = exports.LogLevel = void 0;
|
|
4
|
+
/* -------------------------------------------------
|
|
5
|
+
* Copyright (c) 2026 - Svayam Infoware Pvt. Ltd.
|
|
6
|
+
* All rights reserved. This code may not be copied
|
|
7
|
+
* or duplicated in whole or part by any means
|
|
8
|
+
* without express prior agreement in writing.
|
|
9
|
+
* -----------------------------------------------*/
|
|
10
|
+
/**
|
|
11
|
+
* `@svayam-opensource/svm-util-log` — the shared logging framework mandated by POL-423,
|
|
12
|
+
* carrying only the dependencies a logger needs.
|
|
13
|
+
*
|
|
14
|
+
* The first package of the `svm-util` split (911#224). It exists because the logger and the
|
|
15
|
+
* API server were one package, so a command-line tool could not obey POL-423 without shipping
|
|
16
|
+
* express, mysql, axios and ajv to every adopter — 675 packages for a log line.
|
|
17
|
+
*
|
|
18
|
+
* PUBLIC SCOPE, deliberately. `@svayam/svm-util` publishes to the internal registry, which
|
|
19
|
+
* adopters of `@svayam-opensource/gov` cannot resolve. A logging mandate that makes the
|
|
20
|
+
* governance CLI uninstallable is a mandate nothing can obey.
|
|
21
|
+
*/
|
|
22
|
+
var levels_js_1 = require("./levels.js");
|
|
23
|
+
Object.defineProperty(exports, "LogLevel", { enumerable: true, get: function () { return levels_js_1.LogLevel; } });
|
|
24
|
+
Object.defineProperty(exports, "LogLevelColors", { enumerable: true, get: function () { return levels_js_1.LogLevelColors; } });
|
|
25
|
+
Object.defineProperty(exports, "LogLevelIndex", { enumerable: true, get: function () { return levels_js_1.LogLevelIndex; } });
|
|
26
|
+
Object.defineProperty(exports, "LogMethod", { enumerable: true, get: function () { return levels_js_1.LogMethod; } });
|
|
27
|
+
Object.defineProperty(exports, "logLevelIndex", { enumerable: true, get: function () { return levels_js_1.logLevelIndex; } });
|
|
28
|
+
Object.defineProperty(exports, "withinLevel", { enumerable: true, get: function () { return levels_js_1.withinLevel; } });
|
|
29
|
+
var config_js_1 = require("./config.js");
|
|
30
|
+
Object.defineProperty(exports, "DEFAULT_MASK", { enumerable: true, get: function () { return config_js_1.DEFAULT_MASK; } });
|
|
31
|
+
var format_js_1 = require("./format.js");
|
|
32
|
+
Object.defineProperty(exports, "formatLogMessage", { enumerable: true, get: function () { return format_js_1.formatLogMessage; } });
|
|
33
|
+
Object.defineProperty(exports, "isLogEnabled", { enumerable: true, get: function () { return format_js_1.isLogEnabled; } });
|
|
34
|
+
Object.defineProperty(exports, "maskValues", { enumerable: true, get: function () { return format_js_1.maskValues; } });
|
|
35
|
+
Object.defineProperty(exports, "stringify", { enumerable: true, get: function () { return format_js_1.stringify; } });
|
|
36
|
+
Object.defineProperty(exports, "toLogEntry", { enumerable: true, get: function () { return format_js_1.toLogEntry; } });
|
|
37
|
+
var logger_js_1 = require("./logger.js");
|
|
38
|
+
Object.defineProperty(exports, "createApplicationLogger", { enumerable: true, get: function () { return logger_js_1.createApplicationLogger; } });
|
|
39
|
+
Object.defineProperty(exports, "registerTransport", { enumerable: true, get: function () { return logger_js_1.registerTransport; } });
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* -------------------------------------------------
|
|
3
|
+
* Copyright (c) 2026 - Svayam Infoware Pvt. Ltd.
|
|
4
|
+
* All rights reserved. This code may not be copied
|
|
5
|
+
* or duplicated in whole or part by any means
|
|
6
|
+
* without express prior agreement in writing.
|
|
7
|
+
* -----------------------------------------------*/
|
|
8
|
+
/**
|
|
9
|
+
* Levels, and the shape of a log call — extracted from `svm-util/src/svm-util-base.ts`
|
|
10
|
+
* unchanged, so a line written against either library means the same thing.
|
|
11
|
+
*
|
|
12
|
+
* DELIBERATELY DEPENDENCY-FREE. In `svm-util` these live beside `PoolConfig` from mysql,
|
|
13
|
+
* Axios request types, Ajv and Express, in one shared types module — so importing the level
|
|
14
|
+
* enum pulled a web-server stack. That single file is why a CLI could not take the logger
|
|
15
|
+
* (911#224), and separating it is most of this package's reason to exist.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.LogLevelColors = exports.LogLevelIndex = exports.logLevelIndex = exports.LogLevel = exports.LogMethod = void 0;
|
|
19
|
+
exports.withinLevel = withinLevel;
|
|
20
|
+
/** Where a log line can be sent. `database` and `api` are registered by other packages. */
|
|
21
|
+
exports.LogMethod = { console: "console", file: "file", database: "database", api: "api" };
|
|
22
|
+
exports.LogLevel = {
|
|
23
|
+
error: "error", warn: "warn", info: "info", http: "http",
|
|
24
|
+
verbose: "verbose", debug: "debug", silly: "silly",
|
|
25
|
+
};
|
|
26
|
+
/** Ordered least to most verbose; the index IS the threshold comparison. */
|
|
27
|
+
exports.logLevelIndex = [
|
|
28
|
+
exports.LogLevel.error, exports.LogLevel.warn, exports.LogLevel.info, exports.LogLevel.http,
|
|
29
|
+
exports.LogLevel.verbose, exports.LogLevel.debug, exports.LogLevel.silly,
|
|
30
|
+
];
|
|
31
|
+
exports.LogLevelIndex = { error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6 };
|
|
32
|
+
exports.LogLevelColors = {
|
|
33
|
+
error: "red", warn: "magenta", info: "green", http: "blue",
|
|
34
|
+
verbose: "cyan", debug: "yellow", silly: "gray",
|
|
35
|
+
};
|
|
36
|
+
/** Is `lvl` within the threshold `at`? Both are level names, not numbers, at the call site. */
|
|
37
|
+
function withinLevel(lvl, at) {
|
|
38
|
+
return exports.LogLevelIndex[lvl] <= exports.LogLevelIndex[at];
|
|
39
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerTransport = registerTransport;
|
|
7
|
+
exports.createApplicationLogger = createApplicationLogger;
|
|
8
|
+
/* -------------------------------------------------
|
|
9
|
+
* Copyright (c) 2026 - Svayam Infoware Pvt. Ltd.
|
|
10
|
+
* All rights reserved. This code may not be copied
|
|
11
|
+
* or duplicated in whole or part by any means
|
|
12
|
+
* without express prior agreement in writing.
|
|
13
|
+
* -----------------------------------------------*/
|
|
14
|
+
/**
|
|
15
|
+
* The application logger — `svm-util`'s, with the console and file transports and nothing else.
|
|
16
|
+
*
|
|
17
|
+
* TRANSPORTS ARE PLUGGABLE, and that is the change that makes this package possible.
|
|
18
|
+
* `svm-util/src/svm-logger.ts` builds every transport itself, so it imports `svm-db.js` for the
|
|
19
|
+
* database one and drags `mysql` into anything that wants to log. Here a transport for a method
|
|
20
|
+
* this package does not implement is looked up in a registry, and the packages that own those
|
|
21
|
+
* dependencies register into it (911#224).
|
|
22
|
+
*
|
|
23
|
+
* `exitOnError: false`, as in the original: a library must never take the host process down
|
|
24
|
+
* because a transport failed. That is doubly true here — the transport most likely to fail is
|
|
25
|
+
* the one writing the file you were going to read afterwards.
|
|
26
|
+
*/
|
|
27
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
28
|
+
const winston_1 = require("winston");
|
|
29
|
+
const winston_daily_rotate_file_1 = __importDefault(require("winston-daily-rotate-file"));
|
|
30
|
+
const config_js_1 = require("./config.js");
|
|
31
|
+
const levels_js_1 = require("./levels.js");
|
|
32
|
+
const format_js_1 = require("./format.js");
|
|
33
|
+
const registry = new Map();
|
|
34
|
+
function registerTransport(method, make) {
|
|
35
|
+
registry.set(method, make);
|
|
36
|
+
}
|
|
37
|
+
function buildTransports(config) {
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const conf of config.methods) {
|
|
40
|
+
if (conf.METHOD === "console") {
|
|
41
|
+
out.push(new winston_1.transports.Console({
|
|
42
|
+
level: conf.LOGLEVEL || levels_js_1.LogLevel.info,
|
|
43
|
+
handleExceptions: true,
|
|
44
|
+
format: winston_1.format.combine(winston_1.format.colorize({ all: true }), winston_1.format.printf((info) => (0, format_js_1.formatLogMessage)(info, config))),
|
|
45
|
+
// Diagnostics belong on STDERR. stdout is a program's ANSWER, and a caller piping it
|
|
46
|
+
// into another command should not receive the log as data.
|
|
47
|
+
stderrLevels: Object.values(levels_js_1.LogLevel),
|
|
48
|
+
}));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (conf.METHOD === "file") {
|
|
52
|
+
const f = conf;
|
|
53
|
+
const dir = f.DATA_STORE?.dirname ?? config.logDir;
|
|
54
|
+
out.push(new winston_daily_rotate_file_1.default({
|
|
55
|
+
level: f.LOGLEVEL || levels_js_1.LogLevel.info,
|
|
56
|
+
dirname: dir,
|
|
57
|
+
filename: node_path_1.default.join(String(f.DATA_STORE?.filename ?? ""), `%DATE%-${config.appId}.log`),
|
|
58
|
+
datePattern: f.DATA_STORE?.datePattern ?? "YYYY-MM-DD",
|
|
59
|
+
zippedArchive: f.DATA_STORE?.zippedArchive ?? true,
|
|
60
|
+
maxSize: f.DATA_STORE?.maxSize ?? "20m",
|
|
61
|
+
// A log kept forever is a disclosure risk that grows on its own; a fortnight is enough
|
|
62
|
+
// to diagnose something an adopter noticed today.
|
|
63
|
+
maxFiles: f.DATA_STORE?.maxFiles ?? "14d",
|
|
64
|
+
format: winston_1.format.combine(winston_1.format.printf((info) => (0, format_js_1.formatLogMessage)(info, config))),
|
|
65
|
+
}));
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const make = registry.get(conf.METHOD);
|
|
69
|
+
if (make)
|
|
70
|
+
out.push(make(conf, config));
|
|
71
|
+
// else: declared but unavailable. Silence here is correct — see registerTransport.
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function createApplicationLogger(config) {
|
|
76
|
+
(0, winston_1.addColors)(levels_js_1.LogLevelColors);
|
|
77
|
+
const built = buildTransports(config);
|
|
78
|
+
const winston = (0, winston_1.createLogger)({
|
|
79
|
+
levels: levels_js_1.LogLevelIndex,
|
|
80
|
+
transports: built,
|
|
81
|
+
exitOnError: false,
|
|
82
|
+
});
|
|
83
|
+
const effective = { mask: config_js_1.DEFAULT_MASK, ...config };
|
|
84
|
+
const log = (lvl, msg, pgm_cd, fn_cd, act_id, meta) => {
|
|
85
|
+
// ERRORS AND WARNINGS ALWAYS PASS. Everything below them is opt-in through NODE_DEBUG, so
|
|
86
|
+
// an ordinary run is quiet — but a run that went wrong still said so without anybody
|
|
87
|
+
// having had the foresight to enable anything.
|
|
88
|
+
const always = levels_js_1.LogLevelIndex[lvl] <= levels_js_1.LogLevelIndex[levels_js_1.LogLevel.warn];
|
|
89
|
+
if (!always && !(0, format_js_1.isLogEnabled)(pgm_cd, fn_cd, act_id))
|
|
90
|
+
return;
|
|
91
|
+
winston.log((0, format_js_1.toLogEntry)(effective.appId, lvl, msg, pgm_cd, fn_cd, act_id, meta));
|
|
92
|
+
};
|
|
93
|
+
const toLogObject = () => ({
|
|
94
|
+
APP_ID: effective.appId,
|
|
95
|
+
LOG_DIR: effective.logDir,
|
|
96
|
+
REQUESTED_METHODS: effective.methods.map((m) => m.METHOD),
|
|
97
|
+
ENABLED_TRANSPORTS: winston.transports.map((t) => ({ type: t.constructor.name, level: t.level })),
|
|
98
|
+
MASK: effective.mask,
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
log,
|
|
102
|
+
toLogObject,
|
|
103
|
+
toString: () => JSON.stringify(toLogObject()),
|
|
104
|
+
// A CLI exits in seconds and a rotating file transport writes asynchronously; without a
|
|
105
|
+
// close the last lines of a short run — the ones describing why it ended — are the ones
|
|
106
|
+
// most likely to be lost.
|
|
107
|
+
close: () => { try {
|
|
108
|
+
winston.close();
|
|
109
|
+
}
|
|
110
|
+
catch { /* closing a closed logger is not news */ } },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a consumer declares, and what it injects.
|
|
3
|
+
*
|
|
4
|
+
* TWO DEPARTURES FROM `svm-util`, both deliberate.
|
|
5
|
+
*
|
|
6
|
+
* 1. CONFIG IS INJECTED, NEVER GLOBAL. `svm-util`'s `formatLogMessage` reaches a module-level
|
|
7
|
+
* `getSvayamConfiguration()`, which assumes an application initialised its configuration
|
|
8
|
+
* before anything logged. A long-lived API server can promise that. A command-line tool
|
|
9
|
+
* cannot: `gov` has a first run where no configuration exists at all, several entry points,
|
|
10
|
+
* and a lifetime measured in seconds. A library that requires a global to be set up first
|
|
11
|
+
* is a library that throws during the very failure you are trying to log.
|
|
12
|
+
*
|
|
13
|
+
* 2. ONLY console AND file ARE DECLARED HERE. The database and api transports belong to the
|
|
14
|
+
* packages that carry their dependencies, and register themselves (911#224). Declaring
|
|
15
|
+
* them here would put `mysql` and `axios` back in the type graph, which is the thing this
|
|
16
|
+
* package exists to remove.
|
|
17
|
+
*/
|
|
18
|
+
import type { logLevelType, logMethodType } from "./levels.js";
|
|
19
|
+
/** Where a rotating file transport writes. Winston's own options, narrowed to what is used. */
|
|
20
|
+
export interface FileLogOptions {
|
|
21
|
+
/** directory; defaults to the application's log directory. */
|
|
22
|
+
dirname?: string;
|
|
23
|
+
/** basename; the date and the app id are added around it. */
|
|
24
|
+
filename?: string;
|
|
25
|
+
datePattern?: string;
|
|
26
|
+
zippedArchive?: boolean;
|
|
27
|
+
maxSize?: string;
|
|
28
|
+
maxFiles?: string;
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
export type CONSOLE_LOG_CONFIG_METHOD = {
|
|
32
|
+
METHOD: "console";
|
|
33
|
+
LOGLEVEL: logLevelType;
|
|
34
|
+
};
|
|
35
|
+
export interface FILE_LOG_CONFIG_METHOD {
|
|
36
|
+
METHOD: "file";
|
|
37
|
+
LOGLEVEL: logLevelType;
|
|
38
|
+
DATA_STORE?: FileLogOptions;
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A method this package does not implement — `database`, `api`, or anything a future package
|
|
43
|
+
* adds. Carried through the config so a consumer's file does not have to be rewritten when it
|
|
44
|
+
* moves between environments, and IGNORED unless something registered a transport for it.
|
|
45
|
+
*/
|
|
46
|
+
export interface EXTERNAL_LOG_CONFIG_METHOD {
|
|
47
|
+
METHOD: Exclude<logMethodType, "console" | "file">;
|
|
48
|
+
LOGLEVEL: logLevelType;
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
}
|
|
51
|
+
export type LOG_CONFIG_METHOD = CONSOLE_LOG_CONFIG_METHOD | FILE_LOG_CONFIG_METHOD | EXTERNAL_LOG_CONFIG_METHOD;
|
|
52
|
+
/** Everything the logger needs, handed to it rather than found by it. */
|
|
53
|
+
export interface LoggerConfig {
|
|
54
|
+
/** Names the application in every line and in the log filename. */
|
|
55
|
+
readonly appId: string;
|
|
56
|
+
/** Where file transports write when they do not say otherwise. */
|
|
57
|
+
readonly logDir: string;
|
|
58
|
+
readonly methods: readonly LOG_CONFIG_METHOD[];
|
|
59
|
+
/**
|
|
60
|
+
* Field names to mask wherever they appear in `meta`.
|
|
61
|
+
*
|
|
62
|
+
* POL-427 is C01: no credential, key, token or PII reaches a log, at any level, in any
|
|
63
|
+
* transport. Masking belongs here rather than in a call site's memory — a log is a file, it
|
|
64
|
+
* is copied, and it is sent to other people precisely when something has gone wrong.
|
|
65
|
+
*/
|
|
66
|
+
readonly mask?: readonly string[];
|
|
67
|
+
/** Depth and length limits for `meta`, so one object cannot become the whole file. */
|
|
68
|
+
readonly inspect?: {
|
|
69
|
+
depth?: number;
|
|
70
|
+
maxStringLength?: number;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** What a `mask` covers before a consumer adds to it. Extend, do not replace. */
|
|
74
|
+
export declare const DEFAULT_MASK: readonly string[];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** What a `mask` covers before a consumer adds to it. Extend, do not replace. */
|
|
2
|
+
export const DEFAULT_MASK = [
|
|
3
|
+
"password", "passwd", "pwd", "secret", "token", "apiKey", "api_key",
|
|
4
|
+
"authorization", "auth", "credential", "credentials", "privateKey", "private_key",
|
|
5
|
+
"sessionId", "session_id", "cookie", "jwt", "bearer",
|
|
6
|
+
];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LoggerConfig } from "./config.js";
|
|
2
|
+
import type { LogEntryFields, logLevelType } from "./levels.js";
|
|
3
|
+
/**
|
|
4
|
+
* Is logging switched on for this origin?
|
|
5
|
+
*
|
|
6
|
+
* `node:util.debuglog` is the switch, so `NODE_DEBUG=gov-work:cli:agent-install` enables one
|
|
7
|
+
* module and `NODE_DEBUG=gov-work:*` the package — POL-425's "switchable at run time by
|
|
8
|
+
* whoever is in front of the problem, with no rebuild and no release".
|
|
9
|
+
*
|
|
10
|
+
* NOT CACHED PER CALL BY ACCIDENT: `debuglog` itself is lazy and memoised, so asking it every
|
|
11
|
+
* time is cheap, and asking it every time is what lets a long-running process be told the
|
|
12
|
+
* truth if the environment is re-read.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isLogEnabled(pgm_cd?: string, fn_cd?: string, act_id?: number): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Replace the value of any masked field, at any depth, before it can be rendered.
|
|
17
|
+
*
|
|
18
|
+
* POL-427 (C01). The comparison is case-insensitive and matches a field whose name CONTAINS a
|
|
19
|
+
* masked word, because the field that leaks is rarely named exactly `password` — it is
|
|
20
|
+
* `userPassword`, `db_pwd`, `authToken`. Over-masking costs a reader one lookup; under-masking
|
|
21
|
+
* costs a credential.
|
|
22
|
+
*/
|
|
23
|
+
export declare function maskValues(value: unknown, mask?: readonly string[]): unknown;
|
|
24
|
+
/** Render anything for a log line: masked, depth-limited, and never throwing. */
|
|
25
|
+
export declare function stringify(obj: unknown, config?: Pick<LoggerConfig, "mask" | "inspect">): string;
|
|
26
|
+
/** Give a call its provenance. Padding keeps a wall of lines readable in columns. */
|
|
27
|
+
export declare function toLogEntry(appId: string, lvl: logLevelType, msg: string, pgm_cd?: string, fn_cd?: string, act_id?: number, meta?: unknown): LogEntryFields & {
|
|
28
|
+
logTime: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The single line a transport writes.
|
|
32
|
+
*
|
|
33
|
+
* The origin comes FIRST and at a fixed width, because the common act is scanning a column
|
|
34
|
+
* for one module rather than reading prose — which is what a log is for when it is being read
|
|
35
|
+
* at all.
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatLogMessage(e: LogEntryFields & {
|
|
38
|
+
logTime?: string;
|
|
39
|
+
}, config?: Pick<LoggerConfig, "mask" | "inspect">): string;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/* -------------------------------------------------
|
|
2
|
+
* Copyright (c) 2026 - Svayam Infoware Pvt. Ltd.
|
|
3
|
+
* All rights reserved. This code may not be copied
|
|
4
|
+
* or duplicated in whole or part by any means
|
|
5
|
+
* without express prior agreement in writing.
|
|
6
|
+
* -----------------------------------------------*/
|
|
7
|
+
/**
|
|
8
|
+
* Turning a call into a line — masking, provenance, and the one-line layout.
|
|
9
|
+
*
|
|
10
|
+
* Extracted from `svm-util/src/svm-helper.ts` (`stringify`, `getLogEntry`, `formatLogMessage`,
|
|
11
|
+
* `isLogEnabled`), with the global configuration lookup removed: every function here takes
|
|
12
|
+
* what it needs. See `config.ts` for why.
|
|
13
|
+
*/
|
|
14
|
+
import nodeUtil from "node:util";
|
|
15
|
+
import { DEFAULT_MASK } from "./config.js";
|
|
16
|
+
const COMPACT = {
|
|
17
|
+
depth: 4, compact: true, breakLength: Infinity, maxStringLength: 512, colors: false,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Is logging switched on for this origin?
|
|
21
|
+
*
|
|
22
|
+
* `node:util.debuglog` is the switch, so `NODE_DEBUG=gov-work:cli:agent-install` enables one
|
|
23
|
+
* module and `NODE_DEBUG=gov-work:*` the package — POL-425's "switchable at run time by
|
|
24
|
+
* whoever is in front of the problem, with no rebuild and no release".
|
|
25
|
+
*
|
|
26
|
+
* NOT CACHED PER CALL BY ACCIDENT: `debuglog` itself is lazy and memoised, so asking it every
|
|
27
|
+
* time is cheap, and asking it every time is what lets a long-running process be told the
|
|
28
|
+
* truth if the environment is re-read.
|
|
29
|
+
*/
|
|
30
|
+
export function isLogEnabled(pgm_cd, fn_cd, act_id) {
|
|
31
|
+
const on = (s) => (s ? nodeUtil.debuglog(s).enabled : false);
|
|
32
|
+
return on(pgm_cd) || on(fn_cd) || on(act_id?.toString());
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Replace the value of any masked field, at any depth, before it can be rendered.
|
|
36
|
+
*
|
|
37
|
+
* POL-427 (C01). The comparison is case-insensitive and matches a field whose name CONTAINS a
|
|
38
|
+
* masked word, because the field that leaks is rarely named exactly `password` — it is
|
|
39
|
+
* `userPassword`, `db_pwd`, `authToken`. Over-masking costs a reader one lookup; under-masking
|
|
40
|
+
* costs a credential.
|
|
41
|
+
*/
|
|
42
|
+
export function maskValues(value, mask = DEFAULT_MASK) {
|
|
43
|
+
const hidden = (key) => {
|
|
44
|
+
const k = key.toLowerCase();
|
|
45
|
+
return mask.some((m) => k.includes(m.toLowerCase()));
|
|
46
|
+
};
|
|
47
|
+
const walk = (v, depth) => {
|
|
48
|
+
if (depth > 8 || v === null || typeof v !== "object")
|
|
49
|
+
return v;
|
|
50
|
+
if (Array.isArray(v))
|
|
51
|
+
return v.map((x) => walk(x, depth + 1));
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const [k, val] of Object.entries(v)) {
|
|
54
|
+
out[k] = hidden(k) ? "***" : walk(val, depth + 1);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
};
|
|
58
|
+
return walk(value, 0);
|
|
59
|
+
}
|
|
60
|
+
/** Render anything for a log line: masked, depth-limited, and never throwing. */
|
|
61
|
+
export function stringify(obj, config) {
|
|
62
|
+
if (obj === null)
|
|
63
|
+
return "null";
|
|
64
|
+
if (obj === undefined)
|
|
65
|
+
return "undefined";
|
|
66
|
+
if (typeof obj !== "object")
|
|
67
|
+
return String(obj);
|
|
68
|
+
try {
|
|
69
|
+
const masked = maskValues(obj, config?.mask ?? DEFAULT_MASK);
|
|
70
|
+
return nodeUtil.inspect(masked, { ...COMPACT, ...(config?.inspect ?? {}) });
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
// A logger that throws while rendering takes the program down at the moment it was trying
|
|
74
|
+
// to explain itself. Say what went wrong with the value and keep the line.
|
|
75
|
+
return `<unrenderable: ${e?.message ?? "unknown"}>`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Give a call its provenance. Padding keeps a wall of lines readable in columns. */
|
|
79
|
+
export function toLogEntry(appId, lvl, msg, pgm_cd, fn_cd, act_id, meta) {
|
|
80
|
+
return {
|
|
81
|
+
level: lvl,
|
|
82
|
+
message: msg,
|
|
83
|
+
app_cd: appId,
|
|
84
|
+
pgm_cd: pgm_cd ?? "n/a",
|
|
85
|
+
fn_cd: fn_cd ?? "n/a",
|
|
86
|
+
act_id: act_id ?? 0,
|
|
87
|
+
meta,
|
|
88
|
+
logTime: new Date().toISOString(),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The single line a transport writes.
|
|
93
|
+
*
|
|
94
|
+
* The origin comes FIRST and at a fixed width, because the common act is scanning a column
|
|
95
|
+
* for one module rather than reading prose — which is what a log is for when it is being read
|
|
96
|
+
* at all.
|
|
97
|
+
*/
|
|
98
|
+
export function formatLogMessage(e, config) {
|
|
99
|
+
const pgm = (e.pgm_cd ?? "n/a").padEnd(34);
|
|
100
|
+
const fn = (e.fn_cd ?? "n/a").padEnd(22);
|
|
101
|
+
const act = String(e.act_id ?? 0).padEnd(4);
|
|
102
|
+
const meta = e.meta === undefined ? "" : ` meta=${stringify(e.meta, config)}`;
|
|
103
|
+
return `${e.logTime ?? ""} ${e.level.padEnd(7)} ${pgm} ${fn} ${act} ${e.message}${meta}`;
|
|
104
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@svayam-opensource/svm-util-log` — the shared logging framework mandated by POL-423,
|
|
3
|
+
* carrying only the dependencies a logger needs.
|
|
4
|
+
*
|
|
5
|
+
* The first package of the `svm-util` split (911#224). It exists because the logger and the
|
|
6
|
+
* API server were one package, so a command-line tool could not obey POL-423 without shipping
|
|
7
|
+
* express, mysql, axios and ajv to every adopter — 675 packages for a log line.
|
|
8
|
+
*
|
|
9
|
+
* PUBLIC SCOPE, deliberately. `@svayam/svm-util` publishes to the internal registry, which
|
|
10
|
+
* adopters of `@svayam-opensource/gov` cannot resolve. A logging mandate that makes the
|
|
11
|
+
* governance CLI uninstallable is a mandate nothing can obey.
|
|
12
|
+
*/
|
|
13
|
+
export { LogLevel, LogLevelColors, LogLevelIndex, LogMethod, logLevelIndex, withinLevel } from "./levels.js";
|
|
14
|
+
export type { LogEntryFields, logFunctionType, logLevelType, logMethodType } from "./levels.js";
|
|
15
|
+
export { DEFAULT_MASK } from "./config.js";
|
|
16
|
+
export type { CONSOLE_LOG_CONFIG_METHOD, EXTERNAL_LOG_CONFIG_METHOD, FILE_LOG_CONFIG_METHOD, FileLogOptions, LOG_CONFIG_METHOD, LoggerConfig, } from "./config.js";
|
|
17
|
+
export { formatLogMessage, isLogEnabled, maskValues, stringify, toLogEntry } from "./format.js";
|
|
18
|
+
export { createApplicationLogger, registerTransport } from "./logger.js";
|
|
19
|
+
export type { ApplicationLogger, TransportFactory } from "./logger.js";
|
package/lib/esm/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* -------------------------------------------------
|
|
2
|
+
* Copyright (c) 2026 - Svayam Infoware Pvt. Ltd.
|
|
3
|
+
* All rights reserved. This code may not be copied
|
|
4
|
+
* or duplicated in whole or part by any means
|
|
5
|
+
* without express prior agreement in writing.
|
|
6
|
+
* -----------------------------------------------*/
|
|
7
|
+
/**
|
|
8
|
+
* `@svayam-opensource/svm-util-log` — the shared logging framework mandated by POL-423,
|
|
9
|
+
* carrying only the dependencies a logger needs.
|
|
10
|
+
*
|
|
11
|
+
* The first package of the `svm-util` split (911#224). It exists because the logger and the
|
|
12
|
+
* API server were one package, so a command-line tool could not obey POL-423 without shipping
|
|
13
|
+
* express, mysql, axios and ajv to every adopter — 675 packages for a log line.
|
|
14
|
+
*
|
|
15
|
+
* PUBLIC SCOPE, deliberately. `@svayam/svm-util` publishes to the internal registry, which
|
|
16
|
+
* adopters of `@svayam-opensource/gov` cannot resolve. A logging mandate that makes the
|
|
17
|
+
* governance CLI uninstallable is a mandate nothing can obey.
|
|
18
|
+
*/
|
|
19
|
+
export { LogLevel, LogLevelColors, LogLevelIndex, LogMethod, logLevelIndex, withinLevel } from "./levels.js";
|
|
20
|
+
export { DEFAULT_MASK } from "./config.js";
|
|
21
|
+
export { formatLogMessage, isLogEnabled, maskValues, stringify, toLogEntry } from "./format.js";
|
|
22
|
+
export { createApplicationLogger, registerTransport } from "./logger.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Levels, and the shape of a log call — extracted from `svm-util/src/svm-util-base.ts`
|
|
3
|
+
* unchanged, so a line written against either library means the same thing.
|
|
4
|
+
*
|
|
5
|
+
* DELIBERATELY DEPENDENCY-FREE. In `svm-util` these live beside `PoolConfig` from mysql,
|
|
6
|
+
* Axios request types, Ajv and Express, in one shared types module — so importing the level
|
|
7
|
+
* enum pulled a web-server stack. That single file is why a CLI could not take the logger
|
|
8
|
+
* (911#224), and separating it is most of this package's reason to exist.
|
|
9
|
+
*/
|
|
10
|
+
/** Where a log line can be sent. `database` and `api` are registered by other packages. */
|
|
11
|
+
export declare const LogMethod: {
|
|
12
|
+
readonly console: "console";
|
|
13
|
+
readonly file: "file";
|
|
14
|
+
readonly database: "database";
|
|
15
|
+
readonly api: "api";
|
|
16
|
+
};
|
|
17
|
+
export type logMethodType = keyof typeof LogMethod;
|
|
18
|
+
export declare const LogLevel: {
|
|
19
|
+
readonly error: "error";
|
|
20
|
+
readonly warn: "warn";
|
|
21
|
+
readonly info: "info";
|
|
22
|
+
readonly http: "http";
|
|
23
|
+
readonly verbose: "verbose";
|
|
24
|
+
readonly debug: "debug";
|
|
25
|
+
readonly silly: "silly";
|
|
26
|
+
};
|
|
27
|
+
export type logLevelType = keyof typeof LogLevel;
|
|
28
|
+
/** Ordered least to most verbose; the index IS the threshold comparison. */
|
|
29
|
+
export declare const logLevelIndex: logLevelType[];
|
|
30
|
+
export declare const LogLevelIndex: {
|
|
31
|
+
readonly error: 0;
|
|
32
|
+
readonly warn: 1;
|
|
33
|
+
readonly info: 2;
|
|
34
|
+
readonly http: 3;
|
|
35
|
+
readonly verbose: 4;
|
|
36
|
+
readonly debug: 5;
|
|
37
|
+
readonly silly: 6;
|
|
38
|
+
};
|
|
39
|
+
export declare const LogLevelColors: {
|
|
40
|
+
error: string;
|
|
41
|
+
warn: string;
|
|
42
|
+
info: string;
|
|
43
|
+
http: string;
|
|
44
|
+
verbose: string;
|
|
45
|
+
debug: string;
|
|
46
|
+
silly: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The call every logged line makes.
|
|
50
|
+
*
|
|
51
|
+
* `pgm_cd` and `fn_cd` are not decoration — they are how a reader gets from a line back to the
|
|
52
|
+
* code that wrote it (POL-426). The convention is the CODE PATH: `<package>:<dir>:<module>`,
|
|
53
|
+
* so `gov-work:cli:agent-install` is `src/cli/agent-install.ts` and nothing has to be guessed.
|
|
54
|
+
*
|
|
55
|
+
* `act_id` ties lines from one unit of work together across modules.
|
|
56
|
+
*/
|
|
57
|
+
export type logFunctionType = (lvl: logLevelType, msg: string, pgm_cd?: string, fn_cd?: string, act_id?: number, meta?: unknown) => void;
|
|
58
|
+
/** A line, once it has been given its provenance. */
|
|
59
|
+
export interface LogEntryFields {
|
|
60
|
+
level: logLevelType;
|
|
61
|
+
message: string;
|
|
62
|
+
pgm_cd?: string;
|
|
63
|
+
fn_cd?: string;
|
|
64
|
+
app_cd?: string;
|
|
65
|
+
act_id?: number;
|
|
66
|
+
meta?: unknown;
|
|
67
|
+
}
|
|
68
|
+
/** Is `lvl` within the threshold `at`? Both are level names, not numbers, at the call site. */
|
|
69
|
+
export declare function withinLevel(lvl: logLevelType, at: logLevelType): boolean;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* -------------------------------------------------
|
|
2
|
+
* Copyright (c) 2026 - Svayam Infoware Pvt. Ltd.
|
|
3
|
+
* All rights reserved. This code may not be copied
|
|
4
|
+
* or duplicated in whole or part by any means
|
|
5
|
+
* without express prior agreement in writing.
|
|
6
|
+
* -----------------------------------------------*/
|
|
7
|
+
/**
|
|
8
|
+
* Levels, and the shape of a log call — extracted from `svm-util/src/svm-util-base.ts`
|
|
9
|
+
* unchanged, so a line written against either library means the same thing.
|
|
10
|
+
*
|
|
11
|
+
* DELIBERATELY DEPENDENCY-FREE. In `svm-util` these live beside `PoolConfig` from mysql,
|
|
12
|
+
* Axios request types, Ajv and Express, in one shared types module — so importing the level
|
|
13
|
+
* enum pulled a web-server stack. That single file is why a CLI could not take the logger
|
|
14
|
+
* (911#224), and separating it is most of this package's reason to exist.
|
|
15
|
+
*/
|
|
16
|
+
/** Where a log line can be sent. `database` and `api` are registered by other packages. */
|
|
17
|
+
export const LogMethod = { console: "console", file: "file", database: "database", api: "api" };
|
|
18
|
+
export const LogLevel = {
|
|
19
|
+
error: "error", warn: "warn", info: "info", http: "http",
|
|
20
|
+
verbose: "verbose", debug: "debug", silly: "silly",
|
|
21
|
+
};
|
|
22
|
+
/** Ordered least to most verbose; the index IS the threshold comparison. */
|
|
23
|
+
export const logLevelIndex = [
|
|
24
|
+
LogLevel.error, LogLevel.warn, LogLevel.info, LogLevel.http,
|
|
25
|
+
LogLevel.verbose, LogLevel.debug, LogLevel.silly,
|
|
26
|
+
];
|
|
27
|
+
export const LogLevelIndex = { error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6 };
|
|
28
|
+
export const LogLevelColors = {
|
|
29
|
+
error: "red", warn: "magenta", info: "green", http: "blue",
|
|
30
|
+
verbose: "cyan", debug: "yellow", silly: "gray",
|
|
31
|
+
};
|
|
32
|
+
/** Is `lvl` within the threshold `at`? Both are level names, not numbers, at the call site. */
|
|
33
|
+
export function withinLevel(lvl, at) {
|
|
34
|
+
return LogLevelIndex[lvl] <= LogLevelIndex[at];
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type Transport from "winston-transport";
|
|
2
|
+
import type { LoggerConfig, LOG_CONFIG_METHOD } from "./config.js";
|
|
3
|
+
import { type logFunctionType, type logLevelType, type logMethodType } from "./levels.js";
|
|
4
|
+
/**
|
|
5
|
+
* Transports this package does not implement, contributed by packages that do.
|
|
6
|
+
*
|
|
7
|
+
* Registration is by METHOD name, so a consumer's config file is unchanged whether or not the
|
|
8
|
+
* contributing package is installed — a `database` method simply does nothing until
|
|
9
|
+
* `@svayam/svm-util-db` is present and has registered. A missing transport is a line that does
|
|
10
|
+
* not go somewhere, never a crash at startup.
|
|
11
|
+
*/
|
|
12
|
+
export type TransportFactory = (conf: LOG_CONFIG_METHOD, config: LoggerConfig) => Transport;
|
|
13
|
+
export declare function registerTransport(method: logMethodType, make: TransportFactory): void;
|
|
14
|
+
export interface ApplicationLogger {
|
|
15
|
+
readonly log: logFunctionType;
|
|
16
|
+
/** What is actually enabled, for `gov doctor`-style reporting and for tests. */
|
|
17
|
+
toLogObject(): Record<string, unknown>;
|
|
18
|
+
toString(): string;
|
|
19
|
+
close(): void;
|
|
20
|
+
}
|
|
21
|
+
export declare function createApplicationLogger(config: LoggerConfig): ApplicationLogger;
|
|
22
|
+
export type { logLevelType, logFunctionType };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/* -------------------------------------------------
|
|
2
|
+
* Copyright (c) 2026 - Svayam Infoware Pvt. Ltd.
|
|
3
|
+
* All rights reserved. This code may not be copied
|
|
4
|
+
* or duplicated in whole or part by any means
|
|
5
|
+
* without express prior agreement in writing.
|
|
6
|
+
* -----------------------------------------------*/
|
|
7
|
+
/**
|
|
8
|
+
* The application logger — `svm-util`'s, with the console and file transports and nothing else.
|
|
9
|
+
*
|
|
10
|
+
* TRANSPORTS ARE PLUGGABLE, and that is the change that makes this package possible.
|
|
11
|
+
* `svm-util/src/svm-logger.ts` builds every transport itself, so it imports `svm-db.js` for the
|
|
12
|
+
* database one and drags `mysql` into anything that wants to log. Here a transport for a method
|
|
13
|
+
* this package does not implement is looked up in a registry, and the packages that own those
|
|
14
|
+
* dependencies register into it (911#224).
|
|
15
|
+
*
|
|
16
|
+
* `exitOnError: false`, as in the original: a library must never take the host process down
|
|
17
|
+
* because a transport failed. That is doubly true here — the transport most likely to fail is
|
|
18
|
+
* the one writing the file you were going to read afterwards.
|
|
19
|
+
*/
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { createLogger, format, transports as winstonTransports, addColors } from "winston";
|
|
22
|
+
import DailyRotateFile from "winston-daily-rotate-file";
|
|
23
|
+
import { DEFAULT_MASK } from "./config.js";
|
|
24
|
+
import { LogLevel, LogLevelColors, LogLevelIndex } from "./levels.js";
|
|
25
|
+
import { formatLogMessage, isLogEnabled, toLogEntry } from "./format.js";
|
|
26
|
+
const registry = new Map();
|
|
27
|
+
export function registerTransport(method, make) {
|
|
28
|
+
registry.set(method, make);
|
|
29
|
+
}
|
|
30
|
+
function buildTransports(config) {
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const conf of config.methods) {
|
|
33
|
+
if (conf.METHOD === "console") {
|
|
34
|
+
out.push(new winstonTransports.Console({
|
|
35
|
+
level: conf.LOGLEVEL || LogLevel.info,
|
|
36
|
+
handleExceptions: true,
|
|
37
|
+
format: format.combine(format.colorize({ all: true }), format.printf((info) => formatLogMessage(info, config))),
|
|
38
|
+
// Diagnostics belong on STDERR. stdout is a program's ANSWER, and a caller piping it
|
|
39
|
+
// into another command should not receive the log as data.
|
|
40
|
+
stderrLevels: Object.values(LogLevel),
|
|
41
|
+
}));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (conf.METHOD === "file") {
|
|
45
|
+
const f = conf;
|
|
46
|
+
const dir = f.DATA_STORE?.dirname ?? config.logDir;
|
|
47
|
+
out.push(new DailyRotateFile({
|
|
48
|
+
level: f.LOGLEVEL || LogLevel.info,
|
|
49
|
+
dirname: dir,
|
|
50
|
+
filename: path.join(String(f.DATA_STORE?.filename ?? ""), `%DATE%-${config.appId}.log`),
|
|
51
|
+
datePattern: f.DATA_STORE?.datePattern ?? "YYYY-MM-DD",
|
|
52
|
+
zippedArchive: f.DATA_STORE?.zippedArchive ?? true,
|
|
53
|
+
maxSize: f.DATA_STORE?.maxSize ?? "20m",
|
|
54
|
+
// A log kept forever is a disclosure risk that grows on its own; a fortnight is enough
|
|
55
|
+
// to diagnose something an adopter noticed today.
|
|
56
|
+
maxFiles: f.DATA_STORE?.maxFiles ?? "14d",
|
|
57
|
+
format: format.combine(format.printf((info) => formatLogMessage(info, config))),
|
|
58
|
+
}));
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const make = registry.get(conf.METHOD);
|
|
62
|
+
if (make)
|
|
63
|
+
out.push(make(conf, config));
|
|
64
|
+
// else: declared but unavailable. Silence here is correct — see registerTransport.
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
export function createApplicationLogger(config) {
|
|
69
|
+
addColors(LogLevelColors);
|
|
70
|
+
const built = buildTransports(config);
|
|
71
|
+
const winston = createLogger({
|
|
72
|
+
levels: LogLevelIndex,
|
|
73
|
+
transports: built,
|
|
74
|
+
exitOnError: false,
|
|
75
|
+
});
|
|
76
|
+
const effective = { mask: DEFAULT_MASK, ...config };
|
|
77
|
+
const log = (lvl, msg, pgm_cd, fn_cd, act_id, meta) => {
|
|
78
|
+
// ERRORS AND WARNINGS ALWAYS PASS. Everything below them is opt-in through NODE_DEBUG, so
|
|
79
|
+
// an ordinary run is quiet — but a run that went wrong still said so without anybody
|
|
80
|
+
// having had the foresight to enable anything.
|
|
81
|
+
const always = LogLevelIndex[lvl] <= LogLevelIndex[LogLevel.warn];
|
|
82
|
+
if (!always && !isLogEnabled(pgm_cd, fn_cd, act_id))
|
|
83
|
+
return;
|
|
84
|
+
winston.log(toLogEntry(effective.appId, lvl, msg, pgm_cd, fn_cd, act_id, meta));
|
|
85
|
+
};
|
|
86
|
+
const toLogObject = () => ({
|
|
87
|
+
APP_ID: effective.appId,
|
|
88
|
+
LOG_DIR: effective.logDir,
|
|
89
|
+
REQUESTED_METHODS: effective.methods.map((m) => m.METHOD),
|
|
90
|
+
ENABLED_TRANSPORTS: winston.transports.map((t) => ({ type: t.constructor.name, level: t.level })),
|
|
91
|
+
MASK: effective.mask,
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
log,
|
|
95
|
+
toLogObject,
|
|
96
|
+
toString: () => JSON.stringify(toLogObject()),
|
|
97
|
+
// A CLI exits in seconds and a rotating file transport writes asynchronously; without a
|
|
98
|
+
// close the last lines of a short run — the ones describing why it ended — are the ones
|
|
99
|
+
// most likely to be lost.
|
|
100
|
+
close: () => { try {
|
|
101
|
+
winston.close();
|
|
102
|
+
}
|
|
103
|
+
catch { /* closing a closed logger is not news */ } },
|
|
104
|
+
};
|
|
105
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@svayam-opensource/svm-util-log",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Svayam's shared logging framework — the logger from svm-util, with only the dependencies a logger needs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./lib/esm/index.d.ts",
|
|
9
|
+
"import": "./lib/esm/index.js",
|
|
10
|
+
"require": "./lib/cjs/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"main": "./lib/cjs/index.js",
|
|
14
|
+
"module": "./lib/esm/index.js",
|
|
15
|
+
"types": "./lib/esm/index.d.ts",
|
|
16
|
+
"files": [
|
|
17
|
+
"lib/",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20.0.0"
|
|
22
|
+
},
|
|
23
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/Svayamtech/911-SVM-LIB-SVC.git",
|
|
30
|
+
"directory": "packages/libraries/ts/svm-util-log"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"clean": "rm -rf lib",
|
|
34
|
+
"build": "npm run clean && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json",
|
|
35
|
+
"test": "NODE_OPTIONS='--import tsx' mocha --extensions ts 'test/**/*.test.ts'",
|
|
36
|
+
"prepublishOnly": "npm run build && npm run test"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"winston": "^3.19.0",
|
|
40
|
+
"winston-daily-rotate-file": "^5.0.0",
|
|
41
|
+
"winston-transport": "^4.9.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/chai": "^5.0.1",
|
|
45
|
+
"@types/mocha": "^10.0.10",
|
|
46
|
+
"@types/node": "^22.10.2",
|
|
47
|
+
"chai": "^5.1.2",
|
|
48
|
+
"mocha": "^11.0.1",
|
|
49
|
+
"tsx": "^4.19.2",
|
|
50
|
+
"typescript": "^5.7.2"
|
|
51
|
+
},
|
|
52
|
+
"gov": {
|
|
53
|
+
"contentSha": "4b8408bbbe485fc7feb93faad9f8642ff260fb193d76e20561f5913f6ee1fcc8",
|
|
54
|
+
"shaFrom": "built-ref",
|
|
55
|
+
"commit": "2aec38c1b7ec1dc9f658f76e8a90b7c70bb54d21"
|
|
56
|
+
}
|
|
57
|
+
}
|