From 16d901987b7205812b1ad570f31c9a276239c727 Mon Sep 17 00:00:00 2001 From: Alexj9837 Date: Wed, 11 Mar 2026 12:52:56 +0000 Subject: [PATCH 1/8] Add Graylog GELF logging layer --- Cargo.lock | 82 ++++++++++++++++++++++++++++++------------- Cargo.toml | 2 ++ src/cli/mod.rs | 41 ++++++++++++++++++++-- src/logging.rs | 95 +++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 189 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05d8e0ad..e92843f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,7 +150,7 @@ dependencies = [ "serde_urlencoded", "static_assertions_next", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "tracing-futures", ] @@ -186,7 +186,7 @@ dependencies = [ "quote", "strum", "syn 2.0.106", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -881,7 +881,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -997,7 +997,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -1374,7 +1374,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -1472,6 +1472,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "hostname" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" +dependencies = [ + "cfg-if", + "libc", + "windows-link", +] + [[package]] name = "http" version = "1.3.1" @@ -1545,7 +1556,7 @@ dependencies = [ "similar", "stringmetrics", "tabwriter", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "url", @@ -2126,9 +2137,11 @@ dependencies = [ "serde_json", "sqlx", "tempfile", + "thiserror 2.0.18", "tokio", "toml", "tracing", + "tracing-gelf", "tracing-opentelemetry", "tracing-subscriber", "url", @@ -2207,7 +2220,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", ] @@ -2237,7 +2250,7 @@ dependencies = [ "opentelemetry_sdk", "prost", "reqwest", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tonic", "tracing", @@ -2274,7 +2287,7 @@ dependencies = [ "opentelemetry", "percent-encoding", "rand 0.9.2", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", ] @@ -2378,7 +2391,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", - "thiserror 2.0.17", + "thiserror 2.0.18", "ucd-trie", ] @@ -2576,7 +2589,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -2597,7 +2610,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -2708,7 +2721,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2904,7 +2917,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -3287,7 +3300,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tracing", @@ -3369,7 +3382,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "whoami", ] @@ -3406,7 +3419,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "whoami", ] @@ -3430,7 +3443,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "url", ] @@ -3558,7 +3571,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -3582,11 +3595,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -3602,9 +3615,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -3942,6 +3955,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "tracing-gelf" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92cd4c9013c9c34630174cfb56c53d35f36bc282fc467ae186dd5edf270c3776" +dependencies = [ + "bytes", + "futures-util", + "hostname", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tokio-util", + "tracing-core", + "tracing-futures", + "tracing-subscriber", +] + [[package]] name = "tracing-log" version = "0.2.0" @@ -4006,7 +4038,7 @@ dependencies = [ "log", "rand 0.9.2", "sha1", - "thiserror 2.0.17", + "thiserror 2.0.18", "utf-8", ] diff --git a/Cargo.toml b/Cargo.toml index 185c3762..e1a13a2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,8 @@ dirs = { version = "6.0.0", optional = true } graphql_client = { version = "0.16.0", optional = true } openidconnect = { version = "4.0.0", optional = true } toml = { version = "1.0.0", optional = true } +tracing-gelf = "0.9.0" +thiserror = "2.0.18" [dev-dependencies] assert_matches = "1.5.0" diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 090b67ce..793d636c 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -43,6 +43,12 @@ pub struct TracingOptions { /// The minimum level of tracing events to send #[clap(long, default_value_t = Level::INFO, env = "NUMTRACKER_TRACING_LEVEL")] tracing_level: Level, + /// The URL of the Graylog instance + #[clap(long = "graylog", env = "NUMTRACKER_GRAYLOG")] + graylog_url: Option, + /// The minimum level of logging events to send + #[clap(long, default_value_t = Level::INFO, env = "NUMTRACKER_GRAYLOG_LOG_LEVEL")] + logging_level: Level, } #[derive(Debug, Subcommand)] @@ -157,10 +163,15 @@ impl TracingOptions { pub(crate) fn tracing_url(&self) -> Option { self.tracing_url.clone() } - - pub(crate) fn level(&self) -> Level { + pub(crate) fn level(&self) -> Level { self.tracing_level } + pub(crate) fn graylog_url(&self) -> Option { + self.graylog_url.clone() + } + pub(crate) fn logging_level(&self) -> Level { + self.logging_level + } } #[cfg(test)] @@ -335,6 +346,32 @@ mod tests { assert_eq!(cli.tracing().level(), Level::DEBUG); } + #[test] + fn graylog_opts() { + let cli = Cli::try_parse_from([APP, "--graylog", "tcp://graylog.example.com:12201", "serve"]) + .unwrap(); + assert_eq!( + cli.tracing().graylog_url(), + Some("tcp://graylog.example.com:12201".parse().unwrap()) + ); + assert_eq!(cli.tracing().logging_level(), Level::INFO); + + let cli = Cli::try_parse_from([ + APP, + "--graylog", + "tcp://graylog.example.com:12201", + "--logging-level", + "WARN", + "serve", + ]) + .unwrap(); + assert_eq!( + cli.tracing().graylog_url(), + Some("tcp://graylog.example.com:12201".parse().unwrap()) + ); + assert_eq!(cli.tracing().logging_level(), Level::WARN); + } + #[test] fn schema_command() { let cli = Cli::try_parse_from([APP, "schema"]).unwrap(); diff --git a/src/logging.rs b/src/logging.rs index c0b466e5..0af1f5a0 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -19,17 +19,27 @@ use opentelemetry_sdk::trace::SdkTracerProvider; use opentelemetry_sdk::Resource; use opentelemetry_semantic_conventions::resource::{SERVICE_NAME, SERVICE_VERSION}; use opentelemetry_semantic_conventions::SCHEMA_URL; -use tracing::{Level, Subscriber}; +use tracing::{error, warn, Level, Subscriber}; +use tracing_gelf::Logger; use tracing_opentelemetry::OpenTelemetryLayer; -use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::filter::{FilterFn,LevelFilter}; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::registry::LookupSpan; use tracing_subscriber::util::SubscriberInitExt as _; use tracing_subscriber::{EnvFilter, Layer}; use url::Url; +use crate::build_info::GIT_COMMIT_HASH; use crate::cli::TracingOptions; +#[derive(Debug, thiserror::Error)] +pub enum LoggingError { + #[error("Exporter build error: {0}")] + Exporter(#[from] ExporterBuildError), + #[error("Graylog error: {0}")] + Graylog(#[from] std::io::Error), +} + fn resource() -> Resource { Resource::builder() .with_schema_url( @@ -77,10 +87,57 @@ where } } -pub fn init(logging: Option, tracing: &TracingOptions) -> Result<(), ExporterBuildError> { +fn init_graylog(endpoint: Option,level: Level,) -> Result>, LoggingError> +where + S: Subscriber + for<'s> LookupSpan<'s>, +{ + let Some(endpoint) = endpoint else { + return Ok(None); + }; + + let address = format!( + "{}:{}", + endpoint.host_str().unwrap_or("localhost"), + endpoint.port().unwrap_or(12201), + ); + + match Logger::builder() + .additional_field("version", env!("CARGO_PKG_VERSION")) + .additional_field("build", GIT_COMMIT_HASH.unwrap_or("unknown")) + .connect_tcp(address) + { + Ok((logger, mut handle)) => { + tokio::spawn(async move { + let errors = handle.connect().await; + if errors.0.is_empty() { + error!("Failed to connect to graylog - address lookup failed"); + } else { + warn!( + "Connection to graylog {:?} closed: {:?}", + handle.address(), + errors + ); + } + }); + Ok(Some( + logger + .with_filter(LevelFilter::from_level(level)) + .with_filter(FilterFn::new(|m| { + m.target().starts_with(env!("CARGO_PKG_NAME")) + })), + )) + } + Err(e) => { + eprintln!("Couldn't create graylog logger: {e}"); + Ok(None) + } + } +} + +pub fn init(logging: Option, tracing: &TracingOptions) -> Result<(), LoggingError> { let log_layer = init_stdout(logging); let trace_layer = init_tracing(tracing.tracing_url(), tracing.level())?; - + let graylog_layer = init_graylog(tracing.graylog_url(), tracing.logging_level())?; // Whatever level is set for logging/tracing, ignore the noise from the low-level libraries let filter = EnvFilter::new("trace") // let everything through .add_directive("h2=info".parse().expect("Static string is valid")) // except http, @@ -91,6 +148,36 @@ pub fn init(logging: Option, tracing: &TracingOptions) -> Result<(), Expo .with(filter) .with(trace_layer) .with(log_layer) + .with(graylog_layer) .init(); Ok(()) } + +#[cfg(test)] +mod tests { + use std::net::TcpListener; + + use tracing::Level; + use tracing_subscriber::Registry; + + use super::init_graylog; + + #[test] + fn no_graylog_endpoint_returns_none() { + // No URL configured means no layer should be added + let result = init_graylog::(None, Level::INFO); + assert!(matches!(result, Ok(None))); + } + + #[tokio::test] + async fn graylog_with_endpoint_returns_layer() { + // Bind a random local port to accept the TCP connection + let listener = TcpListener::bind("127.0.0.1:0").expect("bind local port"); + let port = listener.local_addr().expect("local addr").port(); + let url = format!("tcp://127.0.0.1:{port}") + .parse() + .expect("valid url"); + let result = init_graylog::(Some(url), Level::INFO); + assert!(matches!(result, Ok(Some(_)))); + } +} From ccb6cb019b4b28af2ab3237dfce6fd332be6ddf7 Mon Sep 17 00:00:00 2001 From: Alexj9837 Date: Wed, 11 Mar 2026 12:53:10 +0000 Subject: [PATCH 2/8] Add Graylog configuration to Helm chart --- helm/numtracker/templates/statefulset.yaml | 6 ++++++ helm/numtracker/values.yaml | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/helm/numtracker/templates/statefulset.yaml b/helm/numtracker/templates/statefulset.yaml index 97a5b5c0..809af7e9 100644 --- a/helm/numtracker/templates/statefulset.yaml +++ b/helm/numtracker/templates/statefulset.yaml @@ -47,6 +47,12 @@ spec: - name: NUMTRACKER_TRACING_LEVEL value: {{ .Values.numtracker.tracing.level }} {{- end }} + {{- if .Values.numtracker.graylog.enabled }} + - name: NUMTRACKER_GRAYLOG + value: {{ .Values.numtracker.graylog.host }} + - name: NUMTRACKER_GRAYLOG_LOG_LEVEL + value: {{ .Values.numtracker.graylog.level }} + {{- end }} {{- if .Values.numtracker.auth.enabled }} - name: NUMTRACKER_AUTH_HOST value: {{ .Values.numtracker.auth.host }} diff --git a/helm/numtracker/values.yaml b/helm/numtracker/values.yaml index cbe5a848..98a9c855 100644 --- a/helm/numtracker/values.yaml +++ b/helm/numtracker/values.yaml @@ -24,6 +24,10 @@ numtracker: enabled: false level: DEBUG # host: OTEL compatible host + graylog: + enabled: true + level: INFO + # host: DAQ Graylog GELF TCP input eg. tcp://graylog-log-target.diamond.ac.uk:12231 auth: enabled: false # host: OPA instance From c6b491a0e66c3872393c0cb769b797b2b5d3c568 Mon Sep 17 00:00:00 2001 From: Alexj9837 Date: Wed, 11 Mar 2026 12:53:28 +0000 Subject: [PATCH 3/8] Document Graylog logging configuration --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 0979dd3d..4ad09da9 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,25 @@ Additional logging output is available via `-v` verbose flags. | `-vv` |Debug| | `-vvv` |Trace| +### Graylog + +Logs can be sent to a [Graylog][_graylog] instance using the `--graylog` flag: + +```bash +cargo run -- --graylog tcp://graylog.example.com:12201 serve +``` + +The minimum log level sent to Graylog can be set independently of the stderr +output level using `--logging-level` (default: `INFO`): + +```bash +cargo run -- --graylog tcp://graylog.example.com:12201 --logging-level WARN serve +``` + +Both options can also be set via environment variables: +- `NUMTRACKER_GRAYLOG` — Graylog URL +- `NUMTRACKER_GRAYLOG_LOG_LEVEL` — log level (`TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`) + ## Schema The schema is available via the `schema` command. This is also available via the @@ -405,4 +424,5 @@ $ numtracker client visit-directory i22 cm12345-6 ``` [_graphiql]:https://github.com/graphql/graphiql/ +[_graylog]:https://graylog.org/ [_jq]:https://jqlang.github.io/jq/ From 5383572d1da3179ee93176a44d51bd06a962a102 Mon Sep 17 00:00:00 2001 From: Alexj9837 Date: Wed, 11 Mar 2026 12:53:57 +0000 Subject: [PATCH 4/8] Add VSCode debug launch config --- .gitignore | 3 +++ .vscode/launch.json | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.gitignore b/.gitignore index d79ca701..07ce8611 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ # Demo database and file number directories *.db trackers/ + +# Local IDE settings +.vscode/settings.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..830dc485 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'numtracker'", + "cargo": { + "args": [ + "build", + "--bin=numtracker", + "--package=numtracker" + ], + "filter": { + "name": "numtracker", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'numtracker'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=numtracker", + "--package=numtracker" + ], + "filter": { + "name": "numtracker", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file From 002cb7362810774b79a240a261fc2f81e7ac2ec8 Mon Sep 17 00:00:00 2001 From: Alexj9837 Date: Wed, 11 Mar 2026 13:06:22 +0000 Subject: [PATCH 5/8] changed to align with tracing --- helm/numtracker/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/numtracker/values.yaml b/helm/numtracker/values.yaml index 98a9c855..0f8ebdda 100644 --- a/helm/numtracker/values.yaml +++ b/helm/numtracker/values.yaml @@ -25,7 +25,7 @@ numtracker: level: DEBUG # host: OTEL compatible host graylog: - enabled: true + enabled: false level: INFO # host: DAQ Graylog GELF TCP input eg. tcp://graylog-log-target.diamond.ac.uk:12231 auth: From d6accbe44a7383cf20e9edf89e25a469ecd76021 Mon Sep 17 00:00:00 2001 From: Alexj9837 Date: Tue, 17 Mar 2026 16:29:32 +0000 Subject: [PATCH 6/8] removing launch.json --- .vscode/launch.json | 45 --------------------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 830dc485..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug executable 'numtracker'", - "cargo": { - "args": [ - "build", - "--bin=numtracker", - "--package=numtracker" - ], - "filter": { - "name": "numtracker", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in executable 'numtracker'", - "cargo": { - "args": [ - "test", - "--no-run", - "--bin=numtracker", - "--package=numtracker" - ], - "filter": { - "name": "numtracker", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - } - ] -} \ No newline at end of file From 3d3851f6cd4db6c9ca82c734c4fa4b9c0712c4cd Mon Sep 17 00:00:00 2001 From: Alexj9837 Date: Wed, 18 Mar 2026 09:28:55 +0000 Subject: [PATCH 7/8] 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 --- src/cli/mod.rs | 44 +++++++++++++------- src/logging.rs | 110 ++++++++++++++++++++++++++----------------------- src/main.rs | 2 +- 3 files changed, 90 insertions(+), 66 deletions(-) diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 793d636c..f358365a 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -29,8 +29,10 @@ pub mod client; pub struct Cli { #[clap(flatten, next_help_heading = "Logging/Debug")] verbose: Verbosity, - #[clap(flatten, next_help_heading = "Tracing and Logging")] + #[clap(flatten, next_help_heading = "Tracing")] tracing: TracingOptions, + #[clap(flatten, next_help_heading = "Graylog")] + pub graylog: GraylogOptions, #[clap(subcommand)] pub(crate) command: Command, } @@ -43,12 +45,32 @@ pub struct TracingOptions { /// The minimum level of tracing events to send #[clap(long, default_value_t = Level::INFO, env = "NUMTRACKER_TRACING_LEVEL")] tracing_level: Level, +} + +#[derive(Debug, Parser)] +pub struct GraylogOptions { /// The URL of the Graylog instance #[clap(long = "graylog", env = "NUMTRACKER_GRAYLOG")] - graylog_url: Option, + pub graylog_url: Option, /// The minimum level of logging events to send #[clap(long, default_value_t = Level::INFO, env = "NUMTRACKER_GRAYLOG_LOG_LEVEL")] - logging_level: Level, + pub logging_level: Level, +} + +impl GraylogOptions { + /// Returns the `host:port` address string for connecting to Graylog, or `None` if no URL is + /// configured. Returns an error if the URL has no port — a missing port is always a + /// misconfiguration and should not be silently defaulted. + pub fn address(&self) -> Result, String> { + let endpoint = match &self.graylog_url { + Some(u) => u, + None => return Ok(None), + }; + let port = endpoint + .port() + .ok_or_else(|| format!("Graylog URL '{}' has no port - please specify a port (e.g. tcp://{}:12201)", endpoint, endpoint.host_str().unwrap_or("host")))?; + Ok(Some(format!("{}:{}", endpoint.host_str().expect("Graylog URL has no host"), port))) + } } #[derive(Debug, Subcommand)] @@ -163,15 +185,9 @@ impl TracingOptions { pub(crate) fn tracing_url(&self) -> Option { self.tracing_url.clone() } - pub(crate) fn level(&self) -> Level { + pub(crate) fn level(&self) -> Level { self.tracing_level } - pub(crate) fn graylog_url(&self) -> Option { - self.graylog_url.clone() - } - pub(crate) fn logging_level(&self) -> Level { - self.logging_level - } } #[cfg(test)] @@ -351,10 +367,10 @@ mod tests { let cli = Cli::try_parse_from([APP, "--graylog", "tcp://graylog.example.com:12201", "serve"]) .unwrap(); assert_eq!( - cli.tracing().graylog_url(), + cli.graylog.graylog_url, Some("tcp://graylog.example.com:12201".parse().unwrap()) ); - assert_eq!(cli.tracing().logging_level(), Level::INFO); + assert_eq!(cli.graylog.logging_level, Level::INFO); let cli = Cli::try_parse_from([ APP, @@ -366,10 +382,10 @@ mod tests { ]) .unwrap(); assert_eq!( - cli.tracing().graylog_url(), + cli.graylog.graylog_url, Some("tcp://graylog.example.com:12201".parse().unwrap()) ); - assert_eq!(cli.tracing().logging_level(), Level::WARN); + assert_eq!(cli.graylog.logging_level, Level::WARN); } #[test] diff --git a/src/logging.rs b/src/logging.rs index 0af1f5a0..625f841f 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -29,8 +29,7 @@ use tracing_subscriber::util::SubscriberInitExt as _; use tracing_subscriber::{EnvFilter, Layer}; use url::Url; -use crate::build_info::GIT_COMMIT_HASH; -use crate::cli::TracingOptions; +use crate::cli::{GraylogOptions, TracingOptions}; #[derive(Debug, thiserror::Error)] pub enum LoggingError { @@ -38,6 +37,8 @@ pub enum LoggingError { Exporter(#[from] ExporterBuildError), #[error("Graylog error: {0}")] Graylog(#[from] std::io::Error), + #[error("Graylog configuration error: {0}")] + Config(String), } fn resource() -> Resource { @@ -87,57 +88,53 @@ where } } -fn init_graylog(endpoint: Option,level: Level,) -> Result>, LoggingError> +fn init_graylog(opts: &GraylogOptions) -> Result>, LoggingError> where S: Subscriber + for<'s> LookupSpan<'s>, { - let Some(endpoint) = endpoint else { - return Ok(None); - }; - - let address = format!( - "{}:{}", - endpoint.host_str().unwrap_or("localhost"), - endpoint.port().unwrap_or(12201), - ); - - match Logger::builder() - .additional_field("version", env!("CARGO_PKG_VERSION")) - .additional_field("build", GIT_COMMIT_HASH.unwrap_or("unknown")) - .connect_tcp(address) - { - Ok((logger, mut handle)) => { - tokio::spawn(async move { - let errors = handle.connect().await; - if errors.0.is_empty() { - error!("Failed to connect to graylog - address lookup failed"); - } else { - warn!( - "Connection to graylog {:?} closed: {:?}", - handle.address(), - errors - ); - } - }); - Ok(Some( - logger - .with_filter(LevelFilter::from_level(level)) - .with_filter(FilterFn::new(|m| { - m.target().starts_with(env!("CARGO_PKG_NAME")) - })), - )) - } - Err(e) => { - eprintln!("Couldn't create graylog logger: {e}"); - Ok(None) + if let Some(address) = opts.address().map_err(LoggingError::Config)? { + let level = opts.logging_level; + + match Logger::builder().connect_tcp(address) { + Ok((logger, mut handle)) => { + tokio::spawn(async move { + loop { + let errors = handle.connect().await; + if errors.0.is_empty() { + error!( + "Graylog DNS lookup failed for {:?} - no addresses resolved", + handle.address() + ); + break; + } else { + for (addr, err) in &errors.0 { + warn!("Graylog connection to {addr} failed: {err} - reconnecting"); + } + } + } + }); + Ok(Some( + logger + .with_filter(LevelFilter::from_level(level)) + .with_filter(FilterFn::new(|m| { + m.target().starts_with(env!("CARGO_PKG_NAME")) + })), + )) + } + Err(e) => { + eprintln!("Couldn't create graylog logger: {e}"); + Ok(None) + } } + } else { + Ok(None) } } -pub fn init(logging: Option, tracing: &TracingOptions) -> Result<(), LoggingError> { +pub fn init(logging: Option, tracing: &TracingOptions, graylog: &GraylogOptions) -> Result<(), LoggingError> { let log_layer = init_stdout(logging); let trace_layer = init_tracing(tracing.tracing_url(), tracing.level())?; - let graylog_layer = init_graylog(tracing.graylog_url(), tracing.logging_level())?; + let graylog_layer = init_graylog(graylog)?; // Whatever level is set for logging/tracing, ignore the noise from the low-level libraries let filter = EnvFilter::new("trace") // let everything through .add_directive("h2=info".parse().expect("Static string is valid")) // except http, @@ -161,23 +158,34 @@ mod tests { use tracing_subscriber::Registry; use super::init_graylog; + use crate::cli::GraylogOptions; #[test] fn no_graylog_endpoint_returns_none() { - // No URL configured means no layer should be added - let result = init_graylog::(None, Level::INFO); + let opts = GraylogOptions { graylog_url: None, logging_level: Level::INFO }; + let result = init_graylog::(&opts); assert!(matches!(result, Ok(None))); } + #[test] + fn graylog_url_without_port_is_an_error() { + let opts = GraylogOptions { + graylog_url: Some("tcp://graylog.example.com".parse().expect("valid url")), + logging_level: Level::INFO, + }; + let result = init_graylog::(&opts); + assert!(matches!(result, Err(_))); + } + #[tokio::test] async fn graylog_with_endpoint_returns_layer() { - // Bind a random local port to accept the TCP connection let listener = TcpListener::bind("127.0.0.1:0").expect("bind local port"); let port = listener.local_addr().expect("local addr").port(); - let url = format!("tcp://127.0.0.1:{port}") - .parse() - .expect("valid url"); - let result = init_graylog::(Some(url), Level::INFO); + let opts = GraylogOptions { + graylog_url: Some(format!("tcp://127.0.0.1:{port}").parse().expect("valid url")), + logging_level: Level::INFO, + }; + let result = init_graylog::(&opts); assert!(matches!(result, Ok(Some(_)))); } } diff --git a/src/main.rs b/src/main.rs index 51923e8b..55c16597 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,7 +30,7 @@ mod template; #[tokio::main] async fn main() -> Result<(), Box> { let args = Cli::init(); - let _ = logging::init(args.log_level(), args.tracing()); + let _ = logging::init(args.log_level(), args.tracing(), &args.graylog); match args.command { Command::Serve(opts) => graphql::serve_graphql(opts).await, #[cfg(not(feature = "client"))] From 831a9905548e4cfd4052e822dd656118741bf31a Mon Sep 17 00:00:00 2001 From: Alexj9837 Date: Wed, 18 Mar 2026 15:43:26 +0000 Subject: [PATCH 8/8] =?UTF-8?q?Merge=20main=20-=20resolve=20Cargo.lock=20c?= =?UTF-8?q?onflict,=20accept=20StatefulSet=E2=86=92Deployment=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 63 +++++++++---------- .../{statefulset.yaml => deployment.yaml} | 2 +- 2 files changed, 32 insertions(+), 33 deletions(-) rename helm/numtracker/templates/{statefulset.yaml => deployment.yaml} (99%) diff --git a/Cargo.lock b/Cargo.lock index e92843f1..90e53c3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -479,9 +479,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -493,9 +493,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.57" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -503,9 +503,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.57" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -528,9 +528,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "colorchoice" @@ -1103,9 +1103,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -1118,9 +1118,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1128,15 +1128,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1156,9 +1156,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1172,9 +1172,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -1183,15 +1183,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" @@ -1201,9 +1201,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1213,7 +1213,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -2627,7 +2626,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -3766,9 +3765,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.1+spec-1.1.0" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe30f93627849fa362d4a602212d41bb237dc2bd0f8ba0b2ce785012e124220" +checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" dependencies = [ "indexmap 2.11.4", "serde_core", @@ -3811,9 +3810,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.8+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] diff --git a/helm/numtracker/templates/statefulset.yaml b/helm/numtracker/templates/deployment.yaml similarity index 99% rename from helm/numtracker/templates/statefulset.yaml rename to helm/numtracker/templates/deployment.yaml index 809af7e9..478ccb11 100644 --- a/helm/numtracker/templates/statefulset.yaml +++ b/helm/numtracker/templates/deployment.yaml @@ -1,5 +1,5 @@ apiVersion: apps/v1 -kind: StatefulSet +kind: Deployment metadata: name: {{ include "numtracker.fullname" . }} labels: