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.
@@ -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")