Skip to content
Open
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
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions crates/bid-scraper/src/bin/bid-scraper-test-client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use bid_scraper::{
bid_scraper_client::{run_nng_subscriber_with_retries, ScrapedBidsObs},
types::ScrapedRelayBlockBid,
};
use rbuilder_config::LoggerConfig;
use rbuilder_config::{LoggerConfig, TracingConfig};
use std::{env, sync::Arc, time::Duration};
use tokio::signal::ctrl_c;
use tokio_util::sync::CancellationToken;
Expand All @@ -29,12 +29,13 @@ async fn main() -> eyre::Result<()> {
return Ok(());
}

let logger_config = LoggerConfig {
let tracing_config = TracingConfig::from(LoggerConfig {
env_filter: "info".to_owned(),
log_json: false,
log_color: true,
};
logger_config.init_tracing()?;
});

let _guard = tracing_config.init_tracing()?;

let cancel = CancellationToken::new();
tokio::spawn({
Expand Down
15 changes: 11 additions & 4 deletions crates/bid-scraper/src/bin/bid-scraper.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use bid_scraper::{bid_sender::NNGBidSender, config::Config};
use rbuilder_config::{load_toml_config, LoggerConfig};
use rbuilder_config::{load_toml_config, LoggerConfig, OtlpConfig, TracingConfig};
use runng::Listen;
use std::{env, sync::Arc};
use tokio::signal::ctrl_c;
Expand All @@ -15,12 +15,19 @@ async fn main() -> eyre::Result<()> {

let config: Config = load_toml_config(args[1].clone())?;

let logger_config = LoggerConfig {
let tracing_config = TracingConfig::from(LoggerConfig {
env_filter: config.log_level.clone(),
log_json: config.log_json,
log_color: config.log_color,
};
logger_config.init_tracing()?;
})
.maybe_with_otlp(
config
.otlp_env_name
.as_ref()
.map(|name| OtlpConfig::new().with_environment(name.clone())),
);

let _guard = tracing_config.init_tracing()?;

let global_cancel = CancellationToken::new();
let global_cancel_clone = global_cancel.clone();
Expand Down
2 changes: 2 additions & 0 deletions crates/bid-scraper/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub struct Config {
/// Example: "info"
pub log_level: String,
pub log_color: bool,
/// OTLP environment name, e.g. "production", "staging", etc.
pub otlp_env_name: Option<String>,

/// Where we publish the bids. Example:"tcp://0.0.0.0:5555"
pub publisher_url: String,
Expand Down
6 changes: 6 additions & 0 deletions crates/rbuilder-config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ serde_with.workspace = true
toml.workspace = true
eyre.workspace = true
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] }
tracing-opentelemetry = "0.32.0"
opentelemetry_sdk = "0.31.0"
opentelemetry-semantic-conventions = { version = "0.31.0", features = ["semconv_experimental"] }
opentelemetry = "0.31.0"
opentelemetry-otlp = "0.31.0"
tracing.workspace = true
6 changes: 6 additions & 0 deletions crates/rbuilder-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ pub use logger::*;
mod env_or_value;
pub use env_or_value::*;

mod otlp;
pub use otlp::*;

mod tracing;
pub use tracing::*;

/// Loads configuration from the toml file.
pub fn load_toml_config<T: DeserializeOwned>(path: impl AsRef<Path>) -> eyre::Result<T> {
let data = fs::read_to_string(path.as_ref()).with_context(|| {
Expand Down
64 changes: 56 additions & 8 deletions crates/rbuilder-config/src/logger.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
use tracing_subscriber::EnvFilter;
use tracing_subscriber::{
fmt::{
format::{DefaultFields, Format, Json, JsonFields},
SubscriberBuilder,
},
EnvFilter,
};

/// Logger configuration.
#[derive(PartialEq, Eq, Clone, Debug, serde::Deserialize)]
Expand All @@ -22,15 +28,57 @@ impl LoggerConfig {
}

impl LoggerConfig {
/// Create an [`EnvFilter`] from the configuration. Fails if the filter
/// string is invalid.
pub(crate) fn filter(&self) -> eyre::Result<EnvFilter> {
EnvFilter::try_new(&self.env_filter).map_err(|e| eyre::eyre!(e))
}

/// Create a [`SubscriberBuilder`] from the configuration. Fails if the
/// filter string is invalid.
pub(crate) fn builder(
&self,
) -> eyre::Result<SubscriberBuilder<DefaultFields, Format, EnvFilter>> {
self.filter()
.map(|filter| tracing_subscriber::fmt().with_env_filter(filter))
}

/// Create a JSON-formatting [`SubscriberBuilder`] from the configuration.
///
/// Errors if the filter string is invalid.
///
/// # Panics
///
/// If `self.log_json` is false.
pub(crate) fn json(
&self,
) -> eyre::Result<SubscriberBuilder<JsonFields, Format<Json>, EnvFilter>> {
assert!(self.log_json);
self.builder().map(SubscriberBuilder::json)
}

/// Create an ANSI-formatting [`SubscriberBuilder`] from the configuration.
///
/// Errors if the filter string is invalid.
///
/// # Panics
///
/// If `self.log_json` is true.
pub(crate) fn ansi(&self) -> eyre::Result<SubscriberBuilder<DefaultFields, Format, EnvFilter>> {
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type 'Format' is ambiguous and should be fully qualified as 'Format<Full, SystemTime>' to match the pattern used in the 'builder()' method.

Suggested change
pub(crate) fn ansi(&self) -> eyre::Result<SubscriberBuilder<DefaultFields, Format, EnvFilter>> {
pub(crate) fn ansi(&self) -> eyre::Result<SubscriberBuilder<DefaultFields, Format<Full, SystemTime>, EnvFilter>> {

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i go the other way. including default type args you don't have to is overly verbose

assert!(!self.log_json);
self.builder().map(|b| b.with_ansi(self.log_color))
}

/// Initialize tracing subscriber based on the configuration.
#[deprecated(
note = "This function will not configure OTLP tracing, which may be desirable. Instead, use the crate::tracing::TracingConfig to initialize logging and/or tracing."
)]
pub fn init_tracing(self) -> eyre::Result<()> {
let env_filter = EnvFilter::try_new(&self.env_filter)?;
let builder = tracing_subscriber::fmt().with_env_filter(env_filter);
let result = if self.log_json {
builder.json().try_init()
if self.log_json {
self.json()?.try_init()
} else {
builder.with_ansi(self.log_color).try_init()
};
result.map_err(|err| eyre::format_err!("{err}"))
self.ansi()?.try_init()
}
.map_err(|err| eyre::eyre!(err))
}
}
128 changes: 128 additions & 0 deletions crates/rbuilder-config/src/otlp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use opentelemetry::{trace::TracerProvider, KeyValue};
use opentelemetry_sdk::{trace::SdkTracerProvider, Resource};
use opentelemetry_semantic_conventions::{
resource::{DEPLOYMENT_ENVIRONMENT_NAME, SERVICE_NAME, SERVICE_VERSION},
SCHEMA_URL,
};
use tracing_subscriber::Layer;

/// Drop guard for the OTLP exporter. This will shutdown the exporter when
/// dropped, and generally should be held for the lifetime of the `main`
/// function.
///
/// The guard may be used to produce a [`tracing`] layer that can be added to
/// an existing [`tracing::Subscriber`].
#[derive(Debug)]
pub struct OtlpGuard(SdkTracerProvider);

impl OtlpGuard {
/// Get a tracer from the provider.
fn tracer(&self, s: &'static str) -> opentelemetry_sdk::trace::Tracer {
self.0.tracer(s)
}

/// Create a filtered tracing layer.
pub fn layer<S>(&self) -> impl Layer<S>
where
S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
{
let tracer = self.tracer("tracing-otel-subscriber");
tracing_opentelemetry::layer().with_tracer(tracer)
}
}

/// Configuration for the OTLP system. Currently this allows configuring the
/// deployment environment name.
///
/// OTEL and OTLP are configured primarily via environment variables. The
/// standard variables are as follows:
///
/// - `OTEL_TRACES_EXPORTER` - Set the exporter to be used. Generally should be
/// set to `otlp`. Other options include `none`, `zipkin`, etc. See the
/// [relevant documentation] for more details.
///
/// - `OTEL_EXPORTER_OTLP_ENDPOINT` - Set to the URL endpoint to which to send
/// OTLP data. If not set, traces will not be exported. This will typically
/// be a URL ending in port 4317 (grpc) or 4318 (http).
///
/// - `OTEL_EXPORTER_OTLP_PROTOCOL` - Specifies the OTLP transport protocol to
/// be used for all telemetry data. Acceptable values are `grpc`,
/// `http/protobuf`, and `http/json`.
///
/// For advanced exporter configuration via environment variables, see the
/// [OTLP documentation].
///
/// [relevant documentation]: https://opentelemetry.io/docs/instrumentation/js/exporters/#environment-variables
/// [OTLP documentation]: https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/
#[derive(Default, PartialEq, Eq, Clone, Debug, serde::Deserialize)]
#[non_exhaustive]
pub struct OtlpConfig {
/// The name of the deployment environment (a.k.a deployment tier), e.g.
/// "production", "staging". [See documentation here.]
///
/// [See documentation here.]: https://opentelemetry.io/docs/specs/semconv/resource/deployment-environment/
pub otlp_environment: String,
}

impl OtlpConfig {
/// Default OTEL configuration for development.
pub fn dev() -> Self {
Self {
otlp_environment: "development".to_owned(),
}
}

/// Instantiate a new OTEL configuration, with default values.
pub fn new() -> Self {
Self::default()
}

/// Set the environment name.
pub fn with_environment(mut self, name: impl Into<String>) -> Self {
self.otlp_environment = name.into();
self
}

fn resource(&self) -> Resource {
Resource::builder()
.with_schema_url(
[
KeyValue::new(SERVICE_NAME, env!("CARGO_PKG_NAME")),
KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION")),
KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, self.otlp_environment.clone()),
],
SCHEMA_URL,
)
.build()
}

/// Instantiate a new OTEL provider, with a simple batch exporter, and
/// start relevant tasks. Return a guard that will shut down the provider
/// and exporter when dropped.
pub fn provider(&self) -> OtlpGuard {
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_http()
.build()
.unwrap();

let provider = SdkTracerProvider::builder()
// Customize sampling strategy
// If export trace to AWS X-Ray, you can use XrayIdGenerator
.with_resource(self.resource())
.with_batch_exporter(exporter)
.build();

OtlpGuard(provider)
}

/// Create a new OTEL provider, returning both the guard and a tracing
/// layer that can be added to a subscriber.
pub fn into_guard_and_layer<S>(self) -> (OtlpGuard, impl Layer<S>)
where
S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
{
let guard = self.provider();
let layer = guard.layer();
(guard, layer)
}
}
Loading