Add OpenTelemetry, performance metrics, and broaden logging + tests

Observability:
- New FuchsTelemetry (ActivitySource + Meter) defining business counters
  (invoices/reminders/reports rendered, emails/sms sent+failed, MT940 rows,
  MFR calls) and duration histograms (PDF render, report render, email send).
- Program.cs wires OpenTelemetry tracing (ASP.NET Core, HttpClient, SqlClient,
  app source) and metrics (ASP.NET Core, HttpClient, runtime, app meter).
  OTLP export is enabled only when Fuchs:Telemetry:OtlpEndpoint is set, so a
  missing collector never affects the app; disable via Fuchs:Telemetry:Enabled.

Instrumentation + logging:
- Services (Pdf, Invoice, Reminder, Report, Com, Banking, Widget, MfrFactory)
  now emit spans, record metrics, and log entry/result/timing/errors.
- Added dispatch + key-action logging to the previously silent handlers
  (Banking, Reminder, Reports, Requests).

Tests (137 total, +10):
- ProcessWebComServiceTests with a stub HttpMessageHandler cover success
  (API 200), failure (API 500, invalid email, empty mobile), disabled mode,
  the base64 attachment payload contract, and metric emission via MeterListener.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 14:02:13 +02:00
parent 8dee630abb
commit e04d590c3a
17 changed files with 473 additions and 16 deletions
+56
View File
@@ -0,0 +1,56 @@
using System.Diagnostics;
using System.Diagnostics.Metrics;
namespace Fuchs.Observability;
/// <summary>
/// Central definition of the application's OpenTelemetry instrumentation:
/// a single <see cref="ActivitySource"/> for tracing and a single
/// <see cref="Meter"/> with the business / performance instruments.
///
/// These use the in-box <c>System.Diagnostics</c> APIs, which the OpenTelemetry
/// SDK (wired up in <c>Program.cs</c>) collects and exports. Code can therefore
/// emit spans and metrics without taking a direct dependency on the OTel SDK.
/// </summary>
public static class FuchsTelemetry
{
public const string ServiceName = "Fuchs.Intranet";
public static readonly string ServiceVersion =
typeof(FuchsTelemetry).Assembly.GetName().Version?.ToString() ?? "1.0.0";
/// <summary>Tracing source — register this name with the tracer provider.</summary>
public static readonly ActivitySource ActivitySource = new(ServiceName, ServiceVersion);
/// <summary>Metrics meter — register this name with the meter provider.</summary>
public static readonly Meter Meter = new(ServiceName, ServiceVersion);
// ── Business counters ────────────────────────────────────────────────────
public static readonly Counter<long> InvoicesRendered =
Meter.CreateCounter<long>("fuchs.invoices.rendered", "{invoice}", "Number of invoice PDFs rendered.");
public static readonly Counter<long> RemindersRendered =
Meter.CreateCounter<long>("fuchs.reminders.rendered", "{reminder}", "Number of reminder PDFs rendered.");
public static readonly Counter<long> ReportsRendered =
Meter.CreateCounter<long>("fuchs.reports.rendered", "{report}", "Number of reports rendered (by function).");
public static readonly Counter<long> EmailsSent =
Meter.CreateCounter<long>("fuchs.emails.sent", "{email}", "Number of emails accepted by the mailer API.");
public static readonly Counter<long> EmailsFailed =
Meter.CreateCounter<long>("fuchs.emails.failed", "{email}", "Number of emails that failed to send.");
public static readonly Counter<long> SmsSent =
Meter.CreateCounter<long>("fuchs.sms.sent", "{sms}", "Number of SMS messages sent.");
public static readonly Counter<long> Mt940RowsParsed =
Meter.CreateCounter<long>("fuchs.banking.mt940.rows", "{row}", "Number of MT940 transaction lines parsed.");
public static readonly Counter<long> MfrCalls =
Meter.CreateCounter<long>("fuchs.mfr.calls", "{call}", "Number of MFR ERP client calls initiated.");
// ── Performance histograms (durations in milliseconds) ───────────────────
public static readonly Histogram<double> PdfRenderDuration =
Meter.CreateHistogram<double>("fuchs.pdf.render.duration", "ms", "PDF render duration.");
public static readonly Histogram<double> ReportRenderDuration =
Meter.CreateHistogram<double>("fuchs.report.render.duration", "ms", "Report render duration.");
public static readonly Histogram<double> EmailSendDuration =
Meter.CreateHistogram<double>("fuchs.email.send.duration", "ms", "Email send round-trip duration.");
/// <summary>Starts a span on the application's <see cref="ActivitySource"/>.</summary>
public static Activity? StartActivity(string name, ActivityKind kind = ActivityKind.Internal) =>
ActivitySource.StartActivity(name, kind);
}