Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NH-64716 First steps to integrate oboe settings API #223

Merged
merged 22 commits into from
Nov 17, 2023
Merged
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
49 changes: 37 additions & 12 deletions solarwinds_apm/apm_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,11 @@ def __init__(
)

# Calculate c-lib extension usage
# TODO Used returned OboeAPI
# https://swicloud.atlassian.net/browse/NH-64716
self.extension, self.context = self._get_extension_components(
(
self.extension,
self.context,
self.oboe_api,
) = self._get_extension_components(
self.agent_enabled,
self.is_lambda,
)
Expand All @@ -157,15 +159,17 @@ def _get_extension_components(
) -> None:
"""Returns c-lib extension or noop components based on agent_enabled, is_lambda.

TODO: Return OboeAPI as noop or c-lib version
https://swicloud.atlassian.net/browse/NH-64716

agent_enabled T, is_lambda F -> c-lib extension, c-lib Context
agent_enabled T, is_lambda T -> no-op extension, no-op Context
agent_enabled T, is_lambda F -> c-lib extension, c-lib Context, no-op settings API
agent_enabled T, is_lambda T -> no-op extension, no-op Context, c-lib settings API
agent_enabled F -> all no-op
"""
if not agent_enabled:
return noop_extension, noop_extension.Context
return (
noop_extension,
noop_extension.Context,
noop_extension.OboeAPI,
)

try:
# pylint: disable=import-outside-toplevel
import solarwinds_apm.extension.oboe as c_extension
Expand All @@ -177,11 +181,32 @@ def _get_extension_components(
INTL_SWO_SUPPORT_EMAIL,
err,
)
return noop_extension, noop_extension.Context
return (
noop_extension,
noop_extension.Context,
noop_extension.OboeAPI,
)

if is_lambda:
return noop_extension, noop_extension.Context
return c_extension, c_extension.Context
try:
# pylint: disable=import-outside-toplevel,no-name-in-module
from solarwinds_apm.extension.oboe import OboeAPI as oboe_api
except ImportError as err:
# c-lib version may not have settings API
# TODO Update this message to contact support after c-lib 14
# https://swicloud.atlassian.net/browse/NH-64716
logger.warning(
"Could not import API in lambda mode. Installed layer version not compatible. Tracing disabled: %s",
err,
)
return (
noop_extension,
noop_extension.Context,
noop_extension.OboeAPI,
)
Comment on lines +198 to +206
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what happens if APM Python is built using c-lib < 14.0.0

return noop_extension, noop_extension.Context, oboe_api

return c_extension, c_extension.Context, noop_extension.OboeAPI

@classmethod
def calculate_is_lambda(cls) -> bool:
Expand Down
135 changes: 132 additions & 3 deletions solarwinds_apm/apm_meter_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,150 @@

import logging

from opentelemetry import metrics
# TypeError: 'ABCMeta' object is not subscriptable
# with this old import for callback signatures, with current Otel API
# pylint:disable=deprecated-typing-alias
from typing import TYPE_CHECKING, Iterable

from opentelemetry.metrics import CallbackOptions, Observation, get_meter

if TYPE_CHECKING:
from solarwinds_apm.apm_config import SolarWindsApmConfig

logger = logging.getLogger(__name__)


class SolarWindsMeterManager:
"""SolarWinds Python OTLP Meter Manager"""

def __init__(self, **kwargs: int) -> None:
def __init__(
self, apm_config: "SolarWindsApmConfig", **kwargs: int
) -> None:
self.oboe_settings_api = apm_config.oboe_api()

# Returns named `Meter` to handle instrument creation.
# A convenience wrapper for MeterProvider.get_meter
self.meter_response_times = metrics.get_meter("sw.apm.request.metrics")
self.meter_response_times = get_meter("sw.apm.request.metrics")
self.meter_request_counters = get_meter("sw.apm.sampling.metrics")

self.response_time = self.meter_response_times.create_histogram(
name="trace.service.response_time",
description="measures the duration of an inbound HTTP request",
unit="ms",
)

# TODO add trace.service.requests, trace.service.errors
# https://swicloud.atlassian.net/browse/NH-67392

def consume_tracecount(
options: CallbackOptions,
) -> Iterable[Observation]:
status, trace_count = self.oboe_settings_api.consumeTraceCount()
yield Observation(trace_count, {"status": status})

self.tracecount = self.meter_request_counters.create_observable_gauge(
name="trace.service.tracecount",
callbacks=[consume_tracecount],
)

def consume_samplecount(
options: CallbackOptions,
) -> Iterable[Observation]:
status, trace_count = self.oboe_settings_api.consumeSampleCount()
yield Observation(trace_count, {"status": status})

self.samplecount = self.meter_request_counters.create_observable_gauge(
name="trace.service.samplecount",
callbacks=[consume_samplecount],
)

def consume_request_count(
options: CallbackOptions,
) -> Iterable[Observation]:
status, trace_count = self.oboe_settings_api.consumeRequestCount()
yield Observation(trace_count, {"status": status})

self.request_count = (
self.meter_request_counters.create_observable_gauge(
name="trace.service.request_count",
callbacks=[consume_request_count],
)
)

def consume_tokenbucket_exhaustion_count(
options: CallbackOptions,
) -> Iterable[Observation]:
(
status,
trace_count,
) = self.oboe_settings_api.consumeTokenBucketExhaustionCount()
yield Observation(trace_count, {"status": status})

self.tokenbucket_exhaustion_count = (
self.meter_request_counters.create_observable_gauge(
name="trace.service.tokenbucket_exhaustion_count",
callbacks=[consume_tokenbucket_exhaustion_count],
)
)

def consume_through_trace_count(
options: CallbackOptions,
) -> Iterable[Observation]:
(
status,
trace_count,
) = self.oboe_settings_api.consumeThroughTraceCount()
yield Observation(trace_count, {"status": status})

self.through_trace_count = (
self.meter_request_counters.create_observable_gauge(
name="trace.service.through_trace_count",
callbacks=[consume_through_trace_count],
)
)

def consume_triggered_trace_count(
options: CallbackOptions,
) -> Iterable[Observation]:
(
status,
trace_count,
) = self.oboe_settings_api.consumeTriggeredTraceCount()
yield Observation(trace_count, {"status": status})

self.triggered_trace_count = (
self.meter_request_counters.create_observable_gauge(
name="trace.service.triggered_trace_count",
callbacks=[consume_triggered_trace_count],
)
)

def get_last_used_sample_rate(
options: CallbackOptions,
) -> Iterable[Observation]:
(
status,
trace_count,
) = self.oboe_settings_api.getLastUsedSampleRate()
yield Observation(trace_count, {"status": status})

self.sample_rate = self.meter_request_counters.create_observable_gauge(
name="trace.service.sample_rate",
callbacks=[get_last_used_sample_rate],
)

def get_last_used_sample_source(
options: CallbackOptions,
) -> Iterable[Observation]:
(
status,
trace_count,
) = self.oboe_settings_api.getLastUsedSampleSource()
yield Observation(trace_count, {"status": status})

self.sample_source = (
self.meter_request_counters.create_observable_gauge(
name="trace.service.sample_source",
callbacks=[get_last_used_sample_source],
)
)
81 changes: 73 additions & 8 deletions solarwinds_apm/apm_noop.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
# pylint: disable-msg=C0103
import threading

from opentelemetry.metrics._internal.instrument import NoOpHistogram
from opentelemetry.metrics._internal.instrument import (
NoOpHistogram,
NoOpObservableGauge,
)


class Metadata:
Expand Down Expand Up @@ -192,13 +195,75 @@ def getVersionString():
return "No extension loaded."


class OtelHistogram:
@staticmethod
def record(*args, **kwargs):
pass


class SolarWindsMeterManager:
def __init__(self, *args, **kwargs):
self.meter = None
self.meter_response_times = None
self.meter_request_counters = None

self.response_time = NoOpHistogram(name="trace.service.response_time")
self.tracecount = NoOpObservableGauge(name="trace.service.tracecount")
self.samplecount = NoOpObservableGauge(
name="trace.service.samplecount"
)
self.request_count = NoOpObservableGauge(
name="trace.service.request_count"
)
self.tokenbucket_exhaustion_count = NoOpObservableGauge(
name="trace.service.tokenbucket_exhaustion_count"
)
self.through_trace_count = NoOpObservableGauge(
name="trace.service.through_trace_count"
)
self.triggered_trace_count = NoOpObservableGauge(
name="trace.service.triggered_trace_count"
)
self.sample_rate = NoOpObservableGauge(
name="trace.service.sample_rate"
)
self.sample_source = NoOpObservableGauge(
name="trace.service.sample_source"
)


class OboeAPI:
def __init__(self, *args, **kwargs):
pass

def getTracingDecision(self):
return (
0,
0,
0,
0,
0.0,
0.0,
0,
0,
"",
"",
0,
)

def consumeRequestCount(self):
return 0, 0

def consumeTokenBucketExhaustionCount(self):
return 0, 0

def consumeTraceCount(self):
return 0, 0

def consumeSampleCount(self):
return 0, 0

def consumeThroughTraceCount(self):
return 0, 0

def consumeTriggeredTraceCount(self):
return 0, 0

def getLastUsedSampleRate(self):
return 0, 0

def getLastUsedSampleSource(self):
return 0, 0
11 changes: 9 additions & 2 deletions solarwinds_apm/configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def _configure(self, **kwargs: int) -> None:
)
apm_meters = NoopMeterManager()
else:
apm_meters = SolarWindsMeterManager()
apm_meters = SolarWindsMeterManager(apm_config)

reporter = self._initialize_solarwinds_reporter(apm_config)
self._configure_otel_components(
Expand Down Expand Up @@ -273,6 +273,9 @@ def _configure_traces_exporter(
"Setting trace with BatchSpanProcessor using %s",
exporter_name,
)

# TODO or SimpleSpanProcessor is_lambda
# https://swicloud.atlassian.net/browse/NH-67393
span_processor = BatchSpanProcessor(exporter)
trace.get_tracer_provider().add_span_processor(span_processor)

Expand Down Expand Up @@ -333,7 +336,7 @@ def _configure_metrics_exporter(
resource = trace.get_tracer_provider().get_tracer(__name__).resource
sw_resource = Resource.create(
{
"sw.trace_span_mode": "otel",
"sw.data.module": "apm",
"service.name": apm_config.service_name,
}
).merge(resource)
Expand Down Expand Up @@ -541,6 +544,10 @@ def _report_init_event(
keys: dict = None,
) -> None:
"""Report the APM library's init message, when reporter ready."""
if apm_config.is_lambda:
logger.debug("Skipping init event in lambda")
return

reporter_ready = False
if reporter.init_status in (
OboeReporterCode.OBOE_INIT_OK,
Expand Down
4 changes: 4 additions & 0 deletions solarwinds_apm/inbound_metrics_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ def on_end(self, span: "ReadableSpan") -> None:
status_code = self.get_http_status_code(span)
request_method = span.attributes.get(self._HTTP_METHOD, None)

# TODO Change when this is logged (don't when no-op)
# https://swicloud.atlassian.net/browse/NH-65061
logger.debug(
"createHttpSpan with trans_name: %s, url_tran: %s, domain: %s, span_time: %s status_code: %s, request_method: %s, has_error: %s",
trans_name,
Expand All @@ -116,6 +118,8 @@ def on_end(self, span: "ReadableSpan") -> None:
has_error,
)
else:
# TODO Change when this is logged (don't when no-op)
# https://swicloud.atlassian.net/browse/NH-65061
logger.debug(
"createSpan with trans_name: %s, domain: %s, span_time: %s, has_error: %s",
trans_name,
Expand Down
4 changes: 4 additions & 0 deletions solarwinds_apm/otlp_metrics_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,15 @@ def on_end(self, span: "ReadableSpan") -> None:
):
return

# TODO add sw.service_name
# https://swicloud.atlassian.net/browse/NH-67392
# support ssa and conform to Otel proto common_pb2
meter_attrs = {
"sw.nonce": random.getrandbits(64) >> 1,
}

# TODO add trace.service.requests, trace.service.errors
# https://swicloud.atlassian.net/browse/NH-67392
has_error = self.has_error(span)
if has_error:
meter_attrs.update({"sw.is_error": "true"})
Expand Down
Loading
Loading