Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Botello.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<!-- Prevent MSBuild from loading this assembly as a build task -->
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>

<Description>MSBuild custom logger that forwards build events to Azure Application Insights via OpenTelemetry.</Description>
<Description>MSBuild custom logger that forwards build events to Azure Application Insights or any OTLP-compatible backend via OpenTelemetry.</Description>
<Authors>Botello</Authors>
</PropertyGroup>

Expand All @@ -30,8 +30,9 @@
<PackageReference Include="Microsoft.Build.Framework" Version="18.3.3" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="18.3.3" />

<!-- OpenTelemetry + Azure Monitor exporter (covers both logs and traces) -->
<!-- OpenTelemetry + exporters (Azure Monitor and OTLP) -->
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.6.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.0" />

<!-- Configuration: JSON file + environment variables -->
Expand Down
37 changes: 36 additions & 1 deletion Config/ConfigurationLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
87 changes: 78 additions & 9 deletions Config/LoggerConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,80 @@

namespace Botello.Config;

/// <summary>
/// Selects the telemetry export back-end.
/// </summary>
public enum ExporterType
{
/// <summary>Azure Application Insights via the Azure Monitor exporter.</summary>
AzureMonitor,

/// <summary>Any OTLP-compatible collector (Jaeger, Grafana, Seq, Aspire Dashboard, etc.).</summary>
Otlp,
}

/// <summary>
/// Selects the OTLP transport protocol. Only relevant when <see cref="LoggerConfig.Exporter"/>
/// is set to <see cref="ExporterType.Otlp"/>.
/// </summary>
public enum OtlpProtocolType
{
/// <summary>gRPC transport (default OTLP port 4317).</summary>
Grpc,

/// <summary>HTTP/protobuf transport (default OTLP port 4318).</summary>
HttpProtobuf,
}

/// <summary>
/// Configuration for the Botello logger.
/// Loaded from appsettings.json, environment variables (BOTELLO__*), and Logger.Parameters.
/// </summary>
public sealed class LoggerConfig
{
/// <summary>Azure Application Insights connection string. Required.</summary>
// ── Exporter selection ──────────────────────────────────────────────

/// <summary>
/// Selects the telemetry back-end. Defaults to <see cref="ExporterType.AzureMonitor"/>.
/// Set to <see cref="ExporterType.Otlp"/> to export to any OTLP-compatible collector.
/// </summary>
public ExporterType Exporter { get; set; } = ExporterType.AzureMonitor;

// ── Azure Monitor settings ──────────────────────────────────────────

/// <summary>Azure Application Insights connection string. Required when Exporter is AzureMonitor.</summary>
public string? ConnectionString { get; set; }

/// <summary>Service name reported in Application Insights. Defaults to "Botello".</summary>
// ── OTLP settings ───────────────────────────────────────────────────

/// <summary>
/// OTLP collector endpoint. Defaults to <c>http://localhost:4317</c> for gRPC
/// or <c>http://localhost:4318</c> for HTTP/protobuf. Only used when Exporter is Otlp.
/// </summary>
public string? OtlpEndpoint { get; set; }

/// <summary>
/// OTLP transport protocol. Defaults to <see cref="OtlpProtocolType.Grpc"/>.
/// Only used when Exporter is Otlp.
/// </summary>
public OtlpProtocolType OtlpProtocol { get; set; } = OtlpProtocolType.Grpc;

/// <summary>
/// Optional headers sent with every OTLP export request (comma-separated key=value pairs).
/// Example: <c>Authorization=Bearer token123,X-Custom=value</c>.
/// Only used when Exporter is Otlp.
/// </summary>
public string? OtlpHeaders { get; set; }

/// <summary>
/// Timeout in milliseconds for OTLP export requests. Defaults to 10 000 (10 s).
/// Only used when Exporter is Otlp.
/// </summary>
public int OtlpTimeout { get; set; } = 10_000;

// ── Common settings ─────────────────────────────────────────────────

/// <summary>Service name reported in telemetry. Defaults to "Botello".</summary>
public string ServiceName { get; set; } = "Botello";

/// <summary>Minimum log level to emit. Defaults to <see cref="LogLevel.Information"/>.</summary>
Expand All @@ -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";
Expand Down
Loading