Skip to content

Commit e080d79

Browse files
stefanskoricdevshared-context-engineering
authored andcommitted
observability: Add configurable log file retention limit
Add a positive log_file_retention_limit config value with a default of 10 and expose its resolved provenance through config show. Apply the resolved limit to retention cleanup for primary and v2 log files. Co-authored-by: SCE <sce@crocoder.dev>
1 parent 4f683e2 commit e080d79

15 files changed

Lines changed: 103 additions & 29 deletions

File tree

cli/assets/generated/config/schema/sce-config.schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
"type": "string",
3030
"minLength": 1
3131
},
32+
"log_file_retention_limit": {
33+
"default": 10,
34+
"type": "integer",
35+
"minimum": 1
36+
},
3237
"timeout_ms": {
3338
"type": "integer",
3439
"minimum": 0

cli/src/services/config/render.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ pub(super) fn format_show_output(runtime: &RuntimeConfig, report_format: ReportF
6767
runtime.log_format.source,
6868
),
6969
"log_dir": format_optional_resolved_value_json(&runtime.log_dir),
70+
"log_file_retention_limit": format_resolved_value_json(
71+
runtime.log_file_retention_limit.value,
72+
runtime.log_file_retention_limit.source,
73+
),
7074
"timeout_ms": {
7175
"value": runtime.timeout_ms.value,
7276
"source": runtime.timeout_ms.source.as_str(),
@@ -217,6 +221,11 @@ fn format_observability_text_lines(runtime: &RuntimeConfig) -> Vec<String> {
217221
runtime.log_format.source,
218222
),
219223
format_optional_resolved_value_text("log_dir", &runtime.log_dir),
224+
format_resolved_value_text(
225+
"log_file_retention_limit",
226+
&runtime.log_file_retention_limit.value.to_string(),
227+
runtime.log_file_retention_limit.source,
228+
),
220229
]
221230
}
222231

cli/src/services/config/resolver.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ use super::types::{
1818
parse_bool_value_from, ConfigPathSource, ConfigRequest, DatabaseRetryConfig, LoadedConfigPath,
1919
LogFormat, LogLevel, ReportFormat, ResolvedAgentTraceStorageRuntimeConfig,
2020
ResolvedAuthRuntimeConfig, ResolvedHookRuntimeConfig, ResolvedObservabilityRuntimeConfig,
21-
ResolvedOptionalValue, ResolvedValue, ValueSource, ENV_ATTRIBUTION_HOOKS_DISABLED, ENV_LOG_DIR,
22-
ENV_LOG_FORMAT, ENV_LOG_LEVEL,
21+
ResolvedOptionalValue, ResolvedValue, ValueSource, DEFAULT_LOG_FILE_RETENTION_LIMIT,
22+
ENV_ATTRIBUTION_HOOKS_DISABLED, ENV_LOG_DIR, ENV_LOG_FORMAT, ENV_LOG_LEVEL,
2323
};
2424

2525
const DEFAULT_TIMEOUT_MS: u64 = 30000;
@@ -62,6 +62,7 @@ pub(super) struct RuntimeConfig {
6262
pub(super) log_level: ResolvedValue<LogLevel>,
6363
pub(super) log_format: ResolvedValue<LogFormat>,
6464
pub(super) log_dir: ResolvedOptionalValue<String>,
65+
pub(super) log_file_retention_limit: ResolvedValue<usize>,
6566
pub(super) timeout_ms: ResolvedValue<u64>,
6667
pub(super) attribution_hooks_enabled: ResolvedValue<bool>,
6768
pub(super) workos_client_id: ResolvedOptionalValue<String>,
@@ -222,6 +223,7 @@ where
222223
log_level: runtime.log_level.value,
223224
log_format: runtime.log_format.value,
224225
log_dir: runtime.log_dir.value,
226+
log_file_retention_limit: runtime.log_file_retention_limit.value,
225227
loaded_config_paths: runtime.loaded_config_paths,
226228
validation_errors: runtime.validation_errors,
227229
})
@@ -298,6 +300,7 @@ where
298300
log_level: None,
299301
log_format: None,
300302
log_dir: None,
303+
log_file_retention_limit: None,
301304
timeout_ms: None,
302305
attribution_hooks_enabled: None,
303306
workos_client_id: None,
@@ -328,6 +331,9 @@ where
328331
if let Some(log_dir) = layer.log_dir {
329332
file_config.log_dir = Some(log_dir);
330333
}
334+
if let Some(log_file_retention_limit) = layer.log_file_retention_limit {
335+
file_config.log_file_retention_limit = Some(log_file_retention_limit);
336+
}
331337
if let Some(timeout_ms) = layer.timeout_ms {
332338
file_config.timeout_ms = Some(timeout_ms);
333339
}
@@ -411,6 +417,17 @@ where
411417
default_observability_log_dir()?
412418
};
413419

420+
let resolved_log_file_retention_limit = match file_config.log_file_retention_limit {
421+
Some(value) => ResolvedValue {
422+
value: value.value,
423+
source: ValueSource::ConfigFile(value.source),
424+
},
425+
None => ResolvedValue {
426+
value: DEFAULT_LOG_FILE_RETENTION_LIMIT,
427+
source: ValueSource::Default,
428+
},
429+
};
430+
414431
let mut resolved_timeout_ms = ResolvedValue {
415432
value: DEFAULT_TIMEOUT_MS,
416433
source: ValueSource::Default,
@@ -499,6 +516,7 @@ where
499516
log_level: resolved_log_level,
500517
log_format: resolved_log_format,
501518
log_dir: resolved_log_dir,
519+
log_file_retention_limit: resolved_log_file_retention_limit,
502520
timeout_ms: resolved_timeout_ms,
503521
attribution_hooks_enabled: resolved_attribution_hooks_enabled,
504522
workos_client_id: resolved_workos_client_id,

cli/src/services/config/schema.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub(crate) const TOP_LEVEL_CONFIG_KEYS: &[&str] = &[
3333
"log_level",
3434
"log_format",
3535
"log_dir",
36+
"log_file_retention_limit",
3637
"timeout_ms",
3738
super::resolver::WORKOS_CLIENT_ID_KEY.config_key,
3839
"agent_trace",
@@ -41,7 +42,7 @@ pub(crate) const TOP_LEVEL_CONFIG_KEYS: &[&str] = &[
4142
];
4243

4344
pub(crate) const TOP_LEVEL_CONFIG_KEYS_DESCRIPTION: &str =
44-
"$schema, log_level, log_format, timeout_ms, workos_client_id, agent_trace, policies, integrations, log_dir";
45+
"$schema, log_level, log_format, timeout_ms, workos_client_id, agent_trace, policies, integrations, log_dir, log_file_retention_limit";
4546

4647
static CONFIG_SCHEMA_VALIDATOR: OnceLock<Validator> = OnceLock::new();
4748

@@ -68,6 +69,7 @@ pub(crate) struct ParsedFileConfigDocument {
6869
pub(crate) log_level: Option<String>,
6970
pub(crate) log_format: Option<String>,
7071
pub(crate) log_dir: Option<String>,
72+
pub(crate) log_file_retention_limit: Option<usize>,
7173
pub(crate) timeout_ms: Option<u64>,
7274
pub(crate) workos_client_id: Option<String>,
7375
pub(crate) agent_trace: Option<ParsedAgentTraceConfigDocument>,
@@ -151,6 +153,7 @@ pub(crate) struct FileConfig {
151153
pub(crate) log_level: Option<FileConfigValue<LogLevel>>,
152154
pub(crate) log_format: Option<FileConfigValue<LogFormat>>,
153155
pub(crate) log_dir: Option<FileConfigValue<String>>,
156+
pub(crate) log_file_retention_limit: Option<FileConfigValue<usize>>,
154157
pub(crate) timeout_ms: Option<FileConfigValue<u64>>,
155158
pub(crate) attribution_hooks_enabled: Option<FileConfigValue<bool>>,
156159
pub(crate) workos_client_id: Option<FileConfigValue<String>>,
@@ -285,6 +288,9 @@ pub(crate) fn parse_file_config(
285288
})
286289
.transpose()?;
287290
let log_dir = typed.log_dir.map(|value| FileConfigValue { value, source });
291+
let log_file_retention_limit = typed
292+
.log_file_retention_limit
293+
.map(|value| FileConfigValue { value, source });
288294
let timeout_ms = typed
289295
.timeout_ms
290296
.map(|value| FileConfigValue { value, source });
@@ -301,6 +307,7 @@ pub(crate) fn parse_file_config(
301307
log_level,
302308
log_format,
303309
log_dir,
310+
log_file_retention_limit,
304311
timeout_ms,
305312
attribution_hooks_enabled,
306313
workos_client_id,

cli/src/services/config/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub(crate) const ENV_LOG_LEVEL: &str = "SCE_LOG_LEVEL";
1717
pub(crate) const ENV_LOG_FORMAT: &str = "SCE_LOG_FORMAT";
1818
pub(crate) const ENV_LOG_DIR: &str = "SCE_LOG_DIR";
1919
pub(crate) const ENV_ATTRIBUTION_HOOKS_DISABLED: &str = "SCE_ATTRIBUTION_HOOKS_DISABLED";
20+
pub(crate) const DEFAULT_LOG_FILE_RETENTION_LIMIT: usize = 10;
2021

2122
pub type ReportFormat = OutputFormat;
2223

@@ -197,6 +198,7 @@ pub(crate) struct ResolvedObservabilityRuntimeConfig {
197198
pub(crate) log_level: LogLevel,
198199
pub(crate) log_format: LogFormat,
199200
pub(crate) log_dir: Option<String>,
201+
pub(crate) log_file_retention_limit: usize,
200202
pub(crate) loaded_config_paths: Vec<LoadedConfigPath>,
201203
pub(crate) validation_errors: Vec<String>,
202204
}

cli/src/services/observability.rs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ pub const NAME: &str = "observability";
2626
const LOG_FILE_PREFIX: &str = "sce";
2727
const LOG_FILE_EXTENSION: &str = "log";
2828
const EMPTY_SESSION_ID_TOKEN: &str = "%EMPTY";
29-
const LOG_FILE_RETENTION_LIMIT: usize = 10;
3029

3130
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3231
pub struct ObservabilityConfig {
@@ -47,6 +46,7 @@ impl Default for ObservabilityConfig {
4746
pub struct Logger {
4847
config: ObservabilityConfig,
4948
log_dir: Option<PathBuf>,
49+
log_file_retention_limit: usize,
5050
}
5151

5252
impl Logger {
@@ -63,6 +63,7 @@ impl Logger {
6363
format: config.log_format,
6464
},
6565
log_dir: config.log_dir.as_deref().map(PathBuf::from),
66+
log_file_retention_limit: config.log_file_retention_limit,
6667
})
6768
}
6869

@@ -87,7 +88,11 @@ impl Logger {
8788
log_dir = Some(PathBuf::from(raw));
8889
}
8990

90-
Ok(Self { config, log_dir })
91+
Ok(Self {
92+
config,
93+
log_dir,
94+
log_file_retention_limit: config::DEFAULT_LOG_FILE_RETENTION_LIMIT,
95+
})
9196
}
9297

9398
pub fn info(
@@ -188,7 +193,7 @@ impl Logger {
188193
};
189194

190195
let path = current_log_path(log_dir, session_id);
191-
append_log_line(&path, redacted_line)
196+
append_log_line(&path, redacted_line, self.log_file_retention_limit)
192197
}
193198

194199
fn enabled(&self, level: LogLevel) -> bool {
@@ -294,8 +299,10 @@ fn sanitize_session_id_for_filename(session_id: &str) -> String {
294299
sanitized
295300
}
296301

297-
fn append_log_line(path: &Path, redacted_line: &str) -> Result<()> {
298-
append_log_line_with_cleanup(path, redacted_line, enforce_log_retention)
302+
fn append_log_line(path: &Path, redacted_line: &str, retention_limit: usize) -> Result<()> {
303+
append_log_line_with_cleanup(path, redacted_line, |log_dir| {
304+
enforce_log_retention(log_dir, retention_limit)
305+
})
299306
}
300307

301308
fn append_log_line_with_cleanup<F>(path: &Path, redacted_line: &str, cleanup: F) -> Result<()>
@@ -440,11 +447,15 @@ struct ManagedLogFile {
440447
modified: SystemTime,
441448
}
442449

443-
fn enforce_log_retention(log_dir: &Path) -> Result<()> {
444-
enforce_log_retention_with(log_dir, |path| fs::remove_file(path))
450+
fn enforce_log_retention(log_dir: &Path, retention_limit: usize) -> Result<()> {
451+
enforce_log_retention_with(log_dir, retention_limit, |path| fs::remove_file(path))
445452
}
446453

447-
fn enforce_log_retention_with<F>(log_dir: &Path, mut remove_file: F) -> Result<()>
454+
fn enforce_log_retention_with<F>(
455+
log_dir: &Path,
456+
retention_limit: usize,
457+
mut remove_file: F,
458+
) -> Result<()>
448459
where
449460
F: FnMut(&Path) -> io::Result<()>,
450461
{
@@ -455,7 +466,7 @@ where
455466
ordering => ordering,
456467
});
457468

458-
for managed_file in managed_files.into_iter().skip(LOG_FILE_RETENTION_LIMIT) {
469+
for managed_file in managed_files.into_iter().skip(retention_limit) {
459470
if let Err(error) = remove_file(&managed_file.path) {
460471
errors.push(format!(
461472
"failed to remove old log file '{}': {error}",

config/pkl/base/sce-config-schema.pkl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ local sceConfigSchema = new JsonSchema {
7474
type = "string"
7575
minLength = 1
7676
}
77+
["log_file_retention_limit"] = new JsonSchema {
78+
type = "integer"
79+
minimum = 1
80+
default = 10
81+
}
7782
["timeout_ms"] = new JsonSchema {
7883
type = "integer"
7984
minimum = 0

config/schema/sce-config.schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
"type": "string",
3030
"minLength": 1
3131
},
32+
"log_file_retention_limit": {
33+
"default": 10,
34+
"type": "integer",
35+
"minimum": 1
36+
},
3237
"timeout_ms": {
3338
"type": "integer",
3439
"minimum": 0

0 commit comments

Comments
 (0)