Skip to main content

haste_health/
main.rs

1#[global_allocator]
2static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
3
4use std::{collections::HashMap, time::Duration};
5
6use clap::{Parser, Subcommand};
7use haste_config::{Config, ConfigType, get_config};
8use haste_fhir_model::r4::generated::terminology::IssueType;
9use haste_fhir_operation_error::OperationOutcomeError;
10use opentelemetry::KeyValue;
11use opentelemetry::trace::TracerProvider as _;
12use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
13use opentelemetry_otlp::WithExportConfig;
14use opentelemetry_otlp::{Protocol, WithHttpConfig};
15use opentelemetry_sdk::Resource;
16use opentelemetry_sdk::logs::SdkLoggerProvider;
17use opentelemetry_sdk::trace::SdkTracerProvider;
18use reqwest::Url;
19use tracing_subscriber::registry::LookupSpan;
20use tracing_subscriber::{EnvFilter, layer::SubscriberExt, registry::Registry};
21use tracing_tree::HierarchicalLayer;
22
23mod cli;
24mod commands;
25
26#[derive(Parser)]
27#[command(version, about, long_about = None)] // Read from `Cargo.toml`
28pub struct Cli {
29    #[command(subcommand)]
30    command: CliCommand,
31}
32
33#[derive(Subcommand)]
34enum CliCommand {
35    /// Evaluate a FHIRPath expression against a FHIR resource read from stdin.
36    FHIRPath {
37        /// FHIRPath expression to evaluate
38        fhirpath: String,
39    },
40    /// Code generators (Rust FHIR types, operations, TestScripts) used to build this crate.
41    Generate {
42        #[command(subcommand)]
43        command: commands::codegen::CodeGen,
44    },
45    /// Run the FHIR server.
46    Server {
47        #[command(subcommand)]
48        command: commands::server::ServerCommands,
49    },
50    /// Make FHIR REST API calls against the active profile's server.
51    Api {
52        #[command(subcommand)]
53        command: commands::api::ApiCommands,
54    },
55    /// Manage named server connection profiles used by other commands.
56    Config {
57        #[command(subcommand)]
58        command: commands::config::ConfigCommands,
59    },
60    /// Log in as a human user via the browser (authorization_code + PKCE flow).
61    Login,
62    /// Run background workers (search indexing, WAL processing).
63    Worker {
64        #[command(subcommand)]
65        command: Option<commands::worker::WorkerCommands>,
66    },
67    /// Run FHIR TestScript resources against the active profile's server.
68    Testscript {
69        #[command(subcommand)]
70        command: commands::testscript::TestScriptCommands,
71    },
72    /// Server-side administrative operations (tenants, users, clients, migrations).
73    Admin {
74        #[command(subcommand)]
75        command: commands::admin::AdminCommands,
76    },
77    /// Bridge HL7v2 messages to and from the FHIR server.
78    Hl7v2 {
79        #[command(subcommand)]
80        command: commands::hl7v2::HL7v2Commands,
81    },
82    /// Generate Markdown documentation for this CLI's commands.
83    Doc {
84        /// Output markdown file path
85        #[arg(short, long)]
86        output: String,
87    },
88}
89
90enum CliEnvironmentVariables {
91    SentryDSN,
92    OTELEndpoint,
93    OTELHeaders,
94    LogType,
95}
96
97impl From<CliEnvironmentVariables> for String {
98    fn from(value: CliEnvironmentVariables) -> Self {
99        match value {
100            CliEnvironmentVariables::SentryDSN => "SENTRY_DSN".to_string(),
101            CliEnvironmentVariables::OTELEndpoint => "OTEL_ENDPOINT".to_string(),
102            CliEnvironmentVariables::OTELHeaders => "OTEL_HEADERS".to_string(),
103            CliEnvironmentVariables::LogType => "LOG_TYPE".to_string(),
104        }
105    }
106}
107
108struct OtelGuard {
109    _tracer_provider: SdkTracerProvider,
110    _logger_provider: SdkLoggerProvider,
111}
112
113fn otel_guard(config: &dyn Config<CliEnvironmentVariables>) -> Option<OtelGuard> {
114    let endpoint = config.get(CliEnvironmentVariables::OTELEndpoint).ok()?;
115    let headers_str = config
116        .get(CliEnvironmentVariables::OTELHeaders)
117        .unwrap_or_default();
118    let headers = headers_str
119        .split(',')
120        .filter_map(|pair| {
121            let mut parts = pair.splitn(2, '=');
122            if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
123                Some((key.trim().to_string(), value.trim().to_string()))
124            } else {
125                None
126            }
127        })
128        .collect::<HashMap<String, String>>();
129
130    let root_otel_endpoint = Url::parse(&endpoint).expect("Invalid OTLP endpoint URL");
131
132    // See https://opentelemetry.io/docs/specs/otlp/#otlphttp-request
133    // v1/traces for spans, v1/logs for logs
134    let mut trace_endpoint = root_otel_endpoint.clone();
135    trace_endpoint
136        .path_segments_mut()
137        .expect("OTEL endpoint cannot be a base URL")
138        .extend(&["v1", "traces"]);
139
140    let mut log_endpoint = root_otel_endpoint.clone();
141    log_endpoint
142        .path_segments_mut()
143        .expect("OTEL endpoint cannot be a base URL")
144        .extend(&["v1", "logs"]);
145
146    let oltp_span_exporter = opentelemetry_otlp::SpanExporter::builder()
147        .with_http()
148        .with_protocol(Protocol::HttpBinary)
149        .with_endpoint(trace_endpoint.as_str())
150        .with_headers(headers.clone())
151        .with_timeout(Duration::from_secs(5))
152        .build()
153        .expect("Failed to create OpenTelemetry span exporter");
154
155    let oltp_log_exporter = opentelemetry_otlp::LogExporter::builder()
156        .with_http()
157        .with_protocol(Protocol::HttpBinary)
158        .with_endpoint(log_endpoint.as_str())
159        .with_headers(headers)
160        .with_timeout(Duration::from_secs(5))
161        .build()
162        .expect("Failed to create OpenTelemetry log exporter");
163
164    let resource = Resource::builder()
165        .with_attribute(KeyValue::new("service.name", "haste-health"))
166        .build();
167
168    let tracer_provider = SdkTracerProvider::builder()
169        .with_resource(resource.clone())
170        .with_batch_exporter(oltp_span_exporter)
171        .build();
172
173    let logger_provider = SdkLoggerProvider::builder()
174        .with_resource(resource)
175        .with_batch_exporter(oltp_log_exporter)
176        .build();
177
178    opentelemetry::global::set_tracer_provider(tracer_provider.clone());
179
180    Some(OtelGuard {
181        _tracer_provider: tracer_provider,
182        _logger_provider: logger_provider,
183    })
184}
185
186fn inject_otel_subscriber<S>(
187    subscriber: S,
188    config: &dyn Config<CliEnvironmentVariables>,
189) -> Option<OtelGuard>
190where
191    S: tracing::Subscriber + Send + Sync + 'static,
192    for<'span> S: LookupSpan<'span>,
193{
194    if let Some(guard) = otel_guard(config) {
195        let tracer = guard._tracer_provider.tracer("haste-health");
196        let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
197        let otel_logs = OpenTelemetryTracingBridge::new(&guard._logger_provider);
198
199        tracing::subscriber::set_global_default(subscriber.with(telemetry).with(otel_logs))
200            .unwrap();
201
202        Some(guard)
203    } else {
204        tracing::subscriber::set_global_default(subscriber).unwrap();
205        None
206    }
207}
208
209fn setup_tracing(
210    config: &dyn Config<CliEnvironmentVariables>,
211) -> Result<Option<OtelGuard>, OperationOutcomeError> {
212    let log_type = config
213        .get(CliEnvironmentVariables::LogType)
214        .unwrap_or("JSON".to_string());
215
216    let subscriber = Registry::default()
217        .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")));
218
219    match log_type.as_str() {
220        "TREE" => Ok(inject_otel_subscriber(
221            subscriber.with(HierarchicalLayer::new(2)),
222            config,
223        )),
224        "JSON" => Ok(inject_otel_subscriber(
225            subscriber.with(tracing_subscriber::fmt::Layer::default().json()),
226            config,
227        )),
228        _ => Err(OperationOutcomeError::fatal(
229            IssueType::invalid(),
230            "Invalid log type specified in environment variable LOG_TYPE. Supported values are 'TREE' and 'JSON'.".to_string(),
231        )),
232    }
233}
234
235fn main() -> Result<(), OperationOutcomeError> {
236    let config = get_config(ConfigType::Environment);
237
238    let cli = Cli::parse();
239    let cli_state = cli::state::CLI_STATE.clone();
240
241    let sentry_location = config.get(CliEnvironmentVariables::SentryDSN);
242
243    let _guard = sentry::init((
244        sentry_location.unwrap_or_default(),
245        sentry::ClientOptions {
246            release: sentry::release_name!(),
247            // Capture user IPs and potentially sensitive headers when using HTTP server integrations
248            // see https://docs.sentry.io/platforms/rust/data-management/data-collected for more info
249            send_default_pii: true,
250            ..Default::default()
251        },
252    ));
253
254    tokio::runtime::Builder::new_multi_thread()
255        .enable_all()
256        // 8MB stack size
257        .thread_stack_size(1024 * 8000)
258        .build()
259        .unwrap()
260        .block_on(async {
261            let _otel_provider = setup_tracing(config.as_ref())?;
262            commands::run(cli_state, &cli.command).await
263        })
264}