Skip to content
Closed
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
2 changes: 2 additions & 0 deletions sdk/monitor/azure-monitor-opentelemetry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features Added

- Added custom log record processors configuration option
([#43713](https://github.com/Azure/azure-sdk-for-python/pull/43713))
- Performance Counters
([#43262](https://github.com/Azure/azure-sdk-for-python/pull/43262))
- Adding more diagnostic log message IDs
Expand Down
2 changes: 2 additions & 0 deletions sdk/monitor/azure-monitor-opentelemetry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ You can use `configure_azure_monitor` to set up instrumentation for your app to
| `instrumentation_options` | A nested dictionary that determines which instrumentations to enable or disable. Instrumentations are referred to by their [Library Names](#officially-supported-instrumentations). For example, `{"azure_sdk": {"enabled": False}, "flask": {"enabled": False}, "django": {"enabled": True}}` will disable Azure Core Tracing and the Flask instrumentation but leave Django and the other default instrumentations enabled. The `OTEL_PYTHON_DISABLED_INSTRUMENTATIONS` environment variable explained below can also be used to disable instrumentations. | `N/A` |
| `resource` | Specifies the OpenTelemetry [Resource][ot_spec_resource] associated with your application. Passed in [Resource Attributes][ot_spec_resource_attributes] take priority over default attributes and those from [Resource Detectors][ot_python_resource_detectors]. | [OTEL_SERVICE_NAME][ot_spec_service_name], [OTEL_RESOURCE_ATTRIBUTES][ot_spec_resource_attributes], [OTEL_EXPERIMENTAL_RESOURCE_DETECTORS][ot_python_resource_detectors] |
| `span_processors` | A list of [span processors][ot_span_processor] that will perform processing on each of your spans before they are exported. Useful for filtering/modifying telemetry. | `N/A` |
| `log_record_processors` | A list of [log record processors][ot_log_record_processor] that will process log records before they are exported. Useful for adding custom attributes to logs. | `N/A` |
| `views` | A list of [views][ot_view] that will be used to customize metrics exported by the SDK. | `N/A` |
| `traces_per_second` | Configures the Rate Limited sampler by specifying the maximum number of traces to sample per second. When set, this automatically enables the rate-limited sampler. Alternatively, you can configure sampling using the `OTEL_TRACES_SAMPLER` and `OTEL_TRACES_SAMPLER_ARG` environment variables as described in the table below. Please note that the sampling configuration via environment variables will have precedence over the sampling exporter/distro options. | `N/A`

Expand Down Expand Up @@ -231,6 +232,7 @@ contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additio
[ot_sdk_python]: https://github.com/open-telemetry/opentelemetry-python
[ot_sdk_python_metric_reader]: https://opentelemetry-python.readthedocs.io/en/latest/sdk/metrics.export.html#opentelemetry.sdk.metrics.export.MetricReader
[ot_span_processor]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#span-processor
[ot_log_record_processor]: https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/logs/sdk.md#log-record-processor
[ot_view]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view
[ot_sdk_python_view_examples]: https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples/metrics/views
[ot_instrumentation_django]: https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-django
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
SAMPLING_TRACES_PER_SECOND_ARG,
SPAN_PROCESSORS_ARG,
VIEWS_ARG,
LOG_RECORD_PROCESSORS_ARG,
)
from azure.monitor.opentelemetry._types import ConfigurationValue
from azure.monitor.opentelemetry.exporter._quickpulse import ( # pylint: disable=import-error,no-name-in-module
Expand Down Expand Up @@ -101,6 +102,8 @@ def configure_azure_monitor(**kwargs) -> None: # pylint: disable=C4758
Attributes take priority over default attributes and those from Resource Detectors.
:keyword list[~opentelemetry.sdk.trace.SpanProcessor] span_processors: List of `SpanProcessor` objects
to process every span prior to exporting. Will be run sequentially.
:keyword list[~opentelemetry.sdk._logs.LogRecordProcessor] log_record_processors: List of `LogRecordProcessor`
objects to process every log record prior to exporting. Will be run sequentially.
:keyword bool enable_live_metrics: Boolean value to determine whether to enable live metrics feature.
Defaults to `False`.
:keyword bool enable_performance_counters: Boolean value to determine whether to enable performance counters.
Expand Down Expand Up @@ -212,6 +215,8 @@ def _setup_logging(configurations: Dict[str, ConfigurationValue]):
resource: Resource = configurations[RESOURCE_ARG] # type: ignore
enable_performance_counters_config = configurations[ENABLE_PERFORMANCE_COUNTERS_ARG]
logger_provider = LoggerProvider(resource=resource)
for custom_log_record_processor in configurations[LOG_RECORD_PROCESSORS_ARG]: # type: ignore
logger_provider.add_log_record_processor(custom_log_record_processor) # type: ignore
if configurations.get(ENABLE_LIVE_METRICS_ARG):
qlp = _QuickpulseLogRecordProcessor()
logger_provider.add_log_record_processor(qlp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
RATE_LIMITED_SAMPLER = "microsoft.rate_limited"
FIXED_PERCENTAGE_SAMPLER = "microsoft.fixed.percentage"
SAMPLING_TRACES_PER_SECOND_ARG = "traces_per_second"
LOG_RECORD_PROCESSORS_ARG = "log_record_processors"

# --------------------Autoinstrumentation Configuration------------------------------------------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
VIEWS_ARG,
RATE_LIMITED_SAMPLER,
FIXED_PERCENTAGE_SAMPLER,
LOG_RECORD_PROCESSORS_ARG,
)
from azure.monitor.opentelemetry._types import ConfigurationValue
from azure.monitor.opentelemetry._version import VERSION
Expand Down Expand Up @@ -76,6 +77,7 @@ def _get_configurations(**kwargs) -> Dict[str, ConfigurationValue]:
_default_sampling_ratio(configurations)
_default_instrumentation_options(configurations)
_default_span_processors(configurations)
_default_log_record_processors(configurations)
_default_enable_live_metrics(configurations)
_default_enable_performance_counters(configurations)
_default_views(configurations)
Expand Down Expand Up @@ -217,6 +219,8 @@ def _default_instrumentation_options(configurations):
def _default_span_processors(configurations):
configurations.setdefault(SPAN_PROCESSORS_ARG, [])

def _default_log_record_processors(configurations):
configurations.setdefault(LOG_RECORD_PROCESSORS_ARG, [])

def _default_enable_live_metrics(configurations):
configurations.setdefault(ENABLE_LIVE_METRICS_ARG, False)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License in the project root for
# license information.
# --------------------------------------------------------------------------

import logging
from logging import getLogger
from azure.monitor.opentelemetry import configure_azure_monitor
from opentelemetry import trace
from opentelemetry.sdk._logs import LogRecordProcessor, LogData
from azure.monitor.opentelemetry.exporter._generated.models import ContextTagKeys

logger = getLogger(__name__)
logger.setLevel(logging.INFO)



class LogRecordEnrichingProcessor(LogRecordProcessor):
"""
A log record processor that enriches log records with operation name
from the current span context.
"""

def force_flush(self, timeout_millis: int = 30000) -> bool:
"""Force flush any pending log records."""
return True

def shutdown(self) -> None:
"""Shutdown the processor."""
pass

def on_emit(self, log_data: LogData) -> None:
"""
Enrich the log record with operation name from the current span.

Args:
log_data: The log data containing log record and context information
"""
# Get the current span from the current context (not from trace_context which might be None)
current_span = trace.get_current_span()

if current_span and hasattr(current_span, 'name') and current_span.name:
# Add the operation name to log record attributes
if log_data.log_record.attributes is None:
log_data.log_record.attributes = {}
log_data.log_record.attributes[ContextTagKeys.AI_OPERATION_NAME] = current_span.name


# Create the log record enriching processor
log_enriching_processor = LogRecordEnrichingProcessor()

# Configure Azure Monitor with the custom log record processor
configure_azure_monitor(
log_record_processors=[log_enriching_processor]
)

# Your application code here
tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span("span-name-here"):
logger.info("This log will be enriched with operation name")

input()
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@ def test_setup_logging(self, get_logger_mock, pclp_mock):
logging_handler_init_mock = Mock()
logging_handler_mock.return_value = logging_handler_init_mock
logger_mock = Mock()
custom_lrp = Mock()
logger_mock.handlers = []
get_logger_mock.return_value = logger_mock
formatter_init_mock = Mock()
Expand All @@ -535,7 +536,8 @@ def test_setup_logging(self, get_logger_mock, pclp_mock):
"enable_performance_counters": True,
"logger_name": "test",
"resource": TEST_RESOURCE,
"logging_formatter": formatter_init_mock
"logging_formatter": formatter_init_mock,
"log_record_processors": [custom_lrp],
}

# Patch all the necessary modules and imports
Expand All @@ -557,8 +559,8 @@ def test_setup_logging(self, get_logger_mock, pclp_mock):
set_logger_provider_mock.assert_called_once_with(lp_init_mock)
log_exporter_mock.assert_called_once_with(**configurations)
blrp_mock.assert_called_once_with(log_exp_init_mock)
self.assertEqual(lp_init_mock.add_log_record_processor.call_count, 2)
lp_init_mock.add_log_record_processor.assert_has_calls([call(pclp_init_mock), call(blrp_init_mock)])
self.assertEqual(lp_init_mock.add_log_record_processor.call_count, 3)
lp_init_mock.add_log_record_processor.assert_has_calls([call(custom_lrp), call(pclp_init_mock), call(blrp_init_mock)])
logging_handler_mock.assert_called_once_with(logger_provider=lp_init_mock)
logging_handler_init_mock.setFormatter.assert_called_once_with(formatter_init_mock)
get_logger_mock.assert_called_once_with("test")
Expand Down Expand Up @@ -606,6 +608,7 @@ def test_setup_logging_duplicate_logger(self, get_logger_mock, instance_mock, pc
"logger_name": "test",
"resource": TEST_RESOURCE,
"logging_formatter": None,
"log_record_processors": [],
}

# Patch all the necessary modules and imports
Expand Down Expand Up @@ -666,7 +669,8 @@ def test_setup_logging_disable_performance_counters(self, get_logger_mock, pclp_
"enable_performance_counters": False,
"logger_name": "test",
"resource": TEST_RESOURCE,
"logging_formatter": formatter_init_mock
"logging_formatter": formatter_init_mock,
"log_record_processors": [],
}

# Patch all the necessary modules and imports
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def test_get_configurations(self, resource_create_mock):
views=["test_view"],
logger_name="test_logger",
span_processors=["test_processor"],
log_record_processors=["test_log_record_processor"],
)

self.assertEqual(configurations["connection_string"], "test_cs")
Expand Down Expand Up @@ -107,6 +108,7 @@ def test_get_configurations(self, resource_create_mock):
self.assertEqual(configurations["views"], ["test_view"])
self.assertEqual(configurations["logger_name"], "test_logger")
self.assertEqual(configurations["span_processors"], ["test_processor"])
self.assertEqual(configurations["log_record_processors"], ["test_log_record_processor"])

@patch.dict("os.environ", {}, clear=True)
@patch("opentelemetry.sdk.resources.Resource.create", return_value=TEST_DEFAULT_RESOURCE)
Expand Down Expand Up @@ -140,6 +142,7 @@ def test_get_configurations_defaults(self, resource_create_mock):
self.assertEqual(configurations["enable_performance_counters"], True)
self.assertEqual(configurations["logger_name"], "")
self.assertEqual(configurations["span_processors"], [])
self.assertEqual(configurations["log_record_processors"], [])
self.assertEqual(configurations["views"], [])

@patch.dict(
Expand Down