From 90fbf889b8745618bc7216c8741fb6747cd04d1c Mon Sep 17 00:00:00 2001 From: Emily Ashley <15912063+emilyashley@users.noreply.github.com> Date: Mon, 27 Feb 2023 15:11:19 -0600 Subject: [PATCH] doc: audit of docstrings for dev-friendliness & relevance (#85) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Which problem is this PR solving? - Closes #76 - Touches #31 ## Short description of the changes - adds module docstrings for Distro - additional class and function docstrings - more type annotations for return expectations - additional tests on stdout for local vis exporter ## How to verify that this has the expected result - reading the docstrings is helpful to you --------- Co-authored-by: Purvi Kanal --- src/honeycomb/opentelemetry/distro.py | 43 +++++++++++++--- src/honeycomb/opentelemetry/local_exporter.py | 17 +++++-- src/honeycomb/opentelemetry/options.py | 50 ++++++++++++------- src/honeycomb/opentelemetry/sampler.py | 27 ++++++++-- src/honeycomb/opentelemetry/trace.py | 7 ++- tests/test_distro.py | 7 +-- tests/test_local_exporter.py | 10 +++- tests/test_metrics.py | 3 +- 8 files changed, 125 insertions(+), 39 deletions(-) diff --git a/src/honeycomb/opentelemetry/distro.py b/src/honeycomb/opentelemetry/distro.py index 3c28eef..9139dd0 100644 --- a/src/honeycomb/opentelemetry/distro.py +++ b/src/honeycomb/opentelemetry/distro.py @@ -1,5 +1,20 @@ -""" -Add module doc string +"""This honey-flavored Distro configures OpenTelemetry for use with Honeycomb. + +Typical usage example: + + using the opentelemetry-instrument command with + requisite env variables set: + + $bash> opentelemetry-instrument python program.py + + or configured by code within your service: + configure_opentelemetry( + HoneycombOptions( + debug=True, + apikey=os.getenv("HONEYCOMB_API_KEY"), + service_name="otel-python-example" + ) + ) """ from logging import getLogger from opentelemetry.instrumentation.distro import BaseDistro @@ -21,9 +36,12 @@ def configure_opentelemetry( Args: options (HoneycombOptions, optional): the HoneycombOptions used to - configure the the SDK + configure the the SDK. These options can be set either as parameters + to this function or through environment variables + + Note: API key is a required option. """ - _logger.debug("🐝 Configuring OpenTelemetry using Honeycomb distro 🐝") + _logger.info("🐝 Configuring OpenTelemetry using Honeycomb distro 🐝") _logger.debug(vars(options)) resource = create_resource(options) set_tracer_provider( @@ -38,9 +56,22 @@ def configure_opentelemetry( # pylint: disable=too-few-public-methods class HoneycombDistro(BaseDistro): """ - This honey-flavored Distro configures OpenTelemetry for use with Honeycomb. + An extension of the base python OpenTelemetry distro, which provides + a mechanism to automatically configure some of the more common options + for users. This class is auto-detected by the `opentelemetry-instrument` + command. + + This class doesn't need to be touched directly when using the distro. If + you'd like to explicitly set configuration in code, use the + configure_opentelemetry() function above instead of the + `opentelemetry-instrument` command. + + If you're wondering about the under-the-hood magic - we add the following + declaration to package metadata in our pyproject.toml, like so: + + [tool.poetry.plugins."opentelemetry_distro"] + distro = "honeycomb.opentelemetry.distro:HoneycombDistro" """ def _configure(self, **kwargs): - print("🐝 auto instrumented 🐝") configure_opentelemetry() diff --git a/src/honeycomb/opentelemetry/local_exporter.py b/src/honeycomb/opentelemetry/local_exporter.py index 85eebad..1f66865 100644 --- a/src/honeycomb/opentelemetry/local_exporter.py +++ b/src/honeycomb/opentelemetry/local_exporter.py @@ -1,13 +1,20 @@ import typing +from logging import getLogger import requests + + from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from honeycomb.opentelemetry.options import HoneycombOptions, is_classic +MISSING_API_OR_SERVICE_NAME_ERROR = "disabling local visualizations - " + \ + "must have both service name and API key configured." + +_logger = getLogger(__name__) + def configure_local_exporter(options: HoneycombOptions): - """ - Configures and returns an OpenTelemetry Span Exporter that prints + """Configures and returns an OpenTelemetry Span Exporter that prints direct web links for completed traces in Honeycomb on stdout. Args: @@ -26,6 +33,7 @@ def configure_local_exporter(options: HoneycombOptions): class LocalTraceLinkSpanExporter(SpanExporter): """Implementation of :class:`SpanExporter` that prints direct trace links to Honeycomb to the console. + This class can be used for diagnostic purposes. It prints the exported spans to the console STDOUT. """ @@ -37,8 +45,7 @@ def __init__( apikey: str ): if not service_name or not apikey: - print("disabling local visualizations - must have both " + - "service name and API key configured.") + _logger.warning(MISSING_API_OR_SERVICE_NAME_ERROR) return self.trace_link_url = self._build_trace_link_url( @@ -71,7 +78,7 @@ def _build_trace_link_url(self, apikey: str, service_name: str): timeout=30000 # 30 seconds ) if not resp.ok: - print("failed to get auth data from Honeycomb API") + _logger.warning("failed to get auth data from Honeycomb API") return None resp_data = resp.json() diff --git a/src/honeycomb/opentelemetry/options.py b/src/honeycomb/opentelemetry/options.py index 6ed0d8c..9d577e3 100644 --- a/src/honeycomb/opentelemetry/options.py +++ b/src/honeycomb/opentelemetry/options.py @@ -15,18 +15,27 @@ ) from grpc import ssl_channel_credentials +# Environment Variable Names +OTEL_SERVICE_VERSION = "OTEL_SERVICE_VERSION" DEBUG = "DEBUG" +HONEYCOMB_ENABLE_LOCAL_VISUALIZATIONS = "HONEYCOMB_ENABLE_LOCAL_VISUALIZATIONS" +SAMPLE_RATE = "SAMPLE_RATE" + +# HNY Credential Names +HONEYCOMB_API_KEY = "HONEYCOMB_API_KEY" +HONEYCOMB_TRACES_APIKEY = "HONEYCOMB_TRACES_APIKEY" +HONEYCOMB_DATASET = "HONEYCOMB_DATASET" +HONEYCOMB_METRICS_APIKEY = "HONEYCOMB_METRICS_APIKEY" +HONEYCOMB_METRICS_DATASET = "HONEYCOMB_METRICS_DATASET" + +# Default values DEFAULT_API_ENDPOINT = "https://api.honeycomb.io:443" DEFAULT_EXPORTER_PROTOCOL = "grpc" DEFAULT_SERVICE_NAME = "unknown_service:python" DEFAULT_LOG_LEVEL = "ERROR" DEFAULT_SAMPLE_RATE = 1 -HONEYCOMB_API_KEY = "HONEYCOMB_API_KEY" -HONEYCOMB_DATASET = "HONEYCOMB_DATASET" -HONEYCOMB_ENABLE_LOCAL_VISUALIZATIONS = "HONEYCOMB_ENABLE_LOCAL_VISUALIZATIONS" -HONEYCOMB_METRICS_APIKEY = "HONEYCOMB_METRICS_APIKEY" -HONEYCOMB_METRICS_DATASET = "HONEYCOMB_METRICS_DATASET" -HONEYCOMB_TRACES_APIKEY = "HONEYCOMB_TRACES_APIKEY" + +# Errors and Warnings INVALID_DEBUG_ERROR = "Unable to parse DEBUG environment variable. " + \ "Defaulting to False." INVALID_INSECURE_ERROR = "Unable to parse " + \ @@ -55,8 +64,6 @@ IGNORED_DATASET_ERROR = "Dataset is ignored in favor of service name." # not currently supported in OTel SDK, open PR: # https://github.com/open-telemetry/opentelemetry-specification/issues/1901 -OTEL_SERVICE_VERSION = "OTEL_SERVICE_VERSION" -SAMPLE_RATE = "SAMPLE_RATE" log_levels = { "NOTSET": logging.NOTSET, @@ -78,18 +85,21 @@ _logger = logging.getLogger(__name__) -def is_classic(apikey: str): +def is_classic(apikey: str) -> bool: """ Determines whether the passed in API key is a classic API key or not. Modern API keys have 22 or 23 characters. Classic API keys have 32 characters. + + Returns: + bool: true if the api key is a classic key, false if not """ return apikey and len(apikey) == 32 def parse_bool(environment_variable: str, default_value: bool, - error_message: str): + error_message: str) -> bool: """ Attempts to parse the provided environment variable into a bool. If it does not exist or fails parse, the default value is returned instead. @@ -114,7 +124,7 @@ def parse_bool(environment_variable: str, def parse_int(environment_variable: str, param: int, default_value: int, - error_message: str): + error_message: str) -> int: """ Attempts to parse the provided environment variable into an int. If it does not exist or fails parse, the default value is returned instead. @@ -141,7 +151,7 @@ def parse_int(environment_variable: str, return default_value -def _append_traces_path(protocol: str, endpoint: str): +def _append_traces_path(protocol: str, endpoint: str) -> str: """ Appends the OTLP traces HTTP path '/v1/traces' to the endpoint if the protocol is http/protobuf. @@ -154,7 +164,7 @@ def _append_traces_path(protocol: str, endpoint: str): return endpoint -def _append_metrics_path(protocol: str, endpoint: str): +def _append_metrics_path(protocol: str, endpoint: str) -> str: """ Appends the OTLP metrics HTTP path '/v1/metrics' to the endpoint if the protocol is http/protobuf. @@ -172,8 +182,14 @@ class HoneycombOptions: """ Honeycomb Options used to configure the OpenTelemetry SDK. - Setting the debug flag enables verbose logging and sets the OTEL_LOG_LEVEL - to DEBUG. + Setting the debug flag TRUE enables verbose logging and sets the + OTEL_LOG_LEVEL to DEBUG. + + An option set as an environment variable will override any existing + options declared as parameter variables, if neither are present it + will fall back to the default value. + + Defaults are declared at the top of this file, i.e. DEFAULT_SAMPLE_RATE = 1 """ traces_apikey = None metrics_apikey = None @@ -356,13 +372,13 @@ def __init__( INVALID_LOCAL_VIS_ERROR ) - def get_traces_endpoint(self): + def get_traces_endpoint(self) -> str: """ Returns the OTLP traces endpoint to send spans to. """ return self.traces_endpoint - def get_metrics_endpoint(self): + def get_metrics_endpoint(self) -> str: """ Returns the OTLP metrics endpoint to send metrics to. """ diff --git a/src/honeycomb/opentelemetry/sampler.py b/src/honeycomb/opentelemetry/sampler.py index b672cf0..811e31d 100644 --- a/src/honeycomb/opentelemetry/sampler.py +++ b/src/honeycomb/opentelemetry/sampler.py @@ -21,13 +21,34 @@ def configure_sampler( options: HoneycombOptions = HoneycombOptions(), ): + """Configures and returns an OpenTelemetry Sampler that is + configured based on the sample_rate determined in HoneycombOptions. + The configuration initializes a DeterministicSampler with + an inner sampler of either DefaultOn (1), Default Off (0), + or a TraceIdRatio as 1/N. + + Each of these samplers is ParentBased, meaning it respects + its parent span's sampling decision. + + Args: + options (HoneycombOptions): the HoneycombOptins containing + sample_rate used to configure the deterministic sampler. + + Returns: + DeterministicSampler: the configured Sampler based on sample_rate + """ return DeterministicSampler(options.sample_rate) class DeterministicSampler(Sampler): - """ - Custom samplers can be created by subclassing Sampler and implementing - Sampler.should_sample as well as Sampler.get_description. + """Implementation of :class:`Sampler` that uses an inner sampler + of either DefaultOn (1), Default Off (0), or a TraceIdRatio as 1/N + to determine a SamplingResult and SamplingDecision for a given span + in a trace. We append a SampleRate attribute to the span with the + given sample rate. + + Note: Each of these samplers is ParentBased, meaning it respects + its parent span's sampling decision. """ def __init__(self, rate: int): diff --git a/src/honeycomb/opentelemetry/trace.py b/src/honeycomb/opentelemetry/trace.py index ea02dc1..d7ec6ed 100644 --- a/src/honeycomb/opentelemetry/trace.py +++ b/src/honeycomb/opentelemetry/trace.py @@ -17,7 +17,10 @@ from honeycomb.opentelemetry.baggage import BaggageSpanProcessor -def create_tracer_provider(options: HoneycombOptions, resource: Resource): +def create_tracer_provider( + options: HoneycombOptions, + resource: Resource +) -> TracerProvider: """ Configures and returns a new TracerProvider to send traces telemetry. @@ -26,7 +29,7 @@ def create_tracer_provider(options: HoneycombOptions, resource: Resource): resource (Resource): the resource to use with the new tracer provider Returns: - MeterProvider: the new tracer provider + TracerProvider: the new tracer provider """ if options.traces_exporter_protocol == "grpc": exporter = GRPCSpanExporter( diff --git a/tests/test_distro.py b/tests/test_distro.py index 182173d..df799d8 100644 --- a/tests/test_distro.py +++ b/tests/test_distro.py @@ -1,8 +1,5 @@ import platform -from honeycomb.opentelemetry.distro import configure_opentelemetry -from honeycomb.opentelemetry.options import HoneycombOptions -from honeycomb.opentelemetry.version import __version__ from opentelemetry.metrics import get_meter_provider from opentelemetry.trace import get_tracer_provider from opentelemetry.sdk.trace.export import BatchSpanProcessor @@ -11,6 +8,10 @@ OTLPSpanExporter as GRPCSpanExporter ) +from honeycomb.opentelemetry.distro import configure_opentelemetry +from honeycomb.opentelemetry.options import HoneycombOptions +from honeycomb.opentelemetry.version import __version__ + def test_distro_configure_defaults(): configure_opentelemetry() diff --git a/tests/test_local_exporter.py b/tests/test_local_exporter.py index eb638d1..9b9d8d7 100644 --- a/tests/test_local_exporter.py +++ b/tests/test_local_exporter.py @@ -21,7 +21,7 @@ def _check_exporter_can_export_spans_successfully(exporter: SpanExporter): assert result == SpanExportResult.SUCCESS -def test_exporter_formats_correct_url(requests_mock): +def test_exporter_formats_correct_url_and_in_stdout(requests_mock, capsys): requests_mock.get("https://api.honeycomb.io/1/auth", json={"environment": {"slug": "my-env"}, "team": {"slug": "my-team"}}) exporter = LocalTraceLinkSpanExporter( @@ -29,9 +29,12 @@ def test_exporter_formats_correct_url(requests_mock): url = exporter._build_url(TRACE_ID) assert url == "https://ui.honeycomb.io/my-team/environments/my-env/datasets/my-service/trace?trace_id=a59c68a6de76d5e642bdc9a7641ae5f0" _check_exporter_can_export_spans_successfully(exporter) + # ensure the link is in stdout + captured = capsys.readouterr() + assert captured.out == f'Honeycomb link: {url}\n' -def test_exporter_formats_correct_url_classic(requests_mock): +def test_exporter_formats_correct_url_classic_and_in_stdout(requests_mock, capsys): requests_mock.get("https://api.honeycomb.io/1/auth", json={"team": {"slug": "my-team"}}) exporter = LocalTraceLinkSpanExporter( @@ -39,6 +42,9 @@ def test_exporter_formats_correct_url_classic(requests_mock): url = exporter._build_url(TRACE_ID) assert url == "https://ui.honeycomb.io/my-team/datasets/my-service/trace?trace_id=a59c68a6de76d5e642bdc9a7641ae5f0" _check_exporter_can_export_spans_successfully(exporter) + # ensure the link is in stdout + captured = capsys.readouterr() + assert captured.out == f'Honeycomb link: {url}\n' def test_exporter_without_apikey_does_not_build_url(): diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 9b22ba0..dea2a24 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -1,7 +1,8 @@ +from opentelemetry.sdk.metrics import MeterProvider + from honeycomb.opentelemetry.options import HoneycombOptions from honeycomb.opentelemetry.resource import create_resource from honeycomb.opentelemetry.metrics import create_meter_provider -from opentelemetry.sdk.metrics import MeterProvider def test_returns_meter_provider():