widelogger 0.3.0 → 0.5.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
@@ -104,12 +104,11 @@ import { widelog } from "widelogger";
104
104
  export const checkout = async (request, response) => {
105
105
  const { userId } = request.body;
106
106
 
107
- widelog.set("user.id", userId);
108
- widelog.set("user.plan", "premium");
107
+ widelog.setFields({ user: { id: userId, plan: "premium" } });
109
108
 
110
- widelog.time.start("db_ms");
111
- const order = await processOrder(userId);
112
- widelog.time.stop("db_ms");
109
+ const order = await widelog.time.measure("db_ms", () =>
110
+ processOrder(userId),
111
+ );
113
112
 
114
113
  widelog.set("order.total_cents", order.totalCents);
115
114
  widelog.count("order.items", order.itemCount);
@@ -141,12 +140,11 @@ import { widelog } from "widelogger";
141
140
  export const checkout = async (request: Request) => {
142
141
  const { userId } = await request.json();
143
142
 
144
- widelog.set("user.id", userId);
145
- widelog.set("user.plan", "premium");
143
+ widelog.setFields({ user: { id: userId, plan: "premium" } });
146
144
 
147
- widelog.time.start("db_ms");
148
- const order = await processOrder(userId);
149
- widelog.time.stop("db_ms");
145
+ const order = await widelog.time.measure("db_ms", () =>
146
+ processOrder(userId),
147
+ );
150
148
 
151
149
  widelog.set("order.total_cents", order.totalCents);
152
150
  widelog.count("order.items", order.itemCount);
@@ -202,6 +200,31 @@ widelog.set("user.plan", "premium");
202
200
  // Output: { user: { id: "usr_123", plan: "premium" } }
203
201
  ```
204
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
+
205
228
  ### Log Routing
206
229
 
207
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.
@@ -233,12 +256,14 @@ Imported directly from `"widelogger"`. All methods operate on the current async
233
256
  | Method | Description |
234
257
  |--------|-------------|
235
258
  | `set(key, value)` | Set a field value (last write wins) |
259
+ | `setFields(fields)` | Recursively flatten a nested object into dotted-key `set` calls |
236
260
  | `count(key, amount?)` | Increment a counter (default +1) |
237
261
  | `append(key, value)` | Append a value to an array |
238
262
  | `max(key, value)` | Track the maximum value for a key |
239
263
  | `min(key, value)` | Track the minimum value for a key |
240
264
  | `time.start(key)` | Start a timer |
241
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 |
242
267
  | `errorFields(error, opts?)` | Extract error name, message, and stack |
243
268
  | `flush()` | Aggregate all operations and emit the event |
244
269
 
package/dist/index.d.ts CHANGED
@@ -11,9 +11,18 @@ export interface WideloggerOptions {
11
11
  export interface ErrorFieldsOptions {
12
12
  prefix?: string;
13
13
  includeStack?: boolean;
14
+ slug?: string;
15
+ retriable?: boolean;
16
+ requiresReauth?: boolean;
14
17
  }
18
+ interface StickyHandle {
19
+ sticky: () => void;
20
+ }
21
+ declare function measure<K extends string, T>(key: DottedKey<K>, callback: () => Promise<T>): Promise<T>;
22
+ declare function measure<K extends string, T>(key: DottedKey<K>, callback: () => T): T;
15
23
  export declare const widelog: {
16
- set: <K extends string>(key: DottedKey<K>, value: FieldValue) => void;
24
+ set: <K extends string>(key: DottedKey<K>, value: FieldValue) => StickyHandle;
25
+ setFields: (fields: Record<string, unknown>) => StickyHandle;
17
26
  count: <K extends string>(key: DottedKey<K>, amount?: number) => void;
18
27
  append: <K extends string>(key: DottedKey<K>, value: FieldValue) => void;
19
28
  max: <K extends string>(key: DottedKey<K>, value: number) => void;
@@ -21,6 +30,7 @@ export declare const widelog: {
21
30
  time: {
22
31
  start: <K extends string>(key: DottedKey<K>) => void;
23
32
  stop: <K extends string>(key: DottedKey<K>) => void;
33
+ measure: typeof measure;
24
34
  };
25
35
  errorFields: (error: unknown, options?: ErrorFieldsOptions) => void;
26
36
  flush: () => void;
@@ -33,3 +43,4 @@ export declare const widelogger: (options: WideloggerOptions) => {
33
43
  destroy: () => Promise<void>;
34
44
  };
35
45
  export type Widelog = typeof widelog;
46
+ export {};
package/dist/index.js CHANGED
@@ -105,7 +105,15 @@ var flush = (context) => {
105
105
  if (!context) {
106
106
  return {};
107
107
  }
108
+ const hasStickyOperations = context.stickyOperations.length > 0;
109
+ const hasOperations = context.operations.length > 0;
110
+ if (!(hasStickyOperations || hasOperations)) {
111
+ return {};
112
+ }
108
113
  const agg = createAggregators();
114
+ for (const entry of context.stickyOperations) {
115
+ processOperation(agg, entry);
116
+ }
109
117
  for (const entry of context.operations) {
110
118
  processOperation(agg, entry);
111
119
  }
@@ -115,6 +123,9 @@ var flush = (context) => {
115
123
  };
116
124
 
117
125
  // src/index.ts
126
+ var isFieldValue = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean";
127
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
128
+ var isFieldValueArray = (value) => Array.isArray(value) && value.every(isFieldValue);
118
129
  function getErrorFields(error, includeStack = true) {
119
130
  if (error instanceof Error) {
120
131
  return {
@@ -134,38 +145,112 @@ function getErrorFields(error, includeStack = true) {
134
145
  error_message: "Unknown error"
135
146
  };
136
147
  }
148
+ var extractErrorProperty = (error, property) => {
149
+ if (isRecord2(error) && property in error) {
150
+ return error[property];
151
+ }
152
+ return;
153
+ };
154
+ var noop = () => {
155
+ return;
156
+ };
157
+ var noopSticky = { sticky: noop };
137
158
  var storage = new AsyncLocalStorage;
159
+ function pushOp(operation) {
160
+ const store = storage.getStore();
161
+ if (!store) {
162
+ return noopSticky;
163
+ }
164
+ store.operations.push(operation);
165
+ return {
166
+ sticky: () => {
167
+ const index = store.operations.indexOf(operation);
168
+ if (index !== -1) {
169
+ store.operations.splice(index, 1);
170
+ }
171
+ store.stickyOperations.push(operation);
172
+ }
173
+ };
174
+ }
175
+ function applyFields(operations, fields, parentKey) {
176
+ for (const key of Object.keys(fields)) {
177
+ const value = fields[key];
178
+ const fullKey = parentKey ? `${parentKey}.${key}` : key;
179
+ if (isFieldValue(value)) {
180
+ operations.push({ operation: "set", key: fullKey, value });
181
+ continue;
182
+ }
183
+ if (isFieldValueArray(value)) {
184
+ for (const element of value) {
185
+ operations.push({ operation: "append", key: fullKey, value: element });
186
+ }
187
+ continue;
188
+ }
189
+ if (isRecord2(value)) {
190
+ applyFields(operations, value, fullKey);
191
+ }
192
+ }
193
+ }
194
+ function measure(key, callback) {
195
+ const operations = storage.getStore()?.operations;
196
+ operations?.push({ operation: "time.start", key, time: performance.now() });
197
+ let result;
198
+ try {
199
+ result = callback();
200
+ } catch (error) {
201
+ operations?.push({ operation: "time.stop", key, time: performance.now() });
202
+ throw error;
203
+ }
204
+ if (result instanceof Promise) {
205
+ return result.finally(() => {
206
+ operations?.push({
207
+ operation: "time.stop",
208
+ key,
209
+ time: performance.now()
210
+ });
211
+ });
212
+ }
213
+ operations?.push({ operation: "time.stop", key, time: performance.now() });
214
+ return result;
215
+ }
138
216
  var widelog = {
139
217
  set: (key, value) => {
140
- storage.getStore()?.operations.push({ operation: "set", key, value });
218
+ return pushOp({ operation: "set", key, value });
219
+ },
220
+ setFields: (fields) => {
221
+ const store = storage.getStore();
222
+ if (!store) {
223
+ return noopSticky;
224
+ }
225
+ const startIndex = store.operations.length;
226
+ applyFields(store.operations, fields);
227
+ return {
228
+ sticky: () => {
229
+ const added = store.operations.splice(startIndex);
230
+ store.stickyOperations.push(...added);
231
+ }
232
+ };
141
233
  },
142
234
  count: (key, amount = 1) => {
143
- storage.getStore()?.operations.push({ operation: "count", key, amount });
235
+ pushOp({ operation: "count", key, amount });
144
236
  },
145
237
  append: (key, value) => {
146
- storage.getStore()?.operations.push({ operation: "append", key, value });
238
+ pushOp({ operation: "append", key, value });
147
239
  },
148
240
  max: (key, value) => {
149
- storage.getStore()?.operations.push({ operation: "max", key, value });
241
+ pushOp({ operation: "max", key, value });
150
242
  },
151
243
  min: (key, value) => {
152
- storage.getStore()?.operations.push({ operation: "min", key, value });
244
+ pushOp({ operation: "min", key, value });
153
245
  },
154
246
  time: {
155
247
  start: (key) => {
156
- storage.getStore()?.operations.push({
157
- operation: "time.start",
158
- key,
159
- time: performance.now()
160
- });
248
+ pushOp({ operation: "time.start", key, time: performance.now() });
161
249
  },
162
250
  stop: (key) => {
163
- storage.getStore()?.operations.push({
164
- operation: "time.stop",
165
- key,
166
- time: performance.now()
167
- });
168
- }
251
+ pushOp({ operation: "time.stop", key, time: performance.now() });
252
+ },
253
+ measure
169
254
  },
170
255
  errorFields: (error, options = {}) => {
171
256
  const context = storage.getStore();
@@ -174,9 +259,15 @@ var widelog = {
174
259
  }
175
260
  const prefix = options.prefix ?? "error";
176
261
  const fields = getErrorFields(error, options.includeStack ?? true);
177
- const nameKey = `${prefix}.error_name`;
178
- const messageKey = `${prefix}.error_message`;
179
- context.operations.push({ operation: "set", key: nameKey, value: fields.error_name }, { operation: "set", key: messageKey, value: fields.error_message });
262
+ context.operations.push({
263
+ operation: "set",
264
+ key: `${prefix}.error_name`,
265
+ value: fields.error_name
266
+ }, {
267
+ operation: "set",
268
+ key: `${prefix}.error_message`,
269
+ value: fields.error_message
270
+ });
180
271
  if (fields.error_stack !== undefined) {
181
272
  context.operations.push({
182
273
  operation: "set",
@@ -184,21 +275,46 @@ var widelog = {
184
275
  value: fields.error_stack
185
276
  });
186
277
  }
278
+ const slug = options.slug ?? extractErrorProperty(error, "slug");
279
+ if (typeof slug === "string") {
280
+ context.operations.push({
281
+ operation: "set",
282
+ key: `${prefix}.slug`,
283
+ value: slug
284
+ });
285
+ }
286
+ const retriable = options.retriable ?? extractErrorProperty(error, "retriable");
287
+ if (typeof retriable === "boolean") {
288
+ context.operations.push({
289
+ operation: "set",
290
+ key: `${prefix}.retriable`,
291
+ value: retriable
292
+ });
293
+ }
294
+ const requiresReauth = options.requiresReauth ?? extractErrorProperty(error, "requiresReauth");
295
+ if (typeof requiresReauth === "boolean") {
296
+ context.operations.push({
297
+ operation: "set",
298
+ key: `${prefix}.requires_reauth`,
299
+ value: requiresReauth
300
+ });
301
+ }
187
302
  },
188
303
  flush: () => {
189
304
  const store = storage.getStore();
190
305
  if (!store) {
191
306
  return;
192
307
  }
193
- const event = flush(store);
194
- if (Object.keys(event).length === 0) {
308
+ if (store.operations.length === 0 && store.stickyOperations.length === 0) {
195
309
  return;
196
310
  }
311
+ const event = flush(store);
197
312
  store.transport(event);
198
313
  }
199
314
  };
200
315
  var widelogger = (options) => {
201
- const environment = options.environment ?? "development" ?? "development";
316
+ const nodeEnvironment = typeof process.env === "object" ? "development" : undefined;
317
+ const environment = options.environment ?? nodeEnvironment ?? "development";
202
318
  const isDevelopment = environment !== "production";
203
319
  const pinoTransport = isDevelopment ? pino.transport({
204
320
  target: "pino-pretty",
@@ -220,24 +336,26 @@ var widelogger = (options) => {
220
336
  environment
221
337
  }
222
338
  }, pinoTransport);
339
+ const defaultEventName = options.defaultEventName;
223
340
  const transport = (event) => {
224
341
  const statusCode = typeof event.status_code === "number" ? event.status_code : undefined;
225
342
  const isError = statusCode !== undefined ? statusCode >= 500 : event.outcome === "error";
226
- const payload = { event_name: options.defaultEventName, ...event };
343
+ event.event_name = defaultEventName;
227
344
  if (isError) {
228
- logger.error(payload);
345
+ logger.error(event);
229
346
  return;
230
347
  }
231
- logger.info(payload);
348
+ logger.info(event);
232
349
  };
233
350
  const clearContext = () => {
234
351
  const context2 = storage.getStore();
235
- if (context2 && context2.operations.length > 0) {
352
+ if (context2) {
236
353
  context2.operations = [];
354
+ context2.stickyOperations = [];
237
355
  }
238
356
  };
239
357
  function context(callback) {
240
- return storage.run({ operations: [], transport }, () => {
358
+ return storage.run({ operations: [], stickyOperations: [], transport }, () => {
241
359
  let result;
242
360
  try {
243
361
  result = callback();
package/dist/types.d.ts CHANGED
@@ -36,6 +36,7 @@ export type Operation = {
36
36
  };
37
37
  export interface Context {
38
38
  operations: Operation[];
39
+ stickyOperations: Operation[];
39
40
  transport: (event: Record<string, unknown>) => void;
40
41
  }
41
42
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "widelogger",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",