Metrics in ConnectSoft Microservice Template¶
Purpose & Overview¶
Metrics in the ConnectSoft Microservice Template provide quantitative measurements of system behavior, performance, and business operations. Built on OpenTelemetry Metrics API, the template enables teams to collect, aggregate, and export metrics for monitoring, alerting, and performance analysis.
Metrics provide:
- Performance Monitoring: Track latency, throughput, and resource utilization
- Business Metrics: Measure business operations and outcomes
- Error Tracking: Count failures and exceptions
- Resource Monitoring: Track CPU, memory, and other system resources
- SLO Tracking: Monitor service-level objectives and compliance
- Custom Metrics: Application-specific measurements for business intelligence
- Automatic Instrumentation: Built-in metrics for common operations
- Multiple Exporters: Support for Prometheus, OTLP, Application Insights, and more
Metrics Philosophy
Metrics are the quantitative foundation of observability, providing aggregated measurements over time that enable trend analysis, alerting, and capacity planning. The template uses OpenTelemetry Metrics API for standardized, vendor-neutral metrics collection, ensuring compatibility with modern observability platforms while enabling custom business metrics for domain-specific insights.
Architecture Overview¶
Metrics Stack¶
Application Code
↓
OpenTelemetry Metrics API
├── Automatic Instrumentation
│ ├── Runtime Instrumentation (.NET runtime)
│ ├── ASP.NET Core (HTTP requests)
│ ├── HttpClient (outgoing HTTP)
│ ├── Process (system resources)
│ ├── MassTransit (messaging)
│ ├── NServiceBus (messaging)
│ ├── Orleans (actor metrics)
│ └── Semantic Kernel (AI operations)
├── Custom Metrics
│ ├── MicroserviceTemplateMetrics
│ ├── FeatureAMetrics
│ ├── BankAccountActorMetrics
│ └── McpToolMetrics
└── MeterProvider
├── Meter Creation
├── Metric Collection
└── Aggregation
↓
Exporters
├── OTLP Exporter (OpenTelemetry Collector)
├── Console Exporter (development)
├── Prometheus Exporter (optional)
└── Application Insights (via OTLP)
↓
Observability Backends
├── Prometheus
├── Grafana
├── Application Insights
└── OpenTelemetry Collector
Metrics Components¶
| Component | Purpose | Location |
|---|---|---|
| MeterProvider | Creates and manages meters | OpenTelemetry SDK |
| Meter | Factory for creating instruments | OpenTelemetry Metrics API |
| IMeterFactory | Dependency injection for meter creation | ASP.NET Core |
| Counter | Monotonically increasing metric | Custom metrics classes |
| Histogram | Distribution of values | Custom metrics classes |
| Gauge | Current value snapshot | Custom metrics classes |
| UpDownCounter | Value that can increase or decrease | Custom metrics classes |
| MicroserviceTemplateMetrics | Business metrics for aggregate roots | Metrics project |
| FeatureAMetrics | Feature-specific metrics | Metrics project |
| McpToolMetrics | MCP tool invocation metrics | Metrics project |
Service Registration¶
Metrics Registration¶
AddMicroserviceMetrics:
// MicroserviceMetricsExtensions.cs
internal static IServiceCollection AddMicroserviceMetrics(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
#if (UseNHibernate || UseMongoDb)
services.AddSingleton<MicroserviceTemplateMetrics>();
#endif
services.AddSingleton<FeatureAMetrics>();
#if UseOrleans
services.AddSingleton<BankAccountActorMetrics>();
#endif
#if UseMCP
services.AddSingleton<McpToolMetrics>();
#endif
#if (UseNHibernate || UseMongoDb)
services.ActivateSingleton<MicroserviceTemplateMetrics>();
#endif
services.ActivateSingleton<FeatureAMetrics>();
#if UseOrleans
services.ActivateSingleton<BankAccountActorMetrics>();
#endif
#if UseMCP
services.ActivateSingleton<McpToolMetrics>();
#endif
return services;
}
Registration in Startup:
OpenTelemetry Metrics Configuration¶
Metrics Configuration:
// OpenTelemetryExtensions.cs
services.AddOpenTelemetry()
.WithMetrics(metricsBuilder =>
{
metricsBuilder.ConfigureMicroserviceOpenTelemetryMetrics(
OptionsExtensions.OpenTelemetryOptions);
});
Metrics Builder Setup:
private static void ConfigureMicroserviceOpenTelemetryMetrics(
this MeterProviderBuilder metricsBuilder,
OpenTelemetryOptions otelOptions)
{
metricsBuilder
// Automatic instrumentation
.AddRuntimeInstrumentation() // .NET runtime metrics
.AddAspNetCoreInstrumentation() // HTTP request metrics
.AddProcessInstrumentation() // Process metrics (CPU, memory)
.AddHttpClientInstrumentation() // HTTP client metrics
// Framework meters
#if UseNServiceBus
.AddMeter("NServiceBus.Core")
#endif
#if UseMassTransit
.AddMeter(MassTransit.Monitoring.InstrumentationOptions.MeterName)
#endif
#if UseOrleans
.AddMeter("Microsoft.Orleans")
#endif
#if UseSemanticKernel
.AddMeter("Microsoft.SemanticKernel*")
#endif
#if (UseMicrosoftExtensionsAIOpenAIProvider || UseSemanticKernelOpenAIConnector)
.AddMeter("OpenAI.*")
#endif
#if UseMicrosoftExtensionsAI
.AddMeter("Experimental.Microsoft.Extensions.AI*")
#endif
// Custom application meters
.AddMeter(MicroserviceTemplateMetrics.MicroserviceTemplateMetricsMeterName)
.AddMeter(FeatureAMetrics.FeatureAMetricsMeterName)
#if UseMCP
.AddMeter(McpToolMetrics.McpToolMetricsMeterName)
#endif
// Exporters
.AddOtlpExporter(options =>
{
options.Protocol = (OtlpExportProtocol)otelOptions.OtlpExporter.OtlpExportProtocol;
options.Endpoint = new Uri(otelOptions.OtlpExporter.Endpoint);
})
.AddOtlpExporter(options =>
{
// Seq OTLP exporter
options.Protocol = (OtlpExportProtocol)otelOptions.OtlpSeqExporter.OtlpExportProtocol;
options.Endpoint = new Uri($"{otelOptions.OtlpSeqExporter.Endpoint}/ingest/otlp/v1/logs");
});
if (otelOptions.EnableConsoleExporter)
{
metricsBuilder.AddConsoleExporter();
}
// Prometheus exporter (optional)
// metricsBuilder.AddPrometheusExporter();
}
Metric Types¶
Counter¶
Purpose: Monotonically increasing metric for counting events.
Characteristics: - Only increases (never decreases) - Reset on application restart - Used for counting operations, requests, errors
Example:
public class MicroserviceTemplateMetrics
{
private readonly Counter<long> added;
public MicroserviceTemplateMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create(MicroserviceTemplateMetricsMeterName);
this.added = meter.CreateCounter<long>(
name: "connectsoft.microservicetemplate.aggregateroot.added",
unit: "1",
description: "Number of aggregate roots added");
}
public void AddMicroserviceAggregateRoot(string? tenantId, string? aggregate)
{
var tags = BuildTags(tenantId, aggregate);
this.added.Add(1, tags);
}
}
Usage:
// Increment counter
metrics.AddMicroserviceAggregateRoot(tenantId: "tenant-1", aggregate: "Order");
Histogram¶
Purpose: Track distribution of values (e.g., latency, sizes).
Characteristics: - Records individual measurements - Aggregates into buckets for percentile calculation - Used for latency, durations, sizes
Example:
public class MicroserviceTemplateMetrics
{
private readonly Histogram<double> addDurationSeconds;
public MicroserviceTemplateMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create(MicroserviceTemplateMetricsMeterName);
this.addDurationSeconds = meter.CreateHistogram<double>(
name: "connectsoft.microservicetemplate.aggregateroot.add.duration",
unit: "s",
description: "Add operation duration");
}
public void AddMicroserviceAggregateRoot(
TimeSpan duration,
string? tenantId = null,
string? aggregate = null)
{
var tags = BuildTags(tenantId, aggregate);
this.added.Add(1, tags);
this.addDurationSeconds.Record(duration.TotalSeconds, tags);
}
}
Usage:
var stopwatch = Stopwatch.StartNew();
// ... operation ...
stopwatch.Stop();
metrics.AddMicroserviceAggregateRoot(
stopwatch.Elapsed,
tenantId: "tenant-1",
aggregate: "Order");
Benefits: - Calculate percentiles (p50, p95, p99, p999) - Understand value distribution - Track SLO compliance - Identify outliers
UpDownCounter¶
Purpose: Track value that can increase or decrease (e.g., active connections, queue length).
Characteristics: - Can increase or decrease - Represents current state - Used for counts that vary (e.g., active instances, queue size)
Example:
public class MicroserviceTemplateMetrics
{
private readonly UpDownCounter<long> total;
public MicroserviceTemplateMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create(MicroserviceTemplateMetricsMeterName);
this.total = meter.CreateUpDownCounter<long>(
name: "connectsoft.microservicetemplate.aggregateroot.total",
unit: "1",
description: "Live number of aggregate roots");
}
public void IncreaseTotalMicroserviceAggregateRoots(string? tenantId, string? aggregate)
{
var tags = BuildTags(tenantId, aggregate);
this.total.Add(1, tags);
}
public void DecreaseTotalMicroserviceAggregateRoots(string? tenantId, string? aggregate)
{
var tags = BuildTags(tenantId, aggregate);
this.total.Add(-1, tags);
}
}
Usage:
// Create aggregate
metrics.IncreaseTotalMicroserviceAggregateRoots(tenantId: "tenant-1", aggregate: "Order");
// Delete aggregate
metrics.DecreaseTotalMicroserviceAggregateRoots(tenantId: "tenant-1", aggregate: "Order");
ObservableGauge¶
Purpose: Snapshot of current value (e.g., current memory usage, active connections).
Characteristics: - Provides current value on demand - Not cumulative - Used for current state measurements
Example:
public class ResourceMetrics
{
private readonly ObservableGauge<double> currentMemory;
public ResourceMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("ConnectSoft.MicroserviceTemplate.Resources");
this.currentMemory = meter.CreateObservableGauge<double>(
name: "process.memory.usage",
unit: "bytes",
description: "Current memory usage",
observeValues: ObserveCurrentMemory);
}
private Measurement<double> ObserveCurrentMemory()
{
var process = Process.GetCurrentProcess();
return new Measurement<double>(
process.WorkingSet64,
new TagList { { "process.name", process.ProcessName } });
}
}
Metrics Project¶
The Metrics Project (ConnectSoft.MicroserviceTemplate.Metrics) is a dedicated project that contains all custom business metrics classes for the microservice. It provides a clean, organized structure for defining domain-specific metrics following consistent patterns and conventions.
Project Structure¶
ConnectSoft.MicroserviceTemplate.Metrics/
├── ConnectSoft.MicroserviceTemplate.Metrics.csproj
├── MicroserviceTemplateMetrics.cs # Aggregate root metrics
├── FeatureAMetrics.cs # Feature-specific metrics
├── BankAccountActorMetrics.cs # Actor-specific metrics (Orleans)
├── DurationScope.cs # Timing scope helper
└── GlobalSuppressions.cs # Code analysis suppressions
Project Purpose¶
The Metrics project serves as:
- Centralized Metrics Definition: All custom business metrics in one location
- Domain Metrics: Metrics specific to business operations and aggregates
- Consistent Patterns: Standardized approach to creating metrics classes
- Reusability: Metrics classes can be injected and used across the application
- Testability: Metrics classes are easily mockable for unit testing
- Clean Architecture: Separates metrics definition from application logic
Metrics Class Architecture¶
Each metrics class follows a consistent structure:
- Constants: Meter name and metric name constants
- Constructor: Creates meter and instruments via
IMeterFactory - Public Methods: Record metrics with optional tags and duration
- Timing Scopes: Return
IDisposablescopes for automatic timing - Tag Building: Private helper methods for constructing tags
Creating a New Metrics Class¶
Step 1: Define Constants
public class OrderMetrics
{
// Meter name (used for registration)
public const string OrderMetricsMeterName = "connectsoft.microservicetemplate.order";
// Metric name constants
public const string OrdersCreatedCounterName =
"connectsoft.microservicetemplate.order.created";
public const string OrderCreationDurationName =
"connectsoft.microservicetemplate.order.creation.duration";
// Module and component attributes for tags
private const string ModuleAttribute = "ConnectSoft.MicroserviceTemplate";
private const string ComponentAttribute = "Order";
}
Step 2: Create Instruments in Constructor
private readonly Counter<long> ordersCreated;
private readonly Histogram<double> orderCreationDurationSeconds;
public OrderMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create(OrderMetricsMeterName);
this.ordersCreated = meter.CreateCounter<long>(
name: OrdersCreatedCounterName,
unit: "1",
description: "Number of orders created");
this.orderCreationDurationSeconds = meter.CreateHistogram<double>(
name: OrderCreationDurationName,
unit: "s",
description: "Order creation duration");
}
Step 3: Add Public Methods
/// <summary>
/// Records an order creation with optional duration and tags.
/// </summary>
public void RecordOrderCreated(
TimeSpan? duration = null,
string? tenantId = null,
string? orderType = null)
{
var tags = BuildTags(tenantId, orderType);
this.ordersCreated.Add(1, tags);
if (duration.HasValue)
{
this.orderCreationDurationSeconds.Record(duration.Value.TotalSeconds, tags);
}
}
/// <summary>
/// Creates a timing scope for order creation.
/// </summary>
public IDisposable TimeOrderCreation(string? tenantId = null, string? orderType = null)
{
return DurationScope.Seconds(
this.orderCreationDurationSeconds,
BuildTags(tenantId, orderType));
}
Step 4: Add Tag Building Helper
private static KeyValuePair<string, object?>[] BuildTags(
string? tenantId = null,
string? orderType = null)
{
var tags = new List<KeyValuePair<string, object?>>(4)
{
new ("module", ModuleAttribute),
new ("component", ComponentAttribute),
};
if (!string.IsNullOrWhiteSpace(tenantId))
{
tags.Add(new ("tenant_id", tenantId));
}
if (!string.IsNullOrWhiteSpace(orderType))
{
tags.Add(new ("order_type", orderType));
}
return tags.ToArray();
}
Complete Example:
namespace ConnectSoft.MicroserviceTemplate.Metrics
{
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
/// <summary>
/// Order-related business metrics.
/// </summary>
public class OrderMetrics
{
public const string OrderMetricsMeterName = "connectsoft.microservicetemplate.order";
public const string OrdersCreatedCounterName = "connectsoft.microservicetemplate.order.created";
public const string OrderCreationDurationName = "connectsoft.microservicetemplate.order.creation.duration";
private const string ModuleAttribute = "ConnectSoft.MicroserviceTemplate";
private const string ComponentAttribute = "Order";
private readonly Counter<long> ordersCreated;
private readonly Histogram<double> orderCreationDurationSeconds;
public OrderMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create(OrderMetricsMeterName);
this.ordersCreated = meter.CreateCounter<long>(
name: OrdersCreatedCounterName,
unit: "1",
description: "Number of orders created");
this.orderCreationDurationSeconds = meter.CreateHistogram<double>(
name: OrderCreationDurationName,
unit: "s",
description: "Order creation duration");
}
public void RecordOrderCreated(
TimeSpan? duration = null,
string? tenantId = null,
string? orderType = null)
{
var tags = BuildTags(tenantId, orderType);
this.ordersCreated.Add(1, tags);
if (duration.HasValue)
{
this.orderCreationDurationSeconds.Record(duration.Value.TotalSeconds, tags);
}
}
public IDisposable TimeOrderCreation(string? tenantId = null, string? orderType = null)
{
return DurationScope.Seconds(
this.orderCreationDurationSeconds,
BuildTags(tenantId, orderType));
}
private static KeyValuePair<string, object?>[] BuildTags(
string? tenantId = null,
string? orderType = null)
{
var tags = new List<KeyValuePair<string, object?>>(4)
{
new ("module", ModuleAttribute),
new ("component", ComponentAttribute),
};
if (!string.IsNullOrWhiteSpace(tenantId))
{
tags.Add(new ("tenant_id", tenantId));
}
if (!string.IsNullOrWhiteSpace(orderType))
{
tags.Add(new ("order_type", orderType));
}
return tags.ToArray();
}
}
}
Registering Metrics Classes¶
Step 1: Add to ApplicationModel
// MicroserviceMetricsExtensions.cs
internal static IServiceCollection AddMicroserviceMetrics(this IServiceCollection services)
{
services.AddSingleton<OrderMetrics>();
services.ActivateSingleton<OrderMetrics>();
return services;
}
Step 2: Register Meter in OpenTelemetry
DurationScope Helper¶
The DurationScope class provides automatic timing for operations:
Purpose: Automatically records elapsed time when a scope is disposed.
Usage:
// Automatic timing on dispose
using (metrics.TimeOrderCreation(tenantId: "tenant-1", orderType: "standard"))
{
// Operation automatically timed
await CreateOrder(order);
}
Implementation:
public sealed class DurationScope : IDisposable
{
private readonly Histogram<double> histogram;
private readonly KeyValuePair<string, object?>[] tags;
private readonly Stopwatch stopwatch;
private DurationScope(Histogram<double> histogram, KeyValuePair<string, object?>[] tags)
{
this.histogram = histogram;
this.tags = tags;
this.stopwatch = Stopwatch.StartNew();
}
public static DurationScope Seconds(
Histogram<double> histogram,
params KeyValuePair<string, object?>[] tags)
{
return new DurationScope(histogram, tags);
}
public void Dispose()
{
this.stopwatch.Stop();
this.histogram.Record(this.stopwatch.Elapsed.TotalSeconds, this.tags);
}
}
Benefits: - Automatic timing on scope exit - Exception-safe (always records duration) - Consistent timing across codebase - Cleaner code (no manual stopwatch management)
Metrics Class Patterns¶
Pattern 1: CRUD Operations (MicroserviceTemplateMetrics)¶
Structure: Separate counters for Create, Read, Update, Delete operations with duration histograms and failure counters.
public class MicroserviceTemplateMetrics
{
// Counters
private readonly Counter<long> added;
private readonly Counter<long> deleted;
private readonly Counter<long> updated;
private readonly Counter<long> read;
// Duration histograms
private readonly Histogram<double> addDurationSeconds;
private readonly Histogram<double> deleteDurationSeconds;
// Failure counters
private readonly Counter<long> addFailed;
// Methods
public void AddMicroserviceAggregateRoot(TimeSpan duration, ...);
public void AddMicroserviceAggregateRootFailed(...);
public IDisposable TimeAdd(...);
}
Use Case: Aggregate root operations with comprehensive CRUD tracking.
Pattern 2: Feature Operations (FeatureAMetrics)¶
Structure: Success/failure counters with duration histogram and instance tracking.
public class FeatureAMetrics
{
// Success/failure counters
private readonly Counter<long> successes;
private readonly Counter<long> failures;
// Duration histogram
private readonly Histogram<double> durationSeconds;
// Instance tracking
private readonly UpDownCounter<long> instances;
// Methods
public void RecordFeatureAUseCaseASuccess(TimeSpan? duration, ...);
public void RecordFeatureAUseCaseAFailed(TimeSpan? duration, ...);
public IDisposable TimeUseCaseASuccess(...);
public IDisposable TimeUseCaseAFailure(...);
}
Use Case: Feature-specific use cases with success/failure tracking.
Pattern 3: Actor Operations (BankAccountActorMetrics)¶
Structure: Actor-specific metrics with business context (amounts, currency).
public class BankAccountActorMetrics
{
// Operation counters
private readonly Counter<long> withdrawSuccesses;
private readonly Counter<long> withdrawFailures;
// Duration and amount histograms
private readonly Histogram<double> withdrawDurationSeconds;
private readonly Histogram<double> withdrawAmount;
// Instance tracking
private readonly UpDownCounter<long> instances;
// Methods
public void RecordWithdrawSuccess(double amount, TimeSpan? duration, ...);
public void RecordWithdrawFailure(double? amount, TimeSpan? duration, ...);
}
Use Case: Actor operations with business metrics (amounts, currencies).
Tag Building Patterns¶
Standard Tags¶
All metrics classes include standard tags:
private static KeyValuePair<string, object?>[] BuildTags(...)
{
var tags = new List<KeyValuePair<string, object?>>(4)
{
new ("module", ModuleAttribute), // Always included
new ("component", ComponentAttribute), // Always included
};
// Optional tags
if (!string.IsNullOrWhiteSpace(tenantId))
{
tags.Add(new ("tenant_id", tenantId));
}
// ... other optional tags
return tags.ToArray();
}
Tag Best Practices¶
-
Always Include Module and Component
-
Use Low-Cardinality Values
-
Conditional Tags
-
Consistent Tag Names
Meter Naming Conventions¶
Meter Names:
- Format: connectsoft.microservicetemplate.{component}
- Example: connectsoft.microservicetemplate.order
- Used for: Grouping related metrics
Metric Names:
- Format: connectsoft.microservicetemplate.{component}.{operation}.{type}
- Examples:
- connectsoft.microservicetemplate.order.created
- connectsoft.microservicetemplate.order.creation.duration
- connectsoft.microservicetemplate.order.created.failed
Constants:
- Meter name: {Class}MeterName
- Metric names: {Metric}{Type}Name
- Example: OrderMetricsMeterName, OrdersCreatedCounterName
Using Metrics in Application Code¶
Injection:
public class OrderProcessor
{
private readonly OrderMetrics metrics;
public OrderProcessor(OrderMetrics metrics)
{
this.metrics = metrics;
}
}
Manual Timing:
public async Task<Order> CreateOrder(CreateOrderInput input)
{
var stopwatch = Stopwatch.StartNew();
try
{
var order = await SaveOrder(input);
this.metrics.RecordOrderCreated(
duration: stopwatch.Elapsed,
tenantId: input.TenantId,
orderType: input.OrderType);
return order;
}
catch (Exception ex)
{
// Record failure
this.metrics.RecordOrderCreatedFailed(
tenantId: input.TenantId,
orderType: input.OrderType);
throw;
}
}
Automatic Timing:
public async Task<Order> CreateOrder(CreateOrderInput input)
{
using (this.metrics.TimeOrderCreation(
tenantId: input.TenantId,
orderType: input.OrderType))
{
var order = await SaveOrder(input);
this.metrics.RecordOrderCreated(
tenantId: input.TenantId,
orderType: input.OrderType);
return order;
}
}
Testing Metrics Classes¶
Unit Testing:
[TestMethod]
public void OrderMetrics_ShouldRecordOrderCreated()
{
// Arrange
var meterFactory = new TestMeterFactory();
var metrics = new OrderMetrics(meterFactory);
// Act
metrics.RecordOrderCreated(
duration: TimeSpan.FromSeconds(1),
tenantId: "tenant-1",
orderType: "standard");
// Assert
var meter = meterFactory.GetMeter(OrderMetrics.OrderMetricsMeterName);
// Verify metric was recorded
}
Mocking for Application Tests:
[TestMethod]
public void OrderProcessor_ShouldRecordMetrics_OnCreate()
{
// Arrange
var mockMetrics = new Mock<OrderMetrics>();
var processor = new OrderProcessor(mockMetrics.Object);
// Act
await processor.CreateOrder(input);
// Assert
mockMetrics.Verify(m => m.RecordOrderCreated(
It.IsAny<TimeSpan>(),
It.IsAny<string>(),
It.IsAny<string>()),
Times.Once);
}
Metrics Project Best Practices¶
Do's¶
-
Follow Consistent Structure
// ✅ GOOD - Consistent pattern public class OrderMetrics { public const string OrderMetricsMeterName = "..."; private readonly Counter<long> ordersCreated; public OrderMetrics(IMeterFactory meterFactory) { ... } public void RecordOrderCreated(...) { ... } private static KeyValuePair<string, object?>[] BuildTags(...) { ... } } -
Use DurationScope for Timing
-
Include Standard Tags
-
Document Metric Names
Don'ts¶
-
Don't Create Metrics in Hot Paths
-
Don't Use High-Cardinality Tags
-
Don't Mix Business and Technical Metrics
Custom Metrics Implementation¶
MicroserviceTemplateMetrics¶
Purpose: Business metrics for aggregate root operations.
Metrics Provided:
| Metric Type | Name | Description |
|---|---|---|
| Counter | aggregateroot.added |
Count of aggregate roots created |
| Counter | aggregateroot.deleted |
Count of aggregate roots deleted |
| Counter | aggregateroot.updated |
Count of aggregate roots updated |
| Counter | aggregateroot.read |
Count of aggregate roots read |
| UpDownCounter | aggregateroot.total |
Current number of aggregate roots |
| Histogram | aggregateroot.add.duration |
Duration of add operations |
| Histogram | aggregateroot.delete.duration |
Duration of delete operations |
| Histogram | aggregateroot.update.duration |
Duration of update operations |
| Histogram | aggregateroot.read.duration |
Duration of read operations |
| Counter | aggregateroot.add.failed |
Count of failed add operations |
| Counter | aggregateroot.delete.failed |
Count of failed delete operations |
| Counter | aggregateroot.update.failed |
Count of failed update operations |
| Counter | aggregateroot.read.failed |
Count of failed read operations |
Usage in Processor:
public class DefaultMicroserviceAggregateRootsProcessor
{
private readonly MicroserviceTemplateMetrics meters;
public async Task<IMicroserviceAggregateRoot> CreateMicroserviceAggregateRoot(
CreateMicroserviceAggregateRootInput input,
CancellationToken token = default)
{
var stopwatch = Stopwatch.StartNew();
try
{
// Business logic
var entity = await SaveNewEntity(input, token);
// Record metrics
this.meters.AddMicroserviceAggregateRoot(
stopwatch.Elapsed,
tenantId: tenantId,
aggregate: aggregateName);
this.meters.IncreaseTotalMicroserviceAggregateRoots(
tenantId: tenantId,
aggregate: aggregateName);
return entity;
}
catch (Exception ex)
{
// Record failure
this.meters.AddMicroserviceAggregateRootFailed(
tenantId: tenantId,
aggregate: aggregateName);
throw;
}
}
}
FeatureAMetrics¶
Purpose: Feature-specific metrics for UseCaseA operations.
Metrics Provided:
| Metric Type | Name | Description |
|---|---|---|
| Counter | featurea.usecasea.successes |
Count of successful executions |
| Counter | featurea.usecasea.failures |
Count of failed executions |
| Histogram | featurea.usecasea.duration |
Duration of executions |
| UpDownCounter | featurea.instances |
Current number of FeatureA instances |
Usage:
public class FeatureAService
{
private readonly FeatureAMetrics metrics;
public async Task ExecuteUseCaseA(UseCaseAInput input)
{
var stopwatch = Stopwatch.StartNew();
try
{
// Business logic
await ProcessUseCaseA(input);
// Record success with duration
this.metrics.RecordFeatureAUseCaseASuccess(
duration: stopwatch.Elapsed,
tenantId: input.TenantId,
reason: "completed");
}
catch (Exception ex)
{
// Record failure with duration
this.metrics.RecordFeatureAUseCaseAFailed(
duration: stopwatch.Elapsed,
tenantId: input.TenantId,
reason: "exception");
throw;
}
}
}
Timing Scopes¶
DurationScope Pattern:
For automatic duration recording, use timing scopes:
public class MicroserviceTemplateMetrics
{
public IDisposable TimeAdd(string? tenantId = null, string? aggregate = null)
{
return DurationScope.Seconds(
this.addDurationSeconds,
BuildTags(tenantId, aggregate));
}
}
Usage:
// Automatic timing on dispose
using (metrics.TimeAdd(tenantId: "tenant-1", aggregate: "Order"))
{
// Operation automatically timed
await CreateAggregate(input);
}
Automatic Instrumentation¶
Runtime Instrumentation¶
.NET Runtime Metrics:
Collected Metrics: - GC collections (gen0, gen1, gen2) - GC pause time - Thread pool queue length - Thread pool threads - Exception count - Contention count - Timer resolution
ASP.NET Core Instrumentation¶
HTTP Request Metrics:
Collected Metrics: - Request duration - Request count - Active requests - Response status codes
Process Instrumentation¶
Process Metrics:
Collected Metrics: - CPU usage - Memory usage (working set, private bytes) - Virtual memory - Thread count - Handle count
HttpClient Instrumentation¶
Outgoing HTTP Metrics:
Collected Metrics: - HTTP request duration - HTTP request count - HTTP response status codes - HTTP connection pool metrics
Tags and Dimensions¶
Tag Structure¶
Tags provide dimensions for metrics aggregation:
private static KeyValuePair<string, object?>[] BuildTags(
string? tenantId,
string? aggregate)
{
var tags = new List<KeyValuePair<string, object?>>(4)
{
new ("module", "ConnectSoft.MicroserviceTemplate"),
new ("component", "AggregateRoot"),
};
if (!string.IsNullOrWhiteSpace(tenantId))
{
tags.Add(new ("tenant_id", tenantId));
}
if (!string.IsNullOrWhiteSpace(aggregate))
{
tags.Add(new ("aggregate", aggregate));
}
return tags.ToArray();
}
Tag Best Practices¶
-
Use Low-Cardinality Tags
-
Standard Tag Names
-
Include Context Tags
Metric Naming Conventions¶
OpenTelemetry Semantic Conventions¶
Metric Name Format:
Examples:
connectsoft.microservicetemplate.aggregateroot.added
connectsoft.microservicetemplate.aggregateroot.add.duration
connectsoft.microservicetemplate.featurea.usecasea.successes
Naming Best Practices¶
-
Use Dot Notation
-
Use Lowercase
-
Include Units in Name
Metric Exporters¶
OTLP Exporter¶
OpenTelemetry Protocol Exporter:
metricsBuilder.AddOtlpExporter(options =>
{
options.Protocol = OtlpExportProtocol.Grpc;
options.Endpoint = new Uri("https://otel-collector:4317");
});
Supported Backends: - OpenTelemetry Collector - Jaeger - Zipkin - Application Insights (via collector) - Prometheus (via collector)
Console Exporter¶
Development Exporter:
Output: Metrics printed to console (useful for debugging).
Prometheus Exporter¶
Prometheus Scraping Endpoint:
// Uncomment to enable Prometheus exporter
// metricsBuilder.AddPrometheusExporter();
// In middleware
// application.MapPrometheusScrapingEndpoint();
Access: GET /metrics endpoint for Prometheus scraping.
Querying Metrics¶
Prometheus Queries¶
Rate Calculation:
# Requests per second
rate(connectsoft_microservicetemplate_aggregateroot_added_total[5m])
# Error rate
rate(connectsoft_microservicetemplate_aggregateroot_add_failed_total[5m])
Percentiles:
# p95 latency
histogram_quantile(0.95,
rate(connectsoft_microservicetemplate_aggregateroot_add_duration_bucket[5m]))
# p99 latency
histogram_quantile(0.99,
rate(connectsoft_microservicetemplate_aggregateroot_add_duration_bucket[5m]))
Filtering by Tags:
# By tenant
connectsoft_microservicetemplate_aggregateroot_added_total{tenant_id="tenant-1"}
# By aggregate
connectsoft_microservicetemplate_aggregateroot_added_total{aggregate="Order"}
Application Insights Queries¶
Kusto Queries:
// Custom metrics
customMetrics
| where name == "connectsoft.microservicetemplate.aggregateroot.added"
| summarize count() by bin(timestamp, 1m)
// Percentiles
customMetrics
| where name == "connectsoft.microservicetemplate.aggregateroot.add.duration"
| summarize percentiles(value, 50, 95, 99) by bin(timestamp, 1m)
Testing Metrics¶
Unit Testing¶
Mock Metrics:
[TestMethod]
public void Processor_ShouldRecordMetrics_OnCreate()
{
// Arrange
var mockMetrics = new Mock<MicroserviceTemplateMetrics>();
var processor = new DefaultMicroserviceAggregateRootsProcessor(
mockMetrics.Object,
/* other dependencies */);
// Act
await processor.CreateMicroserviceAggregateRoot(input);
// Assert
mockMetrics.Verify(m => m.AddMicroserviceAggregateRoot(
It.IsAny<TimeSpan>(),
It.IsAny<string>(),
It.IsAny<string>()),
Times.Once);
}
Integration Testing¶
Verify Metrics Collection:
[TestMethod]
public async Task Metrics_ShouldBeExported_ToPrometheus()
{
// Arrange
var client = factory.CreateClient();
// Act
await client.PostAsync("/api/aggregates", content);
var metricsResponse = await client.GetAsync("/metrics");
var metricsText = await metricsResponse.Content.ReadAsStringAsync();
// Assert
Assert.IsTrue(metricsText.Contains("aggregateroot_added_total"));
}
Best Practices¶
Do's¶
-
Use Appropriate Metric Types
-
Include Context in Tags
-
Use Timing Scopes for Duration
-
Record Metrics for Both Success and Failure
-
Use Low-Cardinality Tags
Don'ts¶
-
Don't Use High-Cardinality Tags
-
Don't Create Metrics in Hot Paths
-
Don't Mix Business and Technical Metrics
-
Don't Record Metrics Synchronously in Critical Paths
Troubleshooting¶
Issue: Metrics Not Appearing¶
Symptoms: Metrics not showing in observability backend.
Solutions:
1. Verify meter is registered: AddMeter("MeterName")
2. Check exporter configuration
3. Verify metrics class is registered in DI: AddSingleton<MetricsClass>()
4. Check network connectivity to exporter endpoint
5. Enable console exporter for debugging
Issue: High Cardinality¶
Symptoms: Too many unique metric series.
Solutions: 1. Review tag values for high cardinality 2. Remove or aggregate high-cardinality tags 3. Use sampling for high-volume metrics 4. Consider using logs for high-cardinality data
Issue: Missing Metrics¶
Symptoms: Some metrics not collected.
Solutions:
1. Verify meter name matches in AddMeter()
2. Check metrics class is activated: ActivateSingleton<MetricsClass>()
3. Verify metric names match exactly
4. Check metric is being called in code path
Summary¶
Metrics in the ConnectSoft Microservice Template provide:
- ✅ Multiple Metric Types: Counter, Histogram, UpDownCounter, ObservableGauge
- ✅ Automatic Instrumentation: Runtime, ASP.NET Core, HttpClient, Process
- ✅ Custom Business Metrics: Domain-specific metrics for business operations
- ✅ OpenTelemetry Integration: Vendor-neutral metrics collection
- ✅ Multiple Exporters: OTLP, Prometheus, Console, Application Insights
- ✅ Tag Support: Contextual dimensions for metric aggregation
- ✅ Timing Scopes: Automatic duration recording
- ✅ Testing Support: Mockable metrics for unit testing
- ✅ Best Practices: Low-cardinality tags, appropriate metric types
By following these patterns, teams can:
- Monitor Performance: Track latency, throughput, and resource usage
- Measure Business Operations: Track domain-specific metrics
- Alert on Issues: Set up alerts based on metric thresholds
- Plan Capacity: Use metrics for capacity planning and optimization
- Debug Issues: Use metrics to identify performance problems
- Track SLOs: Monitor service-level objectives with metrics
Metrics are a critical component of observability, providing quantitative measurements that enable teams to understand system behavior, track business outcomes, and ensure reliable service delivery.