logstack-py 1.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.
logstack/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """
2
+ LogStack Python SDK
3
+
4
+ A Python SDK for the LogStack logging platform.
5
+ """
6
+
7
+ from .client import LogStackClient, normalize_api_url
8
+ from .middleware import DjangoMiddleware, FastAPIMiddleware, create_fastapi_middleware
9
+
10
+ __version__ = "1.0.1"
11
+ __all__ = [
12
+ "LogStackClient",
13
+ "normalize_api_url",
14
+ "DjangoMiddleware",
15
+ "FastAPIMiddleware",
16
+ "create_fastapi_middleware",
17
+ ]
logstack/client.py ADDED
@@ -0,0 +1,164 @@
1
+ """
2
+ LogStack Client module.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import logging
9
+ import threading
10
+ import time
11
+ from typing import Any, Callable, Dict, List, Optional
12
+
13
+ import requests
14
+
15
+ logger = logging.getLogger("logstack")
16
+
17
+ OnErrorCallback = Callable[[Exception, List[Dict[str, Any]]], None]
18
+
19
+
20
+ def normalize_api_url(raw: str) -> str:
21
+ """Strip trailing slashes and a redundant /v1 suffix."""
22
+ url = raw.rstrip("/")
23
+ if url.endswith("/v1"):
24
+ url = url[:-3]
25
+ return url.rstrip("/")
26
+
27
+
28
+ class LogStackClient:
29
+ """
30
+ Main LogStack client for sending logs to the LogStack API.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ api_key: str,
36
+ api_url: str = "https://api.logstack.tech",
37
+ environment: str = "production",
38
+ flush_interval: float = 5.0,
39
+ batch_size: int = 100,
40
+ on_error: Optional[OnErrorCallback] = None,
41
+ ):
42
+ self.api_key = api_key
43
+ self.api_url = normalize_api_url(api_url)
44
+ self.environment = environment
45
+ self.flush_interval = flush_interval
46
+ self.batch_size = batch_size
47
+ self.on_error = on_error
48
+
49
+ self._batch: List[Dict[str, Any]] = []
50
+ self._lock = threading.Lock()
51
+ self._flush_timer: Optional[threading.Timer] = None
52
+ self._running = True
53
+ self._closed = False
54
+
55
+ self._start_flush_timer()
56
+
57
+ def _start_flush_timer(self) -> None:
58
+ if not self._running:
59
+ return
60
+ if self._flush_timer:
61
+ self._flush_timer.cancel()
62
+
63
+ self._flush_timer = threading.Timer(self.flush_interval, self._flush_callback)
64
+ self._flush_timer.daemon = True
65
+ self._flush_timer.start()
66
+
67
+ def _flush_callback(self) -> None:
68
+ if self._running:
69
+ self.flush()
70
+ self._start_flush_timer()
71
+
72
+ def _add_to_batch(self, level: str, message: str, metadata: Optional[Dict[str, Any]] = None) -> None:
73
+ entry = {
74
+ "level": level,
75
+ "message": message,
76
+ "metadata": metadata or {},
77
+ "source": "python-sdk",
78
+ }
79
+
80
+ batch_to_send: Optional[List[Dict[str, Any]]] = None
81
+ with self._lock:
82
+ if self._closed:
83
+ return
84
+ self._batch.append(entry)
85
+ if len(self._batch) >= self.batch_size:
86
+ batch_to_send = self._batch.copy()
87
+ self._batch.clear()
88
+
89
+ if batch_to_send:
90
+ self._send_batch(batch_to_send)
91
+
92
+ def info(self, message: str, metadata: Optional[Dict[str, Any]] = None) -> None:
93
+ self._add_to_batch("info", message, metadata)
94
+
95
+ def debug(self, message: str, metadata: Optional[Dict[str, Any]] = None) -> None:
96
+ self._add_to_batch("debug", message, metadata)
97
+
98
+ def warn(self, message: str, metadata: Optional[Dict[str, Any]] = None) -> None:
99
+ self._add_to_batch("warn", message, metadata)
100
+
101
+ def error(self, message: str, metadata: Optional[Dict[str, Any]] = None) -> None:
102
+ self._add_to_batch("error", message, metadata)
103
+
104
+ def critical(self, message: str, metadata: Optional[Dict[str, Any]] = None) -> None:
105
+ self._add_to_batch("critical", message, metadata)
106
+
107
+ def fatal(self, message: str, metadata: Optional[Dict[str, Any]] = None) -> None:
108
+ self._add_to_batch("fatal", message, metadata)
109
+ self.flush()
110
+
111
+ def flush(self) -> None:
112
+ with self._lock:
113
+ if not self._batch:
114
+ return
115
+ batch = self._batch.copy()
116
+ self._batch.clear()
117
+
118
+ self._send_batch(batch)
119
+
120
+ def _send_batch(self, batch: List[Dict[str, Any]]) -> None:
121
+ if not batch:
122
+ return
123
+
124
+ payload = {
125
+ "logs": batch,
126
+ "environment": self.environment,
127
+ }
128
+
129
+ try:
130
+ response = requests.post(
131
+ f"{self.api_url}/v1/logs",
132
+ json=payload,
133
+ headers={
134
+ "Content-Type": "application/json",
135
+ "Authorization": f"Bearer {self.api_key}",
136
+ },
137
+ timeout=10,
138
+ )
139
+ if response.status_code not in (200, 201):
140
+ err = requests.HTTPError(
141
+ f"Logstack API error ({response.status_code}): {response.text}",
142
+ response=response,
143
+ )
144
+ raise err
145
+ except requests.RequestException as exc:
146
+ logger.error("Failed to send logs to Logstack: %s", exc)
147
+ if self.on_error:
148
+ self.on_error(exc, batch)
149
+
150
+ def close(self) -> None:
151
+ with self._lock:
152
+ if self._closed:
153
+ return
154
+ self._closed = True
155
+ self._running = False
156
+ if self._flush_timer:
157
+ self._flush_timer.cancel()
158
+ self.flush()
159
+
160
+ def __enter__(self) -> "LogStackClient":
161
+ return self
162
+
163
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
164
+ self.close()
logstack/middleware.py ADDED
@@ -0,0 +1,102 @@
1
+ """
2
+ LogStack middleware for Django and FastAPI.
3
+ """
4
+
5
+ import traceback
6
+ from typing import Any, Callable, Optional
7
+
8
+ from .client import LogStackClient
9
+
10
+
11
+ class DjangoMiddleware:
12
+ """
13
+ Django middleware to automatically log unhandled exceptions.
14
+ """
15
+
16
+ def __init__(self, get_response: Callable, client: Optional[LogStackClient] = None):
17
+ self.get_response = get_response
18
+ self.client = client
19
+
20
+ def __call__(self, request):
21
+ try:
22
+ return self.get_response(request)
23
+ except Exception:
24
+ self._log_exception(request)
25
+ raise
26
+
27
+ def _log_exception(self, request) -> None:
28
+ if not self.client:
29
+ return
30
+
31
+ exc_type, exc_value, exc_traceback = traceback.exc_info()
32
+ traceback_str = "".join(
33
+ traceback.format_exception(exc_type, exc_value, exc_traceback)
34
+ )
35
+
36
+ metadata = {
37
+ "path": request.path,
38
+ "method": request.method,
39
+ "user": str(request.user) if hasattr(request, "user") else "anonymous",
40
+ "ip": self._get_client_ip(request),
41
+ "traceback": traceback_str,
42
+ }
43
+
44
+ self.client.error(
45
+ f"Unhandled exception in {request.path}",
46
+ metadata=metadata,
47
+ )
48
+
49
+ def _get_client_ip(self, request) -> str:
50
+ x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
51
+ if x_forwarded_for:
52
+ return x_forwarded_for.split(",")[0].strip()
53
+ return request.META.get("REMOTE_ADDR", "")
54
+
55
+
56
+ def create_fastapi_middleware(client: LogStackClient):
57
+ """
58
+ Return a Starlette-compatible middleware class that logs unhandled exceptions.
59
+ """
60
+
61
+ try:
62
+ from starlette.middleware.base import BaseHTTPMiddleware
63
+ from starlette.requests import Request
64
+ from starlette.responses import Response
65
+ except ImportError as exc:
66
+ raise ImportError(
67
+ "FastAPI/Starlette is required for create_fastapi_middleware. "
68
+ "Install with: pip install logstack-py[fastapi]"
69
+ ) from exc
70
+
71
+ class LogStackExceptionMiddleware(BaseHTTPMiddleware):
72
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
73
+ try:
74
+ return await call_next(request)
75
+ except Exception as exc:
76
+ metadata = {
77
+ "path": request.url.path,
78
+ "method": request.method,
79
+ "traceback": str(exc),
80
+ }
81
+ client.error(
82
+ f"Unhandled exception in {request.url.path}",
83
+ metadata=metadata,
84
+ )
85
+ raise
86
+
87
+ return LogStackExceptionMiddleware
88
+
89
+
90
+ class FastAPIMiddleware:
91
+ """
92
+ Deprecated alias — use create_fastapi_middleware(client) instead.
93
+ """
94
+
95
+ def __init__(self, app, client: Optional[LogStackClient] = None):
96
+ if client is None:
97
+ raise ValueError("client is required for FastAPIMiddleware")
98
+ middleware_cls = create_fastapi_middleware(client)
99
+ self.app = middleware_cls(app)
100
+
101
+ async def __call__(self, scope, receive, send):
102
+ await self.app(scope, receive, send)
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: logstack-py
3
+ Version: 1.0.1
4
+ Summary: A Python SDK for the Logstack logging platform
5
+ Home-page: https://github.com/Mosesedem/logstack
6
+ Author: Mosesedem
7
+ Author-email: Mosesedem <team@logstack.tech>
8
+ License: MIT
9
+ Project-URL: Homepage, https://github.com/Mosesedem/logstack
10
+ Project-URL: Repository, https://github.com/Mosesedem/logstack
11
+ Project-URL: Issues, https://github.com/Mosesedem/logstack/issues
12
+ Keywords: logstack,logging,logs,monitoring,observability
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.7
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Requires-Python: >=3.7
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: requests>=2.28.0
27
+ Provides-Extra: django
28
+ Requires-Dist: django>=3.2; extra == "django"
29
+ Provides-Extra: fastapi
30
+ Requires-Dist: fastapi>=0.95.0; extra == "fastapi"
31
+ Provides-Extra: async
32
+ Requires-Dist: aiohttp>=3.8.0; extra == "async"
33
+ Dynamic: author
34
+ Dynamic: home-page
35
+ Dynamic: license-file
36
+ Dynamic: requires-python
37
+
38
+ # Logstack Python SDK
39
+
40
+ A Python SDK for the Logstack logging platform.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install logstack-py
46
+ ```
47
+
48
+ ## v1.0.1
49
+
50
+ - Fix batch-flush deadlock when `batch_size` is reached
51
+ - Normalize API URL (strips redundant `/v1` suffix)
52
+ - Optional `on_error` callback; accepts HTTP 201 from ingest API
53
+ - `create_fastapi_middleware(client)` for proper FastAPI/Starlette integration
54
+
55
+ ## Usage
56
+
57
+ ### Basic Usage
58
+
59
+ ```python
60
+ from logstack import LogStackClient
61
+
62
+ # Create a client
63
+ client = LogStackClient(
64
+ api_key="your-api-key",
65
+ environment="production",
66
+ )
67
+
68
+ # Send logs
69
+ client.info("Application started", metadata={"version": "1.0.0"})
70
+ client.error("Database connection failed", metadata={"error": "connection refused"})
71
+
72
+ # Flush and close
73
+ client.close()
74
+ ```
75
+
76
+ ### Using Context Manager
77
+
78
+ ```python
79
+ from logstack import LogStackClient
80
+
81
+ with LogStackClient(api_key="your-api-key") as client:
82
+ client.info("Application started")
83
+ client.error("Something went wrong")
84
+ ```
85
+
86
+ ### Django Integration
87
+
88
+ ```python
89
+ # settings.py
90
+ MIDDLEWARE = [
91
+ # ...
92
+ 'logstack.middleware.DjangoMiddleware',
93
+ # ...
94
+ ]
95
+
96
+ # Or with custom client
97
+ MIDDLEWARE = [
98
+ # ...
99
+ 'logstack.middleware.DjangoMiddleware',
100
+ # ...
101
+ ]
102
+
103
+ # In your code
104
+ from logstack import DjangoMiddleware, LogStackClient
105
+
106
+ client = LogStackClient(api_key="your-api-key")
107
+ middleware = DjangoMiddleware(get_response, client=client)
108
+ ```
109
+
110
+ ### FastAPI Integration
111
+
112
+ ```python
113
+ from fastapi import FastAPI
114
+ from logstack import LogStackClient, FastAPIMiddleware
115
+
116
+ app = FastAPI()
117
+ client = LogStackClient(api_key="your-api-key")
118
+ FastAPIMiddleware(app, client=client)
119
+ ```
120
+
121
+ ## API Reference
122
+
123
+ ### `LogStackClient(api_key, api_url="https://api.logstack.tech", environment="production", flush_interval=5.0, batch_size=100)`
124
+
125
+ Create a new Logstack client.
126
+
127
+ ### `info(message, metadata=None)`
128
+
129
+ Send an info level log.
130
+
131
+ ### `debug(message, metadata=None)`
132
+
133
+ Send a debug level log.
134
+
135
+ ### `warn(message, metadata=None)`
136
+
137
+ Send a warn level log.
138
+
139
+ ### `error(message, metadata=None)`
140
+
141
+ Send an error level log.
142
+
143
+ ### `critical(message, metadata=None)`
144
+
145
+ Send a critical level log.
146
+
147
+ ### `fatal(message, metadata=None)`
148
+
149
+ Send a fatal level log and flush immediately.
150
+
151
+ ### `flush()`
152
+
153
+ Manually flush the batch of logs.
154
+
155
+ ### `close()`
156
+
157
+ Close the client and flush any pending logs.
158
+
159
+ ## License
160
+
161
+ MIT
@@ -0,0 +1,8 @@
1
+ logstack/__init__.py,sha256=o57yeva2-FLcN3AiC5vJUaMqb9HZVSwdWmSNC1tA4QU,385
2
+ logstack/client.py,sha256=G7mupxqyEWMjM2-gO24ZE6fvdPOQiDAxjkyuZgqISGo,4968
3
+ logstack/middleware.py,sha256=RpGD6EYHG2ZsUX1hZ-XPqLi7r8hZquPs-KtVY6x4P2U,3189
4
+ logstack_py-1.0.1.dist-info/licenses/LICENSE,sha256=mk8ItkvPbWO0HU8ePJP-ARQob65rKYUkpwe9daGGIys,1066
5
+ logstack_py-1.0.1.dist-info/METADATA,sha256=N5m7Z2mlGdXB-yWJEVoj7uspkLjYVAklsrm_1-u82qE,3727
6
+ logstack_py-1.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ logstack_py-1.0.1.dist-info/top_level.txt,sha256=mRSQZBXuD3C_9qQA3MWkQ_tg8-G1UMXG66aDZOJWAxc,9
8
+ logstack_py-1.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mosesedem
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ logstack