@@ -7,11 +7,14 @@ const std = @import("std");
77const fs = std .fs ;
88const Allocator = std .mem .Allocator ;
99
10+ const fsutil = @import ("fs.zig" );
11+
1012const stdout_file = fs.File { .handle = std .posix .STDOUT_FILENO };
1113
1214/// Logger for opencoder operations
1315pub const Logger = struct {
1416 main_log : ? fs.File ,
17+ main_log_path : []const u8 ,
1518 cycle_log_dir : []const u8 ,
1619 alerts_file : []const u8 ,
1720 cycle : u32 ,
@@ -28,7 +31,7 @@ pub const Logger = struct {
2831 ) ! Logger {
2932 // Open main log file
3033 const main_log_path = try std .fs .path .join (allocator , &.{ opencoder_dir , "logs" , "main.log" });
31- defer allocator .free (main_log_path );
34+ errdefer allocator .free (main_log_path );
3235
3336 const main_log = fs .cwd ().openFile (main_log_path , .{ .mode = .write_only }) catch | err | blk : {
3437 if (err == error .FileNotFound ) {
@@ -44,6 +47,7 @@ pub const Logger = struct {
4447
4548 return Logger {
4649 .main_log = main_log ,
50+ .main_log_path = main_log_path ,
4751 .cycle_log_dir = cycle_log_dir ,
4852 .alerts_file = alerts_file ,
4953 .cycle = 1 ,
@@ -58,10 +62,134 @@ pub const Logger = struct {
5862 if (self .main_log ) | main_log_file | {
5963 main_log_file .close ();
6064 }
65+ self .allocator .free (self .main_log_path );
6166 self .allocator .free (self .cycle_log_dir );
6267 self .allocator .free (self .alerts_file );
6368 }
6469
70+ /// Rotate the main log file by renaming it with a timestamp
71+ pub fn rotate (self : * Logger ) ! void {
72+ if (self .main_log ) | main_log_file | {
73+ main_log_file .close ();
74+ self .main_log = null ;
75+ }
76+
77+ var ts_buf : [24 ]u8 = undefined ;
78+ const ts = timestampISO (& ts_buf );
79+
80+ var rotated_path_buf : [512 ]u8 = undefined ;
81+ const rotated_path = std .fmt .bufPrint (& rotated_path_buf , "{s}.{s}" , .{
82+ self .main_log_path ,
83+ ts ,
84+ }) catch return error .PathTooLong ;
85+
86+ fs .cwd ().rename (self .main_log_path , rotated_path ) catch | err | {
87+ if (err != error .FileNotFound ) {
88+ return err ;
89+ }
90+ };
91+
92+ const new_log = fs .cwd ().createFile (self .main_log_path , .{}) catch {
93+ return error .CreationFailed ;
94+ };
95+ self .main_log = new_log ;
96+ }
97+
98+ /// Clean up old log files based on retention period (in days)
99+ pub fn cleanup (self : * Logger , log_retention : u32 ) ! void {
100+ const now = std .time .timestamp ();
101+ const cutoff_timestamp = now - (@as (i64 , log_retention ) * 24 * 60 * 60 );
102+
103+ const logs_dir_name = std .fs .path .dirname (self .main_log_path ) orelse return ;
104+ var dir = try fs .cwd ().openDir (logs_dir_name , .{ .iterate = true });
105+ defer dir .close ();
106+
107+ var walker = try dir .walk (self .allocator );
108+ defer walker .deinit ();
109+
110+ while (try walker .next ()) | entry | {
111+ if (entry .kind != .file ) continue ;
112+
113+ const path = entry .path ;
114+ if (! std .mem .endsWith (u8 , path , ".log" )) continue ;
115+ if (std .mem .endsWith (u8 , path , "main.log" )) continue ;
116+
117+ const full_path = try std .fs .path .join (self .allocator , &.{ logs_dir_name , path });
118+ defer self .allocator .free (full_path );
119+
120+ const file = try dir .openFile (path , .{});
121+ defer file .close ();
122+
123+ const stat = try file .stat ();
124+ const mtime = stat .mtime ;
125+
126+ if (mtime < cutoff_timestamp ) {
127+ dir .deleteFile (path ) catch {};
128+ }
129+ }
130+
131+ self .cleanupRotatedLogs (cutoff_timestamp ) catch {};
132+ self .cleanupCycleLogs (log_retention ) catch {};
133+ }
134+
135+ fn cleanupRotatedLogs (self : * Logger , cutoff_timestamp : i64 ) ! void {
136+ const logs_dir = std .fs .path .dirname (self .main_log_path ) orelse return ;
137+
138+ var dir = try fs .cwd ().openDir (logs_dir , .{ .iterate = true });
139+ defer dir .close ();
140+
141+ var walker = try dir .walk (self .allocator );
142+ defer walker .deinit ();
143+
144+ while (try walker .next ()) | entry | {
145+ if (entry .kind != .file ) continue ;
146+
147+ const path = entry .path ;
148+ if (! std .mem .startsWith (u8 , path , "main.log." )) continue ;
149+
150+ const full_path = try std .fs .path .join (self .allocator , &.{ logs_dir , path });
151+ defer self .allocator .free (full_path );
152+
153+ const file = try dir .openFile (path , .{});
154+ defer file .close ();
155+
156+ const stat = try file .stat ();
157+ const mtime = stat .mtime ;
158+
159+ if (mtime < cutoff_timestamp ) {
160+ dir .deleteFile (path ) catch {};
161+ }
162+ }
163+ }
164+
165+ fn cleanupCycleLogs (self : * Logger , log_retention : u32 ) ! void {
166+ const now = std .time .timestamp ();
167+ const cutoff_timestamp = now - (@as (i64 , log_retention ) * 24 * 60 * 60 );
168+
169+ var dir = try fs .cwd ().openDir (self .cycle_log_dir , .{ .iterate = true });
170+ defer dir .close ();
171+
172+ var walker = try dir .walk (self .allocator );
173+ defer walker .deinit ();
174+
175+ while (try walker .next ()) | entry | {
176+ if (entry .kind != .file ) continue ;
177+
178+ const path = entry .path ;
179+ if (! std .mem .startsWith (u8 , path , "cycle_" ) or ! std .mem .endsWith (u8 , path , ".log" )) continue ;
180+
181+ const file = try dir .openFile (path , .{});
182+ defer file .close ();
183+
184+ const stat = try file .stat ();
185+ const mtime = stat .mtime ;
186+
187+ if (mtime < cutoff_timestamp ) {
188+ dir .deleteFile (path ) catch {};
189+ }
190+ }
191+ }
192+
65193 /// Set current cycle number
66194 pub fn setCycle (self : * Logger , cycle : u32 ) void {
67195 self .cycle = cycle ;
@@ -271,3 +399,53 @@ test "timestampISO generates valid ISO 8601 format" {
271399 try std .testing .expectEqual (@as (u8 , ':' ), ts [16 ]);
272400 try std .testing .expectEqual (@as (u8 , 'Z' ), ts [19 ]);
273401}
402+
403+ test "rotate creates rotated log file" {
404+ const test_dir = "/tmp/opencoder_test_rotate" ;
405+ const allocator = std .testing .allocator ;
406+
407+ fs .cwd ().deleteTree (test_dir ) catch {};
408+ defer fs .cwd ().deleteTree (test_dir ) catch {};
409+
410+ try fs .cwd ().makePath (test_dir );
411+ const logs_dir = try std .fs .path .join (allocator , &.{ test_dir , "logs" , "cycles" });
412+ defer allocator .free (logs_dir );
413+ try fs .cwd ().makePath (logs_dir );
414+
415+ var log = try Logger .init (test_dir , false , allocator );
416+ defer log .deinit ();
417+
418+ log .log ("test message" );
419+
420+ try log .rotate ();
421+
422+ try std .testing .expect (fsutil .fileExists (log .main_log_path ));
423+ }
424+
425+ test "cleanup removes old cycle logs" {
426+ const test_dir = "/tmp/opencoder_test_cleanup" ;
427+ const allocator = std .testing .allocator ;
428+
429+ fs .cwd ().deleteTree (test_dir ) catch {};
430+ defer fs .cwd ().deleteTree (test_dir ) catch {};
431+
432+ try fs .cwd ().makePath (test_dir );
433+ const cycle_dir = try std .fs .path .join (allocator , &.{ test_dir , "logs" , "cycles" });
434+ defer allocator .free (cycle_dir );
435+ try fs .cwd ().makePath (cycle_dir );
436+
437+ var log = try Logger .init (test_dir , false , allocator );
438+ defer log .deinit ();
439+
440+ const cycle_path = try std .fs .path .join (allocator , &.{ cycle_dir , "cycle_001.log" });
441+ defer allocator .free (cycle_path );
442+
443+ const cycle_file = try fs .cwd ().createFile (cycle_path , .{});
444+ cycle_file .close ();
445+
446+ try std .testing .expect (fsutil .fileExists (cycle_path ));
447+
448+ try log .cleanup (30 );
449+
450+ try std .testing .expect (fsutil .fileExists (cycle_path ));
451+ }
0 commit comments