|
| 1 | +import logging |
| 2 | +from azure.monitor.opentelemetry import configure_azure_monitor |
| 3 | +from fastapi import FastAPI |
| 4 | +from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor |
| 5 | +from opentelemetry.sdk._logs import LoggingHandler |
| 6 | + |
| 7 | + |
| 8 | +# This is a custom logging handler that does formatting of log messages before passing them on to open telemetry. |
| 9 | +# Note that the class we're inheriting from here *is* an OpenTelemetry derived Python logger. |
| 10 | +class LoggingHandlerWithFormatting(LoggingHandler): |
| 11 | + def emit(self, record: logging.LogRecord) -> None: |
| 12 | + # Do a bit of a hack here to format the message before passing it onwards. |
| 13 | + # At the same time make sure we restore record.msg in case there are other handlers in the chain. |
| 14 | + original_msg = record.msg |
| 15 | + original_args = record.args |
| 16 | + |
| 17 | + formatted_msg = self.format(record) |
| 18 | + record.msg = formatted_msg |
| 19 | + record.args = None |
| 20 | + |
| 21 | + # Note that the logger that we're calling emit on here is an Open Telemetry Logger, not a Python logger. |
| 22 | + self._logger.emit(self._translate(record)) |
| 23 | + |
| 24 | + # For inspecting and debugging the actual telemetry payload, uncomment the following lines. |
| 25 | + # log_record_as_json = self._translate(record).to_json() |
| 26 | + # print(f"---- start payload ----\n{log_record_as_json}\n---- end payload -----", flush=True) |
| 27 | + |
| 28 | + record.msg = original_msg |
| 29 | + record.args = original_args |
| 30 | + |
| 31 | + |
| 32 | +# A lot of the configuration will be read from environment variables during the execution of this function. |
| 33 | +# Notable environment variables that are consumed are: |
| 34 | +# - APPLICATIONINSIGHTS_CONNECTION_STRING |
| 35 | +# - OTEL_SERVICE_NAME |
| 36 | +# - OTEL_RESOURCE_ATTRIBUTES |
| 37 | +def setup_azure_monitor_telemetry(fastapi_app: FastAPI) -> None: |
| 38 | + # Under ideal circumstances, the below call to configure_azure_monitor() should be the only call needed |
| 39 | + # to configure the entire telemetry stack. However, it seems that there are multiple glitches. |
| 40 | + # - Supposedly, FastAPI instrumentation should be added automatically, but this only seems to work |
| 41 | + # if the call to configure_azure_monitor() happens very early in the module loading process, and |
| 42 | + # specifically it seems that it has to happen before any import of FastAPIP |
| 43 | + # - The default log handler that is added does not obey the specified log message format string, |
| 44 | + # even if it is set using OTEL_PYTHON_LOG_FORMAT. It justs logs the raw message string. |
| 45 | + # |
| 46 | + # Note that this call will throw an exception if the APPLICATIONINSIGHTS_CONNECTION_STRING |
| 47 | + # environment variable is not set or if it is invalid |
| 48 | + configure_azure_monitor() |
| 49 | + |
| 50 | + # The log handler that is added by configure_azure_monitor() does not obey the specified log message |
| 51 | + # format string. We therefore replace it with a custom handler that does formatting and set a format string that ensures |
| 52 | + # that the logger name is included in the log message. |
| 53 | + handler_with_formatting = LoggingHandlerWithFormatting() |
| 54 | + handler_with_formatting.setFormatter(logging.Formatter("[%(name)s] %(message)s")) |
| 55 | + |
| 56 | + root_logger = logging.getLogger() |
| 57 | + |
| 58 | + handler_installed_by_config = root_logger.handlers[-1] |
| 59 | + root_logger.removeHandler(handler_installed_by_config) |
| 60 | + root_logger.addHandler(handler_with_formatting) |
| 61 | + |
| 62 | + FastAPIInstrumentor.instrument_app(fastapi_app) |
0 commit comments