33import logging
44import time
55import base64
6+ import snappy
67from typing import List , Dict , Any
78from fastapi import HTTPException
8- from prometheus_client import CollectorRegistry , Gauge
9- from prometheus_client .exposition import basic_auth_handler
10- from prometheus_client .openmetrics .exposition import generate_latest
119import httpx
1210
11+ from prometheus_client .core import CollectorRegistry , Gauge
12+ from prometheus_client .exposition import generate_latest
13+
1314from ...config import Config
1415from ...models import NuonWebhook
1516
@@ -133,6 +134,139 @@ def _build_labels(self, webhook: NuonWebhook) -> Dict[str, str]:
133134
134135 return labels
135136
137+ # PROTOBUF MESSAGE BUILDER
138+ def _build_remote_write_request (self , metrics : List [Dict [str , Any ]], labels : Dict [str , str ], timestamp : int ) -> bytes :
139+ """
140+ Build Prometheus Remote Write protobuf message.
141+
142+ Uses simplified protobuf construction following Prometheus spec.
143+
144+ Args:
145+ metrics: List of metric dictionaries
146+ labels: Label key-value pairs
147+ timestamp: Unix timestamp (seconds)
148+
149+ Returns:
150+ Protobuf-encoded bytes
151+ """
152+ from google .protobuf import text_format
153+ from google .protobuf .message import Message
154+
155+ # Build time series for each metric
156+ timeseries = []
157+ timestamp_ms = timestamp * 1000 # Convert to milliseconds
158+
159+ for metric in metrics :
160+ # Build labels for this metric (metric name + common labels)
161+ metric_labels = [{"name" : "__name__" , "value" : metric ["name" ]}]
162+ for k , v in labels .items ():
163+ metric_labels .append ({"name" : k , "value" : v })
164+
165+ # Build sample (timestamp + value)
166+ sample = {"value" : metric ["value" ], "timestamp" : timestamp_ms }
167+
168+ # Build time series
169+ timeseries .append ({
170+ "labels" : metric_labels ,
171+ "samples" : [sample ]
172+ })
173+
174+ # Build write request
175+ write_request = {"timeseries" : timeseries }
176+
177+ # Convert to protobuf bytes using manual encoding
178+ # This is a simplified protobuf encoding for Remote Write format
179+ return self ._encode_protobuf (write_request )
180+
181+ def _encode_protobuf (self , data : dict ) -> bytes :
182+ """
183+ Manually encode data to Prometheus Remote Write protobuf format.
184+
185+ This implements the minimal protobuf encoding needed for Remote Write.
186+
187+ Args:
188+ data: Dictionary with timeseries data
189+
190+ Returns:
191+ Protobuf-encoded bytes
192+ """
193+ # For now, use a simple encoding approach
194+ # We'll encode the timeseries data following the Prometheus protobuf schema
195+
196+ output = bytearray ()
197+
198+ for ts in data ["timeseries" ]:
199+ # Encode each timeseries
200+ ts_bytes = self ._encode_timeseries (ts )
201+ # Field 1 is repeated TimeSeries (wire type 2 = length-delimited)
202+ output .append (0x0a ) # Field 1, wire type 2
203+ output .extend (self ._encode_varint (len (ts_bytes )))
204+ output .extend (ts_bytes )
205+
206+ return bytes (output )
207+
208+ def _encode_timeseries (self , ts : dict ) -> bytes :
209+ """Encode a single timeseries to protobuf."""
210+ output = bytearray ()
211+
212+ # Encode labels (field 1)
213+ for label in ts ["labels" ]:
214+ label_bytes = self ._encode_label (label )
215+ output .append (0x0a ) # Field 1, wire type 2
216+ output .extend (self ._encode_varint (len (label_bytes )))
217+ output .extend (label_bytes )
218+
219+ # Encode samples (field 2)
220+ for sample in ts ["samples" ]:
221+ sample_bytes = self ._encode_sample (sample )
222+ output .append (0x12 ) # Field 2, wire type 2
223+ output .extend (self ._encode_varint (len (sample_bytes )))
224+ output .extend (sample_bytes )
225+
226+ return bytes (output )
227+
228+ def _encode_label (self , label : dict ) -> bytes :
229+ """Encode a label to protobuf."""
230+ output = bytearray ()
231+
232+ # Name (field 1, string)
233+ name_bytes = label ["name" ].encode ("utf-8" )
234+ output .append (0x0a ) # Field 1, wire type 2
235+ output .extend (self ._encode_varint (len (name_bytes )))
236+ output .extend (name_bytes )
237+
238+ # Value (field 2, string)
239+ value_bytes = label ["value" ].encode ("utf-8" )
240+ output .append (0x12 ) # Field 2, wire type 2
241+ output .extend (self ._encode_varint (len (value_bytes )))
242+ output .extend (value_bytes )
243+
244+ return bytes (output )
245+
246+ def _encode_sample (self , sample : dict ) -> bytes :
247+ """Encode a sample to protobuf."""
248+ output = bytearray ()
249+
250+ # Value (field 1, double)
251+ import struct
252+ output .append (0x09 ) # Field 1, wire type 1 (64-bit)
253+ output .extend (struct .pack ('<d' , sample ["value" ]))
254+
255+ # Timestamp (field 2, int64)
256+ output .append (0x10 ) # Field 2, wire type 0 (varint)
257+ output .extend (self ._encode_varint (sample ["timestamp" ]))
258+
259+ return bytes (output )
260+
261+ def _encode_varint (self , value : int ) -> bytes :
262+ """Encode an integer as a protobuf varint."""
263+ output = bytearray ()
264+ while value > 127 :
265+ output .append ((value & 0x7f ) | 0x80 )
266+ value >>= 7
267+ output .append (value & 0x7f )
268+ return bytes (output )
269+
136270 # SEND TO GRAFANA
137271 async def _send_to_grafana (self , metrics : List [Dict [str , Any ]], labels : Dict [str , str ], timestamp : int ) -> bool :
138272 """
@@ -151,25 +285,11 @@ async def _send_to_grafana(self, metrics: List[Dict[str, Any]], labels: Dict[str
151285 Raises:
152286 HTTPException: If sending fails
153287 """
154- # Create a temporary registry for these metrics
155- registry = CollectorRegistry ()
156-
157- # Get label names and values
158- label_names = list (labels .keys ())
159- label_values = list (labels .values ())
160-
161- # Create and set each metric
162- for metric in metrics :
163- gauge = Gauge (
164- metric ["name" ],
165- f"Nuon webhook metric: { metric ['name' ]} " ,
166- labelnames = label_names ,
167- registry = registry
168- )
169- gauge .labels (* label_values ).set (metric ["value" ])
288+ # Build protobuf message
289+ protobuf_data = self ._build_remote_write_request (metrics , labels , timestamp )
170290
171- # Generate Prometheus exposition format
172- metrics_data = generate_latest ( registry )
291+ # Compress with Snappy
292+ compressed_data = snappy . compress ( protobuf_data )
173293
174294 # Build Basic Auth header (base64-encoded)
175295 auth_string = f"{ self .instance_id } :{ self .api_token } "
@@ -180,10 +300,11 @@ async def _send_to_grafana(self, metrics: List[Dict[str, Any]], labels: Dict[str
180300 async with httpx .AsyncClient () as client :
181301 response = await client .post (
182302 self .metrics_url ,
183- content = metrics_data ,
303+ content = compressed_data ,
184304 headers = {
185305 "Authorization" : f"Basic { auth_b64 } " ,
186- "Content-Type" : "application/openmetrics-text" ,
306+ "Content-Encoding" : "snappy" ,
307+ "Content-Type" : "application/x-protobuf" ,
187308 "X-Prometheus-Remote-Write-Version" : "0.1.0"
188309 },
189310 timeout = 10.0
0 commit comments