Observability

June 10, 2026 · View on GitHub

DataSurface integrates with standard .NET observability infrastructure: structured logging via ILogger, OpenTelemetry metrics, distributed tracing, health checks, and audit logging.


Structured Logging

Both EfDataSurfaceCrudService and DynamicDataSurfaceCrudService emit structured logs for every operation:

[DBG] List User page=1 pageSize=20
[DBG] List User completed in 45ms, returned 20/142 items
[INF] Created User in 12ms
[INF] Updated User id=5 in 8ms
[INF] Deleted User id=5 in 3ms

Log Levels

LevelOperations
DebugOperation start, read completions (List, Get)
InformationMutating operations (Create, Update, Delete)

Structured Properties

All log entries include structured properties for filtering and querying:

PropertyDescription
{Resource}Resource key (e.g., "User")
{Id}Entity ID (when applicable)
{ElapsedMs}Operation duration in milliseconds
{Count} / {Total}List result counts

These properties work with any structured logging sink (Serilog, Seq, Application Insights, etc.).


Metrics

OpenTelemetry-compatible metrics via DataSurfaceMetrics:

Setup

builder.Services.AddSingleton<DataSurfaceMetrics>();

builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics.AddMeter("DataSurface"));

Available Metrics

MetricTypeDescription
datasurface.operationsCounterTotal CRUD operations by resource and operation
datasurface.errorsCounterFailed operations by resource, operation, and error type
datasurface.operation.durationHistogramOperation duration in milliseconds
datasurface.rows_affectedCounterRows affected by operations

Tags/Labels

Each metric includes tags for filtering:

  • resource — Resource key
  • operation — CRUD operation name
  • error_type — Error classification (for error counter)

Feature Flag

opt.Features.EnableMetrics = true;  // default: true in Standard/Full

When EnableMetrics is false, the CRUD and streaming services do not record metrics even if DataSurfaceMetrics is registered.


Distributed Tracing

Activity/span integration via DataSurfaceTracing:

Setup

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing.AddSource("DataSurface"));

Trace Attributes

Each CRUD operation creates a span with these attributes:

AttributeDescription
datasurface.resourceResource key
datasurface.operationCRUD operation
datasurface.entity_idEntity ID (when applicable)
datasurface.rows_affectedRows returned or affected
datasurface.query.pagePage number (List)
datasurface.query.page_sizePage size (List)
datasurface.query.filter_countNumber of active filters (List)
datasurface.query.sort_countNumber of sort fields (List)

Traces integrate with any OpenTelemetry-compatible backend (Jaeger, Zipkin, Azure Monitor, etc.).

Feature Flag

opt.Features.EnableTracing = true;  // default: true in Standard/Full

When EnableTracing is false, the CRUD and streaming services do not start spans even if a listener is subscribed to the DataSurface activity source.


Health Checks

Built-in IHealthCheck implementations for monitoring:

Setup

builder.Services.AddHealthChecks()
    .AddCheck<DataSurfaceDbHealthCheck>("datasurface-db")
    .AddCheck<DataSurfaceContractsHealthCheck>("datasurface-contracts")
    .AddCheck<DynamicMetadataHealthCheck>("datasurface-dynamic-metadata")
    .AddCheck<DynamicContractsHealthCheck>("datasurface-dynamic-contracts");

Available Health Checks

CheckDescription
DataSurfaceDbHealthCheckDatabase connectivity — verifies the DbContext can connect
DataSurfaceContractsHealthCheckStatic contracts loaded — verifies at least one contract is registered
DynamicMetadataHealthCheckDynamic entity definitions table accessible
DynamicContractsHealthCheckDynamic contracts loaded from metadata

Use the standard ASP.NET Core health check endpoint:

app.MapHealthChecks("/health");

Audit Logging

Track all CRUD operations with full before/after state using IAuditLogger:

Setup

using DataSurface.EFCore.Interfaces;

public class DatabaseAuditLogger : IAuditLogger
{
    private readonly AppDbContext _db;
    private readonly IHttpContextAccessor _http;

    public DatabaseAuditLogger(AppDbContext db, IHttpContextAccessor http)
    {
        _db = db;
        _http = http;
    }

    public async Task LogAsync(AuditLogEntry entry, CancellationToken ct)
    {
        _db.AuditLogs.Add(new AuditLog
        {
            UserId = _http.HttpContext?.User.FindFirst("sub")?.Value,
            Operation = entry.Operation.ToString(),
            ResourceKey = entry.ResourceKey,
            EntityId = entry.EntityId,
            Timestamp = entry.Timestamp,
            Success = entry.Success,
            Changes = entry.Changes?.ToJsonString(),
            PreviousValues = entry.PreviousValues?.ToJsonString()
        });
        await _db.SaveChangesAsync(ct);
    }
}

builder.Services.AddScoped<IAuditLogger, DatabaseAuditLogger>();

AuditLogEntry Properties

PropertyTypeDescription
OperationCrudOperationThe operation performed
ResourceKeystringResource that was accessed
EntityIdstring?Entity ID (if applicable)
TimestampDateTimeUTC timestamp
UserIdstring?User who performed the operation
IpAddressstring?Client IP address
SuccessboolWhether the operation succeeded
ErrorMessagestring?Error message when the operation failed
ChangesJsonObject?Fields written (create/update)
PreviousValuesJsonObject?Previous field values (update)

Audit entries are written for cache hits too — a read served from the query cache is still audited. Inside bulk transactions, audit entries (and webhooks) are buffered and emitted only after a successful commit; a rollback discards them.

Feature Flag

opt.Features.EnableAuditLogging = true;  // default: true in Standard/Full

Observability Stack Example

A complete observability setup combining all features:

// Metrics + Tracing
builder.Services.AddSingleton<DataSurfaceMetrics>();
builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics.AddMeter("DataSurface"))
    .WithTracing(tracing => tracing.AddSource("DataSurface"));

// Audit logging
builder.Services.AddScoped<IAuditLogger, DatabaseAuditLogger>();

// Health checks
builder.Services.AddHealthChecks()
    .AddCheck<DataSurfaceDbHealthCheck>("datasurface-db")
    .AddCheck<DataSurfaceContractsHealthCheck>("datasurface-contracts");

// Feature flags — all observability enabled
opt.Features = new DataSurfaceFeatures
{
    EnableAuditLogging = true,
    EnableMetrics = true,
    EnableTracing = true
};