Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,6 @@ cryptoki = "0.10"
secrecy = "0.8"
jsonwebtoken = "9.3"
rsa = "0.9"

[build-dependencies]
chrono = "0.4.19"
147 changes: 147 additions & 0 deletions crates/core/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
use chrono::{SecondsFormat, TimeZone, Utc};
use std::{cmp::Ordering, collections::HashSet, env, process::Command};

fn command_output(program: &str, args: &[&str]) -> Result<String, String> {
let output = Command::new(program)
.args(args)
.output()
.map_err(|e| format!("could not execute command `{program} {}`: {e}", args.join(" ")))?;

let mut value = String::from_utf8(output.stdout)
.map_err(|e| format!("could not parse output as utf-8: {e}"))?;
// https://stackoverflow.com/a/55041833
if value.ends_with('\n') {
value.pop();
if value.ends_with('\r') {
value.pop();
}
};

Ok(value)
}

fn set_env_if_needed(env: &str, value: &Result<String, String>) {
if env::var(env).is_ok() {
return;
}

match value {
Ok(v) => println!("cargo:rustc-env={env}={v}"),
Err(e) => {
// debug
if cfg!(debug_assertions) {
println!("cargo::warning=could not set env {env}: {e}")
}
// release
else {
println!("carg::error=env {env} not set for release: {e}");
}
}
}
}

fn branch_name_from_ref(branch_ref: &str) -> Option<&str> {
// local branch
if let Some(branch_name) = branch_ref.strip_prefix("refs/heads/") {
Some(branch_name)
}
// remote branche
else if let Some(remote_ref) = branch_ref.strip_prefix("refs/remotes/") {
let branch_name = &remote_ref[(remote_ref.find('/')? + 1)..];
if branch_name != "HEAD" {
Some(branch_name)
} else {
None
}
} else {
None
}
}

fn epoch_to_iso_timestamp(epoch: i64) -> String {
Utc.timestamp_opt(epoch, 0).unwrap().to_rfc3339_opts(SecondsFormat::Secs, true)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
// re-run if files change
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=.git/HEAD");
println!("cargo:rerun-if-changed=.git/refs/heads");

// re-run if env change
println!("cargo:rerun-if-env-changed=BUILD_COMMIT_HASH");
println!("cargo:rerun-if-env-changed=BUILD_COMMIT_TIMESTAMP_ISO");
println!("cargo:rerun-if-env-changed=BUILD_COMMIT_LABELS");
println!("cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH");

let commit_hash_result = command_output("git", &["log", "-1", "--format=%H"]);
set_env_if_needed("BUILD_COMMIT_HASH", &commit_hash_result);

set_env_if_needed(
"BUILD_COMMIT_TIMESTAMP_ISO",
&command_output("git", &["log", "-1", "--format=%ct"])
.map(|epoch| epoch_to_iso_timestamp(epoch.parse().unwrap())),
);

// TODO: get commit labels
let mut commit_label_set = HashSet::new();
if let Ok(commit_hash) = &commit_hash_result {
let branch_refs = command_output(
"git",
&["branch", "--points-at", commit_hash, "--format=%(refname)", "--all"],
)?;
for branch_ref in branch_refs.lines() {
let Some(branch_name) = branch_name_from_ref(branch_ref) else {
continue;
};
if !branch_name.is_empty() {
commit_label_set.insert(("branch", branch_name.to_string()));
}
}

let tags = command_output(
"git",
&["tag", "--points-at", commit_hash, "--format=%(refname:short)"],
)?;
for tag in tags.lines() {
if !tag.is_empty() {
commit_label_set.insert(("tag", tag.to_string()));
}
}
}

let mut commit_labels = commit_label_set.into_iter().collect::<Vec<_>>();
commit_labels.sort_unstable_by(|a, b| {
if a.0 != b.0 {
a.1.cmp(&b.1)
}
// tags before branches
else if a.0 == "tag" {
Ordering::Less
} else {
Ordering::Greater
}
});

set_env_if_needed(
"BUILD_COMMIT_LABELS",
&Ok(commit_labels
.into_iter()
.map(|(label_type, value)| match label_type {
"tag" => value.to_string(),
_ => format!("{label_type}:{value}"),
})
.collect::<Vec<_>>()
.join(" ")),
);

// https://reproducible-builds.org/docs/source-date-epoch/#rust
let build_timestamp_iso = epoch_to_iso_timestamp(
option_env!("SOURCE_DATE_EPOCH")
.map(|epoch| epoch.parse().unwrap())
.unwrap_or_else(|| Utc::now().timestamp()),
);
println!("cargo:rustc-env=BUILD_TIMESTAMP_ISO={build_timestamp_iso}");

Ok(())
}
3 changes: 3 additions & 0 deletions crates/core/src/app_state.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::sync::Arc;

use chrono::{DateTime, Utc};
use tokio::sync::Mutex;

use crate::common_types::EvmAddress;
Expand Down Expand Up @@ -56,6 +57,8 @@ impl RelayersAllowedForRandom {
}

pub struct AppState {
/// Timestamp when server was started
pub started_at: DateTime<Utc>,
/// Database client with connection pooling
pub db: Arc<PostgresClient>,
/// EVM blockchain provider connections
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod provider;
pub use provider::create_retry_client;
pub mod relayer;
pub mod safe_proxy;
pub mod server;
pub use safe_proxy::{SafeProxyError, SafeProxyManager, SafeTransaction};
pub use yaml::{
read, ApiConfig, AwsKmsSigningProviderConfig, GasProviders, NetworkSetupConfig,
Expand Down
13 changes: 13 additions & 0 deletions crates/core/src/server/api/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use std::sync::Arc;

use axum::{routing::get, Router};

use crate::app_state::AppState;

mod server_info;

use server_info::get_server_info;

pub fn create_server_routes() -> Router<Arc<AppState>> {
Router::new().route("/info", get(get_server_info))
}
22 changes: 22 additions & 0 deletions crates/core/src/server/api/server_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use std::sync::Arc;

use axum::{extract::State, Json};
use chrono::{SecondsFormat, Utc};

use crate::{
app_state::AppState,
server::{build_info, ServerInfo},
};

/// Returns detailed server info.
pub async fn get_server_info(State(state): State<Arc<AppState>>) -> Json<ServerInfo<'static>> {
Json(ServerInfo {
started_at_timestamp_iso: state.started_at.to_rfc3339_opts(SecondsFormat::Secs, true),
uptime_seconds: (Utc::now() - state.started_at).num_seconds(),

commit_hash: build_info::BUILD_COMMIT_HASH,
commit_timestamp_iso: build_info::BUILD_COMMIT_TIMESTAMP_ISO,
commit_labels: build_info::BUILD_COMMIT_LABELS.split(" ").collect(),
build_timestamp_iso: build_info::BUILD_TIMESTAMP_ISO,
})
}
16 changes: 16 additions & 0 deletions crates/core/src/server/build_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
pub const BUILD_COMMIT_HASH: &str = match option_env!("BUILD_COMMIT_HASH") {
Some(v) => v,
None => "0000000000000000000000000000000000000000",
};

pub const BUILD_COMMIT_TIMESTAMP_ISO: &str = match option_env!("BUILD_COMMIT_TIMESTAMP_ISO") {
Some(v) => v,
None => "1970-01-01T00:00:00.000Z",
};

pub const BUILD_COMMIT_LABELS: &str = match option_env!("BUILD_COMMIT_LABELS") {
Some(v) => v,
None => "",
};

pub const BUILD_TIMESTAMP_ISO: &str = env!("BUILD_TIMESTAMP_ISO");
7 changes: 7 additions & 0 deletions crates/core/src/server/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod api;
pub use api::create_server_routes;

pub mod build_info;

mod types;
pub use types::*;
2 changes: 2 additions & 0 deletions crates/core/src/server/types/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod server_info;
pub use server_info::ServerInfo;
30 changes: 30 additions & 0 deletions crates/core/src/server/types/server_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ServerInfo<'a> {
/// The iso timestamp of when the server was started
#[serde(rename = "startedAtTimestampIso")]
pub started_at_timestamp_iso: String,

/// The number of seconds the server has been running for
#[serde(rename = "uptimeSeconds")]
pub uptime_seconds: i64,

/// The hash of the commit used to build the server
#[serde(rename = "commitHash")]
pub commit_hash: &'a str,

/// The iso timestamp of the commit used to build the server
#[serde(rename = "commitTimestampIso")]
pub commit_timestamp_iso: &'a str,

/// The labels of the commit used to build the server
/// sorted list with tags first, then branches
/// branches are prefixed with `branch:`
#[serde(rename = "commitLabels")]
pub commit_labels: Vec<&'a str>,

/// The iso timestamp when the server was built
#[serde(rename = "buildTimestampIso")]
pub build_timestamp_iso: &'a str,
}
4 changes: 4 additions & 0 deletions crates/core/src/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::background_tasks::run_background_tasks;
use crate::common_types::EvmAddress;
use crate::gas::{BlobGasOracleCache, GasOracleCache};
use crate::network::{create_network_routes, ChainId};
use crate::server::create_server_routes;
use crate::shared::HttpError;
use crate::webhooks::WebhookManager;
use crate::yaml::{AllOrOneOrManyAddresses, ApiKey, NetworkPermissionsConfig, ReadYamlError};
Expand Down Expand Up @@ -37,6 +38,7 @@ use axum::{
routing::get,
Json, Router,
};
use chrono::Utc;
use dotenv::dotenv;
use rustls::crypto::ring::default_provider;
use rustls::crypto::CryptoProvider;
Expand Down Expand Up @@ -207,6 +209,7 @@ async fn start_api(
.collect();

let app_state = Arc::new(AppState {
started_at: Utc::now(),
db: db.clone(),
evm_providers: providers,
gas_oracle_cache,
Expand Down Expand Up @@ -250,6 +253,7 @@ async fn start_api(
.nest("/networks", create_network_routes())
.nest("/relayers", create_relayer_routes())
.nest("/transactions", create_transactions_routes())
.nest("/server", create_server_routes())
.nest("/signing", create_signing_routes());

let app = Router::new()
Expand Down
1 change: 1 addition & 0 deletions documentation/rrelayer/docs/pages/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

### Features

- feat: add `/server/info` endpoint to report build info and uptime
- feat: upgrade alloy dependencies to version 2 for rust consumers
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,37 @@ curl https://your-rrelayer.com/health

This endpoint does not require authentication and returns a simple "healthy" string when the server is operational.

## Server Info

Get server info like the build commit, build timestamp, uptime, etc.

### Endpoint

```
GET /server/info
```

### Response

```json
{
"startedAtTimestampIso": "2026-07-22T16:40:34Z",
"uptimeSeconds": 49,
"commitHash": "645fbf4b56f51774619d59360f5525988e7af073",
"commitTimestampIso": "2026-07-02T12:06:48Z",
"commitLabels": ["branch:master"],
"buildTimestampIso": "2026-07-22T16:40:07Z"
}
```

### Example

```bash
curl https://your-rrelayer.com/server/info
```

This endpoint does not require authentication.

## Rate Limiting Headers

When using rate limiting, include the rate limit key header:
Expand Down
Loading