Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package io.sentrius.sso.controllers.api;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.Instant;
import java.util.*;

/**
* Mock Jaeger API for testing telemetry UI when actual Jaeger is not available.
* This controller provides sample trace data for demonstration purposes.
*/
@RestController
@RequestMapping("/mock/jaeger/api")
public class MockJaegerApiController {

@GetMapping("/services")
public ResponseEntity<?> getMockServices() {
Map<String, Object> response = new HashMap<>();
List<String> services = Arrays.asList(
"sentrius-api",
"sentrius-dataplane",
"sentrius-agent-proxy",
"sentrius-integration-proxy"
);
response.put("data", services);
return ResponseEntity.ok(response);
}

@GetMapping("/traces")
public ResponseEntity<?> getMockTraces() {
Map<String, Object> response = new HashMap<>();
List<Map<String, Object>> traces = new ArrayList<>();

// Create sample trace 1
Map<String, Object> trace1 = createSampleTrace(
"1234567890abcdef",
"sentrius-api",
Arrays.asList("HTTP GET /sso/v1/dashboard", "Database Query", "Cache Lookup"),
150000 // 150ms
);

// Create sample trace 2
Map<String, Object> trace2 = createSampleTrace(
"fedcba0987654321",
"sentrius-api",
Arrays.asList("HTTP POST /api/v1/users", "User Validation", "Database Insert", "Send Notification"),
320000 // 320ms
);

traces.add(trace1);
traces.add(trace2);

response.put("data", traces);
return ResponseEntity.ok(response);
}

private Map<String, Object> createSampleTrace(String traceId, String serviceName, List<String> operations, long totalDuration) {
Map<String, Object> trace = new HashMap<>();
trace.put("traceID", traceId);

List<Map<String, Object>> spans = new ArrayList<>();
long startTime = Instant.now().toEpochMilli() * 1000; // Convert to microseconds
long currentTime = startTime;

for (int i = 0; i < operations.size(); i++) {
Map<String, Object> span = new HashMap<>();
span.put("spanID", String.format("%016x", i + 1));
span.put("operationName", operations.get(i));
span.put("startTime", currentTime);

long duration = totalDuration / operations.size();
span.put("duration", duration);

// Create process info
Map<String, Object> process = new HashMap<>();
process.put("serviceName", serviceName);
span.put("process", process);

// Add references for child spans
if (i > 0) {
List<Map<String, Object>> references = new ArrayList<>();
Map<String, Object> ref = new HashMap<>();
ref.put("refType", "CHILD_OF");
ref.put("spanID", String.format("%016x", i)); // Reference parent
references.add(ref);
span.put("references", references);
} else {
span.put("references", new ArrayList<>());
}

spans.add(span);
currentTime += duration;
}

trace.put("spans", spans);
return trace;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package io.sentrius.sso.controllers.api;

import io.sentrius.sso.core.config.SystemOptions;
import io.sentrius.sso.core.controllers.BaseController;
import io.sentrius.sso.core.services.ErrorOutputService;
import io.sentrius.sso.core.services.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import java.util.*;

@Slf4j
@RestController
@RequestMapping("/api/v1/telemetry")
public class TelemetryApiController extends BaseController {

private final RestTemplate restTemplate = new RestTemplate();

@Value("${jaeger.query.url:http://localhost:16686}")
private String jaegerQueryUrl;

protected TelemetryApiController(
UserService userService,
SystemOptions systemOptions,
ErrorOutputService errorOutputService
) {
super(userService, systemOptions, errorOutputService);
}

@GetMapping("/traces")
public ResponseEntity<?> getTraces(
@RequestParam(required = false) String service,
@RequestParam(required = false) String operation,
@RequestParam(defaultValue = "1h") String lookback,
@RequestParam(required = false) Long minDuration,
@RequestParam(required = false) Long maxDuration,
@RequestParam(required = false) String tags,
@RequestParam(defaultValue = "20") int limit,
@RequestParam(defaultValue = "0") int start
) {
try {
String jaegerApiUrl = buildJaegerApiUrl(service, operation, lookback, minDuration, maxDuration, tags, limit, start);
log.info("Querying Jaeger at: {}", jaegerApiUrl);

HttpHeaders headers = new HttpHeaders();
headers.set("Accept", "application/json");
HttpEntity<String> entity = new HttpEntity<>(headers);

ResponseEntity<Map> response = restTemplate.exchange(
jaegerApiUrl,
HttpMethod.GET,
entity,
Map.class
);

if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
Map<String, Object> jaegerResponse = response.getBody();
List<Map<String, Object>> processedTraces = processJaegerResponse(jaegerResponse);

Map<String, Object> result = new HashMap<>();
result.put("traces", processedTraces);
result.put("status", "success");
result.put("count", processedTraces.size());
result.put("limit", limit);
result.put("start", start);
result.put("hasMore", processedTraces.size() >= limit); // Indicate if there might be more data

return ResponseEntity.ok(result);
} else {
return ResponseEntity.status(response.getStatusCode())
.body(Map.of("error", "Failed to query Jaeger", "status", "error"));
}

} catch (Exception e) {
log.error("Error querying Jaeger traces", e);
return ResponseEntity.internalServerError()
.body(Map.of("error", "Internal server error: " + e.getMessage(), "status", "error"));
}
}

@GetMapping("/services")
public ResponseEntity<?> getServices() {
try {
String servicesUrl = jaegerQueryUrl + "/api/services";
log.info("Fetching services from Jaeger at: {}", servicesUrl);

HttpHeaders headers = new HttpHeaders();
headers.set("Accept", "application/json");
HttpEntity<String> entity = new HttpEntity<>(headers);

ResponseEntity<Map> response = restTemplate.exchange(
servicesUrl,
HttpMethod.GET,
entity,
Map.class
);

return ResponseEntity.ok(response.getBody());

} catch (Exception e) {
log.error("Error fetching services from Jaeger", e);
return ResponseEntity.internalServerError()
.body(Map.of("error", "Failed to fetch services: " + e.getMessage(), "status", "error"));
}
}

@GetMapping("/trace/{traceId}")
public ResponseEntity<?> getTrace(@PathVariable String traceId) {
try {
String traceUrl = jaegerQueryUrl + "/api/traces/" + traceId;
log.info("Fetching trace from Jaeger at: {}", traceUrl);

HttpHeaders headers = new HttpHeaders();
headers.set("Accept", "application/json");
HttpEntity<String> entity = new HttpEntity<>(headers);

ResponseEntity<Map> response = restTemplate.exchange(
traceUrl,
HttpMethod.GET,
entity,
Map.class
);

return ResponseEntity.ok(response.getBody());

} catch (Exception e) {
log.error("Error fetching trace from Jaeger", e);
return ResponseEntity.internalServerError()
.body(Map.of("error", "Failed to fetch trace: " + e.getMessage(), "status", "error"));
}
}

private String buildJaegerApiUrl(String service, String operation, String lookback,
Long minDuration, Long maxDuration, String tags, int limit, int start) {
StringBuilder url = new StringBuilder(jaegerQueryUrl + "/api/traces?");

// Default to sentrius-api if no service is provided to prevent 500 errors
if (service == null || service.isEmpty()) {
service = "sentrius-api";
}
url.append("service=").append(service).append("&");

if (operation != null && !operation.isEmpty()) {
url.append("operation=").append(operation).append("&");
}

url.append("lookback=").append(lookback).append("&");

if (minDuration != null) {
url.append("minDuration=").append(minDuration).append("us&");
}

if (maxDuration != null) {
url.append("maxDuration=").append(maxDuration).append("us&");
}

if (tags != null && !tags.isEmpty()) {
url.append("tags=").append(tags).append("&");
}

// Add pagination parameters
url.append("limit=").append(Math.min(limit, 100)).append("&"); // Cap at 100
url.append("start=").append(start);

return url.toString();
}

private List<Map<String, Object>> processJaegerResponse(Map<String, Object> jaegerResponse) {
List<Map<String, Object>> processedTraces = new ArrayList<>();

try {
Object dataObj = jaegerResponse.get("data");
if (dataObj instanceof List) {
List<Map<String, Object>> traces = (List<Map<String, Object>>) dataObj;

for (Map<String, Object> trace : traces) {
Map<String, Object> processedTrace = new HashMap<>();
processedTrace.put("traceID", trace.get("traceID"));

// Calculate duration and other metrics
Object spansObj = trace.get("spans");
if (spansObj instanceof List) {
List<Map<String, Object>> spans = (List<Map<String, Object>>) spansObj;
processedTrace.put("spans", spans);
processedTrace.put("spanCount", spans.size());

// Find root span for start time and total duration
Optional<Map<String, Object>> rootSpan = spans.stream()
.filter(span -> {
Object refs = span.get("references");
return refs == null || (refs instanceof List && ((List<?>) refs).isEmpty());
})
.findFirst();

if (rootSpan.isPresent()) {
processedTrace.put("startTime", rootSpan.get().get("startTime"));
processedTrace.put("duration", rootSpan.get().get("duration"));
}
}

processedTraces.add(processedTrace);
}
}
} catch (Exception e) {
log.warn("Error processing Jaeger response", e);
}

return processedTraces;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.sentrius.sso.controllers.view;

import io.sentrius.sso.core.config.SystemOptions;
import io.sentrius.sso.core.controllers.BaseController;
import io.sentrius.sso.core.services.ErrorOutputService;
import io.sentrius.sso.core.services.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Slf4j
@Controller
@RequestMapping("/sso")
public class TelemetryController extends BaseController {

protected TelemetryController(
UserService userService,
SystemOptions systemOptions,
ErrorOutputService errorOutputService
) {
super(userService, systemOptions, errorOutputService);
}

@GetMapping("/v1/telemetry")
public String telemetry() {
return "sso/telemetry";
}
}
3 changes: 3 additions & 0 deletions api/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ otel.resource.attributes.service.name=sentrius-api
otel.traces.sampler=always_on
otel.exporter.otlp.timeout=10s

# Jaeger Query API URL
jaeger.query.url=${JAEGER_QUERY_URL:http://localhost:16686}

sentrius.agent.register.bootstrap.allow=true
sentrius.agent.bootstrap.policy=default-policy.yaml
# Optional: set the identity lifetime
Expand Down
13 changes: 9 additions & 4 deletions api/src/main/resources/templates/fragments/sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,15 @@
<i class="fa-solid fa-user"></i> <span class="ms-1 d-none d-sm-inline">Manage Agent/Users</span>
</a>
</li>
<li th:if="${#sets.contains(operatingUser.authorizationType.accessSet, 'CAN_MANAGE_APPLICATION')}">
<a href="/sso/v1/atpl/" class="nav-link px-0 align-middle">
<i class="fas fa-shield-alt"></i> <span class="ms-1 d-none d-sm-inline">Trust Policies</span>
</a>
<li th:if="${#sets.contains(operatingUser.authorizationType.accessSet, 'CAN_MANAGE_APPLICATION')}">
<a href="/sso/v1/atpl/" class="nav-link px-0 align-middle">
<i class="fas fa-shield-alt"></i> <span class="ms-1 d-none d-sm-inline">Trust Policies</span>
</a>
</li>
<li th:if="${#sets.contains(operatingUser.authorizationType.accessSet, 'CAN_MANAGE_APPLICATION')}">
<a href="/sso/v1/telemetry" class="nav-link px-0 align-middle">
<i class="fas fa-chart-line"></i> <span class="ms-1 d-none d-sm-inline">Telemetry</span>
</a>
</li>
</ul>
<hr>
Expand Down
Loading