UsagePulse is a .NET 8 reference implementation for near real-time usage event ingestion, resilient processing, and tenant-facing usage read models on Azure.
The repository is intentionally documented around what is implemented today, not the full platform vision.
UsagePulse is meant to improve four outcomes that matter to product and platform teams:
- Reliability: reject malformed or incompatible events before they pollute downstream data.
- Recovery: make poison-message handling and replay operationally manageable.
- Analytics accuracy: keep hot summary views aligned with accepted events.
- Scalability foundation: provide queue-based processing, batching, and tenant-aware throttling as a base for future growth.
- Event Hub-triggered ingestion through
UsageIngestionFunction. - Version-aware contract validation at ingestion time.
- Uses
SchemaVersion,Source, and per-rule compatibility settings. - This is not a separate schema registry product; it is in-process contract validation logic.
- Uses
- Tenant quota and burst checks before work is sent to the Service Bus processing queue.
- Correlation propagation across queue boundaries.
- Thin Functions triggers with orchestration separated from processing logic.
- Pipeline-style event processor with stages for:
- validation
- deduplication
- persistence
- analytics export
- finalization
- Validation failures are dead-lettered.
- Duplicate events are skipped through idempotency checks.
- Retry and circuit-breaker behavior is implemented with Polly.
- Dead-letter reasons and validation codes are strongly typed.
- Raw event persistence in Cosmos DB.
- Hot summary documents in Cosmos DB for dashboard-style queries.
- Summary windows implemented today:
5m1h24h
- Realtime dashboard endpoint backed by those Cosmos summary documents.
- Buffered Azure Data Explorer ingestion via the Kusto ingestion SDK.
- Configurable batch size and flush interval.
- Stream Analytics query asset plus deployment script that configures:
- Event Hubs input
- Cosmos DB output
- transformation query
- job start
- Dead-letter replay endpoint with:
MaxMessages- tenant filtering
- reason-code filtering
- dry-run mode
- Managed Identity-first configuration.
- Optional Azure Key Vault bootstrap.
- OpenTelemetry instrumentation hooks.
- Application Insights and Log Analytics workspace provisioning via Terraform.
- Azure Monitor alert action group and metric alerts for queue depth and ingress volume via Terraform.
- Architecture tests that protect current layering rules.
The following items should be treated as roadmap work, not current platform guarantees:
- A true external schema registry.
- A standardized event envelope with explicit metadata/payload separation.
- An outbox pattern between Cosmos DB persistence and ADX/Kusto export.
- Fully separated hot and cold processing paths.
- Automated remediation workflows for alerts.
- Anomaly detection.
- Blue/green or progressive delivery.
- A stricter
Domain -> Application -> Infrastructure -> Functions/APIarchitecture split.
Event Hubs
-> UsageIngestionFunction
-> ingress policy evaluation
-> Service Bus work queue
-> UsageProcessingFunction
-> processing pipeline
-> Cosmos DB raw event store
-> Cosmos DB summary view updates
-> buffered ADX/Kusto export
-> dead-letter queue on failure
Query API
-> Cosmos DB summary views for dashboard windows
-> Cosmos DB raw events for broader summary queries
The repository already has the beginnings of hot/cold separation, but it is not fully decoupled yet.
- Hot path today: Cosmos DB summary view documents are updated during event processing and serve the realtime dashboard endpoint.
- Cold path today: the same processing flow also queues accepted events for batched ADX/Kusto ingestion.
- Important limitation: both paths are still initiated from the same processing transaction flow, so there is no outbox yet to guarantee atomic handoff between persistence and analytics export.
Materialized summaries are updated inline during event processing, not by a separate aggregation job.
Specifically:
- the processing pipeline persists accepted usage events
- the analytics sink updates Cosmos summary view documents for
5m,1h, and24hwindows - that same sink also batches events for ADX/Kusto ingestion
This design keeps dashboards simple and low-latency, but it also means summary maintenance currently shares the event-processing path.
The current event model is UsageEvent:
EventIdTenantIdUserIdFeatureQuantityOccurredAtDimensionsSchemaVersionSource
The repo also uses strict value objects for:
EventIdTenantIdFeatureName
A future revision should move to a standardized envelope such as:
correlationIdtenantIdschemaVersioneventTypeoccurredAtsourcepayload
That envelope is planned, not implemented in the current codebase.
GET /healthGET /api/usage/{tenantId}/summary?from=<iso>&to=<iso>GET /api/dashboard/{tenantId}/realtime?window=5m|1h|24h
- Event Hub-triggered ingestion via
UsageIngestionFunction - Service Bus-triggered processing via
UsageProcessingFunction - HTTP replay endpoint via
POST /api/operations/dlq/replay
The realtime dashboard endpoint is intended to surface:
- total event count for the selected window
- total quantity for the selected window
- feature-level usage breakdown
- bucketed time-series points for trend charts
{
"tenantId": "tenant-a",
"from": "2026-01-10T10:00:00Z",
"to": "2026-01-10T11:00:00Z",
"eventCount": 128,
"totalQuantity": 742,
"featureBreakdown": {
"dashboard": 420,
"export": 210,
"alerts": 112
},
"points": [
{
"bucketStart": "2026-01-10T10:00:00Z",
"bucketEnd": "2026-01-10T10:05:00Z",
"eventCount": 11,
"totalQuantity": 57
},
{
"bucketStart": "2026-01-10T10:05:00Z",
"bucketEnd": "2026-01-10T10:10:00Z",
"eventCount": 14,
"totalQuantity": 61
}
]
}{
"tenantId": "tenant-a",
"from": "2026-01-09T11:00:00Z",
"to": "2026-01-10T11:00:00Z",
"eventCount": 2210,
"totalQuantity": 13840,
"featureBreakdown": {
"dashboard": 6400,
"export": 5030,
"alerts": 2410
}
}The following items are currently backed by code and automated validation in the repo:
- solution builds successfully on .NET 8
- processing, Functions, and architecture tests pass
- retry, duplicate suppression, schema rejection, quota rejection, and dead-letter serialization are covered by tests
- batched ADX/Kusto export is implemented with default settings of:
KustoBatchSize = 250KustoFlushIntervalSeconds = 5
- replay requests are capped by code to a maximum of
200messages per call
This repository now includes a repeatable benchmark harness and publication workflow:
- k6 load script in
benchmarks/k6/usagepulse-ingestion.js - baseline guidance in
benchmarks/README.md - published baseline template in
docs/performance/latest-baseline.md - CI stage that runs the benchmark and publishes a summary artifact
Production-scale claims should still be based on the latest measured values captured in docs/performance/latest-baseline.md.
The following targets are recommended and documented here as goals, not current guarantees:
| Area | Target |
|---|---|
| API availability | 99.9% monthly |
| Accepted event processing success rate | >= 99.95% |
| Queue lag | < 60 seconds at steady state |
| P95 ingestion-to-summary latency | < 30 seconds |
| DLQ replay recovery for common operator cases | < 15 minutes |
src/UsagePulse.Contracts
Shared contracts, typed identifiers, failures, and response models.src/UsagePulse.Serialization
JSON serialization helpers for usage events and dead-letter envelopes.src/UsagePulse.Processing
Processing abstractions, pipeline stages, telemetry, and resilience behavior.src/UsagePulse.Functions
Azure Functions host, orchestrators, triggers, replay endpoint, and infrastructure adapters.src/UsagePulse.QueryApi
Read API for summaries and realtime dashboard views.tests/UsagePulse.Processing.Tests
Unit tests for retries, dead-letter behavior, duplicates, and validation.tests/UsagePulse.Functions.Tests
Unit tests for ingress policy decisions and dead-letter envelope behavior.tests/UsagePulse.Architecture.Tests
Tests for current layering boundaries.
Current enforced boundaries are modest:
- contracts must not reference upper layers
- processing must not reference Functions or Query API adapters
The intended next step is to evolve toward clearer Domain -> Application -> Infrastructure -> Functions/API boundaries. That stronger layering is planned work, not a completed claim.
- Runbooks:
docs/runbooks/operations.md - ADRs:
docs/adr/README.md
Both the Functions app and Query API use the UsagePulse configuration section.
EventHubNameServiceBusQueueDeadLetterQueueServiceBusNamespaceServiceBusConnectionStringCosmosEndpointCosmosConnectionStringCosmosDatabaseEventsContainerIdempotencyContainerSummaryViewsContainer
KustoClusterUriKustoDatabaseKustoTableKustoManagedIdentityClientIdKustoBatchSizeKustoFlushIntervalSeconds
CurrentSchemaVersionMinimumCompatibleSchemaVersionSchemaContractsDefaultTenantQuotaTenantQuotas
MaxProcessingAttemptsBaseRetryDelayMsCircuitBreakerSamplingSecondsCircuitBreakerDurationSecondsCircuitBreakerMinimumThroughputCircuitBreakerFailureRatio
KeyVaultUriAllowConnectionStringFallback
Managed Identity is the default runtime path. Connection strings are supported only as an explicit fallback.
- .NET SDK 8.x
- Azure Functions Core Tools
- Terraform 1.6 or later
- Access to Azure resources for Cosmos DB, Service Bus, Event Hubs, and optional ADX
dotnet restore UsagePulse.slnx
dotnet build UsagePulse.slnxdotnet test UsagePulse.slnx
dotnet test tests/UsagePulse.Functions.Tests/UsagePulse.Functions.Tests.csprojdotnet run --project src/UsagePulse.QueryApi/UsagePulse.QueryApi.csproj- Copy
src/UsagePulse.Functions/local.settings.sample.jsontolocal.settings.json. - Populate the
UsagePulseconfiguration values. - Start the Functions host:
func start --csharpNear-term priorities:
- Add an outbox pattern between persistence and analytics export.
- Split hot dashboard updates from long-term analytics export more cleanly.
- Introduce a standardized event envelope.
- Publish recurring baseline performance results from production-like traffic.
- Add anomaly detection and stronger delivery safety controls.
cd infra/terraform
terraform init
terraform validate
terraform planazure-pipelines.yml(Azure DevOps CI/CD: build, test, Terraform plan/apply, Stream Analytics setup, AKS deploy)deploy/aks/usagepulse-queryapi.yamldeploy/stream-analytics/usagepulse-job.asaqldeploy/stream-analytics/deploy-stream-analytics.ps1benchmarks/k6/usagepulse-ingestion.js
Define these pipeline variables/service connections before enabling deploy stages:
azureServiceConnectionaksServiceConnectionresourceGroupNameprefixeventHubPolicyKey(secret)cosmosAccountKey(secret)
Optional for performance baseline stage:
usagePulseBaseUrlusagePulseTenantId