loggily 0.0.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/LICENSE +21 -0
- package/README.md +148 -4
- package/package.json +68 -7
- package/src/colors.ts +27 -0
- package/src/context.ts +170 -0
- package/src/core.ts +850 -0
- package/src/file-writer.ts +110 -0
- package/src/index.browser.ts +72 -0
- package/src/index.ts +18 -1
- package/src/tracing.ts +150 -0
- package/src/worker.ts +674 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File writer for loggily — Node.js/Bun only.
|
|
3
|
+
*
|
|
4
|
+
* Separated from core logger to allow tree-shaking in browser bundles.
|
|
5
|
+
* Uses dynamic import("node:fs") to avoid static dependency on Node APIs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { openSync, writeSync, closeSync } from "node:fs"
|
|
9
|
+
|
|
10
|
+
/** Options for creating an async buffered file writer */
|
|
11
|
+
export interface FileWriterOptions {
|
|
12
|
+
/** Buffer size threshold in bytes before flushing (default: 4096) */
|
|
13
|
+
bufferSize?: number
|
|
14
|
+
/** Flush interval in milliseconds (default: 100) */
|
|
15
|
+
flushInterval?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** An async buffered file writer with automatic flushing */
|
|
19
|
+
export interface FileWriter {
|
|
20
|
+
/** Write a line to the buffer (appends newline) */
|
|
21
|
+
write(line: string): void
|
|
22
|
+
/** Flush the buffer immediately */
|
|
23
|
+
flush(): void
|
|
24
|
+
/** Close the writer and flush remaining buffer */
|
|
25
|
+
close(): void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create an async buffered file writer for log output.
|
|
30
|
+
* Buffers writes and flushes on size threshold or interval.
|
|
31
|
+
* Registers a process.on('exit') handler to flush remaining buffer.
|
|
32
|
+
*
|
|
33
|
+
* **Node.js/Bun only** — not available in browser environments.
|
|
34
|
+
*
|
|
35
|
+
* @param filePath - Path to the log file (opened in append mode)
|
|
36
|
+
* @param options - Buffer size and flush interval configuration
|
|
37
|
+
* @returns FileWriter with write, flush, and close methods
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* const writer = createFileWriter('/tmp/app.log')
|
|
41
|
+
* const unsubscribe = addWriter((formatted) => writer.write(formatted))
|
|
42
|
+
*
|
|
43
|
+
* // On shutdown:
|
|
44
|
+
* unsubscribe()
|
|
45
|
+
* writer.close()
|
|
46
|
+
*/
|
|
47
|
+
export function createFileWriter(filePath: string, options: FileWriterOptions = {}): FileWriter {
|
|
48
|
+
const bufferSize = options.bufferSize ?? 4096
|
|
49
|
+
const flushInterval = options.flushInterval ?? 100
|
|
50
|
+
|
|
51
|
+
let buffer = ""
|
|
52
|
+
let fd: number | null = null
|
|
53
|
+
let timer: ReturnType<typeof setInterval> | null = null
|
|
54
|
+
let closed = false
|
|
55
|
+
|
|
56
|
+
// Open file in append mode
|
|
57
|
+
fd = openSync(filePath, "a")
|
|
58
|
+
|
|
59
|
+
/** Flush buffer contents to disk synchronously */
|
|
60
|
+
function flush(): void {
|
|
61
|
+
if (buffer.length === 0 || fd === null) return
|
|
62
|
+
const data = buffer
|
|
63
|
+
writeSync(fd, data)
|
|
64
|
+
buffer = ""
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Set up periodic flush
|
|
68
|
+
timer = setInterval(flush, flushInterval)
|
|
69
|
+
// Don't let the timer keep the process alive
|
|
70
|
+
if (timer && typeof timer === "object" && "unref" in timer) {
|
|
71
|
+
;(timer as { unref(): void }).unref()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Flush on process exit to avoid data loss
|
|
75
|
+
const exitHandler = (): void => flush()
|
|
76
|
+
process.on("exit", exitHandler)
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
write(line: string): void {
|
|
80
|
+
if (closed) return
|
|
81
|
+
buffer += line + "\n"
|
|
82
|
+
if (buffer.length >= bufferSize) {
|
|
83
|
+
flush()
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
flush,
|
|
88
|
+
|
|
89
|
+
close(): void {
|
|
90
|
+
if (closed) return
|
|
91
|
+
closed = true
|
|
92
|
+
if (timer !== null) {
|
|
93
|
+
clearInterval(timer)
|
|
94
|
+
timer = null
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
flush()
|
|
98
|
+
} catch {
|
|
99
|
+
// Swallow flush errors during close — data loss is unavoidable
|
|
100
|
+
// at this point, but we must still release the fd and exit handler.
|
|
101
|
+
} finally {
|
|
102
|
+
if (fd !== null) {
|
|
103
|
+
closeSync(fd)
|
|
104
|
+
fd = null
|
|
105
|
+
}
|
|
106
|
+
process.removeListener("exit", exitHandler)
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loggily browser entry point.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the full logger API except createFileWriter (which requires node:fs).
|
|
5
|
+
* Bundlers resolve this via the "browser" condition in package.json exports.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export everything from core logger
|
|
9
|
+
export {
|
|
10
|
+
// Types
|
|
11
|
+
type OutputLogLevel,
|
|
12
|
+
type LogLevel,
|
|
13
|
+
type LazyMessage,
|
|
14
|
+
type SpanData,
|
|
15
|
+
type Logger,
|
|
16
|
+
type SpanLogger,
|
|
17
|
+
type OutputMode,
|
|
18
|
+
type LogFormat,
|
|
19
|
+
type ConditionalLogger,
|
|
20
|
+
|
|
21
|
+
// Writers
|
|
22
|
+
addWriter,
|
|
23
|
+
setSuppressConsole,
|
|
24
|
+
setOutputMode,
|
|
25
|
+
getOutputMode,
|
|
26
|
+
|
|
27
|
+
// Configuration
|
|
28
|
+
setLogLevel,
|
|
29
|
+
getLogLevel,
|
|
30
|
+
enableSpans,
|
|
31
|
+
disableSpans,
|
|
32
|
+
spansAreEnabled,
|
|
33
|
+
setTraceFilter,
|
|
34
|
+
getTraceFilter,
|
|
35
|
+
setDebugFilter,
|
|
36
|
+
getDebugFilter,
|
|
37
|
+
setLogFormat,
|
|
38
|
+
getLogFormat,
|
|
39
|
+
|
|
40
|
+
// ID management
|
|
41
|
+
resetIds,
|
|
42
|
+
|
|
43
|
+
// Span collection
|
|
44
|
+
startCollecting,
|
|
45
|
+
stopCollecting,
|
|
46
|
+
getCollectedSpans,
|
|
47
|
+
clearCollectedSpans,
|
|
48
|
+
|
|
49
|
+
// Logger creation
|
|
50
|
+
createLogger,
|
|
51
|
+
} from "./core.js"
|
|
52
|
+
|
|
53
|
+
// Tracing utilities (runtime-agnostic, work in browser)
|
|
54
|
+
export {
|
|
55
|
+
setIdFormat,
|
|
56
|
+
getIdFormat,
|
|
57
|
+
type IdFormat,
|
|
58
|
+
traceparent,
|
|
59
|
+
type TraceparentOptions,
|
|
60
|
+
setSampleRate,
|
|
61
|
+
getSampleRate,
|
|
62
|
+
} from "./tracing.js"
|
|
63
|
+
|
|
64
|
+
// File writer types (exported for type compatibility, but the function throws)
|
|
65
|
+
export type { FileWriterOptions, FileWriter } from "./file-writer.js"
|
|
66
|
+
|
|
67
|
+
/** @throws Always — createFileWriter is not available in browser environments */
|
|
68
|
+
export function createFileWriter(): never {
|
|
69
|
+
throw new Error(
|
|
70
|
+
"createFileWriter is not available in browser environments. Use addWriter() with a custom transport instead.",
|
|
71
|
+
)
|
|
72
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* loggily - Structured logging with spans
|
|
3
|
+
*
|
|
4
|
+
* Full entry point for Node.js, Bun, and Deno.
|
|
5
|
+
* Browser environments use index.browser.ts via the "browser" export condition.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export * from "./core.js"
|
|
9
|
+
export { createFileWriter, type FileWriter, type FileWriterOptions } from "./file-writer.js"
|
|
10
|
+
export {
|
|
11
|
+
setIdFormat,
|
|
12
|
+
getIdFormat,
|
|
13
|
+
type IdFormat,
|
|
14
|
+
traceparent,
|
|
15
|
+
type TraceparentOptions,
|
|
16
|
+
setSampleRate,
|
|
17
|
+
getSampleRate,
|
|
18
|
+
} from "./tracing.js"
|
package/src/tracing.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Distributed tracing utilities for loggily.
|
|
3
|
+
*
|
|
4
|
+
* Provides W3C-compatible trace/span ID generation, traceparent header formatting,
|
|
5
|
+
* and head-based sampling. All features are opt-in and don't break the existing API.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SpanData } from "./core.js"
|
|
9
|
+
|
|
10
|
+
// ============ ID Format ============
|
|
11
|
+
|
|
12
|
+
/** Supported ID formats */
|
|
13
|
+
export type IdFormat = "simple" | "w3c"
|
|
14
|
+
|
|
15
|
+
let currentIdFormat: IdFormat = "simple"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Set the ID format for new spans and traces.
|
|
19
|
+
* - "simple": sp_1, sp_2, tr_1, tr_2 (default, lightweight)
|
|
20
|
+
* - "w3c": 32-char hex trace ID, 16-char hex span ID (W3C Trace Context compatible)
|
|
21
|
+
*/
|
|
22
|
+
export function setIdFormat(format: IdFormat): void {
|
|
23
|
+
currentIdFormat = format
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Get the current ID format */
|
|
27
|
+
export function getIdFormat(): IdFormat {
|
|
28
|
+
return currentIdFormat
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Simple format counters (used by core.ts via the generator functions)
|
|
32
|
+
let simpleSpanCounter = 0
|
|
33
|
+
let simpleTraceCounter = 0
|
|
34
|
+
|
|
35
|
+
/** Generate a hex string of the given byte length using crypto.randomUUID */
|
|
36
|
+
function randomHex(bytes: number): string {
|
|
37
|
+
// crypto.randomUUID() gives us 32 hex chars (128 bits) after removing dashes
|
|
38
|
+
// For 16 bytes (32 hex chars) we use one UUID, for 8 bytes (16 hex chars) we take a slice
|
|
39
|
+
const uuid = crypto.randomUUID().replace(/-/g, "")
|
|
40
|
+
return uuid.slice(0, bytes * 2)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Generate a span ID according to the current format */
|
|
44
|
+
export function generateSpanId(): string {
|
|
45
|
+
if (currentIdFormat === "w3c") {
|
|
46
|
+
return randomHex(8) // 16-char hex
|
|
47
|
+
}
|
|
48
|
+
return `sp_${(++simpleSpanCounter).toString(36)}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Generate a trace ID according to the current format */
|
|
52
|
+
export function generateTraceId(): string {
|
|
53
|
+
if (currentIdFormat === "w3c") {
|
|
54
|
+
return randomHex(16) // 32-char hex
|
|
55
|
+
}
|
|
56
|
+
return `tr_${(++simpleTraceCounter).toString(36)}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Reset ID counters (for testing) */
|
|
60
|
+
export function resetIdCounters(): void {
|
|
61
|
+
simpleSpanCounter = 0
|
|
62
|
+
simpleTraceCounter = 0
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============ W3C Traceparent ============
|
|
66
|
+
|
|
67
|
+
/** Options for traceparent header formatting */
|
|
68
|
+
export interface TraceparentOptions {
|
|
69
|
+
/** Whether this span is sampled. Defaults to true for backwards compatibility. */
|
|
70
|
+
sampled?: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Format a W3C traceparent header from span data.
|
|
75
|
+
*
|
|
76
|
+
* Format: `{version}-{trace-id}-{span-id}-{trace-flags}`
|
|
77
|
+
* - version: "00" (current W3C spec version)
|
|
78
|
+
* - trace-id: 32 hex chars (128 bits)
|
|
79
|
+
* - span-id: 16 hex chars (64 bits)
|
|
80
|
+
* - trace-flags: "01" (sampled) or "00" (not sampled)
|
|
81
|
+
*
|
|
82
|
+
* Works with both simple and W3C ID formats. Simple IDs are zero-padded to spec length.
|
|
83
|
+
*
|
|
84
|
+
* @param spanData - Span data with id and traceId
|
|
85
|
+
* @param options - Optional settings (sampled flag). Defaults to sampled=true.
|
|
86
|
+
* @returns W3C traceparent header string
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* const span = log.span("http-request")
|
|
91
|
+
* const header = traceparent(span.spanData)
|
|
92
|
+
* // → "00-a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6-1a2b3c4d5e6f7a8b-01"
|
|
93
|
+
* fetch(url, { headers: { traceparent: header } })
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export function traceparent(spanData: SpanData, options?: TraceparentOptions): string {
|
|
97
|
+
const traceId = padHex(spanData.traceId, 32)
|
|
98
|
+
const spanId = padHex(spanData.id, 16)
|
|
99
|
+
const flags = (options?.sampled ?? true) ? "01" : "00"
|
|
100
|
+
return `00-${traceId}-${spanId}-${flags}`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Pad or hash an ID to the specified hex length */
|
|
104
|
+
function padHex(id: string, length: number): string {
|
|
105
|
+
// If it's already the right length and looks like hex, use as-is
|
|
106
|
+
if (id.length === length && /^[0-9a-f]+$/.test(id)) {
|
|
107
|
+
return id
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// For simple IDs (sp_1, tr_1), create a deterministic hex representation
|
|
111
|
+
// by encoding the string as hex bytes, zero-padded to the target length
|
|
112
|
+
let hex = ""
|
|
113
|
+
for (let i = 0; i < id.length; i++) {
|
|
114
|
+
hex += id.charCodeAt(i).toString(16).padStart(2, "0")
|
|
115
|
+
}
|
|
116
|
+
// Pad or truncate to target length
|
|
117
|
+
return hex.padStart(length, "0").slice(-length)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============ Sampling ============
|
|
121
|
+
|
|
122
|
+
let sampleRate = 1.0
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Set the head-based sampling rate for new traces.
|
|
126
|
+
* Applied at trace creation — all spans within a sampled trace are kept.
|
|
127
|
+
*
|
|
128
|
+
* @param rate - Sampling rate from 0.0 (sample nothing) to 1.0 (sample everything, default)
|
|
129
|
+
*/
|
|
130
|
+
export function setSampleRate(rate: number): void {
|
|
131
|
+
if (rate < 0 || rate > 1) {
|
|
132
|
+
throw new Error(`Sample rate must be between 0.0 and 1.0, got ${rate}`)
|
|
133
|
+
}
|
|
134
|
+
sampleRate = rate
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Get the current sampling rate */
|
|
138
|
+
export function getSampleRate(): number {
|
|
139
|
+
return sampleRate
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Determine whether a new trace should be sampled.
|
|
144
|
+
* Called at trace creation time (head-based sampling).
|
|
145
|
+
*/
|
|
146
|
+
export function shouldSample(): boolean {
|
|
147
|
+
if (sampleRate >= 1.0) return true
|
|
148
|
+
if (sampleRate <= 0.0) return false
|
|
149
|
+
return Math.random() < sampleRate
|
|
150
|
+
}
|