Xperience.Telemetry.Kentico
0.2.0
dotnet add package Xperience.Telemetry.Kentico --version 0.2.0
NuGet\Install-Package Xperience.Telemetry.Kentico -Version 0.2.0
<PackageReference Include="Xperience.Telemetry.Kentico" Version="0.2.0" />
<PackageVersion Include="Xperience.Telemetry.Kentico" Version="0.2.0" />
<PackageReference Include="Xperience.Telemetry.Kentico" />
paket add Xperience.Telemetry.Kentico --version 0.2.0
#r "nuget: Xperience.Telemetry.Kentico, 0.2.0"
#:package Xperience.Telemetry.Kentico@0.2.0
#addin nuget:?package=Xperience.Telemetry.Kentico&version=0.2.0
#tool nuget:?package=Xperience.Telemetry.Kentico&version=0.2.0
Xperience.Telemetry
Intended for Xperience by Kentico SaaS environments, to get more robust statistics than the Xperience Portal provides.
| Package | Depends on | What is in it |
|---|---|---|
Xperience.Telemetry |
ASP.NET Core, Application Insights | Everything that doesn't need the Kentico SDK. |
Xperience.Telemetry.Kentico |
the above, Kentico.Xperience.Core |
The cache metrics and the endpoints that look inside Kentico's in-memory cache, and the database resource usage metrics. Keep it at the Kentico version of the app. |
dotnet add package Xperience.Telemetry
dotnet add package Xperience.Telemetry.Kentico
Using it
Required: set
CustomApplicationInsights:ConnectionStringto the connection string of your own Application Insights resource (seeCustomApplicationInsights). It is what turns the telemetry on. Without itAddXperienceTelemetrylogs a warning at startup and registers only the watchdog and the probe: no metrics, noAppStarted/AppStoppingevents, no forwarding to your resource and no scheduled captures, andAddXperienceTelemetryKenticoadds no cache metrics.
// Every environment: the probe behind POST gc/pinned, which does nothing until it is called.
services.AddXperienceTelemetryProbe();
// Where the watchdog and the telemetry should run (SaaS), after Application Insights is registered.
// Needs CustomApplicationInsights:ConnectionString to be set (see above).
services.AddXperienceTelemetry(configuration);
services.AddXperienceTelemetryKentico(configuration); // after the line above
// Once the app is built. The delegate decides who gets in; it is required so the endpoints can't be left open.
app.MapXperienceTelemetryEndpoints(MyAuth.AuthorizeAsync);
app.MapXperienceTelemetryKenticoEndpoints(MyAuth.AuthorizeAsync);
Calling AddXperienceTelemetry more than once has no effect. The two Map methods take an optional second argument,
the prefix, api/telemetry unless given. The Kentico endpoints sit under cache in the same prefix, so give both calls
the same one. The default is specific to this package, so it doesn't collide with an app's own endpoints.
The delegate is a Func<HttpContext, ValueTask<bool>>, so whatever sign-in the app already has can be used. Returning
false answers 401. It may also throw, which leaves the response to the app's exception handling. The POST endpoints
change the state of the process (a forced GC), so unless the app protects against cross-site requests the delegate
shouldn't rely on credentials a browser attaches by itself, such as cookies. Every response carries
Cache-Control: no-store.
To put an application's own diagnostics endpoints behind the same check, MapXperienceTelemetryAuthorizedGroup returns
the route group the endpoints above sit in:
app.MapXperienceTelemetryAuthorizedGroup(MyAuth.AuthorizeAsync, "api/telemetry/mine").MapGet("ping", () => "pong");
Configuration
Everything has a default, so nothing has to be configured: a section only needs to exist to change values, and a key
left out keeps its default. It is plain .NET configuration, so appsettings.json, environment variables
(GcMaintenance__RestartPrivateMB) or any other provider works. This is every key at its default:
{
"GcMaintenance": {
"Enabled": true,
"CompactionEnabled": true,
"CompactionFragmentedMB": 1000,
"CompactionMinIntervalMinutes": 10,
"CompactionMaxIntervalMinutes": 60,
"RestartEnabled": false,
"RestartPrivateMB": 2600,
"RestartQuietHoursPrivateMB": 2000,
"QuietHoursStart": 1,
"QuietHoursEnd": 6,
"QuietHoursTimeZone": "UTC",
"MinUptimeMinutes": 60,
"CheckIntervalSeconds": 30
},
"PinnedObjectsSnapshots": {
"Enabled": true,
"FirstAfterMinutes": 20,
"IntervalMinutes": 30,
"MaxPerProcess": 24,
"MaxPauseMs": 1500,
"CheckIntervalSeconds": 30
},
"DatabaseTelemetry": {
"Enabled": true,
"IntervalSeconds": 60,
"CommandTimeoutSeconds": 10
},
"CustomApplicationInsights": {
"ConnectionString": "required: the connection string of your own Application Insights resource",
"EnableDependencyTracking": true
}
}
The memory defaults assume a host limit of 3 GB (3,072 MB). Private bytes run about 120 MB above what the GC has
committed, and a process is killed at around 3,130 MB of them, so the restart thresholds leave a margin below that. On a
host with a different limit, scale CompactionFragmentedMB, RestartPrivateMB and RestartQuietHoursPrivateMB.
Restarting stays off until RestartEnabled is turned on (see below).
GcMaintenance
The watchdog (GcMaintenanceOptions). By default it forces a compacting GC when gen2 holds 1,000 MB of free space.
Restarting is up to you: it stops the app gracefully so that the host starts a new process, which only helps where the
host does that, so it stays off until you set RestartEnabled. Once it is on, the app restarts at 2,600 MB of private
bytes (2,000 MB during the quiet hours, 01:00 to 06:00 UTC), never before the process has been up an hour, and always
after trying a compaction first.
| Key | Default | Meaning |
|---|---|---|
Enabled |
true |
Master switch for everything the watchdog does. The telemetry keeps running. |
CompactionEnabled |
true |
Force a compacting GC when gen2 holds a lot of free space the GC isn't reclaiming on its own. |
CompactionFragmentedMB |
1000 |
The free space (fragmentation), as of the last GC, at which to force a compaction. |
CompactionMinIntervalMinutes |
10 |
The least time between forced compactions. Never less than a minute. |
CompactionMaxIntervalMinutes |
60 |
A compaction that reclaims little (pinned objects do that) doubles the wait for the next one, up to this. |
RestartEnabled |
false |
Stop the app gracefully before the process runs out of memory and the host kills it. Off by default, because it only helps where the host starts the app again. |
RestartPrivateMB |
2600 |
Private bytes at which to restart, whatever the time of day. |
RestartQuietHoursPrivateMB |
2000 |
Private bytes at which to restart during the quiet hours, so a process already well on its way to the limit is replaced when traffic is low. Never above RestartPrivateMB. |
QuietHoursStart |
1 |
The first hour of the quiet hours, 0 to 23, in QuietHoursTimeZone. |
QuietHoursEnd |
6 |
The hour they end before, so 1 and 6 mean 01:00 to 05:59. The window may wrap past midnight (22 and 5). Equal values mean there are none. |
QuietHoursTimeZone |
UTC |
A system time zone id, such as Eastern Standard Time. One the host doesn't know means UTC, with a warning. |
MinUptimeMinutes |
60 |
A process younger than this is never restarted, which rules out restart loops. |
CheckIntervalSeconds |
30 |
How often memory is looked at. Never less than 5. |
PinnedObjectsSnapshots
Scheduled pinned objects captures (PinnedObjectsSnapshotOptions), reported as PinnedObjectsReport events with the
trigger Scheduled, so the series can be read from Application Insights without calling the endpoint. They only run
where CustomApplicationInsights:ConnectionString is set. Each capture stops the app for about as long as one full GC.
| Key | Default | Meaning |
|---|---|---|
Enabled |
true |
Off stops the captures and nothing else changes. |
FirstAfterMinutes |
20 |
How long the process has been up when the first capture is taken. Early, so it sees the pins a fresh process starts with. Never less than 1. |
IntervalMinutes |
30 |
The time between captures after that. Never less than 5. |
MaxPerProcess |
24 |
The most captures one process takes (about 12 hours at the default interval). |
MaxPauseMs |
1500 |
If a capture stops the app for longer than this, the process takes no more and sends PinnedObjectsSnapshotsStopped. Three captures failing in a row does the same. |
CheckIntervalSeconds |
30 |
How often the service looks at the clock. Never less than 1. |
DatabaseTelemetry
How much of its limits the database used (DatabaseTelemetryOptions, Kentico package), reported as the Database.*
metrics. It reads sys.dm_db_resource_stats, which only Azure SQL Database has: on any other database the service logs
one line and stops. It only runs where CustomApplicationInsights:ConnectionString is set, and reads through Kentico's
own connection, so it needs no connection string of its own.
| Key | Default | Meaning |
|---|---|---|
Enabled |
true |
Off stops the polling and nothing else changes. |
IntervalSeconds |
60 |
How often the database is asked for the samples it has taken since the last time. Never less than 15, which is how often the database takes one. |
CommandTimeoutSeconds |
10 |
How long a query may run before it is given up. Short on purpose: this runs when the database is struggling and should not add to the queue. Never less than 1. |
CustomApplicationInsights
Read by AddXperienceTelemetry and AddXperienceTelemetryKentico when they register services.
| Key | Default | Meaning |
|---|---|---|
ConnectionString |
none, required | The connection string of your own Application Insights resource. The telemetry doesn't work without it: a warning is logged at startup and only the watchdog and the probe are registered, so there is no forwarding, no minute-by-minute metrics, no AppStarted / AppStopping and no scheduled captures. |
EnableDependencyTracking |
true |
Turns on the Application Insights SDK's dependency tracking. Only slow (500 ms or more) or failed dependencies are forwarded, but they can only be forwarded if they are tracked. |
When a change takes effect
GcMaintenance and PinnedObjectsSnapshots are read through IOptionsMonitor, so a configuration that reloads (a
changed appsettings.json on a host that watches it) takes effect at the next check. The exception is
CheckIntervalSeconds, which is read once when the service starts, as are DatabaseTelemetry:Enabled and
IntervalSeconds; CommandTimeoutSeconds is read at every query. The CustomApplicationInsights keys decide what
gets registered, so they are read once, at startup. The GcMaintenanceStarted event records the GcMaintenance values
a process started with.
What it sends
The names are the contract: dashboards and queries depend on them, so changing one is a breaking change.
Custom events
AppStarted,AppStopping: process id, machine, process start, and the version andBuildCommit/BuildBranch/BuildNumberof the entry assembly (the last three come fromAssemblyMetadataattributes with those keys, which the app's build has to stamp in). A process killed for running out of memory doesn't sendAppStopping; its absence before the nextAppStartedis the signal.GcConfig(once, at startup): the GC settings that reached the process, which the host may have overridden.GcMaintenanceStarted(its options),GcCompaction(what a forced compaction achieved) andMemoryWatchdogRestart(why the app was restarted).PinnedObjectsReport(triggerManualorScheduled) andPinnedObjectsSnapshotsStopped.
Metrics
Memory.*andGC.*once a minute: working set, private bytes, limit, committed, fragmentation, generation sizes, collection counts, pause per minute, allocation rate, the last full blocking and background GC (GC.FullBlocking.*,GC.Background.*), thread pool.GC.Events.*,GC.PinnedObjects.MaxandGC.GcHandles.Max, counted from the runtime's own GC events.GC.Compaction.PauseMsandGC.Compaction.ReclaimedMB.Cache.Entries,Cache.KeyFamiliesandCache.EntriesByFamily(Kentico package).Database.CpuPercent,Database.DataIoPercent,Database.LogWritePercentandDatabase.MemoryPercent(the average over each 15 second interval),Database.WorkerPercentandDatabase.SessionPercent(the highest in it), all as a percentage of the limit of the service tier, andDatabase.DtuLimit(Kentico package; Azure SQL Database only). One value per interval, stamped with the time the database gave it. At 100Database.WorkerPercentthe database refuses new requests.
Requests get a UserAgent property. With a custom resource, requests, slow or failed dependencies, exceptions, traces,
events and metrics are also forwarded to it, before sampling.
Endpoints
Under the prefix (api/telemetry by default), so GET api/telemetry/gc and so on:
GET gc |
GC settings, memory, the last collection of each kind, and what the watchdog would do now (maintenance is null where it isn't registered). |
POST gc/compact |
Forces a blocking compacting gen2 GC and reports what it achieved. |
POST gc/pinned?top=25 |
What is pinned in the heap, by type and size, and (from the second capture of a process) which pins are the same as last time. 429 with Retry-After while one is running or one ran in the last two minutes. |
GET cache/list?limit=100 |
Kentico's cache key families with entry counts, largest first. |
GET cache/family?family=...&limit=25&measure=true |
The entries of one family with an estimated size each (a reflection walk: keep limit small on a busy server). |
Decisions worth knowing
Pinned objects are read from the runtime's events.
PinnedObjectsProbelistens to the GC keyword at verbose level for the length of one forced gen2 GC. It stops the app for as long as that collection takes (roughly 60 to 200 ms on a heap of a few hundred MB), at most one at a time and one every two minutes. The runtime filters events per listener, so the verbose session doesn't reach the other listeners (GcEventListenerstays at informational).The forwarder has to be the last initializer. It copies what the others have filled in (user agent, role name, operation id). It is added when the
TelemetryConfigurationis created, which is after the Application Insights registration has added the rest, andTelemetryForwardingService, the first hosted service, moves it to the end if the registrations were made in the other order. That service also registers the flush onApplicationStoppingfirst, so, as those callbacks run in reverse, it runs afterAppStoppingandMemoryWatchdogRestarthave been sent. The SDK readsIConfigureOptions<TelemetryConfiguration>but ignoresIPostConfigureOptions, which is whyConfigureis used.A compaction is always tried before a restart, and doesn't repeat for a while (doubling up to an hour) if it reclaimed little, which is what happens when pinned objects are in the way.
Restarting is opt-in. It ends the process with
StopApplicationand relies on the host to start another one. Where the host doesn't, turning it on is an outage; where the limit is higher than the defaults assume, it restarts a healthy process.ObjectSizeEstimatorskips what it can't walk safely: delegates, reflection types, tasks, threads, DI service objects and anything inMicrosoft.AspNetCore.*. It sizesIntPtrandUIntPtras pointers, because their only field is of their own type, so asking for their layout would never end.Database samples are read from the database's own history.
sys.dm_db_resource_statskeeps a sample every 15 seconds for about an hour, so each poll asks for everything newer than the last sample it reported instead of taking a reading. A poll that fails (a database that has run out of workers can refuse this query too) leaves that point where it is, and the next poll that works reports what the failed ones missed, under the times the database gave the samples. Nothing from before the process started is reported, because an earlier process already did. Every instance of an application reports the same database-wide samples, so chart them by maximum, not sum.
Tests
dotnet test Xperience.Telemetry.slnx
They use the real GC, the real runtime events and a test server. The GC-sensitive ones share one process, so the assembly turns off parallel execution. One test allocates hundreds of megabytes and takes seconds, so it only runs when asked to:
XPERIENCE_TELEMETRY_STRESS=1 dotnet test Xperience.Telemetry.slnx
What the services report, under which names, is pinned by tests that call the reports directly. Not covered: the timers that call them on a schedule, and anything that needs a running Kentico site.
Releases
Both packages are published to nuget.org by .github/workflows/publish.yml when a v*.*.* tag is pushed, after the
tests pass.
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Kentico.Xperience.Core (>= 31.7.4)
- Xperience.Telemetry (>= 0.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.