@startsimpli/logging 0.1.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/package.json +21 -0
- package/src/index.ts +2 -0
- package/src/logger.ts +203 -0
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@startsimpli/logging",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Structured logging for StartSimpli apps",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"files": ["src"],
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"type-check": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^20.19.33",
|
|
19
|
+
"typescript": "^5.9.3"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/index.ts
ADDED
package/src/logger.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logger for StartSimpli applications.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { logger, createLogger } from '@startsimpli/logging';
|
|
6
|
+
*
|
|
7
|
+
* // Default instance
|
|
8
|
+
* logger.info('Request received', { path: '/api/recipes' });
|
|
9
|
+
*
|
|
10
|
+
* // Named instance
|
|
11
|
+
* const log = createLogger('llm-pipeline');
|
|
12
|
+
* log.debug('Calling provider', { model: 'gpt-4' });
|
|
13
|
+
*
|
|
14
|
+
* // Child logger with request context
|
|
15
|
+
* const reqLog = log.withContext({ requestId: 'abc123', userId: '42' });
|
|
16
|
+
* reqLog.warn('Retry attempt', { attempt: 2 });
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
|
|
20
|
+
|
|
21
|
+
export interface Logger {
|
|
22
|
+
debug(message: string, data?: Record<string, unknown>): void;
|
|
23
|
+
info(message: string, data?: Record<string, unknown>): void;
|
|
24
|
+
warn(message: string, data?: Record<string, unknown>): void;
|
|
25
|
+
error(message: string, data?: Record<string, unknown>): void;
|
|
26
|
+
withContext(ctx: Record<string, unknown>): Logger;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Internal types
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
type LogData = Record<string, unknown>;
|
|
34
|
+
|
|
35
|
+
interface LogEntry {
|
|
36
|
+
level: LogLevel;
|
|
37
|
+
message: string;
|
|
38
|
+
timestamp: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
data?: LogData;
|
|
41
|
+
context?: LogData;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Level resolution
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
const LOG_LEVEL_RANK: Record<LogLevel, number> = {
|
|
49
|
+
debug: 0,
|
|
50
|
+
info: 1,
|
|
51
|
+
warn: 2,
|
|
52
|
+
error: 3,
|
|
53
|
+
silent: 4,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function resolveLogLevel(): LogLevel {
|
|
57
|
+
const envLevel = (process.env['LOG_LEVEL'] ?? '').toLowerCase() as LogLevel;
|
|
58
|
+
if (envLevel in LOG_LEVEL_RANK) return envLevel;
|
|
59
|
+
return process.env['NODE_ENV'] === 'development' ? 'debug' : 'info';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function shouldLog(level: LogLevel): boolean {
|
|
63
|
+
return LOG_LEVEL_RANK[level] >= LOG_LEVEL_RANK[resolveLogLevel()];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Sensitive data sanitization
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
const SENSITIVE_SUBSTRINGS = [
|
|
71
|
+
'password',
|
|
72
|
+
'token',
|
|
73
|
+
'secret',
|
|
74
|
+
'key',
|
|
75
|
+
'authorization',
|
|
76
|
+
'cookie',
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
function sanitize(data?: LogData): LogData | undefined {
|
|
80
|
+
if (!data) return undefined;
|
|
81
|
+
|
|
82
|
+
const out: LogData = {};
|
|
83
|
+
|
|
84
|
+
for (const [k, v] of Object.entries(data)) {
|
|
85
|
+
const lower = k.toLowerCase();
|
|
86
|
+
if (SENSITIVE_SUBSTRINGS.some(sub => lower.includes(sub))) {
|
|
87
|
+
out[k] = '[REDACTED]';
|
|
88
|
+
} else if (v instanceof Error) {
|
|
89
|
+
out[k] = {
|
|
90
|
+
name: v.name,
|
|
91
|
+
message: v.message,
|
|
92
|
+
stack: process.env['NODE_ENV'] === 'development' ? v.stack : undefined,
|
|
93
|
+
};
|
|
94
|
+
} else {
|
|
95
|
+
out[k] = v;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Formatting
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
function format(entry: LogEntry): string {
|
|
107
|
+
const { level, message, timestamp, name, data, context } = entry;
|
|
108
|
+
const nameTag = name ? ` [${name}]` : '';
|
|
109
|
+
const prefix = `[${timestamp}] [${level.toUpperCase()}]${nameTag}`;
|
|
110
|
+
|
|
111
|
+
if (process.env['NODE_ENV'] === 'development') {
|
|
112
|
+
let out = `${prefix} ${message}`;
|
|
113
|
+
if (context && Object.keys(context).length > 0) {
|
|
114
|
+
out += ` [ctx: ${JSON.stringify(context)}]`;
|
|
115
|
+
}
|
|
116
|
+
if (data && Object.keys(data).length > 0) {
|
|
117
|
+
out += `\n ${JSON.stringify(data, null, 2).split('\n').join('\n ')}`;
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return JSON.stringify(entry);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Logger implementation
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
class LoggerImpl implements Logger {
|
|
130
|
+
private readonly name?: string;
|
|
131
|
+
private readonly context?: LogData;
|
|
132
|
+
|
|
133
|
+
constructor(name?: string, context?: LogData) {
|
|
134
|
+
this.name = name;
|
|
135
|
+
this.context = context;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
withContext(ctx: Record<string, unknown>): Logger {
|
|
139
|
+
return new LoggerImpl(this.name, { ...this.context, ...ctx });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private emit(level: LogLevel, message: string, data?: LogData): void {
|
|
143
|
+
if (!shouldLog(level)) return;
|
|
144
|
+
|
|
145
|
+
const entry: LogEntry = {
|
|
146
|
+
level,
|
|
147
|
+
message,
|
|
148
|
+
timestamp: new Date().toISOString(),
|
|
149
|
+
name: this.name,
|
|
150
|
+
data: sanitize(data),
|
|
151
|
+
context: this.context,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const line = format(entry);
|
|
155
|
+
|
|
156
|
+
switch (level) {
|
|
157
|
+
case 'debug':
|
|
158
|
+
case 'info':
|
|
159
|
+
// eslint-disable-next-line no-console
|
|
160
|
+
console.log(line);
|
|
161
|
+
break;
|
|
162
|
+
case 'warn':
|
|
163
|
+
// eslint-disable-next-line no-console
|
|
164
|
+
console.warn(line);
|
|
165
|
+
break;
|
|
166
|
+
case 'error':
|
|
167
|
+
// eslint-disable-next-line no-console
|
|
168
|
+
console.error(line);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
debug(message: string, data?: LogData): void {
|
|
174
|
+
this.emit('debug', message, data);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
info(message: string, data?: LogData): void {
|
|
178
|
+
this.emit('info', message, data);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
warn(message: string, data?: LogData): void {
|
|
182
|
+
this.emit('warn', message, data);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
error(message: string, data?: LogData): void {
|
|
186
|
+
this.emit('error', message, data);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Public API
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Create a named logger instance. The name appears in every log line to make
|
|
196
|
+
* it easy to filter output by subsystem (e.g. 'llm-pipeline', 'auth').
|
|
197
|
+
*/
|
|
198
|
+
export function createLogger(name?: string): Logger {
|
|
199
|
+
return new LoggerImpl(name);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Default logger with no name prefix, suitable for top-level app code. */
|
|
203
|
+
export const logger: Logger = new LoggerImpl();
|