Webority.Azure.Monitor 0.26.0

dotnet add package Webority.Azure.Monitor --version 0.26.0
                    
NuGet\Install-Package Webority.Azure.Monitor -Version 0.26.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Webority.Azure.Monitor" Version="0.26.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Webority.Azure.Monitor" Version="0.26.0" />
                    
Directory.Packages.props
<PackageReference Include="Webority.Azure.Monitor" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Webority.Azure.Monitor --version 0.26.0
                    
#r "nuget: Webority.Azure.Monitor, 0.26.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Webority.Azure.Monitor@0.26.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Webority.Azure.Monitor&version=0.26.0
                    
Install as a Cake Addin
#tool nuget:?package=Webority.Azure.Monitor&version=0.26.0
                    
Install as a Cake Tool

Webority.Azure

Shared Azure infrastructure for the Webority product fleet — one canonical, optimized implementation of the Azure plumbing every product used to hand-copy (and drift on).

Package IDs stay Webority.Azure.* for a tidy nuget.org grouping, but the C# namespaces are deliberately Webority.AzureStorage / Webority.AzureTelemetry — not Webority.Azure.*. A Webority.Azure.* namespace would shadow the Azure SDK's own Azure root namespace for any consumer code living under a Webority.* namespace, since C# walks enclosing namespaces and finds Webority.Azure before it ever reaches global Azure.Core/Azure.Identity.

Package What it gives a product
Webority.Azure.Storage Blob / Queue / Table services: singleton cached clients, streaming downloads (DownloadStreamingAsync), seekable OpenReadAsync for range-resumable large downloads, resumable chunked block upload (caller-supplied block ids shareable with a browser client, plus GetStagedBlockIdsAsync to resume an interrupted upload) with content type on commit, HTTPS-only read and write SAS for both auth models (user-delegation key cached), Base64 queues with known-queue cache and receipt-based receive (DequeueMessageAsync hands back a QueueMessageReceipt; CompleteMessageAsync deletes once the work has committed), Table service + IDistributedCache implementation, and an ISetIfAbsentStore (atomic claim of a key for a lifetime, for Webority.Security's single-use controls) via AddAzureTableSetIfAbsentStore(), and AddWeborityTableServiceClient() registers one shared TableServiceClient for the configured account, for libraries that keep their own tables.
Webority.Azure.Monitor Shared Azure Monitor readers on the Webority credential, registered by AddWeborityAzureMonitor(): the logs query API (IAzureMonitorLogsClient), platform metrics (IAzureMonitorMetricsClient), resource listing (IAzureResourceClient) and fired alerts (IAzureAlertsClient).
Webority.Azure.Telemetry One-call OpenTelemetry → Azure Monitor bootstrap (connection string, always-on head sampling, failed-only span filtering with successful requests sampled after the fact, export-level log floor) replacing the per-head copied block. See "Telemetry collection policy" below.
Webority.Azure.Telemetry.Functions The same telemetry for an Azure Functions isolated-worker host, plus the worker logging capability, in one call. A separate package so web heads never take the Functions worker dependency. See "Telemetry for Functions hosts" below.
Webority.Azure.Telemetry.Jobs WeborityJobRun alone, with no dependency beyond the .NET base library, so an application or infrastructure layer can record its background workers' runs. Webority.Azure.Telemetry references it and exports the runs. See "Background job runs" below.

Webority.Azure.NotificationHubs is retired and no longer built or published. Use the Webority.Push package instead (Android via FCM, iOS via APNs). Existing versions on NuGet stay installable.

Auth model (Storage)

One options contract, two modes — set exactly one:

  • Azure:Storage:StorageAccountName → Entra / managed identity via a narrowed DefaultAzureCredential chain (ManagedIdentity in Azure, Azure CLI locally — nothing else, so no slow credential probing on constrained plans). One cached credential per process.
  • Azure:Storage:ConnectionString → shared key (the local-development default: developers without Azure access use the staging account's connection string).

Blob containers at start-up (Storage)

The write paths (SaveAsync and block staging) create a missing container on their first write to it in each process, which costs a create round trip on that write, several seconds on a cold account. EnsureContainerAsync(containerName) runs the same create-if-missing once, ahead of time, and records the container in the same per-process cache, so the first write to it makes no create call. Leave containerName out to ensure Azure:Storage:Blob:DefaultContainerName.

  • The container is created private (PublicAccessType.None); an existing one is left as it is.
  • It fails loudly: an invalid container name, an authorization failure or an unreachable account throws, and nothing is cached, so the next write tries the create again.
  • Call it on the registered singleton (inject IAzureStorageBlobService). The cache belongs to that instance, so a second instance built by hand does not share it.
  • Read paths are unchanged: they still never create a container.

Call it from a hosted service's ExecuteAsync, not StartAsync, so the round trips do not hold up every start (dotnet.md → Startup). An exception thrown there stops the host under the default BackgroundServiceExceptionBehavior.StopHost, so a wrong name or a missing role assignment fails the deploy instead of the first upload. A write that arrives before it finishes creates the container itself, as it always has.

public sealed class BlobContainerStartup(IAzureStorageBlobService blobs) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await blobs.EnsureContainerAsync("reports", stoppingToken);
        await blobs.EnsureContainerAsync("logos", stoppingToken);
    }
}

// Startup.ConfigureServices
services.AddAzureStorageBlob();
services.AddHostedService<BlobContainerStartup>();

Conventions

  • All services register as singletons (Azure SDK clients own their transport and are built for process-wide reuse — see dotnet.md, HttpClient/SDK-client rule).
  • Options are validated on start (ValidateDataAnnotations().ValidateOnStart()); a misconfigured host crashes at boot, never at first request. AddWeborityTelemetry is the one that validates earlier still, at registration, because it consumes its own values while wiring the exporter and the span processor, so a startup check would run after the wiring it protects.
  • Public type names match the pre-extraction fleet implementations (IAzureStorageBlobService, …), so migrating a product is a using swap plus deleting its local copy.
  • A blob download returns an open stream; dispose it (await using) as soon as it is read, or the connection stays held.
  • The synchronous IDistributedCache members block a thread while the storage call runs, so they must not be used on a request path.
  • A non-seekable upload through SaveAsync buffers up to 32 MiB, so a host should cap its concurrent uploads.

Telemetry collection policy

AddWeborityTelemetry's fleet defaults implement one rule: collect only what is consumed. Application Insights ingestion is billed, and near nobody looks at the Metrics blade or a complete log of every successful outbound call — so none of that ships to Azure Monitor unless a head opts in.

  • No metrics export by default (EnableMetrics = false). This suppresses the distro's standard-metrics/performance-counter pipeline (AzureMonitorOptions.EnableStandardMetrics / EnablePerformanceCounters, both true by default upstream) and the ASP.NET Core/HttpClient runtime meters UseAzureMonitor wires in on .NET 8+, via a catch-all dropping view. Set EnableMetrics = true to restore metrics fleet-wide; DroppedMetrics then trims just the known-noisy subset instead of everything.

  • Failed-only spans, sampled successes. Failed spans (Status == Error, or any span carrying a recorded exception event) are always kept, and so is any span answering an HTTP status of 400 or above. ASP.NET Core leaves a 4xx at Unset by semantic convention, so without that rule a refusal a caller saw was sampled like a success and three in four never arrived, and a 5xx recorded by instrumentation that sets no span status went the same way. Successful dependency/client spans are always dropped. Successful server/request spans are kept with probability SuccessfulRequestSamplingRatio (default 0.25), decided deterministically per trace so every span belonging to one trace agrees. Outside Production the default is 1.0, so a staging head keeps every request: the environment is read from configuration (environment, then AZURE_FUNCTIONS_ENVIRONMENT, ASPNETCORE_ENVIRONMENT, DOTNET_ENVIRONMENT), none set counts as Production, and a configured ratio still wins.

  • Query values are never exported. Every span's url.query, url.full, http.url, http.target, microsoft.request.url and microsoft.dependency.data, and a display name carrying a query, keep their keys with each value replaced by Redacted (/reset?email=Redacted&token=Redacted). These are every attribute the exporter builds a request row's url or a dependency row's data from. It is always on: the Azure Monitor distribution turns the OpenTelemetry instrumentation's own query redaction off, so without it emails, reset tokens and webhook keys sit in the request table for the whole retention. A segment with no = is kept as a key. Put a secret in a header or the body, never in the query.

  • Email addresses in exported logs are redacted. The formatted message and every string attribute have each address, %40-encoded ones included, replaced with Redacted-email. An exception's message cannot be rewritten and is exported as it is.

  • A call the caller abandoned is not a failure. A dependency span cancelled because the request was aborted (its error type and every exception event name OperationCanceledException or TaskCanceledException) is judged as a success. Azure Identity's LogMsalError log and the JWT bearer handler's ProcessingMessageFailed log of such a cancellation are exported at Information. The abandoned request itself is not a failure either: whatever its status code (typically 499) or exception events, it is recorded with success = true and its real resultCode, carries customDimensions["webority.aborted"], and is always kept with webority.sample_rate 1, so a spike of abandoned requests stays visible. An exception event on it still reaches the exceptions table. A cancellation while the request is still open (a timeout) is still a failure. To see the abandoned requests:

    requests | where tobool(customDimensions["webority.aborted"]) | summarize count() by name, resultCode
    
  • Nothing is exported without a ServiceName. It defaults to WEBSITE_SITE_NAME. A process outside App Service that sets neither it nor WeborityTelemetry:ServiceName starts normally, exports nothing rather than writing rows as unknown_service:dotnet, and logs one warning at startup naming the setting. WeborityTelemetryOptions.Enabled then reads false.

  • A 404 that matched no endpoint is sampled like a success (SampleUnmatchedRoute404, default true). Scanners probing paths that do not exist are most of them. Its span has no http.route, so it is recorded with success = true, its real resultCode and customDimensions["webority.expected_failure"], exactly like a marked expected failure. A 404 answered by a matched endpoint keeps its route and stays a failure. A head that calls UseStatusCodePagesWithReExecute ends every request on the error page's route, so the package adds a middleware (an IStartupFilter, outside everything the head registers) that reads the original request's endpoint when the error page starts its response. Its unmatched 404s are then sampled the same way, with no change in the head; an error page that writes no body leaves them failures. Nothing is added when the option is off, and a Functions worker has no pipeline for it.

  • A request carries the signed-in account's id as enduser.id, which Application Insights shows as user_AuthenticatedId. It is read from the ClaimTypes.NameIdentifier claim of an authenticated user and written only when it is a positive 32-bit integer, so an email, a phone number, a GUID or any other text is never exported. A head whose authentication keeps the id in sub without mapping it to NameIdentifier gets no id.

  • The exporter's customer SDK stats are off. They write item counts into the component every 15 minutes through a meter provider of their own, which EnableMetrics does not reach, and the exporter reads only the process environment variable APPLICATIONINSIGHTS_SDKSTATS_DISABLED. The package sets it to true when it wires the exporter and the variable is unset; a head that sets it itself keeps its value.

  • "Always kept" is literal, because head sampling is switched off. A head sampler decides at span start, before a status code, a response or an exception exists, so it cannot tell a failure from a success and nothing downstream can bring back a trace it dropped. The package therefore pins the exporter to always-on (SamplingRatio to 1.0 and TracesPerSecond to null, both needed because the distro defaults the second to 5.0 and lets it win) and makes every trim after the span ends, where the status is known. SuccessfulRequestSamplingRatio is the one ratio the package has.

    • The cost, stated plainly. Every span is created and fully recorded before it can be judged, which is CPU and allocation a head sampler would have saved. Application Insights also stops extrapolating counts: the exporter stamps one sample rate on every item, taken from its own head ratio, and reads no per-span rate, so itemCount is 1 on every row and count() reads successful requests as the kept fraction while failure counts are exact. Count with the sample-rate dimension below instead.
  • Every exported span carries the rate it was kept at, as customDimensions["webority.sample_rate"]: 1 for a span kept whole (a failure, a refusal, a kept dependency), 1/ratio for a successful request kept by sampling (4 at the default 0.25). The true number of requests a row stands for is that value, so:

    requests
    | where timestamp > ago(1d)
    | summarize requests = round(sum(todouble(customDimensions["webority.sample_rate"]))),
                failed = countif(success == false)
        by bin(timestamp, 1h)
    

    Rows exported before this release have no such dimension and count as nothing in sum(), so start a trend at the release. The exporter's own itemCount cannot carry this: it takes one sample rate from the exporter options for every item.

  • A request can mark its 4xx as an expected outcome. A business-rule refusal answered with 400 is not a failure, but Application Insights counts every request at 400 or above as failed. A head calls MarkExpectedFailure where it produces such an answer:

    using Webority.AzureTelemetry;
    
    httpContext.MarkExpectedFailure();
    

    The request is then recorded with success = true and its real resultCode, carries customDimensions["webority.expected_failure"], and is sampled at SuccessfulRequestSamplingRatio like any success. The mark only ever softens a 4xx: a request that goes on to a 5xx, records an exception or carries an Error status is still a failure, and the dimension is removed. With telemetry off the call does nothing. Setting WeborityTelemetry:TreatConflictAndUnprocessableAsExpected to true treats every 409 and 422 the same way without a call; 400 is left out on purpose, because the framework also answers 400 for malformed input and failed antiforgery checks. To see the refusals themselves:

    requests
    | where tobool(customDimensions["webority.expected_failure"])
    | summarize refusals = round(sum(todouble(customDimensions["webority.sample_rate"]))) by name, resultCode
    
    • The one hole the package cannot close: do not set OTEL_TRACES_SAMPLER or OTEL_TRACES_SAMPLER_ARG on a Webority head. The exporter reads both from the environment and either one installs a head sampler underneath the package, which puts back exactly the trace loss the pin above removes, silently and with no signal in the app's own config. Nothing in the fleet sets them today.
    • The second hole, and it is not closable here either: the trim is not scoped to Azure Monitor. A span is dropped by clearing its Recorded flag, which every exporter in the pipeline reads, so a head that chains its own exporter onto the returned builder loses exactly the same spans, silently. OpenTelemetry has no per-exporter filter and the Azure Monitor distribution exposes no hook for a processor inside its own pipeline. A head that needs the complete span stream somewhere else should give that exporter its own provider rather than chaining it here.
  • Opt-in keeps for successful spans (both off by default, so a head that sets neither sees no change). AlwaysKeepSources lists ActivitySource names whose spans are always kept, and SlowDependencyThresholdMs keeps a successful client or internal span lasting at least that many milliseconds (1 to 600000). A configured AlwaysKeepSources array replaces the default empty list. Each kept span is billed ingestion, so list only sources someone reads and set the threshold above normal latency.

    "WeborityTelemetry": {
      "AlwaysKeepSources": [ "Experimental.Microsoft.Agents.AI", "Experimental.Microsoft.Extensions.AI" ],
      "SlowDependencyThresholdMs": 500,
      "SlowRequestThresholdMs": 1000
    }
    
  • Slow requests are kept (SlowRequestThresholdMs, off by default). A successful server span at least that many milliseconds long is kept whole, and so is the server span of any trace in which a slow dependency was kept, for up to two minutes after that dependency ended. Both are recorded at rate 1 (webority.sample_rate), not at the sampled rate, so counts of kept requests stay exact and the sampled remainder estimates only the requests under the threshold. A SignalR connection (http.long_running) is excluded.

  • SQL statement text (CaptureSqlText, on by default). A kept SQL dependency carries its statement as the dependency's data. The instrumentation has already replaced every literal with ?, and the text is cut at 2,000 characters. Parameter values are never exported. The dependency's name is the statement summary (for example SELECT Projects) rather than SQL: <database>. A stored procedure keeps its procedure name as data. CaptureSqlText: false keeps the summary and drops the text.

  • Kept metrics (KeptMetrics, off by default). An allow list of metric names exported although EnableMetrics is false; every other instrument is dropped. A name starting with dotnet. subscribes the in-box System.Runtime meter. The exporter sends metrics every 60 seconds: a counter as the change in that minute, an up-down counter as its level. Combining it with EnableMetrics: true fails at registration.

  • Runtime resource row off. The exporter adds one resource item to every trace batch, which Application Insights shows as _APPRESOURCEPREVIEW_ in AppMetrics. Telemetry sets OTEL_DOTNET_AZURE_MONITOR_ENABLE_RESOURCE_METRICS to false unless the head sets it itself. The item repeats the service.* attributes already on every row, so nothing is lost.

  • Log export floors at Warning (MinimumLogExportLevel). This filters only what OpenTelemetryLoggerProvider forwards to Azure Monitor — a head's own ILogger calls, and any other registered provider (console, debug, ...), are unaffected.

These are all options on WeborityTelemetryOptions, overridable per head via the configure delegate or the WeborityTelemetry configuration section — this is the fleet default, not a hard rule.

The distro's own AzureMonitor configuration section is not a supported surface here. The package calls the UseAzureMonitor(Action<AzureMonitorOptions>) overload, and only the parameterless overload registers the internal options type that reads that section, so a head putting an AzureMonitor block in its appsettings finds it silently inert. That is deliberate rather than an oversight: the section carries SamplingRatio and TracesPerSecond, so honouring it would hand a head back the head sampler this package pins off. Configure the exporter through the configure delegate.

Tenant on a request

A head calls SetTelemetryTenant where it resolves the tenant, and the request row carries customDimensions["webority.tenant"]. Only a positive id is written.

using Webority.AzureTelemetry;

httpContext.SetTelemetryTenant(accountId);

Client dimensions on a request

A request span carries customDimensions["webority.client_kind"] from the X-Client-Kind header and customDimensions["webority.app_build"] from the X-App-Build header. Each is trimmed and cut to 64 characters, and a blank header writes no dimension.

Background job runs

A job wraps each run in WeborityJobRun.Start. The run is one always-kept request row named after the job, tagged customDimensions["webority.job"]. Call WeborityJobRun.Fail(run, exception) in the catch block to mark it failed. A head referencing Webority.Azure.Telemetry has it already; a project below the web layer references Webority.Azure.Telemetry.Jobs alone, and the head's telemetry exports its runs.

using var run = WeborityJobRun.Start("OutboxDrain");

Telemetry for Functions hosts

An Azure Functions isolated-worker host calls AddWeborityFunctionsTelemetry from Webority.Azure.Telemetry.Functions instead of AddWeborityTelemetry, with the same arguments and the same environment gate:

using Webority.AzureTelemetry.Functions;

if (!environment.IsDevelopment())
{
    services.AddWeborityFunctionsTelemetry(configuration);
}

It does everything AddWeborityTelemetry does, then sets the worker capability WorkerApplicationInsightsLoggingEnabled. That tells the Functions host the worker exports its own logs, so the host stops exporting them a second time as Function.<Name>.User rows. ConfigureFunctionsApplicationInsights sets the same capability but needs the classic Application Insights SDK, which is why it cannot be used here.

  • The capability follows WeborityTelemetry:Enabled. A host that switches telemetry off keeps the Functions host's own log export, rather than losing both.
  • It returns the same builder, so a host adds its own processors by chaining: .WithTracing(tracing => tracing.AddProcessor<MyProcessor>()). The collection policy above, including the warning about chaining a second exporter, applies unchanged.
  • Worker logs and exceptions carry the invocation's operation id. While telemetry is exported the package applies UseFunctionsWorkerDefaults from Microsoft.Azure.Functions.Worker.OpenTelemetry, which listens to the worker's invocation ActivitySource, so each function runs inside an activity parented on the host's trace. That span is internal, not a request, so the host's own request row is not duplicated, and a successful one is dropped like any successful dependency. The worker defaults also name the service after the process's WEBSITE_SITE_NAME or the entry assembly; the package pins ServiceName again after them, so the configured name still wins.
  • Reference the package directly from the Functions project. The isolated worker drops packages that arrive only transitively from its deployed closure, so check that Webority.Azure.Telemetry.Functions.dll and Microsoft.Azure.Functions.Worker.OpenTelemetry.dll are listed in the published *.deps.json.

Upgrading

0.18.2

  • A head with no service name now starts and exports nothing, logging one warning, instead of refusing to start. Check the startup log of a local run or build agent pointed at a real component.
  • The failed-request count falls by the unmatched 404s, which are now sampled successes carrying webority.expected_failure. Set WeborityTelemetry:SampleUnmatchedRoute404 to false to keep them as failures.
  • Function apps: confirm Microsoft.Azure.Functions.Worker.OpenTelemetry.dll is in the published *.deps.json, or add a direct reference to it.

0.18.0

  • Telemetry with no service name refuses to start. A local run or build agent that starts a head with a real connection string outside App Service sets WeborityTelemetry:ServiceName (not a deployed head's name, or its rows merge into that role) or WeborityTelemetry:Enabled to false.
  • A fake implementing IAzureStorageBlobService adds EnsureContainerAsync.
  • The first write to each blob container reads it first, so the identity needs read access on the container. Storage Blob Data Contributor and account keys have it; a custom write-only role gets 403 on its first write.

0.15.0

  • The table-backed distributed cache moves to 256 partitions. Deploy expecting one reset: every session signs in again, and a token revoked shortly before the deploy is accepted again until it expires (up to its lifetime plus 5 minutes, about 65 minutes with the default 60-minute token). Revoke again after the deploy if it matters. Old rows in the cache partition stay until you delete them; nothing reads them.

0.12.0

  • Webority.Azure.Telemetry: nothing changes until a head opts in, apart from one new custom dimension, webority.sample_rate, on every exported span. Mark business-rule 4xx answers with HttpContext.MarkExpectedFailure() (see "Telemetry collection policy"). Marked requests become successes and are sampled, so the failed-request count and the number of 4xx rows both fall.

0.11.0

  • Webority.Azure.DataProtection is removed. AddWeborityDataProtection in Webority.Security.AspNetCore replaces it.

0.10.0

  • Breaking on 0.x: PushNotificationOptions.ImageUrl is a System.Uri. A caller that sets it in code writes ImageUrl = new Uri("https://example.com/a.png"); configuration binding still reads a URL string. The FCM image field carries the absolute URI string.

0.6.0

  • Breaking on 0.x, implementers only: the user and all-installations sends return a PushSendResult. SendNotificationToUserInstallationsAsync and SendNotificationToAllInstallationsAsync return Task<PushSendResult> instead of Task, and the interface gains GetSendOutcomeAsync. A caller that only awaits the send compiles unchanged. A class implementing IAzureNotificationHubsService, a test double above all, must return a result (new PushSendResult([]) will do) and implement the lookup.
  • A send never says whether anybody was reached. The push service answers a send before it has matched a device, so every platform in the result reads Enqueued, even for a user with no device. NoTargetFound and the per-device counts come from GetSendOutcomeAsync(notificationId) later, and only on the Standard tier; the Free and Basic tiers return no notification id and refuse the lookup.

0.5.1

No call site changes. What a head records does change, which a workbook or alert counting rows will see: dependencies that answered an expected 4xx stop appearing, and successful requests a portal started appear at the configured ratio where they appeared at none (or all) before.

0.5.0

The changelog carries every entry; these are the ones that change a call site or need an action before deploying.

  • SlidingExpiration now works, and the refusal shipped in 0.3.0 is gone. A sliding window is stored with the row and every read pushes the expiry forward; an absolute expiry beside it still wins whenever it falls earlier. A head calling AddSession() against this cache no longer fails on every session-touching request. Refresh and RefreshAsync stopped being no-ops, so a read-only request now keeps a session alive. A sliding read costs one extra write, skipped when the row is already at its absolute cap.
  • Breaking on 0.x: IAzureStorageTableService's writes take a TableCacheExpiry, not an int of seconds. SetAsync(..., 60) becomes SetAsync(..., TableCacheExpiry.After(TimeSpan.FromSeconds(60))), and the interface gains RefreshAsync / RefreshValue. Anything implementing the interface itself, a hand-written test double above all, will not compile until it follows.
  • Breaking on 0.x: the push contract speaks in our own types. IAzureNotificationHubsService no longer names Microsoft.Azure.NotificationHubs.Installation or NotificationPlatform, so a product registering a device stops compiling against the provider's package. NotificationPlatform.FcmV1 and NotificationPlatform.Apns become DevicePushPlatform.FcmV1 and DevicePushPlatform.Apns; GetInstallationAsync returns a DeviceInstallation, where PushChannel is now PushToken. Every call site gets a compile error. A device found on a platform this package does not send to is refused by name, and so is registering with DevicePushPlatform.None, which is what an unset value reads as.
  • Breaking on 0.x: SendNotificationToAllInstallationsWithTagsAsync is now SendNotificationToTagsAsync and returns a result. The rename is deliberate: the return type change alone would have compiled everywhere while silently removing the exception callers relied on for failure. Check result.AllDelivered, or walk result.Failures for the tags to retry. A send to an empty tag list makes no request and reports no batches; use SendNotificationToAllInstallationsAsync to reach everyone.
  • A user id the push service cannot hold in a tag is now refused, where it used to fail silently. Tags are checked against the service's own rule, at most 120 characters of letters, digits and _ @ # . : -, before any request. A product whose user identifier can contain a space, a slash or an accented letter must hash or encode it before registering: those devices were registering without a usable tag and receiving nothing. Check this one before upgrading if your user identifier is anything other than a number or a GUID.
  • Breaking on 0.x: an empty or blank queue message raises ArgumentException, not ArgumentNullException. A whitespace-only body used to reach the queue.

0.4.0

  • Breaking on 0.x: DequeueMessageAsync returns a QueueMessageReceipt, not a string. It no longer deletes the message, so a handler that throws leaves the work to be retried instead of losing it. Read .MessageText, and call CompleteMessageAsync(receipt.MessageId, receipt.PopReceipt) once the work has committed. .DequeueCount is there so a caller can move a message aside as poison after too many attempts. Every call site gets a compile error.
  • Breaking on 0.x: data protection refuses to keep keys inside the process by accident. With neither Azure:Storage:StorageAccountName nor Azure:Storage:ConnectionString configured, AddWeborityDataProtectionBlobKeys now throws unless the caller passes allowInProcessKeys: true. Local development says so out loud; a deployed head that had silently lost its storage settings fails at startup naming them instead of signing every user out on each restart.
  • The data protection key-ring container is a provisioning prerequisite. Nothing creates it, and neither does the underlying blobs package, so the container (dataprotection by default, private access) must exist on a freshly provisioned storage account before the first request that protects a cookie or an antiforgery token. Note the asymmetry with the storage package, which does create-on-write for blobs.
  • Breaking on 0.x: telemetry enabled with no Application Insights connection string is refused. It used to wire no exporter and say nothing, so a production head with a missing connection string ran dark. Keep the !env.IsDevelopment() gate around the call, set the connection string, or set WeborityTelemetry:Enabled to false deliberately.
  • Breaking on 0.x: SuccessfulRequestSamplingRatio must be between 0.0 and 1.0. Nothing validated it before, so 25 (meaning 25 per cent) kept every successful request and -1 dropped them all. Correct any head whose configured value is a percentage: 25 becomes 0.25.
  • Breaking on 0.x: an Azure:Storage section with neither credential fails at boot. The check lived only in each service's constructor, and all three are lazily resolved singletons, so a head deployed without the section passed its health check and failed on the first upload.
  • Breaking on 0.x: table cache rows whose keys contain /, \, #, ? or % are rewritten once. The key transform mapped four characters onto one replacement, so two different cache keys could land on the same row and one caller read back another's value. Every rejected character is percent-escaped now, which makes the transform reversible. Keys without those characters are untouched; a key with one misses once and is written again, the same argument the expiry-column rename used. Nothing serves a stale value.
  • Breaking on 0.x: a push to a device on a platform the package cannot build a payload for now throws. WNS, MPNS, legacy FCM, ADM and Baidu used to complete with nothing sent. A legacy FCM installation is pointed at MigrateFcmToFcmV1Async.
  • Breaking on 0.x, interface only: IAzureNotificationHubsService.GetInstallationAsync takes a CancellationToken, and a visible push notification must carry a title. Blank text, action, title, user id, installation id or push token now raise ArgumentException naming the parameter rather than ArgumentNullException or a bare InvalidOperationException.
  • A blob SAS is capped at seven days under managed identity, because it cannot outlive its user delegation key. A longer request is refused naming the limit instead of answering an opaque 400.
  • A container or queue deleted under a running process is recreated rather than failing every later write for the life of the process, matching what the table service already did.
  • The packages now carry an MIT licence.

Earlier releases

  • Breaking on 0.x: WeborityTelemetry:SamplingRatio is retired, and a configuration that still carries it is refused at startup. It set the exporter's head sampler, which decides at span start and so dropped failed requests, 4xx refusals and unhandled exceptions at the same rate as successes: the keep-every-failure rules beside it could only ever trim what it had already let through. Head sampling is pinned to always-on now, and SuccessfulRequestSamplingRatio (default 0.25) is the one ratio, applied to successful requests only. Remove the key from each head's appsettings, and move its value to SuccessfulRequestSamplingRatio only if that head really wants a different success ratio. The refusal is deliberate: binding would ignore the key silently and leave a head reading its own config as a decision nothing applies. It fires before the Enabled check as well, so a head that has switched telemetry off is told about the stale key too, rather than finding out the first time it switches telemetry back on.

  • The distro's rate-limited head sampler is switched off too. AzureMonitorOptions.TracesPerSecond defaults to 5.0 and takes precedence over SamplingRatio, so a busy head was capped at five traces a second whatever ratio it set, with failures discarded above that line. The package sets it to null.

  • Expect more Warning-and-above logs to reach Azure Monitor, not just more traces. The distro's trace-based logs sampler is on by default and exports a log record only when its trace was sampled, so while the rate limiter was capping traces it was quietly suppressing those logs too. With head sampling pinned always-on, every record at or above MinimumLogExportLevel now exports. That is the intended direction, and on a busy head it is a real rise in billed ingestion: check the component's daily cap before upgrading.

  • A 5xx with no span status is kept like a 4xx. The rule read 400 to 499; it reads 400 and above now, so a server error recorded by instrumentation that leaves the span status Unset is no longer sampled like a success.

  • Breaking on 0.x: SlidingExpiration is refused instead of silently written as a fixed window. A row carries one absolute expiry and a read cannot extend it, so an entry asked to slide never slid: the caller asked for one thing and got another, with nothing said. Set and SetAsync now throw ArgumentException whenever SlidingExpiration is set, including when an absolute window sits beside it. Use AbsoluteExpirationRelativeToNow and write the entry again when it should live longer. A head calling AddSession() against this cache is the one to check before upgrading: DistributedSession sets SlidingExpiration on every commit.

  • A cache entry shorter than a second is written with a one-second life, not a zero-second one. The relative and sliding branches truncated the window to whole seconds, so anything under a second became a row that had already expired when it was written and every later read missed. The absolute branch already floored at one second; all of them do now.

  • The cache table has one default name, DistributedCache. The option's own default said PermissionCache while the cache fell back to DistributedCache when no Table section was configured, so adding "Table": {} to a head's config moved the whole cache to another table and emptied it. The surviving value is the one an unconfigured head already resolves, so nothing in a deployed head changes. A head that configured Azure:Storage:Table:CacheTableName explicitly is unaffected either way; one that relied on the option default while binding an empty section will now read the table it was already writing to.

  • A table dropped after the first write is recreated instead of failing forever. The service remembers that it created a table so it stops paying for a create on every write, and that memory outlived the table: drop it by hand, from a cleanup job, or between deployments, and every later write failed for the life of the process. A 404 from a write now forgets the table, creates it and retries once. The failed first attempt is still reported as a failure, because a table disappearing under a running head is worth seeing.

  • Table cache rows written by an earlier version are treated as expired and rewritten. The cache stores its TTL in ExpiresDateTimeUtc (previously ExpiresAtUtc) and its write time in CachedDateTimeUtc (previously CachedAtUtc). A row whose expiry cannot be read — column absent, null, or unparseable — is a miss, never a never-expiring hit, so pre-existing rows simply miss once and are rewritten. No action needed, and no dual-read shim: reading both names would restore exactly the ambiguity that let a stale short-TTL entry validate forever.

  • A table cache miss is quiet. The cache reads with the not-found-tolerant overload, so a missing row no longer throws inside the SDK, logs an error trace, or exports as a failed dependency. Products that count failed dependencies will see the revocation-check misses disappear from that stream. Writes are unchanged.

  • A table cache delete of a row that is already gone is quiet too. The SDK never threw on that 404, so the catch around the delete was doing nothing, but the request still carried no response classifier and the pipeline called every such delete a failure. A per-call policy now classifies 204 and 404 as the success pair for a DELETE, so a sign out racing a TTL, a second instance evicting the same key, or a replayed webhook stops producing an error trace and a failed dependency. A delete that fails for any other reason still throws.

  • Breaking on 0.x: IAzureStorageTableService gained three synchronous members, GetValue, SetValue and RemoveValue. AzureTableDistributedCache needs them because IDistributedCache has a synchronous half, and its synchronous members used to block on the asynchronous ones, parking a thread pool thread for each round trip. Consuming products need no change; anything that implements the interface itself, a hand-written test double above all, will not compile until it adds the three.

  • A missing blob is quiet on the three read paths; a missing container is now loud. GetAsync, OpenReadAsync and GetStagedBlockIdsAsync already answered a missing blob with null or an empty list, but the pipeline underneath called the 404 a failure, so each ordinary miss wrote an error event and exported a failed dependency. Reads now treat a 404 as a non-failure only when the service says BlobNotFound. The SDK still throws either way, because it decides that from the status code rather than from the classifier, so a missing blob behaves exactly as before.

  • Breaking on 0.x: a read only absorbs BlobNotFound now, and throws on any other 404. A missing container answers 404 like a missing blob, and all three read paths used to absorb it and answer null or empty, so a typo in a container name was indistinguishable from an absent file. ContainerNotFound is misconfiguration, not a miss, and a 404 carrying any other code is a case the service does not understand, which is the one that must least become an empty result. Both are let out now, logged by the pipeline as the failures they are. A product that relied on a read of a non-existent container answering empty will start seeing RequestFailedException. Writes are untouched, and still create the container on first use.

  • The existence check is the exception, by SDK design. ExistsAsync answers false for a missing container as well as a missing blob, because BlobBaseClient.ExistsAsync absorbs that 404 inside the SDK before our code sees it. The three read paths are the loud ones; do not read a false from the existence check as proof that the container is right.

  • Blob containers are created on first write. SaveAsync and the block-staging paths create the resolved container (private, PublicAccessType.None) once per container name per process, matching the queue and table services. Products can drop their own create-if-missing call before every upload. Read paths still fail on a missing container, so a wrong container name surfaces instead of being silently created.

Releasing

Version lives once in Directory.Build.props (<VersionPrefix>), the fleet's standard version-of-record for a package repo. Push to main publishes all four packages to public nuget.org through the fleet's shared workflow (.github/workflows/publish.yml, a thin caller into Webority/gh-actions). No code checks run in CI; build and test locally before pushing (CLAUDE.md). Work happens on development; a release is a PR development → main merged with a merge commit, per the fleet git conventions.

version-check.yml gates that PR on two things, both failing the PR rather than warning:

  1. Directory.Build.props <VersionPrefix> has increased against main. Without it the publish skips every package and exits green having shipped nothing.
  2. The release paperwork names the new version. CHANGELOG.md must carry a heading for it and must no longer say ## [Unreleased], and README.md's Upgrading section must no longer say ### Unreleased. 0.4.0 shipped with that heading still in place above changes that had gone out, because nothing touched the file at release time and nothing checked it.

So cutting a release is three edits in one commit: the version, the changelog heading, and the Upgrading heading.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Webority.Azure.Monitor:

Package Downloads
Webority.Insights

Product insights a product hosts on its own data: daily collectors for PostHog usage, funnels and retention, Microsoft Clarity, Google Analytics 4, Application Insights and SendGrid, the rollup tables they write, and the run log. The product owns its DbContext, its migrations and its keys.

Webority.Diagnostics

The Diagnostics engine for Webority products: the build-time infra slice, secret masking, the collection runner a Functions timer calls, history in monthly Azure Tables, and the core sources (Azure metrics and alerts, App Insights, Cloudflare, GitHub, cost, package drift, Webority OS, Crashlytics).

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.26.0 44 10/17/2026
0.25.0 47 10/17/2026
0.24.2 62 10/16/2026
0.24.0 140 10/15/2026
0.23.0 55 10/15/2026
0.22.1 47 10/15/2026
0.22.0 70 10/15/2026