flock-core 0.2.1__py3-none-any.whl → 0.2.3__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 flock-core might be problematic. Click here for more details.
- flock/__init__.py +4 -0
- flock/config.py +29 -0
- flock/core/__init__.py +5 -0
- flock/core/context/context.py +104 -133
- flock/core/execution/temporal_executor.py +22 -6
- flock/core/flock.py +94 -62
- flock/core/flock_agent.py +150 -68
- flock/core/logging/logging.py +22 -4
- flock/core/logging/telemetry.py +109 -0
- flock/core/logging/telemetry_exporter/file_span.py +37 -0
- flock/core/logging/telemetry_exporter/sqllite_span.py +68 -0
- flock/core/logging/trace_and_logged.py +55 -0
- flock/core/mixin/dspy_integration.py +40 -14
- flock/core/registry/agent_registry.py +89 -74
- flock/core/tools/basic_tools.py +27 -4
- flock/core/tools/dev_tools/github.py +37 -8
- flock/core/util/cli_helper.py +7 -3
- flock/workflow/activities.py +148 -90
- {flock_core-0.2.1.dist-info → flock_core-0.2.3.dist-info}/METADATA +36 -2
- {flock_core-0.2.1.dist-info → flock_core-0.2.3.dist-info}/RECORD +22 -22
- flock/agents/__init__.py +0 -0
- flock/agents/batch_agent.py +0 -140
- flock/agents/loop_agent.py +0 -117
- flock/agents/trigger_agent.py +0 -113
- flock/agents/user_agent.py +0 -145
- {flock_core-0.2.1.dist-info → flock_core-0.2.3.dist-info}/WHEEL +0 -0
- {flock_core-0.2.1.dist-info → flock_core-0.2.3.dist-info}/licenses/LICENSE +0 -0
flock/core/flock_agent.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""FlockAgent is the core, declarative base class for all agents in the Flock framework."""
|
|
2
2
|
|
|
3
3
|
from abc import ABC
|
|
4
|
-
from collections.abc import Callable
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from typing import Any, Literal, Union
|
|
7
7
|
|
|
@@ -10,16 +10,28 @@ from pydantic import BaseModel, Field
|
|
|
10
10
|
|
|
11
11
|
from flock.core.context.context import FlockContext
|
|
12
12
|
from flock.core.logging.logging import get_logger
|
|
13
|
-
from flock.core.mixin.dspy_integration import DSPyIntegrationMixin
|
|
13
|
+
from flock.core.mixin.dspy_integration import AgentType, DSPyIntegrationMixin
|
|
14
14
|
from flock.core.mixin.prompt_parser import PromptParserMixin
|
|
15
15
|
|
|
16
16
|
logger = get_logger("flock")
|
|
17
|
+
from opentelemetry import trace
|
|
18
|
+
|
|
19
|
+
tracer = trace.get_tracer(__name__)
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
@dataclass
|
|
20
23
|
class FlockAgentConfig:
|
|
21
24
|
"""Configuration options for a FlockAgent."""
|
|
22
25
|
|
|
26
|
+
agent_type_override: AgentType = field(
|
|
27
|
+
default=None,
|
|
28
|
+
metadata={
|
|
29
|
+
"description": "Overrides the agent type. TOOL USE ONLY WORKS WITH REACT"
|
|
30
|
+
},
|
|
31
|
+
)
|
|
32
|
+
disable_output: bool = field(
|
|
33
|
+
default=False, metadata={"description": "Disables the agent's output."}
|
|
34
|
+
)
|
|
23
35
|
save_to_file: bool = field(
|
|
24
36
|
default=False,
|
|
25
37
|
metadata={
|
|
@@ -30,7 +42,7 @@ class FlockAgentConfig:
|
|
|
30
42
|
|
|
31
43
|
|
|
32
44
|
@dataclass
|
|
33
|
-
class
|
|
45
|
+
class HandOff:
|
|
34
46
|
"""Base class for handoff returns."""
|
|
35
47
|
|
|
36
48
|
next_agent: Union[str, "FlockAgent"] = field(
|
|
@@ -124,14 +136,14 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
|
|
|
124
136
|
"", description="A human-readable description of the agent."
|
|
125
137
|
)
|
|
126
138
|
|
|
127
|
-
input: str | None = Field(
|
|
139
|
+
input: str | Callable[..., str] | None = Field(
|
|
128
140
|
None,
|
|
129
141
|
description=(
|
|
130
142
|
"A comma-separated list of input keys. Optionally supports type hints (:) and descriptions (|). "
|
|
131
143
|
"For example: 'query: str | The search query, chapter_list: list[str] | The chapter list of the document'."
|
|
132
144
|
),
|
|
133
145
|
)
|
|
134
|
-
output: str | None = Field(
|
|
146
|
+
output: str | Callable[..., str] | None = Field(
|
|
135
147
|
None,
|
|
136
148
|
description=(
|
|
137
149
|
"A comma-separated list of output keys. Optionally supports type hints (:) and descriptions (|). "
|
|
@@ -157,7 +169,7 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
|
|
|
157
169
|
),
|
|
158
170
|
)
|
|
159
171
|
|
|
160
|
-
termination: str | None = Field(
|
|
172
|
+
termination: str | Callable[..., str] | None = Field(
|
|
161
173
|
None,
|
|
162
174
|
description="An optional termination condition or phrase used to indicate when the agent should stop processing.",
|
|
163
175
|
)
|
|
@@ -167,32 +179,58 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
|
|
|
167
179
|
description="Configuration options for the agent, such as serialization settings.",
|
|
168
180
|
)
|
|
169
181
|
|
|
182
|
+
# Lifecycle callback fields: if provided, these callbacks are used instead of overriding the methods.
|
|
183
|
+
initialize_callback: Callable[[dict[str, Any]], Awaitable[None]] | None = (
|
|
184
|
+
Field(
|
|
185
|
+
default=None,
|
|
186
|
+
description="Optional callback function for initialization. If provided, this async function is called with the inputs.",
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
terminate_callback: (
|
|
190
|
+
Callable[[dict[str, Any], dict[str, Any]], Awaitable[None]] | None
|
|
191
|
+
) = Field(
|
|
192
|
+
default=None,
|
|
193
|
+
description="Optional callback function for termination. If provided, this async function is called with the inputs and result.",
|
|
194
|
+
)
|
|
195
|
+
on_error_callback: (
|
|
196
|
+
Callable[[Exception, dict[str, Any]], Awaitable[None]] | None
|
|
197
|
+
) = Field(
|
|
198
|
+
default=None,
|
|
199
|
+
description="Optional callback function for error handling. If provided, this async function is called with the error and inputs.",
|
|
200
|
+
)
|
|
201
|
+
|
|
170
202
|
# Lifecycle hooks
|
|
171
203
|
async def initialize(self, inputs: dict[str, Any]) -> None:
|
|
172
|
-
"""
|
|
204
|
+
"""Called at the very start of the agent's execution.
|
|
173
205
|
|
|
174
|
-
Override this method to perform
|
|
175
|
-
such as loading resources or validating inputs.
|
|
206
|
+
Override this method or provide an `initialize_callback` to perform setup tasks such as input validation or resource loading.
|
|
176
207
|
"""
|
|
177
|
-
|
|
208
|
+
if self.initialize_callback is not None:
|
|
209
|
+
await self.initialize_callback(self, inputs)
|
|
210
|
+
else:
|
|
211
|
+
pass
|
|
178
212
|
|
|
179
213
|
async def terminate(
|
|
180
214
|
self, inputs: dict[str, Any], result: dict[str, Any]
|
|
181
215
|
) -> None:
|
|
182
|
-
"""
|
|
216
|
+
"""Called at the very end of the agent's execution.
|
|
183
217
|
|
|
184
|
-
Override this method to perform
|
|
185
|
-
such as releasing resources or logging results.
|
|
218
|
+
Override this method or provide a `terminate_callback` to perform cleanup tasks such as releasing resources or logging results.
|
|
186
219
|
"""
|
|
187
|
-
|
|
220
|
+
if self.terminate_callback is not None:
|
|
221
|
+
await self.terminate_callback(self, inputs, result)
|
|
222
|
+
else:
|
|
223
|
+
pass
|
|
188
224
|
|
|
189
225
|
async def on_error(self, error: Exception, inputs: dict[str, Any]) -> None:
|
|
190
226
|
"""Called if the agent encounters an error during execution.
|
|
191
227
|
|
|
192
|
-
Override this method to implement
|
|
193
|
-
custom error handling or recovery strategies.
|
|
228
|
+
Override this method or provide an `on_error_callback` to implement custom error handling or recovery strategies.
|
|
194
229
|
"""
|
|
195
|
-
|
|
230
|
+
if self.on_error_callback is not None:
|
|
231
|
+
await self.on_error_callback(self, error, inputs)
|
|
232
|
+
else:
|
|
233
|
+
pass
|
|
196
234
|
|
|
197
235
|
async def evaluate(self, inputs: dict[str, Any]) -> dict[str, Any]:
|
|
198
236
|
"""Process the agent's task using the provided inputs and return the result.
|
|
@@ -253,24 +291,35 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
|
|
|
253
291
|
- Return a dictionary similar to:
|
|
254
292
|
{"idea": "A fun app idea based on ...", "query": "build an app", "context": {"previous_idea": "messaging app"}}
|
|
255
293
|
"""
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
294
|
+
with tracer.start_as_current_span("agent.evaluate") as span:
|
|
295
|
+
span.set_attribute("agent.name", self.name)
|
|
296
|
+
span.set_attribute("inputs", str(inputs))
|
|
297
|
+
try:
|
|
298
|
+
# Create and configure the signature and language model.
|
|
299
|
+
self.__dspy_signature = self.create_dspy_signature_class(
|
|
300
|
+
self.name,
|
|
301
|
+
self.description,
|
|
302
|
+
f"{self.input} -> {self.output}",
|
|
303
|
+
)
|
|
304
|
+
self._configure_language_model()
|
|
305
|
+
agent_task = self._select_task(
|
|
306
|
+
self.__dspy_signature,
|
|
307
|
+
agent_type_override=self.config.agent_type_override,
|
|
308
|
+
)
|
|
309
|
+
# Execute the task.
|
|
310
|
+
result = agent_task(**inputs)
|
|
311
|
+
result = self._process_result(result, inputs)
|
|
312
|
+
span.set_attribute("result", str(result))
|
|
313
|
+
logger.info("Evaluation successful", agent=self.name)
|
|
314
|
+
return result
|
|
315
|
+
except Exception as eval_error:
|
|
316
|
+
logger.error(
|
|
317
|
+
"Error during evaluation",
|
|
318
|
+
agent=self.name,
|
|
319
|
+
error=str(eval_error),
|
|
320
|
+
)
|
|
321
|
+
span.record_exception(eval_error)
|
|
322
|
+
raise
|
|
274
323
|
|
|
275
324
|
async def run(self, inputs: dict[str, Any]) -> dict[str, Any]:
|
|
276
325
|
"""Run the agent with the given inputs and return its generated output.
|
|
@@ -318,15 +367,23 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
|
|
|
318
367
|
The method might return:
|
|
319
368
|
{"result": "A conversational chatbot that uses AI to...", "query": "build a chatbot", "context": {"user": "Alice"}}
|
|
320
369
|
"""
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
370
|
+
with tracer.start_as_current_span("agent.run") as span:
|
|
371
|
+
span.set_attribute("agent.name", self.name)
|
|
372
|
+
span.set_attribute("inputs", str(inputs))
|
|
373
|
+
try:
|
|
374
|
+
await self.initialize(inputs)
|
|
375
|
+
result = await self.evaluate(inputs)
|
|
376
|
+
await self.terminate(inputs, result)
|
|
377
|
+
span.set_attribute("result", str(result))
|
|
378
|
+
logger.info("Agent run completed", agent=self.name)
|
|
379
|
+
return result
|
|
380
|
+
except Exception as run_error:
|
|
381
|
+
logger.error(
|
|
382
|
+
"Error running agent", agent=self.name, error=str(run_error)
|
|
383
|
+
)
|
|
384
|
+
await self.on_error(run_error, inputs)
|
|
385
|
+
span.record_exception(run_error)
|
|
386
|
+
raise
|
|
330
387
|
|
|
331
388
|
async def run_temporal(self, inputs: dict[str, Any]) -> dict[str, Any]:
|
|
332
389
|
"""Execute this agent via a Temporal workflow for enhanced fault tolerance and asynchronous processing.
|
|
@@ -369,29 +426,54 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
|
|
|
369
426
|
result = await agent.run_temporal({"query": "analyze data", "context": {"source": "sales"}})
|
|
370
427
|
will execute the agent on a Temporal worker and return the output in a structured dictionary format.
|
|
371
428
|
"""
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
client
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
429
|
+
with tracer.start_as_current_span("agent.run_temporal") as span:
|
|
430
|
+
span.set_attribute("agent.name", self.name)
|
|
431
|
+
span.set_attribute("inputs", str(inputs))
|
|
432
|
+
try:
|
|
433
|
+
from temporalio.client import Client
|
|
434
|
+
|
|
435
|
+
from flock.workflow.agent_activities import (
|
|
436
|
+
run_flock_agent_activity,
|
|
437
|
+
)
|
|
438
|
+
from flock.workflow.temporal_setup import run_activity
|
|
439
|
+
|
|
440
|
+
client = await Client.connect(
|
|
441
|
+
"localhost:7233", namespace="default"
|
|
442
|
+
)
|
|
443
|
+
agent_data = self.to_dict()
|
|
444
|
+
inputs_data = inputs
|
|
445
|
+
|
|
446
|
+
result = await run_activity(
|
|
447
|
+
client,
|
|
448
|
+
self.name,
|
|
449
|
+
run_flock_agent_activity,
|
|
450
|
+
{"agent_data": agent_data, "inputs": inputs_data},
|
|
451
|
+
)
|
|
452
|
+
span.set_attribute("result", str(result))
|
|
453
|
+
logger.info("Temporal run successful", agent=self.name)
|
|
454
|
+
return result
|
|
455
|
+
except Exception as temporal_error:
|
|
456
|
+
logger.error(
|
|
457
|
+
"Error in Temporal workflow",
|
|
458
|
+
agent=self.name,
|
|
459
|
+
error=str(temporal_error),
|
|
460
|
+
)
|
|
461
|
+
span.record_exception(temporal_error)
|
|
462
|
+
raise
|
|
463
|
+
|
|
464
|
+
def resolve_callables(self, context) -> None:
|
|
465
|
+
"""Resolve any callable fields in the agent instance using the provided context.
|
|
466
|
+
|
|
467
|
+
This method resolves any callable fields in the agent instance using the provided context. It iterates over
|
|
468
|
+
the agent's fields and replaces any callable objects (such as lifecycle hooks or tools) with their corresponding
|
|
469
|
+
resolved values from the context. This ensures that the agent is fully configured and ready
|
|
470
|
+
"""
|
|
471
|
+
if isinstance(self.input, Callable):
|
|
472
|
+
self.input = self.input(context)
|
|
473
|
+
if isinstance(self.output, Callable):
|
|
474
|
+
self.output = self.output(context)
|
|
475
|
+
if isinstance(self.description, Callable):
|
|
476
|
+
self.description = self.description(context)
|
|
395
477
|
|
|
396
478
|
def to_dict(self) -> dict[str, Any]:
|
|
397
479
|
"""Serialize the FlockAgent instance to a dictionary.
|
|
@@ -438,7 +520,7 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
|
|
|
438
520
|
return {k: convert_callable(v) for k, v in obj.items()}
|
|
439
521
|
return obj
|
|
440
522
|
|
|
441
|
-
data = self.
|
|
523
|
+
data = self.model_dump()
|
|
442
524
|
return convert_callable(data)
|
|
443
525
|
|
|
444
526
|
@classmethod
|
flock/core/logging/logging.py
CHANGED
|
@@ -13,6 +13,8 @@ Key points:
|
|
|
13
13
|
|
|
14
14
|
import sys
|
|
15
15
|
|
|
16
|
+
from opentelemetry import trace
|
|
17
|
+
|
|
16
18
|
# Always import Temporal workflow (since it's part of the project)
|
|
17
19
|
from temporalio import workflow
|
|
18
20
|
|
|
@@ -37,6 +39,16 @@ def in_workflow_context() -> bool:
|
|
|
37
39
|
return False
|
|
38
40
|
|
|
39
41
|
|
|
42
|
+
def get_current_trace_id() -> str:
|
|
43
|
+
"""Fetch the current trace ID from OpenTelemetry, if available."""
|
|
44
|
+
current_span = trace.get_current_span()
|
|
45
|
+
span_context = current_span.get_span_context()
|
|
46
|
+
# Format the trace_id as hex (if valid)
|
|
47
|
+
if span_context.is_valid:
|
|
48
|
+
return format(span_context.trace_id, "032x")
|
|
49
|
+
return "no-trace"
|
|
50
|
+
|
|
51
|
+
|
|
40
52
|
# Configure Loguru for non-workflow (local/worker) contexts.
|
|
41
53
|
# Note that in workflow code, we will use Temporal's workflow.logger instead.
|
|
42
54
|
loguru_logger.remove()
|
|
@@ -44,7 +56,10 @@ loguru_logger.add(
|
|
|
44
56
|
sys.stderr,
|
|
45
57
|
level="DEBUG",
|
|
46
58
|
colorize=True,
|
|
47
|
-
format=
|
|
59
|
+
format=(
|
|
60
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | "
|
|
61
|
+
"<cyan>[trace_id: {extra[trace_id]}]</cyan> | <magenta>[{extra[category]}]</magenta> | {message}"
|
|
62
|
+
),
|
|
48
63
|
)
|
|
49
64
|
# Optionally add a file handler, e.g.:
|
|
50
65
|
# loguru_logger.add("logs/flock.log", rotation="100 MB", retention="30 days", level="DEBUG")
|
|
@@ -92,9 +107,12 @@ class FlockLogger:
|
|
|
92
107
|
if in_workflow_context():
|
|
93
108
|
# Use Temporal's workflow.logger inside a workflow context.
|
|
94
109
|
return workflow.logger
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
# Bind our logger with category and trace_id
|
|
111
|
+
return loguru_logger.bind(
|
|
112
|
+
name=self.name,
|
|
113
|
+
category=self.name, # Customize this per module (e.g., "flock", "agent", "context")
|
|
114
|
+
trace_id=get_current_trace_id(),
|
|
115
|
+
)
|
|
98
116
|
|
|
99
117
|
def debug(self, message: str, *args, **kwargs):
|
|
100
118
|
self._get_logger().debug(message, *args, **kwargs)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""This module sets up OpenTelemetry tracing for a service."""
|
|
2
|
+
|
|
3
|
+
from opentelemetry import trace
|
|
4
|
+
from opentelemetry.sdk.resources import Resource
|
|
5
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
6
|
+
from opentelemetry.sdk.trace.export import (
|
|
7
|
+
BatchSpanProcessor,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from flock.core.logging.telemetry_exporter.file_span import FileSpanExporter
|
|
11
|
+
from flock.core.logging.telemetry_exporter.sqllite_span import (
|
|
12
|
+
SQLiteSpanExporter,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TelemetryConfig:
|
|
17
|
+
"""This configuration class sets up OpenTelemetry tracing.
|
|
18
|
+
|
|
19
|
+
- Export spans to a Jaeger collector using gRPC.
|
|
20
|
+
- Write spans to a file.
|
|
21
|
+
- Save spans in a SQLite database.
|
|
22
|
+
|
|
23
|
+
Only exporters with a non-None configuration will be activated.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
service_name: str,
|
|
29
|
+
jaeger_endpoint: str = None,
|
|
30
|
+
jaeger_transport: str = "grpc",
|
|
31
|
+
file_export_path: str = None,
|
|
32
|
+
sqlite_db_path: str = None,
|
|
33
|
+
batch_processor_options: dict = None,
|
|
34
|
+
):
|
|
35
|
+
""":param service_name: Name of your service.
|
|
36
|
+
|
|
37
|
+
:param jaeger_endpoint: The Jaeger collector gRPC endpoint (e.g., "localhost:14250").
|
|
38
|
+
:param file_export_path: If provided, spans will be written to this file.
|
|
39
|
+
:param sqlite_db_path: If provided, spans will be stored in this SQLite DB.
|
|
40
|
+
:param batch_processor_options: Dict of options for BatchSpanProcessor (e.g., {"max_export_batch_size": 10}).
|
|
41
|
+
"""
|
|
42
|
+
self.service_name = service_name
|
|
43
|
+
self.jaeger_endpoint = jaeger_endpoint
|
|
44
|
+
self.jaeger_transport = jaeger_transport
|
|
45
|
+
self.file_export_path = file_export_path
|
|
46
|
+
self.sqlite_db_path = sqlite_db_path
|
|
47
|
+
self.batch_processor_options = batch_processor_options or {}
|
|
48
|
+
|
|
49
|
+
def setup_tracing(self):
|
|
50
|
+
# Create a Resource with the service name.
|
|
51
|
+
resource = Resource(attributes={"service.name": self.service_name})
|
|
52
|
+
provider = TracerProvider(resource=resource)
|
|
53
|
+
trace.set_tracer_provider(provider)
|
|
54
|
+
|
|
55
|
+
# List to collect our span processors.
|
|
56
|
+
span_processors = []
|
|
57
|
+
|
|
58
|
+
# If a Jaeger endpoint is specified, add the Jaeger gRPC exporter.
|
|
59
|
+
if self.jaeger_endpoint:
|
|
60
|
+
if self.jaeger_transport == "grpc":
|
|
61
|
+
from opentelemetry.exporter.jaeger.proto.grpc import (
|
|
62
|
+
JaegerExporter,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
jaeger_exporter = JaegerExporter(
|
|
66
|
+
endpoint=self.jaeger_endpoint,
|
|
67
|
+
insecure=True,
|
|
68
|
+
)
|
|
69
|
+
elif self.jaeger_transport == "http":
|
|
70
|
+
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
|
|
71
|
+
|
|
72
|
+
jaeger_exporter = JaegerExporter(
|
|
73
|
+
collector_endpoint=self.jaeger_endpoint,
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
"Invalid JAEGER_TRANSPORT specified. Use 'grpc' or 'http'."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
span_processors.append(
|
|
81
|
+
BatchSpanProcessor(
|
|
82
|
+
jaeger_exporter, **self.batch_processor_options
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# If a file path is provided, add the custom file exporter.
|
|
87
|
+
if self.file_export_path:
|
|
88
|
+
file_exporter = FileSpanExporter(self.file_export_path)
|
|
89
|
+
span_processors.append(
|
|
90
|
+
BatchSpanProcessor(
|
|
91
|
+
file_exporter, **self.batch_processor_options
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# If a SQLite database path is provided, add the custom SQLite exporter.
|
|
96
|
+
if self.sqlite_db_path:
|
|
97
|
+
sqlite_exporter = SQLiteSpanExporter(self.sqlite_db_path)
|
|
98
|
+
span_processors.append(
|
|
99
|
+
BatchSpanProcessor(
|
|
100
|
+
sqlite_exporter, **self.batch_processor_options
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Register all span processors with the provider.
|
|
105
|
+
for processor in span_processors:
|
|
106
|
+
provider.add_span_processor(processor)
|
|
107
|
+
|
|
108
|
+
# Return a tracer instance.
|
|
109
|
+
return trace.get_tracer(__name__)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from opentelemetry.sdk.trace.export import (
|
|
4
|
+
SpanExporter,
|
|
5
|
+
SpanExportResult,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FileSpanExporter(SpanExporter):
|
|
10
|
+
"""A simple exporter that writes span data as JSON lines into a file."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, file_path: str):
|
|
13
|
+
self.file_path = file_path
|
|
14
|
+
|
|
15
|
+
def export(self, spans) -> SpanExportResult:
|
|
16
|
+
try:
|
|
17
|
+
with open(self.file_path, "a") as f:
|
|
18
|
+
for span in spans:
|
|
19
|
+
# Create a dictionary representation of the span.
|
|
20
|
+
span_dict = {
|
|
21
|
+
"name": span.name,
|
|
22
|
+
"trace_id": format(span.context.trace_id, "032x"),
|
|
23
|
+
"span_id": format(span.context.span_id, "016x"),
|
|
24
|
+
"start_time": span.start_time,
|
|
25
|
+
"end_time": span.end_time,
|
|
26
|
+
"attributes": span.attributes,
|
|
27
|
+
"status": str(span.status),
|
|
28
|
+
}
|
|
29
|
+
f.write(json.dumps(span_dict) + "\n")
|
|
30
|
+
return SpanExportResult.SUCCESS
|
|
31
|
+
except Exception as e:
|
|
32
|
+
print("Error exporting spans to file:", e)
|
|
33
|
+
return SpanExportResult.FAILURE
|
|
34
|
+
|
|
35
|
+
def shutdown(self) -> None:
|
|
36
|
+
# Nothing special needed on shutdown.
|
|
37
|
+
pass
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sqlite3
|
|
3
|
+
|
|
4
|
+
from opentelemetry.sdk.trace.export import (
|
|
5
|
+
SpanExporter,
|
|
6
|
+
SpanExportResult,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SQLiteSpanExporter(SpanExporter):
|
|
11
|
+
"""A custom exporter that writes span data into a SQLite database."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, sqlite_db_path: str):
|
|
14
|
+
self.sqlite_db_path = sqlite_db_path
|
|
15
|
+
self.conn = sqlite3.connect(
|
|
16
|
+
self.sqlite_db_path, check_same_thread=False
|
|
17
|
+
)
|
|
18
|
+
self._create_table()
|
|
19
|
+
|
|
20
|
+
def _create_table(self):
|
|
21
|
+
cursor = self.conn.cursor()
|
|
22
|
+
cursor.execute(
|
|
23
|
+
"""
|
|
24
|
+
CREATE TABLE IF NOT EXISTS spans (
|
|
25
|
+
id TEXT PRIMARY KEY,
|
|
26
|
+
name TEXT,
|
|
27
|
+
trace_id TEXT,
|
|
28
|
+
span_id TEXT,
|
|
29
|
+
start_time INTEGER,
|
|
30
|
+
end_time INTEGER,
|
|
31
|
+
attributes TEXT,
|
|
32
|
+
status TEXT
|
|
33
|
+
)
|
|
34
|
+
"""
|
|
35
|
+
)
|
|
36
|
+
self.conn.commit()
|
|
37
|
+
|
|
38
|
+
def export(self, spans) -> SpanExportResult:
|
|
39
|
+
try:
|
|
40
|
+
cursor = self.conn.cursor()
|
|
41
|
+
for span in spans:
|
|
42
|
+
span_id = format(span.context.span_id, "016x")
|
|
43
|
+
trace_id = format(span.context.trace_id, "032x")
|
|
44
|
+
cursor.execute(
|
|
45
|
+
"""
|
|
46
|
+
INSERT OR REPLACE INTO spans
|
|
47
|
+
(id, name, trace_id, span_id, start_time, end_time, attributes, status)
|
|
48
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
49
|
+
""",
|
|
50
|
+
(
|
|
51
|
+
span_id,
|
|
52
|
+
span.name,
|
|
53
|
+
trace_id,
|
|
54
|
+
span_id,
|
|
55
|
+
span.start_time,
|
|
56
|
+
span.end_time,
|
|
57
|
+
json.dumps(span.attributes),
|
|
58
|
+
str(span.status),
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
self.conn.commit()
|
|
62
|
+
return SpanExportResult.SUCCESS
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print("Error exporting spans to SQLite:", e)
|
|
65
|
+
return SpanExportResult.FAILURE
|
|
66
|
+
|
|
67
|
+
def shutdown(self) -> None:
|
|
68
|
+
self.conn.close()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
from opentelemetry import trace
|
|
5
|
+
|
|
6
|
+
from flock.core.logging.logging import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger("tools")
|
|
9
|
+
tracer = trace.get_tracer(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def traced_and_logged(func):
|
|
13
|
+
"""A decorator that wraps a function in an OpenTelemetry span and logs its inputs,
|
|
14
|
+
outputs, and exceptions. Supports both synchronous and asynchronous functions.
|
|
15
|
+
"""
|
|
16
|
+
if inspect.iscoroutinefunction(func):
|
|
17
|
+
|
|
18
|
+
@functools.wraps(func)
|
|
19
|
+
async def async_wrapper(*args, **kwargs):
|
|
20
|
+
with tracer.start_as_current_span(func.__name__) as span:
|
|
21
|
+
span.set_attribute("args", str(args))
|
|
22
|
+
span.set_attribute("kwargs", str(kwargs))
|
|
23
|
+
try:
|
|
24
|
+
result = await func(*args, **kwargs)
|
|
25
|
+
span.set_attribute("result", str(result))
|
|
26
|
+
logger.debug(
|
|
27
|
+
f"{func.__name__} executed successfully", result=result
|
|
28
|
+
)
|
|
29
|
+
return result
|
|
30
|
+
except Exception as e:
|
|
31
|
+
logger.error(f"Error in {func.__name__}", error=str(e))
|
|
32
|
+
span.record_exception(e)
|
|
33
|
+
raise
|
|
34
|
+
|
|
35
|
+
return async_wrapper
|
|
36
|
+
else:
|
|
37
|
+
|
|
38
|
+
@functools.wraps(func)
|
|
39
|
+
def wrapper(*args, **kwargs):
|
|
40
|
+
with tracer.start_as_current_span(func.__name__) as span:
|
|
41
|
+
span.set_attribute("args", str(args))
|
|
42
|
+
span.set_attribute("kwargs", str(kwargs))
|
|
43
|
+
try:
|
|
44
|
+
result = func(*args, **kwargs)
|
|
45
|
+
span.set_attribute("result", str(result))
|
|
46
|
+
logger.debug(
|
|
47
|
+
f"{func.__name__} executed successfully", result=result
|
|
48
|
+
)
|
|
49
|
+
return result
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.error(f"Error in {func.__name__}", error=str(e))
|
|
52
|
+
span.record_exception(e)
|
|
53
|
+
raise
|
|
54
|
+
|
|
55
|
+
return wrapper
|