@svayam-opensource/svm-util-log 1.0.1 → 1.1.1
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 +367 -46
- package/lib/cjs/logger.js +13 -4
- package/lib/esm/config.d.ts +10 -0
- package/lib/esm/logger.js +13 -4
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -1,82 +1,403 @@
|
|
|
1
1
|
# @svayam-opensource/svm-util-log
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
dependencies a logger needs.
|
|
3
|
+
Structured logging for Node applications and command-line tools.
|
|
5
4
|
|
|
6
|
-
**
|
|
7
|
-
|
|
5
|
+
Every line carries **where it came from**, so a log you receive from someone else can be read
|
|
6
|
+
back to the code that wrote it. Debug output is **switched on from the environment**, so the
|
|
7
|
+
person in front of the problem can turn it on without a rebuild. Secrets are **masked before
|
|
8
|
+
anything is written**.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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**.
|
|
10
|
+
```bash
|
|
11
|
+
npm install @svayam-opensource/svm-util-log
|
|
12
|
+
```
|
|
17
13
|
|
|
18
|
-
|
|
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.
|
|
14
|
+
Node 20+. TypeScript types included. Works from both `import` and `require`.
|
|
21
15
|
|
|
22
|
-
|
|
16
|
+
---
|
|
23
17
|
|
|
24
|
-
##
|
|
18
|
+
## Quick start
|
|
25
19
|
|
|
26
20
|
```ts
|
|
27
21
|
import { createApplicationLogger, LogLevel } from "@svayam-opensource/svm-util-log";
|
|
28
22
|
|
|
29
23
|
const logger = createApplicationLogger({
|
|
30
24
|
appId: "gov-work",
|
|
31
|
-
logDir: "
|
|
25
|
+
logDir: "/Users/me/.gov/logs",
|
|
32
26
|
methods: [
|
|
33
|
-
{ METHOD: "console", LOGLEVEL: LogLevel.warn },
|
|
34
|
-
{ METHOD: "file", LOGLEVEL: LogLevel.debug },
|
|
27
|
+
{ METHOD: "console", LOGLEVEL: LogLevel.warn }, // on screen: warnings and errors
|
|
28
|
+
{ METHOD: "file", LOGLEVEL: LogLevel.debug }, // on disk: everything
|
|
35
29
|
],
|
|
36
30
|
});
|
|
37
31
|
|
|
38
|
-
logger.log(LogLevel.
|
|
32
|
+
logger.log(LogLevel.info, "starting up", "myapp:cli:main", "start");
|
|
33
|
+
logger.log(LogLevel.error, "could not reach the registry", "myapp:net:client", "fetch", 0, { status: 503 });
|
|
34
|
+
|
|
35
|
+
logger.close(); // flush before the process exits
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
That is the whole setup. Everything below is detail you can come back for.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## What a line looks like
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
2026-09-09T03:41:02.240Z warn gov-work:cli:agent-install signIn 7 no browser on this machine — offering the key route
|
|
46
|
+
2026-09-09T03:41:02.367Z debug gov-work:cli:sign-in-choice captureAgentKey 7 chose the api-key route meta={ user: 'rk', apiKey: '***' }
|
|
47
|
+
2026-09-09T03:41:02.494Z error gov-work:cli:agent-install run 7 npm could not install the agent meta={ code: 127 }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Columns are fixed-width and the origin comes early, because the usual thing to do with a log
|
|
51
|
+
is **scan one column for one module**, not read it as prose.
|
|
52
|
+
|
|
53
|
+
| Column | From | What it is for |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `2026-09-09T03:41:02.240Z` | automatic | ISO timestamp, UTC |
|
|
56
|
+
| `warn` | `lvl` | severity — see [Levels](#levels) |
|
|
57
|
+
| `gov-work:cli:agent-install` | `pgm_cd` | **the code path** — which module wrote this |
|
|
58
|
+
| `signIn` | `fn_cd` | which function inside it |
|
|
59
|
+
| `7` | `act_id` | ties one unit of work together across modules |
|
|
60
|
+
| `no browser on this machine…` | `msg` | your message |
|
|
61
|
+
| `meta={ … }` | `meta` | structured detail, masked |
|
|
62
|
+
|
|
63
|
+
### Naming `pgm_cd` — the one convention worth following
|
|
64
|
+
|
|
65
|
+
Use the **code path**: `<package>:<directory>:<module>`.
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
gov-work:cli:sign-in-choice → src/cli/sign-in-choice.ts
|
|
69
|
+
myapp:net:client → src/net/client.ts
|
|
39
70
|
```
|
|
40
71
|
|
|
41
|
-
|
|
72
|
+
A reader who receives your log can then open the file without guessing, and — as the next
|
|
73
|
+
section shows — can switch that exact module on by name.
|
|
42
74
|
|
|
43
|
-
|
|
44
|
-
The point is that a reader can get from a line back to the code without guessing (POL-426).
|
|
75
|
+
---
|
|
45
76
|
|
|
46
|
-
|
|
77
|
+
## Turning on debug output in the field
|
|
47
78
|
|
|
48
|
-
Errors and warnings always
|
|
79
|
+
**Errors and warnings are always written.** Everything below them is off until someone asks
|
|
80
|
+
for it, through the standard Node `NODE_DEBUG` variable:
|
|
49
81
|
|
|
50
82
|
```bash
|
|
51
|
-
NODE_DEBUG=gov-work:cli:sign-in-choice gov work
|
|
52
|
-
NODE_DEBUG=gov-work:*
|
|
83
|
+
NODE_DEBUG=gov-work:cli:sign-in-choice gov work # one module
|
|
84
|
+
NODE_DEBUG=gov-work:cli:* gov work # one directory
|
|
85
|
+
NODE_DEBUG=gov-work:* gov work # the whole application
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This matters because **the person who needs the log is usually not the person who can
|
|
89
|
+
reproduce the fault**. You send them one environment variable; they reproduce it and send back
|
|
90
|
+
the file. No debug build, no release, no code change.
|
|
91
|
+
|
|
92
|
+
```mermaid
|
|
93
|
+
flowchart TD
|
|
94
|
+
A["logger.log(level, msg, pgm_cd, …)"] --> B{"level is<br/>error or warn?"}
|
|
95
|
+
B -- yes --> D["write it"]
|
|
96
|
+
B -- no --> C{"NODE_DEBUG matches<br/>pgm_cd, fn_cd or act_id?"}
|
|
97
|
+
C -- yes --> D
|
|
98
|
+
C -- no --> E["dropped, costs nothing"]
|
|
99
|
+
D --> F{"for each configured<br/>transport"}
|
|
100
|
+
F --> G["level within<br/>this transport's LOGLEVEL?"]
|
|
101
|
+
G -- yes --> H["mask meta → format → write"]
|
|
102
|
+
G -- no --> E
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Two gates, and they are different questions. `NODE_DEBUG` decides **whether this line exists
|
|
106
|
+
at all**; a transport's `LOGLEVEL` decides **where an existing line goes**. That is how the
|
|
107
|
+
quick-start config keeps the screen quiet at `warn` while the file keeps everything.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Levels
|
|
112
|
+
|
|
113
|
+
Ordered least to most verbose. A transport set to a level accepts that level and everything
|
|
114
|
+
above it.
|
|
115
|
+
|
|
116
|
+
| Level | Use it for |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `error` | the operation failed — **always written** |
|
|
119
|
+
| `warn` | something is wrong but the run continues — **always written** |
|
|
120
|
+
| `info` | milestones a user would recognise |
|
|
121
|
+
| `http` | requests and responses |
|
|
122
|
+
| `verbose` | more than `info`, less than a trace |
|
|
123
|
+
| `debug` | what you would want during an incident |
|
|
124
|
+
| `silly` | everything |
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { withinLevel, LogLevel } from "@svayam-opensource/svm-util-log";
|
|
128
|
+
|
|
129
|
+
withinLevel(LogLevel.error, LogLevel.info); // true — error passes an info threshold
|
|
130
|
+
withinLevel(LogLevel.debug, LogLevel.info); // false — debug does not
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Keeping secrets out of logs
|
|
136
|
+
|
|
137
|
+
A log is a file. It gets copied and sent to other people — precisely when something has gone
|
|
138
|
+
wrong. So masking happens **in the library, before rendering**, not in your memory at each
|
|
139
|
+
call site.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
logger.log(LogLevel.debug, "signing in", "myapp:auth:login", "run", 0, {
|
|
143
|
+
user: "rk",
|
|
144
|
+
apiKey: "sk-live-9f3a",
|
|
145
|
+
db: { host: "localhost", password: "hunter2" },
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
… signing in meta={ user: 'rk', apiKey: '***', db: { host: 'localhost', password: '***' } }
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Matching is **by field name, case-insensitive, at any depth**, and matches names that
|
|
154
|
+
*contain* a masked word — so `userPassword`, `db_pwd` and `authToken` are all covered.
|
|
155
|
+
|
|
156
|
+
Covered out of the box:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
password passwd pwd secret token apiKey api_key authorization
|
|
160
|
+
auth credential credentials privateKey private_key sessionId
|
|
161
|
+
session_id cookie jwt bearer
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Extend the default, never replace it:**
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { DEFAULT_MASK } from "@svayam-opensource/svm-util-log";
|
|
168
|
+
|
|
169
|
+
createApplicationLogger({
|
|
170
|
+
appId: "myapp",
|
|
171
|
+
logDir: "/var/log/myapp",
|
|
172
|
+
methods: [{ METHOD: "file", LOGLEVEL: LogLevel.info }],
|
|
173
|
+
mask: [...DEFAULT_MASK, "panNumber", "aadhaar"],
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Passing `mask: ["panNumber"]` on its own would silently switch off every default — the one
|
|
178
|
+
mistake worth watching for here.
|
|
179
|
+
|
|
180
|
+
> Masking protects values inside `meta`. It cannot see a secret you interpolated into the
|
|
181
|
+
> message string yourself, so put values in `meta`, not in `msg`.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Where lines go
|
|
186
|
+
|
|
187
|
+
Two transports ship in this package.
|
|
188
|
+
|
|
189
|
+
### `console`
|
|
190
|
+
|
|
191
|
+
Writes to **stderr**, coloured by level. stderr because stdout is your program's answer — a
|
|
192
|
+
caller running `myapp | jq` should not receive log lines as data.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
{ METHOD: "console", LOGLEVEL: LogLevel.warn }
|
|
53
196
|
```
|
|
54
197
|
|
|
55
|
-
|
|
56
|
-
the fault.** They turn it on, reproduce, and send the file.
|
|
198
|
+
### `file`
|
|
57
199
|
|
|
58
|
-
|
|
200
|
+
A daily-rotating file, `<date>-<appId>.log`, in `logDir`.
|
|
59
201
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
202
|
+
```ts
|
|
203
|
+
{
|
|
204
|
+
METHOD: "file",
|
|
205
|
+
LOGLEVEL: LogLevel.debug,
|
|
206
|
+
DATA_STORE: {
|
|
207
|
+
dirname: "/var/log/myapp", // defaults to logDir
|
|
208
|
+
datePattern: "YYYY-MM-DD",
|
|
209
|
+
zippedArchive: true,
|
|
210
|
+
maxSize: "20m",
|
|
211
|
+
maxFiles: "14d", // older files are deleted
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Defaults are 20 MB per file, gzipped, kept a fortnight — long enough to diagnose something
|
|
217
|
+
reported today, short enough that a forgotten log directory is not a growing disclosure risk.
|
|
218
|
+
|
|
219
|
+
### Adding your own
|
|
220
|
+
|
|
221
|
+
`database` and `api` transports are **not** in this package — they would drag a database
|
|
222
|
+
driver and an HTTP client into every install. Register one instead:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { registerTransport } from "@svayam-opensource/svm-util-log";
|
|
226
|
+
import Transport from "winston-transport";
|
|
227
|
+
|
|
228
|
+
class DbTransport extends Transport { /* … */ }
|
|
229
|
+
|
|
230
|
+
registerTransport("database", (conf, config) => new DbTransport(conf, config));
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Register **before** you call `createApplicationLogger`. A method named in your config with
|
|
234
|
+
nothing registered for it is **ignored, not an error** — the same config file can move between
|
|
235
|
+
a service that has a database and a CLI that does not, and the CLI simply writes fewer places.
|
|
236
|
+
|
|
237
|
+
```mermaid
|
|
238
|
+
flowchart LR
|
|
239
|
+
L["logger.log(…)"] --> M["mask meta"] --> F["format one line"]
|
|
240
|
+
F --> C["console → stderr"]
|
|
241
|
+
F --> D["file → daily rotate"]
|
|
242
|
+
F --> X["your transport<br/><i>registerTransport</i>"]
|
|
243
|
+
style X stroke-dasharray: 4 3
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Configuration reference
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
createApplicationLogger({
|
|
252
|
+
appId, // string — names the app in every line and in the filename
|
|
253
|
+
logDir, // string — where file transports write by default
|
|
254
|
+
methods, // array — one entry per destination
|
|
255
|
+
mask, // string[] — optional; extend DEFAULT_MASK
|
|
256
|
+
inspect, // optional — { depth?: number; maxStringLength?: number } for meta rendering
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
| Field | Required | Notes |
|
|
261
|
+
|---|---|---|
|
|
262
|
+
| `appId` | yes | appears as `app_cd` and in the log filename |
|
|
263
|
+
| `logDir` | yes | must exist or be creatable; used when a `file` method sets no `dirname` |
|
|
264
|
+
| `methods` | yes | must name at least one destination — see the note below |
|
|
265
|
+
| `mask` | no | defaults to `DEFAULT_MASK`; **extend it, don't replace it** |
|
|
266
|
+
| `inspect` | no | defaults to depth 4, 512-character strings |
|
|
267
|
+
|
|
268
|
+
> **Give `methods` at least one entry.** An empty array does not throw, but it is not silence
|
|
269
|
+
> either: winston then prints its own `Attempt to write logs with no transports` warning for
|
|
270
|
+
> every line, with the entry as raw JSON. That output bypasses this library's formatting — the
|
|
271
|
+
> masking still applies, because `meta` is masked on the way in, but the result is noisy and
|
|
272
|
+
> not what you configured. `logger.toLogObject().ENABLED_TRANSPORTS` shows what is really
|
|
273
|
+
> attached.
|
|
274
|
+
|
|
275
|
+
Configuration is **passed in, never read from a global**. Nothing has to be initialised before
|
|
276
|
+
you can log, which matters most on a program's first run — the moment you most want a log and
|
|
277
|
+
are least likely to have configuration.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## API
|
|
64
282
|
|
|
65
|
-
|
|
283
|
+
| Export | What it does |
|
|
284
|
+
|---|---|
|
|
285
|
+
| `createApplicationLogger(config)` | returns an `ApplicationLogger` |
|
|
286
|
+
| `registerTransport(method, factory)` | adds a destination this package does not ship |
|
|
287
|
+
| `LogLevel`, `LogLevelIndex`, `logLevelIndex` | the level names, their order, their index |
|
|
288
|
+
| `LogMethod` | `console` · `file` · `database` · `api` |
|
|
289
|
+
| `withinLevel(level, threshold)` | is `level` within `threshold`? |
|
|
290
|
+
| `DEFAULT_MASK` | the field names masked by default |
|
|
291
|
+
| `formatLogMessage(entry, config?)` | render one line — useful in your own transport |
|
|
292
|
+
| `toLogEntry(appId, lvl, msg, …)` | build the entry a transport receives |
|
|
293
|
+
| `maskValues(value, mask?)` | mask an object without logging it |
|
|
294
|
+
| `stringify(value, config?)` | the `meta=` renderer; never throws, handles cycles |
|
|
295
|
+
| `isLogEnabled(pgm_cd, fn_cd?, act_id?)` | is `NODE_DEBUG` on for this origin? |
|
|
66
296
|
|
|
67
|
-
|
|
68
|
-
carrying `mysql` and `axios`, and register themselves:
|
|
297
|
+
### `ApplicationLogger`
|
|
69
298
|
|
|
70
299
|
```ts
|
|
71
|
-
|
|
300
|
+
logger.log(level, message, pgm_cd?, fn_cd?, act_id?, meta?); // write a line
|
|
301
|
+
logger.toLogObject(); // what is actually enabled — for a `doctor`/`status` command
|
|
302
|
+
logger.toString(); // the same, as JSON
|
|
303
|
+
logger.close(); // flush and close every transport
|
|
72
304
|
```
|
|
73
305
|
|
|
74
|
-
|
|
75
|
-
|
|
306
|
+
**Always `close()` before a short-lived process exits.** File writes are asynchronous, so
|
|
307
|
+
without it the last lines of a run — the ones explaining why it ended — are the ones most
|
|
308
|
+
likely to be lost.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Recipes
|
|
313
|
+
|
|
314
|
+
### A command-line tool
|
|
315
|
+
|
|
316
|
+
Quiet by default, everything on disk, and the user can raise the screen level themselves.
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
const logger = createApplicationLogger({
|
|
320
|
+
appId: "mycli",
|
|
321
|
+
logDir: path.join(os.homedir(), ".mycli", "logs"),
|
|
322
|
+
methods: [
|
|
323
|
+
{ METHOD: "console", LOGLEVEL: LogLevel.warn },
|
|
324
|
+
{ METHOD: "file", LOGLEVEL: LogLevel.debug },
|
|
325
|
+
],
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
process.on("exit", () => logger.close());
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### A long-running service
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
const logger = createApplicationLogger({
|
|
335
|
+
appId: "orders-api",
|
|
336
|
+
logDir: "/var/log/orders-api",
|
|
337
|
+
methods: [
|
|
338
|
+
{ METHOD: "console", LOGLEVEL: LogLevel.info },
|
|
339
|
+
{ METHOD: "file", LOGLEVEL: LogLevel.http },
|
|
340
|
+
{ METHOD: "database", LOGLEVEL: LogLevel.error }, // ignored unless registered
|
|
341
|
+
],
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Following one request across modules
|
|
346
|
+
|
|
347
|
+
Give every line of one unit of work the same `act_id`:
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
const actId = nextRequestId();
|
|
351
|
+
logger.log(LogLevel.http, "POST /orders", "orders:api:routes", "create", actId);
|
|
352
|
+
logger.log(LogLevel.debug, "reserving stock", "orders:domain:stock", "reserve", actId);
|
|
353
|
+
logger.log(LogLevel.error, "payment declined", "orders:domain:pay", "charge", actId, { code: "card_declined" });
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Then `grep ' 4471 '` returns that request and nothing else. `NODE_DEBUG` accepts an `act_id`
|
|
357
|
+
too, so a single transaction can be traced without switching on a whole module.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Troubleshooting
|
|
362
|
+
|
|
363
|
+
| What you see | Why |
|
|
364
|
+
|---|---|
|
|
365
|
+
| Nothing below `warn` appears | Expected. Set `NODE_DEBUG` — see [above](#turning-on-debug-output-in-the-field). |
|
|
366
|
+
| Log lines mixed into piped output | `console` writes to **stderr** by design. Redirect with `2>/dev/null`, or read it with `2>&1`. |
|
|
367
|
+
| No file appears | Check `logDir` exists and is writable, and that a `file` method is in `methods`. |
|
|
368
|
+
| The last lines of a run are missing | Call `logger.close()` before exit. |
|
|
369
|
+
| A field you expected to be masked is not | Masking reads **field names** in `meta`. A secret placed in the message string is not visible to it. |
|
|
370
|
+
| Your custom transport never writes | `registerTransport` must run **before** `createApplicationLogger`. |
|
|
371
|
+
| `Attempt to write logs with no transports` | `methods` is empty, or every entry names a transport nothing registered. Check `logger.toLogObject()`. |
|
|
372
|
+
| `mask` seems to have switched off the defaults | It did. Use `[...DEFAULT_MASK, "yours"]`. |
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Notes
|
|
377
|
+
|
|
378
|
+
Console output uses [winston](https://github.com/winstonjs/winston); file rotation uses
|
|
379
|
+
[winston-daily-rotate-file](https://github.com/winstonjs/winston-daily-rotate-file).
|
|
380
|
+
|
|
381
|
+
**Why `moment` is a dependency.** The logger never calls `moment`; the file rotator does
|
|
382
|
+
(winston-daily-rotate-file → file-stream-rotator → `moment ^2.29.1`). Versions 2.29.2–2.30.1
|
|
383
|
+
carry advisory [GHSA-4p3w-j4w9-5jqw](https://github.com/advisories/GHSA-4p3w-j4w9-5jqw), fixed
|
|
384
|
+
in 2.31.0. From 1.1.1 this package depends on `moment ^2.31.0` directly, so installing it
|
|
385
|
+
lifts the rotator's copy to a fixed version — also in projects whose lockfile still holds
|
|
386
|
+
2.30.1. `npm ls moment` in your project should show 2.31.x, deduped.
|
|
387
|
+
|
|
388
|
+
### Design rationale
|
|
389
|
+
|
|
390
|
+
Why this package is separate from `@svayam/svm-util`, why configuration is injected rather
|
|
391
|
+
than global, why console output goes to stderr, and what belongs in this package versus a
|
|
392
|
+
sibling — all of that is in `docs/architecture.md`.
|
|
393
|
+
|
|
394
|
+
That file lives in the **source repository, which is private to Svayam**; the link below will
|
|
395
|
+
not open for you unless you have access. Everything in this README stands on its own, and
|
|
396
|
+
nothing in the architecture document changes how the library behaves.
|
|
397
|
+
|
|
398
|
+
Svayam engineers: [`docs/architecture.md`](./docs/architecture.md).
|
|
76
399
|
|
|
77
|
-
|
|
400
|
+
### Licence and support
|
|
78
401
|
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
402
|
+
Copyright © 2026 Svayam Infoware Pvt. Ltd. Published publicly so that Svayam's own public
|
|
403
|
+
tools — and anyone installing them — can resolve it without registry configuration.
|
package/lib/cjs/logger.js
CHANGED
|
@@ -82,11 +82,20 @@ function createApplicationLogger(config) {
|
|
|
82
82
|
});
|
|
83
83
|
const effective = { mask: config_js_1.DEFAULT_MASK, ...config };
|
|
84
84
|
const log = (lvl, msg, pgm_cd, fn_cd, act_id, meta) => {
|
|
85
|
-
// ERRORS AND WARNINGS ALWAYS PASS.
|
|
86
|
-
//
|
|
87
|
-
//
|
|
85
|
+
// ERRORS AND WARNINGS ALWAYS PASS. Below them, the GATE decides — `debuglog` (the default, opt-in through
|
|
86
|
+
// NODE_DEBUG) or `level`, where each transport's own LOGLEVEL is the only threshold.
|
|
87
|
+
//
|
|
88
|
+
// WHY `level` EXISTS (2026-09-23). A CLI cannot switch `debuglog` on for itself: Node compiles NODE_DEBUG
|
|
89
|
+
// into a matcher at bootstrap, so setting the variable from inside the process — even on the first line,
|
|
90
|
+
// before any import — has no effect. An application that has DECIDED to record every run (gov: "info and
|
|
91
|
+
// above on every run, silent on screen") therefore could not, through this library: its `info` lines were
|
|
92
|
+
// dropped, and its log folders held nothing but the transport's bookkeeping file. Found when gov's six log
|
|
93
|
+
// calls turned out never to have written a line.
|
|
94
|
+
//
|
|
95
|
+
// The default is unchanged, so nothing that relies on quiet-by-default changes.
|
|
88
96
|
const always = levels_js_1.LogLevelIndex[lvl] <= levels_js_1.LogLevelIndex[levels_js_1.LogLevel.warn];
|
|
89
|
-
|
|
97
|
+
const gated = (effective.gate ?? "debuglog") === "debuglog";
|
|
98
|
+
if (!always && gated && !(0, format_js_1.isLogEnabled)(pgm_cd, fn_cd, act_id))
|
|
90
99
|
return;
|
|
91
100
|
winston.log((0, format_js_1.toLogEntry)(effective.appId, lvl, msg, pgm_cd, fn_cd, act_id, meta));
|
|
92
101
|
};
|
package/lib/esm/config.d.ts
CHANGED
|
@@ -64,6 +64,16 @@ export interface LoggerConfig {
|
|
|
64
64
|
* is copied, and it is sent to other people precisely when something has gone wrong.
|
|
65
65
|
*/
|
|
66
66
|
readonly mask?: readonly string[];
|
|
67
|
+
/**
|
|
68
|
+
* What decides whether a line BELOW `warn` is written.
|
|
69
|
+
*
|
|
70
|
+
* `debuglog` (default) — opt-in per origin through `NODE_DEBUG` (POL-425): quiet until someone in front of
|
|
71
|
+
* the problem switches it on, with no rebuild and no release.
|
|
72
|
+
* `level` — each transport's own LOGLEVEL is the threshold, and nothing else. For an
|
|
73
|
+
* application that records every run by decision rather than by environment: a
|
|
74
|
+
* process cannot enable `debuglog` for itself (Node reads NODE_DEBUG at bootstrap).
|
|
75
|
+
*/
|
|
76
|
+
readonly gate?: "debuglog" | "level";
|
|
67
77
|
/** Depth and length limits for `meta`, so one object cannot become the whole file. */
|
|
68
78
|
readonly inspect?: {
|
|
69
79
|
depth?: number;
|
package/lib/esm/logger.js
CHANGED
|
@@ -75,11 +75,20 @@ export function createApplicationLogger(config) {
|
|
|
75
75
|
});
|
|
76
76
|
const effective = { mask: DEFAULT_MASK, ...config };
|
|
77
77
|
const log = (lvl, msg, pgm_cd, fn_cd, act_id, meta) => {
|
|
78
|
-
// ERRORS AND WARNINGS ALWAYS PASS.
|
|
79
|
-
//
|
|
80
|
-
//
|
|
78
|
+
// ERRORS AND WARNINGS ALWAYS PASS. Below them, the GATE decides — `debuglog` (the default, opt-in through
|
|
79
|
+
// NODE_DEBUG) or `level`, where each transport's own LOGLEVEL is the only threshold.
|
|
80
|
+
//
|
|
81
|
+
// WHY `level` EXISTS (2026-09-23). A CLI cannot switch `debuglog` on for itself: Node compiles NODE_DEBUG
|
|
82
|
+
// into a matcher at bootstrap, so setting the variable from inside the process — even on the first line,
|
|
83
|
+
// before any import — has no effect. An application that has DECIDED to record every run (gov: "info and
|
|
84
|
+
// above on every run, silent on screen") therefore could not, through this library: its `info` lines were
|
|
85
|
+
// dropped, and its log folders held nothing but the transport's bookkeeping file. Found when gov's six log
|
|
86
|
+
// calls turned out never to have written a line.
|
|
87
|
+
//
|
|
88
|
+
// The default is unchanged, so nothing that relies on quiet-by-default changes.
|
|
81
89
|
const always = LogLevelIndex[lvl] <= LogLevelIndex[LogLevel.warn];
|
|
82
|
-
|
|
90
|
+
const gated = (effective.gate ?? "debuglog") === "debuglog";
|
|
91
|
+
if (!always && gated && !isLogEnabled(pgm_cd, fn_cd, act_id))
|
|
83
92
|
return;
|
|
84
93
|
winston.log(toLogEntry(effective.appId, lvl, msg, pgm_cd, fn_cd, act_id, meta));
|
|
85
94
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svayam-opensource/svm-util-log",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Svayam's shared logging framework — the logger from svm-util, with only the dependencies a logger needs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -33,9 +33,11 @@
|
|
|
33
33
|
"clean": "rm -rf lib",
|
|
34
34
|
"build": "npm run clean && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && node scripts/cjs-package-json.mjs",
|
|
35
35
|
"test": "NODE_OPTIONS='--import tsx' mocha --extensions ts 'test/**/*.test.ts'",
|
|
36
|
-
"prepublishOnly": "npm run build && npm run test"
|
|
36
|
+
"prepublishOnly": "npm run build && npm run test",
|
|
37
|
+
"audit:prod": "node scripts/audit-prod.mjs"
|
|
37
38
|
},
|
|
38
39
|
"dependencies": {
|
|
40
|
+
"moment": "^2.31.0",
|
|
39
41
|
"winston": "^3.19.0",
|
|
40
42
|
"winston-daily-rotate-file": "^5.0.0",
|
|
41
43
|
"winston-transport": "^4.9.0"
|
|
@@ -50,8 +52,8 @@
|
|
|
50
52
|
"typescript": "^5.7.2"
|
|
51
53
|
},
|
|
52
54
|
"gov": {
|
|
53
|
-
"contentSha": "
|
|
55
|
+
"contentSha": "1423a60c998aec7d8a42a16820dbe01942b77522f5c5bf3e51b58b9f0d99ebed",
|
|
54
56
|
"shaFrom": "built-ref",
|
|
55
|
-
"commit": "
|
|
57
|
+
"commit": "1d1237a45f0578e3b64f54be91aa0d1332a8045b"
|
|
56
58
|
}
|
|
57
59
|
}
|