loki-reader-core 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,21 @@
1
+ """
2
+ Python library for querying Grafana Loki logs via REST API
3
+ """
4
+
5
+ from .client import LokiClient
6
+ from .exceptions import LokiAuthError, LokiConnectionError, LokiError, LokiQueryError
7
+ from .models import LogEntry, LogStream, QueryResult, QueryStats
8
+
9
+ __version__ = "0.0.1"
10
+
11
+ __all__ = [
12
+ "LokiClient",
13
+ "LogEntry",
14
+ "LogStream",
15
+ "QueryResult",
16
+ "QueryStats",
17
+ "LokiError",
18
+ "LokiConnectionError",
19
+ "LokiQueryError",
20
+ "LokiAuthError",
21
+ ]
@@ -0,0 +1,294 @@
1
+ """
2
+ LokiClient for querying Grafana Loki logs via REST API.
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ import requests
8
+
9
+ from .exceptions import LokiAuthError, LokiConnectionError, LokiQueryError
10
+ from .models import QueryResult
11
+ from .utils import now_ns
12
+
13
+
14
+ class LokiClient:
15
+ """
16
+ Client for querying Grafana Loki logs.
17
+
18
+ Example:
19
+ client = LokiClient(
20
+ base_url="https://loki.example.com",
21
+ auth=("user", "pass"),
22
+ org_id="tenant-1"
23
+ )
24
+
25
+ result = client.query_range(
26
+ logql='{job="api"} |= "error"',
27
+ start=hours_ago_ns(1),
28
+ end=now_ns(),
29
+ limit=500
30
+ )
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ base_url: str,
36
+ auth: Optional[tuple[str, str]] = None,
37
+ org_id: Optional[str] = None,
38
+ ca_cert: Optional[str] = None,
39
+ verify_ssl: bool = True,
40
+ timeout: int = 30
41
+ ):
42
+ """
43
+ Initialize the Loki client.
44
+
45
+ Args:
46
+ base_url: Base URL of the Loki server (e.g., "https://loki.example.com").
47
+ auth: Optional tuple of (username, password) for basic authentication.
48
+ org_id: Optional X-Scope-OrgID header for multi-tenant Loki setups.
49
+ ca_cert: Optional path to CA certificate PEM file for self-signed certs.
50
+ verify_ssl: Whether to verify SSL certificates. Set False to disable (insecure).
51
+ timeout: Request timeout in seconds.
52
+ """
53
+ self.base_url = base_url.rstrip("/")
54
+ self.auth = auth
55
+ self.org_id = org_id
56
+ self.ca_cert = ca_cert
57
+ self.verify_ssl = verify_ssl
58
+ self.timeout = timeout
59
+
60
+ self._session: Optional[requests.Session] = None
61
+
62
+ @property
63
+ def session(self) -> requests.Session:
64
+ """
65
+ Get or create HTTP session with configured authentication and SSL settings.
66
+
67
+ Returns:
68
+ Configured requests.Session instance.
69
+ """
70
+ if self._session is None:
71
+ self._session = requests.Session()
72
+
73
+ if self.auth:
74
+ self._session.auth = self.auth
75
+
76
+ if self.org_id:
77
+ self._session.headers["X-Scope-OrgID"] = self.org_id
78
+
79
+ if self.ca_cert:
80
+ self._session.verify = self.ca_cert
81
+ else:
82
+ self._session.verify = self.verify_ssl
83
+
84
+ return self._session
85
+
86
+ def _request(self, method: str, endpoint: str, params: Optional[dict] = None) -> dict:
87
+ """
88
+ Make HTTP request to Loki API.
89
+
90
+ Args:
91
+ method: HTTP method (GET, POST, etc.).
92
+ endpoint: API endpoint path.
93
+ params: Optional query parameters.
94
+
95
+ Returns:
96
+ JSON response as dictionary.
97
+
98
+ Raises:
99
+ LokiConnectionError: If connection to Loki fails.
100
+ LokiAuthError: If authentication fails (401/403).
101
+ LokiQueryError: If the query fails or returns an error.
102
+ """
103
+ url = f"{self.base_url}{endpoint}"
104
+
105
+ try:
106
+ response = self.session.request(
107
+ method=method,
108
+ url=url,
109
+ params=params,
110
+ timeout=self.timeout
111
+ )
112
+ except requests.exceptions.SSLError as e:
113
+ raise LokiConnectionError(f"SSL error connecting to Loki: {e}") from e
114
+ except requests.exceptions.ConnectionError as e:
115
+ raise LokiConnectionError(f"Failed to connect to Loki at {url}: {e}") from e
116
+ except requests.exceptions.Timeout as e:
117
+ raise LokiConnectionError(f"Request to Loki timed out: {e}") from e
118
+ except requests.exceptions.RequestException as e:
119
+ raise LokiConnectionError(f"Request to Loki failed: {e}") from e
120
+
121
+ if response.status_code == 401:
122
+ raise LokiAuthError("Authentication failed: invalid credentials")
123
+ if response.status_code == 403:
124
+ raise LokiAuthError("Authorization failed: access denied")
125
+
126
+ if response.status_code != 200:
127
+ raise LokiQueryError(
128
+ f"Loki query failed with status {response.status_code}: {response.text}"
129
+ )
130
+
131
+ try:
132
+ data = response.json()
133
+ except ValueError as e:
134
+ raise LokiQueryError(f"Invalid JSON response from Loki: {e}") from e
135
+
136
+ if data.get("status") == "error":
137
+ error_msg = data.get("error", "Unknown error")
138
+ raise LokiQueryError(f"Loki query error: {error_msg}")
139
+
140
+ return data
141
+
142
+ def query(
143
+ self,
144
+ logql: str,
145
+ time: Optional[int] = None,
146
+ limit: int = 100
147
+ ) -> QueryResult:
148
+ """
149
+ Execute an instant query at a single point in time.
150
+
151
+ Args:
152
+ logql: LogQL query string (e.g., '{job="api"} |= "error"').
153
+ time: Optional timestamp in nanoseconds. Defaults to now.
154
+ limit: Maximum number of entries to return.
155
+
156
+ Returns:
157
+ QueryResult containing matching log streams.
158
+ """
159
+ params = {
160
+ "query": logql,
161
+ "limit": limit
162
+ }
163
+
164
+ if time is not None:
165
+ params["time"] = str(time)
166
+
167
+ response = self._request("GET", "/loki/api/v1/query", params)
168
+ return QueryResult.from_loki_response(response)
169
+
170
+ def query_range(
171
+ self,
172
+ logql: str,
173
+ start: int,
174
+ end: int,
175
+ limit: int = 1000,
176
+ direction: str = "backward"
177
+ ) -> QueryResult:
178
+ """
179
+ Execute a range query across a time period.
180
+
181
+ Args:
182
+ logql: LogQL query string (e.g., '{job="api"} |= "error"').
183
+ start: Start timestamp in nanoseconds.
184
+ end: End timestamp in nanoseconds.
185
+ limit: Maximum number of entries to return.
186
+ direction: Sort direction - "forward" (oldest first) or "backward" (newest first).
187
+
188
+ Returns:
189
+ QueryResult containing matching log streams.
190
+ """
191
+ params = {
192
+ "query": logql,
193
+ "start": str(start),
194
+ "end": str(end),
195
+ "limit": limit,
196
+ "direction": direction
197
+ }
198
+
199
+ response = self._request("GET", "/loki/api/v1/query_range", params)
200
+ return QueryResult.from_loki_response(response)
201
+
202
+ def get_labels(
203
+ self,
204
+ start: Optional[int] = None,
205
+ end: Optional[int] = None
206
+ ) -> list[str]:
207
+ """
208
+ Get list of available label names.
209
+
210
+ Args:
211
+ start: Optional start timestamp in nanoseconds.
212
+ end: Optional end timestamp in nanoseconds.
213
+
214
+ Returns:
215
+ List of label names.
216
+ """
217
+ params = {}
218
+
219
+ if start is not None:
220
+ params["start"] = str(start)
221
+ if end is not None:
222
+ params["end"] = str(end)
223
+
224
+ response = self._request("GET", "/loki/api/v1/labels", params or None)
225
+ return response.get("data", [])
226
+
227
+ def get_label_values(
228
+ self,
229
+ label: str,
230
+ start: Optional[int] = None,
231
+ end: Optional[int] = None
232
+ ) -> list[str]:
233
+ """
234
+ Get list of values for a specific label.
235
+
236
+ Args:
237
+ label: Label name to get values for.
238
+ start: Optional start timestamp in nanoseconds.
239
+ end: Optional end timestamp in nanoseconds.
240
+
241
+ Returns:
242
+ List of label values.
243
+ """
244
+ params = {}
245
+
246
+ if start is not None:
247
+ params["start"] = str(start)
248
+ if end is not None:
249
+ params["end"] = str(end)
250
+
251
+ endpoint = f"/loki/api/v1/label/{label}/values"
252
+ response = self._request("GET", endpoint, params or None)
253
+ return response.get("data", [])
254
+
255
+ def get_series(
256
+ self,
257
+ match: list[str],
258
+ start: Optional[int] = None,
259
+ end: Optional[int] = None
260
+ ) -> list[dict[str, str]]:
261
+ """
262
+ Get list of unique label sets matching the given selectors.
263
+
264
+ Args:
265
+ match: List of stream selectors (e.g., ['{job="api"}', '{container="web"}']).
266
+ start: Optional start timestamp in nanoseconds.
267
+ end: Optional end timestamp in nanoseconds.
268
+
269
+ Returns:
270
+ List of label dictionaries representing unique streams.
271
+ """
272
+ params = {"match[]": match}
273
+
274
+ if start is not None:
275
+ params["start"] = str(start)
276
+ if end is not None:
277
+ params["end"] = str(end)
278
+
279
+ response = self._request("GET", "/loki/api/v1/series", params)
280
+ return response.get("data", [])
281
+
282
+ def close(self) -> None:
283
+ """Close the HTTP session."""
284
+ if self._session is not None:
285
+ self._session.close()
286
+ self._session = None
287
+
288
+ def __enter__(self) -> "LokiClient":
289
+ """Context manager entry."""
290
+ return self
291
+
292
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
293
+ """Context manager exit - closes session."""
294
+ self.close()
@@ -0,0 +1,23 @@
1
+ """
2
+ Custom exceptions for loki-reader-core.
3
+ """
4
+
5
+
6
+ class LokiError(Exception):
7
+ """Base exception for all Loki-related errors."""
8
+ pass
9
+
10
+
11
+ class LokiConnectionError(LokiError):
12
+ """Raised when connection to Loki server fails."""
13
+ pass
14
+
15
+
16
+ class LokiQueryError(LokiError):
17
+ """Raised when a Loki query fails or returns an error."""
18
+ pass
19
+
20
+
21
+ class LokiAuthError(LokiError):
22
+ """Raised when authentication to Loki fails."""
23
+ pass
@@ -0,0 +1,10 @@
1
+ """
2
+ Data models for loki-reader-core.
3
+ """
4
+
5
+ from .log_entry import LogEntry
6
+ from .log_stream import LogStream
7
+ from .query_stats import QueryStats
8
+ from .query_result import QueryResult
9
+
10
+ __all__ = ["LogEntry", "LogStream", "QueryStats", "QueryResult"]
@@ -0,0 +1,63 @@
1
+ """
2
+ LogEntry model representing a single log line from Loki.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass
9
+ class LogEntry:
10
+ """
11
+ A single log entry from a Loki stream.
12
+
13
+ Attributes:
14
+ timestamp: Unix timestamp in nanoseconds when the log was recorded.
15
+ message: The log message content.
16
+ """
17
+
18
+ timestamp: int
19
+ message: str
20
+
21
+ def to_dict(self) -> dict:
22
+ """
23
+ Convert to dictionary for serialization.
24
+
25
+ Returns:
26
+ Dictionary with timestamp and message.
27
+ """
28
+ return {
29
+ "timestamp": self.timestamp,
30
+ "message": self.message
31
+ }
32
+
33
+ @classmethod
34
+ def from_dict(cls, data: dict) -> "LogEntry":
35
+ """
36
+ Create LogEntry from dictionary.
37
+
38
+ Args:
39
+ data: Dictionary with timestamp and message keys.
40
+
41
+ Returns:
42
+ LogEntry instance.
43
+ """
44
+ return cls(
45
+ timestamp=int(data["timestamp"]),
46
+ message=data["message"]
47
+ )
48
+
49
+ @classmethod
50
+ def from_loki_value(cls, value: list) -> "LogEntry":
51
+ """
52
+ Create LogEntry from Loki's [timestamp, message] format.
53
+
54
+ Args:
55
+ value: List of [timestamp_string, message_string] from Loki API.
56
+
57
+ Returns:
58
+ LogEntry instance.
59
+ """
60
+ return cls(
61
+ timestamp=int(value[0]),
62
+ message=value[1]
63
+ )
@@ -0,0 +1,66 @@
1
+ """
2
+ LogStream model representing a stream of logs with common labels.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from .log_entry import LogEntry
8
+
9
+
10
+ @dataclass
11
+ class LogStream:
12
+ """
13
+ A stream of log entries sharing common labels.
14
+
15
+ Attributes:
16
+ labels: Dictionary of label key-value pairs (e.g., {"job": "api", "container": "web"}).
17
+ entries: List of LogEntry objects in this stream.
18
+ """
19
+
20
+ labels: dict[str, str]
21
+ entries: list[LogEntry]
22
+
23
+ def to_dict(self) -> dict:
24
+ """
25
+ Convert to dictionary for serialization.
26
+
27
+ Returns:
28
+ Dictionary with labels and entries.
29
+ """
30
+ return {
31
+ "labels": self.labels,
32
+ "entries": [entry.to_dict() for entry in self.entries]
33
+ }
34
+
35
+ @classmethod
36
+ def from_dict(cls, data: dict) -> "LogStream":
37
+ """
38
+ Create LogStream from dictionary.
39
+
40
+ Args:
41
+ data: Dictionary with labels and entries keys.
42
+
43
+ Returns:
44
+ LogStream instance.
45
+ """
46
+ entries = [LogEntry.from_dict(e) for e in data.get("entries", [])]
47
+ return cls(
48
+ labels=data["labels"],
49
+ entries=entries
50
+ )
51
+
52
+ @classmethod
53
+ def from_loki_stream(cls, stream_data: dict) -> "LogStream":
54
+ """
55
+ Create LogStream from Loki API response format.
56
+
57
+ Args:
58
+ stream_data: Dictionary with 'stream' (labels) and 'values' (log entries).
59
+
60
+ Returns:
61
+ LogStream instance.
62
+ """
63
+ labels = stream_data.get("stream", {})
64
+ values = stream_data.get("values", [])
65
+ entries = [LogEntry.from_loki_value(v) for v in values]
66
+ return cls(labels=labels, entries=entries)
@@ -0,0 +1,88 @@
1
+ """
2
+ QueryResult model representing the result of a Loki query.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from .log_stream import LogStream
8
+ from .query_stats import QueryStats
9
+
10
+
11
+ @dataclass
12
+ class QueryResult:
13
+ """
14
+ Result of a Loki query containing streams and statistics.
15
+
16
+ Attributes:
17
+ status: Response status from Loki (typically "success").
18
+ streams: List of LogStream objects containing the query results.
19
+ stats: Optional QueryStats with execution statistics.
20
+ """
21
+
22
+ status: str
23
+ streams: list[LogStream]
24
+ stats: QueryStats | None
25
+
26
+ def to_dict(self) -> dict:
27
+ """
28
+ Convert to dictionary for serialization.
29
+
30
+ Returns:
31
+ Dictionary with status, streams, and stats.
32
+ """
33
+ return {
34
+ "status": self.status,
35
+ "streams": [stream.to_dict() for stream in self.streams],
36
+ "stats": self.stats.to_dict() if self.stats else None
37
+ }
38
+
39
+ @classmethod
40
+ def from_dict(cls, data: dict) -> "QueryResult":
41
+ """
42
+ Create QueryResult from dictionary.
43
+
44
+ Args:
45
+ data: Dictionary with status, streams, and stats keys.
46
+
47
+ Returns:
48
+ QueryResult instance.
49
+ """
50
+ streams = [LogStream.from_dict(s) for s in data.get("streams", [])]
51
+ stats = QueryStats.from_dict(data["stats"]) if data.get("stats") else None
52
+ return cls(
53
+ status=data["status"],
54
+ streams=streams,
55
+ stats=stats
56
+ )
57
+
58
+ @classmethod
59
+ def from_loki_response(cls, response_data: dict) -> "QueryResult":
60
+ """
61
+ Create QueryResult from Loki API response format.
62
+
63
+ Args:
64
+ response_data: Full response from Loki query API.
65
+
66
+ Returns:
67
+ QueryResult instance.
68
+ """
69
+ status = response_data.get("status", "unknown")
70
+ data = response_data.get("data", {})
71
+
72
+ result_list = data.get("result", [])
73
+ streams = [LogStream.from_loki_stream(s) for s in result_list]
74
+
75
+ stats_data = data.get("stats")
76
+ stats = QueryStats.from_loki_stats(stats_data) if stats_data else None
77
+
78
+ return cls(status=status, streams=streams, stats=stats)
79
+
80
+ @property
81
+ def total_entries(self) -> int:
82
+ """
83
+ Get total number of log entries across all streams.
84
+
85
+ Returns:
86
+ Total entry count.
87
+ """
88
+ return sum(len(stream.entries) for stream in self.streams)
@@ -0,0 +1,69 @@
1
+ """
2
+ QueryStats model representing statistics from a Loki query.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass
9
+ class QueryStats:
10
+ """
11
+ Statistics about a Loki query execution.
12
+
13
+ Attributes:
14
+ bytes_processed: Total bytes processed during the query.
15
+ lines_processed: Total log lines processed during the query.
16
+ exec_time_seconds: Query execution time in seconds.
17
+ """
18
+
19
+ bytes_processed: int
20
+ lines_processed: int
21
+ exec_time_seconds: float
22
+
23
+ def to_dict(self) -> dict:
24
+ """
25
+ Convert to dictionary for serialization.
26
+
27
+ Returns:
28
+ Dictionary with statistics.
29
+ """
30
+ return {
31
+ "bytes_processed": self.bytes_processed,
32
+ "lines_processed": self.lines_processed,
33
+ "exec_time_seconds": self.exec_time_seconds
34
+ }
35
+
36
+ @classmethod
37
+ def from_dict(cls, data: dict) -> "QueryStats":
38
+ """
39
+ Create QueryStats from dictionary.
40
+
41
+ Args:
42
+ data: Dictionary with statistics keys.
43
+
44
+ Returns:
45
+ QueryStats instance.
46
+ """
47
+ return cls(
48
+ bytes_processed=data.get("bytes_processed", 0),
49
+ lines_processed=data.get("lines_processed", 0),
50
+ exec_time_seconds=data.get("exec_time_seconds", 0.0)
51
+ )
52
+
53
+ @classmethod
54
+ def from_loki_stats(cls, stats_data: dict) -> "QueryStats":
55
+ """
56
+ Create QueryStats from Loki API response format.
57
+
58
+ Args:
59
+ stats_data: Statistics object from Loki response.
60
+
61
+ Returns:
62
+ QueryStats instance.
63
+ """
64
+ summary = stats_data.get("summary", {})
65
+ return cls(
66
+ bytes_processed=summary.get("totalBytesProcessed", 0),
67
+ lines_processed=summary.get("totalLinesProcessed", 0),
68
+ exec_time_seconds=summary.get("execTime", 0.0)
69
+ )
@@ -0,0 +1,85 @@
1
+ """
2
+ Timestamp utility functions for working with Loki's nanosecond timestamps.
3
+ """
4
+
5
+ import time
6
+
7
+
8
+ NANOSECONDS_PER_SECOND = 1_000_000_000
9
+ NANOSECONDS_PER_MINUTE = NANOSECONDS_PER_SECOND * 60
10
+ NANOSECONDS_PER_HOUR = NANOSECONDS_PER_MINUTE * 60
11
+
12
+
13
+ def now_ns() -> int:
14
+ """
15
+ Get current time as Unix nanoseconds.
16
+
17
+ Returns:
18
+ Current timestamp in nanoseconds.
19
+ """
20
+ return int(time.time() * NANOSECONDS_PER_SECOND)
21
+
22
+
23
+ def seconds_to_ns(seconds: int) -> int:
24
+ """
25
+ Convert Unix seconds to nanoseconds.
26
+
27
+ Args:
28
+ seconds: Unix timestamp in seconds.
29
+
30
+ Returns:
31
+ Timestamp in nanoseconds.
32
+ """
33
+ return seconds * NANOSECONDS_PER_SECOND
34
+
35
+
36
+ def ns_to_seconds(nanoseconds: int) -> int:
37
+ """
38
+ Convert Unix nanoseconds to seconds.
39
+
40
+ Args:
41
+ nanoseconds: Unix timestamp in nanoseconds.
42
+
43
+ Returns:
44
+ Timestamp in seconds.
45
+ """
46
+ return nanoseconds // NANOSECONDS_PER_SECOND
47
+
48
+
49
+ def minutes_ago_ns(minutes: int) -> int:
50
+ """
51
+ Get timestamp N minutes ago as nanoseconds.
52
+
53
+ Args:
54
+ minutes: Number of minutes in the past.
55
+
56
+ Returns:
57
+ Timestamp in nanoseconds.
58
+ """
59
+ return now_ns() - (minutes * NANOSECONDS_PER_MINUTE)
60
+
61
+
62
+ def hours_ago_ns(hours: int) -> int:
63
+ """
64
+ Get timestamp N hours ago as nanoseconds.
65
+
66
+ Args:
67
+ hours: Number of hours in the past.
68
+
69
+ Returns:
70
+ Timestamp in nanoseconds.
71
+ """
72
+ return now_ns() - (hours * NANOSECONDS_PER_HOUR)
73
+
74
+
75
+ def days_ago_ns(days: int) -> int:
76
+ """
77
+ Get timestamp N days ago as nanoseconds.
78
+
79
+ Args:
80
+ days: Number of days in the past.
81
+
82
+ Returns:
83
+ Timestamp in nanoseconds.
84
+ """
85
+ return now_ns() - (days * NANOSECONDS_PER_HOUR * 24)
@@ -0,0 +1,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: loki-reader-core
3
+ Version: 0.0.1
4
+ Summary: Python library for querying Grafana Loki logs via REST API
5
+ Project-URL: Homepage, https://github.com/jmazzahacks/loki-reader-core
6
+ Project-URL: Issues, https://github.com/jmazzahacks/loki-reader-core/issues
7
+ Author-email: Jason Byteforge <jmazzahacks@gmail.com>
8
+ License: MIT License
9
+ Keywords: grafana,logging,logs,loki,observability
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: requests
15
+ Description-Content-Type: text/markdown
16
+
17
+ # loki-reader-core
18
+
19
+ A lightweight Python library for querying Grafana Loki logs via REST API.
20
+
21
+ ## Features
22
+
23
+ - Simple, intuitive client for Loki's HTTP API
24
+ - Supports `query`, `query_range`, label discovery, and series matching
25
+ - SSL/TLS support including custom CA certificates for self-signed certs
26
+ - Multi-tenant support via `X-Scope-OrgID` header
27
+ - Basic authentication
28
+ - Clean dataclass models with `to_dict()`/`from_dict()` serialization
29
+ - All timestamps as Unix nanoseconds (Loki's native format)
30
+
31
+ ## Installation
32
+
33
+ From PyPI (coming soon):
34
+
35
+ ```bash
36
+ pip install loki-reader-core
37
+ ```
38
+
39
+ From GitHub:
40
+
41
+ ```bash
42
+ pip install loki-reader-core @ git+ssh://git@github.com/jmazzahacks/loki-reader-core.git@main
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ ### Basic Query
48
+
49
+ ```python
50
+ from loki_reader_core import LokiClient
51
+ from loki_reader_core.utils import hours_ago_ns, now_ns
52
+
53
+ client = LokiClient(base_url="https://loki.example.com")
54
+
55
+ result = client.query_range(
56
+ logql='{job="api-server"} |= "error"',
57
+ start=hours_ago_ns(1),
58
+ end=now_ns(),
59
+ limit=500
60
+ )
61
+
62
+ for stream in result.streams:
63
+ print(f"Labels: {stream.labels}")
64
+ for entry in stream.entries:
65
+ print(f" [{entry.timestamp}] {entry.message}")
66
+ ```
67
+
68
+ ### With Authentication and Self-Signed Certificates
69
+
70
+ ```python
71
+ client = LokiClient(
72
+ base_url="https://loki.internal.company.com:8443",
73
+ auth=("username", "password"),
74
+ ca_cert="/path/to/ca.pem"
75
+ )
76
+ ```
77
+
78
+ ### Multi-Tenant Setup
79
+
80
+ ```python
81
+ client = LokiClient(
82
+ base_url="https://loki.example.com",
83
+ org_id="tenant-1"
84
+ )
85
+ ```
86
+
87
+ ### Exploring Labels
88
+
89
+ ```python
90
+ labels = client.get_labels()
91
+ values = client.get_label_values("application")
92
+ series = client.get_series(match=['{application="my-app"}'])
93
+ ```
94
+
95
+ ### Context Manager
96
+
97
+ ```python
98
+ with LokiClient(base_url="https://loki.example.com") as client:
99
+ result = client.query(logql='{job="api"}')
100
+ ```
101
+
102
+ ## API Reference
103
+
104
+ ### LokiClient
105
+
106
+ | Parameter | Type | Default | Description |
107
+ |-----------|------|---------|-------------|
108
+ | `base_url` | `str` | required | Loki server URL |
109
+ | `auth` | `tuple[str, str]` | `None` | Basic auth `(username, password)` |
110
+ | `org_id` | `str` | `None` | `X-Scope-OrgID` for multi-tenant |
111
+ | `ca_cert` | `str` | `None` | Path to CA certificate PEM file |
112
+ | `verify_ssl` | `bool` | `True` | Set `False` to disable SSL verification |
113
+ | `timeout` | `int` | `30` | Request timeout in seconds |
114
+
115
+ ### Methods
116
+
117
+ | Method | Description |
118
+ |--------|-------------|
119
+ | `query(logql, time, limit)` | Instant query at a single point in time |
120
+ | `query_range(logql, start, end, limit, direction)` | Query across a time range |
121
+ | `get_labels(start, end)` | List available label names |
122
+ | `get_label_values(label, start, end)` | List values for a specific label |
123
+ | `get_series(match, start, end)` | List streams matching selectors |
124
+
125
+ ### Timestamp Utilities
126
+
127
+ ```python
128
+ from loki_reader_core.utils import (
129
+ now_ns, # Current time as nanoseconds
130
+ seconds_to_ns, # Convert Unix seconds to nanoseconds
131
+ ns_to_seconds, # Convert nanoseconds to Unix seconds
132
+ minutes_ago_ns, # Timestamp N minutes ago
133
+ hours_ago_ns, # Timestamp N hours ago
134
+ days_ago_ns, # Timestamp N days ago
135
+ )
136
+ ```
137
+
138
+ ## Development
139
+
140
+ ### Setup
141
+
142
+ ```bash
143
+ # Create virtual environment
144
+ python -m venv .
145
+
146
+ # Activate virtual environment
147
+ source bin/activate
148
+
149
+ # Install dependencies
150
+ pip install -r dev-requirements.txt
151
+ pip install -e .
152
+ ```
153
+
154
+ ### Running Tests
155
+
156
+ ```bash
157
+ source bin/activate
158
+ pytest tests/ -v
159
+ ```
160
+
161
+ ### Building and Publishing
162
+
163
+ ```bash
164
+ ./build-publish.sh
165
+ ```
166
+
167
+ ## License
168
+
169
+ MIT
170
+
171
+ ## Author
172
+
173
+ Jason Byteforge (@jmazzahacks)
@@ -0,0 +1,12 @@
1
+ loki_reader_core/__init__.py,sha256=n9Z3uOxrILiZjojQSX_egG1H2gal1aRcZ8nrsqE4b5w,463
2
+ loki_reader_core/client.py,sha256=Sp7-b6mlR6RzcwqU99DmJT0gSpJTkQTvQrzf7g4PT-Y,8959
3
+ loki_reader_core/exceptions.py,sha256=i10P8n46cwKzH-XMI8QBwOx3Hp6fVQDBMlXKR-U3cUU,446
4
+ loki_reader_core/utils.py,sha256=UpveOw4GiHhjSs_he4lMq65yq13x5iBjljOXF7Kybds,1720
5
+ loki_reader_core/models/__init__.py,sha256=THIMicK2P594P0evr5eWaobF1hmYYX1FBjq3SwF6Lcg,249
6
+ loki_reader_core/models/log_entry.py,sha256=0lj2pAzMlMvV43fJv_lgwGIgSGwNew-3iEejyzwK-Y8,1410
7
+ loki_reader_core/models/log_stream.py,sha256=Ynq0gdlKa5wfrmyLbKuPYY2fCYoOW2B43-s9KdHNdqM,1717
8
+ loki_reader_core/models/query_result.py,sha256=SJVYGLpMw6MwORCIA5gQpzgkZFSFdgmptNW7angbTNU,2455
9
+ loki_reader_core/models/query_stats.py,sha256=CYuc4Jev2NINby9hrTi4AqUtWt2D0Zzt_bdrcnN6t3k,1869
10
+ loki_reader_core-0.0.1.dist-info/METADATA,sha256=jc3ByLUT3JdvXlxQJRwyz5oGIHt0BnkvxMOoFTEgRj8,4224
11
+ loki_reader_core-0.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ loki_reader_core-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any