Skip to content

Commit 9ca655f

Browse files
authored
Route access logs to stdout (#1315)
1 parent ebb8a1d commit 9ca655f

7 files changed

Lines changed: 85 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG.md
22

3+
## unreleased
4+
5+
- **Access logs now go to stdout.** SQLPage now writes the single per-request completion log line to stdout with the target `sqlpage::access`, matching common application-server and container logging conventions. Diagnostic logs, warnings, and internal errors still go to stderr. If your `LOG_LEVEL` or `RUST_LOG` filter is scoped to a specific old target such as `sqlpage::webserver::http=info`, add `sqlpage::access=info` so request-completion logs are still emitted. If your log pipeline only collects stderr, update it to collect stdout too.
6+
37
## v0.44.1
48

59
An AI-assisted security audit found three vulnerabilities: one authentication bypass that is high severity for affected OIDC deployments, and two lower-severity issues. It also led to three hardening changes. Upgrade now if you use custom OIDC protected paths.

configuration.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ The environment variable name can optionally be prefixed with `SQLPAGE_`.
6161
Additionnally, when troubleshooting, you can set the
6262
[`LOG_LEVEL`](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html)
6363
environment variable to `sqlpage=debug` to get more detailed logs and see exactly what SQLPage is doing.
64+
Request-completion access logs use the target `sqlpage::access`. Broad filters such as
65+
`sqlpage=info` include them, but target-specific filters such as `sqlpage::webserver::http=info`
66+
must also include `sqlpage::access=info` if you want to keep request logs.
6467

6568
SQLPage also supports [OpenTelemetry](https://opentelemetry.io/) tracing via the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable. See the [SQLPage monitoring example](https://github.com/sqlpage/sqlpage/tree/main/examples/telemetry).
6669

examples/official-site/sqlpage/migrations/66_log_component.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Use it to track what happens and troubleshoot issues.
88
- Running from a terminal (Linux, macOS, or Windows PowerShell/Command Prompt): they show up in the window.
99
- Docker: run `docker logs <container_name>`.
1010
- Linux service (systemd): run `journalctl -u sqlpage`.
11-
- Output is written to [standard error (stderr)](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)).
11+
- This component''s output is written to [standard error (stderr)](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)). SQLPage request access logs are separate and are written to standard output (stdout).
1212
');
1313

1414
INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'log', * FROM (VALUES
@@ -58,4 +58,4 @@ select ''log'' as component,
5858
''403 - failed for '' || coalesce($username, ''None'') as message,
5959
''error'' as level;
6060
```
61-
')
61+
')

examples/official-site/sqlpage/migrations/73_blog_tracing.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ This is much more actionable than a single request duration number.
106106
107107
Tracing is even more useful when logs and traces are connected.
108108
109-
In the example stack, SQLPage writes structured logs to stdout, Promtail forwards them to Loki, and Grafana lets you move from a log line to the matching trace using the trace id. This makes it possible to start from an error log and immediately inspect the full request timeline.
109+
In the example stack, SQLPage writes request access logs to stdout and diagnostic logs to stderr. The OpenTelemetry Collector forwards both streams to Loki, and Grafana lets you move from a log line to the matching trace using the trace id. This makes it possible to start from an error log and immediately inspect the full request timeline.
110110
111111
That workflow is often the difference between guessing and knowing.
112112

examples/telemetry/README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,14 @@ Key attributes on each span:
8585
SQLPage writes one structured log line per event, for example:
8686

8787
```text
88-
ts=2026-03-08T20:56:15.000Z level=info target=sqlpage::webserver::http msg="request completed" http.request.method=GET url.path=/ trace_id=4f2d...
88+
ts=2026-03-08T20:56:15.000Z level=info target=sqlpage::access msg="200 OK" http.request.method=GET url.path=/ trace_id=4f2d...
8989
```
9090

91+
Request-completion access logs use the target `sqlpage::access` and are written to stdout.
92+
Diagnostic logs, warnings, and internal errors are written to stderr. Docker and most
93+
container log drivers collect both streams by default, but custom log pipelines that read
94+
only stderr need to collect stdout as well to keep access logs.
95+
9196
The OpenTelemetry Collector receives these SQLPage container logs through Docker's syslog
9297
logging driver and forwards them to Loki.
9398
The homepage dashboard filters to the `sqlpage` service so you can see request logs update
@@ -367,7 +372,7 @@ not specific to SQLPage.
367372
| `OTEL_SERVICE_NAME` | No | Service name shown in traces (default: `unknown_service`) | `sqlpage` |
368373
| `OTEL_EXPORTER_OTLP_HEADERS` | No | Comma-separated `key=value` pairs for auth headers | `api-key=abc123` |
369374
| `OTEL_EXPORTER_OTLP_PROTOCOL` | No | Protocol (default: `http/protobuf`) | `http/protobuf` |
370-
| `RUST_LOG` | No | Filter which spans/logs are emitted | `sqlpage=debug,tracing_actix_web=info` |
375+
| `RUST_LOG` or `LOG_LEVEL` | No | Filter which spans/logs are emitted | `sqlpage=debug,tracing_actix_web=info` |
371376

372377
When `OTEL_EXPORTER_OTLP_ENDPOINT` is **not set**, SQLPage uses plain text
373378
logging only (same behavior as versions before tracing support was added).
@@ -411,3 +416,10 @@ includes `sqlpage=info`:
411416
```bash
412417
RUST_LOG="sqlpage=info,actix_web=info,tracing_actix_web=info"
413418
```
419+
420+
If you filter individual targets instead of the broader `sqlpage` target, include
421+
the access-log target too:
422+
423+
```bash
424+
RUST_LOG="sqlpage::access=info,sqlpage::webserver::http=info,actix_web=info,tracing_actix_web=info"
425+
```

src/telemetry.rs

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ static OTLP_HTTP_WORKER_SENDER: OnceLock<
2020
Result<tokio::sync::mpsc::UnboundedSender<OtlpHttpJob>, String>,
2121
> = OnceLock::new();
2222
const DEFAULT_ENV_FILTER_DIRECTIVES: &str = "sqlpage=info,actix_web=info,tracing_actix_web=info,opentelemetry=warn,opentelemetry_sdk=warn,opentelemetry_otlp=warn";
23+
pub(crate) const ACCESS_LOG_TARGET: &str = "sqlpage::access";
2324

2425
type OtlpHttpResponse = Result<opentelemetry_http::Response<opentelemetry_http::Bytes>, String>;
2526

@@ -438,26 +439,29 @@ mod logfmt {
438439

439440
#[derive(Copy, Clone)]
440441
enum OutputMode {
441-
Stderr,
442+
StdoutAndStderr,
442443
TestWriter,
443444
}
444445

445446
pub(super) struct LogfmtLayer {
446-
use_colors: bool,
447+
stdout_colors: bool,
448+
stderr_colors: bool,
447449
output_mode: OutputMode,
448450
}
449451

450452
impl LogfmtLayer {
451453
pub fn new() -> Self {
452454
Self {
453-
use_colors: io::stderr().is_terminal(),
454-
output_mode: OutputMode::Stderr,
455+
stdout_colors: io::stdout().is_terminal(),
456+
stderr_colors: io::stderr().is_terminal(),
457+
output_mode: OutputMode::StdoutAndStderr,
455458
}
456459
}
457460

458461
pub fn test_writer() -> Self {
459462
Self {
460-
use_colors: false,
463+
stdout_colors: false,
464+
stderr_colors: false,
461465
output_mode: OutputMode::TestWriter,
462466
}
463467
}
@@ -496,12 +500,13 @@ mod logfmt {
496500

497501
fn on_event(&self, event: &tracing::Event<'_>, ctx: Context<'_, S>) {
498502
let mut buf = String::with_capacity(256);
499-
let colors = self.use_colors;
500503
let level = *event.metadata().level();
501504
let include_all_span_fields = includes_all_span_fields();
502505
let mut event_fields = HashMap::new();
503506
event.record(&mut FieldCollector(&mut event_fields));
504507
let target = event_target(event, &event_fields);
508+
let output_stream = output_stream_for_target(target);
509+
let colors = self.use_colors(output_stream);
505510
let msg = event_fields.get("message");
506511
let multiline_msg = is_multiline_terminal_message(colors, msg);
507512
let include_all_event_fields =
@@ -518,16 +523,50 @@ mod logfmt {
518523
buf.push('\n');
519524
write_multiline_message(&mut buf, msg, multiline_msg);
520525
match self.output_mode {
521-
OutputMode::Stderr => {
522-
let _ = io::Write::write_all(&mut io::stderr().lock(), buf.as_bytes());
523-
}
526+
OutputMode::StdoutAndStderr => write_to_output(output_stream, &buf),
524527
OutputMode::TestWriter => {
525528
eprint!("{buf}");
526529
}
527530
}
528531
}
529532
}
530533

534+
impl LogfmtLayer {
535+
fn use_colors(&self, output_stream: OutputStream) -> bool {
536+
match output_stream {
537+
OutputStream::Stdout => self.stdout_colors,
538+
OutputStream::Stderr => self.stderr_colors,
539+
}
540+
}
541+
}
542+
543+
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
544+
enum OutputStream {
545+
Stdout,
546+
Stderr,
547+
}
548+
549+
fn output_stream_for_target(target: &str) -> OutputStream {
550+
if target == super::ACCESS_LOG_TARGET {
551+
OutputStream::Stdout
552+
} else {
553+
OutputStream::Stderr
554+
}
555+
}
556+
557+
fn write_to_output(output_stream: OutputStream, buf: &str) {
558+
use std::io::Write as _;
559+
560+
match output_stream {
561+
OutputStream::Stdout => {
562+
let _ = io::stdout().write_all(buf.as_bytes());
563+
}
564+
OutputStream::Stderr => {
565+
let _ = io::stderr().write_all(buf.as_bytes());
566+
}
567+
}
568+
}
569+
531570
fn event_target<'a>(
532571
event: &'a tracing::Event<'_>,
533572
event_fields: &'a HashMap<&'static str, String>,
@@ -819,5 +858,17 @@ mod logfmt {
819858
" name=BatchSpanProcessor.ExportFailed reason=\"connection error\""
820859
);
821860
}
861+
862+
#[test]
863+
fn access_logs_use_stdout_and_other_logs_use_stderr() {
864+
assert_eq!(
865+
output_stream_for_target(super::super::ACCESS_LOG_TARGET),
866+
OutputStream::Stdout
867+
);
868+
assert_eq!(
869+
output_stream_for_target("sqlpage::webserver::http"),
870+
OutputStream::Stderr
871+
);
872+
}
822873
}
823874
}

src/webserver/http.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ impl RootSpanBuilder for SqlPageRootSpanBuilder {
406406
} else {
407407
log::Level::Info
408408
};
409-
log::log!(level, "{status}");
409+
log::log!(target: crate::telemetry::ACCESS_LOG_TARGET, level, "{status}");
410410
}
411411
}
412412
}

0 commit comments

Comments
 (0)