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.
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/PKG-INFO +1 -1
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/pyproject.toml +1 -1
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/__init__.py +1 -1
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/client.py +43 -27
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/tests/test_client.py +35 -39
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/.gitignore +0 -0
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/README.md +0 -0
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/build-publish.sh +0 -0
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/dev-requirements.txt +0 -0
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/exceptions.py +0 -0
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/__init__.py +0 -0
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/log_entry.py +0 -0
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/log_stream.py +0 -0
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/metric_sample.py +0 -0
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/metric_series.py +0 -0
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/query_result.py +0 -0
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/query_stats.py +0 -0
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/utils.py +0 -0
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/tests/__init__.py +0 -0
- {loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/tests/test_models.py +0 -0
- {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.
|
|
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
|
|
@@ -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.
|
|
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] =
|
|
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
|
|
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
|
|
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
|
-
|
|
320
|
-
|
|
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
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
472
|
+
if start is None and end is None:
|
|
473
|
+
start, end = self._discovery_time_range()
|
|
463
474
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
475
|
+
params = {
|
|
476
|
+
"start": str(start),
|
|
477
|
+
"end": str(end),
|
|
478
|
+
}
|
|
468
479
|
|
|
469
|
-
response = self._request("GET", "/loki/api/v1/labels", params
|
|
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
|
-
|
|
504
|
+
if start is None and end is None:
|
|
505
|
+
start, end = self._discovery_time_range()
|
|
490
506
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
477
|
+
def test_find_severity_label_from_series(self) -> None:
|
|
482
478
|
client = LokiClient(base_url="http://localhost:3100")
|
|
483
|
-
with patch.object(client, "
|
|
484
|
-
|
|
485
|
-
|
|
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, "
|
|
496
|
-
|
|
497
|
-
|
|
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.
|
|
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, "
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
522
|
-
|
|
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
|
-
|
|
528
|
-
|
|
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="
|
|
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",
|
|
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")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/metric_sample.py
RENAMED
|
File without changes
|
{loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/metric_series.py
RENAMED
|
File without changes
|
{loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/query_result.py
RENAMED
|
File without changes
|
{loki_reader_core-0.2.2 → loki_reader_core-0.2.4}/src/loki_reader_core/models/query_stats.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|