Skip to content
Draft
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
20 changes: 15 additions & 5 deletions solarwinds_apm/apm_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from opentelemetry.environment_variables import OTEL_PROPAGATORS
from opentelemetry.sdk.resources import Resource

from solarwinds_apm import apm_logging
from solarwinds_apm import apm_logging, apm_resource
from solarwinds_apm.apm_constants import (
INTL_SWO_DEFAULT_PROPAGATORS,
INTL_SWO_PROPAGATOR,
Expand Down Expand Up @@ -97,7 +97,9 @@ def __init__(
"""Initialize SolarWinds APM configuration.

Parameters:
otel_resource (Resource): OpenTelemetry resource with attributes. Defaults to empty Resource.
otel_resource (Resource): OpenTelemetry resource with detector attributes.
In normal usage, passed from Configurator after detector resource created.
Defaults to Resource.create() for backward compatibility.
**kwargs (int): Additional configuration keyword arguments.
"""
self.__config = {}
Comment on lines 97 to 105
Expand Down Expand Up @@ -134,6 +136,13 @@ def __init__(
self.service_name,
)

# Create final APM resource with SolarWinds attributes
# and calculated service name
self.resource = apm_resource.create_apm_resource(
otel_resource,
self.service_name,
)

# Update and apply logging settings to Python logger
self._validate_log_filepath()
apm_logging.update_sw_log_handler(
Expand Down Expand Up @@ -367,10 +376,11 @@ def _calculate_service_name_apm_proto(
# Calculate `service.name` by priority system (decreasing):
# 1. OTEL_SERVICE_NAME
# 2. service.name in OTEL_RESOURCE_ATTRIBUTES
# 3. service name component of SW_APM_SERVICE_KEY
# 4. empty string
# 3. service.name in OTel Resource set by any Resource Detectors
# 4. service name component of SW_APM_SERVICE_KEY
# 5. empty string
#
# Note: 1-3 require that SW_APM_SERVICE_KEY exists and is in the correct
# Note: 1-4 require that SW_APM_SERVICE_KEY exists and is in the correct
# format of "<api_token>:<service_name>". Otherwise agent_enabled: False
Comment on lines +379 to 384
# and service.name is empty string.
#
Expand Down
63 changes: 63 additions & 0 deletions solarwinds_apm/apm_resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# © 2026 SolarWinds Worldwide, LLC. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

"""OpenTelemetry Resource creation for SolarWinds APM."""

from __future__ import annotations

import uuid

from opentelemetry.sdk.resources import Resource

from solarwinds_apm.version import __version__


def create_detector_resource() -> Resource:
"""Create Resource from all configured detectors.

Runs all detectors configured in OTEL_EXPERIMENTAL_RESOURCE_DETECTORS.
Should be called after SolarWindsDistro has configured default detector list.

Returns:
Resource: Resource with attributes from process, OS, cloud, k8s, etc. detectors.
"""
return Resource.create()


def create_apm_resource(
detector_resource: Resource,
service_name: str,
) -> Resource:
"""Create final APM Resource by merging SolarWinds attributes into detector resource.

Adds sw.apm.version, sw.data.module, service.name,
and service.instance.id (if not already present from detectors).

Args:
detector_resource: Resource from detectors with all their attributes.
service_name: Service name should have been calculated by precedence rules

Returns:
Resource: Final resource for telemetry providers (TracerProvider, MeterProvider, LoggerProvider).
"""
sw_attributes = {
"sw.apm.version": __version__,
"sw.data.module": "apm",
"service.name": service_name,
}

# Merge sw_attributes into detector_resource.
# Resource.merge(other) means other.attributes override self.attributes on conflicts.
# This preserves all detector attributes (cloud.*, k8s.*, etc.) while ensuring
# service.name (calculated via precedence) overrides any service.name from detectors.
apm_resource = detector_resource.merge(Resource(sw_attributes))

if "service.instance.id" not in apm_resource.attributes:
apm_resource = apm_resource.merge(
Resource({"service.instance.id": str(uuid.uuid4())})
)

return apm_resource
38 changes: 7 additions & 31 deletions solarwinds_apm/configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import logging
import math
import os
import uuid

from opentelemetry import trace
from opentelemetry._logs import set_logger_provider
Expand Down Expand Up @@ -59,7 +58,7 @@
from opentelemetry.trace import NoOpTracerProvider, set_tracer_provider
from opentelemetry.util._importlib_metadata import entry_points

from solarwinds_apm import apm_logging
from solarwinds_apm import apm_logging, apm_resource
from solarwinds_apm.apm_config import SolarWindsApmConfig
from solarwinds_apm.apm_constants import INTL_SWO_DEFAULT_PROPAGATORS
from solarwinds_apm.response_propagator import (
Expand All @@ -71,7 +70,6 @@
ServiceEntrySpanProcessor,
)
from solarwinds_apm.tracer_provider import SolarwindsTracerProvider
from solarwinds_apm.version import __version__

solarwinds_apm_logger = apm_logging.logger
logger = logging.getLogger(__name__)
Expand All @@ -87,30 +85,8 @@ def __init__(self) -> None:
Creates APM configuration instance for SDK initialization.
"""
super().__init__()
self.apm_config = SolarWindsApmConfig()

def _create_apm_resource(self) -> Resource:
"""Create new OpenTelemetry Resource for telemetry providers.

Returns:
Resource: Resource with SolarWinds APM attributes and service information.
"""
apm_resource = Resource.create(
{
"sw.apm.version": __version__,
"sw.data.module": "apm",
"service.name": self.apm_config.service_name,
}
)
# Prioritize service.instance.id set by any Resource Detectors
updated_apm_resource = apm_resource.merge(
Resource(
{"service.instance.id": str(uuid.uuid4())}
if "service.instance.id" not in apm_resource.attributes
else {}
)
)
return updated_apm_resource
detector_resource = apm_resource.create_detector_resource()
self.apm_config = SolarWindsApmConfig(otel_resource=detector_resource)

def _configure(self, **kwargs: int) -> None:
"""Configure SolarWinds APM and OpenTelemetry components.
Expand Down Expand Up @@ -143,24 +119,24 @@ def _configure(self, **kwargs: int) -> None:
apm_sampler = ParentBasedSwSampler(
self.apm_config,
)
apm_resource = self._create_apm_resource()
resource = self.apm_config.resource

self._custom_init_tracing(
exporters=span_exporters,
id_generator=kwargs.get("id_generator"),
sampler=apm_sampler,
resource=apm_resource,
resource=resource,
)
self._custom_init_metrics(
exporters_or_readers=metric_exporters,
resource=apm_resource,
resource=resource,
)

# Initialize LoggerProvider, log processors, and log exporters.
# As of OTel 1.40.0, auto-instrumentation of Python logging (setup_logging_handler)
# is handled by opentelemetry-instrumentation-logging via instrumentation, not by the
# deprecated LoggingHandler in the SDK. So we pass _init_logging false.
_init_logging(log_exporters, apm_resource, setup_logging_handler=False)
_init_logging(log_exporters, resource, setup_logging_handler=False)

# Set up additional custom SW components
self._configure_service_entry_span_processor()
Expand Down
8 changes: 7 additions & 1 deletion tests/integration/test_base_sw_headers_attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ def setUp(self):
configurator = SolarWindsConfigurator()
configurator._configure_propagator()
configurator._configure_response_propagator()
# Store configurator for access to resource in tests
self.configurator = configurator
# This is done because set_tracer_provider cannot override the
# current tracer provider. Has to be done here.
reset_trace_globals()
Expand All @@ -133,7 +135,11 @@ def setUp(self):
remote_parent_sampled=json_sampler,
remote_parent_not_sampled=json_sampler,
)
self.tracer_provider = TracerProvider(sampler=sampler)
# Pass resource from configurator to TracerProvider so detector attributes are included
self.tracer_provider = TracerProvider(
sampler=sampler,
resource=configurator.apm_config.resource,
)
# Set InMemorySpanExporter for testing
self.memory_exporter = InMemorySpanExporter()
span_processor = export.SimpleSpanProcessor(self.memory_exporter)
Expand Down
Loading
Loading