diff --git a/CHANGELOG.md b/CHANGELOG.md index 838340ce9..f561d8fd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Other guiding principles: ### Added +- **amaru**: log precise build identity (package version, full git commit, dirty flag, OS/arch) at INFO after tracing is set up, so operator log files identify the running binary. ([#1161](https://github.com/pragma-org/amaru/issues/1161)) - **amaru**: add `amaru node rollback` to recover after a wrongly invalidated block or to rewind to an epoch start. Supports `--immutable-tip` (chain store only) and `--epoch` (ledger snapshot reset + chain realign). Clears all descendant validation flags, sets the anchor/best tip, and culls the best-chain fragment. ([#1072](https://github.com/pragma-org/amaru/issues/1072)) - **amaru**: add `amaru mithril sync` to download verified Mithril immutable files and replay their blocks directly into the chain and ledger stores. - **amaru**: add `amaru node rm --wipe-all-dbs` to remove the ledger and chain databases resolved from the selected network. diff --git a/crates/amaru-observability/src/schemas.rs b/crates/amaru-observability/src/schemas.rs index b5e952960..c515b8bdd 100644 --- a/crates/amaru-observability/src/schemas.rs +++ b/crates/amaru-observability/src/schemas.rs @@ -1575,6 +1575,16 @@ define_schemas! { required with_colors: bool } } + build { + /// Running binary build/version identity (package version, git commit, target). + public VERSION { + required version: String + required git_commit: String + required git_dirty: bool + required os: String + required arch: String + } + } trace { /// Resolution of a trace filter from the environment public FILTER { diff --git a/crates/amaru/src/bin/amaru/main.rs b/crates/amaru/src/bin/amaru/main.rs index 26cd1dd11..16ddac75c 100644 --- a/crates/amaru/src/bin/amaru/main.rs +++ b/crates/amaru/src/bin/amaru/main.rs @@ -90,13 +90,16 @@ fn try_main() -> Result<(), Box> { } else { // OpenTelemetry batch exporters require a current Tokio runtime. let _enter = rt.enter(); - setup_observability( + let result = setup_observability( with_open_telemetry, with_json_traces, color_enabled, &ListenAddressHint(listen_address.as_deref()), tui.as_ref().map(tui::Session::layer), - ) + ); + // Record precise binary identity in operator logs as soon as tracing is live. + version::log_build_version(); + result }; let result = runnable.run_on(&rt, &signals, metrics); diff --git a/crates/amaru/src/version.rs b/crates/amaru/src/version.rs index abecdf980..35b58fc12 100644 --- a/crates/amaru/src/version.rs +++ b/crates/amaru/src/version.rs @@ -14,6 +14,8 @@ use std::sync::LazyLock; +use amaru_observability::info; + mod built_info { include!(concat!(env!("OUT_DIR"), "/built.rs")); } @@ -43,6 +45,10 @@ pub fn display_version() -> &'static str { DISPLAY_VERSION.as_str() } +pub fn git_commit_hash() -> Option<&'static str> { + built_info::GIT_COMMIT_HASH +} + pub fn git_commit_hash_short() -> Option<&'static str> { built_info::GIT_COMMIT_HASH_SHORT } @@ -58,3 +64,94 @@ pub fn target_os() -> &'static str { pub fn target_arch() -> &'static str { built_info::CFG_TARGET_ARCH } + +/// Emit a structured INFO event with the running binary's version and git identity. +/// +/// Call this once after the tracing subscriber is installed so operator log files +/// record which build produced them. +pub fn log_build_version() { + info!( + setup::build::VERSION, + version = package_version(), + git_commit = git_commit_hash().unwrap_or("unknown"), + git_dirty = git_dirty().unwrap_or(false), + os = target_os(), + arch = target_arch(), + ); +} + +#[cfg(test)] +mod tests { + use std::{ + io::{self, Write}, + sync::{Arc, Mutex}, + }; + + use tracing_subscriber::fmt::MakeWriter; + + use super::*; + + /// Captures fmt layer output so tests can assert on emitted events. + #[derive(Clone, Default)] + struct CaptureWriter { + buffer: Arc>>, + } + + impl CaptureWriter { + fn contents(&self) -> String { + let bytes = self.buffer.lock().expect("capture buffer lock").clone(); + String::from_utf8_lossy(&bytes).into_owned() + } + } + + impl Write for CaptureWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.buffer.lock().expect("capture buffer lock").write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + impl<'a> MakeWriter<'a> for CaptureWriter { + type Writer = CaptureWriter; + + fn make_writer(&'a self) -> Self::Writer { + self.clone() + } + } + + #[test] + fn log_build_version_emits_package_and_git_fields() { + let writer = CaptureWriter::default(); + let subscriber = tracing_subscriber::fmt() + .with_writer(writer.clone()) + // Equivalent to with_test_writer for cargo test visibility, but captureable for asserts. + .with_max_level(tracing::Level::INFO) + .with_target(true) + .with_level(true) + .finish(); + + tracing::subscriber::with_default(subscriber, || { + log_build_version(); + }); + + let output = writer.contents(); + + assert!( + output.contains("amaru::setup") && output.contains("build.version"), + "expected amaru::setup build.version event target in output:\n{output}" + ); + assert!( + output.contains(package_version()), + "expected package version {} in output:\n{output}", + package_version() + ); + assert!(output.contains(target_os()), "expected os {} in output:\n{output}", target_os()); + assert!(output.contains(target_arch()), "expected arch {} in output:\n{output}", target_arch()); + + let expected_commit = git_commit_hash().unwrap_or("unknown"); + assert!(output.contains(expected_commit), "expected git commit {expected_commit} in output:\n{output}"); + } +} diff --git a/docs/TRACES.md b/docs/TRACES.md index f57a10343..af17c2875 100644 --- a/docs/TRACES.md +++ b/docs/TRACES.md @@ -1846,6 +1846,24 @@ For information on how to use and filter these spans, see [monitoring/README.md] +## target: `amaru::setup::build` + +| name | level | public | description | required fields | optional fields | +| --- | --- | --- | --- | --- | --- | +| `version` | `TRACE` | public | Running binary build/version identity (package version, git commit, target). | version, git_commit, git_dirty, os, arch | | + +
span: `version` + +| field | type | required | +| --- | --- | --- | +| `version` | `string` | ✓ | +| `git_commit` | `string` | ✓ | +| `git_dirty` | `boolean` | ✓ | +| `os` | `string` | ✓ | +| `arch` | `string` | ✓ | + +
+ ## target: `amaru::setup::observability` | name | level | public | description | required fields | optional fields | diff --git a/docs/traces-schema.json b/docs/traces-schema.json index 8cb2c016e..c9641d4af 100644 --- a/docs/traces-schema.json +++ b/docs/traces-schema.json @@ -3915,6 +3915,40 @@ "description": "A connection has been terminated (graceful disconnect, error, handshake refusal, or network error).", "public": true }, + "amaru::setup::build::VERSION": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "git_commit": { + "type": "string" + }, + "git_dirty": { + "type": "boolean" + }, + "os": { + "type": "string" + }, + "arch": { + "type": "string" + } + }, + "required": [ + "version", + "git_commit", + "git_dirty", + "os", + "arch" + ], + "optional": [], + "additionalProperties": false, + "name": "version", + "level": "TRACE", + "target": "amaru::setup::build", + "description": "Running binary build/version identity (package version, git commit, target).", + "public": true + }, "amaru::setup::observability::INIT": { "type": "object", "properties": {