Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/dsw-config/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/dsw-config/dsw/config/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ class _SentryKeys(ConfigKeysContainer):
environment = ConfigKey(
yaml_path=['sentry', 'environment'],
var_names=['SENTRY_ENVIRONMENT'],
default='production',
default='development',
cast=cast_str,
)

Expand Down
54 changes: 52 additions & 2 deletions packages/dsw-config/dsw/config/sentry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import logging
import typing
import uuid

import sentry_sdk
from sentry_sdk.integrations.logging import LoggingIntegration
Expand All @@ -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:
Expand Down Expand Up @@ -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):
Expand All @@ -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
4 changes: 4 additions & 0 deletions packages/dsw-config/tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
73 changes: 71 additions & 2 deletions packages/dsw-config/tests/test_sentry.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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('-', '')
4 changes: 4 additions & 0 deletions packages/dsw-data-seeder/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 5 additions & 6 deletions packages/dsw-data-seeder/dsw/data_seeder/seeder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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='-')
1 change: 1 addition & 0 deletions packages/dsw-document-worker/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 13 additions & 21 deletions packages/dsw-document-worker/dsw/document_worker/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
)

Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions packages/dsw-mailer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading