Skip to content

Commit 99f2333

Browse files
authored
Merge pull request #873 from nissa-seru/create-logging-util
Implement logging utility
2 parents dc46334 + b194bce commit 99f2333

File tree

9 files changed

+1019
-1
lines changed

9 files changed

+1019
-1
lines changed

.clinerules

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
- Prefer fixing the underlying issue over disabling the lint rule
1111
- Document any approved lint rule disabling with a comment explaining the reason
1212

13+
3. Logging Guidelines:
14+
- Always instrument code changes using the logger exported from `src\utils\logging\index.ts`.
15+
- This will facilitate efficient debugging without impacting production (as the logger no-ops outside of a test environment.)
16+
- Logs can be found in `logs\app.log`
17+
- Logfile is overwritten on each run to keep it to a manageable volume.
18+
19+
1320
# Adding a New Setting
1421

15-
To add a new setting that persists its state, follow the steps in cline_docs/settings.md
22+
To add a new setting that persists its state, follow the steps in cline_docs/settings.md

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,9 @@ docs/_site/
2222

2323
# Dotenv
2424
.env.integration
25+
26+
#Local lint config
2527
.eslintrc.local.json
28+
29+
#Logging
30+
logs

src/utils/logging/CompactLogger.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* @fileoverview Implementation of the compact logging system's main logger class
3+
*/
4+
5+
import { ILogger, LogMeta, CompactLogEntry, LogLevel } from "./types"
6+
import { CompactTransport } from "./CompactTransport"
7+
8+
/**
9+
* Main logger implementation providing compact, efficient logging capabilities
10+
* @implements {ILogger}
11+
*/
12+
export class CompactLogger implements ILogger {
13+
private transport: CompactTransport
14+
private parentMeta: LogMeta | undefined
15+
16+
/**
17+
* Creates a new CompactLogger instance
18+
* @param transport - Optional custom transport instance
19+
* @param parentMeta - Optional parent metadata for hierarchical logging
20+
*/
21+
constructor(transport?: CompactTransport, parentMeta?: LogMeta) {
22+
this.transport = transport ?? new CompactTransport()
23+
this.parentMeta = parentMeta
24+
}
25+
26+
/**
27+
* Logs a debug level message
28+
* @param message - The message to log
29+
* @param meta - Optional metadata to include
30+
*/
31+
debug(message: string, meta?: LogMeta): void {
32+
this.log("debug", message, this.combineMeta(meta))
33+
}
34+
35+
/**
36+
* Logs an info level message
37+
* @param message - The message to log
38+
* @param meta - Optional metadata to include
39+
*/
40+
info(message: string, meta?: LogMeta): void {
41+
this.log("info", message, this.combineMeta(meta))
42+
}
43+
44+
/**
45+
* Logs a warning level message
46+
* @param message - The message to log
47+
* @param meta - Optional metadata to include
48+
*/
49+
warn(message: string, meta?: LogMeta): void {
50+
this.log("warn", message, this.combineMeta(meta))
51+
}
52+
53+
/**
54+
* Logs an error level message
55+
* @param message - The error message or Error object
56+
* @param meta - Optional metadata to include
57+
*/
58+
error(message: string | Error, meta?: LogMeta): void {
59+
this.handleErrorLog("error", message, meta)
60+
}
61+
62+
/**
63+
* Logs a fatal level message
64+
* @param message - The error message or Error object
65+
* @param meta - Optional metadata to include
66+
*/
67+
fatal(message: string | Error, meta?: LogMeta): void {
68+
this.handleErrorLog("fatal", message, meta)
69+
}
70+
71+
/**
72+
* Creates a child logger inheriting this logger's metadata
73+
* @param meta - Additional metadata for the child logger
74+
* @returns A new logger instance with combined metadata
75+
*/
76+
child(meta: LogMeta): ILogger {
77+
const combinedMeta = this.parentMeta ? { ...this.parentMeta, ...meta } : meta
78+
return new CompactLogger(this.transport, combinedMeta)
79+
}
80+
81+
/**
82+
* Closes the logger and its transport
83+
*/
84+
close(): void {
85+
this.transport.close()
86+
}
87+
88+
/**
89+
* Handles logging of error and fatal messages with special error object processing
90+
* @private
91+
* @param level - The log level (error or fatal)
92+
* @param message - The message or Error object to log
93+
* @param meta - Optional metadata to include
94+
*/
95+
private handleErrorLog(level: "error" | "fatal", message: string | Error, meta?: LogMeta): void {
96+
if (message instanceof Error) {
97+
const errorMeta: LogMeta = {
98+
...meta,
99+
ctx: meta?.ctx ?? level,
100+
error: {
101+
name: message.name,
102+
message: message.message,
103+
stack: message.stack,
104+
},
105+
}
106+
this.log(level, message.message, this.combineMeta(errorMeta))
107+
} else {
108+
this.log(level, message, this.combineMeta(meta))
109+
}
110+
}
111+
112+
/**
113+
* Combines parent and current metadata with proper context handling
114+
* @private
115+
* @param meta - The current metadata to combine with parent metadata
116+
* @returns Combined metadata or undefined if no metadata exists
117+
*/
118+
private combineMeta(meta?: LogMeta): LogMeta | undefined {
119+
if (!this.parentMeta) {
120+
return meta
121+
}
122+
if (!meta) {
123+
return this.parentMeta
124+
}
125+
return {
126+
...this.parentMeta,
127+
...meta,
128+
ctx: meta.ctx || this.parentMeta.ctx,
129+
}
130+
}
131+
132+
/**
133+
* Core logging function that processes and writes log entries
134+
* @private
135+
* @param level - The log level
136+
* @param message - The message to log
137+
* @param meta - Optional metadata to include
138+
*/
139+
private log(level: LogLevel, message: string, meta?: LogMeta): void {
140+
const entry: CompactLogEntry = {
141+
t: Date.now(),
142+
l: level,
143+
m: message,
144+
c: meta?.ctx,
145+
d: meta ? (({ ctx, ...rest }) => (Object.keys(rest).length > 0 ? rest : undefined))(meta) : undefined,
146+
}
147+
148+
this.transport.write(entry)
149+
}
150+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* @fileoverview Implementation of the compact logging transport system with file output capabilities
3+
*/
4+
5+
import { writeFileSync, mkdirSync } from "fs"
6+
import { dirname } from "path"
7+
import { CompactTransportConfig, ICompactTransport, CompactLogEntry, LogLevel, LOG_LEVELS } from "./types"
8+
9+
/**
10+
* Default configuration for the transport
11+
*/
12+
const DEFAULT_CONFIG: CompactTransportConfig = {
13+
level: "debug",
14+
fileOutput: {
15+
enabled: true,
16+
path: "./logs/app.log",
17+
},
18+
}
19+
20+
/**
21+
* Determines if a log entry should be processed based on configured minimum level
22+
* @param configLevel - The minimum log level from configuration
23+
* @param entryLevel - The level of the current log entry
24+
* @returns Whether the entry should be processed
25+
*/
26+
function isLevelEnabled(configLevel: LogLevel, entryLevel: string): boolean {
27+
const configIdx = LOG_LEVELS.indexOf(configLevel)
28+
const entryIdx = LOG_LEVELS.indexOf(entryLevel as LogLevel)
29+
return entryIdx >= configIdx
30+
}
31+
32+
/**
33+
* Implements the compact logging transport with file output support
34+
* @implements {ICompactTransport}
35+
*/
36+
export class CompactTransport implements ICompactTransport {
37+
private sessionStart: number
38+
private lastTimestamp: number
39+
private filePath?: string
40+
private initialized: boolean = false
41+
42+
/**
43+
* Creates a new CompactTransport instance
44+
* @param config - Optional transport configuration
45+
*/
46+
constructor(readonly config: CompactTransportConfig = DEFAULT_CONFIG) {
47+
this.sessionStart = Date.now()
48+
this.lastTimestamp = this.sessionStart
49+
50+
if (config.fileOutput?.enabled) {
51+
this.filePath = config.fileOutput.path
52+
}
53+
}
54+
55+
/**
56+
* Ensures the log file is initialized with proper directory structure and session start marker
57+
* @private
58+
* @throws {Error} If file initialization fails
59+
*/
60+
private ensureInitialized(): void {
61+
if (this.initialized || !this.filePath) return
62+
63+
try {
64+
mkdirSync(dirname(this.filePath), { recursive: true })
65+
writeFileSync(this.filePath, "", { flag: "w" })
66+
67+
const sessionStart = {
68+
t: 0,
69+
l: "info",
70+
m: "Log session started",
71+
d: { timestamp: new Date(this.sessionStart).toISOString() },
72+
}
73+
writeFileSync(this.filePath, JSON.stringify(sessionStart) + "\n", { flag: "w" })
74+
75+
this.initialized = true
76+
} catch (err) {
77+
throw new Error(`Failed to initialize log file: ${(err as Error).message}`)
78+
}
79+
}
80+
81+
/**
82+
* Writes a log entry to configured outputs (console and/or file)
83+
* @param entry - The log entry to write
84+
*/
85+
write(entry: CompactLogEntry): void {
86+
const deltaT = entry.t - this.lastTimestamp
87+
this.lastTimestamp = entry.t
88+
89+
const compact = {
90+
...entry,
91+
t: deltaT,
92+
}
93+
94+
const output = JSON.stringify(compact) + "\n"
95+
96+
// Write to console if level is enabled
97+
if (this.config.level && isLevelEnabled(this.config.level, entry.l)) {
98+
process.stdout.write(output)
99+
}
100+
101+
// Write to file if enabled
102+
if (this.filePath) {
103+
this.ensureInitialized()
104+
writeFileSync(this.filePath, output, { flag: "a" })
105+
}
106+
}
107+
108+
/**
109+
* Closes the transport and writes session end marker
110+
*/
111+
close(): void {
112+
if (this.filePath && this.initialized) {
113+
const sessionEnd = {
114+
t: Date.now() - this.lastTimestamp,
115+
l: "info",
116+
m: "Log session ended",
117+
d: { timestamp: new Date().toISOString() },
118+
}
119+
writeFileSync(this.filePath, JSON.stringify(sessionEnd) + "\n", { flag: "a" })
120+
}
121+
}
122+
}

0 commit comments

Comments
 (0)