Skip to content

Commit ced4b85

Browse files
committed
feat: write portal log files
1 parent 9878035 commit ced4b85

8 files changed

Lines changed: 206 additions & 23 deletions

File tree

ab/src/main.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,12 @@ fn maybe_start_managed_portal(
3535
return Ok(None);
3636
}
3737

38+
let socket_path = per_container_portal_socket_path();
39+
agent_portal::logging::init(None, Some(&socket_path))?;
40+
3841
Ok(Some(agent_portal::host::spawn_managed(
3942
config.portal.clone(),
40-
per_container_portal_socket_path(),
43+
socket_path,
4144
)?))
4245
}
4346

docs/src/how-to/portal/debug-wrapper-failures.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,21 @@ Find and fix common failures for `wl-paste`/`gh` wrappers and other Portal clien
2222
```
2323
4. Enable host logs:
2424
```bash
25-
RUST_LOG=debug agent-portal-host
25+
agent-portal-host --log-level debug
2626
```
27+
Or use `RUST_LOG` for a more specific tracing filter:
28+
```bash
29+
RUST_LOG=agent_portal=debug,agent_portal_host=trace agent-portal-host
30+
```
31+
5. Inspect the log file:
32+
- Log files live under:
33+
```text
34+
${XDG_STATE_HOME:-$HOME/.local/state}/agent-box/logs/
35+
```
36+
- The log filename matches the socket filename, with `.sock` replaced by `.log`.
37+
- Example: `portal.sock` -> `portal.log`
38+
- In managed per-container mode (`[portal].global = false`), each spawned socket gets its own matching log file.
39+
- Use `RUST_LOG=debug ab spawn ...` if you want more verbose managed-host logs.
2740

2841
## Common failures
2942

docs/src/reference/common/env-vars.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@
1313

1414
- `RUST_LOG`
1515
- Controls tracing filter for `agent-portal-host` and other Rust binaries using tracing subscriber.
16+
- `agent-portal-host --log-level <level>` is a simpler shortcut when you only need a single level.
17+
18+
- `XDG_STATE_HOME`
19+
- Used to resolve the default Portal log directory.
20+
- Default Portal log directory: `$XDG_STATE_HOME/agent-box/logs/`
21+
- Fallback when unset: `~/.local/state/agent-box/logs/`
22+
- Each Portal log filename is derived from the socket filename, replacing `.sock` with `.log`
1623

1724
## Runtime passthrough
1825

docs/src/reference/portal/config.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ Portal config lives under `[portal]` in `~/.agent-box.toml`.
1212
- used directly when `global = true`
1313
- ignored by `ab spawn` when `global = false`, because `ab` allocates a unique per-container socket path
1414
- `prompt_command` (string|null, default: unset)
15+
- Logging is controlled at process startup, not in config:
16+
- `agent-portal-host --log-level <trace|debug|info|warn|error>` sets a simple level
17+
- `RUST_LOG=...` provides full tracing filter control
18+
- logs are written under `${XDG_STATE_HOME:-~/.local/state}/agent-box/logs/`
19+
- each log filename is derived from the socket filename, replacing `.sock` with `.log`
1520
- `timeouts.request_ms` (u64, default: `0` = no timeout)
1621
- `timeouts.prompt_ms` (u64, default: `0` = no timeout)
1722
- `limits.max_inflight` (usize, default: `32`)

portal/README.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,34 @@ Policy mode is configured in `~/.agent-box.toml` via `portal.policy.defaults.gh_
2525

2626
## Logging
2727

28-
`agent-portal-host` uses `tracing` + `RUST_LOG` filtering.
28+
`agent-portal-host` uses `tracing` and writes logs to both stderr and a log file.
2929

30-
Example:
30+
Default log file location:
31+
32+
```text
33+
${XDG_STATE_HOME:-~/.local/state}/agent-box/logs/<socket-name>.log
34+
```
35+
36+
The log filename is derived from the socket filename by replacing `.sock` with `.log`.
37+
38+
Examples:
39+
40+
```text
41+
portal.sock -> portal.log
42+
portal-12345-abc.sock -> portal-12345-abc.log
43+
```
44+
45+
You can set the log level with `--log-level`, or continue using `RUST_LOG` for full tracing filter control.
46+
47+
Examples:
3148

3249
```bash
33-
RUST_LOG=debug agent-portal-host
50+
agent-portal-host --log-level debug
51+
RUST_LOG=agent_portal=debug,agent_portal_host=trace agent-portal-host
3452
```
3553

54+
Managed per-container Portal instances started by `ab spawn` also initialize logging this way, so each managed socket gets a matching per-instance log file.
55+
3656
## Development
3757

3858
From repo root:

portal/src/bin/agent-portal-host.rs

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,31 @@
11
use agent_box_common::config::load_config;
2-
use clap::Parser;
2+
use clap::{Parser, ValueEnum};
33
use eyre::Result;
4-
use std::io::IsTerminal;
54
use std::path::PathBuf;
65
use std::sync::Arc;
76
use std::sync::atomic::AtomicBool;
87
use tracing::error;
9-
use tracing_subscriber::EnvFilter;
8+
9+
#[derive(Copy, Clone, Debug, ValueEnum)]
10+
enum LogLevel {
11+
Trace,
12+
Debug,
13+
Info,
14+
Warn,
15+
Error,
16+
}
17+
18+
impl LogLevel {
19+
fn as_filter(self) -> &'static str {
20+
match self {
21+
Self::Trace => "trace",
22+
Self::Debug => "debug",
23+
Self::Info => "info",
24+
Self::Warn => "warn",
25+
Self::Error => "error",
26+
}
27+
}
28+
}
1029

1130
#[derive(Parser, Debug)]
1231
#[command(name = "agent-portal-host")]
@@ -15,35 +34,46 @@ struct Cli {
1534
/// Override socket path
1635
#[arg(long)]
1736
socket: Option<String>,
18-
}
19-
20-
fn init_logging() {
21-
let env_filter = EnvFilter::try_from_default_env()
22-
.unwrap_or_else(|_| EnvFilter::new("info,agent_portal_host=info"));
2337

24-
tracing_subscriber::fmt()
25-
.with_env_filter(env_filter)
26-
.with_ansi(std::io::stderr().is_terminal())
27-
.init();
38+
/// Set log level for stderr and the portal log file
39+
#[arg(long, value_enum)]
40+
log_level: Option<LogLevel>,
2841
}
2942

3043
fn main() {
31-
init_logging();
44+
let cli = Cli::parse();
45+
46+
let config = match load_config() {
47+
Ok(config) => config,
48+
Err(e) => {
49+
eprintln!("Error: {e}");
50+
std::process::exit(1);
51+
}
52+
};
53+
let socket_path = PathBuf::from(
54+
cli.socket
55+
.clone()
56+
.unwrap_or_else(|| config.portal.socket_path.clone()),
57+
);
58+
59+
if let Err(e) =
60+
agent_portal::logging::init(cli.log_level.map(LogLevel::as_filter), Some(&socket_path))
61+
{
62+
eprintln!("Error: {e}");
63+
std::process::exit(1);
64+
}
3265

33-
if let Err(e) = run() {
66+
if let Err(e) = run(cli, config.portal) {
3467
error!(error = %e, "portal host failed");
3568
std::process::exit(1);
3669
}
3770
}
3871

39-
fn run() -> Result<()> {
72+
fn run(cli: Cli, portal: agent_box_common::portal::PortalConfig) -> Result<()> {
4073
let path = std::env::var("PATH").unwrap_or_default();
4174
let path = path.split(':').collect::<Vec<_>>();
4275
tracing::info!(path = ?path, "PATH");
4376

44-
let cli = Cli::parse();
45-
let config = load_config()?;
46-
let portal = config.portal;
4777
let socket_path = PathBuf::from(cli.socket.unwrap_or_else(|| portal.socket_path.clone()));
4878

4979
agent_portal::host::run_with_config_and_socket(

portal/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod host;
2+
pub mod logging;

portal/src/logging.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use eyre::{Result, WrapErr};
2+
use std::ffi::OsStr;
3+
use std::fs::{self, File, OpenOptions};
4+
use std::io::{self, IsTerminal, Write};
5+
use std::path::{Path, PathBuf};
6+
use std::sync::{Arc, Mutex, OnceLock};
7+
use tracing_subscriber::fmt;
8+
use tracing_subscriber::prelude::*;
9+
use tracing_subscriber::{EnvFilter, registry};
10+
11+
static LOG_PATH: OnceLock<PathBuf> = OnceLock::new();
12+
13+
#[derive(Clone)]
14+
struct SharedFileWriter {
15+
file: Arc<Mutex<File>>,
16+
}
17+
18+
impl Write for SharedFileWriter {
19+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
20+
let mut file = self
21+
.file
22+
.lock()
23+
.map_err(|_| io::Error::other("failed to lock portal log file"))?;
24+
file.write(buf)
25+
}
26+
27+
fn flush(&mut self) -> io::Result<()> {
28+
let mut file = self
29+
.file
30+
.lock()
31+
.map_err(|_| io::Error::other("failed to lock portal log file"))?;
32+
file.flush()
33+
}
34+
}
35+
36+
fn log_dir() -> PathBuf {
37+
if let Some(state_home) = std::env::var_os("XDG_STATE_HOME") {
38+
return PathBuf::from(state_home).join("agent-box").join("logs");
39+
}
40+
41+
if let Some(home) = std::env::var_os("HOME") {
42+
return PathBuf::from(home)
43+
.join(".local")
44+
.join("state")
45+
.join("agent-box")
46+
.join("logs");
47+
}
48+
49+
std::env::temp_dir().join("agent-box").join("logs")
50+
}
51+
52+
pub fn default_log_path(socket_path: Option<&Path>) -> PathBuf {
53+
let file_name = socket_path
54+
.and_then(Path::file_name)
55+
.unwrap_or_else(|| OsStr::new("agent-portal-host.sock"));
56+
let mut log_name = PathBuf::from(file_name);
57+
log_name.set_extension("log");
58+
log_dir().join(log_name)
59+
}
60+
61+
pub fn init(log_level: Option<&str>, socket_path: Option<&Path>) -> Result<PathBuf> {
62+
if let Some(path) = LOG_PATH.get() {
63+
return Ok(path.clone());
64+
}
65+
66+
let log_path = default_log_path(socket_path);
67+
if let Some(parent) = log_path.parent() {
68+
fs::create_dir_all(parent).wrap_err("failed to create portal log directory")?;
69+
}
70+
71+
let file = OpenOptions::new()
72+
.create(true)
73+
.append(true)
74+
.open(&log_path)
75+
.wrap_err_with(|| format!("failed to open portal log file {}", log_path.display()))?;
76+
let file = Arc::new(Mutex::new(file));
77+
78+
let env_filter = match log_level {
79+
Some(level) => EnvFilter::new(level),
80+
None => EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
81+
};
82+
83+
let stderr_is_terminal = std::io::stderr().is_terminal();
84+
let stderr_layer = fmt::layer()
85+
.with_ansi(stderr_is_terminal)
86+
.with_writer(std::io::stderr);
87+
let file_layer = fmt::layer()
88+
.with_ansi(false)
89+
.with_writer(move || SharedFileWriter {
90+
file: Arc::clone(&file),
91+
});
92+
93+
registry()
94+
.with(env_filter)
95+
.with(stderr_layer)
96+
.with(file_layer)
97+
.try_init()
98+
.map_err(|e| eyre::eyre!("failed to initialize portal logging: {e}"))?;
99+
100+
let _ = LOG_PATH.set(log_path.clone());
101+
tracing::info!(log_file = %log_path.display(), "portal logging initialized");
102+
103+
Ok(log_path)
104+
}

0 commit comments

Comments
 (0)