monocle-apptrace 0.1.1__py3-none-any.whl → 0.3.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.
Potentially problematic release.
This version of monocle-apptrace might be problematic. Click here for more details.
- monocle_apptrace/__init__.py +1 -0
- monocle_apptrace/__main__.py +19 -0
- monocle_apptrace/exporters/aws/s3_exporter.py +181 -0
- monocle_apptrace/exporters/aws/s3_exporter_opendal.py +137 -0
- monocle_apptrace/exporters/azure/blob_exporter.py +146 -0
- monocle_apptrace/exporters/azure/blob_exporter_opendal.py +162 -0
- monocle_apptrace/exporters/base_exporter.py +48 -0
- monocle_apptrace/exporters/exporter_processor.py +144 -0
- monocle_apptrace/exporters/file_exporter.py +16 -0
- monocle_apptrace/exporters/monocle_exporters.py +55 -0
- monocle_apptrace/exporters/okahu/okahu_exporter.py +117 -0
- monocle_apptrace/instrumentation/__init__.py +1 -0
- monocle_apptrace/instrumentation/common/__init__.py +2 -0
- monocle_apptrace/instrumentation/common/constants.py +70 -0
- monocle_apptrace/instrumentation/common/instrumentor.py +362 -0
- monocle_apptrace/instrumentation/common/span_handler.py +220 -0
- monocle_apptrace/instrumentation/common/utils.py +356 -0
- monocle_apptrace/instrumentation/common/wrapper.py +92 -0
- monocle_apptrace/instrumentation/common/wrapper_method.py +72 -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 +95 -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/handlers/botocore_span_handler.py +26 -0
- monocle_apptrace/instrumentation/metamodel/botocore/methods.py +16 -0
- monocle_apptrace/instrumentation/metamodel/flask/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/flask/_helper.py +29 -0
- monocle_apptrace/instrumentation/metamodel/flask/methods.py +13 -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 +43 -0
- monocle_apptrace/instrumentation/metamodel/langchain/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/langchain/_helper.py +127 -0
- monocle_apptrace/instrumentation/metamodel/langchain/entities/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/langchain/entities/inference.py +72 -0
- monocle_apptrace/instrumentation/metamodel/langchain/entities/retrieval.py +58 -0
- monocle_apptrace/{metamodel/maps/lang_chain_methods.json → instrumentation/metamodel/langchain/methods.py} +48 -43
- monocle_apptrace/instrumentation/metamodel/langgraph/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/langgraph/_helper.py +48 -0
- monocle_apptrace/instrumentation/metamodel/langgraph/entities/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/langgraph/entities/inference.py +56 -0
- monocle_apptrace/instrumentation/metamodel/langgraph/methods.py +14 -0
- monocle_apptrace/instrumentation/metamodel/llamaindex/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/llamaindex/_helper.py +172 -0
- monocle_apptrace/instrumentation/metamodel/llamaindex/entities/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/llamaindex/entities/agent.py +47 -0
- monocle_apptrace/instrumentation/metamodel/llamaindex/entities/inference.py +73 -0
- monocle_apptrace/instrumentation/metamodel/llamaindex/entities/retrieval.py +57 -0
- monocle_apptrace/instrumentation/metamodel/llamaindex/methods.py +101 -0
- monocle_apptrace/instrumentation/metamodel/openai/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/openai/_helper.py +112 -0
- monocle_apptrace/instrumentation/metamodel/openai/entities/__init__.py +0 -0
- monocle_apptrace/instrumentation/metamodel/openai/entities/inference.py +71 -0
- monocle_apptrace/instrumentation/metamodel/openai/entities/retrieval.py +43 -0
- monocle_apptrace/instrumentation/metamodel/openai/methods.py +45 -0
- monocle_apptrace/instrumentation/metamodel/requests/__init__.py +4 -0
- monocle_apptrace/instrumentation/metamodel/requests/_helper.py +31 -0
- monocle_apptrace/instrumentation/metamodel/requests/methods.py +12 -0
- {monocle_apptrace-0.1.1.dist-info → monocle_apptrace-0.3.0.dist-info}/METADATA +23 -2
- monocle_apptrace-0.3.0.dist-info/RECORD +68 -0
- {monocle_apptrace-0.1.1.dist-info → monocle_apptrace-0.3.0.dist-info}/WHEEL +1 -1
- monocle_apptrace/constants.py +0 -22
- monocle_apptrace/haystack/__init__.py +0 -9
- monocle_apptrace/haystack/wrap_node.py +0 -27
- monocle_apptrace/haystack/wrap_openai.py +0 -44
- monocle_apptrace/haystack/wrap_pipeline.py +0 -62
- monocle_apptrace/instrumentor.py +0 -124
- monocle_apptrace/langchain/__init__.py +0 -6
- monocle_apptrace/llamaindex/__init__.py +0 -15
- monocle_apptrace/metamodel/README.md +0 -47
- monocle_apptrace/metamodel/entities/README.md +0 -54
- monocle_apptrace/metamodel/entities/entity_types.json +0 -157
- monocle_apptrace/metamodel/entities/entity_types.py +0 -51
- monocle_apptrace/metamodel/maps/haystack_methods.json +0 -25
- monocle_apptrace/metamodel/maps/llama_index_methods.json +0 -70
- 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/utils.py +0 -93
- monocle_apptrace/wrap_common.py +0 -311
- monocle_apptrace/wrapper.py +0 -24
- monocle_apptrace-0.1.1.dist-info/RECORD +0 -29
- {monocle_apptrace-0.1.1.dist-info → monocle_apptrace-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {monocle_apptrace-0.1.1.dist-info → monocle_apptrace-0.3.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import random
|
|
3
|
+
import logging
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
6
|
+
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
|
7
|
+
from typing import Sequence
|
|
8
|
+
import asyncio
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
class SpanExporterBase(ABC):
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.backoff_factor = 2
|
|
15
|
+
self.max_retries = 10
|
|
16
|
+
self.export_queue = []
|
|
17
|
+
self.last_export_time = time.time()
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
async def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
async def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def shutdown(self) -> None:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def retry_with_backoff(retries=3, backoff_in_seconds=1, max_backoff_in_seconds=32, exceptions=(Exception,)):
|
|
32
|
+
def decorator(func):
|
|
33
|
+
def wrapper(*args, **kwargs):
|
|
34
|
+
attempt = 0
|
|
35
|
+
while attempt < retries:
|
|
36
|
+
try:
|
|
37
|
+
return func(*args, **kwargs)
|
|
38
|
+
except exceptions as e:
|
|
39
|
+
attempt += 1
|
|
40
|
+
sleep_time = min(max_backoff_in_seconds, backoff_in_seconds * (2 ** (attempt - 1)))
|
|
41
|
+
sleep_time = sleep_time * (1 + random.uniform(-0.1, 0.1)) # Add jitter
|
|
42
|
+
logger.warning(f"Network connectivity error, Attempt {attempt} failed: {e}. Retrying in {sleep_time:.2f} seconds...")
|
|
43
|
+
time.sleep(sleep_time)
|
|
44
|
+
raise Exception(f"Failed after {retries} attempts")
|
|
45
|
+
|
|
46
|
+
return wrapper
|
|
47
|
+
|
|
48
|
+
return decorator
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import queue
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from typing import Callable
|
|
8
|
+
import requests
|
|
9
|
+
from monocle_apptrace.instrumentation.common.constants import AWS_LAMBDA_ENV_NAME
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
LAMBDA_EXTENSION_NAME = "AsyncProcessorMonocle"
|
|
13
|
+
|
|
14
|
+
class ExportTaskProcessor(ABC):
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def start(self):
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def stop(self):
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def queue_task(self, async_task: Callable[[Callable, any], any] = None, args: any = None, is_root_span: bool = False):
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
class LambdaExportTaskProcessor(ExportTaskProcessor):
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
span_check_interval_seconds: int = 1,
|
|
33
|
+
max_time_allowed_seconds: int = 30):
|
|
34
|
+
# An internal queue used by the handler to notify the extension that it can
|
|
35
|
+
# start processing the async task.
|
|
36
|
+
self.async_tasks_queue = queue.Queue()
|
|
37
|
+
self.span_check_interval = span_check_interval_seconds
|
|
38
|
+
self.max_time_allowed = max_time_allowed_seconds
|
|
39
|
+
|
|
40
|
+
def start(self):
|
|
41
|
+
try:
|
|
42
|
+
self._start_async_processor()
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.error(f"LambdaExportTaskProcessor| Failed to start. {e}")
|
|
45
|
+
|
|
46
|
+
def stop(self):
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
def queue_task(self, async_task=None, args=None, is_root_span=False):
|
|
50
|
+
self.async_tasks_queue.put((async_task, args, is_root_span))
|
|
51
|
+
|
|
52
|
+
def set_sagemaker_model(self, endpoint_name: str, span: dict[str, dict[str, str]]):
|
|
53
|
+
try:
|
|
54
|
+
try:
|
|
55
|
+
import boto3
|
|
56
|
+
except ImportError:
|
|
57
|
+
logger.error("LambdaExportTaskProcessor| Failed to import boto3")
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
client = boto3.client('sagemaker')
|
|
61
|
+
response = client.describe_endpoint(
|
|
62
|
+
EndpointName=endpoint_name
|
|
63
|
+
)
|
|
64
|
+
endpoint_config_name = response["EndpointConfigName"]
|
|
65
|
+
endpoint_config_response = client.describe_endpoint_config(
|
|
66
|
+
EndpointConfigName=endpoint_config_name
|
|
67
|
+
)
|
|
68
|
+
model_name = endpoint_config_response["ProductionVariants"][0]["ModelName"]
|
|
69
|
+
model_name_response = client.describe_model(ModelName = model_name)
|
|
70
|
+
model_name_id = ""
|
|
71
|
+
try:
|
|
72
|
+
model_name_id = model_name_response["PrimaryContainer"]["Environment"]["HF_MODEL_ID"]
|
|
73
|
+
except:
|
|
74
|
+
pass
|
|
75
|
+
span["attributes"]["model_name"] = model_name_id
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error(f"LambdaExportTaskProcessor| Failed to get sagemaker model. {e}")
|
|
78
|
+
|
|
79
|
+
def update_spans(self, export_args):
|
|
80
|
+
try:
|
|
81
|
+
if 'batch' in export_args:
|
|
82
|
+
for span in export_args["batch"]:
|
|
83
|
+
try:
|
|
84
|
+
if len(span["attributes"]["sagemaker_endpoint_name"]) > 0 :
|
|
85
|
+
self.set_sagemaker_model(endpoint_name=span["attributes"]["sagemaker_endpoint_name"], span=span)
|
|
86
|
+
except:
|
|
87
|
+
pass
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error(f"LambdaExportTaskProcessor| Failed to update spans. {e}")
|
|
90
|
+
|
|
91
|
+
def _start_async_processor(self):
|
|
92
|
+
# Register internal extension
|
|
93
|
+
logger.debug(f"[{LAMBDA_EXTENSION_NAME}] Registering with Lambda service...")
|
|
94
|
+
response = requests.post(
|
|
95
|
+
url=f"http://{os.environ['AWS_LAMBDA_RUNTIME_API']}/2020-01-01/extension/register",
|
|
96
|
+
json={'events': ['INVOKE']},
|
|
97
|
+
headers={'Lambda-Extension-Name': LAMBDA_EXTENSION_NAME}
|
|
98
|
+
)
|
|
99
|
+
ext_id = response.headers['Lambda-Extension-Identifier']
|
|
100
|
+
logger.debug(f"[{LAMBDA_EXTENSION_NAME}] Registered with ID: {ext_id}")
|
|
101
|
+
|
|
102
|
+
def process_tasks():
|
|
103
|
+
while True:
|
|
104
|
+
# Call /next to get notified when there is a new invocation and let
|
|
105
|
+
# Lambda know that we are done processing the previous task.
|
|
106
|
+
|
|
107
|
+
logger.debug(f"[{LAMBDA_EXTENSION_NAME}] Waiting for invocation...")
|
|
108
|
+
response = requests.get(
|
|
109
|
+
url=f"http://{os.environ['AWS_LAMBDA_RUNTIME_API']}/2020-01-01/extension/event/next",
|
|
110
|
+
headers={'Lambda-Extension-Identifier': ext_id},
|
|
111
|
+
timeout=None
|
|
112
|
+
)
|
|
113
|
+
root_span_found = False
|
|
114
|
+
# all values in seconds
|
|
115
|
+
total_time_elapsed = 0
|
|
116
|
+
while root_span_found is False and total_time_elapsed < self.max_time_allowed:
|
|
117
|
+
logger.debug(response.json())
|
|
118
|
+
# Get next task from internal queue
|
|
119
|
+
logger.info(f"[{LAMBDA_EXTENSION_NAME}] Async thread running, waiting for task from handler")
|
|
120
|
+
while self.async_tasks_queue.empty() is False :
|
|
121
|
+
logger.info(f"[{LAMBDA_EXTENSION_NAME}] Processing task from handler")
|
|
122
|
+
async_task, arg, is_root_span = self.async_tasks_queue.get()
|
|
123
|
+
root_span_found = is_root_span
|
|
124
|
+
# self.update_spans(export_args=arg)
|
|
125
|
+
|
|
126
|
+
if async_task is None:
|
|
127
|
+
# No task to run this invocation
|
|
128
|
+
logger.debug(f"[{LAMBDA_EXTENSION_NAME}] Received null task. Ignoring.")
|
|
129
|
+
else:
|
|
130
|
+
# Invoke task
|
|
131
|
+
logger.debug(f"[{LAMBDA_EXTENSION_NAME}] Received async task from handler. Starting task.")
|
|
132
|
+
async_task(arg)
|
|
133
|
+
total_time_elapsed+=self.span_check_interval
|
|
134
|
+
logger.info(f"[{LAMBDA_EXTENSION_NAME}] Waiting for root span. total_time_elapsed: {total_time_elapsed}, root_span_found: {root_span_found}.")
|
|
135
|
+
time.sleep(self.span_check_interval)
|
|
136
|
+
|
|
137
|
+
logger.debug(f"[{LAMBDA_EXTENSION_NAME}] Finished processing task. total_time_elapsed: {total_time_elapsed}, root_span_found: {root_span_found}.")
|
|
138
|
+
|
|
139
|
+
# Start processing extension events in a separate thread
|
|
140
|
+
threading.Thread(target=process_tasks, daemon=True, name=LAMBDA_EXTENSION_NAME).start()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def is_aws_lambda_environment():
|
|
144
|
+
return AWS_LAMBDA_ENV_NAME in os.environ
|
|
@@ -7,6 +7,7 @@ from typing import Optional, Callable, Sequence
|
|
|
7
7
|
from opentelemetry.sdk.trace import ReadableSpan
|
|
8
8
|
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
|
9
9
|
from opentelemetry.sdk.resources import SERVICE_NAME
|
|
10
|
+
from monocle_apptrace.exporters.exporter_processor import ExportTaskProcessor
|
|
10
11
|
|
|
11
12
|
DEFAULT_FILE_PREFIX:str = "monocle_trace_"
|
|
12
13
|
DEFAULT_TIME_FORMAT:str = "%Y-%m-%d_%H.%M.%S"
|
|
@@ -25,6 +26,7 @@ class FileSpanExporter(SpanExporter):
|
|
|
25
26
|
[ReadableSpan], str
|
|
26
27
|
] = lambda span: span.to_json()
|
|
27
28
|
+ linesep,
|
|
29
|
+
task_processor: Optional[ExportTaskProcessor] = None
|
|
28
30
|
):
|
|
29
31
|
self.out_handle:TextIOWrapper = None
|
|
30
32
|
self.formatter = formatter
|
|
@@ -32,8 +34,20 @@ class FileSpanExporter(SpanExporter):
|
|
|
32
34
|
self.output_path = out_path
|
|
33
35
|
self.file_prefix = file_prefix
|
|
34
36
|
self.time_format = time_format
|
|
37
|
+
self.task_processor = task_processor
|
|
38
|
+
if self.task_processor is not None:
|
|
39
|
+
self.task_processor.start()
|
|
35
40
|
|
|
36
41
|
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
|
|
42
|
+
if self.task_processor is not None and callable(getattr(self.task_processor, 'queue_task', None)):
|
|
43
|
+
# Check if any span is a root span (no parent)
|
|
44
|
+
is_root_span = any(not span.parent for span in spans)
|
|
45
|
+
self.task_processor.queue_task(self._process_spans, spans, is_root_span)
|
|
46
|
+
return SpanExportResult.SUCCESS
|
|
47
|
+
else:
|
|
48
|
+
return self._process_spans(spans)
|
|
49
|
+
|
|
50
|
+
def _process_spans(self, spans: Sequence[ReadableSpan], is_root_span: bool = False) -> SpanExportResult:
|
|
37
51
|
for span in spans:
|
|
38
52
|
if span.context.trace_id != self.current_trace_id:
|
|
39
53
|
self.rotate_file(span.resource.attributes[SERVICE_NAME],
|
|
@@ -60,4 +74,6 @@ class FileSpanExporter(SpanExporter):
|
|
|
60
74
|
self.out_handle = None
|
|
61
75
|
|
|
62
76
|
def shutdown(self) -> None:
|
|
77
|
+
if hasattr(self, 'task_processor') and self.task_processor is not None:
|
|
78
|
+
self.task_processor.stop()
|
|
63
79
|
self.reset_handle()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from typing import Dict, Any, List
|
|
2
|
+
import os
|
|
3
|
+
import logging
|
|
4
|
+
from importlib import import_module
|
|
5
|
+
from opentelemetry.sdk.trace.export import SpanExporter, ConsoleSpanExporter
|
|
6
|
+
from monocle_apptrace.exporters.exporter_processor import LambdaExportTaskProcessor, is_aws_lambda_environment
|
|
7
|
+
from monocle_apptrace.exporters.file_exporter import FileSpanExporter
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
monocle_exporters: Dict[str, Any] = {
|
|
12
|
+
"s3": {"module": "monocle_apptrace.exporters.aws.s3_exporter", "class": "S3SpanExporter"},
|
|
13
|
+
"blob": {"module": "monocle_apptrace.exporters.azure.blob_exporter", "class": "AzureBlobSpanExporter"},
|
|
14
|
+
"okahu": {"module": "monocle_apptrace.exporters.okahu.okahu_exporter", "class": "OkahuSpanExporter"},
|
|
15
|
+
"file": {"module": "monocle_apptrace.exporters.file_exporter", "class": "FileSpanExporter"},
|
|
16
|
+
"memory": {"module": "opentelemetry.sdk.trace.export.in_memory_span_exporter", "class": "InMemorySpanExporter"},
|
|
17
|
+
"console": {"module": "opentelemetry.sdk.trace.export", "class": "ConsoleSpanExporter"}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_monocle_exporter() -> List[SpanExporter]:
|
|
22
|
+
# Retrieve the MONOCLE_EXPORTER environment variable and split it into a list
|
|
23
|
+
exporter_names = os.environ.get("MONOCLE_EXPORTER", "file").split(",")
|
|
24
|
+
exporters = []
|
|
25
|
+
|
|
26
|
+
# Create task processor for AWS Lambda environment
|
|
27
|
+
task_processor = LambdaExportTaskProcessor() if is_aws_lambda_environment() else None
|
|
28
|
+
|
|
29
|
+
for exporter_name in exporter_names:
|
|
30
|
+
exporter_name = exporter_name.strip()
|
|
31
|
+
try:
|
|
32
|
+
exporter_class_path = monocle_exporters[exporter_name]
|
|
33
|
+
except KeyError:
|
|
34
|
+
logger.debug(f"Unsupported Monocle span exporter '{exporter_name}', skipping.")
|
|
35
|
+
continue
|
|
36
|
+
try:
|
|
37
|
+
exporter_module = import_module(exporter_class_path["module"])
|
|
38
|
+
exporter_class = getattr(exporter_module, exporter_class_path["class"])
|
|
39
|
+
# Pass task_processor to all exporters when in AWS Lambda environment
|
|
40
|
+
if task_processor is not None and exporter_module.__name__.startswith("monocle_apptrace"):
|
|
41
|
+
exporters.append(exporter_class(task_processor=task_processor))
|
|
42
|
+
else:
|
|
43
|
+
exporters.append(exporter_class())
|
|
44
|
+
except Exception as ex:
|
|
45
|
+
logger.debug(
|
|
46
|
+
f"Unable to initialize Monocle span exporter '{exporter_name}', error: {ex}. Using ConsoleSpanExporter as a fallback.")
|
|
47
|
+
exporters.append(ConsoleSpanExporter())
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
# If no exporters were created, default to FileSpanExporter
|
|
51
|
+
if not exporters:
|
|
52
|
+
logger.debug("No valid Monocle span exporters configured. Defaulting to FileSpanExporter.")
|
|
53
|
+
exporters.append(FileSpanExporter())
|
|
54
|
+
|
|
55
|
+
return exporters
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from typing import Callable, Optional, Sequence
|
|
5
|
+
import requests
|
|
6
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
7
|
+
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult, ConsoleSpanExporter
|
|
8
|
+
from requests.exceptions import ReadTimeout
|
|
9
|
+
|
|
10
|
+
from monocle_apptrace.exporters.exporter_processor import ExportTaskProcessor
|
|
11
|
+
|
|
12
|
+
REQUESTS_SUCCESS_STATUS_CODES = (200, 202)
|
|
13
|
+
OKAHU_PROD_INGEST_ENDPOINT = "https://ingest.okahu.co/api/v1/trace/ingest"
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OkahuSpanExporter(SpanExporter):
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
endpoint: Optional[str] = None,
|
|
22
|
+
timeout: Optional[int] = None,
|
|
23
|
+
session: Optional[requests.Session] = None,
|
|
24
|
+
task_processor: ExportTaskProcessor = None
|
|
25
|
+
):
|
|
26
|
+
"""Okahu exporter."""
|
|
27
|
+
okahu_endpoint: str = os.environ.get("OKAHU_INGESTION_ENDPOINT", OKAHU_PROD_INGEST_ENDPOINT)
|
|
28
|
+
self.endpoint = endpoint or okahu_endpoint
|
|
29
|
+
api_key: str = os.environ.get("OKAHU_API_KEY")
|
|
30
|
+
self._closed = False
|
|
31
|
+
if not api_key:
|
|
32
|
+
raise ValueError("OKAHU_API_KEY not set.")
|
|
33
|
+
self.timeout = timeout or 15
|
|
34
|
+
self.session = session or requests.Session()
|
|
35
|
+
self.session.headers.update(
|
|
36
|
+
{"Content-Type": "application/json", "x-api-key": api_key}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
self.task_processor = task_processor or None
|
|
40
|
+
if task_processor is not None:
|
|
41
|
+
task_processor.start()
|
|
42
|
+
|
|
43
|
+
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
|
|
44
|
+
# After the call to Shutdown subsequent calls to Export are
|
|
45
|
+
# not allowed and should return a Failure result
|
|
46
|
+
if not hasattr(self, 'session'):
|
|
47
|
+
return self.exporter.export(spans)
|
|
48
|
+
|
|
49
|
+
if self._closed:
|
|
50
|
+
logger.warning("Exporter already shutdown, ignoring batch")
|
|
51
|
+
return SpanExportResult.FAILURE
|
|
52
|
+
if len(spans) == 0:
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
span_list = {
|
|
56
|
+
"batch": []
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# append the batch object with all the spans object
|
|
60
|
+
for span in spans:
|
|
61
|
+
# create a object from serialized span
|
|
62
|
+
obj = json.loads(span.to_json())
|
|
63
|
+
if obj["parent_id"] is None:
|
|
64
|
+
obj["parent_id"] = "None"
|
|
65
|
+
else:
|
|
66
|
+
obj["parent_id"] = remove_0x_from_start(obj["parent_id"])
|
|
67
|
+
if obj["context"] is not None:
|
|
68
|
+
obj["context"]["trace_id"] = remove_0x_from_start(obj["context"]["trace_id"])
|
|
69
|
+
obj["context"]["span_id"] = remove_0x_from_start(obj["context"]["span_id"])
|
|
70
|
+
span_list["batch"].append(obj)
|
|
71
|
+
|
|
72
|
+
# Calculate is_root_span by checking if any span has no parent
|
|
73
|
+
is_root_span = any(not span.parent for span in spans)
|
|
74
|
+
|
|
75
|
+
def send_spans_to_okahu(span_list_local=None, is_root=False):
|
|
76
|
+
try:
|
|
77
|
+
result = self.session.post(
|
|
78
|
+
url=self.endpoint,
|
|
79
|
+
data=json.dumps(span_list_local),
|
|
80
|
+
timeout=self.timeout,
|
|
81
|
+
)
|
|
82
|
+
if result.status_code not in REQUESTS_SUCCESS_STATUS_CODES:
|
|
83
|
+
logger.error(
|
|
84
|
+
"Traces cannot be uploaded; status code: %s, message %s",
|
|
85
|
+
result.status_code,
|
|
86
|
+
result.text,
|
|
87
|
+
)
|
|
88
|
+
return SpanExportResult.FAILURE
|
|
89
|
+
logger.debug("spans successfully exported to okahu. Is root span: %s", is_root)
|
|
90
|
+
return SpanExportResult.SUCCESS
|
|
91
|
+
except ReadTimeout as e:
|
|
92
|
+
logger.warning("Trace export timed out: %s", str(e))
|
|
93
|
+
return SpanExportResult.FAILURE
|
|
94
|
+
|
|
95
|
+
# if async task function is present, then push the request to asnc task
|
|
96
|
+
if self.task_processor is not None and callable(self.task_processor.queue_task):
|
|
97
|
+
self.task_processor.queue_task(send_spans_to_okahu, span_list, is_root_span)
|
|
98
|
+
return SpanExportResult.SUCCESS
|
|
99
|
+
return send_spans_to_okahu(span_list, is_root_span)
|
|
100
|
+
|
|
101
|
+
def shutdown(self) -> None:
|
|
102
|
+
if self._closed:
|
|
103
|
+
logger.warning("Exporter already shutdown, ignoring call")
|
|
104
|
+
return
|
|
105
|
+
if hasattr(self, 'session'):
|
|
106
|
+
self.session.close()
|
|
107
|
+
self._closed = True
|
|
108
|
+
|
|
109
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# only removes the first occurrence of 0x from the string
|
|
114
|
+
def remove_0x_from_start(my_str: str):
|
|
115
|
+
if my_str.startswith("0x"):
|
|
116
|
+
return my_str.replace("0x", "", 1)
|
|
117
|
+
return my_str
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .common import *
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Azure environment constants
|
|
2
|
+
AZURE_ML_ENDPOINT_ENV_NAME = "AZUREML_ENTRY_SCRIPT"
|
|
3
|
+
AZURE_FUNCTION_WORKER_ENV_NAME = "FUNCTIONS_WORKER_RUNTIME"
|
|
4
|
+
AZURE_APP_SERVICE_ENV_NAME = "WEBSITE_SITE_NAME"
|
|
5
|
+
AWS_LAMBDA_ENV_NAME = "AWS_LAMBDA_RUNTIME_API"
|
|
6
|
+
GITHUB_CODESPACE_ENV_NAME = "CODESPACES"
|
|
7
|
+
|
|
8
|
+
AWS_LAMBDA_FUNCTION_IDENTIFIER_ENV_NAME = "AWS_LAMBDA_FUNCTION_NAME"
|
|
9
|
+
AZURE_FUNCTION_IDENTIFIER_ENV_NAME = "WEBSITE_SITE_NAME"
|
|
10
|
+
AZURE_APP_SERVICE_IDENTIFIER_ENV_NAME = "WEBSITE_DEPLOYMENT_ID"
|
|
11
|
+
GITHUB_CODESPACE_IDENTIFIER_ENV_NAME = "GITHUB_REPOSITORY"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Azure naming reference can be found here
|
|
15
|
+
# https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations
|
|
16
|
+
AZURE_FUNCTION_NAME = "azure.func"
|
|
17
|
+
AZURE_APP_SERVICE_NAME = "azure.asp"
|
|
18
|
+
AZURE_ML_SERVICE_NAME = "azure.mlw"
|
|
19
|
+
AWS_LAMBDA_SERVICE_NAME = "aws.lambda"
|
|
20
|
+
GITHUB_CODESPACE_SERVICE_NAME = "github_codespace"
|
|
21
|
+
|
|
22
|
+
# Env variables to identify infra service type
|
|
23
|
+
service_type_map = {
|
|
24
|
+
AZURE_ML_ENDPOINT_ENV_NAME: AZURE_ML_SERVICE_NAME,
|
|
25
|
+
AZURE_APP_SERVICE_ENV_NAME: AZURE_APP_SERVICE_NAME,
|
|
26
|
+
AZURE_FUNCTION_WORKER_ENV_NAME: AZURE_FUNCTION_NAME,
|
|
27
|
+
AWS_LAMBDA_ENV_NAME: AWS_LAMBDA_SERVICE_NAME,
|
|
28
|
+
GITHUB_CODESPACE_ENV_NAME: GITHUB_CODESPACE_SERVICE_NAME
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Env variables to identify infra service name
|
|
32
|
+
service_name_map = {
|
|
33
|
+
AZURE_APP_SERVICE_NAME: AZURE_APP_SERVICE_IDENTIFIER_ENV_NAME,
|
|
34
|
+
AZURE_FUNCTION_NAME: AZURE_FUNCTION_IDENTIFIER_ENV_NAME,
|
|
35
|
+
AZURE_ML_SERVICE_NAME: AZURE_ML_ENDPOINT_ENV_NAME,
|
|
36
|
+
AWS_LAMBDA_SERVICE_NAME: AWS_LAMBDA_FUNCTION_IDENTIFIER_ENV_NAME,
|
|
37
|
+
GITHUB_CODESPACE_SERVICE_NAME: GITHUB_CODESPACE_IDENTIFIER_ENV_NAME
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
llm_type_map = {
|
|
42
|
+
"sagemakerendpoint": "aws_sagemaker",
|
|
43
|
+
"azureopenai": "azure_openai",
|
|
44
|
+
"openai": "openai",
|
|
45
|
+
"chatopenai": "openai",
|
|
46
|
+
"azurechatopenai": "azure_openai",
|
|
47
|
+
"bedrock": "aws_bedrock",
|
|
48
|
+
"sagemakerllm": "aws_sagemaker",
|
|
49
|
+
"chatbedrock": "aws_bedrock",
|
|
50
|
+
"openaigenerator": "openai",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
MONOCLE_INSTRUMENTOR = "monocle_apptrace"
|
|
54
|
+
DATA_INPUT_KEY = "data.input"
|
|
55
|
+
DATA_OUTPUT_KEY = "data.output"
|
|
56
|
+
PROMPT_INPUT_KEY = "data.input"
|
|
57
|
+
PROMPT_OUTPUT_KEY = "data.output"
|
|
58
|
+
QUERY = "input"
|
|
59
|
+
RESPONSE = "response"
|
|
60
|
+
SESSION_PROPERTIES_KEY = "session"
|
|
61
|
+
INFRA_SERVICE_KEY = "infra_service_name"
|
|
62
|
+
META_DATA = 'metadata'
|
|
63
|
+
MONOCLE_SCOPE_NAME_PREFIX = "monocle.scope."
|
|
64
|
+
SCOPE_METHOD_LIST = 'MONOCLE_SCOPE_METHODS'
|
|
65
|
+
SCOPE_METHOD_FILE = 'monocle_scopes.json'
|
|
66
|
+
SCOPE_CONFIG_PATH = 'MONOCLE_SCOPE_CONFIG_PATH'
|
|
67
|
+
TRACE_PROPOGATION_URLS = "MONOCLE_TRACE_PROPAGATATION_URLS"
|
|
68
|
+
WORKFLOW_TYPE_KEY = "monocle.workflow_type"
|
|
69
|
+
WORKFLOW_TYPE_GENERIC = "workflow.generic"
|
|
70
|
+
MONOCLE_SDK_VERSION = "monocle_apptrace.version"
|