Skip to content

Commit 4980b8b

Browse files
Add build info (#85)
* Add build info * add build_info spans into initialization, handler functions --------- Co-authored-by: Yaroslav Litvinov <yaroslav@embucket.com>
1 parent b03d23a commit 4980b8b

10 files changed

Lines changed: 287 additions & 4 deletions

File tree

Cargo.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ members = [
1313
"crates/error-stack-trace",
1414
"crates/embucket-lambda",
1515
"crates/state-store",
16+
"crates/build-info",
1617
]
1718
resolver = "2"
1819
package.license-file = "LICENSE"

crates/build-info/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "build-info"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license-file.workspace = true
6+
7+
[dependencies]
8+
# No runtime dependencies - all info comes from env!() at compile time
9+
10+
[lints]
11+
workspace = true

crates/build-info/build.rs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
use std::process::Command;
2+
3+
fn main() {
4+
// Capture git commit SHA (full)
5+
let git_sha = run_git_command(&["rev-parse", "HEAD"]).unwrap_or_else(|| "unknown".to_string());
6+
7+
// Capture git commit SHA (short, 8 chars)
8+
let git_sha_short = run_git_command(&["rev-parse", "--short=8", "HEAD"])
9+
.unwrap_or_else(|| "unknown".to_string());
10+
11+
// Capture git branch name
12+
let git_branch = run_git_command(&["rev-parse", "--abbrev-ref", "HEAD"])
13+
.unwrap_or_else(|| "unknown".to_string());
14+
15+
// Capture git describe for semantic versioning
16+
// Format: v0.1.0-5-g7b92aa23 (tag-commits_since_tag-short_sha)
17+
// or v0.1.0 if on a tag, or v0.1.0-dirty if dirty
18+
let git_describe = run_git_command(&["describe", "--tags", "--always", "--dirty"])
19+
.or_else(|| {
20+
// Fallback to CARGO_PKG_VERSION if no tags exist
21+
std::env::var("CARGO_PKG_VERSION").ok()
22+
})
23+
.unwrap_or_else(|| "unknown".to_string());
24+
25+
// Check if repository has uncommitted changes
26+
let git_dirty = is_git_dirty();
27+
28+
// Capture build timestamp in ISO 8601 format (YYYY-MM-DD)
29+
let build_timestamp = std::env::var("SOURCE_DATE_EPOCH")
30+
.ok()
31+
.and_then(|epoch| {
32+
use std::time::UNIX_EPOCH;
33+
let secs = epoch.parse::<u64>().ok()?;
34+
let time = UNIX_EPOCH + std::time::Duration::from_secs(secs);
35+
Some(format_timestamp(time))
36+
})
37+
.unwrap_or_else(|| format_timestamp(std::time::SystemTime::now()));
38+
39+
// Set environment variables for the build
40+
println!("cargo:rustc-env=GIT_SHA={git_sha}");
41+
println!("cargo:rustc-env=GIT_SHA_SHORT={git_sha_short}");
42+
println!("cargo:rustc-env=GIT_BRANCH={git_branch}");
43+
println!("cargo:rustc-env=GIT_DESCRIBE={git_describe}");
44+
println!("cargo:rustc-env=GIT_DIRTY={git_dirty}");
45+
println!("cargo:rustc-env=BUILD_TIMESTAMP={build_timestamp}");
46+
47+
// Rerun build script if git HEAD changes
48+
println!("cargo:rerun-if-changed=.git/HEAD");
49+
// Also rerun if the current branch ref changes
50+
if let Some(branch_ref) = run_git_command(&["symbolic-ref", "HEAD"]) {
51+
let ref_path = format!(".git/{branch_ref}");
52+
println!("cargo:rerun-if-changed={ref_path}");
53+
}
54+
}
55+
56+
/// Runs a git command and returns the output as a trimmed string, or None if the command fails.
57+
fn run_git_command(args: &[&str]) -> Option<String> {
58+
let output = Command::new("git").args(args).output().ok()?;
59+
60+
if output.status.success() {
61+
String::from_utf8(output.stdout)
62+
.ok()
63+
.map(|s| s.trim().to_string())
64+
.filter(|s| !s.is_empty())
65+
} else {
66+
None
67+
}
68+
}
69+
70+
/// Checks if the git repository has uncommitted changes (modified, staged, or untracked files).
71+
/// Returns "true" or "false" as a string.
72+
fn is_git_dirty() -> String {
73+
// Check if there are any changes in the index or working tree
74+
// git diff-index --quiet HEAD returns non-zero if there are changes
75+
let has_changes = Command::new("git")
76+
.args(["diff-index", "--quiet", "HEAD", "--"])
77+
.status()
78+
.map(|status| !status.success())
79+
.unwrap_or(false);
80+
81+
if has_changes {
82+
return "true".to_string();
83+
}
84+
85+
// Check for untracked files
86+
let has_untracked = run_git_command(&["ls-files", "--others", "--exclude-standard"])
87+
.is_some_and(|output| !output.is_empty());
88+
89+
if has_untracked {
90+
"true".to_string()
91+
} else {
92+
"false".to_string()
93+
}
94+
}
95+
96+
/// Formats a `SystemTime` as an ISO 8601 date (YYYY-MM-DD).
97+
fn format_timestamp(time: std::time::SystemTime) -> String {
98+
use std::time::UNIX_EPOCH;
99+
100+
let duration = time
101+
.duration_since(UNIX_EPOCH)
102+
.unwrap_or_else(|_| std::time::Duration::from_secs(0));
103+
104+
let total_secs = duration.as_secs();
105+
// Simple date calculation (not accounting for leap seconds, but good enough)
106+
let days_since_epoch = total_secs / 86400;
107+
108+
// Start from 1970-01-01
109+
let mut year = 1970;
110+
let mut remaining_days = days_since_epoch;
111+
112+
loop {
113+
let days_in_year = if is_leap_year(year) { 366 } else { 365 };
114+
if remaining_days < days_in_year {
115+
break;
116+
}
117+
remaining_days -= days_in_year;
118+
year += 1;
119+
}
120+
121+
let days_in_months = if is_leap_year(year) {
122+
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
123+
} else {
124+
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
125+
};
126+
127+
let mut month = 1;
128+
let mut day = remaining_days + 1;
129+
130+
for days_in_month in &days_in_months {
131+
if day <= *days_in_month {
132+
break;
133+
}
134+
day -= days_in_month;
135+
month += 1;
136+
}
137+
138+
format!("{year:04}-{month:02}-{day:02}")
139+
}
140+
141+
const fn is_leap_year(year: u64) -> bool {
142+
(year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
143+
}

crates/build-info/src/lib.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//! Build-time information for Embucket binaries.
2+
//!
3+
//! This crate provides access to version and git metadata captured at build time.
4+
//! All information is embedded at compile time via environment variables set by build.rs.
5+
6+
/// Build information for Embucket binaries.
7+
pub struct BuildInfo;
8+
9+
impl BuildInfo {
10+
/// Version from Cargo.toml (e.g., "0.1.0")
11+
pub const VERSION: &'static str = env!("CARGO_PKG_VERSION");
12+
13+
/// Full git commit hash (e.g., "7b92aa2347...")
14+
pub const GIT_SHA: &'static str = env!("GIT_SHA");
15+
16+
/// Short git commit hash (e.g., "7b92aa23")
17+
pub const GIT_SHA_SHORT: &'static str = env!("GIT_SHA_SHORT");
18+
19+
/// Git branch name (e.g., "main")
20+
pub const GIT_BRANCH: &'static str = env!("GIT_BRANCH");
21+
22+
/// Git describe output - semantic version from tags
23+
/// Format examples:
24+
/// - "v0.1.0" - on a tag
25+
/// - "v0.1.0-5-g7b92aa23" - 5 commits after tag v0.1.0
26+
/// - "v0.1.0-dirty" - on a tag with uncommitted changes
27+
/// - "7b92aa23" - no tags exist, just the commit hash
28+
pub const GIT_DESCRIBE: &'static str = env!("GIT_DESCRIBE");
29+
30+
/// Whether the repository had uncommitted changes ("true" or "false")
31+
pub const GIT_DIRTY: &'static str = env!("GIT_DIRTY");
32+
33+
/// Build timestamp in RFC 3339 format
34+
pub const BUILD_TIMESTAMP: &'static str = env!("BUILD_TIMESTAMP");
35+
36+
/// Returns a formatted version string with git metadata.
37+
///
38+
/// Format: "0.1.0 (7b92aa23) on main built 2025-12-13"
39+
/// If dirty: "0.1.0 (7b92aa23-dirty) on main built 2025-12-13"
40+
#[must_use]
41+
pub fn full_version() -> String {
42+
let dirty_suffix = if Self::GIT_DIRTY == "true" {
43+
"-dirty"
44+
} else {
45+
""
46+
};
47+
format!(
48+
"{} ({}{}) on {} built {}",
49+
Self::VERSION,
50+
Self::GIT_SHA_SHORT,
51+
dirty_suffix,
52+
Self::GIT_BRANCH,
53+
Self::BUILD_TIMESTAMP
54+
)
55+
}
56+
57+
/// Returns true if the repository had uncommitted changes at build time.
58+
#[must_use]
59+
pub fn is_dirty() -> bool {
60+
Self::GIT_DIRTY == "true"
61+
}
62+
}
63+
64+
#[cfg(test)]
65+
mod tests {
66+
use super::*;
67+
68+
#[test]
69+
fn test_build_info_constants() {
70+
// These should all be non-empty (either real values or "unknown")
71+
assert!(!BuildInfo::VERSION.is_empty());
72+
assert!(!BuildInfo::GIT_SHA.is_empty());
73+
assert!(!BuildInfo::GIT_SHA_SHORT.is_empty());
74+
assert!(!BuildInfo::GIT_BRANCH.is_empty());
75+
assert!(!BuildInfo::GIT_DIRTY.is_empty());
76+
assert!(!BuildInfo::BUILD_TIMESTAMP.is_empty());
77+
}
78+
79+
#[test]
80+
fn test_full_version() {
81+
let version = BuildInfo::full_version();
82+
// Should contain at least the version number
83+
assert!(version.contains(BuildInfo::VERSION));
84+
}
85+
86+
#[test]
87+
fn test_is_dirty() {
88+
// Should return a boolean without panicking
89+
let _ = BuildInfo::is_dirty();
90+
}
91+
}

crates/embucket-lambda/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ api-snowflake-rest = { path = "../api-snowflake-rest" }
99
api-snowflake-rest-sessions = { path = "../api-snowflake-rest-sessions" }
1010
catalog-metastore = { path = "../catalog-metastore" }
1111
executor = { path = "../executor" }
12+
build-info = { path = "../build-info" }
1213
lambda_http = "0.17"
1314
tokio = { workspace = true }
1415
tracing = { workspace = true }

crates/embucket-lambda/src/config.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use build_info::BuildInfo;
12
use executor::utils::{Config as ExecutionConfig, MemPoolType};
23
use std::{env, path::PathBuf};
34

@@ -40,7 +41,7 @@ impl EnvConfig {
4041
mem_pool_size_mb: parse_env("MEM_POOL_SIZE_MB"),
4142
mem_enable_track_consumers_pool: parse_env("MEM_ENABLE_TRACK_CONSUMERS_POOL"),
4243
disk_pool_size_mb: parse_env("DISK_POOL_SIZE_MB"),
43-
embucket_version: env_or_default("EMBUCKET_VERSION", "0.1.0"),
44+
embucket_version: env_or_default("EMBUCKET_VERSION", BuildInfo::VERSION),
4445
metastore_config: env::var("METASTORE_CONFIG").ok().map(PathBuf::from),
4546
jwt_secret: env::var("JWT_SECRET").ok(),
4647
max_concurrent_table_fetches: parse_env("MAX_CONCURRENT_TABLE_FETCHES").unwrap_or(5),

crates/embucket-lambda/src/main.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use api_snowflake_rest_sessions::session::SESSION_EXPIRATION_SECONDS;
1010
use axum::Router;
1111
use axum::body::Body as AxumBody;
1212
use axum::extract::connect_info::ConnectInfo;
13+
use build_info::BuildInfo;
1314
use catalog_metastore::metastore_settings_config::MetastoreSettingsConfig;
1415
use http::HeaderMap;
1516
use http_body_util::BodyExt;
@@ -33,6 +34,15 @@ type InitResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
3334
async fn main() -> Result<(), LambdaError> {
3435
init_tracing();
3536

37+
// Log version and build information on startup
38+
info!(
39+
version = %BuildInfo::GIT_DESCRIBE,
40+
git_sha = %BuildInfo::GIT_SHA_SHORT,
41+
git_branch = %BuildInfo::GIT_BRANCH,
42+
build_timestamp = %BuildInfo::BUILD_TIMESTAMP,
43+
"embucket-lambda started"
44+
);
45+
3646
let env_config = EnvConfig::from_env();
3747
info!(
3848
data_format = %env_config.data_format,
@@ -69,7 +79,11 @@ struct LambdaApp {
6979
impl LambdaApp {
7080
#[tracing::instrument(name = "lambda_app_initialize", skip_all, fields(
7181
data_format = %config.data_format,
72-
max_concurrency = config.max_concurrency_level
82+
max_concurrency = config.max_concurrency_level,
83+
version = %BuildInfo::GIT_DESCRIBE,
84+
git_sha = %BuildInfo::GIT_SHA_SHORT,
85+
git_branch = %BuildInfo::GIT_BRANCH,
86+
build_timestamp = %BuildInfo::BUILD_TIMESTAMP,
7387
))]
7488
async fn initialize(config: EnvConfig) -> InitResult<Self> {
7589
let snowflake_cfg = SnowflakeServerConfig::new(
@@ -113,7 +127,11 @@ impl LambdaApp {
113127
http.method = %request.method(),
114128
http.uri = %request.uri(),
115129
http.request_id = tracing::field::Empty,
116-
http.status_code = tracing::field::Empty
130+
http.status_code = tracing::field::Empty,
131+
version = %BuildInfo::GIT_DESCRIBE,
132+
git_sha = %BuildInfo::GIT_SHA_SHORT,
133+
git_branch = %BuildInfo::GIT_BRANCH,
134+
build_timestamp = %BuildInfo::BUILD_TIMESTAMP,
117135
))]
118136
async fn handle_event(&self, request: Request) -> Result<Response<LambdaBody>, LambdaError> {
119137
let (parts, body) = request.into_parts();

crates/embucketd/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ executor = { path = "../executor" }
1010
catalog-metastore = { path = "../catalog-metastore" }
1111
api-snowflake-rest = { path = "../api-snowflake-rest" }
1212
api-snowflake-rest-sessions = { path = "../api-snowflake-rest-sessions" }
13+
build-info = { path = "../build-info" }
1314
axum = { workspace = true }
1415
clap = { workspace = true }
1516
console-subscriber = { version = "0.4.1" }

crates/embucketd/src/main.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use axum::{
1616
Json, Router,
1717
routing::{get, post},
1818
};
19+
use build_info::BuildInfo;
1920
use catalog_metastore::metastore_settings_config::MetastoreSettingsConfig;
2021
use clap::Parser;
2122
use dotenv::dotenv;
@@ -100,6 +101,15 @@ async fn async_main(
100101
opts: cli::CliOpts,
101102
tracing_provider: SdkTracerProvider,
102103
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
104+
// Log version and build information on startup
105+
tracing::info!(
106+
version = %BuildInfo::GIT_DESCRIBE,
107+
git_sha = %BuildInfo::GIT_SHA_SHORT,
108+
git_branch = %BuildInfo::GIT_BRANCH,
109+
build_timestamp = %BuildInfo::BUILD_TIMESTAMP,
110+
"embucketd started"
111+
);
112+
103113
let data_format = opts
104114
.data_format
105115
.clone()
@@ -112,7 +122,7 @@ async fn async_main(
112122
);
113123

114124
let execution_cfg = ExecutionConfig {
115-
embucket_version: "0.1.0".to_string(),
125+
embucket_version: BuildInfo::VERSION.to_string(),
116126
sql_parser_dialect: opts.sql_parser_dialect.clone(),
117127
query_timeout_secs: opts.query_timeout_secs,
118128
max_concurrency_level: opts.max_concurrency_level,

0 commit comments

Comments
 (0)