Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 82 additions & 22 deletions crates/devolutions-log/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ pub trait StaticLogConfig {
const LOG_FILE_PREFIX: &'static str;
}

/// Metadata for a retained log file in the configured log directory.
pub struct LogFile {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missing a doc comment on this new public type. Also the rest of this crate's API speaks Utf8Path, while path here is a std::path::PathBuf. Not a blocker.


LLM Wrote, Unaudited — Read With a Grain of Salt.

pub path: std::path::PathBuf,
pub name: String,
pub size: u64,
pub modified: SystemTime,
}

pub struct LoggerGuard {
_file_guard: WorkerGuard,
_stdio_guard: WorkerGuard,
Expand Down Expand Up @@ -91,38 +99,90 @@ pub fn init<C: StaticLogConfig>(
})
}

/// Find latest log file (by age)
/// Finds the newest retained regular log file by modification time.
///
/// Given path is used to filter out by file name prefix.
/// Uses the same file filter and order as [`list_log_files`].
#[instrument]
pub async fn find_latest_log_file<C: StaticLogConfig>(prefix: &Utf8Path) -> anyhow::Result<std::path::PathBuf> {
let cfg = LogPathCfg::<C>::from_path(prefix)?;
list_log_files::<C>(prefix)
.await?
.into_iter()
.next()
.map(|file| file.path)
.context("no file found")
}

/// Lists the regular log files retained under the configured log path.
pub async fn list_log_files<C: StaticLogConfig>(path: &Utf8Path) -> anyhow::Result<Vec<LogFile>> {
let cfg = LogPathCfg::<C>::from_path(path)?;
let mut read_dir = fs::read_dir(cfg.folder).await.context("couldn't read log directory")?;
let mut files = Vec::new();

let mut read_dir = fs::read_dir(cfg.folder).await.context("couldn't read directory")?;
while let Some(entry) = read_dir.next_entry().await.context("couldn't read log entry")? {
let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
continue;
};

let mut most_recent_time = SystemTime::UNIX_EPOCH;
let mut most_recent = None;
if !name.starts_with(cfg.prefix) || !name.ends_with(".log") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: find_latest_log_file uses contains("log") while this uses ends_with(".log"), so /logs and /logs/files can disagree on what a log file is. Also get_logs still does a plain File::open that follows symlinks. Could get_logs just take the first entry of list_log_files and go through open_regular_log_file? Then we have one definition and the same hardening on both. Can be a follow up!


LLM Wrote, Unaudited — Read With a Grain of Salt.

continue;
}

while let Ok(Some(entry)) = read_dir.next_entry().await {
match entry.file_name().to_str() {
Some(file_name) if file_name.starts_with(cfg.prefix) && file_name.contains("log") => {
debug!(file_name, "Found a log file");
match entry.metadata().await.and_then(|metadata| metadata.modified()) {
Ok(modified) if modified > most_recent_time => {
most_recent_time = modified;
most_recent = Some(entry.path());
}
Ok(_) => {}
Err(error) => {
warn!(%error, file_name, "Couldn't retrieve metadata for file");
}
}
let file_type = match entry.file_type().await {
Ok(file_type) => file_type,
Err(error) => {
warn!(%error, name, "Couldn't read log file type");
continue;
}
_ => continue,
};
if !file_type.is_file() {
continue;
}

let metadata = match entry.metadata().await {
Ok(metadata) => metadata,
Err(error) => {
warn!(%error, name, "Couldn't read log metadata");
continue;
}
};
let modified = match metadata.modified() {
Ok(modified) => modified,
Err(error) => {
warn!(%error, name, "Couldn't read log modification time");
continue;
}
};
files.push(LogFile {
path: entry.path(),
name,
size: metadata.len(),
modified,
});
}

files.sort_by(|left, right| {
right
.modified
.cmp(&left.modified)
.then_with(|| compare_log_file_names(&right.name, &left.name))
});
Ok(files)
}

fn compare_log_file_names(left: &str, right: &str) -> std::cmp::Ordering {
let rotations = left
.strip_suffix(".log")
.and_then(|name| name.rsplit_once('.'))
.zip(right.strip_suffix(".log").and_then(|name| name.rsplit_once('.')));

if let Some(((left_base, left_rotation), (right_base, right_rotation))) = rotations
&& left_base == right_base
&& let (Ok(left_rotation), Ok(right_rotation)) = (left_rotation.parse::<u64>(), right_rotation.parse::<u64>())
{
return left_rotation.cmp(&right_rotation).then_with(|| left.cmp(right));
}

most_recent.context("no file found")
left.cmp(right)
}

/// File deletion task (by age)
Expand Down
73 changes: 73 additions & 0 deletions devolutions-gateway/openapi/gateway-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,63 @@ paths:
security:
- scope_token:
- gateway.diagnostics.read
/jet/diagnostics/logs/files:
get:
tags:
- Diagnostics
summary: Lists the log files retained by this Gateway.
operationId: ListLogFiles
responses:
'200':
description: Retained log files
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/LogFileInfo'
'401':
description: Invalid or missing authorization token
'403':
description: Insufficient permissions
'500':
description: Failed to list log files
security:
- scope_token:
- gateway.diagnostics.read
/jet/diagnostics/logs/files/{name}:
get:
tags:
- Diagnostics
summary: Retrieves one retained log file by its listed name.
operationId: GetLogFile
parameters:
- name: name
in: path
description: Name returned by ListLogFiles
required: true
schema:
type: string
responses:
'200':
description: Log file contents
content:
text/plain:
schema:
type: string
'400':
description: Invalid log file name
'401':
description: Invalid or missing authorization token
'403':
description: Insufficient permissions
'404':
description: Log file not found
'500':
description: Failed to read log file
security:
- scope_token:
- gateway.diagnostics.read
/jet/health:
get:
tags:
Expand Down Expand Up @@ -2223,6 +2280,22 @@ components:
internal_url:
type: string
description: URL to use on local network
LogFileInfo:
type: object
required:
- name
- size
- modifiedUnixMillis
properties:
modifiedUnixMillis:
type: integer
format: int64
name:
type: string
size:
type: integer
format: int64
minimum: 0
MonitorDefinition:
type: object
required:
Expand Down
Loading
Loading