loki-reader-core 0.2.2__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/PKG-INFO +1 -1
  2. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/pyproject.toml +1 -1
  3. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/__init__.py +1 -1
  4. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/client.py +43 -27
  5. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/tests/test_client.py +35 -39
  6. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/.gitignore +0 -0
  7. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/README.md +0 -0
  8. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/build-publish.sh +0 -0
  9. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/dev-requirements.txt +0 -0
  10. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/exceptions.py +0 -0
  11. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/__init__.py +0 -0
  12. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/log_entry.py +0 -0
  13. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/log_stream.py +0 -0
  14. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/metric_sample.py +0 -0
  15. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/metric_series.py +0 -0
  16. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/query_result.py +0 -0
  17. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/query_stats.py +0 -0
  18. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/utils.py +0 -0
  19. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/tests/__init__.py +0 -0
  20. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/tests/test_models.py +0 -0
  21. {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: loki-reader-core
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Python library for querying Grafana Loki logs via REST API
5
5
  Project-URL: Homepage, https://github.com/jmazzahacks/loki-reader-core
6
6
  Project-URL: Issues, https://github.com/jmazzahacks/loki-reader-core/issues
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "loki-reader-core"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  authors = [
5
5
  { name="Jason Byteforge", email="jason@mzmail.me" },
6
6
  ]
@@ -6,7 +6,7 @@ from .client import LokiClient
6
6
  from .exceptions import LokiAuthError, LokiConnectionError, LokiError, LokiQueryError
7
7
  from .models import LogEntry, LogStream, MetricSample, MetricSeries, QueryResult, QueryStats
8
8
 
9
- __version__ = "0.2.2"
9
+ __version__ = "0.2.4"
10
10
 
11
11
  __all__ = [
12
12
  "LokiClient",
@@ -187,7 +187,7 @@ class LokiClient:
187
187
 
188
188
  self._session: Optional[requests.Session] = None
189
189
  self._app_label_cache: dict[str, str] = {}
190
- self._severity_label_cache: Optional[str] = None
190
+ self._severity_label_cache: dict[str, Optional[str]] = {}
191
191
 
192
192
  @property
193
193
  def session(self) -> requests.Session:
@@ -301,9 +301,8 @@ class LokiClient:
301
301
  if app_value in self._app_label_cache:
302
302
  return self._app_label_cache[app_value]
303
303
 
304
- start, end = self._discovery_time_range()
305
304
  for label_name in APP_LABEL_NAMES:
306
- values = self.get_label_values(label_name, start=start, end=end)
305
+ values = self.get_label_values(label_name)
307
306
  if app_value in values:
308
307
  self._app_label_cache[app_value] = label_name
309
308
  return label_name
@@ -313,25 +312,32 @@ class LokiClient:
313
312
  f"({', '.join(APP_LABEL_NAMES)}). Use logql param for custom labels."
314
313
  )
315
314
 
316
- def _find_severity_label(self) -> Optional[str]:
317
- """Discover which label name is used for severity/level.
315
+ def _find_severity_label(self, app_label: str, app_value: str) -> Optional[str]:
316
+ """Discover which severity label name is used by a specific app.
318
317
 
319
- Checks a prioritized list of common severity label names and
320
- returns the first one that has values. Result is cached.
318
+ Queries the actual stream labels for the given app and checks
319
+ which severity label name appears. This avoids false matches
320
+ from other apps using a different severity label.
321
+
322
+ Args:
323
+ app_label: The label name for the application (e.g. "application").
324
+ app_value: The application name (e.g. "materia-server").
321
325
 
322
326
  Returns:
323
- The severity label name, or None if not found.
327
+ The severity label name for this app, or None if not found.
324
328
  """
325
- if self._severity_label_cache is not None:
326
- return self._severity_label_cache
329
+ if app_value in self._severity_label_cache:
330
+ return self._severity_label_cache[app_value]
331
+
332
+ series = self.get_series(match=['{' + f'{app_label}="{app_value}"' + '}'])
327
333
 
328
- start, end = self._discovery_time_range()
329
334
  for label_name in SEVERITY_LABEL_NAMES:
330
- values = self.get_label_values(label_name, start=start, end=end)
331
- if values:
332
- self._severity_label_cache = label_name
333
- return label_name
335
+ for s in series:
336
+ if label_name in s:
337
+ self._severity_label_cache[app_value] = label_name
338
+ return label_name
334
339
 
340
+ self._severity_label_cache[app_value] = None
335
341
  return None
336
342
 
337
343
  def query(
@@ -378,7 +384,7 @@ class LokiClient:
378
384
  app_label = self._find_app_label(app)
379
385
  selector = f'{app_label}="{app}"'
380
386
  if severity is not None:
381
- sev_label = self._find_severity_label()
387
+ sev_label = self._find_severity_label(app_label, app)
382
388
  if sev_label:
383
389
  regex = _build_severity_regex(severity)
384
390
  selector += f', {sev_label}=~"{regex}"'
@@ -452,6 +458,10 @@ class LokiClient:
452
458
  """
453
459
  Get list of available label names.
454
460
 
461
+ Defaults to a 30-day lookback when no time range is provided,
462
+ since Loki's label API may only return labels for recently
463
+ active streams without an explicit range.
464
+
455
465
  Args:
456
466
  start: Optional start timestamp in nanoseconds.
457
467
  end: Optional end timestamp in nanoseconds.
@@ -459,14 +469,15 @@ class LokiClient:
459
469
  Returns:
460
470
  List of label names.
461
471
  """
462
- params = {}
472
+ if start is None and end is None:
473
+ start, end = self._discovery_time_range()
463
474
 
464
- if start is not None:
465
- params["start"] = str(start)
466
- if end is not None:
467
- params["end"] = str(end)
475
+ params = {
476
+ "start": str(start),
477
+ "end": str(end),
478
+ }
468
479
 
469
- response = self._request("GET", "/loki/api/v1/labels", params or None)
480
+ response = self._request("GET", "/loki/api/v1/labels", params)
470
481
  return response.get("data", [])
471
482
 
472
483
  def get_label_values(
@@ -478,6 +489,10 @@ class LokiClient:
478
489
  """
479
490
  Get list of values for a specific label.
480
491
 
492
+ Defaults to a 30-day lookback when no time range is provided,
493
+ since Loki's label API may only return values for recently
494
+ active streams without an explicit range.
495
+
481
496
  Args:
482
497
  label: Label name to get values for.
483
498
  start: Optional start timestamp in nanoseconds.
@@ -486,12 +501,13 @@ class LokiClient:
486
501
  Returns:
487
502
  List of label values.
488
503
  """
489
- params = {}
504
+ if start is None and end is None:
505
+ start, end = self._discovery_time_range()
490
506
 
491
- if start is not None:
492
- params["start"] = str(start)
493
- if end is not None:
494
- params["end"] = str(end)
507
+ params = {
508
+ "start": str(start),
509
+ "end": str(end),
510
+ }
495
511
 
496
512
  endpoint = f"/loki/api/v1/label/{label}/values"
497
513
  response = self._request("GET", endpoint, params or None)
@@ -75,7 +75,7 @@ class TestLokiClientInit:
75
75
  def test_init_label_caches_empty(self) -> None:
76
76
  client = LokiClient(base_url="http://localhost:3100")
77
77
  assert client._app_label_cache == {}
78
- assert client._severity_label_cache is None
78
+ assert client._severity_label_cache == {}
79
79
 
80
80
 
81
81
  class TestLokiClientSession:
@@ -453,16 +453,12 @@ class TestLabelDiscovery:
453
453
  mock_glv.return_value = ["myapp", "otherapp"]
454
454
  result = client._find_app_label("myapp")
455
455
  assert result == "application"
456
- mock_glv.assert_called_once()
457
- args = mock_glv.call_args
458
- assert args.args[0] == "application"
459
- assert "start" in args.kwargs
460
- assert "end" in args.kwargs
456
+ mock_glv.assert_called_once_with("application")
461
457
 
462
458
  def test_find_app_label_job(self) -> None:
463
459
  client = LokiClient(base_url="http://localhost:3100")
464
460
  with patch.object(client, "get_label_values") as mock_glv:
465
- def side_effect(label: str, start: int = None, end: int = None) -> list[str]:
461
+ def side_effect(label: str) -> list[str]:
466
462
  if label == "job":
467
463
  return ["myapp", "worker"]
468
464
  return []
@@ -478,23 +474,32 @@ class TestLabelDiscovery:
478
474
  with pytest.raises(ValueError, match="Could not find 'myapp'"):
479
475
  client._find_app_label("myapp")
480
476
 
481
- def test_find_severity_label(self) -> None:
477
+ def test_find_severity_label_from_series(self) -> None:
482
478
  client = LokiClient(base_url="http://localhost:3100")
483
- with patch.object(client, "get_label_values") as mock_glv:
484
- mock_glv.return_value = ["info", "error", "warn"]
485
- result = client._find_severity_label()
479
+ with patch.object(client, "get_series") as mock_gs:
480
+ mock_gs.return_value = [
481
+ {"application": "myapp", "severity": "info", "logger": "root"},
482
+ ]
483
+ result = client._find_severity_label("application", "myapp")
484
+ assert result == "severity"
485
+ mock_gs.assert_called_once_with(match=['{application="myapp"}'])
486
+
487
+ def test_find_severity_label_level(self) -> None:
488
+ client = LokiClient(base_url="http://localhost:3100")
489
+ with patch.object(client, "get_series") as mock_gs:
490
+ mock_gs.return_value = [
491
+ {"application": "myapp", "level": "info", "logger": "root"},
492
+ ]
493
+ result = client._find_severity_label("application", "myapp")
486
494
  assert result == "level"
487
- mock_glv.assert_called_once()
488
- args = mock_glv.call_args
489
- assert args.args[0] == "level"
490
- assert "start" in args.kwargs
491
- assert "end" in args.kwargs
492
495
 
493
496
  def test_find_severity_label_none(self) -> None:
494
497
  client = LokiClient(base_url="http://localhost:3100")
495
- with patch.object(client, "get_label_values") as mock_glv:
496
- mock_glv.return_value = []
497
- result = client._find_severity_label()
498
+ with patch.object(client, "get_series") as mock_gs:
499
+ mock_gs.return_value = [
500
+ {"application": "myapp", "logger": "root"},
501
+ ]
502
+ result = client._find_severity_label("application", "myapp")
498
503
  assert result is None
499
504
 
500
505
  def test_label_discovery_cached(self) -> None:
@@ -506,29 +511,20 @@ class TestLabelDiscovery:
506
511
  client._find_app_label("myapp")
507
512
 
508
513
  # Only called once - second call uses cache
509
- mock_glv.assert_called_once()
514
+ mock_glv.assert_called_once_with("application")
510
515
 
511
516
  def test_severity_label_cached(self) -> None:
512
517
  client = LokiClient(base_url="http://localhost:3100")
513
- with patch.object(client, "get_label_values") as mock_glv:
514
- mock_glv.return_value = ["info", "error"]
515
-
516
- client._find_severity_label()
517
- client._find_severity_label()
518
-
519
- mock_glv.assert_called_once()
518
+ with patch.object(client, "get_series") as mock_gs:
519
+ mock_gs.return_value = [
520
+ {"application": "myapp", "severity": "info"},
521
+ ]
520
522
 
521
- def test_discovery_uses_30_day_range(self) -> None:
522
- client = LokiClient(base_url="http://localhost:3100")
523
- with patch.object(client, "get_label_values") as mock_glv:
524
- mock_glv.return_value = ["myapp"]
525
- client._find_app_label("myapp")
523
+ client._find_severity_label("application", "myapp")
524
+ client._find_severity_label("application", "myapp")
526
525
 
527
- args = mock_glv.call_args
528
- start = args.kwargs["start"]
529
- end = args.kwargs["end"]
530
- diff_days = (end - start) / (24 * 60 * 60 * 1_000_000_000)
531
- assert diff_days == 30
526
+ # Only called once - second call uses cache
527
+ mock_gs.assert_called_once()
532
528
 
533
529
 
534
530
  class TestMergeStreams:
@@ -659,14 +655,14 @@ class TestLokiClientQueryRedesign:
659
655
  def test_query_app_with_severity(self) -> None:
660
656
  client = LokiClient(base_url="http://localhost:3100")
661
657
  with patch.object(client, "_find_app_label", return_value="application"), \
662
- patch.object(client, "_find_severity_label", return_value="level"), \
658
+ patch.object(client, "_find_severity_label", return_value="severity"), \
663
659
  patch.object(client, "query_range") as mock_qr:
664
660
  mock_qr.return_value = self._make_multi_stream_result()
665
661
 
666
662
  client.query(app="myapp", severity="error")
667
663
 
668
664
  args = mock_qr.call_args
669
- assert args.kwargs["logql"] == '{application="myapp", level=~"error|fatal|critical"}'
665
+ assert args.kwargs["logql"] == '{application="myapp", severity=~"error|fatal|critical"}'
670
666
 
671
667
  def test_query_app_since_minutes(self) -> None:
672
668
  client = LokiClient(base_url="http://localhost:3100")