otlp_embedded/jaeger/
ui.rs1use std::cmp::Reverse;
2
3use axum::{
4 Extension, Json, Router,
5 extract::{Path, Query},
6 http::{StatusCode, Uri, header},
7 response::{Html, IntoResponse, Response},
8 routing::get,
9};
10use itertools::Itertools;
11use rust_embed::RustEmbed;
12use serde::Deserialize;
13use serde_json::json;
14
15use crate::StateRef;
16
17pub fn app(state: StateRef, base_path: &str) -> Router {
29 if !base_path.starts_with('/') || !base_path.ends_with('/') {
30 panic!("base_path must start and end with /");
31 }
32 let base_tag = format!(r#"<base href="{base_path}""#);
33
34 let api = Router::new()
35 .route("/traces/{hex_id}", get(trace))
36 .route("/services", get(services))
37 .route("/services/{service}/operations", get(operations))
38 .route("/traces", get(traces))
39 .layer(Extension(state))
40 .fallback(|_: Uri| async move { not_found_with_msg("API not supported") });
41
42 Router::new()
43 .nest("/api/", api)
44 .fallback(|uri| async move { static_handler(uri, &base_tag).await })
45}
46
47async fn trace(
48 Path(hex_id): Path<String>,
49 Extension(state): Extension<StateRef>,
50) -> impl IntoResponse {
51 let id = hex::decode(&hex_id).unwrap_or_default();
52 let trace = state.write().await.get_by_id(&id);
53
54 if let Some(trace) = trace {
55 Json(trace.to_jaeger_batch()).into_response()
56 } else {
57 not_found_with_msg(format!("Trace {hex_id} not found, maybe expired."))
58 }
59}
60
61async fn services(Extension(state): Extension<StateRef>) -> impl IntoResponse {
62 let state = state.read().await;
63 let all_services = state.get_all_services();
64 let len = all_services.len();
65
66 let res = json!({
67 "data": all_services,
68 "total": len,
69 });
70
71 Json(res).into_response()
72}
73
74async fn operations(
75 Path(service): Path<String>,
76 Extension(state): Extension<StateRef>,
77) -> impl IntoResponse {
78 let state = state.read().await;
79 let operations = state.get_operations(&service);
80 let len = operations.len();
81
82 let res = json!({
83 "data": operations,
84 "total": len,
85 });
86
87 Json(res).into_response()
88}
89
90#[derive(Deserialize)]
91struct TracesQuery {
92 service: Option<String>,
93 operation: Option<String>,
94 limit: usize,
95}
96
97async fn traces(
98 Query(TracesQuery {
99 service,
100 operation,
101 limit,
102 }): Query<TracesQuery>,
103 Extension(state): Extension<StateRef>,
104) -> impl IntoResponse {
105 let traces = (state.read().await)
106 .get_all_complete()
107 .filter(|t| {
108 if let Some(service) = &service {
109 t.service_name().unwrap() == service
110 } else {
111 true
112 }
113 })
114 .filter(|t| {
115 if let Some(operation) = &operation {
116 t.operation().unwrap() == operation
117 } else {
118 true
119 }
120 })
121 .sorted_by_cached_key(|t| Reverse(t.end_time))
122 .map(|t| t.to_jaeger())
123 .take(limit)
124 .collect_vec();
125
126 let mock = json!({
127 "data": traces,
128 "total": traces.len(),
129 });
130
131 Json(mock).into_response()
132}
133
134const INDEX_HTML: &str = "index.html";
135
136#[derive(RustEmbed)]
137#[folder = "jaeger-ui/build"]
138struct Assets;
139
140async fn static_handler(uri: Uri, base_tag: &str) -> Response {
141 let path = uri.path().trim_start_matches('/');
142
143 if path == INDEX_HTML {
144 return index_html(base_tag);
145 }
146
147 match Assets::get(path) {
148 Some(file) => {
149 let mime = file.metadata.mimetype();
150
151 let mut res = file.data.into_response();
152 res.headers_mut()
153 .insert(header::CONTENT_TYPE, mime.parse().unwrap());
154 res
155 }
156
157 None => {
158 if path.starts_with("static") {
159 not_found()
161 } else {
162 (StatusCode::TEMPORARY_REDIRECT, index_html(base_tag)).into_response()
166 }
167 }
168 }
169}
170
171fn index_html(base_tag: &str) -> Response {
172 let file = Assets::get(INDEX_HTML).unwrap();
173 let data = std::str::from_utf8(&file.data)
174 .unwrap()
175 .replace(r#"<base href="/""#, base_tag);
176
177 Html(data).into_response()
178}
179
180fn not_found() -> Response {
181 not_found_with_msg("Not Found")
182}
183
184fn not_found_with_msg(msg: impl Into<String>) -> Response {
185 (StatusCode::NOT_FOUND, msg.into()).into_response()
186}