go-fish-game-server 0.3.0

A WebSocket game server for the Go Fish card game, supporting human players and bots.
Documentation
use clap::Parser;
use go_fish_game_server::{run, Config};
use opentelemetry::{global, trace::TracerProvider as _};
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
use opentelemetry_otlp::WithHttpConfig;
use opentelemetry_sdk::{logs::SdkLoggerProvider, trace::SdkTracerProvider};
use tracing::{info, warn};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};

#[derive(Parser)]
#[command(name = "go-fish-game-server")]
struct Cli {
    /// Path to a TOML config file
    #[arg(long)]
    config: Option<std::path::PathBuf>,
}

fn service_resource() -> opentelemetry_sdk::Resource {
    opentelemetry_sdk::Resource::builder()
        .with_service_name("go-fish-game-server")
        .with_attribute(opentelemetry::KeyValue::new("service.version", env!("CARGO_PKG_VERSION")))
        .build()
}

fn init_tracer_provider() -> SdkTracerProvider {
    let exporter = opentelemetry_otlp::SpanExporter::builder()
        .with_http()
        // Blocking client is safe here: the batch processor runs on its own dedicated
        // thread outside the Tokio runtime, so blocking never affects the async server.
        .with_http_client(reqwest::blocking::Client::new())
        .build()
        .expect("failed to build OTLP span exporter");

    let provider = SdkTracerProvider::builder()
        .with_batch_exporter(exporter)
        .with_resource(service_resource())
        .build();

    global::set_tracer_provider(provider.clone());
    provider
}

fn init_logger_provider() -> SdkLoggerProvider {
    let exporter = opentelemetry_otlp::LogExporter::builder()
        .with_http()
        // Blocking client is safe here: the batch processor runs on its own dedicated
        // thread outside the Tokio runtime, so blocking never affects the async server.
        .with_http_client(reqwest::blocking::Client::new())
        .build()
        .expect("failed to build OTLP log exporter");

    SdkLoggerProvider::builder()
        .with_batch_exporter(exporter)
        .with_resource(service_resource())
        .build()
}

// main is intentionally sync so that the telemetry providers are created and dropped
// outside the Tokio runtime. reqwest::blocking::Client owns an internal runtime, and
// dropping it from within an async context panics.
fn main() -> Result<(), anyhow::Error> {
    let tracer_provider = init_tracer_provider();
    let logger_provider = init_logger_provider();
    let trace_layer = tracing_opentelemetry::layer().with_tracer(tracer_provider.tracer("go-fish-game-server"));
    let log_layer = OpenTelemetryTracingBridge::new(&logger_provider);

    tracing_subscriber::registry()
        .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
        .with(tracing_subscriber::fmt::layer())
        .with(trace_layer)
        .with(log_layer)
        .init();

    let result = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()?
        .block_on(run_server());

    // Flush telemetry before exit
    let _ = tracer_provider.shutdown();
    let _ = logger_provider.shutdown();

    result
}

async fn run_server() -> Result<(), anyhow::Error> {
    let cli = Cli::parse();

    let config = match cli.config {
        Some(path) => match std::fs::read_to_string(&path) {
            Ok(contents) => match toml::from_str::<Config>(&contents) {
                Ok(cfg) => cfg,
                Err(e) => {
                    warn!(event = "config_parse_failed", error = %e, path = %path.display(), "Failed to parse config file. Using defaults.");
                    Config::default()
                }
            },
            Err(e) => {
                warn!(event = "config_read_failed", error = %e, path = %path.display(), "Failed to read config file. Using defaults.");
                Config::default()
            }
        },
        None => Config::default(),
    };

    info!(
        event = "server_started",
        address = %config.address,
        max_client_connections = %config.max_client_connections,
        lobby_max_players = %config.lobby_max_players
    );
    run(config).await
}