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.
Files changed (88) hide show
  1. harness_sdk/__init__.py +5 -0
  2. harness_sdk/agent.py +171 -0
  3. harness_sdk/agent_init.py +151 -0
  4. harness_sdk/autoinstrumentation/__init__.py +0 -0
  5. harness_sdk/autoinstrumentation/sitecustomize.py +30 -0
  6. harness_sdk/autoinstrumentation/wrapper.py +78 -0
  7. harness_sdk/config/__init__.py +0 -0
  8. harness_sdk/config/config.py +152 -0
  9. harness_sdk/config/config_pb2.py +112 -0
  10. harness_sdk/config/default.py +58 -0
  11. harness_sdk/config/environment.py +149 -0
  12. harness_sdk/constants.py +9 -0
  13. harness_sdk/custom_logger.py +43 -0
  14. harness_sdk/env.py +10 -0
  15. harness_sdk/excluded_by_attribute_span_processor.py +40 -0
  16. harness_sdk/gen_ai/__init__.py +7 -0
  17. harness_sdk/gen_ai/exceptions.py +17 -0
  18. harness_sdk/instrumentation/__init__.py +222 -0
  19. harness_sdk/instrumentation/aiohttp/__init__.py +218 -0
  20. harness_sdk/instrumentation/anthropic/__init__.py +379 -0
  21. harness_sdk/instrumentation/botocore/__init__.py +11 -0
  22. harness_sdk/instrumentation/django/__init__.py +77 -0
  23. harness_sdk/instrumentation/django/django_auto_instrumentation_compat.py +70 -0
  24. harness_sdk/instrumentation/fast_api/__init__.py +222 -0
  25. harness_sdk/instrumentation/fast_api/fast_api_auto_instrumentation_compat.py +65 -0
  26. harness_sdk/instrumentation/flask/__init__.py +187 -0
  27. harness_sdk/instrumentation/genai_env.py +71 -0
  28. harness_sdk/instrumentation/grpc/__init__.py +266 -0
  29. harness_sdk/instrumentation/httpx/__init__.py +73 -0
  30. harness_sdk/instrumentation/httpx/utils.py +160 -0
  31. harness_sdk/instrumentation/instrumentation_definitions.py +190 -0
  32. harness_sdk/instrumentation/litellm/__init__.py +401 -0
  33. harness_sdk/instrumentation/mcp/__init__.py +40 -0
  34. harness_sdk/instrumentation/mcp/gen_ai_mirror.py +204 -0
  35. harness_sdk/instrumentation/mysql/__init__.py +11 -0
  36. harness_sdk/instrumentation/openai/__init__.py +340 -0
  37. harness_sdk/instrumentation/postgresql/__init__.py +13 -0
  38. harness_sdk/instrumentation/requests/__init__.py +40 -0
  39. harness_sdk/otlp_reporting.py +75 -0
  40. harness_sdk/plugins/__init__.py +12 -0
  41. harness_sdk/plugins/builtin/__init__.py +1 -0
  42. harness_sdk/plugins/builtin/pipeline.py +56 -0
  43. harness_sdk/plugins/builtin/span_attributes.py +26 -0
  44. harness_sdk/plugins/control.py +132 -0
  45. harness_sdk/plugins/loader.py +105 -0
  46. harness_sdk/plugins/observability.py +23 -0
  47. harness_sdk/sampling_span_processor.py +42 -0
  48. harness_sdk/span_attributes_processor.py +19 -0
  49. harness_sdk/version.py +1 -0
  50. harness_sdk-1.0.0.dist-info/METADATA +318 -0
  51. harness_sdk-1.0.0.dist-info/RECORD +88 -0
  52. harness_sdk-1.0.0.dist-info/WHEEL +5 -0
  53. harness_sdk-1.0.0.dist-info/entry_points.txt +6 -0
  54. harness_sdk-1.0.0.dist-info/licenses/LICENSE.md +202 -0
  55. harness_sdk-1.0.0.dist-info/licenses/NOTICE.md +28 -0
  56. harness_sdk-1.0.0.dist-info/top_level.txt +2 -0
  57. opentelemetry/instrumentation/anthropic/__init__.py +119 -0
  58. opentelemetry/instrumentation/anthropic/messages_extractors.py +251 -0
  59. opentelemetry/instrumentation/anthropic/package.py +15 -0
  60. opentelemetry/instrumentation/anthropic/patch.py +131 -0
  61. opentelemetry/instrumentation/anthropic/utils.py +243 -0
  62. opentelemetry/instrumentation/anthropic/version.py +15 -0
  63. opentelemetry/instrumentation/anthropic/wrappers.py +435 -0
  64. opentelemetry/instrumentation/openai_v2/__init__.py +168 -0
  65. opentelemetry/instrumentation/openai_v2/instruments.py +52 -0
  66. opentelemetry/instrumentation/openai_v2/package.py +18 -0
  67. opentelemetry/instrumentation/openai_v2/patch.py +952 -0
  68. opentelemetry/instrumentation/openai_v2/response_extractors.py +419 -0
  69. opentelemetry/instrumentation/openai_v2/response_wrappers.py +467 -0
  70. opentelemetry/instrumentation/openai_v2/utils.py +502 -0
  71. opentelemetry/instrumentation/openai_v2/version.py +15 -0
  72. opentelemetry/util/genai/__init__.py +13 -0
  73. opentelemetry/util/genai/_embedding_invocation.py +123 -0
  74. opentelemetry/util/genai/_inference_invocation.py +358 -0
  75. opentelemetry/util/genai/_invocation.py +140 -0
  76. opentelemetry/util/genai/_tool_invocation.py +96 -0
  77. opentelemetry/util/genai/_upload/__init__.py +90 -0
  78. opentelemetry/util/genai/_upload/completion_hook.py +404 -0
  79. opentelemetry/util/genai/_workflow_invocation.py +115 -0
  80. opentelemetry/util/genai/completion_hook.py +126 -0
  81. opentelemetry/util/genai/environment_variables.py +71 -0
  82. opentelemetry/util/genai/handler.py +335 -0
  83. opentelemetry/util/genai/instruments.py +54 -0
  84. opentelemetry/util/genai/invocation.py +47 -0
  85. opentelemetry/util/genai/metrics.py +54 -0
  86. opentelemetry/util/genai/types.py +255 -0
  87. opentelemetry/util/genai/utils.py +143 -0
  88. opentelemetry/util/genai/version.py +15 -0
@@ -0,0 +1,5 @@
1
+ """Harness Python SDK — generic instrumentation and plugin architecture."""
2
+
3
+ from harness_sdk.agent import Agent
4
+
5
+ __all__ = ["Agent"]
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
+ )