Coverage for anaconda_opentelemetry/logging.py: 89%

83 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-11 20:51 +0000

1# -*- coding: utf-8 -*- 

2# SPDX-FileCopyrightText: 2025 Anaconda, Inc 

3# SPDX-License-Identifier: Apache-2.0 

4 

5# logging.py 

6""" 

7Anaconda Telemetry - Logging signal class and EventLogger. 

8""" 

9 

10import inspect 

11import json 

12import logging 

13from time import time_ns 

14from typing import Dict 

15 

16from opentelemetry import _logs 

17from opentelemetry.sdk._logs import Logger, LoggerProvider, LoggingHandler 

18from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, ConsoleLogExporter 

19 

20try: # opentelemetry-sdk >= 1.40.0 removed LogRecord from the public SDK namespace 

21 from opentelemetry.sdk._logs import LogRecord 

22except ImportError: # pragma: no cover - depends on installed SDK version 

23 LogRecord = None 

24 

25from .common import _AnacondaCommon 

26from .config import Configuration as Config 

27from .attributes import ResourceAttributes as Attributes 

28from .exporter_shim import OTLPLogExporterShim 

29from .formatting import AttrDict, EventPayload, log_event_name_key 

30 

31# ``Logger.emit`` gained keyword arguments (body=, attributes=, ...) in 

32# opentelemetry-sdk 1.40.0; before that it took a single positional LogRecord. 

33# This package pins ==1.40.0, but an older SDK already present in the 

34# environment wins at import time, so detect the shape instead of assuming it. 

35_EMIT_TAKES_KWARGS = 'body' in inspect.signature(Logger.emit).parameters 

36 

37 

38class EventLogger: 

39 """ 

40 Emits log records purely as OTel log telemetry, bypassing Python's 

41 logging hierarchy so they never appear in console/file handlers or 

42 interfere with developer log levels. Should be used if the logs 

43 being collected are not developer logs, and correspond to events. 

44 """ 

45 

46 def __init__( 

47 self, 

48 provider: LoggerProvider, 

49 logger_name: str = "event_logger", 

50 ): 

51 self._logger = provider.get_logger(logger_name) 

52 

53 def _send_event( 

54 self, 

55 body: EventPayload, 

56 event_name: str, 

57 attributes: AttrDict={}, 

58 ): 

59 if not isinstance(body, str): 

60 body = json.dumps(body) 

61 # update attributes with event name - mandatory for event logs 

62 # copy first: `attributes` may be a caller-owned dict (or the default {}) 

63 attributes = {**attributes, log_event_name_key: event_name} 

64 if _EMIT_TAKES_KWARGS: 

65 self._logger.emit(body=body, attributes=attributes) 

66 else: 

67 # opentelemetry-sdk < 1.40.0: emit() takes a single LogRecord. 

68 self._logger.emit(LogRecord( 

69 body=body, 

70 attributes=attributes, 

71 observed_timestamp=time_ns(), 

72 )) 

73 

74 

75class _AnacondaLogger(_AnacondaCommon): 

76 # Singleton instance (internal only); provide a logger handler for OpenTelemetry log instrumentation 

77 _instance = None 

78 

79 _default_log_attributes = {log_event_name_key: "__LOG__"} 

80 

81 def __init__(self, config: Config, attributes: Attributes): 

82 super().__init__(config, attributes) 

83 self.log_level = self._get_log_level(config._get_logging_level()) 

84 self.logger_endpoint = config._get_logging_endpoint() 

85 

86 # Create logger provider 

87 self._provider = LoggerProvider(resource=self.resource, shutdown_on_exit=self._shutdown_on_exit) 

88 try: 

89 _logs.set_logger_provider(self._provider) 

90 except Exception: 

91 self.logger.warning("The logger provider was previously set; this call is ignored.") 

92 self._console_exporter: ConsoleLogExporter | None = None 

93 # Add OTLP exporter 

94 if self.use_console_exporters: 

95 exporter = ConsoleLogExporter() 

96 self._console_exporter = exporter 

97 else: 

98 auth_token = config._get_auth_token_logging() 

99 headers: Dict[str, str] = {} 

100 if auth_token is not None: 

101 headers['authorization'] = f'Bearer {auth_token}' 

102 if config._get_request_protocol_logging() in ['grpc', 'grpcs']: # gRPC 

103 from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter as OTLPLogExportergRPC 

104 insecure = not config._get_TLS_logging() 

105 exporter = OTLPLogExporterShim( 

106 OTLPLogExportergRPC, 

107 endpoint=self.logger_endpoint, 

108 insecure=insecure, 

109 credentials=config._get_ca_cert_logging() if not insecure else None, 

110 headers=headers 

111 ) 

112 else: # HTTP 

113 from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter as OTLPLogExporterHTTP 

114 http_kwargs = self._build_http_exporter_kwargs('logging', self.logger_endpoint, headers) 

115 exporter = OTLPLogExporterShim( 

116 OTLPLogExporterHTTP, 

117 **http_kwargs 

118 ) 

119 

120 self.exporter = exporter 

121 self._processor = BatchLogRecordProcessor(self.exporter) 

122 self._provider.add_log_record_processor(self._processor) 

123 

124 def _get_log_handler(self) -> LoggingHandler: 

125 handler = LoggingHandler(level=self.log_level, logger_provider=self._provider) 

126 handler.addFilter(self._set_default_attribute_filter()) 

127 return handler 

128 

129 def _set_default_attribute_filter(self) -> logging.Filter: 

130 attrs = self._default_log_attributes 

131 f = logging.Filter() 

132 def _filter(record): 

133 for k, v in attrs.items(): 

134 if not hasattr(record, k): 

135 setattr(record, k, v) 

136 return True 

137 f.filter = _filter 

138 return f 

139 

140 def _get_event_logger(self, logger_name: str = None) -> EventLogger: 

141 if logger_name is None: 

142 logger_name = f'{self.service_name}_event_logger' 

143 return EventLogger(self._provider, logger_name=logger_name) 

144 

145 def _get_log_level(self, str_level: str)-> int: 

146 # Convert string from config file to logging level. 

147 levels = { 

148 "debug": logging.DEBUG, 

149 "info": logging.INFO, 

150 "warning": logging.WARNING, 

151 "warn": logging.WARNING, 

152 "error": logging.ERROR, 

153 "critical": logging.CRITICAL, 

154 "fatal": logging.CRITICAL 

155 } 

156 return levels.get(str_level.lower(), logging.DEBUG) 

157 

158 def _test_set_console_mock(self, new_out): # For testing only... 

159 if self._console_exporter is not None and new_out is not None: 

160 saved = self._console_exporter.out 

161 self._console_exporter.out = new_out 

162 return saved 

163 return None