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+ }
0 commit comments