Logging and telemetry
IntentSharp writes to Microsoft.Extensions.Logging. With AddIntentSharp, the client uses the application's
logging. With the builder, pass a logger factory to WithLoggerFactory, before the provider methods:
// WithLoggerFactory comes first: the provider methods create their providers at once, with the logger
// factory the builder has at that moment.
var client = new IntentClientBuilder()
.WithLoggerFactory(loggerFactory)
.UseProvidersFromEnvironment()
.Build();
A client built without a logger factory logs nothing, license notices included. A provider added before
WithLoggerFactory logs nothing either.
The log categories
Each category is the full name of the type that writes it.
| Category | Level | What it logs |
|---|---|---|
IntentSharp.IntentClient |
Debug | Each completion call (judge calls are not logged): the provider, the model, the input and output tokens and the stop reason. Each attempt that fails its checks: the attempt, the start of the intent and the issues. A cached reply that fails the current checks, after which the attempt goes to the provider. |
IntentSharp.IntentClient |
Warning | A request sent to the fallback provider after the primary failed, with the primary's error. A memory record that could not be written; the result is not affected. |
IntentSharp.Providers.OpenAIProvider, IntentSharp.Providers.AnthropicProvider |
Warning | Each retry of an HTTP call after a 429, a 5xx or a transport error, with the wait and the retry count. |
IntentSharp.Verification.ModelJudge |
Warning | A judge reply that was not JSON. The attempt is accepted without a verdict. |
A category under IntentSharp.Licensing |
Information, Warning or Error | The license notices, with event id 9100 (below). |
IntentSharp.Catalog.FileIntentCatalog |
Information | One line per load of the intent folder: how many intents, the folder, and how many problems. |
IntentSharp.Catalog.FileIntentCatalog |
Warning, Error | Warning: each problem in an intent file, with the file and the line, an error from the watcher of the folder, after which every file is read again; a failure to watch the folder again, which is retried on a timer and logged once; and a change callback that threw, after which the catalog was still reloaded. Error: a reload that failed; the intents loaded before stay in use. |
IntentSharp.Catalog.IntentRuntime |
Information | One line per run of an intent from a file: its name, its version, whether it succeeded and the number of attempts. |
AddIntentCatalog and AddIntentRuntime give the catalog and the runtime their loggers. When you create
FileIntentCatalog or IntentRuntime yourself, pass an ILogger<FileIntentCatalog> or ILogger<IntentRuntime> to
the constructor.
Some types log only to a logger you pass them, in the logger parameter: the provider made by UseChatClient
(Debug, each chat client exception that becomes an IntentSharp error), the agent tools made by AsAIFunction (Warning, a tool call that failed
and was reported to the agent), and the two memories (Warning, an embedding that failed, so word overlap was used
in its place).
What the logs hold
IntentSharp never logs the serialized input, a whole request or a whole reply. These lines carry part of a call's text:
- The start of the intent, its first 60 characters: the Debug line of a failed attempt, the Debug line of a cached reply that fails the current checks, and the judge's Warning for a reply that was not JSON.
- The issues of a failed attempt, in its Debug line: the judge's problems, your rule and validator messages and IntentSharp's own check messages. Each can quote your input or the reply.
- Exceptions, with their messages, which can hold a provider's error text: in the Warning of a fallback, the
chat client's exception that the
UseChatClientprovider logs at Debug, the exception a memory logs when an embedding fails, and the exception an agent tool logs when a tool call fails.
To log every request and reply, see Seeing every request.
A call that ends with Success false logs no error of its own: the result says so. Log the outcome yourself where
it matters:
public static void LogCall<T>(ILogger logger, AIResult<T> result)
{
logger.LogInformation(
"Intent finished: success={Success}, attempts={Attempts}, provider={Provider}, model={Model}, " +
"tokens={Tokens}, elapsed={ElapsedMs} ms, fromCache={FromCache}",
result.Success, result.Attempts.Count, result.Provider, result.Model,
result.Usage.TotalTokens, (long)result.Elapsed.TotalMilliseconds, result.FromCache);
if (!result.Success)
logger.LogWarning("Intent failed: {Failure}", result.FailureSummary);
}
Filtering
Filters on the category prefix IntentSharp cover every category above:
logging.AddFilter("IntentSharp", LogLevel.Information); // every IntentSharp category
logging.AddFilter("IntentSharp.IntentClient", LogLevel.Debug); // each attempt and each model call
logging.AddFilter("IntentSharp.Providers", LogLevel.Warning); // retries of the HTTP providers
In appsettings.json, the same filters are entries under Logging:LogLevel, such as "IntentSharp": "Warning".
License notices: event id 9100
Every license notice is logged with event id 9100, whose name is IntentSharp.License; LicenseInfo.NoticeEventId
holds it. Route on the event id rather than on the category. A notice is logged when the client is
built. A notice that asks you to act is repeated at most once an hour by default, while the client is used
(LicenseReminderInterval). License behaviour lists each notice and its level.
To send the notices somewhere of their own, pass them on by event id:
/// <summary>Passes on only the license notices (event id 9100), for example to the channel your operations team reads.</summary>
public sealed class LicenseNoticeLogger(ILogger inner) : ILogger
{
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => inner.BeginScope(state);
public bool IsEnabled(LogLevel logLevel) => inner.IsEnabled(logLevel);
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
{
if (eventId.Id == LicenseInfo.NoticeEventId.Id)
inner.Log(logLevel, eventId, state, exception, formatter);
}
}
Tracing: one activity per call
Each call starts one activity named IntentSharp.Transform, from the activity source IntentSharp. It covers the
whole call, every attempt included, and carries two tags: intentsharp.target_type, the name of the result type,
and intentsharp.success. With OpenTelemetry, add the source by name with AddSource("IntentSharp"). Without it,
any ActivityListener receives the activities:
// IntentSharp starts one activity, "IntentSharp.Transform", per call, from the activity source "IntentSharp".
var listener = new ActivityListener
{
ShouldListenTo = source => source.Name == "IntentSharp",
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
ActivityStopped = activity => Console.WriteLine(
$"{activity.OperationName} {activity.GetTagItem("intentsharp.target_type")} " +
$"success={activity.GetTagItem("intentsharp.success")} {activity.Duration.TotalMilliseconds:0} ms")
};
ActivitySource.AddActivityListener(listener);
There is no activity per attempt, and IntentSharp publishes no metrics.
Tokens and time
The figures are on each result and on the client. The result's Usage, Attempts, Elapsed and FromCache are
described in What a result holds. The client's
running totals, TotalUsage and TotalModelCalls, are described in
How a call works.