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)] pub 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 Doc {
80 #[arg(short, long)]
82 output: String,
83 },
84}
85
86static CONFIG_LOCATION: LazyLock<PathBuf> = LazyLock::new(|| {
87 let config_dir = std::env::home_dir()
88 .unwrap_or_else(|| std::path::PathBuf::from("."))
89 .join(".haste_health");
90
91 std::fs::create_dir_all(&config_dir).expect("Failed to create config directory");
92
93 config_dir.join("config.toml")
94});
95
96struct CLIState {
97 config: CLIConfiguration,
98 access_token: Option<String>,
99 well_known_document: Option<WellKnownDiscoveryDocument>,
100}
101
102impl CLIState {
103 fn new(config: CLIConfiguration) -> Self {
104 CLIState {
105 config,
106 access_token: None,
107 well_known_document: None,
108 }
109 }
110}
111
112static CLI_STATE: LazyLock<Arc<Mutex<CLIState>>> = LazyLock::new(|| {
113 let config = load_config(&CONFIG_LOCATION);
114
115 Arc::new(Mutex::new(CLIState::new(config)))
116});
117
118enum CLIEnvironmentVariables {
119 SentryDSN,
120 OTELEndpoint,
121 OTELHeaders,
122 LogType,
123}
124
125impl From<CLIEnvironmentVariables> for String {
126 fn from(value: CLIEnvironmentVariables) -> Self {
127 match value {
128 CLIEnvironmentVariables::SentryDSN => "SENTRY_DSN".to_string(),
129 CLIEnvironmentVariables::OTELEndpoint => "OTEL_ENDPOINT".to_string(),
130 CLIEnvironmentVariables::OTELHeaders => "OTEL_HEADERS".to_string(),
131 CLIEnvironmentVariables::LogType => "LOG_TYPE".to_string(),
132 }
133 }
134}
135
136struct OtelGuard {
137 _tracer_provider: SdkTracerProvider,
138 _logger_provider: SdkLoggerProvider,
139}
140
141fn otel_guard(config: &dyn Config<CLIEnvironmentVariables>) -> Option<OtelGuard> {
142 let endpoint = config.get(CLIEnvironmentVariables::OTELEndpoint).ok()?;
143 let headers_str = config
144 .get(CLIEnvironmentVariables::OTELHeaders)
145 .unwrap_or_default();
146 let headers = headers_str
147 .split(',')
148 .filter_map(|pair| {
149 let mut parts = pair.splitn(2, '=');
150 if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
151 Some((key.trim().to_string(), value.trim().to_string()))
152 } else {
153 None
154 }
155 })
156 .collect::<HashMap<String, String>>();
157
158 let root_otel_endpoint = Url::parse(&endpoint).expect("Invalid OTLP endpoint URL");
159
160 let mut trace_endpoint = root_otel_endpoint.clone();
163 trace_endpoint
164 .path_segments_mut()
165 .expect("OTEL endpoint cannot be a base URL")
166 .extend(&["v1", "traces"]);
167
168 let mut log_endpoint = root_otel_endpoint.clone();
169 log_endpoint
170 .path_segments_mut()
171 .expect("OTEL endpoint cannot be a base URL")
172 .extend(&["v1", "logs"]);
173
174 let oltp_span_exporter = opentelemetry_otlp::SpanExporter::builder()
175 .with_http()
176 .with_protocol(Protocol::HttpBinary)
177 .with_endpoint(trace_endpoint.as_str())
178 .with_headers(headers.clone())
179 .with_timeout(Duration::from_secs(5))
180 .build()
181 .expect("Failed to create OpenTelemetry span exporter");
182
183 let oltp_log_exporter = opentelemetry_otlp::LogExporter::builder()
184 .with_http()
185 .with_protocol(Protocol::HttpBinary)
186 .with_endpoint(log_endpoint.as_str())
187 .with_headers(headers)
188 .with_timeout(Duration::from_secs(5))
189 .build()
190 .expect("Failed to create OpenTelemetry log exporter");
191
192 let resource = Resource::builder()
193 .with_attribute(KeyValue::new("service.name", "haste-health"))
194 .build();
195
196 let tracer_provider = SdkTracerProvider::builder()
197 .with_resource(resource.clone())
198 .with_batch_exporter(oltp_span_exporter)
199 .build();
200
201 let logger_provider = SdkLoggerProvider::builder()
202 .with_resource(resource)
203 .with_batch_exporter(oltp_log_exporter)
204 .build();
205
206 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
207
208 Some(OtelGuard {
209 _tracer_provider: tracer_provider,
210 _logger_provider: logger_provider,
211 })
212}
213
214fn inject_otel_subscriber<S>(
215 subscriber: S,
216 config: &dyn Config<CLIEnvironmentVariables>,
217) -> Option<OtelGuard>
218where
219 S: tracing::Subscriber + Send + Sync + 'static,
220 for<'span> S: LookupSpan<'span>,
221{
222 if let Some(guard) = otel_guard(config) {
223 let tracer = guard._tracer_provider.tracer("haste-health");
224 let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
225 let otel_logs = OpenTelemetryTracingBridge::new(&guard._logger_provider);
226
227 tracing::subscriber::set_global_default(subscriber.with(telemetry).with(otel_logs))
228 .unwrap();
229
230 Some(guard)
231 } else {
232 tracing::subscriber::set_global_default(subscriber).unwrap();
233 None
234 }
235}
236
237fn setup_tracing(
238 config: &dyn Config<CLIEnvironmentVariables>,
239) -> Result<Option<OtelGuard>, OperationOutcomeError> {
240 let log_type = config
241 .get(CLIEnvironmentVariables::LogType)
242 .unwrap_or("JSON".to_string());
243
244 let subscriber = Registry::default()
245 .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")));
246
247 match log_type.as_str() {
248 "TREE" => Ok(inject_otel_subscriber(
249 subscriber.with(HierarchicalLayer::new(2)),
250 config,
251 )),
252 "JSON" => Ok(inject_otel_subscriber(
253 subscriber.with(tracing_subscriber::fmt::Layer::default().json()),
254 config,
255 )),
256 _ => Err(OperationOutcomeError::fatal(
257 IssueType::invalid(),
258 "Invalid log type specified in environment variable LOG_TYPE. Supported values are 'TREE' and 'JSON'.".to_string(),
259 )),
260 }
261}
262
263fn main() -> Result<(), OperationOutcomeError> {
264 let config = get_config(ConfigType::Environment);
265
266 let cli = Cli::parse();
267 let cli_state = CLI_STATE.clone();
268
269 let sentry_location = config.get(CLIEnvironmentVariables::SentryDSN);
270
271 let _guard = sentry::init((
272 sentry_location.unwrap_or_default(),
273 sentry::ClientOptions {
274 release: sentry::release_name!(),
275 send_default_pii: true,
278 ..Default::default()
279 },
280 ));
281
282 tokio::runtime::Builder::new_multi_thread()
283 .enable_all()
284 .thread_stack_size(1024 * 8000)
286 .build()
287 .unwrap()
288 .block_on(async {
289 let _otel_provider = setup_tracing(config.as_ref())?;
290 match &cli.command {
291 CLICommand::Doc { output } => commands::doc::generate_cli_markdown(output).await,
292 CLICommand::FHIRPath { fhirpath } => commands::fhirpath::fhirpath(fhirpath).await,
293 CLICommand::Generate { command } => commands::codegen::codegen(command).await,
294 CLICommand::Server { command } => commands::server::server(command).await,
295 CLICommand::Worker { command } => commands::worker::worker(command).await,
296 CLICommand::Config { command } => {
297 commands::config::config(&cli_state, command).await
298 }
299 CLICommand::Api { command } => {
300 commands::api::api_commands(cli_state, command).await
301 }
302 CLICommand::Testscript { command } => {
303 commands::testscript::testscript_commands(cli_state, command).await
304 }
305 CLICommand::Admin { command } => commands::admin::admin(command).await,
306 CLICommand::Hl7v2 { command } => commands::hl7v2::hl7v2(cli_state, command).await,
307 }
308 })
309}