@@ -191,69 +191,144 @@ def add_args(parser):
191191
192192
193193class GrafanaMeasurementsPlugin (BasePlugin ):
194+ def __init__ (self , args ):
195+ super ().__init__ (args )
196+ self ._session = requests .Session ()
197+
198+ @property
199+ def batch_size (self ):
200+ return getattr (self .args , "grafana_chunk_size" , 50 )
201+
202+ @staticmethod
203+ def _empty_timerange (ri ):
204+ return (
205+ ri .start is None
206+ or ri .end is None
207+ or int (ri .start .timestamp ()) == int (ri .end .timestamp ())
208+ )
209+
194210 def _sanitize_target (self , target ):
195211 target = target .replace ("$Node" , self .args .grafana_node )
196212 target = target .replace ("$Interface" , self .args .grafana_interface )
197213 target = target .replace ("$Cloud" , self .args .grafana_prefix )
198214 return target
199215
200- @retry .retry_on_traceback (max_attempts = 10 , wait_seconds = 1 )
201- def measure (
202- self ,
203- ri ,
204- name ,
205- grafana_target ,
206- grafana_enritchment = {},
207- grafana_include_vars = False ,
208- ):
209- assert (
210- ri .start is not None and ri .end is not None
211- ), "We need timerange to approach Grafana"
212- if ri .start .strftime ("%s" ) == ri .end .strftime ("%s" ):
213- return name , None
214-
215- # Metadata for the request
216- headers = {
217- "Accept" : "application/json, text/plain, */*" ,
218- }
216+ def _fetch_targets (self , ri , targets ):
217+ """Fetch one or more targets in a single Graphite render request."""
218+ headers = {"Accept" : "application/json, text/plain, */*" }
219219 if self .args .grafana_token is not None :
220220 headers ["Authorization" ] = "Bearer %s" % self .args .grafana_token
221221 params = {
222- "target" : [ self . _sanitize_target ( grafana_target )] ,
222+ "target" : targets ,
223223 "from" : int (ri .start .timestamp ()),
224224 "until" : round (ri .end .timestamp ()),
225225 "format" : "json" ,
226226 }
227- url = f"{ self .args .grafana_host } :{ self .args .grafana_port } /api/datasources/proxy/{ self .args .grafana_datasource } /render"
228-
227+ url = (
228+ f"{ self .args .grafana_host } :{ self .args .grafana_port } "
229+ f"/api/datasources/proxy/{ self .args .grafana_datasource } /render"
230+ )
229231 urllib3 .disable_warnings (urllib3 .exceptions .InsecureRequestWarning )
230- r = requests .post (
231- url = url , headers = headers , params = params , timeout = 60 , verify = False
232+ r = self . _session .post (
233+ url = url , headers = headers , data = params , timeout = 60 , verify = False
232234 )
233235 if (
234236 not r .ok
235237 or r .headers ["Content-Type" ] != "application/json"
236238 or r .json () == []
237239 ):
238240 _debug_response (r )
239- logging . debug ( "Response: %s" % r .json () )
241+ return r .json ()
240242
241- points = [float (i [0 ]) for i in r .json ()[0 ]["datapoints" ] if i [0 ] is not None ]
242- stats = data .data_stats (points )
243+ @retry .retry_on_traceback (max_attempts = 10 , wait_seconds = 1 )
244+ def measure (self , ri , name , grafana_target , grafana_enritchment = {}, grafana_include_vars = False ):
245+ assert (
246+ ri .start is not None and ri .end is not None
247+ ), "We need timerange to approach Grafana"
248+ if self ._empty_timerange (ri ):
249+ return name , None
243250
244- # Add user defined data to the computed stats
245- if grafana_enritchment is not None and grafana_enritchment != {}:
246- stats ["enritchment" ] = grafana_enritchment
251+ response = self ._fetch_targets (ri , [self ._sanitize_target (grafana_target )])
252+ logging .debug ("Response: %s" % response )
247253
248- # If requested, add info about what variables were used to the computed stats
249- if grafana_include_vars :
254+ points = [float (i [0 ]) for i in response [0 ]["datapoints" ] if i [0 ] is not None ]
255+ item = {
256+ "grafana_enritchment" : grafana_enritchment ,
257+ "grafana_include_vars" : grafana_include_vars ,
258+ }
259+ stats = self ._apply_item_extras (data .data_stats (points ), item )
260+
261+ return name , stats
262+
263+ def _apply_item_extras (self , stats , item ):
264+ """Apply per-item enritchment and variables to computed stats."""
265+ if stats is None :
266+ return stats
267+
268+ enritchment = item .get ("grafana_enritchment" , {})
269+ if enritchment :
270+ stats ["enritchment" ] = enritchment
271+
272+ if item .get ("grafana_include_vars" , False ):
250273 stats ["variables" ] = {
251274 "$Node" : self .args .grafana_node ,
252275 "$Interface" : self .args .grafana_interface ,
253276 "$Cloud" : self .args .grafana_prefix ,
254277 }
255278
256- return name , stats
279+ return stats
280+
281+ def _measure_batched (self , ri , config_items ):
282+ """Fetch multiple Grafana items in a single HTTP request.
283+
284+ We rely on Graphite preserving request order here.
285+ A malformed response that silently omits an interior target
286+ could cause positional mismatches for later items.
287+ """
288+ targets = [self ._sanitize_target (item ["grafana_target" ]) for item in config_items ]
289+ response = self ._fetch_targets (ri , targets )
290+
291+ results = []
292+ for idx , item in enumerate (config_items ):
293+ if idx < len (response ):
294+ points = [
295+ float (p [0 ]) for p in response [idx ]["datapoints" ] if p [0 ] is not None
296+ ]
297+ stats = self ._apply_item_extras (data .data_stats (points ), item )
298+ else :
299+ stats = self ._apply_item_extras (data .data_stats ([]), item )
300+ results .append ((item ["name" ], stats ))
301+
302+ logging .debug (f"Batched { len (results )} Grafana targets in one request" )
303+ return results
304+
305+ def measure_many (self , ri , config_items ):
306+ """Execute a group of Grafana items with batch-first, per-target-fallback.
307+
308+ Tries a single batched HTTP request for all items. On failure,
309+ degrades to individual measure() calls (each with its own retry)
310+ to isolate failures per target.
311+ """
312+ if self ._empty_timerange (ri ):
313+ return [(item ["name" ], None ) for item in config_items ]
314+
315+ try :
316+ return self ._measure_batched (ri , config_items )
317+ except Exception as e :
318+ logging .warning (
319+ f"Batch request failed ({ e } ), falling back to "
320+ f"individual queries for { len (config_items )} targets"
321+ )
322+ results = []
323+ for item in config_items :
324+ try :
325+ results .append (self .measure (ri , ** item ))
326+ except Exception as e2 :
327+ logging .exception (
328+ f"Failed to measure { item ['name' ]} : { e2 } "
329+ )
330+ results .append ((None , None ))
331+ return results
257332
258333 @staticmethod
259334 def add_args (parser ):
@@ -263,8 +338,8 @@ def add_args(parser):
263338 parser .add_argument (
264339 "--grafana-chunk-size" ,
265340 type = int ,
266- default = 10 ,
267- help = "How many metrices to obtain from Grafana at one request" ,
341+ default = 50 ,
342+ help = "How many metrics to obtain from Grafana in one request" ,
268343 )
269344 parser .add_argument (
270345 "--grafana-port" ,
@@ -540,6 +615,7 @@ def __init__(
540615 self .sd = sd
541616
542617 self ._index = 0 # which config item are we processing?
618+ self ._batch_buffer = [] # buffered results from a batched request
543619 self ._token = None # OCP token - we will take it from `oc whoami -t` if needed
544620 self .measurement_plugins = (
545621 {}
@@ -569,34 +645,63 @@ def _find_plugin(self, keys):
569645
570646 def __next__ (self ):
571647 """
572- Gives tuple of key and value for every item in the config file
648+ Gives tuple of key and value for every item in the config file.
649+
650+ Plugins that implement measure_many() get consecutive items of
651+ the same type grouped and executed in a single call. Results are
652+ buffered and yielded one at a time.
573653 """
654+ # Drain buffer from a previous batch
655+ if self ._batch_buffer :
656+ return self ._batch_buffer .pop (0 )
657+
574658 i = self ._index
659+ if i >= len (self .config ):
660+ raise StopIteration
661+
662+ instance = self ._find_plugin (self .config [i ].keys ())
663+ if not instance :
664+ self ._index += 1
665+ raise Exception (f"Unknown config '{ self .config [i ]} '" )
666+
667+ # Group consecutive items handled by the same plugin if it
668+ # supports measure_many()
669+ if hasattr (instance , "measure_many" ) and self .start and self .end :
670+ chunk_size = getattr (instance , "batch_size" , 10 )
671+ chunk_items = []
672+ j = i
673+ while (
674+ j < len (self .config )
675+ and self ._find_plugin (self .config [j ].keys ()) is instance
676+ and len (chunk_items ) < chunk_size
677+ ):
678+ chunk_items .append (self .config [j ])
679+ j += 1
680+ self ._index = j
681+
682+ results = instance .measure_many (self , chunk_items )
683+ self ._batch_buffer = results [1 :]
684+ return results [0 ]
685+
686+ # Non-Grafana item: process individually (unchanged)
575687 self ._index += 1
576- if i < len (self .config ):
577- if self ._find_plugin (self .config [i ].keys ()):
578- instance = self ._find_plugin (self .config [i ].keys ())
579- name = list (self .config [i ].keys ())[1 ]
580- try :
581- if name == "log_source_command" :
582- output = instance .measure (self , self .config [i ])
583- else :
584- output = instance .measure (self , ** self .config [i ])
585- except NoDataException as e :
586- logging .warning (
587- f"Failed to measure { self .config [i ]['name' ]} : { e } "
588- )
589- output = (None , None )
590- except Exception as e :
591- logging .exception (
592- f"Failed to measure { self .config [i ]['name' ]} : { e } "
593- )
594- output = (None , None )
595- return output
688+ name = list (self .config [i ].keys ())[1 ]
689+ try :
690+ if name == "log_source_command" :
691+ output = instance .measure (self , self .config [i ])
596692 else :
597- raise Exception (f"Unknown config '{ self .config [i ]} '" )
598- else :
599- raise StopIteration
693+ output = instance .measure (self , ** self .config [i ])
694+ except NoDataException as e :
695+ logging .warning (
696+ f"Failed to measure { self .config [i ]['name' ]} : { e } "
697+ )
698+ output = (None , None )
699+ except Exception as e :
700+ logging .exception (
701+ f"Failed to measure { self .config [i ]['name' ]} : { e } "
702+ )
703+ output = (None , None )
704+ return output
600705
601706
602707def doit (args ):
0 commit comments