Skip to content

Commit 3d3851f

Browse files
committed
Address PR review feedback on Graylog logging
- Extract GraylogOptions into its own struct, independent of TracingOptions - Remove .vscode/launch.json (to be added in a separate PR) - Remove version/build additional_field calls from Graylog logger - Switch let-else to if-let in init_graylog - Warn when Graylog URL has no port rather than silently defaulting - Add reconnect loop so dropped connections recover automatically - Improve connection error messages to distinguish DNS failure from TCP errors
1 parent d6accbe commit 3d3851f

3 files changed

Lines changed: 90 additions & 66 deletions

File tree

src/cli/mod.rs

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ pub mod client;
2929
pub struct Cli {
3030
#[clap(flatten, next_help_heading = "Logging/Debug")]
3131
verbose: Verbosity,
32-
#[clap(flatten, next_help_heading = "Tracing and Logging")]
32+
#[clap(flatten, next_help_heading = "Tracing")]
3333
tracing: TracingOptions,
34+
#[clap(flatten, next_help_heading = "Graylog")]
35+
pub graylog: GraylogOptions,
3436
#[clap(subcommand)]
3537
pub(crate) command: Command,
3638
}
@@ -43,12 +45,32 @@ pub struct TracingOptions {
4345
/// The minimum level of tracing events to send
4446
#[clap(long, default_value_t = Level::INFO, env = "NUMTRACKER_TRACING_LEVEL")]
4547
tracing_level: Level,
48+
}
49+
50+
#[derive(Debug, Parser)]
51+
pub struct GraylogOptions {
4652
/// The URL of the Graylog instance
4753
#[clap(long = "graylog", env = "NUMTRACKER_GRAYLOG")]
48-
graylog_url: Option<Url>,
54+
pub graylog_url: Option<Url>,
4955
/// The minimum level of logging events to send
5056
#[clap(long, default_value_t = Level::INFO, env = "NUMTRACKER_GRAYLOG_LOG_LEVEL")]
51-
logging_level: Level,
57+
pub logging_level: Level,
58+
}
59+
60+
impl GraylogOptions {
61+
/// Returns the `host:port` address string for connecting to Graylog, or `None` if no URL is
62+
/// configured. Returns an error if the URL has no port — a missing port is always a
63+
/// misconfiguration and should not be silently defaulted.
64+
pub fn address(&self) -> Result<Option<String>, String> {
65+
let endpoint = match &self.graylog_url {
66+
Some(u) => u,
67+
None => return Ok(None),
68+
};
69+
let port = endpoint
70+
.port()
71+
.ok_or_else(|| format!("Graylog URL '{}' has no port - please specify a port (e.g. tcp://{}:12201)", endpoint, endpoint.host_str().unwrap_or("host")))?;
72+
Ok(Some(format!("{}:{}", endpoint.host_str().expect("Graylog URL has no host"), port)))
73+
}
5274
}
5375

5476
#[derive(Debug, Subcommand)]
@@ -163,15 +185,9 @@ impl TracingOptions {
163185
pub(crate) fn tracing_url(&self) -> Option<Url> {
164186
self.tracing_url.clone()
165187
}
166-
pub(crate) fn level(&self) -> Level {
188+
pub(crate) fn level(&self) -> Level {
167189
self.tracing_level
168190
}
169-
pub(crate) fn graylog_url(&self) -> Option<Url> {
170-
self.graylog_url.clone()
171-
}
172-
pub(crate) fn logging_level(&self) -> Level {
173-
self.logging_level
174-
}
175191
}
176192

177193
#[cfg(test)]
@@ -351,10 +367,10 @@ mod tests {
351367
let cli = Cli::try_parse_from([APP, "--graylog", "tcp://graylog.example.com:12201", "serve"])
352368
.unwrap();
353369
assert_eq!(
354-
cli.tracing().graylog_url(),
370+
cli.graylog.graylog_url,
355371
Some("tcp://graylog.example.com:12201".parse().unwrap())
356372
);
357-
assert_eq!(cli.tracing().logging_level(), Level::INFO);
373+
assert_eq!(cli.graylog.logging_level, Level::INFO);
358374

359375
let cli = Cli::try_parse_from([
360376
APP,
@@ -366,10 +382,10 @@ mod tests {
366382
])
367383
.unwrap();
368384
assert_eq!(
369-
cli.tracing().graylog_url(),
385+
cli.graylog.graylog_url,
370386
Some("tcp://graylog.example.com:12201".parse().unwrap())
371387
);
372-
assert_eq!(cli.tracing().logging_level(), Level::WARN);
388+
assert_eq!(cli.graylog.logging_level, Level::WARN);
373389
}
374390

375391
#[test]

src/logging.rs

Lines changed: 59 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,16 @@ use tracing_subscriber::util::SubscriberInitExt as _;
2929
use tracing_subscriber::{EnvFilter, Layer};
3030
use url::Url;
3131

32-
use crate::build_info::GIT_COMMIT_HASH;
33-
use crate::cli::TracingOptions;
32+
use crate::cli::{GraylogOptions, TracingOptions};
3433

3534
#[derive(Debug, thiserror::Error)]
3635
pub enum LoggingError {
3736
#[error("Exporter build error: {0}")]
3837
Exporter(#[from] ExporterBuildError),
3938
#[error("Graylog error: {0}")]
4039
Graylog(#[from] std::io::Error),
40+
#[error("Graylog configuration error: {0}")]
41+
Config(String),
4142
}
4243

4344
fn resource() -> Resource {
@@ -87,57 +88,53 @@ where
8788
}
8889
}
8990

90-
fn init_graylog<S>(endpoint: Option<Url>,level: Level,) -> Result<Option<impl Layer<S>>, LoggingError>
91+
fn init_graylog<S>(opts: &GraylogOptions) -> Result<Option<impl Layer<S>>, LoggingError>
9192
where
9293
S: Subscriber + for<'s> LookupSpan<'s>,
9394
{
94-
let Some(endpoint) = endpoint else {
95-
return Ok(None);
96-
};
97-
98-
let address = format!(
99-
"{}:{}",
100-
endpoint.host_str().unwrap_or("localhost"),
101-
endpoint.port().unwrap_or(12201),
102-
);
103-
104-
match Logger::builder()
105-
.additional_field("version", env!("CARGO_PKG_VERSION"))
106-
.additional_field("build", GIT_COMMIT_HASH.unwrap_or("unknown"))
107-
.connect_tcp(address)
108-
{
109-
Ok((logger, mut handle)) => {
110-
tokio::spawn(async move {
111-
let errors = handle.connect().await;
112-
if errors.0.is_empty() {
113-
error!("Failed to connect to graylog - address lookup failed");
114-
} else {
115-
warn!(
116-
"Connection to graylog {:?} closed: {:?}",
117-
handle.address(),
118-
errors
119-
);
120-
}
121-
});
122-
Ok(Some(
123-
logger
124-
.with_filter(LevelFilter::from_level(level))
125-
.with_filter(FilterFn::new(|m| {
126-
m.target().starts_with(env!("CARGO_PKG_NAME"))
127-
})),
128-
))
129-
}
130-
Err(e) => {
131-
eprintln!("Couldn't create graylog logger: {e}");
132-
Ok(None)
95+
if let Some(address) = opts.address().map_err(LoggingError::Config)? {
96+
let level = opts.logging_level;
97+
98+
match Logger::builder().connect_tcp(address) {
99+
Ok((logger, mut handle)) => {
100+
tokio::spawn(async move {
101+
loop {
102+
let errors = handle.connect().await;
103+
if errors.0.is_empty() {
104+
error!(
105+
"Graylog DNS lookup failed for {:?} - no addresses resolved",
106+
handle.address()
107+
);
108+
break;
109+
} else {
110+
for (addr, err) in &errors.0 {
111+
warn!("Graylog connection to {addr} failed: {err} - reconnecting");
112+
}
113+
}
114+
}
115+
});
116+
Ok(Some(
117+
logger
118+
.with_filter(LevelFilter::from_level(level))
119+
.with_filter(FilterFn::new(|m| {
120+
m.target().starts_with(env!("CARGO_PKG_NAME"))
121+
})),
122+
))
123+
}
124+
Err(e) => {
125+
eprintln!("Couldn't create graylog logger: {e}");
126+
Ok(None)
127+
}
133128
}
129+
} else {
130+
Ok(None)
134131
}
135132
}
136133

137-
pub fn init(logging: Option<Level>, tracing: &TracingOptions) -> Result<(), LoggingError> {
134+
pub fn init(logging: Option<Level>, tracing: &TracingOptions, graylog: &GraylogOptions) -> Result<(), LoggingError> {
138135
let log_layer = init_stdout(logging);
139136
let trace_layer = init_tracing(tracing.tracing_url(), tracing.level())?;
140-
let graylog_layer = init_graylog(tracing.graylog_url(), tracing.logging_level())?;
137+
let graylog_layer = init_graylog(graylog)?;
141138
// Whatever level is set for logging/tracing, ignore the noise from the low-level libraries
142139
let filter = EnvFilter::new("trace") // let everything through
143140
.add_directive("h2=info".parse().expect("Static string is valid")) // except http,
@@ -161,23 +158,34 @@ mod tests {
161158
use tracing_subscriber::Registry;
162159

163160
use super::init_graylog;
161+
use crate::cli::GraylogOptions;
164162

165163
#[test]
166164
fn no_graylog_endpoint_returns_none() {
167-
// No URL configured means no layer should be added
168-
let result = init_graylog::<Registry>(None, Level::INFO);
165+
let opts = GraylogOptions { graylog_url: None, logging_level: Level::INFO };
166+
let result = init_graylog::<Registry>(&opts);
169167
assert!(matches!(result, Ok(None)));
170168
}
171169

170+
#[test]
171+
fn graylog_url_without_port_is_an_error() {
172+
let opts = GraylogOptions {
173+
graylog_url: Some("tcp://graylog.example.com".parse().expect("valid url")),
174+
logging_level: Level::INFO,
175+
};
176+
let result = init_graylog::<Registry>(&opts);
177+
assert!(matches!(result, Err(_)));
178+
}
179+
172180
#[tokio::test]
173181
async fn graylog_with_endpoint_returns_layer() {
174-
// Bind a random local port to accept the TCP connection
175182
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local port");
176183
let port = listener.local_addr().expect("local addr").port();
177-
let url = format!("tcp://127.0.0.1:{port}")
178-
.parse()
179-
.expect("valid url");
180-
let result = init_graylog::<Registry>(Some(url), Level::INFO);
184+
let opts = GraylogOptions {
185+
graylog_url: Some(format!("tcp://127.0.0.1:{port}").parse().expect("valid url")),
186+
logging_level: Level::INFO,
187+
};
188+
let result = init_graylog::<Registry>(&opts);
181189
assert!(matches!(result, Ok(Some(_))));
182190
}
183191
}

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ mod template;
3030
#[tokio::main]
3131
async fn main() -> Result<(), Box<dyn Error>> {
3232
let args = Cli::init();
33-
let _ = logging::init(args.log_level(), args.tracing());
33+
let _ = logging::init(args.log_level(), args.tracing(), &args.graylog);
3434
match args.command {
3535
Command::Serve(opts) => graphql::serve_graphql(opts).await,
3636
#[cfg(not(feature = "client"))]

0 commit comments

Comments
 (0)