Skip to main content

Telemetry

Mnemosyne emits OpenTelemetry traces and metrics for every store operation, every recall and every call to an evaluator. It uses only the in-box System.Diagnostics types, so it adds no dependency and stays native AOT clean. Your host decides whether to collect the signals and where to send them.

Turn it on​

The store and the recall facade share one ActivitySource and one Meter, both named Mnemosyne.NET. The names are constants on MnemosyneTelemetry, in Mnemosyne.NET.Abstractions:

ConstantValue
MnemosyneTelemetry.ActivitySourceNameMnemosyne.NET
MnemosyneTelemetry.MeterNameMnemosyne.NET

Add both to your OpenTelemetry setup. With the OpenTelemetry.Extensions.Hosting package:

using Mnemosyne.NET.Abstractions;

builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing.AddSource(MnemosyneTelemetry.ActivitySourceName))
.WithMetrics(metrics => metrics.AddMeter(MnemosyneTelemetry.MeterName));

Add the exporters you already use, such as OTLP, as usual.

No activity is created without a listener, and the store builds no tag for one, so a host that doesn't collect the signals pays very little for them.

Operations​

Every store method is one operation, and so is each recall and each judge call. The operation name appears in the activity name, as mnemosyne.<operation>, and in the mnemosyne.operation tag on the metrics.

OperationEmitted byWhat it is
migratestoreMigrateAsync
rememberstorestoring a capture
getstorereading one Reasonlet
execution.appendstorerecording a run
feedback.appendstorerecording feedback on a run
searchstorefinding the nearest candidates
erasestoreEraseAsync
prunestorePruneAsync
recallfacadeRecallAsync
evaluatefacadeone call to the registered IReasonletEvaluator

The facade instruments recall and judge calls only. Remember, recording runs and feedback, and erase are instrumented by the store operations they call, and wrapping them again would count each twice. A judge call happens inside a remember or RecordExecutionAsync, before the store records the run, and is not a store operation, so it is counted on its own. A recall's mnemosyne.recall activity is the parent of the mnemosyne.search activity the store emits for it.

Activities​

There is one activity per operation, with these tags:

ActivityTags
mnemosyne.migratenone
mnemosyne.rememberon success: mnemosyne.capture_kind (NewReasonlet, ExecutionOnly or NewVersion), mnemosyne.reasonlet.id, mnemosyne.reasonlet.version, and mnemosyne.verdict_source of the run it records: evaluator when the capture comes with an evaluator's verdict, else agent
mnemosyne.getnone
mnemosyne.execution.appendmnemosyne.verdict_source, always: evaluator when the run comes with an evaluator's verdict, else agent
mnemosyne.feedback.appendon success, when the feedback gives a Worked verdict: mnemosyne.verdict_source feedback, whether or not it changes the run's verdict
mnemosyne.searchmnemosyne.search.limit, always; on success mnemosyne.search.returned, and mnemosyne.search.top_similarity when at least one candidate is returned
mnemosyne.erasemnemosyne.erase.by (scope or subject), always; on success mnemosyne.erased.reasonlets and mnemosyne.erased.executions
mnemosyne.prunemnemosyne.pruned.reasonlets and mnemosyne.pruned.executions, always; they count the committed batches, including when the prune fails
mnemosyne.recallsee below
mnemosyne.evaluatesee below

The recall activity​

TagSet when
mnemosyne.recall.topalways: the query's Top
mnemosyne.recall.intent_filteredalways, a bool: whether recall filtered on an intent. A recall that fails before the intent is resolved reports whether the query carried one.
mnemosyne.recall.intent_fallbackonly when the intent extractor was down and recall searched every intent: true
mnemosyne.recall.candidatesafter the search: how many candidates the store returned
mnemosyne.recall.returnedon success: how many matches recall returned
mnemosyne.recall.chosen.idon success, when at least one match is returned: the first match's id
mnemosyne.recall.chosen.versionon success, when at least one match is returned: the first match's version
mnemosyne.recall.chosen.scoreon success, when at least one match is returned: the first match's total score

The candidate and returned counts show how much the cut-off drops, which helps when you tune it for your embedding model.

The evaluate activity​

TagSet when
mnemosyne.evaluatorthe judge answered with a valid verdict: the judge's name, from ReasonletEvaluation.Evaluator
mnemosyne.evaluation.successthe judge answered with a valid verdict: its success, a bool
mnemosyne.evaluation.scorethe judge answered with a valid verdict: its score

The activity records how the judge call itself ended. A judge that is down, because it threw, timed out or answered Unavailable, shows as a failure here, even though the remember or the run record then succeeds with the agent's verdict. A timeout, whether the judge's own or MnemosyneOptions.EvaluationTimeout, is reported as Unavailable. A failed judge call has no verdict to name a judge from, so it carries no mnemosyne.evaluator tag, on the activity or on the metrics.

An answer that is blank or longer than 32,000 code points is not judged, so no mnemosyne.evaluate activity starts for it. With an evaluator registered, it counts once on mnemosyne.evaluations with outcome skipped and no other tag, and nothing else: no duration and no evaluate operation.

Failures on an activity​

  • A failure result sets error.type to the error code's name, such as Conflict, and the activity status to Error.
  • An exception, cancellation included, adds an exception event that carries exception.type only. It sets error.type to the exception's full type name and the status to Error. The message and the stack trace are never recorded, because a database error can quote stored values and a facade error can quote the question.
  • An unmapped database error. When the exception is an NpgsqlException with a SQLSTATE, such as XX000, the store's activity also gets db.response.status_code set to it, the OpenTelemetry database convention's name. A SQLSTATE is a five-character code from a fixed list, so it carries no user data, and it tells you what failed where the message can't.

Metrics​

InstrumentType and unitTagsEmitted by
mnemosyne.operationscounter, {operation}mnemosyne.operation, mnemosyne.outcomestore and facade
mnemosyne.operation.durationhistogram, secondsmnemosyne.operationstore and facade
mnemosyne.reasonlets.rememberedcounter, {capture}mnemosyne.capture_kindstore, on each successful remember
mnemosyne.search.returnedhistogram, {candidate}nonestore, on each successful search
mnemosyne.erased.reasonletscounter, {reasonlet}nonestore, on each successful erase
mnemosyne.erased.executionscounter, {execution}nonestore, on each successful erase
mnemosyne.pruned.reasonletscounter, {reasonlet}nonestore, for each committed prune batch
mnemosyne.pruned.executionscounter, {execution}nonestore, for each committed prune batch
mnemosyne.recall.returnedhistogram, {match}nonefacade, on each successful recall
mnemosyne.recall.emptycounter, {recall}nonefacade, on each successful recall that returned nothing
mnemosyne.evaluationscounter, {evaluation}mnemosyne.evaluator when the judge answered, mnemosyne.outcomefacade, on each judge call, and on each answer it skips as unusable
mnemosyne.evaluation.durationhistogram, secondsmnemosyne.evaluator when the judge answeredfacade, on each judge call

mnemosyne.operations and mnemosyne.operation.duration come from both the store and the facade. The two meters share one name and the instruments match exactly, so each aggregates as one instrument, and recall and evaluate sit next to the store's operations.

Outcomes​

mnemosyne.outcome on mnemosyne.operations and mnemosyne.evaluations is one of:

OutcomeMeaning
okThe operation succeeded.
Validation, NotFound, Conflict or UnavailableThe operation answered a failure result with that error code.
exceptionAn exception propagated, cancellation included.
skippedOn mnemosyne.evaluations only: the answer was blank or too long, so the judge was not called and the run kept the agent's verdict.

A rising Conflict count on erase or prune means concurrent writes are colliding; both are safe to retry. A rising Unavailable count means the database can't serve. On evaluate, a rising Unavailable or exception count means the judge is failing; while it is down, runs are stored with the agent's verdict. A Validation, NotFound or Conflict outcome on evaluate means the evaluator adapter has a bug: the facade throws InvalidOperationException for it. Data lifecycle and the store's errors explain when each happens.

Privacy​

No signal carries user data: no question, answer, intent, name, graph, tags, scope, subject or run id, and no error or exception message. Reasonlet ids and versions are opaque, so they appear, and so do scores. An evaluator's name appears too: it names the judge, such as llm-judge, and nothing about the run.

Scope and subject are left out on purpose. A scope often names a tenant and a subject names a person, and telemetry is exported, sampled and kept in other systems with their own retention. If you need a breakdown by tenant, add it in your own wrapper, where you decide what may leave your application.