From a72090534d3cb6cc72ecdf7876c138d861d703fd Mon Sep 17 00:00:00 2001 From: Panu Oksala Date: Mon, 9 Mar 2026 19:25:28 +0200 Subject: [PATCH] Add OTLP exporter support alongside Azure Monitor Botello can now export build telemetry to any OTLP-compatible backend (Jaeger, Grafana Tempo, Seq, Aspire Dashboard, OTel Collector, etc.) in addition to Azure Application Insights. A new 'Exporter' config option selects the backend (AzureMonitor or Otlp), with OTLP-specific settings for endpoint, protocol (gRPC/HTTP), headers, and timeout. All configurable via appsettings.json, env vars, or CLI params. --- Botello.csproj | 5 +- Config/ConfigurationLoader.cs | 37 ++++++- Config/LoggerConfig.cs | 87 +++++++++++++-- README.md | 182 ++++++++++++++++++++++++++----- Telemetry/OtelPipelineManager.cs | 58 ++++++++-- appsettings.json | 30 ++++- 6 files changed, 350 insertions(+), 49 deletions(-) diff --git a/Botello.csproj b/Botello.csproj index 3bec3ae..9f00ed2 100644 --- a/Botello.csproj +++ b/Botello.csproj @@ -14,7 +14,7 @@ true - MSBuild custom logger that forwards build events to Azure Application Insights via OpenTelemetry. + MSBuild custom logger that forwards build events to Azure Application Insights or any OTLP-compatible backend via OpenTelemetry. Botello @@ -30,8 +30,9 @@ - + + diff --git a/Config/ConfigurationLoader.cs b/Config/ConfigurationLoader.cs index 2ae59af..18452c2 100644 --- a/Config/ConfigurationLoader.cs +++ b/Config/ConfigurationLoader.cs @@ -56,8 +56,43 @@ private static void ApplyLoggerParameters(string parameters, LoggerConfig config if (string.IsNullOrEmpty(value)) continue; - if (key.Equals("ConnectionString", StringComparison.OrdinalIgnoreCase)) + // ── Exporter selection ────────────────────────────────────── + if (key.Equals("Exporter", StringComparison.OrdinalIgnoreCase)) + { + if (Enum.TryParse(value, ignoreCase: true, out ExporterType exporter)) + config.Exporter = exporter; + else + throw new InvalidOperationException( + $"Botello: Unknown Exporter value '{value}'. " + + "Valid values: AzureMonitor, Otlp."); + } + // ── Azure Monitor ─────────────────────────────────────────── + else if (key.Equals("ConnectionString", StringComparison.OrdinalIgnoreCase)) config.ConnectionString = value; + // ── OTLP ──────────────────────────────────────────────────── + else if (key.Equals("OtlpEndpoint", StringComparison.OrdinalIgnoreCase)) + config.OtlpEndpoint = value; + else if (key.Equals("OtlpProtocol", StringComparison.OrdinalIgnoreCase)) + { + if (Enum.TryParse(value, ignoreCase: true, out OtlpProtocolType protocol)) + config.OtlpProtocol = protocol; + else + throw new InvalidOperationException( + $"Botello: Unknown OtlpProtocol value '{value}'. " + + "Valid values: Grpc, HttpProtobuf."); + } + else if (key.Equals("OtlpHeaders", StringComparison.OrdinalIgnoreCase)) + config.OtlpHeaders = value; + else if (key.Equals("OtlpTimeout", StringComparison.OrdinalIgnoreCase)) + { + if (int.TryParse(value, out var timeout) && timeout > 0) + config.OtlpTimeout = timeout; + else + throw new InvalidOperationException( + $"Botello: Invalid OtlpTimeout value '{value}'. " + + "Provide a positive integer (milliseconds)."); + } + // ── Common ────────────────────────────────────────────────── else if (key.Equals("ServiceName", StringComparison.OrdinalIgnoreCase)) config.ServiceName = value; else if (key.Equals("MinimumLevel", StringComparison.OrdinalIgnoreCase)) diff --git a/Config/LoggerConfig.cs b/Config/LoggerConfig.cs index 464b147..7131e59 100644 --- a/Config/LoggerConfig.cs +++ b/Config/LoggerConfig.cs @@ -2,16 +2,80 @@ namespace Botello.Config; +/// +/// Selects the telemetry export back-end. +/// +public enum ExporterType +{ + /// Azure Application Insights via the Azure Monitor exporter. + AzureMonitor, + + /// Any OTLP-compatible collector (Jaeger, Grafana, Seq, Aspire Dashboard, etc.). + Otlp, +} + +/// +/// Selects the OTLP transport protocol. Only relevant when +/// is set to . +/// +public enum OtlpProtocolType +{ + /// gRPC transport (default OTLP port 4317). + Grpc, + + /// HTTP/protobuf transport (default OTLP port 4318). + HttpProtobuf, +} + /// /// Configuration for the Botello logger. /// Loaded from appsettings.json, environment variables (BOTELLO__*), and Logger.Parameters. /// public sealed class LoggerConfig { - /// Azure Application Insights connection string. Required. + // ── Exporter selection ────────────────────────────────────────────── + + /// + /// Selects the telemetry back-end. Defaults to . + /// Set to to export to any OTLP-compatible collector. + /// + public ExporterType Exporter { get; set; } = ExporterType.AzureMonitor; + + // ── Azure Monitor settings ────────────────────────────────────────── + + /// Azure Application Insights connection string. Required when Exporter is AzureMonitor. public string? ConnectionString { get; set; } - /// Service name reported in Application Insights. Defaults to "Botello". + // ── OTLP settings ─────────────────────────────────────────────────── + + /// + /// OTLP collector endpoint. Defaults to http://localhost:4317 for gRPC + /// or http://localhost:4318 for HTTP/protobuf. Only used when Exporter is Otlp. + /// + public string? OtlpEndpoint { get; set; } + + /// + /// OTLP transport protocol. Defaults to . + /// Only used when Exporter is Otlp. + /// + public OtlpProtocolType OtlpProtocol { get; set; } = OtlpProtocolType.Grpc; + + /// + /// Optional headers sent with every OTLP export request (comma-separated key=value pairs). + /// Example: Authorization=Bearer token123,X-Custom=value. + /// Only used when Exporter is Otlp. + /// + public string? OtlpHeaders { get; set; } + + /// + /// Timeout in milliseconds for OTLP export requests. Defaults to 10 000 (10 s). + /// Only used when Exporter is Otlp. + /// + public int OtlpTimeout { get; set; } = 10_000; + + // ── Common settings ───────────────────────────────────────────────── + + /// Service name reported in telemetry. Defaults to "Botello". public string ServiceName { get; set; } = "Botello"; /// Minimum log level to emit. Defaults to . @@ -34,13 +98,18 @@ public sealed class LoggerConfig internal void Validate() { - if (string.IsNullOrWhiteSpace(ConnectionString)) - throw new InvalidOperationException( - "Botello: A connection string for Application Insights is required. " + - "Provide it via appsettings.json (Botello:ConnectionString), " + - "the APPLICATIONINSIGHTS_CONNECTION_STRING or BOTELLO__CONNECTIONSTRING " + - "environment variable, or the Logger.Parameters CLI argument " + - "(-logger:Botello.dll;ConnectionString=...)."); + if (Exporter == ExporterType.AzureMonitor) + { + if (string.IsNullOrWhiteSpace(ConnectionString)) + throw new InvalidOperationException( + "Botello: A connection string for Application Insights is required when " + + "Exporter is AzureMonitor. Provide it via appsettings.json (Botello:ConnectionString), " + + "the APPLICATIONINSIGHTS_CONNECTION_STRING or BOTELLO__CONNECTIONSTRING " + + "environment variable, or the Logger.Parameters CLI argument " + + "(-logger:Botello.dll;ConnectionString=...)."); + } + + // OTLP does not strictly require an endpoint — it falls back to localhost defaults. if (string.IsNullOrWhiteSpace(ServiceName)) ServiceName = "Botello"; diff --git a/README.md b/README.md index 9b205c0..e21175b 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ # Botello📝 -A custom logger that captures build events and forwards them to Open Telemetry compatible logging system such as **Azure Application Insights** as distributed traces and structured logs. +A custom logger that captures build events and forwards them to any OpenTelemetry-compatible backend — **Azure Application Insights**, **Jaeger**, **Grafana Tempo**, **Seq**, **.NET Aspire Dashboard**, or any **OTLP collector** — as distributed traces and structured logs. -Drop `Botello.dll` onto any `dotnet build` or `msbuild` command and get full build observability in App Insights — no code changes to your projects required. +Drop `Botello.dll` onto any `dotnet build` or `msbuild` command and get full build observability — no code changes to your projects required. --- @@ -12,17 +12,20 @@ Drop `Botello.dll` onto any `dotnet build` or `msbuild` command and get full bui - **Distributed traces** — hierarchical spans for build → project → target with pass/fail status - **Structured logs** — errors, warnings, messages, and lifecycle events with rich `customDimensions` +- **Two exporters** — Azure Application Insights or any OTLP-compatible backend (gRPC / HTTP) - **Zero-code-change** — attach as an external logger; nothing in your project files changes - **Flexible configuration** — `appsettings.json`, environment variables, or inline CLI parameters - **Parallel-build safe** — span keys are composite to avoid collisions across MSBuild nodes -- **Guaranteed flush** — all in-flight telemetry is flushed to Azure Monitor before the process exits +- **Guaranteed flush** — all in-flight telemetry is flushed before the process exits --- ## Requirements🔗 - .NET 10 SDK or later -- An Azure Application Insights resource (connection string) +- One of the following: + - An Azure Application Insights resource (connection string), **or** + - An OTLP-compatible collector endpoint (e.g. Jaeger, Grafana Tempo, Seq, Aspire Dashboard, OTel Collector) --- @@ -46,7 +49,9 @@ Copy the entire `net10.0/` output directory to a stable location (e.g. `~/.msbui ## Quick Start🚀 -Set your Application Insights connection string as an environment variable and pass the logger path to `dotnet build`: +### Azure Application Insights + +Set your connection string as an environment variable and pass the logger path to `dotnet build`: ```bash export APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=00000000-...;IngestionEndpoint=https://..." @@ -62,9 +67,18 @@ $env:APPLICATIONINSIGHTS_CONNECTION_STRING = "InstrumentationKey=00000000-...;In dotnet build MyApp.sln -logger:"C:\tools\Botello\Botello.dll" ``` +### OTLP (Jaeger, Grafana, Aspire Dashboard, etc.) + +Point the logger at any OTLP collector — no Azure account needed: + +```bash +dotnet build MyApp.sln \ + -logger:"Botello.dll;Exporter=Otlp;OtlpEndpoint=http://localhost:4317" +``` + Within seconds of the build completing you will see: -- A **`build`** span in the App Insights Transaction Search / Application Map +- A **`build`** span in your tracing UI - Child **`project: `** spans for each `.csproj` built - Log entries for warnings, errors, and messages under the corresponding categories @@ -72,7 +86,7 @@ Within seconds of the build completing you will see: ## Usage📝 -### Minimal — connection string from environment +### Azure Monitor — connection string from environment ```bash dotnet build -logger:Botello.dll @@ -83,26 +97,81 @@ Botello reads the connection string from either of these environment variables ( 1. `BOTELLO__CONNECTIONSTRING` 2. `APPLICATIONINSIGHTS_CONNECTION_STRING` (the standard App Insights variable) -### Inline parameters - -Pass all options directly on the command line using semicolon-separated `Key=Value` pairs after the DLL path: +### Azure Monitor — inline parameters ```bash dotnet build MyApp.sln \ -logger:"Botello.dll;ConnectionString=InstrumentationKey=...;ServiceName=my-app;MinimumLevel=Debug" ``` -### Override a single option while keeping appsettings.json +### OTLP — gRPC (default protocol) ```bash -# Enable target-level spans (off by default) without touching appsettings.json -dotnet build -logger:"Botello.dll;IncludeTargetEvents=true" +# Jaeger with OTLP gRPC on default port 4317 +dotnet build MyApp.sln \ + -logger:"Botello.dll;Exporter=Otlp;OtlpEndpoint=http://localhost:4317;ServiceName=my-app" +``` + +### OTLP — HTTP/protobuf + +```bash +# Grafana Tempo or OTel Collector with HTTP/protobuf on default port 4318 +dotnet build MyApp.sln \ + -logger:"Botello.dll;Exporter=Otlp;OtlpProtocol=HttpProtobuf;OtlpEndpoint=http://localhost:4318" +``` + +### OTLP — with authentication headers + +```bash +# Grafana Cloud, Honeycomb, or any backend requiring auth headers +dotnet build MyApp.sln \ + -logger:"Botello.dll;Exporter=Otlp;OtlpEndpoint=https://otlp.example.com:4317;OtlpHeaders=Authorization=Bearer mytoken123" +``` + +### OTLP — .NET Aspire Dashboard + +```bash +# The Aspire Dashboard listens on OTLP gRPC port 4317 by default +docker run -d -p 18888:18888 -p 4317:18889 mcr.microsoft.com/dotnet/aspire-dashboard:latest + +dotnet build MyApp.sln \ + -logger:"Botello.dll;Exporter=Otlp;OtlpEndpoint=http://localhost:4317;ServiceName=my-app" +``` + +Then open `http://localhost:18888` to see your build traces and logs. + +### OTLP — localhost defaults + +When using OTLP with no endpoint specified, Botello uses the OTel SDK defaults: +- gRPC → `http://localhost:4317` +- HTTP/protobuf → `http://localhost:4318` + +```bash +# If your collector is on default ports, just set the exporter +dotnet build -logger:"Botello.dll;Exporter=Otlp" +``` + +### OTLP — via environment variables + +```bash +export BOTELLO__EXPORTER=Otlp +export BOTELLO__OTLPENDPOINT=http://localhost:4317 +export BOTELLO__SERVICENAME=my-app + +dotnet build MyApp.sln -logger:Botello.dll ``` ### Use with msbuild.exe ```bash -msbuild MyApp.sln /logger:"C:\tools\Botello\Botello.dll;ConnectionString=...;ServiceName=MyApp" +msbuild MyApp.sln /logger:"C:\tools\Botello\Botello.dll;Exporter=Otlp;OtlpEndpoint=http://localhost:4317" +``` + +### Override a single option while keeping appsettings.json + +```bash +# Enable target-level spans (off by default) without touching appsettings.json +dotnet build -logger:"Botello.dll;IncludeTargetEvents=true" ``` ### Suppress noisy telemetry on large solutions @@ -120,18 +189,40 @@ Configuration is merged from three sources in ascending priority (last wins): | Priority | Source | Example | |---|---|---| -| 1 (lowest) | `appsettings.json` next to the DLL | `"Botello": { "ServiceName": "my-app" }` | -| 2 | Environment variables (`BOTELLO__*`) | `BOTELLO__SERVICENAME=my-app` | -| 3 (highest) | MSBuild `Logger.Parameters` | `-logger:"Botello.dll;ServiceName=my-app"` | +| 1 (lowest) | `appsettings.json` next to the DLL | `"Botello": { "Exporter": "Otlp" }` | +| 2 | Environment variables (`BOTELLO__*`) | `BOTELLO__EXPORTER=Otlp` | +| 3 (highest) | MSBuild `Logger.Parameters` | `-logger:"Botello.dll;Exporter=Otlp"` | ### All options +#### Exporter selection + | Key | Type | Default | Env var | Description | |---|---|---|---|---| -| `ConnectionString` | `string` | *(none)* | `BOTELLO__CONNECTIONSTRING` | **Required.** Azure Application Insights connection string. Also accepts `APPLICATIONINSIGHTS_CONNECTION_STRING` as a fallback. | -| `ServiceName` | `string` | `Botello` | `BOTELLO__SERVICENAME` | Service name shown in Application Insights. Reported as the `service.name` OpenTelemetry resource attribute. | -| `MinimumLevel` | `LogLevel` | `Information` | `BOTELLO__MINIMUMLEVEL` | Minimum log level to emit. Accepted values: `Trace` `Debug` `Information` `Warning` `Error` `Critical`. | -| `IncludeMessages` | `bool` | `true` | `BOTELLO__INCLUDEMESSAGES` | Forward `MessageRaised` events. High-importance → `Information`, Normal → `Debug`, Low → `Trace`. | +| `Exporter` | `ExporterType` | `AzureMonitor` | `BOTELLO__EXPORTER` | Selects the telemetry back-end. Values: `AzureMonitor`, `Otlp`. | + +#### Azure Monitor options (used when Exporter = AzureMonitor) + +| Key | Type | Default | Env var | Description | +|---|---|---|---|---| +| `ConnectionString` | `string` | *(none)* | `BOTELLO__CONNECTIONSTRING` | **Required.** App Insights connection string. Also accepts `APPLICATIONINSIGHTS_CONNECTION_STRING` as a fallback. | + +#### OTLP options (used when Exporter = Otlp) + +| Key | Type | Default | Env var | Description | +|---|---|---|---|---| +| `OtlpEndpoint` | `string` | *(SDK default)* | `BOTELLO__OTLPENDPOINT` | Collector endpoint URL. Defaults to `http://localhost:4317` (gRPC) or `http://localhost:4318` (HTTP/protobuf). | +| `OtlpProtocol` | `OtlpProtocolType` | `Grpc` | `BOTELLO__OTLPPROTOCOL` | Transport protocol. Values: `Grpc`, `HttpProtobuf`. | +| `OtlpHeaders` | `string` | *(none)* | `BOTELLO__OTLPHEADERS` | Comma-separated `key=value` headers sent with every export request. | +| `OtlpTimeout` | `int` | `10000` | `BOTELLO__OTLPTIMEOUT` | Export request timeout in milliseconds. | + +#### Common options + +| Key | Type | Default | Env var | Description | +|---|---|---|---|---| +| `ServiceName` | `string` | `Botello` | `BOTELLO__SERVICENAME` | Service name reported as the `service.name` OTel resource attribute. | +| `MinimumLevel` | `LogLevel` | `Information` | `BOTELLO__MINIMUMLEVEL` | Minimum log level to emit. Values: `Trace` `Debug` `Information` `Warning` `Error` `Critical`. | +| `IncludeMessages` | `bool` | `true` | `BOTELLO__INCLUDEMESSAGES` | Forward `MessageRaised` events. High → `Information`, Normal → `Debug`, Low → `Trace`. | | `IncludeWarnings` | `bool` | `true` | `BOTELLO__INCLUDEWARNINGS` | Forward `WarningRaised` events at `Warning` level. | | `IncludeErrors` | `bool` | `true` | `BOTELLO__INCLUDEERRORS` | Forward `ErrorRaised` events at `Error` level and mark the active span as failed. | | `IncludeProjectEvents` | `bool` | `true` | `BOTELLO__INCLUDEPROJECTEVENTS` | Emit spans and `Debug` log entries for `ProjectStarted` / `ProjectFinished`. | @@ -141,11 +232,35 @@ Boolean options in `Logger.Parameters` accept `true`/`false`, `1`/`0`, `yes`/`no ### appsettings.json reference +#### Azure Monitor + ```json { "Botello": { - "ConnectionString": "", - "ServiceName": "Botello", + "Exporter": "AzureMonitor", + "ConnectionString": "InstrumentationKey=...", + "ServiceName": "my-app", + "MinimumLevel": "Information", + "IncludeMessages": true, + "IncludeWarnings": true, + "IncludeErrors": true, + "IncludeProjectEvents": true, + "IncludeTargetEvents": false + } +} +``` + +#### OTLP + +```json +{ + "Botello": { + "Exporter": "Otlp", + "OtlpEndpoint": "http://localhost:4317", + "OtlpProtocol": "Grpc", + "OtlpHeaders": "", + "OtlpTimeout": 10000, + "ServiceName": "my-app", "MinimumLevel": "Information", "IncludeMessages": true, "IncludeWarnings": true, @@ -179,7 +294,7 @@ Spans are marked `Ok` on success and `Error` (with a description) on failure. Wh ### Log categories -Logs appear in Application Insights under the following category names (visible as the logger name / `customDimensions`): +Logs appear under the following category names (visible as the logger name / `customDimensions`): | Category | Events | |---|---| @@ -212,7 +327,7 @@ Every telemetry item carries these OpenTelemetry resource attributes: ## CI/CD Integration -### GitHub Actions +### GitHub Actions — Azure Monitor ```yaml - name: Build @@ -223,6 +338,20 @@ Every telemetry item carries these OpenTelemetry resource attributes: -logger:"${{ github.workspace }}/tools/Botello/Botello.dll;ServiceName=my-app;MinimumLevel=Warning" ``` +### GitHub Actions — OTLP + +```yaml +- name: Build + env: + BOTELLO__EXPORTER: Otlp + BOTELLO__OTLPENDPOINT: ${{ secrets.OTLP_ENDPOINT }} + BOTELLO__OTLPHEADERS: "Authorization=Bearer ${{ secrets.OTLP_TOKEN }}" + BOTELLO__SERVICENAME: my-app + run: | + dotnet build MyApp.sln \ + -logger:"${{ github.workspace }}/tools/Botello/Botello.dll" +``` + ### Azure Pipelines ```yaml @@ -243,7 +372,7 @@ Every telemetry item carries these OpenTelemetry resource attributes: 1. MSBuild loads `Botello.dll` and calls `AppInsightsLogger.Initialize()`. 2. `ConfigurationLoader` merges settings from `appsettings.json`, `BOTELLO__*` environment variables, and the `Logger.Parameters` string. -3. `OtelPipelineManager` initialises a `TracerProvider` and an `ILoggerFactory`, both pointed at Azure Monitor. +3. `OtelPipelineManager` initialises a `TracerProvider` and an `ILoggerFactory`, routing to either Azure Monitor or an OTLP collector based on the `Exporter` setting. 4. `AppInsightsLogger` subscribes to the requested MSBuild event sources. 5. During the build, each event creates an OTel span and/or log entry. 6. When MSBuild calls `Shutdown()`, `OtelPipelineManager.Dispose()` flushes all in-flight batches — traces first, then logs — before returning, so no telemetry is dropped. @@ -257,6 +386,7 @@ Every telemetry item carries these OpenTelemetry resource attributes: | `Microsoft.Build.Framework` | 18.3.3 | | `Microsoft.Build.Utilities.Core` | 18.3.3 | | `Azure.Monitor.OpenTelemetry.Exporter` | 1.6.0 | +| `OpenTelemetry.Exporter.OpenTelemetryProtocol` | 1.15.0 | | `OpenTelemetry.Extensions.Hosting` | 1.15.0 | | `Microsoft.Extensions.Configuration.Json` | 10.0.3 | | `Microsoft.Extensions.Configuration.EnvironmentVariables` | 10.0.3 | diff --git a/Telemetry/OtelPipelineManager.cs b/Telemetry/OtelPipelineManager.cs index f7a79ee..1f99039 100644 --- a/Telemetry/OtelPipelineManager.cs +++ b/Telemetry/OtelPipelineManager.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using Botello.Config; using OpenTelemetry; +using OpenTelemetry.Exporter; using OpenTelemetry.Logs; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -11,7 +12,8 @@ namespace Botello.Telemetry; /// /// Owns the OTel pipeline lifetime: a for spans -/// and an for log records, both exporting to Azure Monitor. +/// and an for log records, exporting to either +/// Azure Monitor or any OTLP-compatible collector. /// Call once before use and to flush. /// internal sealed class OtelPipelineManager : IDisposable @@ -52,11 +54,13 @@ public void Dispose() if (_disposed) return; _disposed = true; - // Traces first, then logs — preserves causality in App Insights. + // Traces first, then logs — preserves causality in the back-end. _tracerProvider?.Dispose(); _loggerFactory?.Dispose(); } + // ── Resource ──────────────────────────────────────────────────────── + private static ResourceBuilder BuildResourceBuilder(LoggerConfig config) => ResourceBuilder .CreateDefault() @@ -67,12 +71,23 @@ private static ResourceBuilder BuildResourceBuilder(LoggerConfig config) => ["telemetry.sdk.name"] = "Botello", }); - private static TracerProvider BuildTracerProvider(LoggerConfig config, ResourceBuilder resourceBuilder) => - Sdk.CreateTracerProviderBuilder() + // ── Traces ────────────────────────────────────────────────────────── + + private static TracerProvider BuildTracerProvider(LoggerConfig config, ResourceBuilder resourceBuilder) + { + var builder = Sdk.CreateTracerProviderBuilder() .SetResourceBuilder(resourceBuilder) - .AddSource(ActivitySourceName) - .AddAzureMonitorTraceExporter(o => o.ConnectionString = config.ConnectionString) - .Build()!; + .AddSource(ActivitySourceName); + + if (config.Exporter == ExporterType.Otlp) + builder.AddOtlpExporter(o => ApplyOtlpOptions(config, o)); + else + builder.AddAzureMonitorTraceExporter(o => o.ConnectionString = config.ConnectionString); + + return builder.Build()!; + } + + // ── Logs ──────────────────────────────────────────────────────────── private static ILoggerFactory BuildLoggerFactory(LoggerConfig config, ResourceBuilder resourceBuilder) => LoggerFactory.Create(builder => @@ -81,9 +96,32 @@ private static ILoggerFactory BuildLoggerFactory(LoggerConfig config, ResourceBu builder.AddOpenTelemetry(otel => { otel.SetResourceBuilder(resourceBuilder); - otel.IncludeFormattedMessage = true; // store rendered message, not just the template - otel.IncludeScopes = true; // emit BeginScope values as customDimensions - otel.AddAzureMonitorLogExporter(o => o.ConnectionString = config.ConnectionString); + otel.IncludeFormattedMessage = true; + otel.IncludeScopes = true; + + if (config.Exporter == ExporterType.Otlp) + otel.AddOtlpExporter(o => ApplyOtlpOptions(config, o)); + else + otel.AddAzureMonitorLogExporter(o => o.ConnectionString = config.ConnectionString); }); }); + + // ── OTLP shared configuration ────────────────────────────────────── + + private static void ApplyOtlpOptions(LoggerConfig config, OtlpExporterOptions options) + { + options.Protocol = config.OtlpProtocol switch + { + OtlpProtocolType.HttpProtobuf => OtlpExportProtocol.HttpProtobuf, + _ => OtlpExportProtocol.Grpc, + }; + + if (!string.IsNullOrWhiteSpace(config.OtlpEndpoint)) + options.Endpoint = new Uri(config.OtlpEndpoint); + + if (!string.IsNullOrWhiteSpace(config.OtlpHeaders)) + options.Headers = config.OtlpHeaders; + + options.TimeoutMilliseconds = config.OtlpTimeout; + } } diff --git a/appsettings.json b/appsettings.json index 774c0a2..4c0b11c 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,12 +1,40 @@ { "Botello": { + // Exporter back-end: "AzureMonitor" (default) or "Otlp". + // Override via env var: BOTELLO__EXPORTER + "Exporter": "AzureMonitor", + + // ── Azure Monitor settings (used when Exporter is "AzureMonitor") ── + // Azure Application Insights connection string. // Override via env var: BOTELLO__CONNECTIONSTRING // or the standard env var: APPLICATIONINSIGHTS_CONNECTION_STRING // or via Logger.Parameters on the CLI: -logger:Botello.dll;ConnectionString=... "ConnectionString": "", - // Logical service name shown in Application Insights. + // ── OTLP settings (used when Exporter is "Otlp") ─────────────────── + + // OTLP collector endpoint. + // Defaults to http://localhost:4317 (gRPC) or http://localhost:4318 (HTTP/protobuf). + // Override via env var: BOTELLO__OTLPENDPOINT + "OtlpEndpoint": "", + + // OTLP transport protocol: "Grpc" (default) or "HttpProtobuf". + // Override via env var: BOTELLO__OTLPPROTOCOL + "OtlpProtocol": "Grpc", + + // Optional headers sent with every OTLP export request. + // Comma-separated key=value pairs, e.g. "Authorization=Bearer token,X-Tenant=abc". + // Override via env var: BOTELLO__OTLPHEADERS + "OtlpHeaders": "", + + // Timeout in milliseconds for OTLP export requests. Default: 10000 (10 s). + // Override via env var: BOTELLO__OTLPTIMEOUT + "OtlpTimeout": 10000, + + // ── Common settings ───────────────────────────────────────────────── + + // Logical service name shown in telemetry. // Override via env var: BOTELLO__SERVICENAME "ServiceName": "Botello",