Skip to content

Commit 24a768a

Browse files
authored
Merge pull request #99 from SentriusLLC/copilot/fix-98
Add Jaeger OpenTelemetry UI with improved layout, pagination, and error handling
2 parents 20a3afd + ef71b6a commit 24a768a

File tree

10 files changed

+1130
-4
lines changed

10 files changed

+1130
-4
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package io.sentrius.sso.controllers.api;
2+
3+
import org.springframework.http.ResponseEntity;
4+
import org.springframework.web.bind.annotation.GetMapping;
5+
import org.springframework.web.bind.annotation.RequestMapping;
6+
import org.springframework.web.bind.annotation.RestController;
7+
8+
import java.time.Instant;
9+
import java.util.*;
10+
11+
/**
12+
* Mock Jaeger API for testing telemetry UI when actual Jaeger is not available.
13+
* This controller provides sample trace data for demonstration purposes.
14+
*/
15+
@RestController
16+
@RequestMapping("/mock/jaeger/api")
17+
public class MockJaegerApiController {
18+
19+
@GetMapping("/services")
20+
public ResponseEntity<?> getMockServices() {
21+
Map<String, Object> response = new HashMap<>();
22+
List<String> services = Arrays.asList(
23+
"sentrius-api",
24+
"sentrius-dataplane",
25+
"sentrius-agent-proxy",
26+
"sentrius-integration-proxy"
27+
);
28+
response.put("data", services);
29+
return ResponseEntity.ok(response);
30+
}
31+
32+
@GetMapping("/traces")
33+
public ResponseEntity<?> getMockTraces() {
34+
Map<String, Object> response = new HashMap<>();
35+
List<Map<String, Object>> traces = new ArrayList<>();
36+
37+
// Create sample trace 1
38+
Map<String, Object> trace1 = createSampleTrace(
39+
"1234567890abcdef",
40+
"sentrius-api",
41+
Arrays.asList("HTTP GET /sso/v1/dashboard", "Database Query", "Cache Lookup"),
42+
150000 // 150ms
43+
);
44+
45+
// Create sample trace 2
46+
Map<String, Object> trace2 = createSampleTrace(
47+
"fedcba0987654321",
48+
"sentrius-api",
49+
Arrays.asList("HTTP POST /api/v1/users", "User Validation", "Database Insert", "Send Notification"),
50+
320000 // 320ms
51+
);
52+
53+
traces.add(trace1);
54+
traces.add(trace2);
55+
56+
response.put("data", traces);
57+
return ResponseEntity.ok(response);
58+
}
59+
60+
private Map<String, Object> createSampleTrace(String traceId, String serviceName, List<String> operations, long totalDuration) {
61+
Map<String, Object> trace = new HashMap<>();
62+
trace.put("traceID", traceId);
63+
64+
List<Map<String, Object>> spans = new ArrayList<>();
65+
long startTime = Instant.now().toEpochMilli() * 1000; // Convert to microseconds
66+
long currentTime = startTime;
67+
68+
for (int i = 0; i < operations.size(); i++) {
69+
Map<String, Object> span = new HashMap<>();
70+
span.put("spanID", String.format("%016x", i + 1));
71+
span.put("operationName", operations.get(i));
72+
span.put("startTime", currentTime);
73+
74+
long duration = totalDuration / operations.size();
75+
span.put("duration", duration);
76+
77+
// Create process info
78+
Map<String, Object> process = new HashMap<>();
79+
process.put("serviceName", serviceName);
80+
span.put("process", process);
81+
82+
// Add references for child spans
83+
if (i > 0) {
84+
List<Map<String, Object>> references = new ArrayList<>();
85+
Map<String, Object> ref = new HashMap<>();
86+
ref.put("refType", "CHILD_OF");
87+
ref.put("spanID", String.format("%016x", i)); // Reference parent
88+
references.add(ref);
89+
span.put("references", references);
90+
} else {
91+
span.put("references", new ArrayList<>());
92+
}
93+
94+
spans.add(span);
95+
currentTime += duration;
96+
}
97+
98+
trace.put("spans", spans);
99+
return trace;
100+
}
101+
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package io.sentrius.sso.controllers.api;
2+
3+
import io.sentrius.sso.core.config.SystemOptions;
4+
import io.sentrius.sso.core.controllers.BaseController;
5+
import io.sentrius.sso.core.services.ErrorOutputService;
6+
import io.sentrius.sso.core.services.UserService;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.beans.factory.annotation.Value;
9+
import org.springframework.http.HttpEntity;
10+
import org.springframework.http.HttpHeaders;
11+
import org.springframework.http.HttpMethod;
12+
import org.springframework.http.ResponseEntity;
13+
import org.springframework.web.bind.annotation.*;
14+
import org.springframework.web.client.RestTemplate;
15+
16+
import java.util.*;
17+
18+
@Slf4j
19+
@RestController
20+
@RequestMapping("/api/v1/telemetry")
21+
public class TelemetryApiController extends BaseController {
22+
23+
private final RestTemplate restTemplate = new RestTemplate();
24+
25+
@Value("${jaeger.query.url:http://localhost:16686}")
26+
private String jaegerQueryUrl;
27+
28+
protected TelemetryApiController(
29+
UserService userService,
30+
SystemOptions systemOptions,
31+
ErrorOutputService errorOutputService
32+
) {
33+
super(userService, systemOptions, errorOutputService);
34+
}
35+
36+
@GetMapping("/traces")
37+
public ResponseEntity<?> getTraces(
38+
@RequestParam(required = false) String service,
39+
@RequestParam(required = false) String operation,
40+
@RequestParam(defaultValue = "1h") String lookback,
41+
@RequestParam(required = false) Long minDuration,
42+
@RequestParam(required = false) Long maxDuration,
43+
@RequestParam(required = false) String tags,
44+
@RequestParam(defaultValue = "20") int limit,
45+
@RequestParam(defaultValue = "0") int start
46+
) {
47+
try {
48+
String jaegerApiUrl = buildJaegerApiUrl(service, operation, lookback, minDuration, maxDuration, tags, limit, start);
49+
log.info("Querying Jaeger at: {}", jaegerApiUrl);
50+
51+
HttpHeaders headers = new HttpHeaders();
52+
headers.set("Accept", "application/json");
53+
HttpEntity<String> entity = new HttpEntity<>(headers);
54+
55+
ResponseEntity<Map> response = restTemplate.exchange(
56+
jaegerApiUrl,
57+
HttpMethod.GET,
58+
entity,
59+
Map.class
60+
);
61+
62+
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
63+
Map<String, Object> jaegerResponse = response.getBody();
64+
List<Map<String, Object>> processedTraces = processJaegerResponse(jaegerResponse);
65+
66+
Map<String, Object> result = new HashMap<>();
67+
result.put("traces", processedTraces);
68+
result.put("status", "success");
69+
result.put("count", processedTraces.size());
70+
result.put("limit", limit);
71+
result.put("start", start);
72+
result.put("hasMore", processedTraces.size() >= limit); // Indicate if there might be more data
73+
74+
return ResponseEntity.ok(result);
75+
} else {
76+
return ResponseEntity.status(response.getStatusCode())
77+
.body(Map.of("error", "Failed to query Jaeger", "status", "error"));
78+
}
79+
80+
} catch (Exception e) {
81+
log.error("Error querying Jaeger traces", e);
82+
return ResponseEntity.internalServerError()
83+
.body(Map.of("error", "Internal server error: " + e.getMessage(), "status", "error"));
84+
}
85+
}
86+
87+
@GetMapping("/services")
88+
public ResponseEntity<?> getServices() {
89+
try {
90+
String servicesUrl = jaegerQueryUrl + "/api/services";
91+
log.info("Fetching services from Jaeger at: {}", servicesUrl);
92+
93+
HttpHeaders headers = new HttpHeaders();
94+
headers.set("Accept", "application/json");
95+
HttpEntity<String> entity = new HttpEntity<>(headers);
96+
97+
ResponseEntity<Map> response = restTemplate.exchange(
98+
servicesUrl,
99+
HttpMethod.GET,
100+
entity,
101+
Map.class
102+
);
103+
104+
return ResponseEntity.ok(response.getBody());
105+
106+
} catch (Exception e) {
107+
log.error("Error fetching services from Jaeger", e);
108+
return ResponseEntity.internalServerError()
109+
.body(Map.of("error", "Failed to fetch services: " + e.getMessage(), "status", "error"));
110+
}
111+
}
112+
113+
@GetMapping("/trace/{traceId}")
114+
public ResponseEntity<?> getTrace(@PathVariable String traceId) {
115+
try {
116+
String traceUrl = jaegerQueryUrl + "/api/traces/" + traceId;
117+
log.info("Fetching trace from Jaeger at: {}", traceUrl);
118+
119+
HttpHeaders headers = new HttpHeaders();
120+
headers.set("Accept", "application/json");
121+
HttpEntity<String> entity = new HttpEntity<>(headers);
122+
123+
ResponseEntity<Map> response = restTemplate.exchange(
124+
traceUrl,
125+
HttpMethod.GET,
126+
entity,
127+
Map.class
128+
);
129+
130+
return ResponseEntity.ok(response.getBody());
131+
132+
} catch (Exception e) {
133+
log.error("Error fetching trace from Jaeger", e);
134+
return ResponseEntity.internalServerError()
135+
.body(Map.of("error", "Failed to fetch trace: " + e.getMessage(), "status", "error"));
136+
}
137+
}
138+
139+
private String buildJaegerApiUrl(String service, String operation, String lookback,
140+
Long minDuration, Long maxDuration, String tags, int limit, int start) {
141+
StringBuilder url = new StringBuilder(jaegerQueryUrl + "/api/traces?");
142+
143+
// Default to sentrius-api if no service is provided to prevent 500 errors
144+
if (service == null || service.isEmpty()) {
145+
service = "sentrius-api";
146+
}
147+
url.append("service=").append(service).append("&");
148+
149+
if (operation != null && !operation.isEmpty()) {
150+
url.append("operation=").append(operation).append("&");
151+
}
152+
153+
url.append("lookback=").append(lookback).append("&");
154+
155+
if (minDuration != null) {
156+
url.append("minDuration=").append(minDuration).append("us&");
157+
}
158+
159+
if (maxDuration != null) {
160+
url.append("maxDuration=").append(maxDuration).append("us&");
161+
}
162+
163+
if (tags != null && !tags.isEmpty()) {
164+
url.append("tags=").append(tags).append("&");
165+
}
166+
167+
// Add pagination parameters
168+
url.append("limit=").append(Math.min(limit, 100)).append("&"); // Cap at 100
169+
url.append("start=").append(start);
170+
171+
return url.toString();
172+
}
173+
174+
private List<Map<String, Object>> processJaegerResponse(Map<String, Object> jaegerResponse) {
175+
List<Map<String, Object>> processedTraces = new ArrayList<>();
176+
177+
try {
178+
Object dataObj = jaegerResponse.get("data");
179+
if (dataObj instanceof List) {
180+
List<Map<String, Object>> traces = (List<Map<String, Object>>) dataObj;
181+
182+
for (Map<String, Object> trace : traces) {
183+
Map<String, Object> processedTrace = new HashMap<>();
184+
processedTrace.put("traceID", trace.get("traceID"));
185+
186+
// Calculate duration and other metrics
187+
Object spansObj = trace.get("spans");
188+
if (spansObj instanceof List) {
189+
List<Map<String, Object>> spans = (List<Map<String, Object>>) spansObj;
190+
processedTrace.put("spans", spans);
191+
processedTrace.put("spanCount", spans.size());
192+
193+
// Find root span for start time and total duration
194+
Optional<Map<String, Object>> rootSpan = spans.stream()
195+
.filter(span -> {
196+
Object refs = span.get("references");
197+
return refs == null || (refs instanceof List && ((List<?>) refs).isEmpty());
198+
})
199+
.findFirst();
200+
201+
if (rootSpan.isPresent()) {
202+
processedTrace.put("startTime", rootSpan.get().get("startTime"));
203+
processedTrace.put("duration", rootSpan.get().get("duration"));
204+
}
205+
}
206+
207+
processedTraces.add(processedTrace);
208+
}
209+
}
210+
} catch (Exception e) {
211+
log.warn("Error processing Jaeger response", e);
212+
}
213+
214+
return processedTraces;
215+
}
216+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.sentrius.sso.controllers.view;
2+
3+
import io.sentrius.sso.core.config.SystemOptions;
4+
import io.sentrius.sso.core.controllers.BaseController;
5+
import io.sentrius.sso.core.services.ErrorOutputService;
6+
import io.sentrius.sso.core.services.UserService;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.stereotype.Controller;
9+
import org.springframework.web.bind.annotation.GetMapping;
10+
import org.springframework.web.bind.annotation.RequestMapping;
11+
12+
@Slf4j
13+
@Controller
14+
@RequestMapping("/sso")
15+
public class TelemetryController extends BaseController {
16+
17+
protected TelemetryController(
18+
UserService userService,
19+
SystemOptions systemOptions,
20+
ErrorOutputService errorOutputService
21+
) {
22+
super(userService, systemOptions, errorOutputService);
23+
}
24+
25+
@GetMapping("/v1/telemetry")
26+
public String telemetry() {
27+
return "sso/telemetry";
28+
}
29+
}

api/src/main/resources/application.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ otel.resource.attributes.service.name=sentrius-api
8989
otel.traces.sampler=always_on
9090
otel.exporter.otlp.timeout=10s
9191

92+
# Jaeger Query API URL
93+
jaeger.query.url=${JAEGER_QUERY_URL:http://localhost:16686}
94+
9295
sentrius.agent.register.bootstrap.allow=true
9396
sentrius.agent.bootstrap.policy=default-policy.yaml
9497
# Optional: set the identity lifetime

api/src/main/resources/templates/fragments/sidebar.html

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,15 @@
5555
<i class="fa-solid fa-user"></i> <span class="ms-1 d-none d-sm-inline">Manage Agent/Users</span>
5656
</a>
5757
</li>
58-
<li th:if="${#sets.contains(operatingUser.authorizationType.accessSet, 'CAN_MANAGE_APPLICATION')}">
59-
<a href="/sso/v1/atpl/" class="nav-link px-0 align-middle">
60-
<i class="fas fa-shield-alt"></i> <span class="ms-1 d-none d-sm-inline">Trust Policies</span>
61-
</a>
58+
<li th:if="${#sets.contains(operatingUser.authorizationType.accessSet, 'CAN_MANAGE_APPLICATION')}">
59+
<a href="/sso/v1/atpl/" class="nav-link px-0 align-middle">
60+
<i class="fas fa-shield-alt"></i> <span class="ms-1 d-none d-sm-inline">Trust Policies</span>
61+
</a>
62+
</li>
63+
<li th:if="${#sets.contains(operatingUser.authorizationType.accessSet, 'CAN_MANAGE_APPLICATION')}">
64+
<a href="/sso/v1/telemetry" class="nav-link px-0 align-middle">
65+
<i class="fas fa-chart-line"></i> <span class="ms-1 d-none d-sm-inline">Telemetry</span>
66+
</a>
6267
</li>
6368
</ul>
6469
<hr>

0 commit comments

Comments
 (0)