Skip to content
Open
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
30 changes: 30 additions & 0 deletions .github/workflows/main-rc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,33 @@ jobs:
with:
user: __token__
password: ${{ secrets.PYPI_TOKEN }}

deploy-k8s:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
echo "Building middleware-io-k8s (HTTP exporter)..."
cp pyproject-k8s.toml pyproject.toml
cat pyproject.toml
pip install -r dev-requirements.txt
pip install setuptools wheel build twine
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_ORG_PWD }}
run: |
python -m build
twine upload dist/*
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_ORG_PWD }}
40 changes: 40 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,43 @@ jobs:
Release ${{ github.ref }}
draft: false
prerelease: false
deploy-k8s:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
cp pyproject-k8s.toml pyproject.toml
cat pyproject.toml
pip install -r dev-requirements.txt
pip install setuptools wheel build twine
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_ORG_PWD }}
run: |
python -m build
twine upload dist/*
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_ORG_PWD }}
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release K8s ${{ github.ref }}
body: |
Release K8s ${{ github.ref }}
draft: false
prerelease: false
12 changes: 10 additions & 2 deletions middleware/distro.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@

import pkg_resources
from opentelemetry.instrumentation.distro import BaseDistro
from middleware.metrics import create_meter_provider
try:
from middleware.metrics import create_meter_provider
PSUTIL_AVAILABLE = True
except ImportError:
PSUTIL_AVAILABLE = False

from middleware.options import MWOptions, parse_bool
from middleware.resource import create_resource
from middleware.trace import create_tracer_provider
Expand Down Expand Up @@ -80,7 +85,10 @@ def mw_tracker_internal(
if options.collect_traces:
create_tracer_provider(options, resource)
if options.collect_metrics:
create_meter_provider(options, resource)
if PSUTIL_AVAILABLE:
create_meter_provider(options, resource)
else:
_logger.warning("Metrics collection skipped - psutil not available")
if options.collect_logs:
handler = create_logger_handler(options, resource)
logging.getLogger().addHandler(handler)
Expand Down
118 changes: 118 additions & 0 deletions middleware/exporter_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""
Exporter configuration that adapts based on the package variant.
Automatically selects HTTP or gRPC exporters based on installed package.

- middleware-io: gRPC exporters (standard)
- middleware-io-k8s: HTTP exporters (for Kubernetes)
"""

import logging
from middleware.version import __package_name__, __version__

_logger = logging.getLogger(__name__)

# PACKAGE VARIANT DETECTION

IS_K8S_VARIANT = "k8s" in __package_name__.lower()

_logger.info(f"Detected package: {__package_name__}")

# IMPORT APPROPRIATE EXPORTERS

if IS_K8S_VARIANT:
# K8s variant - MUST use HTTP exporters
try:
from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
OTLPMetricExporter as MetricExporter,
)
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
OTLPSpanExporter as TraceExporter,
)
from opentelemetry.exporter.otlp.proto.http._log_exporter import (
OTLPLogExporter as LogExporter,
)

_logger.info("✓ Using HTTP exporters for K8s variant")
except ImportError as e:
_logger.error(f"Failed to import HTTP exporters for K8s variant: {e}")
raise ImportError(
f"middleware-io-k8s package requires HTTP exporter dependencies. "
f"Please ensure 'opentelemetry-exporter-otlp-proto-http' is installed. "
f"Original error: {e}"
) from e
else:
# Standard variant - MUST use gRPC exporters
try:
import grpc
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
OTLPMetricExporter as MetricExporter,
)
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
OTLPSpanExporter as TraceExporter,
)
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import (
OTLPLogExporter as LogExporter,
)

_logger.info("✓ Using gRPC exporters for standard variant")
except ImportError as e:
_logger.error(f"Failed to import gRPC exporters for standard variant: {e}")
raise ImportError(
f"middleware-io package requires gRPC exporter dependencies. "
f"Please ensure 'opentelemetry-exporter-otlp-proto-grpc' is installed. "
f"Original error: {e}"
) from e


def create_metric_exporter(endpoint: str):
"""
Create OTLP metric exporter of appropriate type.

Args:
endpoint (str): The OTLP endpoint URL

Returns:
OTLPMetricExporter: HTTP or gRPC based on package variant
"""
if IS_K8S_VARIANT:
# K8s: HTTP exporter
return MetricExporter(endpoint=endpoint + "/v1/metrics")
else:
# Standard: gRPC exporter with compression
return MetricExporter(endpoint=endpoint, compression=grpc.Compression.Gzip)


def create_trace_exporter(endpoint: str):
"""
Create OTLP trace exporter of appropriate type.

Args:
endpoint (str): The OTLP endpoint URL

Returns:
OTLPSpanExporter: HTTP or gRPC based on package variant
"""
if IS_K8S_VARIANT:
# K8s: HTTP exporter
return TraceExporter(endpoint=endpoint + "/v1/traces")
else:
# Standard: gRPC exporter with compression
return TraceExporter(endpoint=endpoint, compression=grpc.Compression.Gzip)


def create_log_exporter(endpoint: str):
"""
Create OTLP log exporter of appropriate type.

Args:
endpoint (str): The OTLP endpoint URL

Returns:
OTLPLogExporter: HTTP or gRPC based on package variant
"""
if IS_K8S_VARIANT:
# K8s: HTTP exporter
return LogExporter(endpoint=endpoint + "/v1/logs")
else:
# Standard: gRPC exporter with compression
return LogExporter(endpoint=endpoint, compression=grpc.Compression.Gzip)
18 changes: 8 additions & 10 deletions middleware/log.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import grpc
# import grpc
import sys
import logging
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import (
OTLPLogExporter,
)
from middleware.exporter_config import create_log_exporter

from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import (
BatchLogRecordProcessor,
Expand All @@ -29,10 +28,8 @@ def create_logger_handler(options: MWOptions, resource: Resource) -> LoggingHand
Returns:
LoggerProvider: the new logger provider
"""
exporter = OTLPLogExporter(
endpoint=options.target,
compression=grpc.Compression.Gzip,
)
exporter = create_log_exporter(options.target)

logger_provider = LoggerProvider(resource=resource, shutdown_on_exit=True)
logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
if options.console_exporter:
Expand Down Expand Up @@ -60,6 +57,7 @@ def create_logger_handler(options: MWOptions, resource: Resource) -> LoggingHand

return handler


class MWLoggingHandler(LoggingHandler):
@staticmethod
def _get_attributes(record: LogRecord):
Expand All @@ -69,12 +67,12 @@ def _get_attributes(record: LogRecord):
if key == "request":
if hasattr(value, "method") and hasattr(value, "path"):
if len(vars(value)) == 2:
attributes[key] = f'{value.method} {value.path}'
attributes[key] = f"{value.method} {value.path}"
else:
attributes[key] = str(value)
else:
attributes[key] = str(value)
elif not isinstance(value, (bool, str, bytes, int, float)):
attributes[key] = str(value)

return attributes
return attributes
14 changes: 6 additions & 8 deletions middleware/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@
import gc
import functools
from typing import Generator
import grpc
# import grpc
import sys
import logging
from sys import getswitchinterval
from typing import NamedTuple
from opentelemetry.metrics import CallbackOptions, Observation, set_meter_provider
import grpc
# import grpc
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.metrics import MeterProvider, Meter
from opentelemetry.sdk.metrics.export import (
PeriodicExportingMetricReader,
ConsoleMetricExporter,
)
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from middleware.exporter_config import create_metric_exporter

from middleware.options import MWOptions

_logger = logging.getLogger(__name__)
Expand All @@ -34,11 +35,8 @@ def create_meter_provider(options: MWOptions, resource: Resource):
Returns:
MeterProvider: the new meter provider
"""

exporter = OTLPMetricExporter(
endpoint=options.target,
compression=grpc.Compression.Gzip,
)
exporter = create_metric_exporter(options.target)

readers = [PeriodicExportingMetricReader(exporter)]
if options.console_exporter:
output = sys.stdout
Expand Down
28 changes: 22 additions & 6 deletions middleware/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from middleware.detectors.detector import Detector, process_detector_input
from typing import Union, List
from middleware.version import __version__

# Environment Variable Names
OTEL_SERVICE_VERSION = "OTEL_SERVICE_VERSION"
DEBUG = "DEBUG"
Expand Down Expand Up @@ -267,9 +268,19 @@ def __init__(
)

if "https" not in self.target:
self.mw_agent_service = os.environ.get(MW_AGENT_SERVICE, mw_agent_service)
if self.mw_agent_service is not None:
self.target = f"http://{self.mw_agent_service}:{DEFAULT_PORT}"
# Special case: Kubernetes Python auto-instrumentation
if os.environ.get("MW_K8S_PYTHON_INSTRUMENTATION") == "true":
# Only set the service name, don't modify self.target
self.mw_agent_service = os.environ.get(
MW_AGENT_SERVICE, mw_agent_service
)
else:
# Default behavior
self.mw_agent_service = os.environ.get(
MW_AGENT_SERVICE, mw_agent_service
)
if self.mw_agent_service is not None:
self.target = f"http://{self.mw_agent_service}:{DEFAULT_PORT}"

self.custom_resource_attributes = os.environ.get(
MW_CUSTOM_RESOURCE_ATTRIBUTES, custom_resource_attributes
Expand All @@ -278,7 +289,7 @@ def __init__(
self.otel_propagators = os.environ.get(
OTEL_PROPAGATORS, os.environ.get(MW_PROPAGATORS, otel_propagators)
)
os.environ["OTEL_PROPAGATORS"] = self.otel_propagators
os.environ["OTEL_PROPAGATORS"] = self.otel_propagators
self.console_exporter = parse_bool(MW_CONSOLE_EXPORTER, console_exporter)
self.debug_log_file = parse_bool(MW_DEBUG_LOG_FILE, debug_log_file)
self.project_name = os.environ.get(MW_PROJECT_NAME, project_name)
Expand All @@ -287,6 +298,7 @@ def __init__(
_health_check(options=self)
_get_instrument_info(options=self)


def parse_bool(
environment_variable: str, default_value: bool, error_message: str = None
) -> bool:
Expand Down Expand Up @@ -345,8 +357,9 @@ def parse_int(
else:
return default_value


def _health_check(options: MWOptions):
if options.target == "" or ("https" not in options.target) :
if options.target == "" or ("https" not in options.target):
try:
response = requests.get(
f"http://{options.mw_agent_service}:13133/healthcheck", timeout=5
Expand All @@ -356,7 +369,10 @@ def _health_check(options: MWOptions):
"MW Agent Health Check is failing ...\nThis could be due to incorrect value of MW_AGENT_SERVICE\nIgnore the warning if you are using MW Agent older than 1.7.7 (You can confirm by running `mw-agent version`)"
)
except requests.exceptions.RequestException as e:
_logger.warning(f"MW Agent Health Check is failing ...\nException while MW Agent Health Check:{e}")
_logger.warning(
f"MW Agent Health Check is failing ...\nException while MW Agent Health Check:{e}"
)


def _get_instrument_info(options: MWOptions):
_logger.debug(
Expand Down
Loading