Skip to content

instrument() and InstrumentationMiddleware

from fastmcp_feedback.instrumentation import InstrumentationMiddleware, instrument
instrument(app, sinks=None, **kwargs) -> InstrumentationMiddleware

Creates an InstrumentationMiddleware(sinks, **kwargs), adds it to app with app.add_middleware, and returns it.

FastMCP runs the first middleware added as the outermost. Call instrument() before adding other middleware to include their time and record calls they reject; call it last to time only the tool.

InstrumentationMiddleware(
sinks=None, *, mode=None, redactor=None, meta_only_tools=(),
identity_resolver=None, enricher=None, server_version=None,
hook_timeout=0.25, max_queue=1000, batch_size=100, sink_timeout=10.0,
max_error_chars=500, recent_calls=2000, correlation_timeout=1.0,
result_classifier=DEFAULT_RESULT_CLASSIFIER, exclude_tools=(),
sample_rates=None, random=random.random, recent_events=2000,
capture_llm_text=False, max_llm_text_chars=None,
)
ParameterDefaultDescription
sinks[JsonLinesSink()]Where records go. See Sinks.
modeFEEDBACK_INSTRUMENTATION_MODE, then "meta""off", "meta" or "full". Anything else raises ValueError.
redactorRedactor()Secret redaction for arguments, results, error text, hook output, event attributes and embedded text. See Redaction.
meta_only_tools()Tools never recorded above meta, even in full mode.
identity_resolverNone(context) -> dict, may be async. Keys user_sub, caller_kind, client_id become columns; other keys go to identity.
enricherNone(tool, args, result, context) -> dict, may be async. Output goes to extra, except server_version, which fills that column.
server_versionNoneDefault for the server_version column of calls and events.
hook_timeout0.25Seconds an async hook may take before it is skipped for that call.
max_queue1000Records waiting for the sinks before new ones are dropped and counted.
batch_size100Records handed to each sink per write.
sink_timeout10.0Seconds one sink write may take.
max_error_chars500Longest error message stored, after redaction.
recent_calls2000Call records kept in memory for feedback linking, including ones still queued.
correlation_timeout1.0Seconds the database lookup in capture_recent_calls may take.
result_classifierDEFAULT_RESULT_CLASSIFIER(tool, result) -> str | None. None turns soft-error detection off. See Classifier.
exclude_tools()Tools never recorded and never linked. The calls themselves are untouched.
sample_ratesNoneTool name to the fraction of its ok calls recorded, in (0, 1]. Other values raise ValueError. Failures are always recorded.
randomrandom.random() -> float in [0, 1) deciding sampling; a call is kept when it returns less than the rate. For tests.
recent_events2000Events kept in memory for events_for.
capture_llm_textFalseStore prompt and completion passed to record_llm_call.
max_llm_text_chars8 * max_error_charsLongest prompt or completion stored.
MethodReturnsDescription
record_event(kind, *, key=None, attrs=None, user_sub=None, call_id=None)boolRecord an event. Synchronous, safe from any thread, never raises or blocks. False when not recorded: mode off, kind empty or over 128 characters, key over 255 characters, attrs not a mapping, a full queue, or no event loop.
record_llm_call(*, model, provider=None, duration_ms=None, input_tokens=None, output_tokens=None, ok=None, error=None, key=None, attrs=None, prompt=None, completion=None)boolRecord an llm.call event. See Record LLM calls.
MethodReturnsDescription
events_for(key, *, kinds=None, limit=1000)list[dict]A key’s events, oldest first, the newest limit of them. Reads the DatabaseSink plus events still queued; without one, memory only. occurred_at is a UTC datetime.
capture_recent_calls(ctx=None, n=20, *, user_sub=None, window=timedelta(minutes=15))list[CallLink]Up to n calls that led up to now, chronological: same session first, then same user_sub within window. ctx is looked up when omitted; user_sub is asked of the identity resolver when omitted. Never raises; [] on failure.
link_feedback(feedback_ref, links)boolStore links for a feedback id of any format, replacing earlier ones. Needs a DatabaseSink. False instead of raising.
feedback_context(feedback_ref)list[dict]Linked calls in order, with call columns filled from the database or from memory for calls not yet written. Keys: call_id, position, rule, tool, started_at, duration_ms, ok, outcome, error_type, error_message, session_id, user_sub, caller_kind, server_version, extra. [] without a DatabaseSink.
similar(text, *, source_types=None, k=10, min_score=None)list[dict]Embedded texts most like text, best first. Needs an EmbeddingSink. Never raises.
similar_feedback(feedback_ref, *, k=10, source_types=None, min_score=None)list[dict]Texts most like an embedded feedback item, excluding the item. [] until it is embedded. Never raises.

Each similar result has source_type, source_id, text, score (1 minus cosine distance, in [0, 1]) and created_at.

MethodDescription
flush()Wait until queued records have been handed to the sinks. Does not wait for an EmbeddingSink’s own queue; use mw.embedding_sink.flush() for that.
aclose()Flush, stop the background task, and close every sink that has aclose. Call on server shutdown.
NameDescription
database_sinkThe first DatabaseSink among the sinks, or None.
embedding_sinkThe first EmbeddingSink among the sinks, or None.
redactorThe Redactor in use.
modeThe resolved mode.
dispatcherThe RecordDispatcher. dispatcher.dropped counts records dropped (full queue, no event loop); dispatcher.sink_errors counts failed sink writes.
NameValue
MODES("off", "meta", "full")
MODE_ENV_VAR"FEEDBACK_INSTRUMENTATION_MODE"
DEFAULT_TABLE_PREFIX"ffb_"
DEFAULT_EMBEDDING_DIM1024
EMBEDDING_SOURCES("feedback", "call_error", "event", "llm")
DEFAULT_EVENT_TEXT_KEYS("error_excerpt", "error", "message", "reason")