From 9e342f02667fd15f89d8ae6658cd21f934203706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Such=C3=A1nek?= Date: Fri, 30 Oct 2026 10:12:28 +0100 Subject: [PATCH] feat(config): Align Sentry events with the backend --- packages/dsw-config/CHANGELOG.md | 6 ++ packages/dsw-config/dsw/config/keys.py | 2 +- packages/dsw-config/dsw/config/sentry.py | 54 +++++++++++++- packages/dsw-config/tests/test_parser.py | 4 + packages/dsw-config/tests/test_sentry.py | 73 ++++++++++++++++++- packages/dsw-data-seeder/CHANGELOG.md | 4 + .../dsw-data-seeder/dsw/data_seeder/seeder.py | 11 ++- packages/dsw-document-worker/CHANGELOG.md | 1 + .../dsw/document_worker/worker.py | 34 ++++----- packages/dsw-mailer/CHANGELOG.md | 1 + packages/dsw-mailer/dsw/mailer/mailer.py | 16 ++-- 11 files changed, 163 insertions(+), 43 deletions(-) diff --git a/packages/dsw-config/CHANGELOG.md b/packages/dsw-config/CHANGELOG.md index e2123407..3faf8369 100644 --- a/packages/dsw-config/CHANGELOG.md +++ b/packages/dsw-config/CHANGELOG.md @@ -8,9 +8,15 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added +- `SentryReporter.set_command` / `reset_command` tag events with the persistent command being processed the way the backend does: `function`, `tenantUuid` and `traceUuid` tags, the creator as the user, the command UUID as the trace ID, and `uuid`, `attempts` and `maxAttempts` extras - `read_config` loads the configuration YAML from AWS AppConfig when `AWS_APP_CONFIG` is set (environment `Default`, profile derived from the file name, overridable by `AWS_APP_CONFIG_ENVIRONMENT` / `AWS_APP_CONFIG_PROFILE`), and from the local file otherwise; a warm Lambda reuses its session and re-polls only after the poll interval +### Changed + +- `sentry.environment` (`SENTRY_ENVIRONMENT`) defaults to `development` instead of `production`, as in the backend, so a deployment that does not set it no longer reports as production +- `SentryReporter.initialize` takes `component` (the `component` tag) instead of `prog_name` + ## [4.34.0] ### Added diff --git a/packages/dsw-config/dsw/config/keys.py b/packages/dsw-config/dsw/config/keys.py index 5cf384c9..d0ad71fe 100644 --- a/packages/dsw-config/dsw/config/keys.py +++ b/packages/dsw-config/dsw/config/keys.py @@ -202,7 +202,7 @@ class _SentryKeys(ConfigKeysContainer): environment = ConfigKey( yaml_path=['sentry', 'environment'], var_names=['SENTRY_ENVIRONMENT'], - default='production', + default='development', cast=cast_str, ) diff --git a/packages/dsw-config/dsw/config/sentry.py b/packages/dsw-config/dsw/config/sentry.py index 3c694a0b..76e2bd05 100644 --- a/packages/dsw-config/dsw/config/sentry.py +++ b/packages/dsw-config/dsw/config/sentry.py @@ -2,6 +2,7 @@ import logging import typing +import uuid import sentry_sdk from sentry_sdk.integrations.logging import LoggingIntegration @@ -18,12 +19,21 @@ LOG = logging.getLogger(__name__) +class Command(typing.Protocol): + uuid: str + function: str + tenant_uuid: str + created_by: str | None + attempts: int + max_attempts: int + + class SentryReporter: report = False filters: list[EventProcessor] = [] @classmethod - def initialize(cls, *, config: SentryConfig, prog_name: str, release: str, + def initialize(cls, *, config: SentryConfig, component: str, release: str, breadcrumb_level: int | None = logging.INFO, event_level: int | None = logging.ERROR): if config.enabled and not config.workers_dsn: @@ -52,7 +62,7 @@ def before_send(event, hint): ), ], ) - sentry_sdk.set_tag('component', prog_name) + sentry_sdk.set_tag('component', component) @classmethod def capture_exception(cls, *args, **kwargs): @@ -68,3 +78,43 @@ def capture_message(cls, *args, **kwargs): def set_tags(cls, **tags): if cls.report: sentry_sdk.set_tags(tags) + + @classmethod + def set_command(cls, command: Command, *, tenant_uuid: str | None = None): + """Tag events with the persistent command being processed, the way the backend does. + + The command UUID is the trace UUID of the job (it is the `traceId` in the logs). + """ + if not cls.report: + return + sentry_sdk.set_tags({ + 'function': command.function, + 'tenantUuid': tenant_uuid or command.tenant_uuid, + 'traceUuid': command.uuid, + }) + sentry_sdk.set_extra('uuid', command.uuid) + sentry_sdk.set_extra('attempts', str(command.attempts)) + sentry_sdk.set_extra('maxAttempts', str(command.max_attempts)) + sentry_sdk.set_user({'id': command.created_by} if command.created_by else None) + trace_id = _to_trace_id(command.uuid) + if trace_id is not None: + sentry_sdk.continue_trace({'sentry-trace': f'{trace_id}-{trace_id[:16]}'}) + + @classmethod + def reset_command(cls): + if not cls.report: + return + scope = sentry_sdk.get_isolation_scope() + for tag in ('function', 'tenantUuid', 'traceUuid'): + scope.remove_tag(tag) + for extra in ('uuid', 'attempts', 'maxAttempts'): + scope.remove_extra(extra) + scope.set_user(None) + scope.set_new_propagation_context() + + +def _to_trace_id(value: str) -> str | None: + try: + return uuid.UUID(value).hex + except ValueError: + return None diff --git a/packages/dsw-config/tests/test_parser.py b/packages/dsw-config/tests/test_parser.py index dba0d99c..8a517980 100644 --- a/packages/dsw-config/tests/test_parser.py +++ b/packages/dsw-config/tests/test_parser.py @@ -91,3 +91,7 @@ def test_sentry_defaults(): assert sentry.workers_dsn is None assert sentry.max_breadcrumbs is None assert sentry.traces_sample_rate is None + + +def test_sentry_environment_defaults_to_development(): + assert make_parser().sentry.environment == 'development' diff --git a/packages/dsw-config/tests/test_sentry.py b/packages/dsw-config/tests/test_sentry.py index ecd86632..2489b26a 100644 --- a/packages/dsw-config/tests/test_sentry.py +++ b/packages/dsw-config/tests/test_sentry.py @@ -1,6 +1,8 @@ +import dataclasses import logging import pytest +import sentry_sdk from dsw.config.model import SentryConfig from dsw.config.sentry import SentryReporter @@ -25,7 +27,7 @@ def _reset_reporter(): def test_not_reporting_when_disabled(): SentryReporter.initialize( config=make_config(enabled=False, dsn='https://token@sentry.io/1'), - prog_name='test', + component='test', release='0.0.0', ) assert SentryReporter.report is False @@ -36,8 +38,75 @@ def test_not_reporting_without_dsn(caplog, dsn): with caplog.at_level(logging.WARNING): SentryReporter.initialize( config=make_config(enabled=True, dsn=dsn), - prog_name='test', + component='test', release='0.0.0', ) assert SentryReporter.report is False assert 'no DSN' in caplog.text + + +@dataclasses.dataclass +class FakeCommand: + uuid: str = '8a7d1f0e-3b2c-4d5e-9f60-7182a3b4c5d6' + function: str = 'generateDocument' + tenant_uuid: str = '00000000-0000-0000-0000-000000000000' + created_by: str | None = 'c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f' + attempts: int = 1 + max_attempts: int = 10 + + +@pytest.fixture +def captured(): + events = [] + + def capture(event, _hint): + events.append(event) + return None + + SentryReporter.initialize( + config=make_config(enabled=True, dsn='https://token@sentry.example.org/1'), + component='document_worker', + release='v4.0.0~abc1234', + ) + SentryReporter.filters.append(capture) + yield events + SentryReporter.filters.remove(capture) + sentry_sdk.get_isolation_scope().clear() + sentry_sdk.get_client().close() + + +def test_command_tags_match_backend(captured): + command = FakeCommand() + SentryReporter.set_command(command) + SentryReporter.capture_message('failed') + + event = captured[-1] + assert event['environment'] == 'test' + assert event['release'] == 'v4.0.0~abc1234' + assert event['tags'] == { + 'component': 'document_worker', + 'function': 'generateDocument', + 'tenantUuid': command.tenant_uuid, + 'traceUuid': command.uuid, + } + assert event['user'] == {'id': command.created_by} + assert event['extra']['maxAttempts'] == '10' + assert event['contexts']['trace']['trace_id'] == command.uuid.replace('-', '') + + +def test_command_tenant_override(captured): + SentryReporter.set_command(FakeCommand(), tenant_uuid='t') + SentryReporter.capture_message('failed') + assert captured[-1]['tags']['tenantUuid'] == 't' + + +def test_reset_command(captured): + command = FakeCommand() + SentryReporter.set_command(command) + SentryReporter.reset_command() + SentryReporter.capture_message('failed') + + event = captured[-1] + assert event['tags'] == {'component': 'document_worker'} + assert 'user' not in event + assert event['contexts']['trace']['trace_id'] != command.uuid.replace('-', '') diff --git a/packages/dsw-data-seeder/CHANGELOG.md b/packages/dsw-data-seeder/CHANGELOG.md index db530b41..b5a8014e 100644 --- a/packages/dsw-data-seeder/CHANGELOG.md +++ b/packages/dsw-data-seeder/CHANGELOG.md @@ -11,6 +11,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Lambda handler reads its configuration from AWS AppConfig when `AWS_APP_CONFIG` is set (see `dsw-config`) +### Changed + +- Sentry events carry the same tags as the backend's: `component` is the persistent command component (`data_seeder`), and a command sets `function`, `tenantUuid`, `traceUuid` (the command UUID, the `traceId` of the logs), the user who created it, and its trace; the tenant is the one being seeded, `recipe_name` is `recipeName` and `command_uuid` / `tenant_uuid` are gone + ## [4.34.0] ### Fixed diff --git a/packages/dsw-data-seeder/dsw/data_seeder/seeder.py b/packages/dsw-data-seeder/dsw/data_seeder/seeder.py index afc7333c..416ae0e7 100644 --- a/packages/dsw-data-seeder/dsw/data_seeder/seeder.py +++ b/packages/dsw-data-seeder/dsw/data_seeder/seeder.py @@ -323,7 +323,7 @@ def _init_sentry(self): SentryReporter.initialize( config=self.cfg.sentry, release=BUILD_INFO.version, - prog_name=consts.PROG_NAME, + component=consts.CMD_COMPONENT, ) def _init_extra_connections(self): @@ -339,7 +339,7 @@ def _init_extra_connections(self): ) def _prepare_recipe(self, recipe_name: str): - SentryReporter.set_tags(recipe_name=recipe_name) + SentryReporter.set_tags(recipeName=recipe_name) LOG.info('Loading recipe "%s"', recipe_name) self.recipes = SeedRecipe.load_from_dir(self.workdir) if recipe_name not in self.recipes: @@ -375,8 +375,8 @@ def run_once(self): def work(self, command: PersistentCommand): Context.get().update_trace_id(command.uuid) - SentryReporter.set_tags(command_uuid=command.uuid) tenant_uuid: str = command.body['tenantUuid'] + SentryReporter.set_command(command, tenant_uuid=tenant_uuid) recipe_name: str | None = command.body.get('recipe') LOG.debug('Processing command: %s (tenant: %s, seed: %s)', command.uuid, tenant_uuid, recipe_name) @@ -390,7 +390,7 @@ def work(self, command: PersistentCommand): attempt=command.attempts, ) Context.get().update_trace_id('-') - SentryReporter.set_tags(command_uuid='-') + SentryReporter.reset_command() def process_timeout(self, e: BaseException): LOG.error('Failed with timeout', exc_info=e) @@ -420,7 +420,7 @@ def seed(self, tenant_uuid: str, recipe_name: str, attempt: int = 0): self._execute(tenant_uuid=tenant_uuid) def _execute(self, tenant_uuid: str): - SentryReporter.set_tags(tenant_uuid=tenant_uuid) + SentryReporter.set_tags(tenantUuid=tenant_uuid) # Run SQL scripts app_ctx = Context.get().app phase = 'DB' @@ -487,4 +487,3 @@ def _execute(self, tenant_uuid: str): self.dbs[target].conn_query.connection.commit() finally: LOG.info('Data seeding done') - SentryReporter.set_tags(tenant_uuid='-') diff --git a/packages/dsw-document-worker/CHANGELOG.md b/packages/dsw-document-worker/CHANGELOG.md index 42995661..a7d9f028 100644 --- a/packages/dsw-document-worker/CHANGELOG.md +++ b/packages/dsw-document-worker/CHANGELOG.md @@ -17,6 +17,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `docx-landscape.lua` Pandoc filter (a `landscape` div, or `\landscape` / `\portrait` paragraphs, switch DOCX page orientation) ### Changed +- Sentry events carry the same tags as the backend's: `component` is the persistent command component (`document_worker`), and a command sets `function`, `tenantUuid`, `traceUuid` (the command UUID, the `traceId` of the logs), the user who created it, and its trace; the job tags are camelCase (`documentUuid`, `templateUuid`, `formatUuid`) and `command_uuid` / `tenant_uuid` are gone - Persistent command component (and its notification channel) renamed `doc_worker` → `document_worker`, to match the backend; commands still stored as `doc_worker` are no longer picked up - Update to DT metamodel 19.0 (WEP 24): only document contexts of major version 19 are accepted; any other (e.g. of a document queued before the upgrade) fails the document with "Unsupported document context metamodel version X, expected 19.x. Regenerate the document." (`missing` when there is none) and is not reported to Sentry diff --git a/packages/dsw-document-worker/dsw/document_worker/worker.py b/packages/dsw-document-worker/dsw/document_worker/worker.py index e5c1a6e7..85add265 100644 --- a/packages/dsw-document-worker/dsw/document_worker/worker.py +++ b/packages/dsw-document-worker/dsw/document_worker/worker.py @@ -116,8 +116,8 @@ def get_document(self): SentryReporter.set_tags(phase='fetch') SentryReporter.set_tags( template='?', - template_uuid='?', - format_uuid='?', + templateUuid='?', + formatUuid='?', ) if self.tenant_uuid != consts.NULL_UUID: LOG.info('Limiting to tenant with UUID: %s', self.tenant_uuid) @@ -156,8 +156,8 @@ def prepare_template(self): template_uuid, format_uuid) # update Sentry info SentryReporter.set_tags( - template_uuid=template_uuid, - format_uuid=format_uuid, + templateUuid=template_uuid, + formatUuid=format_uuid, ) # prepare template template = TemplateRegistry.get().prepare_template( @@ -402,7 +402,7 @@ def _init_sentry(self): SentryReporter.initialize( config=self.config.sentry, release=BUILD_INFO.version, - prog_name=consts.PROG_NAME, + component=consts.CMD_COMPONENT, event_level=None, ) @@ -479,38 +479,30 @@ def work(self, command: PersistentCommand): @staticmethod def _work_pot_file(command: PersistentCommand): Context.get().update_trace_id(command.uuid) - SentryReporter.set_tags( - command_uuid=command.uuid, - tenant_uuid=command.tenant_uuid, - phase='pot', - ) + SentryReporter.set_command(command) + SentryReporter.set_tags(phase='pot') LOG.info('Running POT file job #%s', command.uuid) PotFileJob(command=command).run() - SentryReporter.set_tags( - command_uuid='-', - tenant_uuid='-', - phase='done', - ) + SentryReporter.reset_command() + SentryReporter.set_tags(phase='done') Context.get().reset_ids() def _work_document(self, command: PersistentCommand): document_uuid = command.body['document']['uuid'] Context.get().update_trace_id(command.uuid) Context.get().update_document_id(document_uuid) + SentryReporter.set_command(command) SentryReporter.set_tags( - command_uuid=command.uuid, - tenant_uuid=command.tenant_uuid, - document_uuid=document_uuid, + documentUuid=document_uuid, phase='init', ) LOG.info('Running job #%s', command.uuid) self.current_job = Job(command=command, document_uuid=document_uuid) self.current_job.run() self.current_job = None + SentryReporter.reset_command() SentryReporter.set_tags( - command_uuid='-', - tenant_uuid='-', - document_uuid='-', + documentUuid='-', phase='done', ) Context.get().reset_ids() diff --git a/packages/dsw-mailer/CHANGELOG.md b/packages/dsw-mailer/CHANGELOG.md index bb266a77..697686db 100644 --- a/packages/dsw-mailer/CHANGELOG.md +++ b/packages/dsw-mailer/CHANGELOG.md @@ -14,6 +14,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - The default mail POT (Wizard 5.0): `dsw-mailer make-pot` builds one `mail.pot` of all the templates into `templates/locale/mail.pot` (the image has it), and the mailer keeps it current in S3 at `mail-templates/mail.pot`, with its version as object metadata, at startup and with `dsw-mailer sync-default-pot [--force]` or the Lambda event `{"syncDefaultPot": {...}}`; it never downgrades it unless forced and never blocks mail; `mail.syncDefaultPot` (`MAIL_SYNC_DEFAULT_POT`) turns it off ### Changed +- Sentry events carry the same tags as the backend's: `component` is the persistent command component (`mailer`), and a command sets `function`, `tenantUuid`, `traceUuid` (the command UUID, the `traceId` of the logs), the user who created it, and its trace; `command_uuid` / `tenant_uuid` are gone - Mails are translated with the tenant's mail template locales (Wizard 5.0): one mail is sent per recipient, in the mail template locale whose code matches the recipient's UI locale (exact code, then the base language), otherwise the tenant's default one, otherwise `en`, otherwise built-in English; disabled locales are skipped and a recipient who is not a user gets the default - A command with `sendAsOne` sends a single mail to all its recipients in English (the analytics mails); a command without the field, queued before the upgrade, is sent per recipient diff --git a/packages/dsw-mailer/dsw/mailer/mailer.py b/packages/dsw-mailer/dsw/mailer/mailer.py index d261cde4..276c7cbd 100644 --- a/packages/dsw-mailer/dsw/mailer/mailer.py +++ b/packages/dsw-mailer/dsw/mailer/mailer.py @@ -69,7 +69,7 @@ def _init_sentry(self): SentryReporter.initialize( config=self.cfg.sentry, release=BUILD_INFO.version, - prog_name=consts.PROG_NAME, + component=consts.CMD_COMPONENT, ) @staticmethod @@ -207,11 +207,8 @@ def _prepare_custom_templates(self, tenant_uuid: str, destination: pathlib.Path) def work(self, command: PersistentCommand): # init Sentry info - SentryReporter.set_tags( - template='?', - command_uuid=command.uuid, - tenant_uuid=command.tenant_uuid, - ) + SentryReporter.set_command(command) + SentryReporter.set_tags(template='?') Context.get().update_trace_id(command.uuid) # prepare requests = self._get_msg_requests(MailerCommand.load(command)) @@ -221,11 +218,8 @@ def work(self, command: PersistentCommand): SentryReporter.set_tags(template=rq.template_name) self.send(rq, mail_cfg) # reset Sentry info - SentryReporter.set_tags( - template='-', - command_uuid='-', - tenant_uuid='-', - ) + SentryReporter.reset_command() + SentryReporter.set_tags(template='-') Context.get().update_trace_id('-') def process_timeout(self, e: BaseException):