sloplog 0.0.3

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/dist/index.js ADDED
@@ -0,0 +1,354 @@
1
+ import { extractPartialMetadata } from './registry.js';
2
+ /**
3
+ * Generate a nano ID for unique identifiers
4
+ */
5
+ function nanoId() {
6
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
7
+ let result = '';
8
+ for (let i = 0; i < 21; i++) {
9
+ result += chars[Math.floor(Math.random() * chars.length)];
10
+ }
11
+ return result;
12
+ }
13
+ /** Current sloplog library version (propagated onto Service). */
14
+ export const SLOPLOG_VERSION = '0.0.2';
15
+ /** Default language marker when Service.sloplogLanguage is not set. */
16
+ const SLOPLOG_LANGUAGE = 'typescript';
17
+ function isOriginatorResult(input) {
18
+ return (typeof input === 'object' &&
19
+ input !== null &&
20
+ 'originator' in input &&
21
+ 'traceId' in input &&
22
+ typeof input.traceId === 'string');
23
+ }
24
+ /**
25
+ * Create a Service payload with sloplog defaults applied.
26
+ */
27
+ export function service(details) {
28
+ return {
29
+ ...details,
30
+ sloplogVersion: details.sloplogVersion ?? SLOPLOG_VERSION,
31
+ sloplogLanguage: details.sloplogLanguage ?? SLOPLOG_LANGUAGE,
32
+ };
33
+ }
34
+ // Re-export originator types and functions
35
+ export {
36
+ // Constants
37
+ ORIGINATOR_HEADER, TRACE_ID_HEADER,
38
+ // Functions
39
+ httpOriginator, nodeHttpOriginator, cronOriginator, tracingHeaders, extractTracingContext, } from './originator/index.js';
40
+ // Collectors are exposed via subpath exports (sloplog/collectors/*).
41
+ // Re-export built-in partials registry and types
42
+ export { builtInPartials, builtInRegistry, builtInPartialMetadata } from './partials.js';
43
+ /**
44
+ * Zod-based DSL for defining wide event partials
45
+ * Restricted to only allow specific primitive types for cross-language compatibility
46
+ */
47
+ export { z, partial, registry, extractPartialMetadata } from './registry.js';
48
+ // Re-export codegen functions
49
+ export { generateTypeScript, generatePython, generateJsonSchema } from './codegen.js';
50
+ export { config } from './codegen.js';
51
+ /**
52
+ * Core WideEvent class.
53
+ * Create one WideEvent per request or unit of work and add partials as you go.
54
+ * Use log() for structured partials or for lightweight log_message entries.
55
+ * @param R pass in a valid Registry type, which defines the wide event partials you may pass in
56
+ */
57
+ export class WideEvent {
58
+ eventId;
59
+ traceId;
60
+ collector;
61
+ /** All partials - singular as objects, repeatable as arrays */
62
+ partials = new Map();
63
+ service;
64
+ originator;
65
+ openSpans = new Map();
66
+ /** Metadata about partials (repeatable, alwaysSample) */
67
+ partialMetadata;
68
+ /** Whether this event should always be sampled */
69
+ _alwaysSample = false;
70
+ /**
71
+ * Create a wide event
72
+ *
73
+ * @param service Service that the wide event is being emitted on
74
+ * @param originator Originator (i.e. request, schedule, etc) of the wide event
75
+ * @param collector Location to collect/flush logs to
76
+ * @param options Optional configuration including traceId and partialMetadata
77
+ */
78
+ constructor(service, originator, collector, options = {}) {
79
+ this.eventId = `evt_${nanoId()}`;
80
+ this.traceId = options.traceId || `trace_${nanoId()}`;
81
+ this.service = {
82
+ ...service,
83
+ sloplogVersion: service.sloplogVersion ?? SLOPLOG_VERSION,
84
+ sloplogLanguage: service.sloplogLanguage ?? SLOPLOG_LANGUAGE,
85
+ };
86
+ this.originator = originator;
87
+ this.collector = collector;
88
+ this.partialMetadata = options.partialMetadata ?? new Map();
89
+ }
90
+ /**
91
+ * Check if this event should always be sampled
92
+ */
93
+ get alwaysSample() {
94
+ return this._alwaysSample;
95
+ }
96
+ /**
97
+ * Manually mark this event to always be sampled
98
+ */
99
+ markAlwaysSample() {
100
+ this._alwaysSample = true;
101
+ }
102
+ /**
103
+ * Add a partial to a wide event.
104
+ * For singular partials, this overwrites any existing partial of the same type.
105
+ * For repeatable partials, this appends to the array.
106
+ * If the partial has alwaysSample=true, the event will be marked to always be sampled.
107
+ * Overwriting a singular partial records a sloplog_usage_error.
108
+ *
109
+ * @param partial wide event partial to add
110
+ */
111
+ partial(partial) {
112
+ this.addPartialInternal(partial);
113
+ }
114
+ log(arg1, arg2, arg3) {
115
+ if (typeof arg1 !== 'string') {
116
+ this.addPartialInternal(arg1);
117
+ return;
118
+ }
119
+ let data;
120
+ if (typeof arg2 === 'string') {
121
+ data = arg2;
122
+ }
123
+ else if (arg2 !== undefined) {
124
+ try {
125
+ data = JSON.stringify(arg2);
126
+ }
127
+ catch {
128
+ this.addUsageError('log_message_stringify_error', 'Failed to stringify log data');
129
+ }
130
+ }
131
+ const level = arg3 ?? 'info';
132
+ this.addPartialInternal({
133
+ type: 'log_message',
134
+ message: arg1,
135
+ level,
136
+ ...(data !== undefined ? { data } : {}),
137
+ });
138
+ }
139
+ /**
140
+ * Add an error partial from an Error or message.
141
+ */
142
+ error(error, code) {
143
+ if (typeof error === 'object' && error !== null) {
144
+ const maybePartial = error;
145
+ if (maybePartial.type === 'error' && typeof maybePartial.message === 'string') {
146
+ this.addPartialInternal(error);
147
+ return;
148
+ }
149
+ }
150
+ const payload = {
151
+ type: 'error',
152
+ message: this.formatErrorMessage(error),
153
+ };
154
+ if (error instanceof Error && error.stack) {
155
+ payload.stack = error.stack;
156
+ }
157
+ else if (typeof error === 'object' &&
158
+ error !== null &&
159
+ typeof error.stack === 'string') {
160
+ payload.stack = error.stack;
161
+ }
162
+ const errorCode = typeof code === 'number'
163
+ ? code
164
+ : typeof error === 'object' &&
165
+ error !== null &&
166
+ typeof error.code === 'number'
167
+ ? error.code
168
+ : undefined;
169
+ if (typeof errorCode === 'number') {
170
+ payload.code = errorCode;
171
+ }
172
+ this.addPartialInternal(payload);
173
+ }
174
+ formatErrorMessage(error) {
175
+ if (typeof error === 'string') {
176
+ return error;
177
+ }
178
+ if (error instanceof Error) {
179
+ return error.message || 'Unknown error';
180
+ }
181
+ if (typeof error === 'object' && error !== null) {
182
+ const maybeMessage = error.message;
183
+ if (typeof maybeMessage === 'string') {
184
+ return maybeMessage;
185
+ }
186
+ try {
187
+ const serialized = JSON.stringify(error);
188
+ return typeof serialized === 'string' ? serialized : 'Unknown error';
189
+ }
190
+ catch {
191
+ return 'Unknown error';
192
+ }
193
+ }
194
+ return String(error);
195
+ }
196
+ /**
197
+ * Time a span around a callback and emit a span partial.
198
+ * Unended spans are recorded as sloplog_usage_error on flush.
199
+ */
200
+ async span(name, fn) {
201
+ this.spanStart(name);
202
+ try {
203
+ return await fn();
204
+ }
205
+ finally {
206
+ this.spanEnd(name);
207
+ }
208
+ }
209
+ /**
210
+ * Start a span by name
211
+ */
212
+ spanStart(name) {
213
+ const startedAt = Date.now();
214
+ const existing = this.openSpans.get(name);
215
+ if (existing) {
216
+ existing.push(startedAt);
217
+ }
218
+ else {
219
+ this.openSpans.set(name, [startedAt]);
220
+ }
221
+ }
222
+ /**
223
+ * End a span by name
224
+ * Ending a span that was never started records a sloplog_usage_error.
225
+ */
226
+ spanEnd(name) {
227
+ const existing = this.openSpans.get(name);
228
+ if (!existing || existing.length === 0) {
229
+ this.addUsageError('span_end_without_start', `Span "${name}" ended without start`, {
230
+ spanName: name,
231
+ });
232
+ return;
233
+ }
234
+ const startedAt = existing.pop();
235
+ if (startedAt === undefined) {
236
+ return;
237
+ }
238
+ if (existing.length === 0) {
239
+ this.openSpans.delete(name);
240
+ }
241
+ const endedAt = Date.now();
242
+ const durationMs = endedAt - startedAt;
243
+ this.addPartialInternal({
244
+ type: 'span',
245
+ name,
246
+ startedAt,
247
+ endedAt,
248
+ durationMs,
249
+ });
250
+ }
251
+ /**
252
+ * Get the current state of the wide event as a log object
253
+ */
254
+ toLog() {
255
+ const result = {
256
+ eventId: this.eventId,
257
+ traceId: this.traceId,
258
+ service: this.service,
259
+ originator: this.originator,
260
+ };
261
+ for (const [key, value] of this.partials) {
262
+ result[key] = value;
263
+ }
264
+ return result;
265
+ }
266
+ /**
267
+ * Emit the full wide log to the collector.
268
+ * Any usage errors (e.g. unended spans, partial overwrites) are emitted
269
+ * as sloplog_usage_error partials before flushing.
270
+ */
271
+ async flush() {
272
+ this.recordOpenSpans();
273
+ await this.collector.flush({
274
+ eventId: this.eventId,
275
+ traceId: this.traceId,
276
+ originator: this.originator,
277
+ service: this.service,
278
+ }, this.partials, { alwaysSample: this._alwaysSample });
279
+ }
280
+ addPartialInternal(partial) {
281
+ const metadata = this.partialMetadata.get(partial.type);
282
+ const isRepeatable = metadata?.repeatable ?? this.isRepeatableFallback(partial.type);
283
+ if (metadata?.alwaysSample || this.isAlwaysSampleFallback(partial.type)) {
284
+ this._alwaysSample = true;
285
+ }
286
+ if (isRepeatable) {
287
+ this.appendRepeatablePartial(partial.type, partial);
288
+ return;
289
+ }
290
+ if (this.partials.has(partial.type)) {
291
+ this.addUsageError('partial_overwrite', `Partial "${partial.type}" was overwritten`, {
292
+ partialType: partial.type,
293
+ });
294
+ }
295
+ this.partials.set(partial.type, partial);
296
+ }
297
+ appendRepeatablePartial(type, partial) {
298
+ const existing = this.partials.get(type);
299
+ if (Array.isArray(existing)) {
300
+ existing.push(partial);
301
+ return;
302
+ }
303
+ if (existing) {
304
+ this.partials.set(type, [existing, partial]);
305
+ return;
306
+ }
307
+ this.partials.set(type, [partial]);
308
+ }
309
+ isRepeatableFallback(type) {
310
+ return (type === 'error' ||
311
+ type === 'log_message' ||
312
+ type === 'span' ||
313
+ type === 'sloplog_usage_error');
314
+ }
315
+ isAlwaysSampleFallback(type) {
316
+ return type === 'error';
317
+ }
318
+ addUsageError(kind, message, details = {}) {
319
+ this.appendRepeatablePartial('sloplog_usage_error', {
320
+ type: 'sloplog_usage_error',
321
+ kind,
322
+ message,
323
+ ...details,
324
+ });
325
+ }
326
+ recordOpenSpans() {
327
+ if (this.openSpans.size === 0) {
328
+ return;
329
+ }
330
+ for (const [name, starts] of this.openSpans) {
331
+ for (const startedAt of starts) {
332
+ this.addUsageError('span_unended', `Span "${name}" was started but never ended`, {
333
+ spanName: name,
334
+ startedAt,
335
+ });
336
+ }
337
+ }
338
+ this.openSpans.clear();
339
+ }
340
+ }
341
+ /**
342
+ * Create a WideEvent instance. Prefer this factory over class construction.
343
+ * Provide the registry first to infer partial types and repeatable metadata.
344
+ * originator can be a raw Originator or the { originator, traceId } result from httpOriginator().
345
+ */
346
+ export function wideEvent(registry, service, originator, collector, options = {}) {
347
+ const resolvedOriginator = isOriginatorResult(originator) ? originator.originator : originator;
348
+ const traceId = options.traceId ?? (isOriginatorResult(originator) ? originator.traceId : undefined);
349
+ const partialMetadata = options.partialMetadata ?? extractPartialMetadata(registry);
350
+ return new WideEvent(service, resolvedOriginator, collector, {
351
+ traceId,
352
+ partialMetadata,
353
+ });
354
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Originator module - functions for creating originators from various sources
3
+ */
4
+ /** HTTP method types */
5
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
6
+ /**
7
+ * Base originator interface - an external thing that triggered your service
8
+ * This is like a trace that can cross service boundaries
9
+ */
10
+ export interface Originator {
11
+ /** Unique identifier for this originator chain (propagates across services) */
12
+ originatorId: string;
13
+ /** Type discriminator for the originator */
14
+ type: string;
15
+ /** Timestamp when the originator was created (Unix ms) */
16
+ timestamp: number;
17
+ /** Parent originator ID if this is a child span */
18
+ parentId?: string;
19
+ [key: string]: unknown;
20
+ }
21
+ /**
22
+ * HTTP request originator
23
+ */
24
+ export interface HttpOriginator extends Originator {
25
+ type: 'http';
26
+ method: HttpMethod;
27
+ path: string;
28
+ /** Query string (without leading ?) */
29
+ query?: string;
30
+ /** Request headers */
31
+ headers?: Record<string, string>;
32
+ /** Client IP address */
33
+ clientIp?: string;
34
+ /** User agent string */
35
+ userAgent?: string;
36
+ /** Content type of the request */
37
+ contentType?: string;
38
+ /** Content length in bytes */
39
+ contentLength?: number;
40
+ /** HTTP protocol version (e.g., "1.1", "2") */
41
+ httpVersion?: string;
42
+ /** Host header value */
43
+ host?: string;
44
+ }
45
+ /**
46
+ * WebSocket message originator
47
+ */
48
+ export interface WebSocketOriginator extends Originator {
49
+ type: 'websocket';
50
+ /** WebSocket session/connection ID */
51
+ sessionId: string;
52
+ /** Message source identifier */
53
+ source: string;
54
+ /** Message type (e.g., "text", "binary") */
55
+ messageType?: 'text' | 'binary';
56
+ /** Size of the message in bytes */
57
+ messageSize?: number;
58
+ }
59
+ /**
60
+ * Cron/scheduled task originator
61
+ */
62
+ export interface CronOriginator extends Originator {
63
+ type: 'cron';
64
+ /** Cron expression (e.g., "0 0 * * *") */
65
+ cron: string;
66
+ /** Name of the scheduled job */
67
+ jobName?: string;
68
+ /** Scheduled execution time (Unix ms) */
69
+ scheduledTime?: number;
70
+ }
71
+ /** Header name for propagating originator ID across services */
72
+ export declare const ORIGINATOR_HEADER = "x-sloplog-originator";
73
+ /** Header name for propagating trace ID across services */
74
+ export declare const TRACE_ID_HEADER = "x-sloplog-trace-id";
75
+ /**
76
+ * Tracing context to propagate across services
77
+ */
78
+ export interface TracingContext {
79
+ /** The trace ID (stays constant across the entire distributed trace) */
80
+ traceId: string;
81
+ /** The originator ID of the calling service (becomes parentId in the callee) */
82
+ originatorId: string;
83
+ }
84
+ /**
85
+ * Create headers for propagating tracing context to downstream services
86
+ */
87
+ export declare function tracingHeaders(context: TracingContext): Record<string, string>;
88
+ /**
89
+ * Extract tracing context from incoming request headers
90
+ * Returns null if no tracing headers are present
91
+ */
92
+ export declare function extractTracingContext(headers: Record<string, string | string[] | undefined>): TracingContext | null;
93
+ /**
94
+ * Options for creating an HTTP originator
95
+ */
96
+ export interface HttpOriginatorOptions {
97
+ /** Override the originator ID (useful when continuing a trace) */
98
+ originatorId?: string;
99
+ /** Parent originator ID (for child originators) */
100
+ parentId?: string;
101
+ }
102
+ /**
103
+ * Result of creating an originator from an incoming request
104
+ * Contains both the originator and the extracted traceId (if any)
105
+ */
106
+ export interface OriginatorFromRequestResult {
107
+ /** The created HTTP originator */
108
+ originator: HttpOriginator;
109
+ /** The trace ID extracted from headers, or a newly generated one */
110
+ traceId: string;
111
+ }
112
+ /**
113
+ * Create an HTTP originator from a Web Fetch API Request
114
+ * Extracts tracing context from headers if present:
115
+ * - traceId: extracted from x-sloplog-trace-id header, or generated if not present
116
+ * - parentId: set to the incoming x-sloplog-originator header value (the caller's originatorId),
117
+ * or can be explicitly provided via options.parentId
118
+ */
119
+ export declare function httpOriginator(request: Request, options?: HttpOriginatorOptions): OriginatorFromRequestResult;
120
+ /**
121
+ * Node.js IncomingMessage-like interface
122
+ */
123
+ export interface NodeIncomingMessage {
124
+ method?: string;
125
+ url?: string;
126
+ headers: Record<string, string | string[] | undefined>;
127
+ httpVersion?: string;
128
+ socket?: {
129
+ remoteAddress?: string;
130
+ };
131
+ }
132
+ /**
133
+ * Create an HTTP originator from a Node.js IncomingMessage (http/https/express)
134
+ * Extracts tracing context from headers if present:
135
+ * - traceId: extracted from x-sloplog-trace-id header, or generated if not present
136
+ * - parentId: set to the incoming x-sloplog-originator header value (the caller's originatorId),
137
+ * or can be explicitly provided via options.parentId
138
+ */
139
+ export declare function nodeHttpOriginator(request: NodeIncomingMessage, options?: HttpOriginatorOptions): OriginatorFromRequestResult;
140
+ /**
141
+ * Options for creating a cron originator
142
+ */
143
+ export interface CronOriginatorOptions {
144
+ /** Parent originator ID (for child originators) */
145
+ parentId?: string;
146
+ }
147
+ /**
148
+ * Create a cron originator for scheduled tasks
149
+ */
150
+ export declare function cronOriginator(cron: string, jobName?: string, options?: CronOriginatorOptions): CronOriginator;