Skip to content

EventCapture

EventCapture(event_type, name, input=None, model=None, metadata=None)
ParameterTypeDefaultDescription
event_typestr(required)Type of event: "llm_call", "tool_call", or "agent_step"
namestr(required)Display name for the event in the UI timeline
inputanyNoneInput data to the function being traced
modelstrNoneModel name (only relevant for llm_call events)
metadatadictNoneArbitrary data attached to the trace
from openjck import EventCapture
# Manual instrumentation of a function
def my_function(arg1, arg2):
with EventCapture("tool_call", "my_function", input={"arg1": arg1, "arg2": arg2}) as ctx:
# Your function logic here
result = some_operation(arg1, arg2)
# Optionally set output fields
ctx.output = result
ctx.latency_ms = 150 # if you want to override auto-calculated latency
return result

Inside the with block, you can set these attributes on the context object:

FieldTypeDescription
outputanyThe result/output of the function
tokens_inintNumber of input tokens (for llm_call events)
tokens_outintNumber of output tokens (for llm_call events)
latency_msintExecution time in milliseconds
cost_usdfloatEstimated cost of the operation
errorstr/nullException traceback if the function failed
from openjck import EventCapture
import sqlite3
def get_user_data(user_id):
with EventCapture("tool_call", "database_query",
input={"user_id": user_id, "query": "SELECT * FROM users WHERE id = ?"}) as ctx:
try:
conn = sqlite3.connect('example.db')
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
result = cursor.fetchone()
conn.close()
ctx.output = result
# Optionally set latency if known
# ctx.latency_ms = 45
return result
except Exception as e:
ctx.error = str(e)
raise # Re-raise so normal error handling works

Use EventCapture instead of decorators when:

  1. Dynamic naming: You need to determine the trace name at runtime
  2. Partial tracing: You only want to trace specific sections within a function
  3. Async contexts: When working with complex async flows that don’t fit decorator patterns
  4. Library code: When you can’t modify function signatures but want to trace specific calls
  5. Hybrid approaches: Combining automatic decorator tracing with manual instrumentation for specific cases

Example of dynamic naming:

def process_items(items):
results = []
for i, item in enumerate(items):
# Dynamic name based on loop iteration
with EventCapture("tool_call", f"process_item_{i}", input={"item": item}) as ctx:
result = process_single_item(item)
ctx.output = result
results.append(result)
return results