monocle-apptrace 0.3.0b2__py3-none-any.whl → 0.3.0b3__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.
Potentially problematic release.
This version of monocle-apptrace might be problematic. Click here for more details.
- monocle_apptrace/exporters/aws/s3_exporter.py +1 -1
- monocle_apptrace/exporters/aws/s3_exporter_opendal.py +126 -0
- monocle_apptrace/exporters/azure/blob_exporter_opendal.py +147 -0
- monocle_apptrace/exporters/monocle_exporters.py +38 -20
- monocle_apptrace/instrumentation/__init__.py +0 -0
- monocle_apptrace/instrumentation/common/__init__.py +0 -0
- monocle_apptrace/{constants.py → instrumentation/common/constants.py} +13 -0
- monocle_apptrace/instrumentation/common/instrumentor.py +208 -0
- monocle_apptrace/instrumentation/common/span_handler.py +154 -0
- monocle_apptrace/instrumentation/common/utils.py +171 -0
- monocle_apptrace/instrumentation/common/wrapper.py +69 -0
- monocle_apptrace/instrumentation/common/wrapper_method.py +45 -0
- monocle_apptrace/instrumentation/metamodel/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/botocore/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/botocore/_helper.py +126 -0
- monocle_apptrace/instrumentation/metamodel/botocore/entities/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/botocore/entities/inference.py +65 -0
- monocle_apptrace/instrumentation/metamodel/botocore/methods.py +16 -0
- monocle_apptrace/instrumentation/metamodel/haystack/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/haystack/_helper.py +127 -0
- monocle_apptrace/instrumentation/metamodel/haystack/entities/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/haystack/entities/inference.py +76 -0
- monocle_apptrace/instrumentation/metamodel/haystack/entities/retrieval.py +61 -0
- monocle_apptrace/instrumentation/metamodel/haystack/methods.py +42 -0
- monocle_apptrace/instrumentation/metamodel/langchain/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/langchain/_helper.py +121 -0
- monocle_apptrace/instrumentation/metamodel/langchain/entities/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/langchain/entities/inference.py +71 -0
- monocle_apptrace/instrumentation/metamodel/langchain/entities/retrieval.py +58 -0
- monocle_apptrace/instrumentation/metamodel/langchain/methods.py +105 -0
- monocle_apptrace/instrumentation/metamodel/llamaindex/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/llamaindex/_helper.py +154 -0
- monocle_apptrace/instrumentation/metamodel/llamaindex/entities/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/llamaindex/entities/inference.py +71 -0
- monocle_apptrace/instrumentation/metamodel/llamaindex/entities/retrieval.py +57 -0
- monocle_apptrace/{metamodel/maps/llamaindex_methods.json → instrumentation/metamodel/llamaindex/methods.py} +28 -31
- {monocle_apptrace-0.3.0b2.dist-info → monocle_apptrace-0.3.0b3.dist-info}/METADATA +14 -1
- monocle_apptrace-0.3.0b3.dist-info/RECORD +48 -0
- monocle_apptrace/botocore/__init__.py +0 -9
- monocle_apptrace/haystack/__init__.py +0 -9
- monocle_apptrace/haystack/wrap_pipeline.py +0 -63
- monocle_apptrace/instrumentor.py +0 -121
- monocle_apptrace/langchain/__init__.py +0 -9
- monocle_apptrace/llamaindex/__init__.py +0 -16
- monocle_apptrace/message_processing.py +0 -80
- monocle_apptrace/metamodel/README.md +0 -47
- monocle_apptrace/metamodel/entities/README.md +0 -77
- monocle_apptrace/metamodel/entities/app_hosting_types.json +0 -29
- monocle_apptrace/metamodel/entities/entities.json +0 -49
- monocle_apptrace/metamodel/entities/inference_types.json +0 -33
- monocle_apptrace/metamodel/entities/model_types.json +0 -41
- monocle_apptrace/metamodel/entities/vector_store_types.json +0 -25
- monocle_apptrace/metamodel/entities/workflow_types.json +0 -22
- monocle_apptrace/metamodel/maps/attributes/inference/botocore_entities.json +0 -27
- monocle_apptrace/metamodel/maps/attributes/inference/haystack_entities.json +0 -57
- monocle_apptrace/metamodel/maps/attributes/inference/langchain_entities.json +0 -57
- monocle_apptrace/metamodel/maps/attributes/inference/llamaindex_entities.json +0 -57
- monocle_apptrace/metamodel/maps/attributes/retrieval/haystack_entities.json +0 -31
- monocle_apptrace/metamodel/maps/attributes/retrieval/langchain_entities.json +0 -31
- monocle_apptrace/metamodel/maps/attributes/retrieval/llamaindex_entities.json +0 -31
- monocle_apptrace/metamodel/maps/botocore_methods.json +0 -13
- monocle_apptrace/metamodel/maps/haystack_methods.json +0 -45
- monocle_apptrace/metamodel/maps/langchain_methods.json +0 -129
- monocle_apptrace/metamodel/spans/README.md +0 -121
- monocle_apptrace/metamodel/spans/span_example.json +0 -140
- monocle_apptrace/metamodel/spans/span_format.json +0 -55
- monocle_apptrace/metamodel/spans/span_types.json +0 -16
- monocle_apptrace/utils.py +0 -252
- monocle_apptrace/wrap_common.py +0 -511
- monocle_apptrace/wrapper.py +0 -27
- monocle_apptrace-0.3.0b2.dist-info/RECORD +0 -48
- {monocle_apptrace-0.3.0b2.dist-info → monocle_apptrace-0.3.0b3.dist-info}/WHEEL +0 -0
- {monocle_apptrace-0.3.0b2.dist-info → monocle_apptrace-0.3.0b3.dist-info}/licenses/LICENSE +0 -0
- {monocle_apptrace-0.3.0b2.dist-info → monocle_apptrace-0.3.0b3.dist-info}/licenses/NOTICE +0 -0
|
@@ -25,7 +25,7 @@ class S3SpanExporter(SpanExporterBase):
|
|
|
25
25
|
super().__init__()
|
|
26
26
|
# Use environment variables if credentials are not provided
|
|
27
27
|
DEFAULT_FILE_PREFIX = "monocle_trace_"
|
|
28
|
-
DEFAULT_TIME_FORMAT = "%Y-%m-%
|
|
28
|
+
DEFAULT_TIME_FORMAT = "%Y-%m-%d__%H.%M.%S"
|
|
29
29
|
self.max_batch_size = 500
|
|
30
30
|
self.export_interval = 1
|
|
31
31
|
self.s3_client = boto3.client(
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import datetime
|
|
4
|
+
import logging
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Sequence
|
|
7
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
8
|
+
from opentelemetry.sdk.trace.export import SpanExportResult
|
|
9
|
+
from monocle_apptrace.exporters.base_exporter import SpanExporterBase
|
|
10
|
+
from opendal import Operator
|
|
11
|
+
from opendal.exceptions import PermissionDenied, ConfigInvalid, Unexpected
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
class OpenDALS3Exporter(SpanExporterBase):
|
|
18
|
+
def __init__(self, bucket_name=None, region_name=None):
|
|
19
|
+
super().__init__()
|
|
20
|
+
DEFAULT_FILE_PREFIX = "monocle_trace_"
|
|
21
|
+
DEFAULT_TIME_FORMAT = "%Y-%m-%d__%H.%M.%S"
|
|
22
|
+
self.max_batch_size = 500
|
|
23
|
+
self.export_interval = 1
|
|
24
|
+
self.file_prefix = DEFAULT_FILE_PREFIX
|
|
25
|
+
self.time_format = DEFAULT_TIME_FORMAT
|
|
26
|
+
self.export_queue = []
|
|
27
|
+
self.last_export_time = time.time()
|
|
28
|
+
self.bucket_name = bucket_name or os.getenv("MONOCLE_S3_BUCKET_NAME", "default-bucket")
|
|
29
|
+
|
|
30
|
+
# Initialize OpenDAL S3 operator
|
|
31
|
+
self.op = Operator(
|
|
32
|
+
"s3",
|
|
33
|
+
root = "/",
|
|
34
|
+
region=os.getenv("AWS_REGION", region_name),
|
|
35
|
+
bucket=self.bucket_name,
|
|
36
|
+
access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
|
|
37
|
+
secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
|
|
42
|
+
"""Synchronous export method that internally handles async logic."""
|
|
43
|
+
try:
|
|
44
|
+
# Run the asynchronous export logic in an event loop
|
|
45
|
+
asyncio.run(self.__export_async(spans))
|
|
46
|
+
return SpanExportResult.SUCCESS
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.error(f"Error exporting spans: {e}")
|
|
49
|
+
return SpanExportResult.FAILURE
|
|
50
|
+
|
|
51
|
+
async def __export_async(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
|
|
52
|
+
try:
|
|
53
|
+
# Add spans to the export queue
|
|
54
|
+
for span in spans:
|
|
55
|
+
self.export_queue.append(span)
|
|
56
|
+
if len(self.export_queue) >= self.max_batch_size:
|
|
57
|
+
await self.__export_spans()
|
|
58
|
+
|
|
59
|
+
# Check if it's time to force a flush
|
|
60
|
+
current_time = time.time()
|
|
61
|
+
if current_time - self.last_export_time >= self.export_interval:
|
|
62
|
+
await self.__export_spans()
|
|
63
|
+
self.last_export_time = current_time
|
|
64
|
+
|
|
65
|
+
return SpanExportResult.SUCCESS
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.error(f"Error exporting spans: {e}")
|
|
68
|
+
return SpanExportResult.FAILURE
|
|
69
|
+
|
|
70
|
+
def __serialize_spans(self, spans: Sequence[ReadableSpan]) -> str:
|
|
71
|
+
try:
|
|
72
|
+
# Serialize spans to JSON or any other format you prefer
|
|
73
|
+
valid_json_list = []
|
|
74
|
+
for span in spans:
|
|
75
|
+
try:
|
|
76
|
+
valid_json_list.append(span.to_json(indent=0).replace("\n", ""))
|
|
77
|
+
except json.JSONDecodeError as e:
|
|
78
|
+
logger.warning(f"Invalid JSON format in span data: {span.context.span_id}. Error: {e}")
|
|
79
|
+
continue
|
|
80
|
+
return "\n".join(valid_json_list) + "\n"
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.warning(f"Error serializing spans: {e}")
|
|
83
|
+
|
|
84
|
+
async def __export_spans(self):
|
|
85
|
+
if not self.export_queue:
|
|
86
|
+
return
|
|
87
|
+
# Take a batch of spans from the queue
|
|
88
|
+
batch_to_export = self.export_queue[:self.max_batch_size]
|
|
89
|
+
serialized_data = self.__serialize_spans(batch_to_export)
|
|
90
|
+
self.export_queue = self.export_queue[self.max_batch_size:]
|
|
91
|
+
try:
|
|
92
|
+
self.__upload_to_s3(serialized_data)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"Failed to upload span batch: {e}")
|
|
95
|
+
|
|
96
|
+
@SpanExporterBase.retry_with_backoff(exceptions=(Unexpected))
|
|
97
|
+
def __upload_to_s3(self, span_data_batch: str):
|
|
98
|
+
|
|
99
|
+
current_time = datetime.datetime.now().strftime(self.time_format)
|
|
100
|
+
file_name = f"{self.file_prefix}{current_time}.ndjson"
|
|
101
|
+
try:
|
|
102
|
+
# Attempt to write the span data batch to S3
|
|
103
|
+
self.op.write(file_name, span_data_batch.encode("utf-8"))
|
|
104
|
+
logger.info(f"Span batch uploaded to S3 as {file_name}.")
|
|
105
|
+
|
|
106
|
+
except PermissionDenied as e:
|
|
107
|
+
# S3 bucket is forbidden.
|
|
108
|
+
logger.error(f"Access to bucket {self.bucket_name} is forbidden (403).")
|
|
109
|
+
raise PermissionError(f"Access to bucket {self.bucket_name} is forbidden.")
|
|
110
|
+
|
|
111
|
+
except ConfigInvalid as e:
|
|
112
|
+
# Bucket does not exist.
|
|
113
|
+
if "404" in str(e):
|
|
114
|
+
logger.error("Bucket does not exist. Please check the bucket name and region.")
|
|
115
|
+
raise Exception(f"Bucket does not exist. Error: {e}")
|
|
116
|
+
else:
|
|
117
|
+
logger.error(f"Unexpected error when accessing bucket {self.bucket_name}: {e}")
|
|
118
|
+
raise e
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
122
|
+
await self.__export_spans()
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
def shutdown(self) -> None:
|
|
126
|
+
logger.info("S3SpanExporter has been shut down.")
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import datetime
|
|
4
|
+
import logging
|
|
5
|
+
import asyncio
|
|
6
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
7
|
+
from opentelemetry.sdk.trace.export import SpanExportResult
|
|
8
|
+
from typing import Sequence
|
|
9
|
+
from opendal import Operator
|
|
10
|
+
from monocle_apptrace.exporters.base_exporter import SpanExporterBase
|
|
11
|
+
from opendal.exceptions import Unexpected, PermissionDenied, NotFound
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
class OpenDALAzureExporter(SpanExporterBase):
|
|
17
|
+
def __init__(self, connection_string=None, container_name=None):
|
|
18
|
+
super().__init__()
|
|
19
|
+
DEFAULT_FILE_PREFIX = "monocle_trace_"
|
|
20
|
+
DEFAULT_TIME_FORMAT = "%Y-%m-%d_%H.%M.%S"
|
|
21
|
+
self.max_batch_size = 500
|
|
22
|
+
self.export_interval = 1
|
|
23
|
+
self.container_name = container_name
|
|
24
|
+
|
|
25
|
+
# Default values
|
|
26
|
+
self.file_prefix = DEFAULT_FILE_PREFIX
|
|
27
|
+
self.time_format = DEFAULT_TIME_FORMAT
|
|
28
|
+
|
|
29
|
+
# Validate input
|
|
30
|
+
if not connection_string:
|
|
31
|
+
connection_string = os.getenv('MONOCLE_BLOB_CONNECTION_STRING')
|
|
32
|
+
if not connection_string:
|
|
33
|
+
raise ValueError("Azure Storage connection string is not provided or set in environment variables.")
|
|
34
|
+
|
|
35
|
+
if not container_name:
|
|
36
|
+
container_name = os.getenv('MONOCLE_BLOB_CONTAINER_NAME', 'default-container')
|
|
37
|
+
endpoint, account_name , account_key = self.parse_connection_string(connection_string)
|
|
38
|
+
|
|
39
|
+
if not account_name or not account_key:
|
|
40
|
+
raise ValueError("AccountName or AccountKey missing in the connection string.")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
# Initialize OpenDAL operator with explicit credentials
|
|
44
|
+
self.operator = Operator(
|
|
45
|
+
"azblob",
|
|
46
|
+
endpoint=endpoint,
|
|
47
|
+
account_name=account_name,
|
|
48
|
+
account_key=account_key,
|
|
49
|
+
container=container_name
|
|
50
|
+
)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
raise RuntimeError(f"Failed to initialize OpenDAL operator: {e}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def parse_connection_string(self,connection_string):
|
|
56
|
+
connection_params = dict(item.split('=', 1) for item in connection_string.split(';') if '=' in item)
|
|
57
|
+
|
|
58
|
+
account_name = connection_params.get('AccountName')
|
|
59
|
+
account_key = connection_params.get('AccountKey')
|
|
60
|
+
endpoint_suffix = connection_params.get('EndpointSuffix')
|
|
61
|
+
|
|
62
|
+
if not all([account_name, account_key, endpoint_suffix]):
|
|
63
|
+
raise ValueError("Invalid connection string. Ensure it contains AccountName, AccountKey, and EndpointSuffix.")
|
|
64
|
+
|
|
65
|
+
endpoint = f"https://{account_name}.blob.{endpoint_suffix}"
|
|
66
|
+
return endpoint, account_name, account_key
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
|
|
70
|
+
"""Synchronous export method that internally handles async logic."""
|
|
71
|
+
try:
|
|
72
|
+
# Run the asynchronous export logic in an event loop
|
|
73
|
+
asyncio.run(self._export_async(spans))
|
|
74
|
+
return SpanExportResult.SUCCESS
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.error(f"Error exporting spans: {e}")
|
|
77
|
+
return SpanExportResult.FAILURE
|
|
78
|
+
|
|
79
|
+
async def _export_async(self, spans: Sequence[ReadableSpan]):
|
|
80
|
+
"""The actual async export logic is run here."""
|
|
81
|
+
# Add spans to the export queue
|
|
82
|
+
for span in spans:
|
|
83
|
+
self.export_queue.append(span)
|
|
84
|
+
if len(self.export_queue) >= self.max_batch_size:
|
|
85
|
+
await self.__export_spans()
|
|
86
|
+
|
|
87
|
+
# Force a flush if the interval has passed
|
|
88
|
+
current_time = time.time()
|
|
89
|
+
if current_time - self.last_export_time >= self.export_interval:
|
|
90
|
+
await self.__export_spans()
|
|
91
|
+
self.last_export_time = current_time
|
|
92
|
+
|
|
93
|
+
def __serialize_spans(self, spans: Sequence[ReadableSpan]) -> str:
|
|
94
|
+
try:
|
|
95
|
+
valid_json_list = []
|
|
96
|
+
for span in spans:
|
|
97
|
+
try:
|
|
98
|
+
valid_json_list.append(span.to_json(indent=0).replace("\n", ""))
|
|
99
|
+
except json.JSONDecodeError as e:
|
|
100
|
+
logger.warning(f"Invalid JSON format in span data: {span.context.span_id}. Error: {e}")
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
ndjson_data = "\n".join(valid_json_list) + "\n"
|
|
104
|
+
return ndjson_data
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.warning(f"Error serializing spans: {e}")
|
|
107
|
+
|
|
108
|
+
async def __export_spans(self):
|
|
109
|
+
if len(self.export_queue) == 0:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
batch_to_export = self.export_queue[:self.max_batch_size]
|
|
113
|
+
serialized_data = self.__serialize_spans(batch_to_export)
|
|
114
|
+
self.export_queue = self.export_queue[self.max_batch_size:]
|
|
115
|
+
try:
|
|
116
|
+
self.__upload_to_opendal(serialized_data)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.error(f"Failed to upload span batch: {e}")
|
|
119
|
+
|
|
120
|
+
@SpanExporterBase.retry_with_backoff(exceptions=(Unexpected,))
|
|
121
|
+
def __upload_to_opendal(self, span_data_batch: str):
|
|
122
|
+
current_time = datetime.datetime.now().strftime(self.time_format)
|
|
123
|
+
file_name = f"{self.file_prefix}{current_time}.ndjson"
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
self.operator.write(file_name, span_data_batch.encode('utf-8'))
|
|
127
|
+
logger.info(f"Span batch uploaded to Azure Blob Storage as {file_name}.")
|
|
128
|
+
except PermissionDenied as e:
|
|
129
|
+
# Azure Container is forbidden.
|
|
130
|
+
logger.error(f"Access to container {self.container_name} is forbidden (403).")
|
|
131
|
+
raise PermissionError(f"Access to container {self.container_name} is forbidden.")
|
|
132
|
+
|
|
133
|
+
except NotFound as e:
|
|
134
|
+
# Container does not exist.
|
|
135
|
+
if "404" in str(e):
|
|
136
|
+
logger.error("Container does not exist. Please check the container name.")
|
|
137
|
+
raise Exception(f"Container does not exist. Error: {e}")
|
|
138
|
+
else:
|
|
139
|
+
logger.error(f"Unexpected NotFound error when accessing container {self.container_name}: {e}")
|
|
140
|
+
raise e
|
|
141
|
+
|
|
142
|
+
async def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
143
|
+
await self.__export_spans()
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
def shutdown(self) -> None:
|
|
147
|
+
logger.info("OpenDALAzureExporter has been shut down.")
|
|
@@ -1,27 +1,45 @@
|
|
|
1
|
-
from typing import Dict, Any
|
|
2
|
-
import os
|
|
1
|
+
from typing import Dict, Any, List
|
|
2
|
+
import os
|
|
3
|
+
import warnings
|
|
3
4
|
from importlib import import_module
|
|
4
5
|
from opentelemetry.sdk.trace.export import SpanExporter, ConsoleSpanExporter
|
|
5
6
|
from monocle_apptrace.exporters.file_exporter import FileSpanExporter
|
|
6
7
|
|
|
7
|
-
monocle_exporters:Dict[str, Any] = {
|
|
8
|
+
monocle_exporters: Dict[str, Any] = {
|
|
8
9
|
"s3": {"module": "monocle_apptrace.exporters.aws.s3_exporter", "class": "S3SpanExporter"},
|
|
9
|
-
"blob": {"module":"monocle_apptrace.exporters.azure.blob_exporter", "class": "AzureBlobSpanExporter"},
|
|
10
|
-
"okahu": {"module":"monocle_apptrace.exporters.okahu.okahu_exporter", "class": "OkahuSpanExporter"},
|
|
11
|
-
"file": {"module":"monocle_apptrace.exporters.file_exporter", "class": "FileSpanExporter"}
|
|
10
|
+
"blob": {"module": "monocle_apptrace.exporters.azure.blob_exporter", "class": "AzureBlobSpanExporter"},
|
|
11
|
+
"okahu": {"module": "monocle_apptrace.exporters.okahu.okahu_exporter", "class": "OkahuSpanExporter"},
|
|
12
|
+
"file": {"module": "monocle_apptrace.exporters.file_exporter", "class": "FileSpanExporter"},
|
|
13
|
+
"memory": {"module": "opentelemetry.sdk.trace.export.in_memory_span_exporter", "class": "InMemorySpanExporter"},
|
|
14
|
+
"console": {"module": "opentelemetry.sdk.trace.export", "class": "ConsoleSpanExporter"}
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
17
|
+
|
|
18
|
+
def get_monocle_exporter() -> List[SpanExporter]:
|
|
19
|
+
# Retrieve the MONOCLE_EXPORTER environment variable and split it into a list
|
|
20
|
+
exporter_names = os.environ.get("MONOCLE_EXPORTER", "file").split(",")
|
|
21
|
+
exporters = []
|
|
22
|
+
|
|
23
|
+
for exporter_name in exporter_names:
|
|
24
|
+
exporter_name = exporter_name.strip()
|
|
25
|
+
try:
|
|
26
|
+
exporter_class_path = monocle_exporters[exporter_name]
|
|
27
|
+
except KeyError:
|
|
28
|
+
warnings.warn(f"Unsupported Monocle span exporter '{exporter_name}', skipping.")
|
|
29
|
+
continue
|
|
30
|
+
try:
|
|
31
|
+
exporter_module = import_module(exporter_class_path["module"])
|
|
32
|
+
exporter_class = getattr(exporter_module, exporter_class_path["class"])
|
|
33
|
+
exporters.append(exporter_class())
|
|
34
|
+
except Exception as ex:
|
|
35
|
+
warnings.warn(
|
|
36
|
+
f"Unable to initialize Monocle span exporter '{exporter_name}', error: {ex}. Using ConsoleSpanExporter as a fallback.")
|
|
37
|
+
exporters.append(ConsoleSpanExporter())
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
# If no exporters were created, default to FileSpanExporter
|
|
41
|
+
if not exporters:
|
|
42
|
+
warnings.warn("No valid Monocle span exporters configured. Defaulting to FileSpanExporter.")
|
|
43
|
+
exporters.append(FileSpanExporter())
|
|
44
|
+
|
|
45
|
+
return exporters
|
|
File without changes
|
|
File without changes
|
|
@@ -9,6 +9,8 @@ AWS_LAMBDA_FUNCTION_IDENTIFIER_ENV_NAME = "AWS_LAMBDA_FUNCTION_NAME"
|
|
|
9
9
|
AZURE_FUNCTION_IDENTIFIER_ENV_NAME = "WEBSITE_SITE_NAME"
|
|
10
10
|
AZURE_APP_SERVICE_IDENTIFIER_ENV_NAME = "WEBSITE_DEPLOYMENT_ID"
|
|
11
11
|
GITHUB_CODESPACE_IDENTIFIER_ENV_NAME = "GITHUB_REPOSITORY"
|
|
12
|
+
|
|
13
|
+
|
|
12
14
|
# Azure naming reference can be found here
|
|
13
15
|
# https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations
|
|
14
16
|
AZURE_FUNCTION_NAME = "azure.func"
|
|
@@ -34,3 +36,14 @@ service_name_map = {
|
|
|
34
36
|
AWS_LAMBDA_SERVICE_NAME: AWS_LAMBDA_FUNCTION_IDENTIFIER_ENV_NAME,
|
|
35
37
|
GITHUB_CODESPACE_SERVICE_NAME: GITHUB_CODESPACE_IDENTIFIER_ENV_NAME
|
|
36
38
|
}
|
|
39
|
+
|
|
40
|
+
WORKFLOW_TYPE_KEY = "workflow_type"
|
|
41
|
+
DATA_INPUT_KEY = "data.input"
|
|
42
|
+
DATA_OUTPUT_KEY = "data.output"
|
|
43
|
+
PROMPT_INPUT_KEY = "data.input"
|
|
44
|
+
PROMPT_OUTPUT_KEY = "data.output"
|
|
45
|
+
QUERY = "input"
|
|
46
|
+
RESPONSE = "response"
|
|
47
|
+
SESSION_PROPERTIES_KEY = "session"
|
|
48
|
+
INFRA_SERVICE_KEY = "infra_service_name"
|
|
49
|
+
META_DATA = 'metadata'
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Collection, Dict, List, Union
|
|
3
|
+
import random
|
|
4
|
+
import uuid
|
|
5
|
+
from opentelemetry import trace
|
|
6
|
+
from opentelemetry.context import attach, get_value, set_value, get_current, detach
|
|
7
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
8
|
+
from opentelemetry.instrumentation.utils import unwrap
|
|
9
|
+
from opentelemetry.trace import SpanContext
|
|
10
|
+
from opentelemetry.sdk.trace import TracerProvider, Span, id_generator
|
|
11
|
+
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
|
|
12
|
+
from opentelemetry.sdk.trace import Span, TracerProvider
|
|
13
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanProcessor
|
|
14
|
+
from opentelemetry.trace import get_tracer
|
|
15
|
+
from wrapt import wrap_function_wrapper
|
|
16
|
+
from opentelemetry.trace.propagation import set_span_in_context
|
|
17
|
+
from monocle_apptrace.exporters.monocle_exporters import get_monocle_exporter
|
|
18
|
+
from monocle_apptrace.instrumentation.common.span_handler import SpanHandler
|
|
19
|
+
from monocle_apptrace.instrumentation.common.wrapper_method import (
|
|
20
|
+
DEFAULT_METHODS_LIST,
|
|
21
|
+
WrapperMethod,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
SESSION_PROPERTIES_KEY = "session"
|
|
27
|
+
|
|
28
|
+
_instruments = ()
|
|
29
|
+
|
|
30
|
+
monocle_tracer_provider: TracerProvider = None
|
|
31
|
+
|
|
32
|
+
MONOCLE_INSTRUMENTOR = "monocle_apptrace"
|
|
33
|
+
|
|
34
|
+
class MonocleInstrumentor(BaseInstrumentor):
|
|
35
|
+
workflow_name: str = ""
|
|
36
|
+
user_wrapper_methods: list[Union[dict,WrapperMethod]] = []
|
|
37
|
+
instrumented_method_list: list[object] = []
|
|
38
|
+
handlers:Dict[str,SpanHandler] = {} # dict of handlers
|
|
39
|
+
union_with_default_methods: bool = False
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
handlers,
|
|
44
|
+
user_wrapper_methods: list[Union[dict,WrapperMethod]] = None,
|
|
45
|
+
union_with_default_methods: bool = True
|
|
46
|
+
) -> None:
|
|
47
|
+
self.user_wrapper_methods = user_wrapper_methods or []
|
|
48
|
+
self.handlers = handlers or {'default':SpanHandler()}
|
|
49
|
+
self.union_with_default_methods = union_with_default_methods
|
|
50
|
+
super().__init__()
|
|
51
|
+
|
|
52
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
53
|
+
return _instruments
|
|
54
|
+
|
|
55
|
+
def _instrument(self, **kwargs):
|
|
56
|
+
tracer_provider: TracerProvider = kwargs.get("tracer_provider")
|
|
57
|
+
global monocle_tracer_provider
|
|
58
|
+
monocle_tracer_provider = tracer_provider
|
|
59
|
+
tracer = get_tracer(instrumenting_module_name=MONOCLE_INSTRUMENTOR, tracer_provider=tracer_provider)
|
|
60
|
+
|
|
61
|
+
final_method_list = []
|
|
62
|
+
if self.union_with_default_methods is True:
|
|
63
|
+
final_method_list= final_method_list + DEFAULT_METHODS_LIST
|
|
64
|
+
|
|
65
|
+
for method in self.user_wrapper_methods:
|
|
66
|
+
if isinstance(method, dict):
|
|
67
|
+
final_method_list.append(method)
|
|
68
|
+
elif isinstance(method, WrapperMethod):
|
|
69
|
+
final_method_list.append(method.to_dict())
|
|
70
|
+
|
|
71
|
+
for method_config in final_method_list:
|
|
72
|
+
target_package = method_config.get("package", None)
|
|
73
|
+
target_object = method_config.get("object", None)
|
|
74
|
+
target_method = method_config.get("method", None)
|
|
75
|
+
wrapped_by = method_config.get("wrapper_method", None)
|
|
76
|
+
#get the requisite handler or default one
|
|
77
|
+
handler_key = method_config.get("span_handler",'default')
|
|
78
|
+
try:
|
|
79
|
+
handler = self.handlers.get(handler_key)
|
|
80
|
+
wrap_function_wrapper(
|
|
81
|
+
target_package,
|
|
82
|
+
f"{target_object}.{target_method}" if target_object else target_method,
|
|
83
|
+
wrapped_by(tracer, handler, method_config),
|
|
84
|
+
)
|
|
85
|
+
self.instrumented_method_list.append(method_config)
|
|
86
|
+
except ModuleNotFoundError as e:
|
|
87
|
+
logger.debug(f"ignoring module {e.name}")
|
|
88
|
+
|
|
89
|
+
except Exception as ex:
|
|
90
|
+
logger.error(f"""_instrument wrap exception: {str(ex)}
|
|
91
|
+
for package: {target_package},
|
|
92
|
+
object:{target_object},
|
|
93
|
+
method:{target_method}""")
|
|
94
|
+
|
|
95
|
+
def _uninstrument(self, **kwargs):
|
|
96
|
+
for wrapped_method in self.instrumented_method_list:
|
|
97
|
+
try:
|
|
98
|
+
wrap_package = wrapped_method.get("package")
|
|
99
|
+
wrap_object = wrapped_method.get("object")
|
|
100
|
+
wrap_method = wrapped_method.get("method")
|
|
101
|
+
unwrap(
|
|
102
|
+
f"{wrap_package}.{wrap_object}" if wrap_object else wrap_package,
|
|
103
|
+
wrap_method,
|
|
104
|
+
)
|
|
105
|
+
except Exception as ex:
|
|
106
|
+
logger.error(f"""_instrument unwrap exception: {str(ex)}
|
|
107
|
+
for package: {wrap_package},
|
|
108
|
+
object:{wrap_object},
|
|
109
|
+
method:{wrap_method}""")
|
|
110
|
+
|
|
111
|
+
def setup_monocle_telemetry(
|
|
112
|
+
workflow_name: str,
|
|
113
|
+
span_processors: List[SpanProcessor] = None,
|
|
114
|
+
span_handlers: Dict[str,SpanHandler] = None,
|
|
115
|
+
wrapper_methods: List[Union[dict,WrapperMethod]] = None,
|
|
116
|
+
union_with_default_methods: bool = True) -> None:
|
|
117
|
+
resource = Resource(attributes={
|
|
118
|
+
SERVICE_NAME: workflow_name
|
|
119
|
+
})
|
|
120
|
+
exporters = get_monocle_exporter()
|
|
121
|
+
span_processors = span_processors or [BatchSpanProcessor(exporter) for exporter in exporters]
|
|
122
|
+
trace_provider = TracerProvider(resource=resource)
|
|
123
|
+
attach(set_value("workflow_name", workflow_name))
|
|
124
|
+
tracer_provider_default = trace.get_tracer_provider()
|
|
125
|
+
provider_type = type(tracer_provider_default).__name__
|
|
126
|
+
is_proxy_provider = "Proxy" in provider_type
|
|
127
|
+
for processor in span_processors:
|
|
128
|
+
processor.on_start = on_processor_start
|
|
129
|
+
if not is_proxy_provider:
|
|
130
|
+
tracer_provider_default.add_span_processor(processor)
|
|
131
|
+
else:
|
|
132
|
+
trace_provider.add_span_processor(processor)
|
|
133
|
+
if is_proxy_provider:
|
|
134
|
+
trace.set_tracer_provider(trace_provider)
|
|
135
|
+
instrumentor = MonocleInstrumentor(user_wrapper_methods=wrapper_methods or [],
|
|
136
|
+
handlers=span_handlers, union_with_default_methods = union_with_default_methods)
|
|
137
|
+
# instrumentor.app_name = workflow_name
|
|
138
|
+
if not instrumentor.is_instrumented_by_opentelemetry:
|
|
139
|
+
instrumentor.instrument(trace_provider=trace_provider)
|
|
140
|
+
|
|
141
|
+
return instrumentor
|
|
142
|
+
|
|
143
|
+
def on_processor_start(span: Span, parent_context):
|
|
144
|
+
context_properties = get_value(SESSION_PROPERTIES_KEY)
|
|
145
|
+
if context_properties is not None:
|
|
146
|
+
for key, value in context_properties.items():
|
|
147
|
+
span.set_attribute(
|
|
148
|
+
f"{SESSION_PROPERTIES_KEY}.{key}", value
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def set_context_properties(properties: dict) -> None:
|
|
152
|
+
attach(set_value(SESSION_PROPERTIES_KEY, properties))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def propagate_trace_id(traceId = "", use_trace_context = False):
|
|
156
|
+
try:
|
|
157
|
+
if traceId.startswith("0x"):
|
|
158
|
+
traceId = traceId.lstrip("0x")
|
|
159
|
+
tracer = get_tracer(instrumenting_module_name= MONOCLE_INSTRUMENTOR, tracer_provider= monocle_tracer_provider)
|
|
160
|
+
initial_id_generator = tracer.id_generator
|
|
161
|
+
_parent_span_context = get_current() if use_trace_context else None
|
|
162
|
+
if traceId and is_valid_trace_id_uuid(traceId):
|
|
163
|
+
tracer.id_generator = FixedIdGenerator(uuid.UUID(traceId).int)
|
|
164
|
+
|
|
165
|
+
span = tracer.start_span(name = "parent_placeholder_span", context= _parent_span_context)
|
|
166
|
+
updated_span_context = set_span_in_context(span=span, context= _parent_span_context)
|
|
167
|
+
updated_span_context = set_value("root_span_id", span.get_span_context().span_id, updated_span_context)
|
|
168
|
+
token = attach(updated_span_context)
|
|
169
|
+
|
|
170
|
+
span.end()
|
|
171
|
+
tracer.id_generator = initial_id_generator
|
|
172
|
+
return token
|
|
173
|
+
except:
|
|
174
|
+
logger.warning("Failed to propagate trace id")
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def propagate_trace_id_from_traceparent():
|
|
179
|
+
propagate_trace_id(use_trace_context = True)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def stop_propagate_trace_id(token) -> None:
|
|
183
|
+
try:
|
|
184
|
+
detach(token)
|
|
185
|
+
except:
|
|
186
|
+
logger.warning("Failed to stop propagating trace id")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def is_valid_trace_id_uuid(traceId: str) -> bool:
|
|
190
|
+
try:
|
|
191
|
+
uuid.UUID(traceId)
|
|
192
|
+
return True
|
|
193
|
+
except:
|
|
194
|
+
pass
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class FixedIdGenerator(id_generator.IdGenerator):
|
|
199
|
+
def __init__(
|
|
200
|
+
self,
|
|
201
|
+
trace_id: int) -> None:
|
|
202
|
+
self.trace_id = trace_id
|
|
203
|
+
|
|
204
|
+
def generate_span_id(self) -> int:
|
|
205
|
+
return random.getrandbits(64)
|
|
206
|
+
|
|
207
|
+
def generate_trace_id(self) -> int:
|
|
208
|
+
return self.trace_id
|