react-native-nitro-logger 0.1.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 +20 -0
- package/NitroLogger.podspec +29 -0
- package/README.md +214 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +124 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +11 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/AndroidPlatformIo.kt +100 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/HybridFileSink.kt +292 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/HybridNativeConsoleSink.kt +23 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogFileWriter.kt +1328 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogRotationPolicy.kt +186 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogSecureFile.kt +80 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogWriterRegistry.kt +420 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/NativeConsoleWriter.kt +237 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/NitroLoggerPackage.kt +22 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/PlatformIo.kt +165 -0
- package/docs/PARITY.md +330 -0
- package/docs/PRIVACY.md +341 -0
- package/eslint-plugin/README.md +238 -0
- package/eslint-plugin/index.js +62 -0
- package/eslint-plugin/rules/literal-subsystem.js +102 -0
- package/eslint-plugin/rules/no-computed-metadata-key.js +174 -0
- package/eslint-plugin/rules/no-derived-correlation.js +171 -0
- package/eslint-plugin/rules/no-dynamic-message.js +60 -0
- package/eslint-plugin/shared.js +1948 -0
- package/ios/Gzip.swift +203 -0
- package/ios/HybridFileSink.swift +291 -0
- package/ios/HybridNativeConsoleSink.swift +20 -0
- package/ios/LogFileWriter.swift +1280 -0
- package/ios/LogSecureFile.swift +296 -0
- package/ios/LogWriterRegistry.swift +444 -0
- package/ios/NativeConsoleWriter.swift +180 -0
- package/lib/module/Logger.js +381 -0
- package/lib/module/Logger.js.map +1 -0
- package/lib/module/ScopedLogger.js +70 -0
- package/lib/module/ScopedLogger.js.map +1 -0
- package/lib/module/config.js +23 -0
- package/lib/module/config.js.map +1 -0
- package/lib/module/destinations/Batcher.js +647 -0
- package/lib/module/destinations/Batcher.js.map +1 -0
- package/lib/module/destinations/ConsoleDestination.js +51 -0
- package/lib/module/destinations/ConsoleDestination.js.map +1 -0
- package/lib/module/destinations/FileDestination.js +360 -0
- package/lib/module/destinations/FileDestination.js.map +1 -0
- package/lib/module/destinations/NativeConsoleDestination.js +170 -0
- package/lib/module/destinations/NativeConsoleDestination.js.map +1 -0
- package/lib/module/destinations/types.js +4 -0
- package/lib/module/destinations/types.js.map +1 -0
- package/lib/module/formatters/DefaultFormatter.js +136 -0
- package/lib/module/formatters/DefaultFormatter.js.map +1 -0
- package/lib/module/formatters/JsonLinesFormatter.js +162 -0
- package/lib/module/formatters/JsonLinesFormatter.js.map +1 -0
- package/lib/module/formatters/timestamp.js +15 -0
- package/lib/module/formatters/timestamp.js.map +1 -0
- package/lib/module/formatters/types.js +4 -0
- package/lib/module/formatters/types.js.map +1 -0
- package/lib/module/index.js +31 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/integrations/appState.js +81 -0
- package/lib/module/integrations/appState.js.map +1 -0
- package/lib/module/integrations/errorHandler.js +166 -0
- package/lib/module/integrations/errorHandler.js.map +1 -0
- package/lib/module/integrations/sanitizeError.js +238 -0
- package/lib/module/integrations/sanitizeError.js.map +1 -0
- package/lib/module/levels.js +35 -0
- package/lib/module/levels.js.map +1 -0
- package/lib/module/metadata.js +52 -0
- package/lib/module/metadata.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/privacy.js +349 -0
- package/lib/module/privacy.js.map +1 -0
- package/lib/module/specs/FileSink.nitro.js +4 -0
- package/lib/module/specs/FileSink.nitro.js.map +1 -0
- package/lib/module/specs/NativeConsoleSink.nitro.js +4 -0
- package/lib/module/specs/NativeConsoleSink.nitro.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utf8.js +42 -0
- package/lib/module/utf8.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/Logger.d.ts +122 -0
- package/lib/typescript/src/Logger.d.ts.map +1 -0
- package/lib/typescript/src/ScopedLogger.d.ts +36 -0
- package/lib/typescript/src/ScopedLogger.d.ts.map +1 -0
- package/lib/typescript/src/config.d.ts +12 -0
- package/lib/typescript/src/config.d.ts.map +1 -0
- package/lib/typescript/src/destinations/Batcher.d.ts +244 -0
- package/lib/typescript/src/destinations/Batcher.d.ts.map +1 -0
- package/lib/typescript/src/destinations/ConsoleDestination.d.ts +27 -0
- package/lib/typescript/src/destinations/ConsoleDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/FileDestination.d.ts +200 -0
- package/lib/typescript/src/destinations/FileDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts +86 -0
- package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/types.d.ts +19 -0
- package/lib/typescript/src/destinations/types.d.ts.map +1 -0
- package/lib/typescript/src/formatters/DefaultFormatter.d.ts +22 -0
- package/lib/typescript/src/formatters/DefaultFormatter.d.ts.map +1 -0
- package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts +73 -0
- package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts.map +1 -0
- package/lib/typescript/src/formatters/timestamp.d.ts +6 -0
- package/lib/typescript/src/formatters/timestamp.d.ts.map +1 -0
- package/lib/typescript/src/formatters/types.d.ts +27 -0
- package/lib/typescript/src/formatters/types.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +34 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/integrations/appState.d.ts +33 -0
- package/lib/typescript/src/integrations/appState.d.ts.map +1 -0
- package/lib/typescript/src/integrations/errorHandler.d.ts +55 -0
- package/lib/typescript/src/integrations/errorHandler.d.ts.map +1 -0
- package/lib/typescript/src/integrations/sanitizeError.d.ts +89 -0
- package/lib/typescript/src/integrations/sanitizeError.d.ts.map +1 -0
- package/lib/typescript/src/levels.d.ts +9 -0
- package/lib/typescript/src/levels.d.ts.map +1 -0
- package/lib/typescript/src/metadata.d.ts +23 -0
- package/lib/typescript/src/metadata.d.ts.map +1 -0
- package/lib/typescript/src/privacy.d.ts +103 -0
- package/lib/typescript/src/privacy.d.ts.map +1 -0
- package/lib/typescript/src/specs/FileSink.nitro.d.ts +131 -0
- package/lib/typescript/src/specs/FileSink.nitro.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts +21 -0
- package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +37 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utf8.d.ts +15 -0
- package/lib/typescript/src/utf8.d.ts.map +1 -0
- package/nitro.json +33 -0
- package/nitrogen/generated/android/c++/JAppendResult.hpp +79 -0
- package/nitrogen/generated/android/c++/JClearOutcome.hpp +88 -0
- package/nitrogen/generated/android/c++/JFlushOutcome.hpp +81 -0
- package/nitrogen/generated/android/c++/JHybridFileSinkSpec.cpp +120 -0
- package/nitrogen/generated/android/c++/JHybridFileSinkSpec.hpp +69 -0
- package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.cpp +71 -0
- package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.hpp +64 -0
- package/nitrogen/generated/android/c++/JRejectReason.hpp +64 -0
- package/nitrogen/generated/android/c++/JRotationConfig.hpp +77 -0
- package/nitrogen/generated/android/c++/JSinkStatus.hpp +69 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/AppendResult.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/ClearOutcome.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/FlushOutcome.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridFileSinkSpec.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridNativeConsoleSinkSpec.kt +58 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RejectReason.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RotationConfig.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/SinkStatus.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/nitrologgerOnLoad.kt +35 -0
- package/nitrogen/generated/android/nitrologger+autolinking.cmake +83 -0
- package/nitrogen/generated/android/nitrologger+autolinking.gradle +27 -0
- package/nitrogen/generated/android/nitrologgerOnLoad.cpp +70 -0
- package/nitrogen/generated/android/nitrologgerOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/NitroLogger+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.cpp +50 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.hpp +216 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Umbrella.hpp +69 -0
- package/nitrogen/generated/ios/NitroLoggerAutolinking.mm +41 -0
- package/nitrogen/generated/ios/NitroLoggerAutolinking.swift +38 -0
- package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.hpp +150 -0
- package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.hpp +87 -0
- package/nitrogen/generated/ios/swift/AppendResult.swift +60 -0
- package/nitrogen/generated/ios/swift/ClearOutcome.swift +50 -0
- package/nitrogen/generated/ios/swift/FlushOutcome.swift +59 -0
- package/nitrogen/generated/ios/swift/HybridFileSinkSpec.swift +61 -0
- package/nitrogen/generated/ios/swift/HybridFileSinkSpec_cxx.swift +227 -0
- package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec_cxx.swift +148 -0
- package/nitrogen/generated/ios/swift/RejectReason.swift +48 -0
- package/nitrogen/generated/ios/swift/RotationConfig.swift +93 -0
- package/nitrogen/generated/ios/swift/SinkStatus.swift +44 -0
- package/nitrogen/generated/shared/c++/AppendResult.hpp +105 -0
- package/nitrogen/generated/shared/c++/ClearOutcome.hpp +96 -0
- package/nitrogen/generated/shared/c++/FlushOutcome.hpp +107 -0
- package/nitrogen/generated/shared/c++/HybridFileSinkSpec.cpp +28 -0
- package/nitrogen/generated/shared/c++/HybridFileSinkSpec.hpp +84 -0
- package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.hpp +64 -0
- package/nitrogen/generated/shared/c++/RejectReason.hpp +84 -0
- package/nitrogen/generated/shared/c++/RotationConfig.hpp +103 -0
- package/nitrogen/generated/shared/c++/SinkStatus.hpp +95 -0
- package/package.json +151 -0
- package/src/Logger.ts +417 -0
- package/src/ScopedLogger.ts +94 -0
- package/src/config.ts +25 -0
- package/src/destinations/Batcher.ts +805 -0
- package/src/destinations/ConsoleDestination.ts +60 -0
- package/src/destinations/FileDestination.ts +473 -0
- package/src/destinations/NativeConsoleDestination.ts +220 -0
- package/src/destinations/types.ts +19 -0
- package/src/formatters/DefaultFormatter.ts +150 -0
- package/src/formatters/JsonLinesFormatter.ts +195 -0
- package/src/formatters/timestamp.ts +12 -0
- package/src/formatters/types.ts +29 -0
- package/src/index.tsx +106 -0
- package/src/integrations/appState.ts +111 -0
- package/src/integrations/errorHandler.ts +223 -0
- package/src/integrations/sanitizeError.ts +287 -0
- package/src/levels.ts +35 -0
- package/src/metadata.ts +52 -0
- package/src/privacy.ts +389 -0
- package/src/specs/FileSink.nitro.ts +144 -0
- package/src/specs/NativeConsoleSink.nitro.ts +21 -0
- package/src/types.ts +45 -0
- package/src/utf8.ts +39 -0
package/src/Logger.ts
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import type { LazyMessage, LogEntry, LogLevel, LogMetadata } from './types';
|
|
2
|
+
import type { LogDestination } from './destinations/types';
|
|
3
|
+
import type { PrivacyDefault, PrivacySettings } from './privacy';
|
|
4
|
+
import { ConsoleDestination } from './destinations/ConsoleDestination';
|
|
5
|
+
import { resolveSubsystemLevel } from './config';
|
|
6
|
+
import { levelAtLeast } from './levels';
|
|
7
|
+
import {
|
|
8
|
+
buildCatalog,
|
|
9
|
+
normalizePrivacyDefault,
|
|
10
|
+
redactMetadata,
|
|
11
|
+
} from './privacy';
|
|
12
|
+
import { ScopedLogger } from './ScopedLogger';
|
|
13
|
+
|
|
14
|
+
export interface LogOptions {
|
|
15
|
+
level?: LogLevel;
|
|
16
|
+
subsystem?: string;
|
|
17
|
+
metadata?: LogMetadata;
|
|
18
|
+
/** Scope defaults, outranked by `metadata` on any key they share. Kept a
|
|
19
|
+
* separate field rather than pre-merged: redaction settles precedence per
|
|
20
|
+
* key and validates the winner before reading it, which a merge would
|
|
21
|
+
* defeat by reading everything first. `ScopedLogger` sets this. */
|
|
22
|
+
scopeMetadata?: LogMetadata;
|
|
23
|
+
correlation?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A destination is cut off after this many consecutive write failures. */
|
|
27
|
+
const MAX_CONSECUTIVE_FAILURES = 5;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A registered destination plus the label captured once, at registration.
|
|
31
|
+
* The label is never re-read afterwards: a getter that succeeds during
|
|
32
|
+
* registration and throws later would otherwise break removal, and failure
|
|
33
|
+
* accounting needs a stable key or a misbehaving destination can never be
|
|
34
|
+
* disabled.
|
|
35
|
+
*/
|
|
36
|
+
interface Registration {
|
|
37
|
+
readonly destination: LogDestination;
|
|
38
|
+
readonly label: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The logger. Access through the `Log` singleton.
|
|
43
|
+
*
|
|
44
|
+
* Everything runs on the JS thread, so unlike the Swift original there is no
|
|
45
|
+
* lock or config-snapshot machinery — plain fields mutated by the fluent
|
|
46
|
+
* methods are safe by construction.
|
|
47
|
+
*
|
|
48
|
+
* Pipeline per message: resolve effective level (exact subsystem → parent
|
|
49
|
+
* walk → global) → collect destinations passing isEnabled AND minimumLevel →
|
|
50
|
+
* return BEFORE evaluating a lazy message when none remain → evaluate once,
|
|
51
|
+
* guarded → prepare metadata (redaction seam; M2 adds privacy wrappers) →
|
|
52
|
+
* freeze → fan out with per-destination isolation.
|
|
53
|
+
*
|
|
54
|
+
* Destinations are treated as untrusted: every property read and call is
|
|
55
|
+
* guarded so one faulty implementation can neither crash the app nor starve
|
|
56
|
+
* its siblings.
|
|
57
|
+
*/
|
|
58
|
+
export class Logger {
|
|
59
|
+
private globalMinimum: LogLevel = 'debug';
|
|
60
|
+
private readonly subsystemLevels = new Map<string, LogLevel>();
|
|
61
|
+
private registrations: Registration[] = defaultRegistrations();
|
|
62
|
+
|
|
63
|
+
/** Consecutive write-failure counts per destination label. */
|
|
64
|
+
private readonly failureCounts = new Map<string, number>();
|
|
65
|
+
private readonly disabledLabels = new Set<string>();
|
|
66
|
+
|
|
67
|
+
// Privacy state. Every transition tightens; nothing here can be loosened
|
|
68
|
+
// at runtime, so a compromised or careless call site cannot turn redaction
|
|
69
|
+
// off in a shipped build.
|
|
70
|
+
private privacyDefaultValue: PrivacyDefault = 'public';
|
|
71
|
+
private privacyDefaultLocked = false;
|
|
72
|
+
private redactAll = false;
|
|
73
|
+
private keyCatalog: ReadonlySet<string> | undefined;
|
|
74
|
+
|
|
75
|
+
// ── Privacy ─────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Choose how bare (unmarked) metadata values are treated.
|
|
79
|
+
*
|
|
80
|
+
* `'public'` — the OSS default — renders them. `'private'` redacts every
|
|
81
|
+
* bare value outside dev builds unless wrapped in `pub()`, so a forgotten
|
|
82
|
+
* wrapper hides data instead of leaking it. Apps handling PHI set
|
|
83
|
+
* `'private'` in their entry point.
|
|
84
|
+
*
|
|
85
|
+
* First call wins; later calls may only tighten (`'public'` → `'private'`).
|
|
86
|
+
* A request to loosen is ignored, not obeyed. Anything that is not exactly
|
|
87
|
+
* `'public'` or `'private'` — a JS caller, JSON config, an `any` — resolves
|
|
88
|
+
* to `'private'`: ambiguity must never resolve toward disclosure.
|
|
89
|
+
*
|
|
90
|
+
* `'private'` also makes the metadata key catalog mandatory — see
|
|
91
|
+
* {@link metadataKeyCatalog}.
|
|
92
|
+
*/
|
|
93
|
+
privacyDefault(value: PrivacyDefault): this {
|
|
94
|
+
const normalized = normalizePrivacyDefault(value);
|
|
95
|
+
if (!this.privacyDefaultLocked) {
|
|
96
|
+
this.privacyDefaultValue = normalized;
|
|
97
|
+
this.privacyDefaultLocked = true;
|
|
98
|
+
} else if (normalized === 'private') {
|
|
99
|
+
this.privacyDefaultValue = 'private';
|
|
100
|
+
}
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Redact every metadata value from here on, marked or not, dev or not.
|
|
105
|
+
* One-way: there is no call that undoes it. */
|
|
106
|
+
redactAllMetadata(): this {
|
|
107
|
+
this.redactAll = true;
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Restrict metadata to an approved set of keys; anything else is dropped
|
|
113
|
+
* and counted. Rejecting computed keys is not enough on its own — a
|
|
114
|
+
* literal key like `patient123` is still PHI — so the catalog checks exact
|
|
115
|
+
* membership at runtime.
|
|
116
|
+
*
|
|
117
|
+
* Tighten-only: repeat calls intersect with the existing catalog rather
|
|
118
|
+
* than replacing it. Mandatory under a `'private'` privacy default, where
|
|
119
|
+
* an unconfigured catalog approves nothing and all metadata drops.
|
|
120
|
+
*
|
|
121
|
+
* Input is not trusted: a non-iterable, a throwing iterator, a non-string
|
|
122
|
+
* or malformed entry, or an over-long iterable all yield an empty catalog
|
|
123
|
+
* rather than an exception or a hung JS thread.
|
|
124
|
+
*/
|
|
125
|
+
metadataKeyCatalog(keys: Iterable<string>): this {
|
|
126
|
+
const incoming = buildCatalog(keys);
|
|
127
|
+
if (this.keyCatalog === undefined) {
|
|
128
|
+
this.keyCatalog = incoming;
|
|
129
|
+
} else {
|
|
130
|
+
const intersection = new Set<string>();
|
|
131
|
+
for (const key of this.keyCatalog) {
|
|
132
|
+
if (incoming.has(key)) intersection.add(key);
|
|
133
|
+
}
|
|
134
|
+
this.keyCatalog = intersection;
|
|
135
|
+
}
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private privacySettings(): PrivacySettings {
|
|
140
|
+
return {
|
|
141
|
+
privacyDefault: this.privacyDefaultValue,
|
|
142
|
+
redactAll: this.redactAll,
|
|
143
|
+
keyCatalog: this.keyCatalog,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Fluent configuration ────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/** Global minimum level; messages below it are discarded. Default 'debug'. */
|
|
150
|
+
minimumLevel(level: LogLevel): this {
|
|
151
|
+
this.globalMinimum = level;
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Minimum level for a dot-hierarchical subsystem (`network` covers
|
|
156
|
+
* `network.api` unless the child sets its own). */
|
|
157
|
+
subsystem(name: string, level: LogLevel): this {
|
|
158
|
+
this.subsystemLevels.set(name, level);
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
resetSubsystem(name: string): this {
|
|
163
|
+
this.subsystemLevels.delete(name);
|
|
164
|
+
return this;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Toggle console printing. The console destination's test sink, when set,
|
|
168
|
+
* keeps receiving lines regardless. */
|
|
169
|
+
consoleLogging(enabled: boolean): this {
|
|
170
|
+
for (const { destination } of this.registrations) {
|
|
171
|
+
if (destination instanceof ConsoleDestination) {
|
|
172
|
+
destination.printEnabled = enabled;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Register a destination. A *different* destination already holding the
|
|
180
|
+
* same label is flushed, disposed, and replaced; re-adding an instance
|
|
181
|
+
* that is already registered is a no-op, because disposing and re-pushing
|
|
182
|
+
* the same object would leave a live destination in a closed state.
|
|
183
|
+
*/
|
|
184
|
+
addDestination(destination: LogDestination): this {
|
|
185
|
+
// Identity first: an already-registered instance must not have its label
|
|
186
|
+
// getter touched again, or the capture-once invariant leaks right here.
|
|
187
|
+
if (this.registrations.some((r) => r.destination === destination)) {
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
let label: string;
|
|
191
|
+
try {
|
|
192
|
+
label = destination.label;
|
|
193
|
+
} catch {
|
|
194
|
+
warnFixed(
|
|
195
|
+
'[Logger] a destination was rejected: its label could not be read'
|
|
196
|
+
);
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
199
|
+
if (typeof label !== 'string') {
|
|
200
|
+
warnFixed(
|
|
201
|
+
'[Logger] a destination was rejected: its label is not a string'
|
|
202
|
+
);
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
205
|
+
this.removeDestination(label);
|
|
206
|
+
this.registrations.push({ destination, label });
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
removeDestination(label: string): this {
|
|
211
|
+
const index = this.registrations.findIndex((r) => r.label === label);
|
|
212
|
+
if (index >= 0) {
|
|
213
|
+
const removed = this.registrations.splice(index, 1)[0]!;
|
|
214
|
+
// Guarded separately: a failing flush must not skip dispose, or the
|
|
215
|
+
// removed destination leaks timers/handles it will never release.
|
|
216
|
+
try {
|
|
217
|
+
removed.destination.flush();
|
|
218
|
+
} catch {
|
|
219
|
+
// a failing flush must not break reconfiguration
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
removed.destination.dispose();
|
|
223
|
+
} catch {
|
|
224
|
+
// a failing dispose must not break reconfiguration
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
this.failureCounts.delete(label);
|
|
228
|
+
this.disabledLabels.delete(label);
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Drain every destination synchronously (bounded per destination). */
|
|
233
|
+
flush(deadlineMs = 2000): void {
|
|
234
|
+
for (const { destination } of this.registrations) {
|
|
235
|
+
try {
|
|
236
|
+
destination.flush(deadlineMs);
|
|
237
|
+
} catch {
|
|
238
|
+
// isolation: one failing flush must not stop the others
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── Scopes & correlation ────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
/** Short-lived random correlation ID — the encouraged way to correlate.
|
|
246
|
+
* Never derive correlation IDs from patient/visit/record identifiers. */
|
|
247
|
+
newCorrelationId(): string {
|
|
248
|
+
return Math.random().toString(36).slice(2, 10);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** A scoped logger tagging every message with a correlation ID, optional
|
|
252
|
+
* subsystem, and default metadata. Omit `correlation` to auto-generate. */
|
|
253
|
+
scoped(
|
|
254
|
+
correlation?: string,
|
|
255
|
+
subsystem?: string,
|
|
256
|
+
metadata?: LogMetadata
|
|
257
|
+
): ScopedLogger {
|
|
258
|
+
return new ScopedLogger(
|
|
259
|
+
this,
|
|
260
|
+
correlation ?? this.newCorrelationId(),
|
|
261
|
+
subsystem,
|
|
262
|
+
metadata
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Logging ─────────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
log(message: LazyMessage, options?: LogOptions): void {
|
|
269
|
+
this.logMessage(message, options);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
verbose(
|
|
273
|
+
message: LazyMessage,
|
|
274
|
+
metadata?: LogMetadata,
|
|
275
|
+
subsystem?: string
|
|
276
|
+
): void {
|
|
277
|
+
this.logMessage(message, { level: 'verbose', metadata, subsystem });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
debug(
|
|
281
|
+
message: LazyMessage,
|
|
282
|
+
metadata?: LogMetadata,
|
|
283
|
+
subsystem?: string
|
|
284
|
+
): void {
|
|
285
|
+
this.logMessage(message, { level: 'debug', metadata, subsystem });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
info(message: LazyMessage, metadata?: LogMetadata, subsystem?: string): void {
|
|
289
|
+
this.logMessage(message, { level: 'info', metadata, subsystem });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
warning(
|
|
293
|
+
message: LazyMessage,
|
|
294
|
+
metadata?: LogMetadata,
|
|
295
|
+
subsystem?: string
|
|
296
|
+
): void {
|
|
297
|
+
this.logMessage(message, { level: 'warning', metadata, subsystem });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
error(
|
|
301
|
+
message: LazyMessage,
|
|
302
|
+
metadata?: LogMetadata,
|
|
303
|
+
subsystem?: string
|
|
304
|
+
): void {
|
|
305
|
+
this.logMessage(message, { level: 'error', metadata, subsystem });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Marks incomplete work; highest severity so it always surfaces. */
|
|
309
|
+
todo(message: LazyMessage, metadata?: LogMetadata, subsystem?: string): void {
|
|
310
|
+
this.logMessage(message, { level: 'todo', metadata, subsystem });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Integration entry point (ScopedLogger, error handler, bridges). */
|
|
314
|
+
logMessage(message: LazyMessage, options?: LogOptions): void {
|
|
315
|
+
const level = options?.level ?? 'info';
|
|
316
|
+
const subsystem = options?.subsystem;
|
|
317
|
+
|
|
318
|
+
const effectiveMinimum =
|
|
319
|
+
(subsystem !== undefined
|
|
320
|
+
? resolveSubsystemLevel(this.subsystemLevels, subsystem)
|
|
321
|
+
: undefined) ?? this.globalMinimum;
|
|
322
|
+
if (!levelAtLeast(level, effectiveMinimum)) return;
|
|
323
|
+
|
|
324
|
+
// Eligibility BEFORE message evaluation: a lazy thunk must not run when
|
|
325
|
+
// nothing will receive the entry. isEnabled/minimumLevel may be throwing
|
|
326
|
+
// getters, so each read is isolated and charged to the stable label.
|
|
327
|
+
const eligible: Registration[] = [];
|
|
328
|
+
for (const registration of this.registrations) {
|
|
329
|
+
const { destination, label } = registration;
|
|
330
|
+
if (this.disabledLabels.has(label)) continue;
|
|
331
|
+
try {
|
|
332
|
+
if (!destination.isEnabled) continue;
|
|
333
|
+
if (
|
|
334
|
+
destination.minimumLevel !== undefined &&
|
|
335
|
+
!levelAtLeast(level, destination.minimumLevel)
|
|
336
|
+
) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
this.noteFailure(label);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
eligible.push(registration);
|
|
344
|
+
}
|
|
345
|
+
if (eligible.length === 0) return;
|
|
346
|
+
|
|
347
|
+
let text: string;
|
|
348
|
+
try {
|
|
349
|
+
text = typeof message === 'function' ? message() : message;
|
|
350
|
+
} catch {
|
|
351
|
+
// Payload-free by design: the thrown value could contain anything.
|
|
352
|
+
text = '[Logger] message thunk threw';
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Frozen: the same entry fans out to every destination, so a mutating
|
|
356
|
+
// destination must not change what its siblings receive. Every field is
|
|
357
|
+
// a primitive or a frozen null-prototype object, so freezing the entry
|
|
358
|
+
// is enough — nothing reachable from it is mutable.
|
|
359
|
+
const entry: LogEntry = Object.freeze({
|
|
360
|
+
timestamp: Date.now(),
|
|
361
|
+
level,
|
|
362
|
+
message: text,
|
|
363
|
+
metadata: redactMetadata(
|
|
364
|
+
options?.scopeMetadata,
|
|
365
|
+
options?.metadata,
|
|
366
|
+
this.privacySettings()
|
|
367
|
+
),
|
|
368
|
+
correlation: options?.correlation,
|
|
369
|
+
subsystem,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
for (const { destination, label } of eligible) {
|
|
373
|
+
try {
|
|
374
|
+
destination.write(entry);
|
|
375
|
+
this.failureCounts.delete(label);
|
|
376
|
+
} catch {
|
|
377
|
+
this.noteFailure(label);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Count a consecutive failure; disable the destination at the threshold.
|
|
383
|
+
* The diagnostic is fixed text — labels are caller-controlled, so including
|
|
384
|
+
* one would break the payload-free contract and invite log injection. */
|
|
385
|
+
private noteFailure(label: string): void {
|
|
386
|
+
const failures = (this.failureCounts.get(label) ?? 0) + 1;
|
|
387
|
+
this.failureCounts.set(label, failures);
|
|
388
|
+
if (failures >= MAX_CONSECUTIVE_FAILURES) {
|
|
389
|
+
this.disabledLabels.add(label);
|
|
390
|
+
warnFixed(
|
|
391
|
+
'[Logger] a destination was disabled after repeated write failures'
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Deliberately no reset/reconfigure hook. Such a method would be a public
|
|
397
|
+
// runtime path to loosen every privacy control on the `Log` singleton, and
|
|
398
|
+
// an `@internal` tag does not remove a method from a JS build. Tests
|
|
399
|
+
// construct their own `new Logger()` instead.
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function defaultRegistrations(): Registration[] {
|
|
403
|
+
const console_ = new ConsoleDestination();
|
|
404
|
+
return [{ destination: console_, label: console_.label }];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Fixed-text diagnostic; never interpolates caller-controlled data. */
|
|
408
|
+
function warnFixed(message: string): void {
|
|
409
|
+
try {
|
|
410
|
+
console.warn(message);
|
|
411
|
+
} catch {
|
|
412
|
+
// nothing left to report to
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Global shorthand, mirroring SwiftLogger's `Log`. */
|
|
417
|
+
export const Log = new Logger();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { LazyMessage, LogLevel, LogMetadata } from './types';
|
|
2
|
+
import type { Logger } from './Logger';
|
|
3
|
+
import { safeSnapshotMetadata } from './metadata';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A lightweight logger that tags every message with a correlation ID,
|
|
7
|
+
* optional default subsystem, and default metadata. Direct port of
|
|
8
|
+
* SwiftLogger's ScopedLogger: immutable, delegates all work to the parent
|
|
9
|
+
* Logger, nests.
|
|
10
|
+
*
|
|
11
|
+
* Metadata semantics: the scope's metadata merges into every message;
|
|
12
|
+
* call-site keys win on collision (a message reporting `state: 'failed'`
|
|
13
|
+
* must not be masked by the scope's `state: 'running'`). Child scopes merge
|
|
14
|
+
* into — never replace — the parent's metadata.
|
|
15
|
+
*/
|
|
16
|
+
export class ScopedLogger {
|
|
17
|
+
/** Frozen snapshot taken at construction — mutating the object the caller
|
|
18
|
+
* passed in (or this field) after the fact must not change future logs,
|
|
19
|
+
* matching SwiftLogger's value semantics. */
|
|
20
|
+
readonly metadata?: Readonly<LogMetadata>;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly logger: Logger,
|
|
24
|
+
readonly correlation: string,
|
|
25
|
+
readonly subsystem?: string,
|
|
26
|
+
metadata?: LogMetadata
|
|
27
|
+
) {
|
|
28
|
+
const snapshot = safeSnapshotMetadata(metadata);
|
|
29
|
+
this.metadata = snapshot ? Object.freeze(snapshot) : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
log(
|
|
33
|
+
message: LazyMessage,
|
|
34
|
+
level: LogLevel = 'info',
|
|
35
|
+
metadata?: LogMetadata
|
|
36
|
+
): void {
|
|
37
|
+
this.logger.logMessage(message, {
|
|
38
|
+
level,
|
|
39
|
+
subsystem: this.subsystem,
|
|
40
|
+
// Handed over unmerged: redaction settles precedence per key and
|
|
41
|
+
// validates the winner BEFORE reading it, so a getter behind a
|
|
42
|
+
// rejected key — or one the call site overrode — never runs. Merging
|
|
43
|
+
// here would read every value first and defeat that.
|
|
44
|
+
scopeMetadata: this.metadata,
|
|
45
|
+
metadata,
|
|
46
|
+
correlation: this.correlation,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
verbose(message: LazyMessage, metadata?: LogMetadata): void {
|
|
51
|
+
this.log(message, 'verbose', metadata);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
debug(message: LazyMessage, metadata?: LogMetadata): void {
|
|
55
|
+
this.log(message, 'debug', metadata);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
info(message: LazyMessage, metadata?: LogMetadata): void {
|
|
59
|
+
this.log(message, 'info', metadata);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
warning(message: LazyMessage, metadata?: LogMetadata): void {
|
|
63
|
+
this.log(message, 'warning', metadata);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
error(message: LazyMessage, metadata?: LogMetadata): void {
|
|
67
|
+
this.log(message, 'error', metadata);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
todo(message: LazyMessage, metadata?: LogMetadata): void {
|
|
71
|
+
this.log(message, 'todo', metadata);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Child scope: inherits this scope's subsystem unless overridden, merges
|
|
75
|
+
* metadata (child keys win). The merge happens at construction, like any
|
|
76
|
+
* scope snapshot — both sides are materialized here so later mutation of
|
|
77
|
+
* the caller's object cannot change what the child reports. */
|
|
78
|
+
scoped(
|
|
79
|
+
correlation: string,
|
|
80
|
+
subsystem?: string,
|
|
81
|
+
metadata?: LogMetadata
|
|
82
|
+
): ScopedLogger {
|
|
83
|
+
const child = safeSnapshotMetadata(metadata);
|
|
84
|
+
const merged = this.metadata
|
|
85
|
+
? Object.assign(Object.create(null) as LogMetadata, this.metadata, child)
|
|
86
|
+
: child;
|
|
87
|
+
return new ScopedLogger(
|
|
88
|
+
this.logger,
|
|
89
|
+
correlation,
|
|
90
|
+
subsystem ?? this.subsystem,
|
|
91
|
+
merged
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { LogLevel } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the effective minimum level for a subsystem by walking the
|
|
5
|
+
* dot-separated hierarchy: exact match, then each parent (`a.b.c` → `a.b` →
|
|
6
|
+
* `a`), then undefined so the caller falls back to the global minimum.
|
|
7
|
+
* Direct port of SwiftLogger's LoggerConfiguration.resolveSubsystemLevel.
|
|
8
|
+
*
|
|
9
|
+
* Single-threaded JS means no snapshot/locking machinery — a plain Map
|
|
10
|
+
* mutated by the fluent config methods is safe.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveSubsystemLevel(
|
|
13
|
+
subsystemLevels: ReadonlyMap<string, LogLevel>,
|
|
14
|
+
name: string
|
|
15
|
+
): LogLevel | undefined {
|
|
16
|
+
if (subsystemLevels.size === 0) return undefined;
|
|
17
|
+
let current = name;
|
|
18
|
+
for (;;) {
|
|
19
|
+
const level = subsystemLevels.get(current);
|
|
20
|
+
if (level !== undefined) return level;
|
|
21
|
+
const dot = current.lastIndexOf('.');
|
|
22
|
+
if (dot < 0) return undefined;
|
|
23
|
+
current = current.slice(0, dot);
|
|
24
|
+
}
|
|
25
|
+
}
|