Coverage for src / sentry_tool / monitoring.py: 100.00%

26 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-02-17 21:46 -0500

1"""Monitoring setup: Sentry error tracking and structlog logging. 

2 

3Logging goes to stderr to keep stdout clean for data output (piping). 

4Sentry is initialized after logging for self-monitoring. 

5DSN resolution order: SENTRY_DSN env var > config file sentry_dsn > hardcoded default. 

6""" 

7 

8import logging 

9import os 

10import sys 

11from typing import Any 

12 

13import sentry_sdk 

14import structlog 

15 

16from sentry_tool.__about__ import __version__ 

17from sentry_tool.config import load_config 

18 

19_LOG_LEVELS: dict[str, int] = { 

20 "debug": logging.DEBUG, 

21 "info": logging.INFO, 

22 "warning": logging.WARNING, 

23 "error": logging.ERROR, 

24 "critical": logging.CRITICAL, 

25} 

26 

27 

28def setup_logging(verbose: bool = False) -> None: 

29 log_level = "debug" if verbose else "info" 

30 

31 structlog.configure( 

32 processors=[ 

33 structlog.contextvars.merge_contextvars, 

34 structlog.processors.add_log_level, 

35 structlog.processors.TimeStamper(fmt="iso"), 

36 structlog.dev.ConsoleRenderer(colors=sys.stderr.isatty()), 

37 ], 

38 wrapper_class=structlog.make_filtering_bound_logger(_LOG_LEVELS[log_level]), 

39 context_class=dict, 

40 logger_factory=structlog.PrintLoggerFactory(file=sys.stderr), 

41 cache_logger_on_first_use=True, 

42 ) 

43 

44 

45def get_logger(name: str | None = None) -> Any: 

46 logger = structlog.get_logger() 

47 if name: 

48 logger = logger.bind(logger=name) 

49 return logger 

50 

51 

52_DEFAULT_DSN = "https://a176b6acecc8529b8f985532d49e2e04@o4508594232426496.ingest.us.sentry.io/4510896961093633" 

53 

54 

55def resolve_dsn() -> str | None: 

56 """Check SENTRY_DSN env var first, then config file sentry_dsn field. 

57 

58 Returns an override DSN if configured, or None to use the hardcoded default. 

59 """ 

60 dsn = os.environ.get("SENTRY_DSN") 

61 if dsn: 

62 return dsn 

63 

64 config = load_config() 

65 return config.sentry_dsn 

66 

67 

68def setup_sentry(environment: str = "local") -> None: 

69 sentry_sdk.init( 

70 dsn=resolve_dsn() or _DEFAULT_DSN, 

71 traces_sample_rate=0.03, 

72 environment=environment, 

73 release=__version__, 

74 attach_stacktrace=True, 

75 send_default_pii=False, 

76 )