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)] pub struct Cli {
29 #[command(subcommand)]
30 command: CliCommand,
31}
32
33#[derive(Subcommand)]
34enum CliCommand {
35 FHIRPath {
37 fhirpath: String,
39 },
40 Generate {
42 #[command(subcommand)]
43 command: commands::codegen::CodeGen,
44 },
45 Server {
47 #[command(subcommand)]
48 command: commands::server::ServerCommands,
49 },
50 Api {
52 #[command(subcommand)]
53 command: commands::api::ApiCommands,
54 },
55 Config {
57 #[command(subcommand)]
58 command: commands::config::ConfigCommands,
59 },
60 Login,
62 Worker {
64 #[command(subcommand)]
65 command: Option<commands::worker::WorkerCommands>,
66 },
67 Testscript {
69 #[command(subcommand)]
70 command: commands::testscript::TestScriptCommands,
71 },
72 Admin {
74 #[command(subcommand)]
75 command: commands::admin::AdminCommands,
76 },
77 Hl7v2 {
79 #[command(subcommand)]
80 command: commands::hl7v2::HL7v2Commands,
81 },
82 Doc {
84 #[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 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 send_default_pii: true,
250 ..Default::default()
251 },
252 ));
253
254 tokio::runtime::Builder::new_multi_thread()
255 .enable_all()
256 .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}