From c2aad8182317d9ca7f56904822855f21e0cc70a5 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Wed, 21 Oct 2026 21:15:19 +0300 Subject: [PATCH 1/3] feat(logs): write_logs on api.logs and the missing hub log routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api.logs.write_logs(entries=[...]) — POST /{version}/logs on the Api (sync and async; also NorbixApi.logs). - hub.logs: get_logs, get_log, clean_logs, get_log_settings, save_log_settings (finding L12), verbs checked against the manifest. - save/test logging integration docstrings say nothing is delivered until the last test passed (L29). Co-Authored-By: Claude Opus 5.5 --- src/norbix_python/api/__init__.py | 3 + src/norbix_python/api/logs.py | 77 ++++++++++++ src/norbix_python/client.py | 2 + src/norbix_python/hub/logs.py | 200 +++++++++++++++++++++++++++++- 4 files changed, 278 insertions(+), 4 deletions(-) create mode 100644 src/norbix_python/api/logs.py diff --git a/src/norbix_python/api/__init__.py b/src/norbix_python/api/__init__.py index 879cc6e..9c4f75a 100644 --- a/src/norbix_python/api/__init__.py +++ b/src/norbix_python/api/__init__.py @@ -5,6 +5,7 @@ from .database import AsyncDatabaseModule, DatabaseModule from .echo import AsyncEchoModule, EchoModule from .files import AsyncFilesModule, FilesModule +from .logs import AsyncLogsModule, LogsModule from .membership import AsyncMembershipModule, MembershipModule from .payments import AsyncPaymentsModule, PaymentsModule from .public import AsyncPublicModule, PublicModule @@ -16,6 +17,7 @@ def __init__(self, transport: Transport) -> None: self.database = DatabaseModule(transport) self.echo = EchoModule(transport) self.files = FilesModule(transport) + self.logs = LogsModule(transport) self.membership = MembershipModule(transport) self.payments = PaymentsModule(transport) self.public = PublicModule(transport) @@ -27,6 +29,7 @@ def __init__(self, transport: AsyncTransport) -> None: self.database = AsyncDatabaseModule(transport) self.echo = AsyncEchoModule(transport) self.files = AsyncFilesModule(transport) + self.logs = AsyncLogsModule(transport) self.membership = AsyncMembershipModule(transport) self.payments = AsyncPaymentsModule(transport) self.public = AsyncPublicModule(transport) diff --git a/src/norbix_python/api/logs.py b/src/norbix_python/api/logs.py new file mode 100644 index 0000000..047d690 --- /dev/null +++ b/src/norbix_python/api/logs.py @@ -0,0 +1,77 @@ +"""Write your app's own log entries on the Api (project scope). + +``write_logs`` sends 1–100 entries in one call. They are queued and the call +returns at once with ``{"accepted": n}``; read them back on the Hub with +``hub.logs.get_logs`` / ``get_log`` (an entry's ``eventCode`` is stored as +``app:``). +""" + +from __future__ import annotations + +from typing import Any + +from ..transport import AsyncTransport, Transport + + +class LogsModule: + def __init__(self, transport: Transport) -> None: + self._transport = transport + + def write_logs( + self, + *, + entries: list[dict[str, Any]], + timeout: float | None = None, + bearer_token: str | None = None, + **request: Any, + ) -> Any: + """POST /{version}/logs + + Write 1–100 log entries. Each entry: ``title`` (required, ≤200), + ``level`` (``"information"`` / ``"warning"`` / ``"error"``), + ``message`` (≤8000), ``eventCode`` (a-z 0-9 ``_ . : -``, ≤64), + ``correlationId`` (letters, digits, ``_ -``, ≤64), ``timestamp`` (an + ISO-8601 UTC string, ≤7 days old, ≤5 minutes ahead) and ``meta`` (a + string map, ≤20 keys). Optional ``projectId``. Gives back + ``{"accepted": n}``. + + Raises ``ValidationError`` (400) for a broken entry or + ``CM-ERRORS-LOGGING-002`` when the Logs module is off, and + ``RateLimitError`` (429, ``CM-ERRORS-LOGGING-003``) when the project's + per-minute quota is used up. The call is not retried. + """ + return self._transport.send( + target="api", + path="/{version}/logs", + method="POST", + path_params={}, + request={"entries": entries, **request}, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + +class AsyncLogsModule: + def __init__(self, transport: AsyncTransport) -> None: + self._transport = transport + + async def write_logs( + self, + *, + entries: list[dict[str, Any]], + timeout: float | None = None, + bearer_token: str | None = None, + **request: Any, + ) -> Any: + """POST /{version}/logs — see :meth:`LogsModule.write_logs`.""" + return await self._transport.send( + target="api", + path="/{version}/logs", + method="POST", + path_params={}, + request={"entries": entries, **request}, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) diff --git a/src/norbix_python/client.py b/src/norbix_python/client.py index 257cff7..6014ef0 100644 --- a/src/norbix_python/client.py +++ b/src/norbix_python/client.py @@ -264,12 +264,14 @@ def __init__( self.membership = api.membership self.echo = api.echo self.files = api.files + self.logs = api.logs self.payments = api.payments self.public = api.public self.Database = self.database self.Membership = self.membership self.Echo = self.echo self.Files = self.files + self.Logs = self.logs self.Payments = self.payments self.Public = self.public diff --git a/src/norbix_python/hub/logs.py b/src/norbix_python/hub/logs.py index f359669..b6df9f5 100644 --- a/src/norbix_python/hub/logs.py +++ b/src/norbix_python/hub/logs.py @@ -101,7 +101,12 @@ def get_logging_integrations(self, *, timeout: float | None = None, bearer_token ) def save_logging_integration(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: - """POST /{version}/logs/integrations""" + """POST /{version}/logs/integrations + + Save (create or update) a logging integration — Datadog, AWS + CloudWatch or MongoDB. Gives back ``{"id": ...}``. Then call + ``test_logging_integration``: nothing is delivered until a test passed. + """ return self._transport.send( target="hub", path="/{version}/logs/integrations", @@ -114,7 +119,12 @@ def save_logging_integration(self, *, timeout: float | None = None, bearer_token ) def test_logging_integration(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: - """POST /{version}/logs/integrations/test""" + """POST /{version}/logs/integrations/test + + Test a logging integration: ``integrationId``. Call it after every + ``save_logging_integration`` — nothing is delivered to an integration + until its last test passed. + """ return self._transport.send( target="hub", path="/{version}/logs/integrations/test", @@ -139,6 +149,92 @@ def get_logs_by_correlation_id(self, *, timeout: float | None = None, bearer_tok bearer_token=bearer_token, ) + def get_logs(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/logs + + The project's log entries, filtered by the optional + ``level``, ``module``, ``logCorrelationId``, ``eventCode``, ``search``, + ``metaKey`` / ``metaValue``, ``fromUtc`` / ``toUtc`` and the cursor + paging fields ``pageSize``, ``startingAfter``, ``endingBefore`` (query). Gives back ``{"list": {"items": [...], ...}}``. + """ + return self._transport.send( + target="hub", + path="/{version}/logs", + method="GET", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def get_log(self, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/logs/{Id} + + One log entry by its id. Gives back ``{"item": entry}``. + """ + return self._transport.send( + target="hub", + path="/{version}/logs/{Id}", + method="GET", + path_params={"Id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def clean_logs(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/logs/clean + + Delete every log entry stored in the project's Norbix Logging integration. + """ + return self._transport.send( + target="hub", + path="/{version}/logs/clean", + method="POST", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def get_log_settings(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/logs/settings + + The project's log flags: ``skipCloudDashboardLogs``, ``skipHttpBodyMeta``, + ``aiChatLoggingEnabled`` and ``hasNorbixLogging``. + """ + return self._transport.send( + target="hub", + path="/{version}/logs/settings", + method="GET", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + def save_log_settings(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/logs/settings + + Change the project's log flags: ``skipCloudDashboardLogs``, + ``skipHttpBodyMeta``, ``aiChatLoggingEnabled`` (each optional; a flag + left out keeps its value). + """ + return self._transport.send( + target="hub", + path="/{version}/logs/settings", + method="POST", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + class AsyncLogsModule: def __init__(self, transport: AsyncTransport) -> None: @@ -236,7 +332,12 @@ async def get_logging_integrations(self, *, timeout: float | None = None, bearer ) async def save_logging_integration(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: - """POST /{version}/logs/integrations""" + """POST /{version}/logs/integrations + + Save (create or update) a logging integration — Datadog, AWS + CloudWatch or MongoDB. Gives back ``{"id": ...}``. Then call + ``test_logging_integration``: nothing is delivered until a test passed. + """ return await self._transport.send( target="hub", path="/{version}/logs/integrations", @@ -249,7 +350,12 @@ async def save_logging_integration(self, *, timeout: float | None = None, bearer ) async def test_logging_integration(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: - """POST /{version}/logs/integrations/test""" + """POST /{version}/logs/integrations/test + + Test a logging integration: ``integrationId``. Call it after every + ``save_logging_integration`` — nothing is delivered to an integration + until its last test passed. + """ return await self._transport.send( target="hub", path="/{version}/logs/integrations/test", @@ -273,3 +379,89 @@ async def get_logs_by_correlation_id(self, *, timeout: float | None = None, bear timeout=timeout, bearer_token=bearer_token, ) + + async def get_logs(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/logs + + The project's log entries, filtered by the optional + ``level``, ``module``, ``logCorrelationId``, ``eventCode``, ``search``, + ``metaKey`` / ``metaValue``, ``fromUtc`` / ``toUtc`` and the cursor + paging fields ``pageSize``, ``startingAfter``, ``endingBefore`` (query). Gives back ``{"list": {"items": [...], ...}}``. + """ + return await self._transport.send( + target="hub", + path="/{version}/logs", + method="GET", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def get_log(self, id: str, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/logs/{Id} + + One log entry by its id. Gives back ``{"item": entry}``. + """ + return await self._transport.send( + target="hub", + path="/{version}/logs/{Id}", + method="GET", + path_params={"Id": id}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def clean_logs(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/logs/clean + + Delete every log entry stored in the project's Norbix Logging integration. + """ + return await self._transport.send( + target="hub", + path="/{version}/logs/clean", + method="POST", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def get_log_settings(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """GET /{version}/logs/settings + + The project's log flags: ``skipCloudDashboardLogs``, ``skipHttpBodyMeta``, + ``aiChatLoggingEnabled`` and ``hasNorbixLogging``. + """ + return await self._transport.send( + target="hub", + path="/{version}/logs/settings", + method="GET", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) + + async def save_log_settings(self, *, timeout: float | None = None, bearer_token: str | None = None, **request: Any) -> Any: + """POST /{version}/logs/settings + + Change the project's log flags: ``skipCloudDashboardLogs``, + ``skipHttpBodyMeta``, ``aiChatLoggingEnabled`` (each optional; a flag + left out keeps its value). + """ + return await self._transport.send( + target="hub", + path="/{version}/logs/settings", + method="POST", + path_params={}, + request=request, + scope="project", + timeout=timeout, + bearer_token=bearer_token, + ) From 958656232b9f23e047fcd69a3d57f4499e1cbb82 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Wed, 21 Oct 2026 21:15:19 +0300 Subject: [PATCH 2/3] test(logs): request shape of write_logs and the new hub log routes Co-Authored-By: Claude Opus 5.5 --- tests/api/test_logs.py | 94 ++++++++++++++++++++++++++++++++++++ tests/hub/test_logs.py | 105 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 tests/api/test_logs.py diff --git a/tests/api/test_logs.py b/tests/api/test_logs.py new file mode 100644 index 0000000..e483842 --- /dev/null +++ b/tests/api/test_logs.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import asyncio +import json +from urllib.parse import urlparse + +import httpx +import pytest + +from norbix_python import AsyncNorbix, Norbix, RateLimitError + +from ..helpers import make_client + + +def test_api_logs_module_surface() -> None: + client, _ = make_client() + assert callable(client.api.logs.write_logs) + + +def test_api_logs_write_logs_request_shape() -> None: + client, transport = make_client(account_id=None) + entries = [ + { + "level": "error", + "title": "Checkout failed", + "message": "card declined", + "eventCode": "checkout.failed", + "correlationId": "req-42", + "timestamp": "2026-10-21T10:00:00Z", + "meta": {"orderId": "ord_1"}, + }, + {"title": "Retry scheduled"}, + ] + client.api.logs.write_logs(entries=entries) + assert transport.last_request is not None + assert transport.last_request['method'] == 'POST' + url = transport.last_request['url'] + assert url.startswith('https://api.') + assert urlparse(url).path == '/v3/logs' + assert json.loads(transport.last_request['body']) == {"entries": entries} + + +def test_api_logs_write_logs_passes_project_id() -> None: + client, transport = make_client(account_id=None) + client.api.logs.write_logs(entries=[{"title": "t"}], projectId="proj_2") + assert transport.last_request is not None + assert json.loads(transport.last_request['body']) == {"entries": [{"title": "t"}], "projectId": "proj_2"} + + +def test_api_logs_write_logs_quota_raises_rate_limit_error() -> None: + calls: list[httpx.Request] = [] + + def handler(request: httpx.Request) -> httpx.Response: + calls.append(request) + return httpx.Response( + 429, + headers={"Retry-After": "12"}, + json={"responseStatus": {"errorCode": "CM-ERRORS-LOGGING-003", "message": "quota used up"}}, + ) + + client = Norbix( + project_id="test-project", + bearer_token="test-token", + http_client=httpx.Client(transport=httpx.MockTransport(handler)), + ) + with pytest.raises(RateLimitError) as info: + client.api.logs.write_logs(entries=[{"title": "t"}]) + assert info.value.http_status == 429 + assert info.value.error_code == "CM-ERRORS-LOGGING-003" + assert len(calls) == 1 # a POST is not retried + + +def test_async_api_logs_write_logs() -> None: + seen: list[httpx.Request] = [] + + def handler(request: httpx.Request) -> httpx.Response: + seen.append(request) + return httpx.Response(200, json={"accepted": 1}) + + async def run() -> object: + client = AsyncNorbix( + project_id='test-project', + bearer_token='test-token', + http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)), + ) + try: + return await client.api.logs.write_logs(entries=[{"title": "t"}]) + finally: + await client.aclose() + + result = asyncio.run(run()) + assert result == {"accepted": 1} + assert [(r.method, urlparse(str(r.url)).path) for r in seen] == [('POST', '/v3/logs')] + assert json.loads(seen[0].content) == {"entries": [{"title": "t"}]} diff --git a/tests/hub/test_logs.py b/tests/hub/test_logs.py index 92fb54b..2fb3781 100644 --- a/tests/hub/test_logs.py +++ b/tests/hub/test_logs.py @@ -1,6 +1,12 @@ from __future__ import annotations -from norbix_python import Norbix, NorbixError +import asyncio +import json +from urllib.parse import parse_qs, urlparse + +import httpx + +from norbix_python import AsyncNorbix, Norbix, NorbixError from ..helpers import make_client @@ -95,3 +101,100 @@ def test_hub_logs_get_logs_by_correlation_id_request_shape() -> None: assert transport.last_request is not None assert transport.last_request['url'].startswith('https://') + +def test_hub_logs_read_and_settings_surface() -> None: + client, _ = make_client() + module = client.hub.logs + assert callable(module.get_logs) + assert callable(module.get_log) + assert callable(module.clean_logs) + assert callable(module.get_log_settings) + assert callable(module.save_log_settings) + + +def test_hub_logs_get_logs_request_shape() -> None: + client, transport = make_client(account_id=None) + client.hub.logs.get_logs(level="error", eventCode="app:order.paid", pageSize=50) + assert transport.last_request is not None + assert transport.last_request['method'] == 'GET' + url = urlparse(transport.last_request['url']) + assert url.path == '/v3/logs' + assert parse_qs(url.query) == {"level": ["error"], "eventCode": ["app:order.paid"], "pageSize": ["50"]} + assert transport.last_request['body'] == '' + + +def test_hub_logs_get_log_request_shape() -> None: + client, transport = make_client(account_id=None) + client.hub.logs.get_log("log_1") + assert transport.last_request is not None + assert transport.last_request['method'] == 'GET' + assert urlparse(transport.last_request['url']).path == '/v3/logs/log_1' + + +def test_hub_logs_clean_logs_request_shape() -> None: + client, transport = make_client(account_id=None) + client.hub.logs.clean_logs() + assert transport.last_request is not None + assert transport.last_request['method'] == 'POST' + assert urlparse(transport.last_request['url']).path == '/v3/logs/clean' + + +def test_hub_logs_get_log_settings_request_shape() -> None: + client, transport = make_client(account_id=None) + client.hub.logs.get_log_settings() + assert transport.last_request is not None + assert transport.last_request['method'] == 'GET' + assert urlparse(transport.last_request['url']).path == '/v3/logs/settings' + + +def test_hub_logs_save_log_settings_request_shape() -> None: + client, transport = make_client(account_id=None) + client.hub.logs.save_log_settings(skipHttpBodyMeta=True, aiChatLoggingEnabled=False) + assert transport.last_request is not None + assert transport.last_request['method'] == 'POST' + assert urlparse(transport.last_request['url']).path == '/v3/logs/settings' + assert json.loads(transport.last_request['body']) == {"skipHttpBodyMeta": True, "aiChatLoggingEnabled": False} + + +def test_hub_logs_test_logging_integration_sends_integration_id() -> None: + client, transport = make_client(account_id=None) + client.hub.logs.test_logging_integration(integrationId="int_1") + assert transport.last_request is not None + assert urlparse(transport.last_request['url']).path == '/v3/logs/integrations/test' + assert json.loads(transport.last_request['body']) == {"integrationId": "int_1"} + + +def test_async_hub_logs_hit_the_routes() -> None: + seen: list[httpx.Request] = [] + + def handler(request: httpx.Request) -> httpx.Response: + seen.append(request) + return httpx.Response(200, json={}) + + async def run() -> None: + client = AsyncNorbix( + project_id='test-project', + bearer_token='test-token', + http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)), + ) + try: + await client.hub.logs.get_logs(level="error") + await client.hub.logs.get_log("log_1") + await client.hub.logs.clean_logs() + await client.hub.logs.get_log_settings() + await client.hub.logs.save_log_settings(skipCloudDashboardLogs=True) + await client.hub.logs.enable_logging() + await client.hub.logs.disable_logging() + finally: + await client.aclose() + + asyncio.run(run()) + assert [(r.method, urlparse(str(r.url)).path) for r in seen] == [ + ('GET', '/v3/logs'), + ('GET', '/v3/logs/log_1'), + ('POST', '/v3/logs/clean'), + ('GET', '/v3/logs/settings'), + ('POST', '/v3/logs/settings'), + ('PUT', '/v3/logs/enable'), + ('PUT', '/v3/logs/disable'), + ] From e11ac4eb10b8257cd01014597d818408fc5b0fd6 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Wed, 21 Oct 2026 21:15:20 +0300 Subject: [PATCH 3/3] docs(logs): api logs page, full hub logs table, test after saving an integration Co-Authored-By: Claude Opus 5.5 --- docs/api/_index.md | 1 + docs/api/logs.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++ docs/hub/_index.md | 2 +- docs/hub/logs.md | 40 ++++++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 docs/api/logs.md diff --git a/docs/api/_index.md b/docs/api/_index.md index aea4a47..f095eff 100644 --- a/docs/api/_index.md +++ b/docs/api/_index.md @@ -6,6 +6,7 @@ | [`database`](./database.md) | 22 | | [`echo`](./echo.md) | 1 | | [`files`](./files.md) | 10 | +| [`logs`](./logs.md) | 1 | | [`membership`](./membership.md) | 18 | | [`payments`](./payments.md) | 4 | | [`public`](./public.md) | 2 | diff --git a/docs/api/logs.md b/docs/api/logs.md new file mode 100644 index 0000000..fc204e8 --- /dev/null +++ b/docs/api/logs.md @@ -0,0 +1,57 @@ +# API · Logs + +Access with `norbix.api.logs`. + +| Method | Verb | Path | Scope | +| --- | --- | --- | --- | +| `write_logs` | `POST` | `/{version}/logs` | `project` | + +Write your app's own log entries. Send 1–100 entries in one call. They are +queued: the call returns at once with `{"accepted": n}`. Read them back on the +Hub with `norbix.hub.logs.get_logs` / `get_log` (see [HUB · Logs](../hub/logs.md)). + +Each entry: + +| Field | Required | Rule | +| --- | --- | --- | +| `title` | yes | ≤ 200 characters | +| `level` | no | `"information"`, `"warning"` or `"error"` | +| `message` | no | ≤ 8000 characters | +| `eventCode` | no | a-z, 0-9, `_ . : -`, ≤ 64; stored as `app:` | +| `correlationId` | no | letters, digits, `_ -`, ≤ 64 | +| `timestamp` | no | ISO-8601 UTC string, at most 7 days old and 5 minutes ahead | +| `meta` | no | string → string map, ≤ 20 keys | + +`projectId` is optional; the client's project is used when it is left out. + +```python +from datetime import datetime, timezone + +result = norbix.api.logs.write_logs(entries=[ + { + "level": "error", + "title": "Checkout failed", + "message": "card declined", + "eventCode": "checkout.failed", + "correlationId": request_id, + "timestamp": datetime.now(timezone.utc).isoformat(), + "meta": {"orderId": order_id}, + }, +]) +print(result["accepted"]) +``` + +`timestamp` must be a string: pass `datetime.isoformat()`, not the `datetime` +itself. + +## Refusals + +| HTTP | Error | When | +| --- | --- | --- | +| 400 | `ValidationError` | an entry breaks a rule above | +| 400 | `ValidationError`, `CM-ERRORS-LOGGING-002` | the Logs module is off for the project | +| 429 | `RateLimitError`, `CM-ERRORS-LOGGING-003` | the project's per-minute quota is used up | + +The SDK does not retry `write_logs` (it is a POST). On a 429 the gateway sends +a `Retry-After` header with the seconds to wait; the SDK does not expose +response headers on errors yet, so wait about a minute before you send again. diff --git a/docs/hub/_index.md b/docs/hub/_index.md index 3064919..35d63e0 100644 --- a/docs/hub/_index.md +++ b/docs/hub/_index.md @@ -10,7 +10,7 @@ | [`email`](./email.md) | 1 | | [`files`](./files.md) | 23 | | [`internal`](./internal.md) | 1 | -| [`logs`](./logs.md) | 9 | +| [`logs`](./logs.md) | 15 | | [`membership`](./membership.md) | 25 | | [`notifications`](./notifications.md) | 117 | | [`payments`](./payments.md) | 16 | diff --git a/docs/hub/logs.md b/docs/hub/logs.md index c5d6df1..a280562 100644 --- a/docs/hub/logs.md +++ b/docs/hub/logs.md @@ -6,6 +6,12 @@ Access with `norbix.hub.logs`. | --- | --- | --- | --- | | `disable_logging` | `PUT` | `/{version}/logs/disable` | `project` | | `enable_logging` | `PUT` | `/{version}/logs/enable` | `project` | +| `get_logs` | `GET` | `/{version}/logs` | `project` | +| `get_log` | `GET` | `/{version}/logs/{Id}` | `project` | +| `get_logs_by_correlation_id` | `GET` | `/{version}/logs/audit` | `project` | +| `clean_logs` | `POST` | `/{version}/logs/clean` | `project` | +| `get_log_settings` | `GET` | `/{version}/logs/settings` | `project` | +| `save_log_settings` | `POST` | `/{version}/logs/settings` | `project` | | `delete_logging_integration` | `DELETE` | `/{version}/logs/integrations/{Id}` | `project` | | `disable_logging_integration` | `PUT` | `/{version}/logs/integrations/{Id}/disable` | `project` | | `enable_logging_integration` | `PUT` | `/{version}/logs/integrations/{Id}/enable` | `project` | @@ -13,3 +19,37 @@ Access with `norbix.hub.logs`. | `get_logging_integrations` | `GET` | `/{version}/logs/integrations` | `project` | | `save_logging_integration` | `POST` | `/{version}/logs/integrations` | `project` | | `test_logging_integration` | `POST` | `/{version}/logs/integrations/test` | `project` | + +Your app writes its own entries on the Api with `norbix.api.logs.write_logs` +(see [API · Logs](../api/logs.md)). + +## Reading logs + +`get_logs` takes the optional filters `level`, `module`, `logCorrelationId`, +`eventCode`, `search`, `metaKey` / `metaValue`, `fromUtc` / `toUtc` and the +cursor paging fields `pageSize`, `startingAfter`, `endingBefore`. An entry your +app wrote with `eventCode="order.paid"` is stored as `app:order.paid`. + +```python +page = norbix.hub.logs.get_logs(level="error", eventCode="app:checkout.failed", pageSize=50) +for entry in page["list"]["items"]: + print(entry["id"], entry["title"]) + +one = norbix.hub.logs.get_log(page["list"]["items"][0]["id"])["item"] + +settings = norbix.hub.logs.get_log_settings() +norbix.hub.logs.save_log_settings(skipHttpBodyMeta=True) + +norbix.hub.logs.clean_logs() # deletes every entry stored in Norbix Logging +``` + +## Logging integrations + +An integration sends the project's logs on to Datadog, AWS CloudWatch or +MongoDB. **After you save an integration, call `test_logging_integration`. +Nothing is delivered to an integration until its last test passed.** + +```python +saved = norbix.hub.logs.save_logging_integration(**datadog_or_cloudwatch_or_mongodb_request) +norbix.hub.logs.test_logging_integration(integrationId=saved["id"]) +```