Skip to content

OpenTelemetry

OpenTelemetry provides distributed tracing and metrics collection for all Microtec ERP backend services. It instruments HTTP requests, database queries, message bus operations, and custom business metrics using the OTLP (OpenTelemetry Protocol) exporter.


OTLP Endpoints

ProtocolPortFormatUsed By
gRPC4317Protobuf.NET services (default)
HTTP/Protobuf4318Protobuf over HTTPServices behind HTTP-only proxies

Local OpenTelemetry Collector receives on both ports and routes signals to Jaeger (traces), Prometheus (metrics), and Application Insights (cloud).


Configuration via appsettings.json

json
{
  "OpenTelemetry": {
    "EnableTracing": true,
    "EnableMetrics": true,
    "ServiceName": "accounting-api",
    "ServiceVersion": "1.0.0",
    "OtlpEndpoint": "http://localhost:4317",
    "OtlpProtocol": "grpc",
    "SamplingRatio": 1.0
  }
}

For production, SamplingRatio is set to 0.1 (10% sampling) to manage volume and cost.


Service Registration (Program.cs)

csharp
// In Shared.Web — OpenTelemetryExtensions.cs
public static IServiceCollection AddMicrotecOpenTelemetry(
    this IServiceCollection services,
    IConfiguration configuration)
{
    var settings = configuration.GetSection("OpenTelemetry").Get<OpenTelemetrySettings>();

    services.AddOpenTelemetry()
        .ConfigureResource(resource => resource
            .AddService(
                serviceName: settings.ServiceName,
                serviceVersion: settings.ServiceVersion)
            .AddAttributes(new Dictionary<string, object>
            {
                ["deployment.environment"] = configuration["ASPNETCORE_ENVIRONMENT"] ?? "unknown",
                ["service.instance.id"] = Environment.MachineName
            }))
        .WithTracing(tracing =>
        {
            if (!settings.EnableTracing) return;

            tracing
                .AddAspNetCoreInstrumentation(opts =>
                {
                    opts.RecordException = true;
                    opts.Filter = ctx =>
                        !ctx.Request.Path.StartsWithSegments("/health") &&
                        !ctx.Request.Path.StartsWithSegments("/metrics");
                })
                .AddHttpClientInstrumentation(opts =>
                {
                    opts.RecordException = true;
                })
                .AddEntityFrameworkCoreInstrumentation(opts =>
                {
                    // Only include SQL text in non-production environments
                    opts.SetDbStatementForText =
                        configuration["ASPNETCORE_ENVIRONMENT"] != "production";
                })
                .AddRedisInstrumentation()
                .AddAzureServiceBusInstrumentation()
                .SetSampler(new TraceIdRatioBasedSampler(settings.SamplingRatio))
                .AddOtlpExporter(opts =>
                {
                    opts.Endpoint = new Uri(settings.OtlpEndpoint);
                    opts.Protocol = settings.OtlpProtocol == "grpc"
                        ? OtlpExportProtocol.Grpc
                        : OtlpExportProtocol.HttpProtobuf;
                });

            // Cloud: also export to Application Insights
            if (IsCloudEnvironment(configuration))
            {
                tracing.AddAzureMonitorTraceExporter(opts =>
                {
                    opts.ConnectionString = configuration["ApplicationInsights:ConnectionString"];
                });
            }
        })
        .WithMetrics(metrics =>
        {
            if (!settings.EnableMetrics) return;

            metrics
                .AddAspNetCoreInstrumentation()
                .AddHttpClientInstrumentation()
                .AddRuntimeInstrumentation()
                .AddProcessInstrumentation()
                .AddOtlpExporter(opts =>
                {
                    opts.Endpoint = new Uri(settings.OtlpEndpoint);
                });

            if (IsCloudEnvironment(configuration))
            {
                metrics.AddAzureMonitorMetricExporter(opts =>
                {
                    opts.ConnectionString = configuration["ApplicationInsights:ConnectionString"];
                });
            }
        });

    return services;
}

Traces

What Is Traced

InstrumentationActivity NameKey Attributes
ASP.NET CoreMicrosoft.AspNetCore.Hosting.HttpRequestInhttp.method, http.url, http.status_code
HttpClientSystem.Net.Http.HttpRequestOuthttp.url, http.method, http.status_code
EF CoreMicrosoft.EntityFrameworkCore.Database.Commanddb.statement (dev only), db.name
RedisStackExchange.Redisdb.statement, net.peer.name
Service BusAzure.Messaging.ServiceBusmessaging.destination, messaging.operation

Trace Propagation Headers

All services read and write W3C Trace Context headers:

traceparent: 00-{traceId}-{spanId}-{flags}
tracestate:  (vendor-specific, may be empty)

Gateway.API generates the root traceparent for incoming external requests. Downstream services propagate it via HttpClient automatically (handled by AddHttpClientInstrumentation).

Viewing Traces Locally

Open Jaeger at http://localhost:16686:

  1. Select a service from the dropdown (e.g., accounting-api)
  2. Set time range and click Find Traces
  3. Click any trace to see the full span waterfall

Metrics

Default Metrics (Auto-Instrumented)

Metric NameTypeDescription
http.server.request.durationHistogramLatency of handled HTTP requests (ms)
http.server.active_requestsUpDownCounterIn-flight request count
http.client.request.durationHistogramLatency of outbound HTTP calls
db.client.operation.durationHistogramDatabase query latency
process.cpu.timeCounterTotal CPU time used
process.memory.usageObservableGaugeResident set size
dotnet.gc.collectionsCounterGC collection count by generation

Custom Business Metrics

Custom metrics are registered via IMeterFactory:

csharp
// In a service or handler
public class TenantMetrics
{
    private readonly Counter<long> _requestCounter;
    private readonly Histogram<double> _processingDuration;

    public TenantMetrics(IMeterFactory meterFactory)
    {
        var meter = meterFactory.Create("Microtec.Accounting");

        _requestCounter = meter.CreateCounter<long>(
            "microtec.accounting.requests",
            unit: "{request}",
            description: "Total accounting requests processed");

        _processingDuration = meter.CreateHistogram<double>(
            "microtec.accounting.processing_duration",
            unit: "ms",
            description: "Time to process accounting transactions");
    }

    public void RecordRequest(string tenantId, string operation)
    {
        _requestCounter.Add(1,
            new KeyValuePair<string, object?>("tenant.id", tenantId),
            new KeyValuePair<string, object?>("operation", operation));
    }
}

Viewing Metrics Locally

  • Prometheus: http://localhost:9090 — query using PromQL
  • Grafana: http://localhost:3000 — pre-built dashboards for all services

Example PromQL queries:

promql
# Request rate per service
rate(http_server_request_duration_seconds_count[5m])

# P95 latency
histogram_quantile(0.95, rate(http_server_request_duration_seconds_bucket[5m]))

# Error rate
rate(http_server_request_duration_seconds_count{http_status_code=~"5.."}[5m])
  /
rate(http_server_request_duration_seconds_count[5m])

OpenTelemetry Collector Configuration

The OTEL Collector in dev/ routes signals to local backends:

yaml
# dev/otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 5s
    send_batch_size: 512

exporters:
  jaeger:
    endpoint: jaeger:14250
    tls:
      insecure: true

  prometheus:
    endpoint: 0.0.0.0:8889
    namespace: microtec

  logging:
    loglevel: warn

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [jaeger, logging]

    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus, logging]

Production Configuration

In cloud environments, the OTLP Collector is not deployed. Services export directly to Azure Monitor:

json
{
  "OpenTelemetry": {
    "EnableTracing": true,
    "EnableMetrics": true,
    "OtlpEndpoint": "",
    "SamplingRatio": 0.1
  },
  "ApplicationInsights": {
    "ConnectionString": "keyvaultref:https://mic-erp-be-production-kv.vault.azure.net/secrets/ApplicationInsights--ConnectionString"
  }
}

The IsCloudEnvironment() check in the registration code enables the Azure Monitor exporter when ASPNETCORE_ENVIRONMENT is not Development.


Disabling Specific Instrumentation

To reduce noise or avoid sensitive data in traces, individual instrumentations can be disabled:

json
{
  "OpenTelemetry": {
    "EnableTracing": true,
    "DisableInstrumentation": ["EntityFrameworkCore", "Redis"]
  }
}

Note: Disabling EF Core instrumentation in production is recommended to avoid logging SQL queries that may contain sensitive data, even without the db.statement attribute.


Internal Documentation — Microtec Platform Team