66"""
77
88import argparse
9- import datetime
109import json
1110import os
1211import re
1312import sys
14- from typing import Annotated , Any , Dict , Optional , Union , cast
13+ from datetime import datetime , timedelta , timezone
14+ from typing import Annotated , Any , Dict , Optional , cast
1515
1616# mypy: ignore-errors
1717import requests
3232DEFAULT_GRAFANA_API_KEY = os .environ .get ("GRAFANA_API_KEY" , "" )
3333
3434
35- def iso8601_to_unix_nano (ts : str ) -> Optional [int ]:
36- """Convert ISO8601 string to UNIX nanoseconds. Return None if not ISO8601."""
37- try :
38- # Accepts e.g. 2025-05-27T14:59:33.073316 or 2025-05-27T14:59:33.073316Z
39- dt = datetime .datetime .fromisoformat (ts .replace ("Z" , "+00:00" ))
40- return int (dt .timestamp () * 1_000_000_000 )
41- except Exception :
42- return None
43-
44-
4535class GrafanaClient :
4636 """Client for interacting with Grafana API."""
4737
@@ -127,23 +117,11 @@ def query_loki(
127117 "direction" : direction ,
128118 }
129119 if start is not None :
130- # Accept ISO8601 or UNIX ns or Grafana relative time
131- if start .startswith ("now" ) or start .isdigit ():
132- # Keep Grafana relative time or Unix timestamp as is
133- params ["start" ] = start
134- else :
135- # Convert ISO8601 to UNIX nanoseconds
136- unix_start = iso8601_to_unix_nano (start )
137- params ["start" ] = unix_start if unix_start is not None else start
120+ # Use parse_grafana_time to convert all time formats to Unix nanoseconds
121+ params ["start" ] = parse_grafana_time (start )
138122 if end is not None :
139- # Accept ISO8601 or UNIX ns or Grafana relative time
140- if end .startswith ("now" ) or end .isdigit ():
141- # Keep Grafana relative time or Unix timestamp as is
142- params ["end" ] = end
143- else :
144- # Convert ISO8601 to UNIX nanoseconds
145- unix_end = iso8601_to_unix_nano (end )
146- params ["end" ] = unix_end if unix_end is not None else end
123+ # Use parse_grafana_time to convert all time formats to Unix nanoseconds
124+ params ["end" ] = parse_grafana_time (end )
147125
148126 # Send request
149127 try :
@@ -194,33 +172,6 @@ def get_loki_labels(self) -> Dict[str, Any]:
194172 response .raise_for_status ()
195173 return response .json ()
196174
197- # ... rest of the file unchanged ...
198-
199- datasource_id = self ._get_loki_datasource_uid ()
200-
201- # Set base URL for API request
202- base_url = f"{ self .base_url } /api/datasources/proxy/{ datasource_id } "
203-
204- url = f"{ base_url } /loki/api/v1/labels"
205-
206- try :
207- response = requests .get (url , headers = self .headers )
208- response .raise_for_status ()
209- return cast (Dict [str , Any ], response .json ())
210- except requests .exceptions .RequestException as e :
211- # Get more detailed error information
212- error_detail = str (e )
213- if hasattr (e , "response" ) and e .response is not None :
214- try :
215- error_json = e .response .json ()
216- error_detail = f"{ error_detail } - Details: { json .dumps (error_json )} "
217- except Exception :
218- if e .response .text :
219- error_detail = f"{ error_detail } - Response: { e .response .text } "
220-
221- # Raise a ValueError with the detailed error message
222- raise ValueError (f"Error getting Loki labels: { error_detail } " ) from e
223-
224175 def get_loki_label_values (self , label : str ) -> Dict [str , Any ]:
225176 """Get values for a specific label from Loki.
226177
@@ -405,7 +356,7 @@ def get_grafana_client() -> GrafanaClient:
405356 return GrafanaClient (args .grafana_url , args .grafana_api_key )
406357
407358
408- def parse_grafana_time (time_str : str ) -> Union [ str , datetime . datetime ] :
359+ def parse_grafana_time (time_str : str ) -> str :
409360 """Parse time string in various formats.
410361
411362 Args:
@@ -416,23 +367,59 @@ def parse_grafana_time(time_str: str) -> Union[str, datetime.datetime]:
416367 - RFC3339 format
417368
418369 Returns:
419- Original string if it's a Grafana relative time or Unix timestamp,
420- or datetime object for other formats
370+ Unix nanosecond timestamp string for all formats
421371 """
422372 if not time_str :
423- return "now"
373+ return str (int (datetime .now (timezone .utc ).timestamp () * 1_000_000_000 ))
374+
375+ # Handle 'now'
376+ if time_str == "now" :
377+ return str (int (datetime .now (timezone .utc ).timestamp () * 1_000_000_000 ))
378+
379+ # Handle Grafana relative time format (now-1h, now-5m, etc.)
380+ if time_str .startswith ("now-" ):
381+ match = re .match (r"^now-(\d+)([smhdwMy])$" , time_str )
382+ if match :
383+ amount = int (match .group (1 ))
384+ unit = match .group (2 )
385+
386+ # Convert to timedelta
387+ if unit == "s" :
388+ delta = timedelta (seconds = amount )
389+ elif unit == "m" :
390+ delta = timedelta (minutes = amount )
391+ elif unit == "h" :
392+ delta = timedelta (hours = amount )
393+ elif unit == "d" :
394+ delta = timedelta (days = amount )
395+ elif unit == "w" :
396+ delta = timedelta (weeks = amount )
397+ elif unit == "M" :
398+ delta = timedelta (days = amount * 30 ) # Approximate month
399+ elif unit == "y" :
400+ delta = timedelta (days = amount * 365 ) # Approximate year
401+ else :
402+ # Invalid unit, return current time
403+ return str (int (datetime .now (timezone .utc ).timestamp () * 1_000_000_000 ))
424404
425- # Grafana relative time format - return as-is for Loki API
426- if time_str == "now" or re . match ( r"^ now-\d+[smhdwMy]$" , time_str ):
427- return time_str
405+ # Calculate the time
406+ target_time = datetime . now ( timezone . utc ) - delta
407+ return str ( int ( target_time . timestamp () * 1_000_000_000 ))
428408
429- # Unix timestamp (numeric string) - return as-is
409+ # Unix timestamp (numeric string) - convert to nanoseconds if needed
430410 if time_str .isdigit ():
431- return time_str
411+ # Assume it's in seconds if less than 13 digits, otherwise nanoseconds
412+ if len (time_str ) <= 10 :
413+ return str (int (time_str ) * 1_000_000_000 )
414+ else :
415+ return time_str
432416
433417 # Try to parse as ISO format
434418 try :
435- return datetime .datetime .fromisoformat (time_str )
419+ dt = datetime .fromisoformat (time_str )
420+ if dt .tzinfo is None :
421+ dt = dt .replace (tzinfo = timezone .utc )
422+ return str (int (dt .timestamp () * 1_000_000_000 ))
436423 except ValueError :
437424 pass
438425
@@ -442,12 +429,13 @@ def parse_grafana_time(time_str: str) -> Union[str, datetime.datetime]:
442429 iso_str = (
443430 time_str .replace ("Z" , "+00:00" ) if time_str .endswith ("Z" ) else time_str
444431 )
445- return datetime .datetime .fromisoformat (iso_str )
432+ dt = datetime .fromisoformat (iso_str )
433+ return str (int (dt .timestamp () * 1_000_000_000 ))
446434 except ValueError :
447435 pass
448436
449- # If all parsing fails, return as Grafana relative time
450- return " now"
437+ # If all parsing fails, return current time
438+ return str ( int ( datetime . now ( timezone . utc ). timestamp () * 1_000_000_000 ))
451439
452440
453441def get_custom_query_loki_description () -> str :
@@ -577,25 +565,13 @@ def query_loki(
577565 if start is not None and end is None :
578566 end = "now" # Default end to current time when start is specified
579567
580- # Parse start time
568+ # Parse start time - parse_grafana_time now always returns string
581569 if start :
582- parsed_start = parse_grafana_time (start )
583- if isinstance (parsed_start , str ):
584- # Grafana relative time or Unix timestamp - use as-is
585- start = parsed_start
586- else :
587- # Convert datetime to Unix nanoseconds
588- start = str (int (parsed_start .timestamp () * 1_000_000_000 ))
570+ start = parse_grafana_time (start )
589571
590- # Parse end time
572+ # Parse end time - parse_grafana_time now always returns string
591573 if end :
592- parsed_end = parse_grafana_time (end )
593- if isinstance (parsed_end , str ):
594- # Grafana relative time or Unix timestamp - use as-is
595- end = parsed_end
596- else :
597- # Convert datetime to Unix nanoseconds
598- end = str (int (parsed_end .timestamp () * 1_000_000_000 ))
574+ end = parse_grafana_time (end )
599575
600576 client = get_grafana_client ()
601577 return client .query_loki (query , start , end , limit , direction , max_per_line )
0 commit comments