1use std::{
2 collections::HashMap,
3 path::PathBuf,
4 sync::{Arc, LazyLock},
5 time::Duration,
6};
7
8use clap::{Parser, Subcommand};
9use haste_config::{Config, ConfigType, get_config};
10use haste_fhir_model::r4::generated::terminology::IssueType;
11use haste_fhir_operation_error::OperationOutcomeError;
12use haste_server::auth_n::oidc::routes::discovery::WellKnownDiscoveryDocument;
13use opentelemetry::KeyValue;
14use opentelemetry::trace::TracerProvider as _;
15use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
16use opentelemetry_otlp::WithExportConfig;
17use opentelemetry_otlp::{Protocol, WithHttpConfig};
18use opentelemetry_sdk::Resource;
19use opentelemetry_sdk::logs::SdkLoggerProvider;
20use opentelemetry_sdk::trace::SdkTracerProvider;
21use reqwest::Url;
22use tokio::sync::Mutex;
23use tracing_subscriber::registry::LookupSpan;
24use tracing_subscriber::{EnvFilter, layer::SubscriberExt, registry::Registry};
25use tracing_tree::HierarchicalLayer;
26
27use crate::commands::config::{CLIConfiguration, load_config};
28
29mod client;
30mod commands;
31
32#[derive(Parser)]
33#[command(version, about, long_about = None)] struct Cli {
35 #[command(subcommand)]
36 command: CLICommand,
37}
38
39#[derive(Subcommand)]
40enum CLICommand {
41 FHIRPath {
43 fhirpath: String,
45 },
46 Generate {
47 #[command(subcommand)]
49 command: commands::codegen::CodeGen,
50 },
51 Server {
52 #[command(subcommand)]
53 command: commands::server::ServerCommands,
54 },
55 Api {
56 #[command(subcommand)]
57 command: commands::api::ApiCommands,
58 },
59 Config {
60 #[command(subcommand)]
61 command: commands::config::ConfigCommands,
62 },
63 Worker {
64 #[command(subcommand)]
65 command: Option<commands::worker::WorkerCommands>,
66 },
67 Testscript {
68 #[command(subcommand)]
69 command: commands::testscript::TestScriptCommands,
70 },
71 Admin {
72 #[command(subcommand)]
73 command: commands::admin::AdminCommands,
74 },
75 Hl7v2 {
76 #[command(subcommand)]
77 command: commands::hl7v2::HL7v2Commands,
78 },
79}
80
81static CONFIG_LOCATION: LazyLock<PathBuf> = LazyLock::new(|| {
82 let config_dir = std::env::home_dir()
83 .unwrap_or_else(|| std::path::PathBuf::from("."))
84 .join(".haste_health");
85
86 std::fs::create_dir_all(&config_dir).expect("Failed to create config directory");
87
88 config_dir.join("config.toml")
89});
90
91pub struct CLIState {
92 config: CLIConfiguration,
93 access_token: Option<String>,
94 well_known_document: Option<WellKnownDiscoveryDocument>,
95}
96
97impl CLIState {
98 pub fn new(config: CLIConfiguration) -> Self {
99 CLIState {
100 config,
101 access_token: None,
102 well_known_document: None,
103 }
104 }
105}
106
107static CLI_STATE: LazyLock<Arc<Mutex<CLIState>>> = LazyLock::new(|| {
108 let config = load_config(&CONFIG_LOCATION);
109
110 Arc::new(Mutex::new(CLIState::new(config)))
111});
112
113enum CLIEnvironmentVariables {
114 SentryDSN,
115 OTELEndpoint,
116 OTELHeaders,
117 LogType,
118}
119
120impl From<CLIEnvironmentVariables> for String {
121 fn from(value: CLIEnvironmentVariables) -> Self {
122 match value {
123 CLIEnvironmentVariables::SentryDSN => "SENTRY_DSN".to_string(),
124 CLIEnvironmentVariables::OTELEndpoint => "OTEL_ENDPOINT".to_string(),
125 CLIEnvironmentVariables::OTELHeaders => "OTEL_HEADERS".to_string(),
126 CLIEnvironmentVariables::LogType => "LOG_TYPE".to_string(),
127 }
128 }
129}
130
131struct OtelGuard {
132 _tracer_provider: SdkTracerProvider,
133 _logger_provider: SdkLoggerProvider,
134}
135
136fn otel_guard(config: &dyn Config<CLIEnvironmentVariables>) -> Option<OtelGuard> {
137 let endpoint = config.get(CLIEnvironmentVariables::OTELEndpoint).ok()?;
138 let headers_str = config
139 .get(CLIEnvironmentVariables::OTELHeaders)
140 .unwrap_or_default();
141 let headers = headers_str
142 .split(',')
143 .filter_map(|pair| {
144 let mut parts = pair.splitn(2, '=');
145 if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
146 Some((key.trim().to_string(), value.trim().to_string()))
147 } else {
148 None
149 }
150 })
151 .collect::<HashMap<String, String>>();
152
153 let root_otel_endpoint = Url::parse(&endpoint).expect("Invalid OTLP endpoint URL");
154
155 let mut trace_endpoint = root_otel_endpoint.clone();
158 trace_endpoint
159 .path_segments_mut()
160 .expect("OTEL endpoint cannot be a base URL")
161 .extend(&["v1", "traces"]);
162
163 let mut log_endpoint = root_otel_endpoint.clone();
164 log_endpoint
165 .path_segments_mut()
166 .expect("OTEL endpoint cannot be a base URL")
167 .extend(&["v1", "logs"]);
168
169 let oltp_span_exporter = opentelemetry_otlp::SpanExporter::builder()
170 .with_http()
171 .with_protocol(Protocol::HttpBinary)
172 .with_endpoint(trace_endpoint.as_str())
173 .with_headers(headers.clone())
174 .with_timeout(Duration::from_secs(5))
175 .build()
176 .expect("Failed to create OpenTelemetry span exporter");
177
178 let oltp_log_exporter = opentelemetry_otlp::LogExporter::builder()
179 .with_http()
180 .with_protocol(Protocol::HttpBinary)
181 .with_endpoint(log_endpoint.as_str())
182 .with_headers(headers)
183 .with_timeout(Duration::from_secs(5))
184 .build()
185 .expect("Failed to create OpenTelemetry log exporter");
186
187 let resource = Resource::builder()
188 .with_attribute(KeyValue::new("service.name", "haste-health"))
189 .build();
190
191 let tracer_provider = SdkTracerProvider::builder()
192 .with_resource(resource.clone())
193 .with_batch_exporter(oltp_span_exporter)
194 .build();
195
196 let logger_provider = SdkLoggerProvider::builder()
197 .with_resource(resource)
198 .with_batch_exporter(oltp_log_exporter)
199 .build();
200
201 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
202
203 Some(OtelGuard {
204 _tracer_provider: tracer_provider,
205 _logger_provider: logger_provider,
206 })
207}
208
209fn inject_otel_subscriber<S>(
210 subscriber: S,
211 config: &dyn Config<CLIEnvironmentVariables>,
212) -> Option<OtelGuard>
213where
214 S: tracing::Subscriber + Send + Sync + 'static,
215 for<'span> S: LookupSpan<'span>,
216{
217 if let Some(guard) = otel_guard(config) {
218 let tracer = guard._tracer_provider.tracer("haste-health");
219 let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
220 let otel_logs = OpenTelemetryTracingBridge::new(&guard._logger_provider);
221
222 tracing::subscriber::set_global_default(subscriber.with(telemetry).with(otel_logs))
223 .unwrap();
224
225 Some(guard)
226 } else {
227 tracing::subscriber::set_global_default(subscriber).unwrap();
228 None
229 }
230}
231
232fn setup_tracing(
233 config: &dyn Config<CLIEnvironmentVariables>,
234) -> Result<Option<OtelGuard>, OperationOutcomeError> {
235 let log_type = config
236 .get(CLIEnvironmentVariables::LogType)
237 .unwrap_or("JSON".to_string());
238
239 let subscriber = Registry::default()
240 .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")));
241
242 match log_type.as_str() {
243 "TREE" => Ok(inject_otel_subscriber(
244 subscriber.with(HierarchicalLayer::new(2)),
245 config,
246 )),
247 "JSON" => Ok(inject_otel_subscriber(
248 subscriber.with(tracing_subscriber::fmt::Layer::default().json()),
249 config,
250 )),
251 _ => Err(OperationOutcomeError::fatal(
252 IssueType::Invalid(None),
253 "Invalid log type specified in environment variable LOG_TYPE. Supported values are 'TREE' and 'JSON'.".to_string(),
254 )),
255 }
256}
257
258fn main() -> Result<(), OperationOutcomeError> {
259 let config = get_config(ConfigType::Environment);
260
261 let cli = Cli::parse();
262 let cli_state = CLI_STATE.clone();
263
264 let sentry_location = config.get(CLIEnvironmentVariables::SentryDSN);
265
266 let _guard = sentry::init((
267 sentry_location.unwrap_or_default(),
268 sentry::ClientOptions {
269 release: sentry::release_name!(),
270 send_default_pii: true,
273 ..Default::default()
274 },
275 ));
276
277 tokio::runtime::Builder::new_multi_thread()
278 .enable_all()
279 .thread_stack_size(1024 * 8000)
281 .build()
282 .unwrap()
283 .block_on(async {
284 let _otel_provider = setup_tracing(config.as_ref())?;
285 match &cli.command {
286 CLICommand::FHIRPath { fhirpath } => commands::fhirpath::fhirpath(fhirpath).await,
287 CLICommand::Generate { command } => commands::codegen::codegen(command).await,
288 CLICommand::Server { command } => commands::server::server(command).await,
289 CLICommand::Worker { command } => commands::worker::worker(command).await,
290 CLICommand::Config { command } => {
291 commands::config::config(&cli_state, command).await
292 }
293 CLICommand::Api { command } => {
294 commands::api::api_commands(cli_state, command).await
295 }
296 CLICommand::Testscript { command } => {
297 commands::testscript::testscript_commands(cli_state, command).await
298 }
299 CLICommand::Admin { command } => commands::admin::admin(command).await,
300 CLICommand::Hl7v2 { command } => commands::hl7v2::hl7v2(cli_state, command).await,
301 }
302 })
303}