@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 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
@@ -0,0 +1,2 @@
1
+ export { logger, createLogger } from './logger';
2
+ export type { Logger, LogLevel } from './logger';
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();