1+ use crate :: datamodel:: SensAppDateTime ;
2+ use crate :: datamodel:: sensapp_datetime:: SensAppDateTimeExt ;
3+ use crate :: parsing:: prometheus:: chunk_encoder:: ChunkEncoder ;
4+ use crate :: parsing:: prometheus:: converter:: { build_prometheus_labels, sensor_data_to_timeseries} ;
5+ use crate :: parsing:: prometheus:: remote_read_models:: {
6+ QueryResult , ReadResponse , read_request:: ResponseType ,
7+ } ;
18use crate :: parsing:: prometheus:: remote_read_parser:: {
2- create_empty_read_response , parse_remote_read_request, serialize_read_response,
9+ parse_remote_read_request, serialize_read_response,
310} ;
11+ use crate :: parsing:: prometheus:: remote_write_models:: Sample as PromSample ;
12+ use crate :: parsing:: prometheus:: stream_writer:: StreamWriter ;
13+ use crate :: storage:: query:: LabelMatcher ;
414
515use super :: { app_error:: AppError , state:: HttpServerState } ;
616use axum:: {
@@ -10,7 +20,7 @@ use axum::{
1020 response:: Response ,
1121} ;
1222use tokio_util:: bytes:: Bytes ;
13- use tracing:: { debug, info, warn } ;
23+ use tracing:: { debug, info} ;
1424
1525fn verify_read_headers ( headers : & HeaderMap ) -> Result < ( ) , AppError > {
1626 // Check that we have the right content encoding, that must be snappy
@@ -93,7 +103,7 @@ fn verify_read_headers(headers: &HeaderMap) -> Result<(), AppError> {
93103) ]
94104#[ debug_handler]
95105pub async fn prometheus_remote_read (
96- State ( _state ) : State < HttpServerState > ,
106+ State ( state ) : State < HttpServerState > ,
97107 headers : HeaderMap ,
98108 bytes : Bytes ,
99109) -> Result < Response < axum:: body:: Body > , AppError > {
@@ -139,33 +149,198 @@ pub async fn prometheus_remote_read(
139149 read_request. accepted_response_types
140150 ) ;
141151
142- // For now, create an empty response
143- warn ! ( "Returning empty response - data fetching not yet implemented" ) ;
144- let empty_response = create_empty_read_response ( & read_request) . map_err ( |e| {
145- AppError :: internal_server_error ( anyhow:: anyhow!( "Failed to create response: {}" , e) )
146- } ) ?;
152+ // Check if client accepts streamed XOR chunks
153+ let use_streaming = read_request
154+ . accepted_response_types
155+ . contains ( & ( ResponseType :: StreamedXorChunks as i32 ) ) ;
156+
157+ if use_streaming {
158+ // Return streamed chunked response
159+ handle_streamed_response ( & state, & read_request) . await
160+ } else {
161+ // Return standard SAMPLES response
162+ handle_samples_response ( & state, & read_request) . await
163+ }
164+ }
165+
166+ /// Handle standard SAMPLES response type
167+ async fn handle_samples_response (
168+ state : & HttpServerState ,
169+ read_request : & crate :: parsing:: prometheus:: remote_read_models:: ReadRequest ,
170+ ) -> Result < Response < axum:: body:: Body > , AppError > {
171+ let mut results = Vec :: with_capacity ( read_request. queries . len ( ) ) ;
172+
173+ for query in & read_request. queries {
174+ // Convert Prometheus matchers to SensApp matchers
175+ let matchers: Vec < LabelMatcher > = query. matchers . iter ( ) . map ( LabelMatcher :: from) . collect ( ) ;
176+
177+ // Convert timestamps from milliseconds to SensAppDateTime
178+ let start_time = SensAppDateTime :: from_unix_milliseconds_i64 ( query. start_timestamp_ms ) ;
179+ let end_time = SensAppDateTime :: from_unix_milliseconds_i64 ( query. end_timestamp_ms ) ;
180+
181+ // Query storage (numeric_only=true for Prometheus compatibility)
182+ let sensor_data = state
183+ . storage
184+ . query_sensors_by_labels ( & matchers, Some ( start_time) , Some ( end_time) , None , true )
185+ . await
186+ . map_err ( |e| {
187+ AppError :: internal_server_error ( anyhow:: anyhow!( "Storage query failed: {}" , e) )
188+ } ) ?;
189+
190+ debug ! ( "Query returned {} sensors" , sensor_data. len( ) ) ;
191+
192+ // Convert to Prometheus TimeSeries
193+ let timeseries = sensor_data
194+ . iter ( )
195+ . filter_map ( sensor_data_to_timeseries)
196+ . collect ( ) ;
197+
198+ results. push ( QueryResult { timeseries } ) ;
199+ }
200+
201+ let response = ReadResponse { results } ;
147202
148203 // Serialize and compress the response
149- let response_bytes = serialize_read_response ( & empty_response ) . map_err ( |e| {
204+ let response_bytes = serialize_read_response ( & response ) . map_err ( |e| {
150205 AppError :: internal_server_error ( anyhow:: anyhow!( "Failed to serialize response: {}" , e) )
151206 } ) ?;
152207
153208 info ! (
154- "Prometheus remote read: Returning response with {} bytes" ,
209+ "Prometheus remote read: Returning SAMPLES response with {} bytes" ,
155210 response_bytes. len( )
156211 ) ;
157212
158213 // Build HTTP response with appropriate headers
159- let response = Response :: builder ( )
214+ Response :: builder ( )
160215 . status ( StatusCode :: OK )
161216 . header ( "content-type" , "application/x-protobuf" )
162217 . header ( "content-encoding" , "snappy" )
163218 . body ( axum:: body:: Body :: from ( response_bytes) )
164219 . map_err ( |e| {
165220 AppError :: internal_server_error ( anyhow:: anyhow!( "Failed to build response: {}" , e) )
166- } ) ?;
221+ } )
222+ }
223+
224+ /// Handle STREAMED_XOR_CHUNKS response type
225+ async fn handle_streamed_response (
226+ state : & HttpServerState ,
227+ read_request : & crate :: parsing:: prometheus:: remote_read_models:: ReadRequest ,
228+ ) -> Result < Response < axum:: body:: Body > , AppError > {
229+ let mut chunked_responses = Vec :: with_capacity ( read_request. queries . len ( ) ) ;
230+
231+ for ( query_index, query) in read_request. queries . iter ( ) . enumerate ( ) {
232+ // Convert Prometheus matchers to SensApp matchers
233+ let matchers: Vec < LabelMatcher > = query. matchers . iter ( ) . map ( LabelMatcher :: from) . collect ( ) ;
234+
235+ // Convert timestamps from milliseconds to SensAppDateTime
236+ let start_time = SensAppDateTime :: from_unix_milliseconds_i64 ( query. start_timestamp_ms ) ;
237+ let end_time = SensAppDateTime :: from_unix_milliseconds_i64 ( query. end_timestamp_ms ) ;
238+
239+ // Query storage (numeric_only=true for Prometheus compatibility)
240+ let sensor_data = state
241+ . storage
242+ . query_sensors_by_labels ( & matchers, Some ( start_time) , Some ( end_time) , None , true )
243+ . await
244+ . map_err ( |e| {
245+ AppError :: internal_server_error ( anyhow:: anyhow!( "Storage query failed: {}" , e) )
246+ } ) ?;
247+
248+ debug ! (
249+ "Query {} returned {} sensors" ,
250+ query_index,
251+ sensor_data. len( )
252+ ) ;
253+
254+ // Convert to ChunkedSeries
255+ let chunked_series: Vec < _ > = sensor_data
256+ . iter ( )
257+ . filter_map ( |sd| {
258+ // Extract labels
259+ let labels = build_prometheus_labels ( & sd. sensor ) ;
260+
261+ // Extract samples as Prometheus format
262+ let samples = extract_prom_samples_for_chunks ( sd) ?;
263+
264+ // Encode as XOR chunks
265+ ChunkEncoder :: encode_series ( labels, samples) . ok ( )
266+ } )
267+ . collect ( ) ;
268+
269+ let chunked_response = ChunkEncoder :: create_response ( query_index as i64 , chunked_series) ;
270+ chunked_responses. push ( chunked_response) ;
271+ }
272+
273+ // Create the streaming response body
274+ let body = StreamWriter :: create_stream_body ( & chunked_responses) . map_err ( |e| {
275+ AppError :: internal_server_error ( anyhow:: anyhow!( "Failed to create stream body: {}" , e) )
276+ } ) ?;
277+
278+ info ! (
279+ "Prometheus remote read: Returning STREAMED_XOR_CHUNKS response with {} bytes" ,
280+ body. len( )
281+ ) ;
282+
283+ // Build HTTP response with appropriate headers for streaming
284+ Response :: builder ( )
285+ . status ( StatusCode :: OK )
286+ . header (
287+ "content-type" ,
288+ "application/x-streamed-protobuf; proto=prometheus.ChunkedReadResponse" ,
289+ )
290+ . body ( axum:: body:: Body :: from ( body) )
291+ . map_err ( |e| {
292+ AppError :: internal_server_error ( anyhow:: anyhow!( "Failed to build response: {}" , e) )
293+ } )
294+ }
167295
168- Ok ( response)
296+ /// Extract Prometheus samples from SensorData for chunk encoding.
297+ /// Returns None for non-numeric types.
298+ fn extract_prom_samples_for_chunks (
299+ sensor_data : & crate :: datamodel:: SensorData ,
300+ ) -> Option < Vec < PromSample > > {
301+ use crate :: datamodel:: TypedSamples ;
302+
303+ match & sensor_data. samples {
304+ TypedSamples :: Float ( samples) => {
305+ let prom_samples = samples
306+ . iter ( )
307+ . map ( |s| PromSample {
308+ value : s. value ,
309+ timestamp : s. datetime . to_unix_milliseconds ( ) . floor ( ) as i64 ,
310+ } )
311+ . collect ( ) ;
312+ Some ( prom_samples)
313+ }
314+ TypedSamples :: Integer ( samples) => {
315+ let prom_samples = samples
316+ . iter ( )
317+ . map ( |s| PromSample {
318+ value : s. value as f64 ,
319+ timestamp : s. datetime . to_unix_milliseconds ( ) . floor ( ) as i64 ,
320+ } )
321+ . collect ( ) ;
322+ Some ( prom_samples)
323+ }
324+ TypedSamples :: Numeric ( samples) => {
325+ use rust_decimal:: prelude:: ToPrimitive ;
326+ let prom_samples = samples
327+ . iter ( )
328+ . filter_map ( |s| {
329+ s. value . to_f64 ( ) . map ( |value| PromSample {
330+ value,
331+ timestamp : s. datetime . to_unix_milliseconds ( ) . floor ( ) as i64 ,
332+ } )
333+ } )
334+ . collect ( ) ;
335+ Some ( prom_samples)
336+ }
337+ // Non-numeric types cannot be represented in Prometheus format
338+ TypedSamples :: String ( _)
339+ | TypedSamples :: Boolean ( _)
340+ | TypedSamples :: Location ( _)
341+ | TypedSamples :: Blob ( _)
342+ | TypedSamples :: Json ( _) => None ,
343+ }
169344}
170345
171346#[ cfg( test) ]
0 commit comments