logiq-sdk 1.0.0__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.
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: logiq-sdk
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK for the LogIQ AI Observability Platform
5
+ License: MIT
6
+ Keywords: monitoring,observability,logging,apm
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: requests>=2.28
10
+ Provides-Extra: asgi
11
+ Provides-Extra: flask
12
+ Requires-Dist: flask>=2.0; extra == "flask"
13
+
14
+ # logiq
15
+
16
+ Official Python SDK for the [LogIQ](https://logiq.thetechvoyager.in) AI observability platform. Batches structured log/error events on a background thread and ships them to your LogIQ project over HTTP.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install logiq-sdk
22
+ ```
23
+
24
+ ## Quick start
25
+
26
+ ```python
27
+ from logiq import Monitor
28
+
29
+ monitor = Monitor(
30
+ api_key="<YOUR_API_KEY>",
31
+ base_url="https://your-logiq-backend.example.com",
32
+ service_name="checkout-service",
33
+ )
34
+
35
+ monitor.info("Order placed", operation="create_order", metadata={"order_id": 123})
36
+
37
+ try:
38
+ ...
39
+ except Exception as exc:
40
+ monitor.capture_exception(exc, operation="create_order")
41
+ ```
42
+
43
+ Only `WARN` and above are sent by default — pass `min_level="INFO"` (or `"DEBUG"`) to `Monitor(...)` to lower the threshold.
44
+
45
+ ## ASGI / Flask middleware
46
+
47
+ ```python
48
+ from logiq import MonitorASGIMiddleware, attach_flask_middleware
49
+
50
+ # FastAPI / Starlette
51
+ app.add_middleware(MonitorASGIMiddleware, monitor=monitor)
52
+
53
+ # Flask
54
+ attach_flask_middleware(app, monitor)
55
+ ```
56
+
57
+ ## Correlation IDs
58
+
59
+ ```python
60
+ from logiq import set_correlation_id, reset_correlation_id, get_correlation_id
61
+
62
+ token = set_correlation_id("req-123")
63
+ try:
64
+ ...
65
+ finally:
66
+ reset_correlation_id(token)
67
+ ```
68
+
69
+ ## Heartbeats
70
+
71
+ ```python
72
+ monitor.heartbeat() # one-off
73
+ monitor.start_heartbeat_loop(30) # periodic, every 30s, stops on monitor.close()
74
+ ```
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,65 @@
1
+ # logiq
2
+
3
+ Official Python SDK for the [LogIQ](https://logiq.thetechvoyager.in) AI observability platform. Batches structured log/error events on a background thread and ships them to your LogIQ project over HTTP.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install logiq-sdk
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ from logiq import Monitor
15
+
16
+ monitor = Monitor(
17
+ api_key="<YOUR_API_KEY>",
18
+ base_url="https://your-logiq-backend.example.com",
19
+ service_name="checkout-service",
20
+ )
21
+
22
+ monitor.info("Order placed", operation="create_order", metadata={"order_id": 123})
23
+
24
+ try:
25
+ ...
26
+ except Exception as exc:
27
+ monitor.capture_exception(exc, operation="create_order")
28
+ ```
29
+
30
+ Only `WARN` and above are sent by default — pass `min_level="INFO"` (or `"DEBUG"`) to `Monitor(...)` to lower the threshold.
31
+
32
+ ## ASGI / Flask middleware
33
+
34
+ ```python
35
+ from logiq import MonitorASGIMiddleware, attach_flask_middleware
36
+
37
+ # FastAPI / Starlette
38
+ app.add_middleware(MonitorASGIMiddleware, monitor=monitor)
39
+
40
+ # Flask
41
+ attach_flask_middleware(app, monitor)
42
+ ```
43
+
44
+ ## Correlation IDs
45
+
46
+ ```python
47
+ from logiq import set_correlation_id, reset_correlation_id, get_correlation_id
48
+
49
+ token = set_correlation_id("req-123")
50
+ try:
51
+ ...
52
+ finally:
53
+ reset_correlation_id(token)
54
+ ```
55
+
56
+ ## Heartbeats
57
+
58
+ ```python
59
+ monitor.heartbeat() # one-off
60
+ monitor.start_heartbeat_loop(30) # periodic, every 30s, stops on monitor.close()
61
+ ```
62
+
63
+ ## License
64
+
65
+ MIT
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "logiq-sdk"
7
+ version = "1.0.0"
8
+ description = "Official Python SDK for the LogIQ AI Observability Platform"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ keywords = ["monitoring", "observability", "logging", "apm"]
13
+ dependencies = [
14
+ "requests>=2.28",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ asgi = [] # MonitorASGIMiddleware ships in the package; no extra deps needed
19
+ flask = ["flask>=2.0"]
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ from logiq.client import Monitor
2
+ from logiq.context import get_correlation_id, reset_correlation_id, set_correlation_id
3
+ from logiq.middleware import MonitorASGIMiddleware, attach_flask_middleware
4
+
5
+ __all__ = [
6
+ "Monitor",
7
+ "MonitorASGIMiddleware",
8
+ "attach_flask_middleware",
9
+ "set_correlation_id",
10
+ "get_correlation_id",
11
+ "reset_correlation_id",
12
+ ]
@@ -0,0 +1,334 @@
1
+ from __future__ import annotations
2
+
3
+ import atexit
4
+ import threading
5
+ import time
6
+ import traceback
7
+ import uuid
8
+ from dataclasses import dataclass
9
+ from typing import Any, Optional
10
+
11
+ import requests
12
+
13
+ from logiq.context import get_correlation_id
14
+
15
+
16
+ _LEVEL_PRIORITY: dict[str, int] = {
17
+ "DEBUG": 0,
18
+ "INFO": 1,
19
+ "WARN": 2,
20
+ "WARNING": 2,
21
+ "ERROR": 3,
22
+ "CRITICAL": 4,
23
+ }
24
+
25
+
26
+ @dataclass
27
+ class _MonitorConfig:
28
+ api_key: str
29
+ base_url: str
30
+ service_name: str | None
31
+ source: str
32
+ batch_size: int
33
+ flush_interval: float
34
+ timeout_seconds: float
35
+ max_retries: int
36
+ retry_backoff_seconds: float
37
+ min_level: str
38
+
39
+
40
+ class Monitor:
41
+ """Client SDK for LogIQ log ingestion.
42
+
43
+ Features:
44
+ - non-blocking batching on background thread
45
+ - retry with exponential backoff
46
+ - request context propagation through correlation IDs
47
+ - exception capture helpers
48
+ - min_level filter: events below the threshold are silently dropped
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ *,
54
+ api_key: str,
55
+ base_url: str,
56
+ service_name: str | None = None,
57
+ source: str = "sdk",
58
+ batch_size: int = 50,
59
+ flush_interval: float = 2.0,
60
+ timeout_seconds: float = 5.0,
61
+ max_retries: int = 3,
62
+ retry_backoff_seconds: float = 0.5,
63
+ min_level: str = "WARN",
64
+ session: Optional[requests.Session] = None,
65
+ start_background: bool = True,
66
+ ) -> None:
67
+ if not api_key:
68
+ raise ValueError("api_key is required")
69
+ if not base_url:
70
+ raise ValueError("base_url is required")
71
+ if batch_size < 1:
72
+ raise ValueError("batch_size must be >= 1")
73
+ if flush_interval <= 0:
74
+ raise ValueError("flush_interval must be > 0")
75
+ if max_retries < 0:
76
+ raise ValueError("max_retries must be >= 0")
77
+ if min_level.upper() not in _LEVEL_PRIORITY:
78
+ raise ValueError(f"min_level must be one of {list(_LEVEL_PRIORITY)}")
79
+
80
+ self._cfg = _MonitorConfig(
81
+ api_key=api_key,
82
+ base_url=base_url.rstrip("/"),
83
+ service_name=service_name,
84
+ source=source,
85
+ batch_size=batch_size,
86
+ flush_interval=flush_interval,
87
+ timeout_seconds=timeout_seconds,
88
+ max_retries=max_retries,
89
+ retry_backoff_seconds=retry_backoff_seconds,
90
+ min_level=min_level.upper(),
91
+ )
92
+
93
+ self._session = session or requests.Session()
94
+ self._lock = threading.Lock()
95
+ self._buffer: list[dict[str, Any]] = []
96
+ self._dead_letter: list[dict[str, Any]] = []
97
+ self._stop_event = threading.Event()
98
+ self._flush_thread: threading.Thread | None = None
99
+
100
+ if start_background:
101
+ self.start()
102
+ atexit.register(self.close)
103
+
104
+ def start(self) -> None:
105
+ if self._flush_thread and self._flush_thread.is_alive():
106
+ return
107
+ self._stop_event.clear()
108
+ self._flush_thread = threading.Thread(
109
+ target=self._flush_loop, name="logiq-flush", daemon=True
110
+ )
111
+ self._flush_thread.start()
112
+
113
+ def start_heartbeat_loop(self, interval: float = 30.0) -> None:
114
+ """Send a periodic heartbeat every *interval* seconds so the service
115
+ stays visible in the Servers dashboard while it is running.
116
+ Stops automatically when the Monitor is closed.
117
+ """
118
+ def _loop() -> None:
119
+ while not self._stop_event.is_set():
120
+ self._stop_event.wait(interval)
121
+ if not self._stop_event.is_set():
122
+ self.heartbeat()
123
+
124
+ t = threading.Thread(target=_loop, name="logiq-heartbeat", daemon=True)
125
+ t.start()
126
+
127
+ def close(self) -> None:
128
+ self._stop_event.set()
129
+ if self._flush_thread and self._flush_thread.is_alive():
130
+ self._flush_thread.join(timeout=2.0)
131
+ self.flush()
132
+
133
+ def dead_letter(self) -> list[dict[str, Any]]:
134
+ with self._lock:
135
+ return list(self._dead_letter)
136
+
137
+ def log(
138
+ self,
139
+ message: str,
140
+ *,
141
+ level: str = "INFO",
142
+ operation: str | None = None,
143
+ status: str | None = None,
144
+ error_type: str | None = None,
145
+ metadata: dict[str, Any] | None = None,
146
+ correlation_id: str | None = None,
147
+ service_name: str | None = None,
148
+ source: str | None = None,
149
+ ) -> None:
150
+ if not message:
151
+ return
152
+ if _LEVEL_PRIORITY.get(level.upper(), 1) < _LEVEL_PRIORITY[self._cfg.min_level]:
153
+ return
154
+
155
+ resolved_correlation = correlation_id or get_correlation_id()
156
+ payload = {
157
+ "service_name": service_name or self._cfg.service_name,
158
+ "operation": operation,
159
+ "level": level.upper(),
160
+ "status": status,
161
+ "message": message,
162
+ "error_type": error_type,
163
+ "correlation_id": resolved_correlation,
164
+ "metadata": metadata,
165
+ "source": source or self._cfg.source,
166
+ }
167
+
168
+ with self._lock:
169
+ self._buffer.append(payload)
170
+ should_flush = len(self._buffer) >= self._cfg.batch_size
171
+
172
+ if should_flush:
173
+ self.flush()
174
+
175
+ def heartbeat(self, service_name: str | None = None) -> None:
176
+ """Register this service with the monitor immediately, bypassing min_level.
177
+
178
+ Useful at startup so the service appears in the Servers dashboard even
179
+ before any real errors occur.
180
+ """
181
+ resolved = service_name or self._cfg.service_name
182
+ payload = {
183
+ "service_name": resolved,
184
+ "level": "INFO",
185
+ "message": f"Service '{resolved}' started",
186
+ "source": self._cfg.source,
187
+ "operation": "startup",
188
+ "status": "started",
189
+ "error_type": None,
190
+ "correlation_id": None,
191
+ "metadata": None,
192
+ }
193
+ with self._lock:
194
+ self._buffer.append(payload)
195
+
196
+ def info(self, message: str, **kwargs: Any) -> None:
197
+ self.log(message, level="INFO", **kwargs)
198
+
199
+ def warn(self, message: str, **kwargs: Any) -> None:
200
+ self.log(message, level="WARN", **kwargs)
201
+
202
+ def error(self, message: str, **kwargs: Any) -> None:
203
+ self.log(message, level="ERROR", **kwargs)
204
+
205
+ def debug(self, message: str, **kwargs: Any) -> None:
206
+ self.log(message, level="DEBUG", **kwargs)
207
+
208
+ def capture_exception(
209
+ self,
210
+ exc: BaseException,
211
+ *,
212
+ operation: str | None = None,
213
+ metadata: dict[str, Any] | None = None,
214
+ correlation_id: str | None = None,
215
+ ) -> None:
216
+ exc_type = type(exc).__name__
217
+ detail = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
218
+ merged_metadata = dict(metadata or {})
219
+ merged_metadata.setdefault("traceback", detail)
220
+
221
+ self.log(
222
+ message=str(exc) or exc_type,
223
+ level="ERROR",
224
+ operation=operation,
225
+ status="error",
226
+ error_type=exc_type,
227
+ metadata=merged_metadata,
228
+ correlation_id=correlation_id,
229
+ )
230
+
231
+ def trace(self, operation: str, *, metadata: dict[str, Any] | None = None):
232
+ return _TraceContext(self, operation=operation, metadata=metadata)
233
+
234
+ def install_excepthook(self) -> None:
235
+ import sys
236
+
237
+ previous_hook = sys.excepthook
238
+
239
+ def _hook(exc_type: type[BaseException], exc: BaseException, tb: Any) -> None:
240
+ self.capture_exception(exc, operation="unhandled_exception")
241
+ self.flush()
242
+ previous_hook(exc_type, exc, tb)
243
+
244
+ sys.excepthook = _hook
245
+
246
+ def flush(self) -> None:
247
+ while True:
248
+ batch = self._drain_batch(self._cfg.batch_size)
249
+ if not batch:
250
+ return
251
+ success = self._send_batch(batch)
252
+ if not success:
253
+ with self._lock:
254
+ self._dead_letter.extend(batch)
255
+
256
+ def _flush_loop(self) -> None:
257
+ while not self._stop_event.is_set():
258
+ self._stop_event.wait(self._cfg.flush_interval)
259
+ if self._stop_event.is_set():
260
+ break
261
+ self.flush()
262
+
263
+ def _drain_batch(self, n: int) -> list[dict[str, Any]]:
264
+ with self._lock:
265
+ if not self._buffer:
266
+ return []
267
+ batch = self._buffer[:n]
268
+ self._buffer = self._buffer[n:]
269
+ return batch
270
+
271
+ def _send_batch(self, batch: list[dict[str, Any]]) -> bool:
272
+ url = f"{self._cfg.base_url}/api/v1/logs"
273
+ attempt = 0
274
+ while attempt <= self._cfg.max_retries:
275
+ attempt += 1
276
+ try:
277
+ resp = self._session.post(
278
+ url,
279
+ json={"logs": batch},
280
+ headers={
281
+ "Content-Type": "application/json",
282
+ "X-API-Key": self._cfg.api_key,
283
+ "Idempotency-Key": str(uuid.uuid4()),
284
+ },
285
+ timeout=self._cfg.timeout_seconds,
286
+ )
287
+ if 200 <= resp.status_code < 300:
288
+ return True
289
+ except requests.RequestException:
290
+ pass
291
+
292
+ if attempt <= self._cfg.max_retries:
293
+ time.sleep(self._cfg.retry_backoff_seconds * (2 ** (attempt - 1)))
294
+
295
+ return False
296
+
297
+
298
+ class _TraceContext:
299
+ def __init__(self, monitor: Monitor, *, operation: str, metadata: dict[str, Any] | None):
300
+ self._monitor = monitor
301
+ self._operation = operation
302
+ self._metadata = metadata or {}
303
+ self._start = 0.0
304
+
305
+ def __enter__(self):
306
+ self._start = time.perf_counter()
307
+ self._monitor.debug(
308
+ message=f"{self._operation} started",
309
+ operation=self._operation,
310
+ metadata=self._metadata,
311
+ status="start",
312
+ )
313
+ return self
314
+
315
+ def __exit__(self, exc_type, exc, _tb):
316
+ duration_ms = int((time.perf_counter() - self._start) * 1000)
317
+ metadata = dict(self._metadata)
318
+ metadata["duration_ms"] = duration_ms
319
+
320
+ if exc is None:
321
+ self._monitor.info(
322
+ message=f"{self._operation} completed",
323
+ operation=self._operation,
324
+ status="ok",
325
+ metadata=metadata,
326
+ )
327
+ return False
328
+
329
+ self._monitor.capture_exception(
330
+ exc,
331
+ operation=self._operation,
332
+ metadata=metadata,
333
+ )
334
+ return False
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ import contextvars
4
+
5
+ _correlation_id_var: contextvars.ContextVar[str | None] = contextvars.ContextVar(
6
+ "logiq_correlation_id", default=None
7
+ )
8
+
9
+
10
+ def set_correlation_id(correlation_id: str | None) -> contextvars.Token:
11
+ return _correlation_id_var.set(correlation_id)
12
+
13
+
14
+ def reset_correlation_id(token: contextvars.Token) -> None:
15
+ _correlation_id_var.reset(token)
16
+
17
+
18
+ def get_correlation_id() -> str | None:
19
+ return _correlation_id_var.get()
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import uuid
5
+ from typing import Any, Callable
6
+
7
+ from logiq.context import reset_correlation_id, set_correlation_id
8
+
9
+
10
+ class MonitorASGIMiddleware:
11
+ """ASGI middleware that propagates correlation IDs and logs request lifecycle."""
12
+
13
+ def __init__(self, app: Callable[..., Any], monitor: Any):
14
+ self.app = app
15
+ self.monitor = monitor
16
+
17
+ async def __call__(self, scope: dict[str, Any], receive: Callable[..., Any], send: Callable[..., Any]) -> None:
18
+ if scope.get("type") != "http":
19
+ await self.app(scope, receive, send)
20
+ return
21
+
22
+ headers = {
23
+ k.decode("latin-1").lower(): v.decode("latin-1")
24
+ for k, v in scope.get("headers", [])
25
+ }
26
+ correlation_id = (
27
+ headers.get("x-request-id")
28
+ or headers.get("x-correlation-id")
29
+ or str(uuid.uuid4())
30
+ )
31
+ token = set_correlation_id(correlation_id)
32
+
33
+ method = scope.get("method", "UNKNOWN")
34
+ path = scope.get("path", "/")
35
+ started = time.perf_counter()
36
+ status_code = 500
37
+
38
+ async def wrapped_send(message: dict[str, Any]) -> None:
39
+ nonlocal status_code
40
+ if message.get("type") == "http.response.start":
41
+ status_code = int(message.get("status", 500))
42
+ await send(message)
43
+
44
+ try:
45
+ await self.app(scope, receive, wrapped_send)
46
+ self.monitor.log(
47
+ message=f"{method} {path}",
48
+ level="INFO" if status_code < 500 else "ERROR",
49
+ operation="http_request",
50
+ status=str(status_code),
51
+ metadata={
52
+ "method": method,
53
+ "path": path,
54
+ "duration_ms": int((time.perf_counter() - started) * 1000),
55
+ },
56
+ correlation_id=correlation_id,
57
+ )
58
+ except Exception as exc:
59
+ self.monitor.capture_exception(
60
+ exc,
61
+ operation="http_request",
62
+ metadata={"method": method, "path": path},
63
+ correlation_id=correlation_id,
64
+ )
65
+ raise
66
+ finally:
67
+ reset_correlation_id(token)
68
+
69
+
70
+ def attach_flask_middleware(app: Any, monitor: Any) -> None:
71
+ """Attach request logging middleware to a Flask app."""
72
+
73
+ try:
74
+ from flask import g, request
75
+ except Exception as exc:
76
+ raise RuntimeError("Flask is not installed. Install flask to use Flask middleware.") from exc
77
+
78
+ @app.before_request
79
+ def _before_request() -> None:
80
+ correlation_id = (
81
+ request.headers.get("X-Request-ID")
82
+ or request.headers.get("X-Correlation-ID")
83
+ or str(uuid.uuid4())
84
+ )
85
+ g._monitor_started = time.perf_counter()
86
+ g._monitor_token = set_correlation_id(correlation_id)
87
+ g._monitor_correlation_id = correlation_id
88
+
89
+ @app.after_request
90
+ def _after_request(response: Any) -> Any:
91
+ started = getattr(g, "_monitor_started", time.perf_counter())
92
+ correlation_id = getattr(g, "_monitor_correlation_id", None)
93
+ monitor.log(
94
+ message=f"{request.method} {request.path}",
95
+ level="INFO" if response.status_code < 500 else "ERROR",
96
+ operation="http_request",
97
+ status=str(response.status_code),
98
+ metadata={
99
+ "method": request.method,
100
+ "path": request.path,
101
+ "duration_ms": int((time.perf_counter() - started) * 1000),
102
+ },
103
+ correlation_id=correlation_id,
104
+ )
105
+ token = getattr(g, "_monitor_token", None)
106
+ if token is not None:
107
+ reset_correlation_id(token)
108
+ return response
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: logiq-sdk
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK for the LogIQ AI Observability Platform
5
+ License: MIT
6
+ Keywords: monitoring,observability,logging,apm
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: requests>=2.28
10
+ Provides-Extra: asgi
11
+ Provides-Extra: flask
12
+ Requires-Dist: flask>=2.0; extra == "flask"
13
+
14
+ # logiq
15
+
16
+ Official Python SDK for the [LogIQ](https://logiq.thetechvoyager.in) AI observability platform. Batches structured log/error events on a background thread and ships them to your LogIQ project over HTTP.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install logiq-sdk
22
+ ```
23
+
24
+ ## Quick start
25
+
26
+ ```python
27
+ from logiq import Monitor
28
+
29
+ monitor = Monitor(
30
+ api_key="<YOUR_API_KEY>",
31
+ base_url="https://your-logiq-backend.example.com",
32
+ service_name="checkout-service",
33
+ )
34
+
35
+ monitor.info("Order placed", operation="create_order", metadata={"order_id": 123})
36
+
37
+ try:
38
+ ...
39
+ except Exception as exc:
40
+ monitor.capture_exception(exc, operation="create_order")
41
+ ```
42
+
43
+ Only `WARN` and above are sent by default — pass `min_level="INFO"` (or `"DEBUG"`) to `Monitor(...)` to lower the threshold.
44
+
45
+ ## ASGI / Flask middleware
46
+
47
+ ```python
48
+ from logiq import MonitorASGIMiddleware, attach_flask_middleware
49
+
50
+ # FastAPI / Starlette
51
+ app.add_middleware(MonitorASGIMiddleware, monitor=monitor)
52
+
53
+ # Flask
54
+ attach_flask_middleware(app, monitor)
55
+ ```
56
+
57
+ ## Correlation IDs
58
+
59
+ ```python
60
+ from logiq import set_correlation_id, reset_correlation_id, get_correlation_id
61
+
62
+ token = set_correlation_id("req-123")
63
+ try:
64
+ ...
65
+ finally:
66
+ reset_correlation_id(token)
67
+ ```
68
+
69
+ ## Heartbeats
70
+
71
+ ```python
72
+ monitor.heartbeat() # one-off
73
+ monitor.start_heartbeat_loop(30) # periodic, every 30s, stops on monitor.close()
74
+ ```
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/logiq/__init__.py
4
+ src/logiq/client.py
5
+ src/logiq/context.py
6
+ src/logiq/middleware.py
7
+ src/logiq_sdk.egg-info/PKG-INFO
8
+ src/logiq_sdk.egg-info/SOURCES.txt
9
+ src/logiq_sdk.egg-info/dependency_links.txt
10
+ src/logiq_sdk.egg-info/requires.txt
11
+ src/logiq_sdk.egg-info/top_level.txt
@@ -0,0 +1,6 @@
1
+ requests>=2.28
2
+
3
+ [asgi]
4
+
5
+ [flask]
6
+ flask>=2.0