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

python configuration prototype #44

Closed
wants to merge 7 commits into from
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
27 changes: 15 additions & 12 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ sdk:
disabled: false
# Configure resource attributes and resource detection for all signals.
resource:
# detectors: [gcp, host]
# Key-value pairs to be used as resource attributes.
#
# Environment variable: OTEL_RESOURCE_ATTRIBUTES
Expand All @@ -35,7 +36,7 @@ sdk:
# Environment variable: OTEL_ATTRIBUTE_COUNT_LIMIT
attribute_count_limit: 128
# Configure the tracer provider.
tracer_provider:
traces:
# Span exporters. Each exporter key refers to the type of the exporter. Values configure the exporter. Exporters must be associated with a span processor.
exporters:
# Configure the otlp exporter.
Expand Down Expand Up @@ -113,6 +114,7 @@ sdk:
#
# Environment variable: OTEL_EXPORTER_JAEGER_AGENT_HOST
agent_port: 6832
console:
# List of span processors. Each span processor has a type and args used to configure it.
span_processors:
# Add a batch span processor.
Expand Down Expand Up @@ -140,15 +142,15 @@ sdk:
# Sets the exporter. Exporter must refer to a key in sdk.tracer_provider.exporters.
#
# Environment variable: OTEL_TRACES_EXPORTER
exporter: otlp
exporter: console
# Add a batch span processor configured with zipkin exporter. For full description of options see sdk.tracer_provider.span_processors[0].
- type: batch
args:
exporter: zipkin
# Add a batch span processor configured with jaeger exporter. For full description of options see sdk.tracer_provider.span_processors[0].
- type: batch
args:
exporter: jaeger
# - type: batch
# args:
# exporter: zipkin
# # Add a batch span processor configured with jaeger exporter. For full description of options see sdk.tracer_provider.span_processors[0].
# - type: batch
# args:
# exporter: jaeger
# Configure the span limits. See also sdk.attribute_limits.
span_limits:
# Set the max span attribute value size. Overrides sdk.attribute_limits.attribute_value_length_limit.
Expand Down Expand Up @@ -228,7 +230,7 @@ sdk:
# Set the sampler. Sampler must refer to a key in sdk.tracer_provider.sampler_config.
sampler: parent_based
# Configure the meter provider.
meter_provider:
metrics:
# Metric exporters. Each exporter key refers to the type of the exporter. Values configure the exporter. Exporters must be associated with a metric reader.
exporters:
# Configure the otlp exporter.
Expand Down Expand Up @@ -274,6 +276,7 @@ sdk:
#
# Environment variable: OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION
default_histogram_aggregation: exponential_bucket_histogram
console:
# List of metric readers. Each metric reader has a type and args used to configure it.
metric_readers:
# Add a periodic metric reader.
Expand All @@ -292,7 +295,7 @@ sdk:
# Sets the exporter. Exporter must refer to a key in sdk.meter_provider.exporters.
#
# Environment variable: OTEL_METRICS_EXPORTER
exporter: otlp
exporter: console
# Add a prometheus metric reader. Some languages SDKs may implement this as a metric exporter.
#
# Environment variable: OTEL_METRICS_EXPORTER=prometheus
Expand Down Expand Up @@ -342,7 +345,7 @@ sdk:
- key1
- key2
# Configure the logger provider.
logger_provider:
logs:
# Log record exporters. Each exporter key refers to the type of the exporter. Values configure the exporter. Exporters must be associated with a log record processor.
exporters:
# Configure the otlp exporter.
Expand Down
17 changes: 8 additions & 9 deletions json_schema/schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@
"attribute_limits": {
"$ref": "#/definitions/Limits"
},
"tracer_provider": {
"traces": {
"$ref": "#/definitions/TracerProvider"
},
"meter_provider": {
"metrics": {
"$ref": "#/definitions/MeterProvider"
},
"logger_provider": {
"logs": {
"$ref": "#/definitions/LoggerProvider"
}
},
Expand Down Expand Up @@ -96,15 +96,12 @@
},
"LoggerProviderExporters": {
"type": "object",
"additionalProperties": false,
"additionalProperties": true,
"properties": {
"otlp": {
"$ref": "#/definitions/Otlp"
}
},
"required": [
"otlp"
],
"title": "LoggerProviderExporters"
},
"Otlp": {
Expand Down Expand Up @@ -145,11 +142,13 @@
},
"default_histogram_aggregation": {
"type": "string"
},
"insecure": {
"type": "boolean"
}
},
"required": [
"endpoint",
"protocol"
"endpoint"
],
"title": "Otlp"
},
Expand Down
1 change: 1 addition & 0 deletions prototypes/python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.venv
11 changes: 11 additions & 0 deletions prototypes/python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# JSON Schema validation with Python

The code in this directory shows an example of using `jsonschema` for validation in combination with `pyyaml` YAML parser in Python.
Usage:

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python prototype.py ../../config.yaml ../../json_schema/schema/schema.json
```
127 changes: 127 additions & 0 deletions prototypes/python/otel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@

import logging
from typing import List, Sequence, Tuple
from pkg_resources import iter_entry_points
Copy link

Choose a reason for hiding this comment

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

pkg_resources has been deprecated (see this, and also this).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah good to know. It's probably ok for the prototype but wouldn't want to ship this code :D



import yaml
from pathlib import Path
from jsonschema import validate, validators
from jsonschema.exceptions import ValidationError

from opentelemetry.metrics import set_meter_provider
from opentelemetry.trace import set_tracer_provider
from opentelemetry.sdk.trace.export import SpanExporter
from opentelemetry.sdk.metrics.export import MetricExporter


# borromed from opentelemetry/sdk/_configuration
def _import_config_component(
selected_component: str, entry_point_name: str
) -> object:
component_entry_points = {
ep.name: ep for ep in iter_entry_points(entry_point_name)
}
entry_point = component_entry_points.get(selected_component, None)
if not entry_point:
raise RuntimeError(
f"Requested component '{selected_component}' not found in entry points for '{entry_point_name}'"
)

return entry_point.load()

class Config:
def __init__(self, config=None) -> None:
self._config = config

def _resource(self):
# resource detection
# attributes
from opentelemetry.sdk.resources import Resource
Copy link

Choose a reason for hiding this comment

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

Why import here?

Copy link
Collaborator Author

@codeboten codeboten Feb 23, 2023

Choose a reason for hiding this comment

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

not necessary to have the import in the method, that was just me being lazy

return Resource.create(self._config.get("sdk").get("resource").get("attributes"))

# _get_exporter returns an exporter class for the signal
def _get_exporter(self, signal: str, name: str):
if name not in self._config.get("sdk").get(signal).get("exporters"):
raise Exception(f"exporter {name} not specified for {signal} signal")

exporter = _import_config_component(name, f"opentelemetry_{signal}_exporter")
if signal == "metrics":
cls_type = MetricExporter
elif signal == "traces":
cls_type = SpanExporter
elif signal == "logs":
cls_type = SpanExporter
if issubclass(exporter, cls_type):
if self._config.get("sdk").get(signal).get("exporters").get(name) is not None:
return exporter(**self._config.get("sdk").get(signal).get("exporters").get(name))
return exporter()
raise RuntimeError(f"{name} is not a {signal} exporter")

def configure_tracing(self, cfg):
if cfg is None:
return
from opentelemetry.sdk.trace import TracerProvider
provider = TracerProvider(resource=self._resource())
from opentelemetry.sdk.trace.export import BatchSpanProcessor

for processor in cfg.get("span_processors"):
logging.debug("adding span processor %s", processor)
try:
processor = BatchSpanProcessor(self._get_exporter("traces", processor.get("args").get("exporter")))
provider.add_span_processor(processor)
except ModuleNotFoundError as exc:
logging.error("module not found", exc)
set_tracer_provider(provider)

def configure_metrics(self, cfg):
if cfg is None:
return
from opentelemetry.sdk.metrics import MeterProvider
readers = []
for reader in cfg.get("metric_readers"):
if reader.get("type") == "periodic":
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
readers.append(PeriodicExportingMetricReader(self._get_exporter("metrics", reader.get("args").get("exporter"))))
provider = MeterProvider(resource=self._resource(), metric_readers=readers)
set_meter_provider(provider)

def apply(self):
logging.debug("applying configuration %s", self._config)
if self._config is None or self._config.get("sdk").get("disabled"):
logging.debug("sdk disabled, nothing to apply")
return
self.configure_tracing(self._config.get("sdk").get("traces"))
self.configure_metrics(self._config.get("sdk").get("metrics"))


NoOpConfig = Config()

def configure(configuration: Config) -> None:
configuration.apply()

def parse_and_validate_from_config_file(filename: str, schema: str="../schema/schema.json") -> Config:
logging.debug(f"Loading config file: {filename}")
path = Path(__file__).parent.resolve()
resolver = validators.RefResolver(
base_uri=f"{path.as_uri()}/",
referrer=True,
)

with open(filename, "r") as stream:
try:
parse_config = yaml.safe_load(stream)
logging.debug("YAML parsed successfully")
logging.debug(f"Validating using schema file: {schema}")
validate(
instance=parse_config,
schema={"$ref": schema},
resolver=resolver,
)
logging.debug("No validation errors")
return Config(parse_config)
except yaml.YAMLError as exc:
logging.error(exc)
except ValidationError as exc:
logging.error(exc)
return NoOpConfig
30 changes: 30 additions & 0 deletions prototypes/python/prototype.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env python3

import logging
import sys

from opentelemetry.trace import get_tracer
from opentelemetry.metrics import get_meter

import otel


def main():
logging.basicConfig(level=logging.DEBUG)
otel.configure(otel.parse_and_validate_from_config_file(sys.argv[1], sys.argv[2]))

tracer = get_tracer("config-prototype")
meter = get_meter("config-prototype")

counter = meter.create_counter("work", unit="1")

with tracer.start_as_current_span("operation-a"):
with tracer.start_as_current_span("operation-b"):
with tracer.start_as_current_span("operation-c"):
logging.debug("you should see telemetry after this line")
counter.add(1)



if __name__ == "__main__":
main()
7 changes: 7 additions & 0 deletions prototypes/python/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
jsonschema
pyyaml
opentelemetry-api
opentelemetry-sdk
opentelemetry-exporter-otlp
opentelemetry-exporter-jaeger
opentelemetry-exporter-zipkin