1- import pino , { type Level , type Logger } from "pino" ;
1+ /**
2+ * @module logger
3+ * Provides a standardized, environment-aware logging utility for the library.
4+ *
5+ * This module configures a `pino` logger instance that can be controlled via
6+ * environment variables in Node.js or `localStorage` in the browser. It also
7+ * exposes a global API (`window.__NILLION`) for runtime log level management
8+ * in browser environments.
9+ *
10+ * @example
11+ * ```bash
12+ * # In Node.js
13+ * NILLION_LOG_LEVEL=debug node my-script.js
14+ * ```
15+ *
16+ * @example
17+ * ```ts
18+ * // In a browser's developer console
19+ * localStorage.setItem("NILLION_LOG_LEVEL", "debug");
20+ * // Or, to control all Nillion libraries on the page at once:
21+ * __NILLION.setLogLevel("debug");
22+ * ```
23+ */
24+ import pino from "pino" ;
225
3- export type LogLevel = Level | "silent" ;
26+ export type LogLevel = pino . Level | "silent" ;
427
528const LOG_LEVEL_KEY = "NILLION_LOG_LEVEL" ;
629const DEFAULT_LOG_LEVEL : LogLevel = "silent" ;
7- const VALID_LOG_LEVELS : ReadonlyArray < LogLevel > = [
30+ const VALID_LOG_LEVELS : ReadonlySet < string > = new Set < LogLevel > ( [
831 "fatal" ,
932 "error" ,
1033 "warn" ,
1134 "info" ,
1235 "debug" ,
1336 "trace" ,
1437 "silent" ,
15- ] ;
38+ ] ) ;
1639
40+ /**
41+ * A resilient wrapper around `localStorage` that fails gracefully in non-browser environments.
42+ * @internal
43+ */
1744const safeStorage = {
1845 getItem : ( key : string ) : string | null => {
1946 try {
20- // Use optional chaining for resilience in non-browser environments.
2147 return globalThis . localStorage ?. getItem ( key ) ?? null ;
2248 } catch {
2349 return null ;
@@ -27,55 +53,58 @@ const safeStorage = {
2753 try {
2854 globalThis . localStorage ?. setItem ( key , value ) ;
2955 } catch {
30- // Ignore errors if storage is disabled
56+ // Fails silently if storage is unavailable.
3157 }
3258 } ,
3359 removeItem : ( key : string ) : void => {
3460 try {
3561 globalThis . localStorage ?. removeItem ( key ) ;
3662 } catch {
37- // Ignore errors
63+ // Fails silently if storage is unavailable.
3864 }
3965 } ,
4066} ;
4167
68+ /**
69+ * Type guard to check if a value is a valid `LogLevel`.
70+ * @internal
71+ */
4272function isValidLogLevel ( value : unknown ) : value is LogLevel {
43- return (
44- typeof value === "string" && VALID_LOG_LEVELS . includes ( value as LogLevel )
45- ) ;
73+ return typeof value === "string" && VALID_LOG_LEVELS . has ( value ) ;
4674}
4775
76+ /**
77+ * Determines the initial log level with a clear order of precedence.
78+ * @internal
79+ */
4880function getInitialLogLevel ( ) : LogLevel {
49- let level = DEFAULT_LOG_LEVEL ;
50- const sources = [
51- process . env ?. [ LOG_LEVEL_KEY ] ,
52- safeStorage . getItem ( LOG_LEVEL_KEY ) ,
53- // @ts -expect-error
54- globalThis [ LOG_LEVEL_KEY ] ,
55- ] ;
56-
57- for ( const source of sources ) {
58- const levelAttempt = source ?. toLowerCase ( ) ;
59- if ( isValidLogLevel ( levelAttempt ) ) {
60- level = levelAttempt ;
61- break ;
62- }
81+ const fromEnv =
82+ typeof process !== "undefined" ? process . env [ LOG_LEVEL_KEY ] : undefined ;
83+ if ( isValidLogLevel ( fromEnv ) ) {
84+ return fromEnv ;
6385 }
6486
65- return level ;
66- }
87+ const fromStorage = safeStorage . getItem ( LOG_LEVEL_KEY ) ;
88+ if ( isValidLogLevel ( fromStorage ) ) {
89+ return fromStorage ;
90+ }
6791
68- function getLoggerForEnv ( ) : Logger < never , boolean > {
69- const level = getInitialLogLevel ( ) ;
70- return pino ( {
71- level,
72- browser : { asObject : true } ,
73- base : null ,
74- } ) ;
92+ return DEFAULT_LOG_LEVEL ;
7593}
7694
77- export const Log = getLoggerForEnv ( ) ;
95+ /**
96+ * The shared, singleton logger instance for this library module.
97+ */
98+ export const Log : pino . Logger = pino ( {
99+ level : getInitialLogLevel ( ) ,
100+ browser : { asObject : true } ,
101+ base : null ,
102+ } ) ;
78103
104+ /**
105+ * Sets the log level for this specific logger instance and persists the choice.
106+ * @param level The new log level to set.
107+ */
79108export function setLogLevel ( level : LogLevel ) : void {
80109 if ( ! isValidLogLevel ( level ) ) {
81110 console . warn ( `[Logger] Invalid log level: "${ level } ". Ignoring.` ) ;
@@ -85,28 +114,90 @@ export function setLogLevel(level: LogLevel): void {
85114 safeStorage . setItem ( LOG_LEVEL_KEY , level ) ;
86115}
87116
117+ /**
118+ * Returns the current log level of this specific logger instance.
119+ */
88120export function getLogLevel ( ) : LogLevel {
89121 return Log . level as LogLevel ;
90122}
91123
124+ /**
125+ * Removes the stored log level from `localStorage`.
126+ */
92127export function clearStoredLogLevel ( ) : void {
93128 safeStorage . removeItem ( LOG_LEVEL_KEY ) ;
94129}
95130
96- interface NillionGlobal {
97- setLogLevel : ( level : LogLevel ) => void ;
98- getLogLevel : ( ) => LogLevel ;
99- clearStoredLogLevel : ( ) => void ;
131+ /**
132+ * The interface for a single logger's control functions.
133+ * @internal
134+ */
135+ interface LoggerApi {
136+ set : ( level : LogLevel ) => void ;
137+ get : ( ) => LogLevel ;
138+ clear : ( ) => void ;
100139}
101140
102141declare global {
103- var __NILLION : NillionGlobal ;
142+ // eslint-disable-next-line no-var
143+ var __NILLION : {
144+ /**
145+ * A set of all registered Nillion logger instances on the page.
146+ * @internal
147+ */
148+ _instances : Set < LoggerApi > ;
149+ /**
150+ * Sets the log level for all Nillion libraries on the page.
151+ */
152+ setLogLevel : ( level : LogLevel ) => void ;
153+ /**
154+ * Gets the log level of the first registered Nillion library.
155+ * Note: If different libraries have different levels, this may not be representative.
156+ */
157+ getLogLevel : ( ) => LogLevel ;
158+ /**
159+ * Clears the persisted log level setting from localStorage.
160+ */
161+ clearStoredLogLevel : ( ) => void ;
162+ } ;
104163}
105164
165+ /**
166+ * Attaches a global controller to `globalThis` for managing all logger instances.
167+ * This handles the case where multiple libraries using this logger are present.
168+ * @internal
169+ */
106170if ( typeof globalThis !== "undefined" ) {
107- globalThis . __NILLION = {
108- setLogLevel,
109- getLogLevel,
110- clearStoredLogLevel,
111- } ;
171+ // Initialize the global controller only once.
172+ if ( ! globalThis . __NILLION ) {
173+ const instances = new Set < LoggerApi > ( ) ;
174+ globalThis . __NILLION = {
175+ _instances : instances ,
176+ setLogLevel : ( level : LogLevel ) => {
177+ // Broadcast the command to all registered logger instances.
178+ for ( const instance of instances ) {
179+ instance . set ( level ) ;
180+ }
181+ } ,
182+ getLogLevel : ( ) : LogLevel => {
183+ // Return the level of the first registered instance as a representative value.
184+ const first = instances . values ( ) . next ( ) . value ;
185+ return first ? first . get ( ) : getInitialLogLevel ( ) ;
186+ } ,
187+ clearStoredLogLevel : ( ) => {
188+ // This is a global action, so only one instance needs to perform it.
189+ const first = instances . values ( ) . next ( ) . value ;
190+ if ( first ) {
191+ first . clear ( ) ;
192+ }
193+ } ,
194+ } ;
195+ }
196+
197+ // Register this specific logger instance's API with the global controller.
198+ globalThis . __NILLION . _instances . add ( {
199+ set : setLogLevel ,
200+ get : getLogLevel ,
201+ clear : clearStoredLogLevel ,
202+ } ) ;
112203}
0 commit comments