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 +17 -0
- logstack/client.py +164 -0
- logstack/middleware.py +102 -0
- logstack_py-1.0.1.dist-info/METADATA +161 -0
- logstack_py-1.0.1.dist-info/RECORD +8 -0
- logstack_py-1.0.1.dist-info/WHEEL +5 -0
- logstack_py-1.0.1.dist-info/licenses/LICENSE +21 -0
- logstack_py-1.0.1.dist-info/top_level.txt +1 -0
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,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
|