infrawatch-sdk 0.1.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.
- infrawatch_sdk-0.1.0/.gitignore +41 -0
- infrawatch_sdk-0.1.0/PKG-INFO +85 -0
- infrawatch_sdk-0.1.0/README.md +59 -0
- infrawatch_sdk-0.1.0/infrawatch/__init__.py +23 -0
- infrawatch_sdk-0.1.0/infrawatch/client.py +305 -0
- infrawatch_sdk-0.1.0/infrawatch/exceptions.py +41 -0
- infrawatch_sdk-0.1.0/infrawatch/logs.py +119 -0
- infrawatch_sdk-0.1.0/infrawatch/metrics.py +110 -0
- infrawatch_sdk-0.1.0/infrawatch/otlp.py +250 -0
- infrawatch_sdk-0.1.0/infrawatch/traces.py +127 -0
- infrawatch_sdk-0.1.0/infrawatch/version.py +3 -0
- infrawatch_sdk-0.1.0/pyproject.toml +49 -0
- infrawatch_sdk-0.1.0/tests/__init__.py +0 -0
- infrawatch_sdk-0.1.0/tests/test_client.py +431 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Binaries
|
|
2
|
+
bin/
|
|
3
|
+
/dist/
|
|
4
|
+
infrawatch-agent
|
|
5
|
+
infrawatch-collector
|
|
6
|
+
|
|
7
|
+
# OCB generated (regenerated by `make generate`)
|
|
8
|
+
/cmd/agent/
|
|
9
|
+
/cmd/collector/
|
|
10
|
+
|
|
11
|
+
# Go
|
|
12
|
+
*.exe
|
|
13
|
+
*.exe~
|
|
14
|
+
*.dll
|
|
15
|
+
*.so
|
|
16
|
+
*.dylib
|
|
17
|
+
*.test
|
|
18
|
+
*.out
|
|
19
|
+
go.work.sum
|
|
20
|
+
|
|
21
|
+
# IDE
|
|
22
|
+
.idea/
|
|
23
|
+
.vscode/
|
|
24
|
+
*.swp
|
|
25
|
+
*.swo
|
|
26
|
+
*~
|
|
27
|
+
|
|
28
|
+
# OS
|
|
29
|
+
.DS_Store
|
|
30
|
+
Thumbs.db
|
|
31
|
+
|
|
32
|
+
# Environment
|
|
33
|
+
.env
|
|
34
|
+
.env.local
|
|
35
|
+
|
|
36
|
+
# SDK build artifacts
|
|
37
|
+
sdk/node/node_modules/
|
|
38
|
+
sdk/node/dist/
|
|
39
|
+
sdk/python/*.egg-info/
|
|
40
|
+
sdk/python/.pytest_cache/
|
|
41
|
+
__pycache__/
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: infrawatch-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the InfraWatch observability platform
|
|
5
|
+
Project-URL: Homepage, https://infrawatchlabs.com
|
|
6
|
+
Project-URL: Documentation, https://docs.infrawatchlabs.com/sdk/python
|
|
7
|
+
Author-email: InfraWatch Labs <support@infrawatchlabs.com>
|
|
8
|
+
License-Expression: LicenseRef-Proprietary
|
|
9
|
+
Keywords: infrawatch,logs,metrics,monitoring,observability,traces
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: Other/Proprietary License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: System :: Monitoring
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: httpx>=0.24.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest-httpx>=0.21; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# InfraWatch Python SDK
|
|
28
|
+
|
|
29
|
+
Python client library for the [InfraWatch](https://infrawatchlabs.com) observability platform.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install infrawatch-sdk
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from infrawatch import InfraWatchClient
|
|
41
|
+
|
|
42
|
+
client = InfraWatchClient(
|
|
43
|
+
base_url="https://infrawatch.example.com", # Backend URL for queries
|
|
44
|
+
collector_url="https://collector.example.com", # Collector URL for push
|
|
45
|
+
api_key="iw_sk_a3f8b2c1...", # API key for push auth
|
|
46
|
+
email="admin@example.com", # Backend login for queries
|
|
47
|
+
password="secret", # Backend login for queries
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Push traces (uses API key via X-InfraWatch-API-Key header)
|
|
51
|
+
client.traces.push(spans=[
|
|
52
|
+
{
|
|
53
|
+
"name": "http.request",
|
|
54
|
+
"start_time": "2026-01-01T00:00:00Z",
|
|
55
|
+
"end_time": "2026-01-01T00:00:01Z",
|
|
56
|
+
}
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
# Push metrics
|
|
60
|
+
client.metrics.push(metrics=[
|
|
61
|
+
{"name": "cpu.usage", "value": 72.5, "type": "gauge"}
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
# Push logs
|
|
65
|
+
client.logs.push(logs=[
|
|
66
|
+
{"body": "Request processed successfully", "severity": "INFO"}
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
# Query traces (uses email/password session)
|
|
70
|
+
traces = client.traces.search(service="api-gateway")
|
|
71
|
+
|
|
72
|
+
# Health check
|
|
73
|
+
status = client.health()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Authentication
|
|
77
|
+
|
|
78
|
+
The SDK uses two authentication mechanisms:
|
|
79
|
+
|
|
80
|
+
- **Push (to collector):** API key via `X-InfraWatch-API-Key` header. Generate your API key in **Admin > Observability** in the InfraWatch UI.
|
|
81
|
+
- **Query (from backend):** Email/password session cookies. The SDK handles login automatically on the first query.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
Proprietary. See LICENSE for details.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# InfraWatch Python SDK
|
|
2
|
+
|
|
3
|
+
Python client library for the [InfraWatch](https://infrawatchlabs.com) observability platform.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install infrawatch-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from infrawatch import InfraWatchClient
|
|
15
|
+
|
|
16
|
+
client = InfraWatchClient(
|
|
17
|
+
base_url="https://infrawatch.example.com", # Backend URL for queries
|
|
18
|
+
collector_url="https://collector.example.com", # Collector URL for push
|
|
19
|
+
api_key="iw_sk_a3f8b2c1...", # API key for push auth
|
|
20
|
+
email="admin@example.com", # Backend login for queries
|
|
21
|
+
password="secret", # Backend login for queries
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Push traces (uses API key via X-InfraWatch-API-Key header)
|
|
25
|
+
client.traces.push(spans=[
|
|
26
|
+
{
|
|
27
|
+
"name": "http.request",
|
|
28
|
+
"start_time": "2026-01-01T00:00:00Z",
|
|
29
|
+
"end_time": "2026-01-01T00:00:01Z",
|
|
30
|
+
}
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
# Push metrics
|
|
34
|
+
client.metrics.push(metrics=[
|
|
35
|
+
{"name": "cpu.usage", "value": 72.5, "type": "gauge"}
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
# Push logs
|
|
39
|
+
client.logs.push(logs=[
|
|
40
|
+
{"body": "Request processed successfully", "severity": "INFO"}
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
# Query traces (uses email/password session)
|
|
44
|
+
traces = client.traces.search(service="api-gateway")
|
|
45
|
+
|
|
46
|
+
# Health check
|
|
47
|
+
status = client.health()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Authentication
|
|
51
|
+
|
|
52
|
+
The SDK uses two authentication mechanisms:
|
|
53
|
+
|
|
54
|
+
- **Push (to collector):** API key via `X-InfraWatch-API-Key` header. Generate your API key in **Admin > Observability** in the InfraWatch UI.
|
|
55
|
+
- **Query (from backend):** Email/password session cookies. The SDK handles login automatically on the first query.
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
Proprietary. See LICENSE for details.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""InfraWatch SDK -- Python client for the InfraWatch observability platform."""
|
|
2
|
+
|
|
3
|
+
from infrawatch.client import InfraWatchClient
|
|
4
|
+
from infrawatch.exceptions import (
|
|
5
|
+
InfraWatchError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
ConnectionError,
|
|
8
|
+
APIError,
|
|
9
|
+
TimeoutError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
)
|
|
12
|
+
from infrawatch.version import __version__
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"InfraWatchClient",
|
|
16
|
+
"InfraWatchError",
|
|
17
|
+
"AuthenticationError",
|
|
18
|
+
"ConnectionError",
|
|
19
|
+
"APIError",
|
|
20
|
+
"TimeoutError",
|
|
21
|
+
"ValidationError",
|
|
22
|
+
"__version__",
|
|
23
|
+
]
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Main InfraWatch client — two-layer architecture.
|
|
2
|
+
|
|
3
|
+
Layer 1 (Push): OTLP HTTP → Collector (API key auth via X-InfraWatch-API-Key)
|
|
4
|
+
Layer 2 (Query): REST API → Backend (session cookie auth via email/password)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from infrawatch.exceptions import (
|
|
14
|
+
APIError,
|
|
15
|
+
AuthenticationError,
|
|
16
|
+
ConnectionError,
|
|
17
|
+
TimeoutError,
|
|
18
|
+
)
|
|
19
|
+
from infrawatch.traces import TracesAPI
|
|
20
|
+
from infrawatch.metrics import MetricsAPI
|
|
21
|
+
from infrawatch.logs import LogsAPI
|
|
22
|
+
from infrawatch.version import __version__
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_DEFAULT_TIMEOUT = 30
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InfraWatchClient:
|
|
29
|
+
"""Client for the InfraWatch observability platform.
|
|
30
|
+
|
|
31
|
+
Provides two communication layers:
|
|
32
|
+
|
|
33
|
+
* **Push** — send logs, metrics, and traces to the OTel Collector via
|
|
34
|
+
OTLP/HTTP (``collector_url``, authenticated with an API key via
|
|
35
|
+
the ``X-InfraWatch-API-Key`` header).
|
|
36
|
+
* **Query** — read back data from the InfraWatch backend REST API
|
|
37
|
+
(``base_url``, authenticated with email/password session cookies).
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
base_url: Backend URL for queries (e.g. ``https://infrawatch.example.com``).
|
|
41
|
+
collector_url: Collector URL for push (e.g. ``https://collector.example.com``).
|
|
42
|
+
Port 4318 is used for OTLP/HTTP.
|
|
43
|
+
api_key: API key for the collector (generated in Admin > Observability).
|
|
44
|
+
Sent as ``X-InfraWatch-API-Key`` header on push requests.
|
|
45
|
+
email: Email for backend login.
|
|
46
|
+
password: Password for backend login.
|
|
47
|
+
timeout: Request timeout in seconds. Defaults to 30.
|
|
48
|
+
|
|
49
|
+
Example::
|
|
50
|
+
|
|
51
|
+
client = InfraWatchClient(
|
|
52
|
+
base_url="https://infrawatch.example.com",
|
|
53
|
+
collector_url="https://collector.example.com",
|
|
54
|
+
api_key="iw_sk_a3f8b2c1...",
|
|
55
|
+
email="admin@example.com",
|
|
56
|
+
password="secret",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Push (OTLP to collector via API key)
|
|
60
|
+
client.logs.push([{"body": "msg", "severity": "INFO"}])
|
|
61
|
+
|
|
62
|
+
# Query (REST from backend via session)
|
|
63
|
+
logs = client.logs.search(q="error", severity="ERROR", limit=50)
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
base_url: str = "",
|
|
69
|
+
collector_url: str = "",
|
|
70
|
+
api_key: str = "",
|
|
71
|
+
email: str = "",
|
|
72
|
+
password: str = "",
|
|
73
|
+
timeout: int = _DEFAULT_TIMEOUT,
|
|
74
|
+
) -> None:
|
|
75
|
+
if not base_url and not collector_url:
|
|
76
|
+
raise ValueError("At least one of base_url or collector_url is required")
|
|
77
|
+
|
|
78
|
+
self.base_url = base_url.rstrip("/") if base_url else ""
|
|
79
|
+
self.collector_url = collector_url.rstrip("/") if collector_url else ""
|
|
80
|
+
self.api_key = api_key
|
|
81
|
+
self.email = email
|
|
82
|
+
self.password = password
|
|
83
|
+
self.timeout = timeout
|
|
84
|
+
|
|
85
|
+
# Collector HTTP client — used for OTLP push
|
|
86
|
+
collector_headers: dict[str, str] = {
|
|
87
|
+
"Content-Type": "application/json",
|
|
88
|
+
"User-Agent": f"infrawatch-python-sdk/{__version__}",
|
|
89
|
+
}
|
|
90
|
+
if api_key:
|
|
91
|
+
collector_headers["X-InfraWatch-API-Key"] = api_key
|
|
92
|
+
|
|
93
|
+
self._collector_http = httpx.Client(
|
|
94
|
+
headers=collector_headers,
|
|
95
|
+
timeout=self.timeout,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Backend HTTP client — used for REST queries (session cookie auth)
|
|
99
|
+
self._backend_http = httpx.Client(
|
|
100
|
+
headers={
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
"User-Agent": f"infrawatch-python-sdk/{__version__}",
|
|
103
|
+
},
|
|
104
|
+
timeout=self.timeout,
|
|
105
|
+
)
|
|
106
|
+
self._authenticated = False
|
|
107
|
+
|
|
108
|
+
# Sub-APIs
|
|
109
|
+
self.traces = TracesAPI(self)
|
|
110
|
+
self.metrics = MetricsAPI(self)
|
|
111
|
+
self.logs = LogsAPI(self)
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------------
|
|
114
|
+
# Push helpers (OTLP HTTP to collector)
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def _push(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
118
|
+
"""POST an OTLP JSON payload to the collector.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
path: OTLP endpoint path (e.g. ``/v1/logs``).
|
|
122
|
+
payload: OTLP JSON body.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Parsed JSON response (usually empty ``{}`` on success).
|
|
126
|
+
"""
|
|
127
|
+
if not self.collector_url:
|
|
128
|
+
raise ValueError("collector_url is required for push operations")
|
|
129
|
+
|
|
130
|
+
url = f"{self.collector_url}:4318{path}"
|
|
131
|
+
try:
|
|
132
|
+
response = self._collector_http.post(url, json=payload)
|
|
133
|
+
except httpx.TimeoutException as exc:
|
|
134
|
+
raise TimeoutError(f"Push request timed out: {exc}") from exc
|
|
135
|
+
except httpx.ConnectError as exc:
|
|
136
|
+
raise ConnectionError(
|
|
137
|
+
f"Failed to connect to collector at {url}: {exc}"
|
|
138
|
+
) from exc
|
|
139
|
+
|
|
140
|
+
if response.status_code in (401, 403):
|
|
141
|
+
raise AuthenticationError(
|
|
142
|
+
f"Collector auth failed: {response.status_code}",
|
|
143
|
+
status_code=response.status_code,
|
|
144
|
+
)
|
|
145
|
+
if response.status_code >= 400:
|
|
146
|
+
body = response.json() if response.content else None
|
|
147
|
+
raise APIError(
|
|
148
|
+
f"Collector returned {response.status_code}",
|
|
149
|
+
status_code=response.status_code,
|
|
150
|
+
response=body,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if response.status_code == 200 and response.content:
|
|
154
|
+
return response.json()
|
|
155
|
+
return {}
|
|
156
|
+
|
|
157
|
+
# ------------------------------------------------------------------
|
|
158
|
+
# Query helpers (REST API to backend)
|
|
159
|
+
# ------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
def _ensure_authenticated(self) -> None:
|
|
162
|
+
"""Login to the backend if we haven't already."""
|
|
163
|
+
if self._authenticated:
|
|
164
|
+
return
|
|
165
|
+
if not self.base_url:
|
|
166
|
+
raise ValueError("base_url is required for query operations")
|
|
167
|
+
if not self.email or not self.password:
|
|
168
|
+
raise ValueError("email and password are required for query operations")
|
|
169
|
+
|
|
170
|
+
url = f"{self.base_url}/api/auth/login"
|
|
171
|
+
try:
|
|
172
|
+
response = self._backend_http.post(
|
|
173
|
+
url,
|
|
174
|
+
json={"email": self.email, "password": self.password},
|
|
175
|
+
)
|
|
176
|
+
except httpx.TimeoutException as exc:
|
|
177
|
+
raise TimeoutError(f"Login timed out: {exc}") from exc
|
|
178
|
+
except httpx.ConnectError as exc:
|
|
179
|
+
raise ConnectionError(
|
|
180
|
+
f"Failed to connect to backend at {url}: {exc}"
|
|
181
|
+
) from exc
|
|
182
|
+
|
|
183
|
+
if response.status_code in (401, 403):
|
|
184
|
+
raise AuthenticationError(
|
|
185
|
+
"Backend login failed — invalid credentials",
|
|
186
|
+
status_code=response.status_code,
|
|
187
|
+
)
|
|
188
|
+
if response.status_code >= 400:
|
|
189
|
+
raise APIError(
|
|
190
|
+
f"Backend login returned {response.status_code}",
|
|
191
|
+
status_code=response.status_code,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
self._authenticated = True
|
|
195
|
+
|
|
196
|
+
def _query(
|
|
197
|
+
self,
|
|
198
|
+
method: str,
|
|
199
|
+
path: str,
|
|
200
|
+
params: Optional[dict[str, Any]] = None,
|
|
201
|
+
) -> Any:
|
|
202
|
+
"""Make an authenticated request to the backend REST API.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
method: HTTP method.
|
|
206
|
+
path: API path (e.g. ``/api/logs/``).
|
|
207
|
+
params: Optional query parameters.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Parsed JSON response.
|
|
211
|
+
"""
|
|
212
|
+
self._ensure_authenticated()
|
|
213
|
+
|
|
214
|
+
url = f"{self.base_url}{path}"
|
|
215
|
+
# Strip None values from params
|
|
216
|
+
if params:
|
|
217
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
response = self._backend_http.request(method, url, params=params)
|
|
221
|
+
except httpx.TimeoutException as exc:
|
|
222
|
+
raise TimeoutError(f"Query timed out: {exc}") from exc
|
|
223
|
+
except httpx.ConnectError as exc:
|
|
224
|
+
raise ConnectionError(
|
|
225
|
+
f"Failed to connect to backend at {url}: {exc}"
|
|
226
|
+
) from exc
|
|
227
|
+
|
|
228
|
+
if response.status_code in (401, 403):
|
|
229
|
+
# Session may have expired — try re-auth once
|
|
230
|
+
self._authenticated = False
|
|
231
|
+
self._ensure_authenticated()
|
|
232
|
+
try:
|
|
233
|
+
response = self._backend_http.request(method, url, params=params)
|
|
234
|
+
except httpx.TimeoutException as exc:
|
|
235
|
+
raise TimeoutError(f"Query timed out: {exc}") from exc
|
|
236
|
+
|
|
237
|
+
if response.status_code in (401, 403):
|
|
238
|
+
raise AuthenticationError(
|
|
239
|
+
f"Backend auth failed: {response.status_code}",
|
|
240
|
+
status_code=response.status_code,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if response.status_code >= 400:
|
|
244
|
+
body = response.json() if response.content else None
|
|
245
|
+
raise APIError(
|
|
246
|
+
f"Backend returned {response.status_code}",
|
|
247
|
+
status_code=response.status_code,
|
|
248
|
+
response=body,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if response.status_code == 204 or not response.content:
|
|
252
|
+
return {}
|
|
253
|
+
|
|
254
|
+
return response.json()
|
|
255
|
+
|
|
256
|
+
# ------------------------------------------------------------------
|
|
257
|
+
# Health
|
|
258
|
+
# ------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
def health(self) -> dict[str, Any]:
|
|
261
|
+
"""Check health of both the collector and backend.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Dict with ``collector`` and ``backend`` health status.
|
|
265
|
+
"""
|
|
266
|
+
result: dict[str, Any] = {}
|
|
267
|
+
|
|
268
|
+
if self.collector_url:
|
|
269
|
+
try:
|
|
270
|
+
resp = self._collector_http.get(
|
|
271
|
+
f"{self.collector_url}:4318/v1/health"
|
|
272
|
+
)
|
|
273
|
+
result["collector"] = {
|
|
274
|
+
"status": "ok" if resp.status_code < 400 else "error",
|
|
275
|
+
"status_code": resp.status_code,
|
|
276
|
+
}
|
|
277
|
+
except Exception as exc:
|
|
278
|
+
result["collector"] = {"status": "error", "error": str(exc)}
|
|
279
|
+
|
|
280
|
+
if self.base_url:
|
|
281
|
+
try:
|
|
282
|
+
resp = self._backend_http.get(f"{self.base_url}/api/health")
|
|
283
|
+
result["backend"] = {
|
|
284
|
+
"status": "ok" if resp.status_code < 400 else "error",
|
|
285
|
+
"status_code": resp.status_code,
|
|
286
|
+
}
|
|
287
|
+
except Exception as exc:
|
|
288
|
+
result["backend"] = {"status": "error", "error": str(exc)}
|
|
289
|
+
|
|
290
|
+
return result
|
|
291
|
+
|
|
292
|
+
# ------------------------------------------------------------------
|
|
293
|
+
# Lifecycle
|
|
294
|
+
# ------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
def close(self) -> None:
|
|
297
|
+
"""Close the underlying HTTP clients."""
|
|
298
|
+
self._collector_http.close()
|
|
299
|
+
self._backend_http.close()
|
|
300
|
+
|
|
301
|
+
def __enter__(self) -> InfraWatchClient:
|
|
302
|
+
return self
|
|
303
|
+
|
|
304
|
+
def __exit__(self, *args: Any) -> None:
|
|
305
|
+
self.close()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Custom exceptions for the InfraWatch SDK."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class InfraWatchError(Exception):
|
|
5
|
+
"""Base exception for all InfraWatch SDK errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, status_code: int | None = None, response: dict | None = None):
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.message = message
|
|
10
|
+
self.status_code = status_code
|
|
11
|
+
self.response = response
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuthenticationError(InfraWatchError):
|
|
15
|
+
"""Raised when API key is invalid or missing."""
|
|
16
|
+
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConnectionError(InfraWatchError):
|
|
21
|
+
"""Raised when the SDK cannot connect to the InfraWatch backend."""
|
|
22
|
+
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class APIError(InfraWatchError):
|
|
27
|
+
"""Raised when the API returns a non-success status code."""
|
|
28
|
+
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TimeoutError(InfraWatchError):
|
|
33
|
+
"""Raised when a request exceeds the configured timeout."""
|
|
34
|
+
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ValidationError(InfraWatchError):
|
|
39
|
+
"""Raised when input data fails validation before sending."""
|
|
40
|
+
|
|
41
|
+
pass
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Logs API — push (OTLP) and query (REST) methods."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from infrawatch.otlp import build_logs_payload
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from infrawatch.client import InfraWatchClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LogsAPI:
|
|
14
|
+
"""Push and query log data.
|
|
15
|
+
|
|
16
|
+
Accessed via ``InfraWatchClient.logs``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, client: InfraWatchClient) -> None:
|
|
20
|
+
self._client = client
|
|
21
|
+
|
|
22
|
+
# ------------------------------------------------------------------
|
|
23
|
+
# Push (OTLP HTTP → Collector)
|
|
24
|
+
# ------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
def push(
|
|
27
|
+
self,
|
|
28
|
+
logs: list[dict[str, Any]],
|
|
29
|
+
resource_attributes: Optional[dict[str, Any]] = None,
|
|
30
|
+
) -> dict[str, Any]:
|
|
31
|
+
"""Push log records to the collector via OTLP/HTTP.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
logs: List of log dicts. Each may contain:
|
|
35
|
+
``body`` (str), ``severity`` (str), ``attributes`` (dict),
|
|
36
|
+
``timestamp`` (str, nanosecond epoch).
|
|
37
|
+
resource_attributes: Optional resource-level attributes attached
|
|
38
|
+
to the OTLP resource.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Collector response (usually empty on success).
|
|
42
|
+
"""
|
|
43
|
+
payload = build_logs_payload(logs, resource_attributes)
|
|
44
|
+
return self._client._push("/v1/logs", payload)
|
|
45
|
+
|
|
46
|
+
# ------------------------------------------------------------------
|
|
47
|
+
# Query (REST API → Backend)
|
|
48
|
+
# ------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def search(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
q: Optional[str] = None,
|
|
54
|
+
severity: Optional[str] = None,
|
|
55
|
+
service: Optional[str] = None,
|
|
56
|
+
namespace: Optional[str] = None,
|
|
57
|
+
start: Optional[str] = None,
|
|
58
|
+
end: Optional[str] = None,
|
|
59
|
+
limit: Optional[int] = None,
|
|
60
|
+
cursor: Optional[str] = None,
|
|
61
|
+
) -> Any:
|
|
62
|
+
"""Search logs.
|
|
63
|
+
|
|
64
|
+
All parameters are optional and act as filters.
|
|
65
|
+
"""
|
|
66
|
+
return self._client._query("GET", "/api/logs/", params={
|
|
67
|
+
"q": q,
|
|
68
|
+
"severity": severity,
|
|
69
|
+
"service": service,
|
|
70
|
+
"namespace": namespace,
|
|
71
|
+
"start": start,
|
|
72
|
+
"end": end,
|
|
73
|
+
"limit": limit,
|
|
74
|
+
"cursor": cursor,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
def histogram(
|
|
78
|
+
self,
|
|
79
|
+
*,
|
|
80
|
+
start: Optional[str] = None,
|
|
81
|
+
end: Optional[str] = None,
|
|
82
|
+
service: Optional[str] = None,
|
|
83
|
+
severity: Optional[str] = None,
|
|
84
|
+
bucket: Optional[str] = None,
|
|
85
|
+
) -> Any:
|
|
86
|
+
"""Get log count histogram."""
|
|
87
|
+
return self._client._query("GET", "/api/logs/histogram", params={
|
|
88
|
+
"start": start,
|
|
89
|
+
"end": end,
|
|
90
|
+
"service": service,
|
|
91
|
+
"severity": severity,
|
|
92
|
+
"bucket": bucket,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
def tail(
|
|
96
|
+
self,
|
|
97
|
+
*,
|
|
98
|
+
service: Optional[str] = None,
|
|
99
|
+
severity: Optional[str] = None,
|
|
100
|
+
limit: Optional[int] = None,
|
|
101
|
+
) -> Any:
|
|
102
|
+
"""Live-tail recent logs."""
|
|
103
|
+
return self._client._query("GET", "/api/logs/tail", params={
|
|
104
|
+
"service": service,
|
|
105
|
+
"severity": severity,
|
|
106
|
+
"limit": limit,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
def services(self) -> Any:
|
|
110
|
+
"""List services that emit logs."""
|
|
111
|
+
return self._client._query("GET", "/api/logs/services")
|
|
112
|
+
|
|
113
|
+
def namespaces(self) -> Any:
|
|
114
|
+
"""List namespaces."""
|
|
115
|
+
return self._client._query("GET", "/api/logs/namespaces")
|
|
116
|
+
|
|
117
|
+
def clusters(self) -> Any:
|
|
118
|
+
"""List clusters."""
|
|
119
|
+
return self._client._query("GET", "/api/logs/clusters")
|