Skip to content

Commit dc0dac3

Browse files
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 49f0325 commit dc0dac3

15 files changed

Lines changed: 103 additions & 28 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
@@ -58,6 +58,10 @@ pub(super) fn format_show_output(runtime: &RuntimeConfig, report_format: ReportF
5858
runtime.log_format.source,
5959
),
6060
"log_dir": format_optional_resolved_value_json(&runtime.log_dir),
61+
"log_file_retention_limit": format_resolved_value_json(
62+
runtime.log_file_retention_limit.value,
63+
runtime.log_file_retention_limit.source,
64+
),
6165
"timeout_ms": {
6266
"value": runtime.timeout_ms.value,
6367
"source": runtime.timeout_ms.source.as_str(),
@@ -201,6 +205,11 @@ fn format_observability_text_lines(runtime: &RuntimeConfig) -> Vec<String> {
201205
runtime.log_format.source,
202206
),
203207
format_optional_resolved_value_text("log_dir", &runtime.log_dir),
208+
format_resolved_value_text(
209+
"log_file_retention_limit",
210+
&runtime.log_file_retention_limit.value.to_string(),
211+
runtime.log_file_retention_limit.source,
212+
),
204213
]
205214
}
206215

cli/src/services/config/resolver.rs

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

2425
const DEFAULT_TIMEOUT_MS: u64 = 30000;
@@ -60,6 +61,7 @@ pub(super) struct RuntimeConfig {
6061
pub(super) log_level: ResolvedValue<LogLevel>,
6162
pub(super) log_format: ResolvedValue<LogFormat>,
6263
pub(super) log_dir: ResolvedOptionalValue<String>,
64+
pub(super) log_file_retention_limit: ResolvedValue<usize>,
6365
pub(super) timeout_ms: ResolvedValue<u64>,
6466
pub(super) attribution_hooks_enabled: ResolvedValue<bool>,
6567
pub(super) workos_client_id: ResolvedOptionalValue<String>,
@@ -192,6 +194,7 @@ where
192194
log_level: runtime.log_level.value,
193195
log_format: runtime.log_format.value,
194196
log_dir: runtime.log_dir.value,
197+
log_file_retention_limit: runtime.log_file_retention_limit.value,
195198
loaded_config_paths: runtime.loaded_config_paths,
196199
validation_errors: runtime.validation_errors,
197200
})
@@ -268,6 +271,7 @@ where
268271
log_level: None,
269272
log_format: None,
270273
log_dir: None,
274+
log_file_retention_limit: None,
271275
timeout_ms: None,
272276
attribution_hooks_enabled: None,
273277
workos_client_id: None,
@@ -296,6 +300,9 @@ where
296300
if let Some(log_dir) = layer.log_dir {
297301
file_config.log_dir = Some(log_dir);
298302
}
303+
if let Some(log_file_retention_limit) = layer.log_file_retention_limit {
304+
file_config.log_file_retention_limit = Some(log_file_retention_limit);
305+
}
299306
if let Some(timeout_ms) = layer.timeout_ms {
300307
file_config.timeout_ms = Some(timeout_ms);
301308
}
@@ -373,6 +380,17 @@ where
373380
default_observability_log_dir()?
374381
};
375382

383+
let resolved_log_file_retention_limit = match file_config.log_file_retention_limit {
384+
Some(value) => ResolvedValue {
385+
value: value.value,
386+
source: ValueSource::ConfigFile(value.source),
387+
},
388+
None => ResolvedValue {
389+
value: DEFAULT_LOG_FILE_RETENTION_LIMIT,
390+
source: ValueSource::Default,
391+
},
392+
};
393+
376394
let mut resolved_timeout_ms = ResolvedValue {
377395
value: DEFAULT_TIMEOUT_MS,
378396
source: ValueSource::Default,
@@ -439,6 +457,7 @@ where
439457
log_level: resolved_log_level,
440458
log_format: resolved_log_format,
441459
log_dir: resolved_log_dir,
460+
log_file_retention_limit: resolved_log_file_retention_limit,
442461
timeout_ms: resolved_timeout_ms,
443462
attribution_hooks_enabled: resolved_attribution_hooks_enabled,
444463
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,14 +33,15 @@ 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
"policies",
3940
"integrations",
4041
];
4142

4243
pub(crate) const TOP_LEVEL_CONFIG_KEYS_DESCRIPTION: &str =
43-
"$schema, log_level, log_format, log_dir, timeout_ms, workos_client_id, policies, integrations";
44+
"$schema, log_level, log_format, log_dir, log_file_retention_limit, timeout_ms, workos_client_id, policies, integrations";
4445

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

@@ -67,6 +68,7 @@ pub(crate) struct ParsedFileConfigDocument {
6768
pub(crate) log_level: Option<String>,
6869
pub(crate) log_format: Option<String>,
6970
pub(crate) log_dir: Option<String>,
71+
pub(crate) log_file_retention_limit: Option<usize>,
7072
pub(crate) timeout_ms: Option<u64>,
7173
pub(crate) workos_client_id: Option<String>,
7274
pub(crate) policies: Option<ParsedPoliciesConfigDocument>,
@@ -142,6 +144,7 @@ pub(crate) struct FileConfig {
142144
pub(crate) log_level: Option<FileConfigValue<LogLevel>>,
143145
pub(crate) log_format: Option<FileConfigValue<LogFormat>>,
144146
pub(crate) log_dir: Option<FileConfigValue<String>>,
147+
pub(crate) log_file_retention_limit: Option<FileConfigValue<usize>>,
145148
pub(crate) timeout_ms: Option<FileConfigValue<u64>>,
146149
pub(crate) attribution_hooks_enabled: Option<FileConfigValue<bool>>,
147150
pub(crate) workos_client_id: Option<FileConfigValue<String>>,
@@ -274,6 +277,9 @@ pub(crate) fn parse_file_config(
274277
})
275278
.transpose()?;
276279
let log_dir = typed.log_dir.map(|value| FileConfigValue { value, source });
280+
let log_file_retention_limit = typed
281+
.log_file_retention_limit
282+
.map(|value| FileConfigValue { value, source });
277283
let timeout_ms = typed
278284
.timeout_ms
279285
.map(|value| FileConfigValue { value, source });
@@ -288,6 +294,7 @@ pub(crate) fn parse_file_config(
288294
log_level,
289295
log_format,
290296
log_dir,
297+
log_file_retention_limit,
291298
timeout_ms,
292299
attribution_hooks_enabled,
293300
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)