From 94c864815ee5b722af088e01d8c6371934f705b6 Mon Sep 17 00:00:00 2001 From: Petter Holt Juliussen Date: Tue, 19 Mar 2024 09:19:14 +0100 Subject: [PATCH] Serialize exceptions as string --- CHANGELOG.md | 5 +++++ okdata/aws/status/model.py | 35 ++++++++++++++++++----------------- okdata/aws/status/wrapper.py | 23 ++++++++++++++--------- tests/test_model.py | 5 +++++ tests/test_status.py | 30 +++++++++++++++++++++++++++++- 5 files changed, 71 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1408258..f6c182c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## ?.?.? - 2024-03-19 + +* Status data exceptions are now accurately converted to strings when exported + as JSON. + ## 3.0.0 - 2024-03-18 * Refactored status data helpers to adopt standard dataclasses in place of diff --git a/okdata/aws/status/model.py b/okdata/aws/status/model.py index 41872a5..78e75f3 100644 --- a/okdata/aws/status/model.py +++ b/okdata/aws/status/model.py @@ -18,6 +18,8 @@ class TraceEventStatus(str, Enum): class StatusJSONEncoder(JSONEncoder): def default(self, obj, *args, **kwargs): + if isinstance(obj, Exception): + return str(obj) if isinstance(obj, datetime): return obj.isoformat() return super().default(obj) @@ -77,21 +79,20 @@ class StatusData(BaseModel): exception: Optional[str] = None errors: Optional[List] = None - def __post_init__(self): - # Ensure that `meta` is of type `StatusMeta` if provided as a dictionary. - if isinstance(self.meta, dict): - self.meta = StatusMeta(**self.meta) - - # Ensure that exception data is a string - self.exception = str(self.exception) if self.exception else None - + def __setattr__(self, name, value): # Validate and ensure format of errors - for error in self.errors or []: - if not isinstance(error, dict): - raise TypeError(f"{error} is not a dict.") - if "message" not in error: - raise ValueError("Missing key 'message'.") - if not isinstance(error["message"], dict): - raise TypeError("error['message'] is not a dict.") - if "nb" not in error["message"]: - raise ValueError("Missing key 'nb' in error['message'].") + if name == "errors" and value is not None: + if not isinstance(value, list): + raise TypeError("`errors` must be provided as a list.") + + for error in value: + if not isinstance(error, dict): + raise TypeError(f"{error} is not a dict.") + if "message" not in error: + raise ValueError("Missing key 'message'.") + if not isinstance(error["message"], dict): + raise TypeError("error['message'] is not a dict.") + if "nb" not in error["message"]: + raise ValueError("Missing key 'nb' in error['message'].") + + super().__setattr__(name, value) diff --git a/okdata/aws/status/wrapper.py b/okdata/aws/status/wrapper.py index cd81a91..365e038 100644 --- a/okdata/aws/status/wrapper.py +++ b/okdata/aws/status/wrapper.py @@ -5,7 +5,12 @@ from requests.exceptions import HTTPError -from .model import StatusData, TraceStatus, TraceEventStatus +from .model import ( + StatusData, + StatusMeta, + TraceEventStatus, + TraceStatus, +) from .sdk import Status _status_logger = None @@ -57,13 +62,13 @@ def _status_from_lambda_context(event, context): "trace_id": event.get("execution_name"), "user": authorizer.get("principalId"), "component": os.getenv("SERVICE_NAME"), - "meta": { - "function_name": getattr(context, "function_name", None), - "function_version": getattr(context, "function_version", None), - "function_stage": request_context.get("stage"), - "function_api_id": request_context.get("apiId"), - "git_rev": os.getenv("GIT_REV"), - "git_branch": os.getenv("GIT_BRANCH"), - }, + "meta": StatusMeta( + function_name=getattr(context, "function_name", None), + function_version=getattr(context, "function_version", None), + function_stage=request_context.get("stage"), + function_api_id=request_context.get("apiId"), + git_rev=os.getenv("GIT_REV"), + git_branch=os.getenv("GIT_BRANCH"), + ), } ) diff --git a/tests/test_model.py b/tests/test_model.py index e7bbac7..2517a9f 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -6,6 +6,11 @@ class TestStatusData: + def test_invalid_errors(self): + params = {"errors": "foo"} + with pytest.raises(TypeError): + StatusData(**params) + def test_errors_entry_valid(self): params = {"errors": [OK_ERROR, OK_ERROR]} result = StatusData(**params) diff --git a/tests/test_status.py b/tests/test_status.py index f0de346..d89a0b1 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -1,3 +1,4 @@ +import json import re from copy import deepcopy from unittest.mock import patch @@ -6,7 +7,12 @@ from freezegun import freeze_time from okdata.sdk.config import Config -from okdata.aws.status.model import StatusData, TraceStatus, TraceEventStatus +from okdata.aws.status.model import ( + StatusData, + StatusMeta, + TraceStatus, + TraceEventStatus, +) from okdata.aws.status.sdk import Status from okdata.aws.status.wrapper import _status_from_lambda_context @@ -129,3 +135,25 @@ def test_add_status_data_payload(self, mock_openid, mock_status_api): s = Status(mock_status_data) s.add(domain_id="my-domain-id") assert s.status_data.domain_id == "my-domain-id" + + @freeze_time(utc_now) + def test_status_data_as_json(self, mock_openid, mock_status_api): + s = Status(mock_status_data) + s.add( + domain_id="my-domain-id", + exception=Exception("This did not work as expected"), + user=None, + meta=StatusMeta(function_name="foo-bar"), + ) + assert json.loads(s.status_data.json(exclude_none=True)) == { + "trace_id": "my-trace-id", + "domain": "dataset", + "domain_id": "my-domain-id", + "start_time": utc_now, + "end_time": utc_now, + "trace_status": "CONTINUE", + "trace_event_status": "OK", + "component": "system32", + "meta": {"function_name": "foo-bar"}, + "exception": "This did not work as expected", + }