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/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 HandoffBase:
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
- """The very first thing to get called.
204
+ """Called at the very start of the agent's execution.
173
205
 
174
- Override this method to perform any setup or configuration tasks,
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
- pass
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
- """The very last thing to get called.
216
+ """Called at the very end of the agent's execution.
183
217
 
184
- Override this method to perform any cleanup tasks,
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
- pass
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
- pass
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
- try:
257
- self.__dspy_signature = self.create_dspy_signature_class(
258
- self.name, self.description, f"{self.input} -> {self.output}"
259
- )
260
- # Initialize the language model.
261
- self._configure_language_model()
262
- # Select the appropriate DSPy task based on tool availability.
263
- agent_task = self._select_task(self.__dspy_signature)
264
- # Execute the task with the provided inputs.
265
- result = agent_task(**inputs)
266
- # Process the result and ensure fallback values for missing keys.
267
- result = self._process_result(result, inputs)
268
- return result
269
- except Exception as eval_error:
270
- logger.error(
271
- f"Error during evaluation in agent '{self.name}': {eval_error}"
272
- )
273
- raise
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
- try:
322
- await self.initialize(inputs)
323
- result = await self.evaluate(inputs)
324
- await self.terminate(inputs, result)
325
- return result
326
- except Exception as run_error:
327
- await self.on_error(run_error, inputs)
328
- logger.error(f"Error running agent '{self.name}': {run_error}")
329
- raise
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
- try:
373
- from temporalio.client import Client
374
-
375
- from flock.workflow.agent_activities import run_flock_agent_activity
376
- from flock.workflow.temporal_setup import run_activity
377
-
378
- client = await Client.connect("localhost:7233", namespace="default")
379
- agent_data = self.to_dict()
380
- inputs_data = inputs
381
-
382
- result = await run_activity(
383
- client,
384
- self.name,
385
- run_flock_agent_activity,
386
- {"agent_data": agent_data, "inputs": inputs_data},
387
- )
388
- return result
389
-
390
- except Exception as temporal_error:
391
- logger.error(
392
- f"Error running Temporal workflow for agent '{self.name}': {temporal_error}"
393
- )
394
- raise
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.dict()
523
+ data = self.model_dump()
442
524
  return convert_callable(data)
443
525
 
444
526
  @classmethod
@@ -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="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>",
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
- else:
96
- # Bind a new Loguru logger with the given name as context.
97
- return loguru_logger.bind(name=self.name)
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