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;
25mod utilities;
26
27#[derive(Parser)]
28#[command(version, about, long_about = None)] pub struct Cli {
30 #[command(subcommand)]
31 command: CliCommand,
32}
33
34#[derive(Subcommand)]
35enum CliCommand {
36 #[command(name = "fhirpath")]
38 FHIRPath {
39 fhirpath: String,
41 },
42 Generate {
44 #[command(subcommand)]
45 command: commands::codegen::CodeGen,
46 },
47 Artifacts {
49 #[command(subcommand)]
50 command: commands::artifacts::ArtifactCommands,
51 },
52 Subscription {
54 #[command(subcommand)]
55 command: commands::subscription::SubscriptionCommands,
56 },
57 Server {
59 #[command(subcommand)]
60 command: commands::server::ServerCommands,
61 },
62 Api {
64 #[command(subcommand)]
65 command: commands::api::ApiCommands,
66 },
67 Profile {
69 #[command(subcommand)]
70 command: commands::profile::ProfileCommands,
71 },
72 Login {
74 #[arg(long)]
76 no_browser: bool,
77 },
78 Worker {
80 #[command(subcommand)]
81 command: Option<commands::worker::WorkerCommands>,
82 },
83 Testscript {
85 #[command(subcommand)]
86 command: commands::testscript::TestScriptCommands,
87 },
88 Admin {
90 #[command(subcommand)]
91 command: commands::admin::AdminCommands,
92 },
93 Hl7v2 {
95 #[command(subcommand)]
96 command: commands::hl7v2::HL7v2Commands,
97 },
98 Package {
101 #[command(subcommand)]
102 command: commands::package::PackageCommands,
103 },
104 Doc {
106 #[arg(short, long)]
108 output: String,
109 },
110}
111
112enum CliEnvironmentVariables {
113 SentryDSN,
114 OTELEndpoint,
115 OTELHeaders,
116 LogType,
117 ErrorBacktraces,
119}
120
121impl From<CliEnvironmentVariables> for String {
122 fn from(value: CliEnvironmentVariables) -> Self {
123 match value {
124 CliEnvironmentVariables::SentryDSN => "SENTRY_DSN".to_string(),
125 CliEnvironmentVariables::OTELEndpoint => "OTEL_ENDPOINT".to_string(),
126 CliEnvironmentVariables::OTELHeaders => "OTEL_HEADERS".to_string(),
127 CliEnvironmentVariables::LogType => "LOG_TYPE".to_string(),
128 CliEnvironmentVariables::ErrorBacktraces => "ERROR_BACKTRACES".to_string(),
129 }
130 }
131}
132
133struct OtelGuard {
134 _tracer_provider: SdkTracerProvider,
135 _logger_provider: SdkLoggerProvider,
136}
137
138fn otel_guard(config: &dyn Config<CliEnvironmentVariables>) -> Option<OtelGuard> {
139 let endpoint = config.get(CliEnvironmentVariables::OTELEndpoint).ok()?;
140 let headers_str = config
141 .get(CliEnvironmentVariables::OTELHeaders)
142 .unwrap_or_default();
143 let headers = headers_str
144 .split(',')
145 .filter_map(|pair| {
146 let mut parts = pair.splitn(2, '=');
147 if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
148 Some((key.trim().to_string(), value.trim().to_string()))
149 } else {
150 None
151 }
152 })
153 .collect::<HashMap<String, String>>();
154
155 let root_otel_endpoint = Url::parse(&endpoint).expect("Invalid OTLP endpoint URL");
156
157 let mut trace_endpoint = root_otel_endpoint.clone();
160 trace_endpoint
161 .path_segments_mut()
162 .expect("OTEL endpoint cannot be a base URL")
163 .extend(&["v1", "traces"]);
164
165 let mut log_endpoint = root_otel_endpoint.clone();
166 log_endpoint
167 .path_segments_mut()
168 .expect("OTEL endpoint cannot be a base URL")
169 .extend(&["v1", "logs"]);
170
171 let oltp_span_exporter = opentelemetry_otlp::SpanExporter::builder()
172 .with_http()
173 .with_protocol(Protocol::HttpBinary)
174 .with_endpoint(trace_endpoint.as_str())
175 .with_headers(headers.clone())
176 .with_timeout(Duration::from_secs(5))
177 .build()
178 .expect("Failed to create OpenTelemetry span exporter");
179
180 let oltp_log_exporter = opentelemetry_otlp::LogExporter::builder()
181 .with_http()
182 .with_protocol(Protocol::HttpBinary)
183 .with_endpoint(log_endpoint.as_str())
184 .with_headers(headers)
185 .with_timeout(Duration::from_secs(5))
186 .build()
187 .expect("Failed to create OpenTelemetry log exporter");
188
189 let resource = Resource::builder()
190 .with_attribute(KeyValue::new("service.name", "haste-health"))
191 .build();
192
193 let tracer_provider = SdkTracerProvider::builder()
194 .with_resource(resource.clone())
195 .with_batch_exporter(oltp_span_exporter)
196 .build();
197
198 let logger_provider = SdkLoggerProvider::builder()
199 .with_resource(resource)
200 .with_batch_exporter(oltp_log_exporter)
201 .build();
202
203 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
204
205 Some(OtelGuard {
206 _tracer_provider: tracer_provider,
207 _logger_provider: logger_provider,
208 })
209}
210
211fn inject_otel_subscriber<S>(
212 subscriber: S,
213 config: &dyn Config<CliEnvironmentVariables>,
214) -> Option<OtelGuard>
215where
216 S: tracing::Subscriber + Send + Sync + 'static,
217 for<'span> S: LookupSpan<'span>,
218{
219 if let Some(guard) = otel_guard(config) {
220 let tracer = guard._tracer_provider.tracer("haste-health");
221 let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
222 let otel_logs = OpenTelemetryTracingBridge::new(&guard._logger_provider);
223
224 tracing::subscriber::set_global_default(subscriber.with(telemetry).with(otel_logs))
225 .unwrap();
226
227 Some(guard)
228 } else {
229 tracing::subscriber::set_global_default(subscriber).unwrap();
230 None
231 }
232}
233
234fn setup_tracing(
235 config: &dyn Config<CliEnvironmentVariables>,
236) -> Result<Option<OtelGuard>, OperationOutcomeError> {
237 let log_type = config
238 .get(CliEnvironmentVariables::LogType)
239 .unwrap_or("JSON".to_string());
240
241 let sentry_layer = sentry::integrations::tracing::layer().event_filter(|metadata| {
245 if *metadata.level() == tracing::Level::ERROR {
246 sentry::integrations::tracing::EventFilter::Event
247 } else {
248 sentry::integrations::tracing::EventFilter::Ignore
249 }
250 });
251
252 let subscriber = Registry::default()
253 .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
254 .with(sentry_layer);
255
256 match log_type.as_str() {
257 "TREE" => Ok(inject_otel_subscriber(
258 subscriber.with(HierarchicalLayer::new(2)),
259 config,
260 )),
261 "JSON" => Ok(inject_otel_subscriber(
262 subscriber.with(tracing_subscriber::fmt::Layer::default().json()),
263 config,
264 )),
265 _ => Err(OperationOutcomeError::fatal(
266 IssueType::invalid(),
267 "Invalid log type specified in environment variable LOG_TYPE. Supported values are 'TREE' and 'JSON'.".to_string(),
268 )),
269 }
270}
271
272fn main() -> Result<(), OperationOutcomeError> {
273 let config = get_config(ConfigType::Environment);
274
275 let cli = Cli::parse();
276 let cli_state = cli::state::CLI_STATE.clone();
277
278 OperationOutcomeError::capture_backtraces(
279 config
280 .get(CliEnvironmentVariables::ErrorBacktraces)
281 .is_ok_and(|value| matches!(value.as_str(), "true" | "1")),
282 );
283
284 let sentry_location = config.get(CliEnvironmentVariables::SentryDSN);
285
286 let _guard = sentry::init((
287 sentry_location.unwrap_or_default(),
288 sentry::ClientOptions {
289 release: sentry::release_name!(),
290 send_default_pii: true,
293 ..Default::default()
294 },
295 ));
296
297 tokio::runtime::Builder::new_multi_thread()
298 .enable_all()
299 .thread_stack_size(1024 * 8000)
301 .build()
302 .unwrap()
303 .block_on(async {
304 let _otel_provider = setup_tracing(config.as_ref())?;
305 commands::run(cli_state, &cli.command).await
306 })
307}