widelogger 0.2.1 → 0.4.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 CHANGED
@@ -41,7 +41,7 @@ npm install widelogger
41
41
  ```ts
42
42
  import { widelogger } from "widelogger";
43
43
 
44
- const { widelog, destroy } = widelogger({
44
+ const { context, destroy } = widelogger({
45
45
  service: "checkout-api",
46
46
  defaultEventName: "http_request",
47
47
  version: "1.0.0",
@@ -49,15 +49,23 @@ const { widelog, destroy } = widelogger({
49
49
  });
50
50
  ```
51
51
 
52
+ `widelogger()` returns a `context` function to wrap request lifecycles and a `destroy` function for graceful shutdown. To log fields, import `widelog` directly from the package — it works anywhere inside a `context()` call thanks to `AsyncLocalStorage`.
53
+
54
+ ```ts
55
+ import { widelog } from "widelogger";
56
+
57
+ widelog.set("user.id", userId);
58
+ ```
59
+
52
60
  ### Express
53
61
 
54
- Create a shared logger instance, use middleware to wrap requests in a context, and accumulate fields from anywhere in your codebase.
62
+ Create a logger instance once, use middleware to wrap requests in a context, and accumulate fields from anywhere in your codebase by importing `widelog` directly.
55
63
 
56
64
  ```ts
57
65
  // src/logger.ts
58
66
  import { widelogger } from "widelogger";
59
67
 
60
- export const { widelog, destroy } = widelogger({
68
+ export const { context, destroy } = widelogger({
61
69
  service: "checkout-api",
62
70
  defaultEventName: "http_request",
63
71
  });
@@ -65,10 +73,11 @@ export const { widelog, destroy } = widelogger({
65
73
 
66
74
  ```ts
67
75
  // src/middleware/logging.ts
68
- import { widelog } from "../logger";
76
+ import { widelog } from "widelogger";
77
+ import { context } from "../logger";
69
78
 
70
79
  export const logging = (request, response, next) => {
71
- widelog.context(
80
+ context(
72
81
  () =>
73
82
  new Promise((resolve) => {
74
83
  widelog.set("method", request.method);
@@ -90,17 +99,16 @@ export const logging = (request, response, next) => {
90
99
 
91
100
  ```ts
92
101
  // src/routes/checkout.ts
93
- import { widelog } from "../logger";
102
+ import { widelog } from "widelogger";
94
103
 
95
- export const checkout = (request, response) => {
104
+ export const checkout = async (request, response) => {
96
105
  const { userId } = request.body;
97
106
 
98
- widelog.set("user.id", userId);
99
- widelog.set("user.plan", "premium");
107
+ widelog.setFields({ user: { id: userId, plan: "premium" } });
100
108
 
101
- widelog.time.start("db_ms");
102
- const order = await processOrder(userId);
103
- widelog.time.stop("db_ms");
109
+ const order = await widelog.time.measure("db_ms", () =>
110
+ processOrder(userId),
111
+ );
104
112
 
105
113
  widelog.set("order.total_cents", order.totalCents);
106
114
  widelog.count("order.items", order.itemCount);
@@ -109,7 +117,7 @@ export const checkout = (request, response) => {
109
117
  };
110
118
  ```
111
119
 
112
- The handler doesn't need to know about context setup or flushing — it just imports `widelog` and adds fields. `AsyncLocalStorage` ensures concurrent requests never leak into each other.
120
+ The handler doesn't need to know about context setup or flushing — it just imports `widelog` from the package and adds fields. `AsyncLocalStorage` ensures concurrent requests never leak into each other.
113
121
 
114
122
  ### Bun
115
123
 
@@ -119,7 +127,7 @@ The same pattern works with Bun's built-in server.
119
127
  // src/logger.ts
120
128
  import { widelogger } from "widelogger";
121
129
 
122
- export const { widelog, destroy } = widelogger({
130
+ export const { context, destroy } = widelogger({
123
131
  service: "checkout-api",
124
132
  defaultEventName: "http_request",
125
133
  });
@@ -127,17 +135,16 @@ export const { widelog, destroy } = widelogger({
127
135
 
128
136
  ```ts
129
137
  // src/routes/checkout.ts
130
- import { widelog } from "../logger";
138
+ import { widelog } from "widelogger";
131
139
 
132
140
  export const checkout = async (request: Request) => {
133
141
  const { userId } = await request.json();
134
142
 
135
- widelog.set("user.id", userId);
136
- widelog.set("user.plan", "premium");
143
+ widelog.setFields({ user: { id: userId, plan: "premium" } });
137
144
 
138
- widelog.time.start("db_ms");
139
- const order = await processOrder(userId);
140
- widelog.time.stop("db_ms");
145
+ const order = await widelog.time.measure("db_ms", () =>
146
+ processOrder(userId),
147
+ );
141
148
 
142
149
  widelog.set("order.total_cents", order.totalCents);
143
150
  widelog.count("order.items", order.itemCount);
@@ -149,12 +156,13 @@ export const checkout = async (request: Request) => {
149
156
  ```ts
150
157
  // src/server.ts
151
158
  import { serve } from "bun";
152
- import { widelog } from "./logger";
159
+ import { widelog } from "widelogger";
160
+ import { context } from "./logger";
153
161
  import { checkout } from "./routes/checkout";
154
162
 
155
163
  serve({
156
164
  fetch: (request) =>
157
- widelog.context(async () => {
165
+ context(async () => {
158
166
  const url = new URL(request.url);
159
167
  widelog.set("method", request.method);
160
168
  widelog.set("path", url.pathname);
@@ -192,6 +200,31 @@ widelog.set("user.plan", "premium");
192
200
  // Output: { user: { id: "usr_123", plan: "premium" } }
193
201
  ```
194
202
 
203
+ ### Bulk Fields
204
+
205
+ `setFields` accepts a nested object and recursively flattens it into individual `set` calls. Non-primitive values (other than plain objects) are silently ignored.
206
+
207
+ ```ts
208
+ widelog.setFields({
209
+ user: { id: "usr_123", plan: "premium" },
210
+ status_code: 200,
211
+ });
212
+ // Equivalent to:
213
+ // widelog.set("user.id", "usr_123");
214
+ // widelog.set("user.plan", "premium");
215
+ // widelog.set("status_code", 200);
216
+ ```
217
+
218
+ ### Measured Timing
219
+
220
+ `time.measure` wraps a sync or async callback, automatically recording start and stop times. It returns the callback's result and still records timing even if the callback throws.
221
+
222
+ ```ts
223
+ const order = await widelog.time.measure("db_ms", () =>
224
+ processOrder(userId),
225
+ );
226
+ ```
227
+
195
228
  ### Log Routing
196
229
 
197
230
  Events with `status_code >= 500` or `outcome === "error"` are emitted at `error` level. Everything else is `info`. In development, logs are pretty-printed; in production, they're structured JSON.
@@ -200,7 +233,7 @@ Events with `status_code >= 500` or `outcome === "error"` are emitted at `error`
200
233
 
201
234
  ### `widelogger(options)`
202
235
 
203
- Creates a logger instance. Returns `{ widelog, destroy }`.
236
+ Creates a logger instance. Returns `{ context, destroy }`.
204
237
 
205
238
  | Option | Type | Description |
206
239
  |--------|------|-------------|
@@ -212,18 +245,25 @@ Creates a logger instance. Returns `{ widelog, destroy }`.
212
245
  | `environment` | `string` | Environment name (defaults to `NODE_ENV`) |
213
246
  | `level` | `string` | Log level (defaults to `LOG_LEVEL` env or `"info"`) |
214
247
 
248
+ ### `context(fn)`
249
+
250
+ Run a function inside an isolated async context. All `widelog` calls within this function (and any functions it calls) are scoped to this context. Supports both sync and async callbacks.
251
+
215
252
  ### `widelog`
216
253
 
254
+ Imported directly from `"widelogger"`. All methods operate on the current async context established by `context()`.
255
+
217
256
  | Method | Description |
218
257
  |--------|-------------|
219
- | `context(fn)` | Run a function in an isolated async context |
220
258
  | `set(key, value)` | Set a field value (last write wins) |
259
+ | `setFields(fields)` | Recursively flatten a nested object into dotted-key `set` calls |
221
260
  | `count(key, amount?)` | Increment a counter (default +1) |
222
261
  | `append(key, value)` | Append a value to an array |
223
262
  | `max(key, value)` | Track the maximum value for a key |
224
263
  | `min(key, value)` | Track the minimum value for a key |
225
264
  | `time.start(key)` | Start a timer |
226
265
  | `time.stop(key)` | Stop a timer and record elapsed ms |
266
+ | `time.measure(key, fn)` | Time a sync or async callback, returns the callback's result |
227
267
  | `errorFields(error, opts?)` | Extract error name, message, and stack |
228
268
  | `flush()` | Aggregate all operations and emit the event |
229
269
 
package/dist/index.d.ts CHANGED
@@ -12,24 +12,29 @@ export interface ErrorFieldsOptions {
12
12
  prefix?: string;
13
13
  includeStack?: boolean;
14
14
  }
15
+ declare function measure<K extends string, T>(key: DottedKey<K>, callback: () => Promise<T>): Promise<T>;
16
+ declare function measure<K extends string, T>(key: DottedKey<K>, callback: () => T): T;
17
+ export declare const widelog: {
18
+ set: <K extends string>(key: DottedKey<K>, value: FieldValue) => void;
19
+ setFields: (fields: Record<string, unknown>) => void;
20
+ count: <K extends string>(key: DottedKey<K>, amount?: number) => void;
21
+ append: <K extends string>(key: DottedKey<K>, value: FieldValue) => void;
22
+ max: <K extends string>(key: DottedKey<K>, value: number) => void;
23
+ min: <K extends string>(key: DottedKey<K>, value: number) => void;
24
+ time: {
25
+ start: <K extends string>(key: DottedKey<K>) => void;
26
+ stop: <K extends string>(key: DottedKey<K>) => void;
27
+ measure: typeof measure;
28
+ };
29
+ errorFields: (error: unknown, options?: ErrorFieldsOptions) => void;
30
+ flush: () => void;
31
+ };
15
32
  export declare const widelogger: (options: WideloggerOptions) => {
16
- widelog: {
17
- set: <K extends string>(key: DottedKey<K>, value: FieldValue) => void;
18
- count: <K extends string>(key: DottedKey<K>, amount?: number) => void;
19
- append: <K extends string>(key: DottedKey<K>, value: FieldValue) => void;
20
- max: <K extends string>(key: DottedKey<K>, value: number) => void;
21
- min: <K extends string>(key: DottedKey<K>, value: number) => void;
22
- time: {
23
- start: <K extends string>(key: DottedKey<K>) => void;
24
- stop: <K extends string>(key: DottedKey<K>) => void;
25
- };
26
- errorFields: (error: unknown, options?: ErrorFieldsOptions) => void;
27
- flush: () => void;
28
- context: {
29
- <T>(callback: () => Promise<T>): Promise<T>;
30
- <T>(callback: () => T): T;
31
- };
33
+ context: {
34
+ <T>(callback: () => Promise<T>): Promise<T>;
35
+ <T>(callback: () => T): T;
32
36
  };
33
37
  destroy: () => Promise<void>;
34
38
  };
35
- export type Widelog = ReturnType<typeof widelogger>["widelog"];
39
+ export type Widelog = typeof widelog;
40
+ export {};
package/dist/index.js CHANGED
@@ -115,6 +115,8 @@ var flush = (context) => {
115
115
  };
116
116
 
117
117
  // src/index.ts
118
+ var isFieldValue = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean";
119
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
118
120
  function getErrorFields(error, includeStack = true) {
119
121
  if (error instanceof Error) {
120
122
  return {
@@ -134,8 +136,112 @@ function getErrorFields(error, includeStack = true) {
134
136
  error_message: "Unknown error"
135
137
  };
136
138
  }
139
+ var storage = new AsyncLocalStorage;
140
+ function pushOp(operation) {
141
+ storage.getStore()?.operations.push(operation);
142
+ }
143
+ function applyFields(operations, fields, parentKey) {
144
+ for (const key of Object.keys(fields)) {
145
+ const value = fields[key];
146
+ const fullKey = parentKey ? `${parentKey}.${key}` : key;
147
+ if (isFieldValue(value)) {
148
+ operations.push({ operation: "set", key: fullKey, value });
149
+ continue;
150
+ }
151
+ if (isRecord2(value)) {
152
+ applyFields(operations, value, fullKey);
153
+ }
154
+ }
155
+ }
156
+ function measure(key, callback) {
157
+ const operations = storage.getStore()?.operations;
158
+ operations?.push({ operation: "time.start", key, time: performance.now() });
159
+ let result;
160
+ try {
161
+ result = callback();
162
+ } catch (error) {
163
+ operations?.push({ operation: "time.stop", key, time: performance.now() });
164
+ throw error;
165
+ }
166
+ if (result instanceof Promise) {
167
+ return result.finally(() => {
168
+ operations?.push({
169
+ operation: "time.stop",
170
+ key,
171
+ time: performance.now()
172
+ });
173
+ });
174
+ }
175
+ operations?.push({ operation: "time.stop", key, time: performance.now() });
176
+ return result;
177
+ }
178
+ var widelog = {
179
+ set: (key, value) => {
180
+ pushOp({ operation: "set", key, value });
181
+ },
182
+ setFields: (fields) => {
183
+ const operations = storage.getStore()?.operations;
184
+ if (operations) {
185
+ applyFields(operations, fields);
186
+ }
187
+ },
188
+ count: (key, amount = 1) => {
189
+ pushOp({ operation: "count", key, amount });
190
+ },
191
+ append: (key, value) => {
192
+ pushOp({ operation: "append", key, value });
193
+ },
194
+ max: (key, value) => {
195
+ pushOp({ operation: "max", key, value });
196
+ },
197
+ min: (key, value) => {
198
+ pushOp({ operation: "min", key, value });
199
+ },
200
+ time: {
201
+ start: (key) => {
202
+ pushOp({ operation: "time.start", key, time: performance.now() });
203
+ },
204
+ stop: (key) => {
205
+ pushOp({ operation: "time.stop", key, time: performance.now() });
206
+ },
207
+ measure
208
+ },
209
+ errorFields: (error, options = {}) => {
210
+ const context = storage.getStore();
211
+ if (!context) {
212
+ return;
213
+ }
214
+ const prefix = options.prefix ?? "error";
215
+ const fields = getErrorFields(error, options.includeStack ?? true);
216
+ context.operations.push({
217
+ operation: "set",
218
+ key: `${prefix}.error_name`,
219
+ value: fields.error_name
220
+ }, {
221
+ operation: "set",
222
+ key: `${prefix}.error_message`,
223
+ value: fields.error_message
224
+ });
225
+ if (fields.error_stack !== undefined) {
226
+ context.operations.push({
227
+ operation: "set",
228
+ key: `${prefix}.error_stack`,
229
+ value: fields.error_stack
230
+ });
231
+ }
232
+ },
233
+ flush: () => {
234
+ const store = storage.getStore();
235
+ if (!store || store.operations.length === 0) {
236
+ return;
237
+ }
238
+ const event = flush(store);
239
+ store.transport(event);
240
+ }
241
+ };
137
242
  var widelogger = (options) => {
138
- const environment = options.environment ?? "development" ?? "development";
243
+ const nodeEnvironment = typeof process.env === "object" ? "development" : undefined;
244
+ const environment = options.environment ?? nodeEnvironment ?? "development";
139
245
  const isDevelopment = environment !== "production";
140
246
  const pinoTransport = isDevelopment ? pino.transport({
141
247
  target: "pino-pretty",
@@ -157,28 +263,25 @@ var widelogger = (options) => {
157
263
  environment
158
264
  }
159
265
  }, pinoTransport);
160
- const storage = new AsyncLocalStorage;
266
+ const defaultEventName = options.defaultEventName;
161
267
  const transport = (event) => {
162
- if (Object.keys(event).length === 0) {
163
- return;
164
- }
165
268
  const statusCode = typeof event.status_code === "number" ? event.status_code : undefined;
166
269
  const isError = statusCode !== undefined ? statusCode >= 500 : event.outcome === "error";
167
- const payload = { event_name: options.defaultEventName, ...event };
270
+ event.event_name = defaultEventName;
168
271
  if (isError) {
169
- logger.error(payload);
272
+ logger.error(event);
170
273
  return;
171
274
  }
172
- logger.info(payload);
275
+ logger.info(event);
173
276
  };
174
277
  const clearContext = () => {
175
- const context = storage.getStore();
176
- if (context && context.operations.length > 0) {
177
- context.operations = [];
278
+ const context2 = storage.getStore();
279
+ if (context2 && context2.operations.length > 0) {
280
+ context2.operations = [];
178
281
  }
179
282
  };
180
- function runContext(callback) {
181
- return storage.run({ operations: [] }, () => {
283
+ function context(callback) {
284
+ return storage.run({ operations: [], transport }, () => {
182
285
  let result;
183
286
  try {
184
287
  result = callback();
@@ -193,62 +296,6 @@ var widelogger = (options) => {
193
296
  return result;
194
297
  });
195
298
  }
196
- const widelog = {
197
- set: (key, value) => {
198
- storage.getStore()?.operations.push({ operation: "set", key, value });
199
- },
200
- count: (key, amount = 1) => {
201
- storage.getStore()?.operations.push({ operation: "count", key, amount });
202
- },
203
- append: (key, value) => {
204
- storage.getStore()?.operations.push({ operation: "append", key, value });
205
- },
206
- max: (key, value) => {
207
- storage.getStore()?.operations.push({ operation: "max", key, value });
208
- },
209
- min: (key, value) => {
210
- storage.getStore()?.operations.push({ operation: "min", key, value });
211
- },
212
- time: {
213
- start: (key) => {
214
- storage.getStore()?.operations.push({
215
- operation: "time.start",
216
- key,
217
- time: performance.now()
218
- });
219
- },
220
- stop: (key) => {
221
- storage.getStore()?.operations.push({
222
- operation: "time.stop",
223
- key,
224
- time: performance.now()
225
- });
226
- }
227
- },
228
- errorFields: (error, options2 = {}) => {
229
- const context = storage.getStore();
230
- if (!context) {
231
- return;
232
- }
233
- const prefix = options2.prefix ?? "error";
234
- const fields = getErrorFields(error, options2.includeStack ?? true);
235
- const nameKey = `${prefix}.error_name`;
236
- const messageKey = `${prefix}.error_message`;
237
- context.operations.push({ operation: "set", key: nameKey, value: fields.error_name }, { operation: "set", key: messageKey, value: fields.error_message });
238
- if (fields.error_stack !== undefined) {
239
- context.operations.push({
240
- operation: "set",
241
- key: `${prefix}.error_stack`,
242
- value: fields.error_stack
243
- });
244
- }
245
- },
246
- flush: () => {
247
- const event = flush(storage.getStore());
248
- transport(event);
249
- },
250
- context: runContext
251
- };
252
299
  const destroy = async () => {
253
300
  await new Promise((resolve) => {
254
301
  logger.flush(() => resolve());
@@ -257,8 +304,9 @@ var widelogger = (options) => {
257
304
  pinoTransport.end();
258
305
  }
259
306
  };
260
- return { widelog, destroy };
307
+ return { context, destroy };
261
308
  };
262
309
  export {
263
- widelogger
310
+ widelogger,
311
+ widelog
264
312
  };
package/dist/types.d.ts CHANGED
@@ -36,5 +36,6 @@ export type Operation = {
36
36
  };
37
37
  export interface Context {
38
38
  operations: Operation[];
39
+ transport: (event: Record<string, unknown>) => void;
39
40
  }
40
41
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "widelogger",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,7 +9,8 @@
9
9
  "exports": {
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js"
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
13
14
  }
14
15
  },
15
16
  "repository": {