harness-sdk 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- harness_sdk/__init__.py +5 -0
- harness_sdk/agent.py +171 -0
- harness_sdk/agent_init.py +151 -0
- harness_sdk/autoinstrumentation/__init__.py +0 -0
- harness_sdk/autoinstrumentation/sitecustomize.py +30 -0
- harness_sdk/autoinstrumentation/wrapper.py +78 -0
- harness_sdk/config/__init__.py +0 -0
- harness_sdk/config/config.py +152 -0
- harness_sdk/config/config_pb2.py +112 -0
- harness_sdk/config/default.py +58 -0
- harness_sdk/config/environment.py +149 -0
- harness_sdk/constants.py +9 -0
- harness_sdk/custom_logger.py +43 -0
- harness_sdk/env.py +10 -0
- harness_sdk/excluded_by_attribute_span_processor.py +40 -0
- harness_sdk/gen_ai/__init__.py +7 -0
- harness_sdk/gen_ai/exceptions.py +17 -0
- harness_sdk/instrumentation/__init__.py +222 -0
- harness_sdk/instrumentation/aiohttp/__init__.py +218 -0
- harness_sdk/instrumentation/anthropic/__init__.py +379 -0
- harness_sdk/instrumentation/botocore/__init__.py +11 -0
- harness_sdk/instrumentation/django/__init__.py +77 -0
- harness_sdk/instrumentation/django/django_auto_instrumentation_compat.py +70 -0
- harness_sdk/instrumentation/fast_api/__init__.py +222 -0
- harness_sdk/instrumentation/fast_api/fast_api_auto_instrumentation_compat.py +65 -0
- harness_sdk/instrumentation/flask/__init__.py +187 -0
- harness_sdk/instrumentation/genai_env.py +71 -0
- harness_sdk/instrumentation/grpc/__init__.py +266 -0
- harness_sdk/instrumentation/httpx/__init__.py +73 -0
- harness_sdk/instrumentation/httpx/utils.py +160 -0
- harness_sdk/instrumentation/instrumentation_definitions.py +190 -0
- harness_sdk/instrumentation/litellm/__init__.py +401 -0
- harness_sdk/instrumentation/mcp/__init__.py +40 -0
- harness_sdk/instrumentation/mcp/gen_ai_mirror.py +204 -0
- harness_sdk/instrumentation/mysql/__init__.py +11 -0
- harness_sdk/instrumentation/openai/__init__.py +340 -0
- harness_sdk/instrumentation/postgresql/__init__.py +13 -0
- harness_sdk/instrumentation/requests/__init__.py +40 -0
- harness_sdk/otlp_reporting.py +75 -0
- harness_sdk/plugins/__init__.py +12 -0
- harness_sdk/plugins/builtin/__init__.py +1 -0
- harness_sdk/plugins/builtin/pipeline.py +56 -0
- harness_sdk/plugins/builtin/span_attributes.py +26 -0
- harness_sdk/plugins/control.py +132 -0
- harness_sdk/plugins/loader.py +105 -0
- harness_sdk/plugins/observability.py +23 -0
- harness_sdk/sampling_span_processor.py +42 -0
- harness_sdk/span_attributes_processor.py +19 -0
- harness_sdk/version.py +1 -0
- harness_sdk-1.0.0.dist-info/METADATA +318 -0
- harness_sdk-1.0.0.dist-info/RECORD +88 -0
- harness_sdk-1.0.0.dist-info/WHEEL +5 -0
- harness_sdk-1.0.0.dist-info/entry_points.txt +6 -0
- harness_sdk-1.0.0.dist-info/licenses/LICENSE.md +202 -0
- harness_sdk-1.0.0.dist-info/licenses/NOTICE.md +28 -0
- harness_sdk-1.0.0.dist-info/top_level.txt +2 -0
- opentelemetry/instrumentation/anthropic/__init__.py +119 -0
- opentelemetry/instrumentation/anthropic/messages_extractors.py +251 -0
- opentelemetry/instrumentation/anthropic/package.py +15 -0
- opentelemetry/instrumentation/anthropic/patch.py +131 -0
- opentelemetry/instrumentation/anthropic/utils.py +243 -0
- opentelemetry/instrumentation/anthropic/version.py +15 -0
- opentelemetry/instrumentation/anthropic/wrappers.py +435 -0
- opentelemetry/instrumentation/openai_v2/__init__.py +168 -0
- opentelemetry/instrumentation/openai_v2/instruments.py +52 -0
- opentelemetry/instrumentation/openai_v2/package.py +18 -0
- opentelemetry/instrumentation/openai_v2/patch.py +952 -0
- opentelemetry/instrumentation/openai_v2/response_extractors.py +419 -0
- opentelemetry/instrumentation/openai_v2/response_wrappers.py +467 -0
- opentelemetry/instrumentation/openai_v2/utils.py +502 -0
- opentelemetry/instrumentation/openai_v2/version.py +15 -0
- opentelemetry/util/genai/__init__.py +13 -0
- opentelemetry/util/genai/_embedding_invocation.py +123 -0
- opentelemetry/util/genai/_inference_invocation.py +358 -0
- opentelemetry/util/genai/_invocation.py +140 -0
- opentelemetry/util/genai/_tool_invocation.py +96 -0
- opentelemetry/util/genai/_upload/__init__.py +90 -0
- opentelemetry/util/genai/_upload/completion_hook.py +404 -0
- opentelemetry/util/genai/_workflow_invocation.py +115 -0
- opentelemetry/util/genai/completion_hook.py +126 -0
- opentelemetry/util/genai/environment_variables.py +71 -0
- opentelemetry/util/genai/handler.py +335 -0
- opentelemetry/util/genai/instruments.py +54 -0
- opentelemetry/util/genai/invocation.py +47 -0
- opentelemetry/util/genai/metrics.py +54 -0
- opentelemetry/util/genai/types.py +255 -0
- opentelemetry/util/genai/utils.py +143 -0
- opentelemetry/util/genai/version.py +15 -0
harness_sdk/__init__.py
ADDED
harness_sdk/agent.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import threading
|
|
4
|
+
import traceback
|
|
5
|
+
|
|
6
|
+
import distro
|
|
7
|
+
|
|
8
|
+
from harness_sdk import constants
|
|
9
|
+
from harness_sdk.agent_init import AgentInit
|
|
10
|
+
from harness_sdk.config.config import Config
|
|
11
|
+
from harness_sdk.custom_logger import get_custom_logger
|
|
12
|
+
from harness_sdk.env import get_env_value
|
|
13
|
+
from harness_sdk.instrumentation.instrumentation_definitions import (
|
|
14
|
+
SUPPORTED_LIBRARIES,
|
|
15
|
+
get_instrumentation_wrapper,
|
|
16
|
+
FLASK_KEY,
|
|
17
|
+
DJANGO_KEY,
|
|
18
|
+
FAST_API_KEY,
|
|
19
|
+
instrument_supported_contrib_without_wrapper,
|
|
20
|
+
)
|
|
21
|
+
from harness_sdk.instrumentation.genai_env import maybe_set_genai_payload_capture_env_vars
|
|
22
|
+
from harness_sdk.plugins.control import ControlPlugin, get_control_registry
|
|
23
|
+
from harness_sdk.plugins.loader import load_control_plugins, load_observability_plugins
|
|
24
|
+
from harness_sdk.version import __version__ # pylint:disable=C0413
|
|
25
|
+
|
|
26
|
+
logger = get_custom_logger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Agent:
|
|
30
|
+
_instance = None
|
|
31
|
+
_singleton_lock = threading.Lock()
|
|
32
|
+
|
|
33
|
+
def __new__(cls):
|
|
34
|
+
if cls._instance is None:
|
|
35
|
+
with cls._singleton_lock:
|
|
36
|
+
logger.debug('Creating Agent')
|
|
37
|
+
logger.debug('Python version: %s', sys.version)
|
|
38
|
+
logger.debug('Harness SDK version: %s', __version__)
|
|
39
|
+
cls._instance = super(Agent, cls).__new__(cls)
|
|
40
|
+
else:
|
|
41
|
+
logger.debug('Using existing Agent.')
|
|
42
|
+
return cls._instance
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
if getattr(self, "_initialized", False):
|
|
46
|
+
self.is_lambda = False
|
|
47
|
+
return
|
|
48
|
+
logger.debug('Initializing Agent.')
|
|
49
|
+
if not self.is_enabled():
|
|
50
|
+
return
|
|
51
|
+
try:
|
|
52
|
+
self._config = Config()
|
|
53
|
+
self._init = AgentInit(self._config)
|
|
54
|
+
load_control_plugins(self._config)
|
|
55
|
+
self._init.init_trace_provider()
|
|
56
|
+
self._init.init_propagation()
|
|
57
|
+
load_observability_plugins(self._config)
|
|
58
|
+
self._initialized = True
|
|
59
|
+
except Exception as err: # pylint: disable=W0703
|
|
60
|
+
logger.error(
|
|
61
|
+
'Failed to initialize Agent: exception=%s, stacktrace=%s',
|
|
62
|
+
err,
|
|
63
|
+
traceback.format_exc(),
|
|
64
|
+
)
|
|
65
|
+
self.is_lambda = False
|
|
66
|
+
logger.debug("Platform: %s", distro.id())
|
|
67
|
+
logger.debug("Platform version: %s", distro.version())
|
|
68
|
+
logger.debug('Harness SDK version: %s', __version__)
|
|
69
|
+
logger.debug("successfully initialized harness sdk")
|
|
70
|
+
|
|
71
|
+
if hasattr(os, 'register_at_fork'):
|
|
72
|
+
logger.info('Registering after_in_child handler.')
|
|
73
|
+
os.register_at_fork(after_in_child=self.post_fork) # pylint:disable=E1101
|
|
74
|
+
|
|
75
|
+
def post_fork(self):
|
|
76
|
+
logger.info("In post fork hook")
|
|
77
|
+
self._init.post_fork()
|
|
78
|
+
load_control_plugins(self._config)
|
|
79
|
+
|
|
80
|
+
def instrument(self, app=None, skip_libraries=None, auto_instrument=False):
|
|
81
|
+
logger.debug("Beginning instrumentation")
|
|
82
|
+
self._init.apply_config(self._config)
|
|
83
|
+
|
|
84
|
+
if skip_libraries is None:
|
|
85
|
+
skip_libraries = []
|
|
86
|
+
if not self.is_initialized():
|
|
87
|
+
logger.debug('agent is not initialized, not instrumenting')
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
maybe_set_genai_payload_capture_env_vars()
|
|
91
|
+
|
|
92
|
+
for library_key in SUPPORTED_LIBRARIES:
|
|
93
|
+
if library_key in skip_libraries:
|
|
94
|
+
logger.debug('not attempting to instrument %s', library_key)
|
|
95
|
+
continue
|
|
96
|
+
logger.debug("attempting to instrument %s", library_key)
|
|
97
|
+
self._instrument(library_key, app, auto_instrument)
|
|
98
|
+
|
|
99
|
+
if self._should_instrument_generic_contrib():
|
|
100
|
+
instrument_supported_contrib_without_wrapper(skip_libraries)
|
|
101
|
+
else:
|
|
102
|
+
logger.debug("Skipping generic contrib fallback instrumentation")
|
|
103
|
+
logger.debug("Complete instrumentation")
|
|
104
|
+
|
|
105
|
+
def _should_instrument_generic_contrib(self):
|
|
106
|
+
"""
|
|
107
|
+
Fallback instrumentation runs when no control plugin provides blocking.
|
|
108
|
+
"""
|
|
109
|
+
if get_control_registry().has_blocking_capability():
|
|
110
|
+
logger.info("Blocking control plugin active, skipping fallback instrumentation")
|
|
111
|
+
return False
|
|
112
|
+
logger.info("No blocking control plugin, enabling fallback instrumentation")
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
def _instrument(self, library_key, app=None, auto_instrument=False):
|
|
116
|
+
wrapper_instance = get_instrumentation_wrapper(library_key)
|
|
117
|
+
if wrapper_instance is None:
|
|
118
|
+
logger.debug("no instrumentation wrapper instance available for %s", library_key)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
if library_key == FLASK_KEY and app is not None:
|
|
122
|
+
wrapper_instance.with_app(app)
|
|
123
|
+
|
|
124
|
+
if library_key == DJANGO_KEY and auto_instrument is True:
|
|
125
|
+
from harness_sdk.instrumentation.django.django_auto_instrumentation_compat import ( # pylint: disable=C0415
|
|
126
|
+
add_django_auto_instr_wrappers,
|
|
127
|
+
)
|
|
128
|
+
add_django_auto_instr_wrappers(self, wrapper_instance)
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
if library_key == FAST_API_KEY:
|
|
132
|
+
from harness_sdk.instrumentation.fast_api.fast_api_auto_instrumentation_compat import ( # pylint: disable=C0415
|
|
133
|
+
add_fast_api_auto_instr_wrappers,
|
|
134
|
+
)
|
|
135
|
+
add_fast_api_auto_instr_wrappers(self, wrapper_instance)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
logger.debug("registering library %s with wrapper instance", library_key)
|
|
139
|
+
self.register_library(library_key, wrapper_instance)
|
|
140
|
+
|
|
141
|
+
def register_library(self, library_name, wrapper_instance):
|
|
142
|
+
logger.debug('attempting to register library instrumentation: %s', library_name)
|
|
143
|
+
try:
|
|
144
|
+
self._init.init_library_instrumentation(library_name, wrapper_instance)
|
|
145
|
+
except Exception as err: # pylint: disable=W0703
|
|
146
|
+
logger.debug(constants.EXCEPTION_MESSAGE, library_name, err, traceback.format_exc())
|
|
147
|
+
|
|
148
|
+
def register_control_plugin(self, plugin: ControlPlugin) -> None:
|
|
149
|
+
logger.debug('Registering control plugin %s', plugin.name)
|
|
150
|
+
plugin.on_init(self._config)
|
|
151
|
+
get_control_registry().register(plugin)
|
|
152
|
+
|
|
153
|
+
def register_processor(self, processor) -> None: # pylint: disable=R1710
|
|
154
|
+
logger.debug('Entering Agent.register_processor().')
|
|
155
|
+
if not self.is_initialized():
|
|
156
|
+
return None
|
|
157
|
+
return self._init.register_processor(processor)
|
|
158
|
+
|
|
159
|
+
def is_enabled(self) -> bool:
|
|
160
|
+
enabled = get_env_value('ENABLED')
|
|
161
|
+
if enabled and enabled.lower() == 'false':
|
|
162
|
+
logger.debug("ENABLED is disabled.")
|
|
163
|
+
return False
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
def is_initialized(self) -> bool:
|
|
167
|
+
if not self.is_enabled():
|
|
168
|
+
return False
|
|
169
|
+
if not self._initialized:
|
|
170
|
+
return False
|
|
171
|
+
return True
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'''Initialize all the components using configuration from AgentConfig'''
|
|
2
|
+
# pylint: disable=C0303
|
|
3
|
+
import os
|
|
4
|
+
import traceback
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from opentelemetry import trace
|
|
8
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
9
|
+
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
|
|
10
|
+
try:
|
|
11
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter as OTLPGrpcSpanExporter
|
|
12
|
+
except ImportError:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter as OTLPHttpSpanExporter
|
|
16
|
+
from opentelemetry.trace import ProxyTracerProvider
|
|
17
|
+
from opentelemetry.sdk.resources import Resource
|
|
18
|
+
from harness_sdk import constants
|
|
19
|
+
from harness_sdk.config import config_pb2
|
|
20
|
+
from harness_sdk.otlp_reporting import (
|
|
21
|
+
compression_type_to_otlp_grpc,
|
|
22
|
+
compression_type_to_otlp_http,
|
|
23
|
+
is_supported_encoding,
|
|
24
|
+
normalize_otlp_http_traces_endpoint,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__) # pylint: disable=C0103
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AgentInit: # pylint: disable=R0902,R0903
|
|
31
|
+
'''Initialize all the OTel components using configuration from AgentConfig'''
|
|
32
|
+
|
|
33
|
+
def __init__(self, agent_config):
|
|
34
|
+
logger.debug('Initializing AgentInit object.')
|
|
35
|
+
self._config = agent_config
|
|
36
|
+
|
|
37
|
+
if hasattr(os, 'register_at_fork'):
|
|
38
|
+
logger.info('Registering after_in_child handler.')
|
|
39
|
+
os.register_at_fork(after_in_child=self.post_fork) # pylint:disable=E1101
|
|
40
|
+
|
|
41
|
+
def post_fork(self):
|
|
42
|
+
self.apply_config(self._config) # pylint:disable=W0212
|
|
43
|
+
|
|
44
|
+
def apply_config(self, agent_config):
|
|
45
|
+
if agent_config:
|
|
46
|
+
self._config = agent_config
|
|
47
|
+
self.init_trace_provider()
|
|
48
|
+
self.init_propagation()
|
|
49
|
+
if (
|
|
50
|
+
"HA_ENABLE_CONSOLE_SPAN_EXPORTER" in os.environ
|
|
51
|
+
or "AT_ENABLE_CONSOLE_SPAN_EXPORTER" in os.environ
|
|
52
|
+
or "TA_ENABLE_CONSOLE_SPAN_EXPORTER" in os.environ
|
|
53
|
+
):
|
|
54
|
+
self.set_console_span_processor()
|
|
55
|
+
|
|
56
|
+
def init_trace_provider(self) -> None:
|
|
57
|
+
if isinstance(trace.get_tracer_provider(), ProxyTracerProvider):
|
|
58
|
+
logger.debug("no configured trace provider detected, adding one")
|
|
59
|
+
resource_attributes = {
|
|
60
|
+
"service.name": self._config.config.service_name.value,
|
|
61
|
+
"service.instance.id": os.getpid(),
|
|
62
|
+
"telemetry.sdk.version": constants.TELEMETRY_SDK_VERSION,
|
|
63
|
+
"telemetry.sdk.name": constants.TELEMETRY_SDK_NAME,
|
|
64
|
+
"telemetry.sdk.language": constants.TELEMETRY_SDK_LANGUAGE
|
|
65
|
+
}
|
|
66
|
+
if self._config.config.resource_attributes:
|
|
67
|
+
resource_attributes.update(self._config.config.resource_attributes)
|
|
68
|
+
tracer_provider = TracerProvider(
|
|
69
|
+
resource=Resource.create(resource_attributes)
|
|
70
|
+
)
|
|
71
|
+
trace.set_tracer_provider(tracer_provider)
|
|
72
|
+
else:
|
|
73
|
+
logger.debug("tracer provider already configured, skipping trace provider configuration")
|
|
74
|
+
|
|
75
|
+
def init_propagation(self) -> None:
|
|
76
|
+
propagator_list = []
|
|
77
|
+
for prop_format in self._config.config.propagation_formats:
|
|
78
|
+
if prop_format == config_pb2.PropagationFormat.TRACECONTEXT:
|
|
79
|
+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator # pylint: disable=C0415
|
|
80
|
+
propagator_list += [TraceContextTextMapPropagator()]
|
|
81
|
+
if prop_format == config_pb2.PropagationFormat.B3:
|
|
82
|
+
from opentelemetry.propagators.b3 import B3MultiFormat # pylint: disable=C0415
|
|
83
|
+
propagator_list += [B3MultiFormat()]
|
|
84
|
+
|
|
85
|
+
from opentelemetry.propagate import set_global_textmap # pylint: disable=C0415
|
|
86
|
+
from opentelemetry.propagators.composite import CompositePropagator # pylint: disable=C0415
|
|
87
|
+
set_global_textmap(CompositePropagator(propagator_list))
|
|
88
|
+
|
|
89
|
+
def init_library_instrumentation(self, instrumentation_name, wrapper_instance):
|
|
90
|
+
logger.debug("Attempting to initialize %s instrumentation", instrumentation_name)
|
|
91
|
+
try:
|
|
92
|
+
wrapper_instance.instrument()
|
|
93
|
+
except Exception as err: # pylint: disable=W0703
|
|
94
|
+
logger.debug(
|
|
95
|
+
constants.INST_WRAP_EXCEPTION_MSSG,
|
|
96
|
+
instrumentation_name,
|
|
97
|
+
err,
|
|
98
|
+
traceback.format_exc(),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def register_processor(self, processor) -> None:
|
|
102
|
+
trace.get_tracer_provider().add_span_processor(processor)
|
|
103
|
+
|
|
104
|
+
def set_console_span_processor(self) -> None:
|
|
105
|
+
console_span_exporter = ConsoleSpanExporter(
|
|
106
|
+
service_name=self._config.config.service_name)
|
|
107
|
+
simple_export_span_processor = SimpleSpanProcessor(console_span_exporter)
|
|
108
|
+
trace.get_tracer_provider().add_span_processor(simple_export_span_processor)
|
|
109
|
+
|
|
110
|
+
def _init_exporter(self, trace_reporter_type):
|
|
111
|
+
exporter_type = ''
|
|
112
|
+
exporter = None
|
|
113
|
+
exporter_endpoint = None
|
|
114
|
+
reporting = self._config.config.reporting
|
|
115
|
+
reporting_encoding = getattr(self._config, 'reporting_encoding', None)
|
|
116
|
+
if reporting_encoding and not is_supported_encoding(reporting_encoding):
|
|
117
|
+
logger.warning(
|
|
118
|
+
'Unsupported reporting encoding `%s`; OTLP protobuf encoding will be used',
|
|
119
|
+
reporting_encoding,
|
|
120
|
+
)
|
|
121
|
+
try:
|
|
122
|
+
_token = reporting.token.value
|
|
123
|
+
headers = {"harness-sdk-token": _token} if _token else {}
|
|
124
|
+
if trace_reporter_type == config_pb2.TraceReporterType.OTLP:
|
|
125
|
+
exporter_type = 'otlp'
|
|
126
|
+
exporter_endpoint = reporting.endpoint.value
|
|
127
|
+
exporter = OTLPGrpcSpanExporter(
|
|
128
|
+
endpoint=exporter_endpoint,
|
|
129
|
+
insecure=not reporting.secure.value,
|
|
130
|
+
headers=headers,
|
|
131
|
+
compression=compression_type_to_otlp_grpc(reporting.compression_type),
|
|
132
|
+
)
|
|
133
|
+
elif trace_reporter_type == config_pb2.TraceReporterType.OTLP_HTTP:
|
|
134
|
+
exporter_type = 'otlp_http'
|
|
135
|
+
exporter_endpoint = normalize_otlp_http_traces_endpoint(
|
|
136
|
+
reporting.endpoint.value
|
|
137
|
+
)
|
|
138
|
+
exporter = OTLPHttpSpanExporter(
|
|
139
|
+
endpoint=exporter_endpoint,
|
|
140
|
+
headers=headers,
|
|
141
|
+
compression=compression_type_to_otlp_http(reporting.compression_type),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if exporter:
|
|
145
|
+
logger.info('Initialized %s exporter reporting to `%s`',
|
|
146
|
+
exporter_type, exporter_endpoint)
|
|
147
|
+
return exporter
|
|
148
|
+
except Exception as err: # pylint: disable=W0703
|
|
149
|
+
logger.error('Failed to initialize %s exporter: exception=%s, stacktrace=%s',
|
|
150
|
+
exporter_type, err, traceback.format_exc())
|
|
151
|
+
return None
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import psutil
|
|
4
|
+
from harness_sdk.agent import Agent # pylint:disable=C0413
|
|
5
|
+
|
|
6
|
+
from harness_sdk.config.config import Config # pylint:disable=C0413,C0412
|
|
7
|
+
from harness_sdk.custom_logger import get_custom_logger # pylint:disable=C0413,C0412,C0411
|
|
8
|
+
|
|
9
|
+
config = Config()
|
|
10
|
+
logger = get_custom_logger(__name__)
|
|
11
|
+
|
|
12
|
+
a = Agent()
|
|
13
|
+
a.instrument(None, None, auto_instrument=True)
|
|
14
|
+
|
|
15
|
+
__POST_INIT = False
|
|
16
|
+
POST_FORK_SERVERS = ['gunicorn']
|
|
17
|
+
|
|
18
|
+
original_process = psutil.Process(os.getpid())
|
|
19
|
+
args = original_process.cmdline()
|
|
20
|
+
|
|
21
|
+
for entry in POST_FORK_SERVERS:
|
|
22
|
+
for arg in args:
|
|
23
|
+
if entry in arg:
|
|
24
|
+
__POST_INIT = True
|
|
25
|
+
logger.info('Detected server %s - deferring filter loading until post fork', entry)
|
|
26
|
+
break
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __POST_INIT is not True:
|
|
30
|
+
logger.info("Control plugins loaded during autoinstrumentation agent init")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Based upon the OTel autoinstrumentation feature
|
|
2
|
+
'''This module implements a CLI command that be used to
|
|
3
|
+
autoinstrument existing python programs that use supported
|
|
4
|
+
modules.'''
|
|
5
|
+
import argparse
|
|
6
|
+
from logging import getLogger
|
|
7
|
+
from os import environ, execl, getcwd
|
|
8
|
+
from os.path import abspath, dirname, pathsep
|
|
9
|
+
from shutil import which
|
|
10
|
+
|
|
11
|
+
logger = getLogger(__file__)
|
|
12
|
+
|
|
13
|
+
def parse_args():
|
|
14
|
+
'''Parse CLI arguments.'''
|
|
15
|
+
parser = argparse.ArgumentParser(
|
|
16
|
+
description="""
|
|
17
|
+
harness_sdk-instrument automatically instruments a Python
|
|
18
|
+
program and runs the program
|
|
19
|
+
"""
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
parser.add_argument("command", help="Your Python application.")
|
|
23
|
+
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"command_args",
|
|
26
|
+
help="Arguments for your application.",
|
|
27
|
+
nargs=argparse.REMAINDER,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return parser.parse_args()
|
|
31
|
+
|
|
32
|
+
def update_python_path() -> None:
|
|
33
|
+
'''Retrieve existing PYTHONPATH'''
|
|
34
|
+
python_path = environ.get("PYTHONPATH")
|
|
35
|
+
|
|
36
|
+
# Split the paths
|
|
37
|
+
if not python_path:
|
|
38
|
+
python_path = []
|
|
39
|
+
else:
|
|
40
|
+
python_path = python_path.split(pathsep)
|
|
41
|
+
|
|
42
|
+
# Get the current working directory
|
|
43
|
+
cwd_path = getcwd()
|
|
44
|
+
|
|
45
|
+
# If this directory is already in python_path, remove it.
|
|
46
|
+
python_path = [path for path in python_path if path != cwd_path]
|
|
47
|
+
|
|
48
|
+
# Add CWD to the front.
|
|
49
|
+
python_path.insert(0, cwd_path)
|
|
50
|
+
|
|
51
|
+
# What is the directory containing this python file?
|
|
52
|
+
filedir_path = dirname(abspath(__file__))
|
|
53
|
+
|
|
54
|
+
# If this directory is already in python_path, remove it.
|
|
55
|
+
python_path = [path for path in python_path if path != filedir_path]
|
|
56
|
+
|
|
57
|
+
# Add this directory to the front
|
|
58
|
+
python_path.insert(0, filedir_path)
|
|
59
|
+
|
|
60
|
+
# Reset PYTHONPATH environment variable
|
|
61
|
+
environ["PYTHONPATH"] = pathsep.join(python_path)
|
|
62
|
+
|
|
63
|
+
def run() -> None:
|
|
64
|
+
'''harness_sdk-instrument Entry point'''
|
|
65
|
+
args = parse_args()
|
|
66
|
+
|
|
67
|
+
# update PYTHONPATH env var
|
|
68
|
+
update_python_path()
|
|
69
|
+
|
|
70
|
+
# Get full path to the command that was passed in as an
|
|
71
|
+
# argument
|
|
72
|
+
executable = which(args.command)
|
|
73
|
+
|
|
74
|
+
# Execute the app
|
|
75
|
+
execl(executable, executable, *args.command_args)
|
|
76
|
+
|
|
77
|
+
if __name__ == '__main__':
|
|
78
|
+
run()
|
|
File without changes
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
from google.protobuf import json_format
|
|
8
|
+
|
|
9
|
+
from harness_sdk.config import config_pb2
|
|
10
|
+
from harness_sdk.config.default import DEFAULT, SDK_CONFIG_KEYS
|
|
11
|
+
from harness_sdk.config.environment import overwrite_with_environment
|
|
12
|
+
from harness_sdk.otlp_reporting import normalize_reporting_dict
|
|
13
|
+
from harness_sdk.custom_logger import get_custom_logger
|
|
14
|
+
|
|
15
|
+
logger = get_custom_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _parse_plugin_env(env_key: str) -> list | None:
|
|
19
|
+
raw = os.environ.get(env_key)
|
|
20
|
+
if not raw:
|
|
21
|
+
return None
|
|
22
|
+
return [name.strip() for name in raw.split(',') if name.strip()]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _ordered_plugin_names_from_section(section) -> list:
|
|
26
|
+
if section is None:
|
|
27
|
+
return []
|
|
28
|
+
if isinstance(section, list):
|
|
29
|
+
return [str(name).strip() for name in section if str(name).strip()]
|
|
30
|
+
if isinstance(section, dict):
|
|
31
|
+
return [
|
|
32
|
+
name for name, cfg in section.items()
|
|
33
|
+
if _is_plugin_entry_enabled(cfg)
|
|
34
|
+
]
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _is_plugin_entry_enabled(cfg) -> bool:
|
|
39
|
+
if isinstance(cfg, dict):
|
|
40
|
+
return cfg.get('enabled', True)
|
|
41
|
+
return bool(cfg)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _filter_sdk_config(file_dict):
|
|
45
|
+
if not file_dict:
|
|
46
|
+
return {}
|
|
47
|
+
return {key: file_dict[key] for key in SDK_CONFIG_KEYS if key in file_dict}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Config: # pylint:disable=R0903
|
|
51
|
+
_instance = None
|
|
52
|
+
|
|
53
|
+
def __new__(cls):
|
|
54
|
+
if not hasattr(cls, "_instance") or cls._instance is None:
|
|
55
|
+
cls._instance = super(Config, cls).__new__(cls)
|
|
56
|
+
return cls._instance
|
|
57
|
+
|
|
58
|
+
def __init__(self):
|
|
59
|
+
if not hasattr(self, '_initialized'):
|
|
60
|
+
self._initialized = True
|
|
61
|
+
(
|
|
62
|
+
self.config,
|
|
63
|
+
self.plugins_config,
|
|
64
|
+
self.enabled_control_plugins,
|
|
65
|
+
self.enabled_observability_plugins,
|
|
66
|
+
self.reporting_encoding,
|
|
67
|
+
) = build_config()
|
|
68
|
+
|
|
69
|
+
def get_plugin_option(self, plugin_name: str, option: str, default=None, plugin_type="control"):
|
|
70
|
+
section = self.plugins_config.get(plugin_type, {})
|
|
71
|
+
if plugin_name in section and isinstance(section[plugin_name], dict):
|
|
72
|
+
return section[plugin_name].get(option, default)
|
|
73
|
+
return default
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def merge_config(base_config, overriding_config):
|
|
77
|
+
for key in overriding_config:
|
|
78
|
+
if key in base_config and isinstance(base_config[key], dict):
|
|
79
|
+
if key in overriding_config:
|
|
80
|
+
base_config[key] = merge_config(base_config[key], overriding_config[key])
|
|
81
|
+
else:
|
|
82
|
+
base_config[key] = overriding_config[key]
|
|
83
|
+
return base_config
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_config():
|
|
87
|
+
agent_config = config_pb2.AgentConfig()
|
|
88
|
+
config_dict = copy.deepcopy(DEFAULT)
|
|
89
|
+
file_dict = read_from_file()
|
|
90
|
+
if file_dict is not None:
|
|
91
|
+
merge_config(config_dict, _filter_sdk_config(file_dict))
|
|
92
|
+
|
|
93
|
+
plugins_config = config_dict.pop('plugins', {})
|
|
94
|
+
|
|
95
|
+
enabled_control_plugins = _ordered_plugin_names_from_section(
|
|
96
|
+
plugins_config.get('control')
|
|
97
|
+
)
|
|
98
|
+
if not enabled_control_plugins:
|
|
99
|
+
enabled_control_plugins = _parse_plugin_env('HA_CONTROL_PLUGINS') or []
|
|
100
|
+
if not enabled_control_plugins:
|
|
101
|
+
enabled_control_plugins = _parse_plugin_env('AT_CONTROL_PLUGINS') or []
|
|
102
|
+
|
|
103
|
+
enabled_observability_plugins = _ordered_plugin_names_from_section(
|
|
104
|
+
plugins_config.get('observability')
|
|
105
|
+
)
|
|
106
|
+
if not enabled_observability_plugins:
|
|
107
|
+
enabled_observability_plugins = _parse_plugin_env('HA_OBSERVABILITY_PLUGINS')
|
|
108
|
+
if not enabled_observability_plugins:
|
|
109
|
+
enabled_observability_plugins = _parse_plugin_env('AT_OBSERVABILITY_PLUGINS')
|
|
110
|
+
if not enabled_observability_plugins:
|
|
111
|
+
enabled_observability_plugins = ['builtin_pipeline', 'builtin_span_attributes']
|
|
112
|
+
|
|
113
|
+
reporting_encoding = None
|
|
114
|
+
if 'reporting' in config_dict:
|
|
115
|
+
reporting_encoding = normalize_reporting_dict(config_dict['reporting'])
|
|
116
|
+
|
|
117
|
+
json_string = json.dumps(config_dict)
|
|
118
|
+
json_format.Parse(json_string, agent_config, ignore_unknown_fields=True)
|
|
119
|
+
|
|
120
|
+
agent_config, reporting_encoding = overwrite_with_environment(
|
|
121
|
+
agent_config, reporting_encoding
|
|
122
|
+
)
|
|
123
|
+
logger.debug(json_string)
|
|
124
|
+
return (
|
|
125
|
+
agent_config,
|
|
126
|
+
plugins_config,
|
|
127
|
+
enabled_control_plugins,
|
|
128
|
+
enabled_observability_plugins,
|
|
129
|
+
reporting_encoding,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def read_from_file():
|
|
134
|
+
config_path = _config_file_path()
|
|
135
|
+
if config_path is None or not os.path.exists(config_path):
|
|
136
|
+
logger.debug("HA_CONFIG_FILE path not set")
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
with open(config_path, 'r', encoding="UTF-8") as config_file:
|
|
140
|
+
try:
|
|
141
|
+
return yaml.safe_load(config_file)
|
|
142
|
+
except yaml.YAMLError as exc:
|
|
143
|
+
logger.debug(exc)
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _config_file_path():
|
|
148
|
+
return (
|
|
149
|
+
os.environ.get('HA_CONFIG_FILE')
|
|
150
|
+
or os.environ.get('AT_CONFIG_FILE')
|
|
151
|
+
or os.environ.get('TA_CONFIG_FILE')
|
|
152
|
+
)
|