READ NOW
All posts

Forward Your Serilog Logs to Sawmills via OpenTelemetry: A Complete Guide

Observability
Sep
26
2025
Sep
25
2025

If you're already using Serilog for structured logging in your .NET applications, you're probably familiar with its powerful sink ecosystem. But what if you want to send those logs to a centralized telemetry management platform, such as Sawmills? In this guide, we'll show you how to seamlessly forward your existing Serilog logs through OpenTelemetry to Sawmills without disrupting your current logging setup.

Why OpenTelemetry + Sawmills?

Before diving into the implementation, let's understand why this combination is powerful:

  • Centralized Telemetry Management: Sawmills provides a smart telemetry management platform for logs, metrics, and traces that leverages AI to help organizations route and filter telemetry data before it’s sent to the observability platform.  No other solution in the market applies AI-based detection for noise to telemetry optimization, making Sawmills uniquely positioned to cut waste, improve data quality, and enforce governance at scale.  
  • Vendor Neutrality: OpenTelemetry is an industry standard, avoiding vendor lock-in
  • Rich Context: Your existing structured Serilog data flows seamlessly into Sawmills
  • Multi-Sink Architecture: Keep your existing console, file, and other sinks while adding Sawmills

Prerequisites

  • Existing .NET application with Serilog configured
  • Access to a Sawmills collector (we'll show you how to identify it)
  • Basic knowledge of NuGet package management

Step 1: Add OpenTelemetry Packages

First, add the necessary NuGet packages to your existing project:

<PackageReference Include="Serilog.Sinks.OpenTelemetry" Version="4.1.0" />
<PackageReference Include="OpenTelemetry" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />

Important: Update your Serilog version to 4.0.0+ for compatibility:

<PackageReference Include="Serilog" Version="4.0.0" />

Step 2: Find Your Sawmills Collector Endpoint

If you're running Sawmills in Kubernetes, locate your collector:

# List Sawmills collector pods
kubectl get pods -n sawmills -l app.kubernetes.io/name=sawmills-collector-chart

# Get the pod IP (you'll need this)
kubectl get pods -n sawmills -o wide | grep sawmills-collector

You should see output like:

sawmills-collector-69848894bc-6bqsw   3/3   Running   192.168.194.120

Note the IP address (192.168.194.120 in this example) - this is your collector endpoint.

Step 3: Configure the OpenTelemetry Sink

Option A: Configuration File Approach

Add the OpenTelemetry sink to your existing appsettings.json:

{
  "Serilog": {
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
        }
      },
      {
        "Name": "File",
        "Args": {
          "path": "logs/app-.log",
          "rollingInterval": "Day"
        }
      },
      {
        "Name": "OpenTelemetry",
        "Args": {
          "endpoint": "http://192.168.194.120:4318",
          "protocol": "HttpProtobuf",
          "headers": {
            "X-Source": "YourApp-to-Sawmills"
          },
          "resourceAttributes": {
            "service.name": "your-service-name",
            "service.version": "1.0.0",
            "service.instance.id": "instance-1",
            "deployment.environment": "production"
          },
          "includeFormattedMessage": true,
          "includeScopeInformation": true
        }
      }
    ]
  }
}

Option B: Code-First Approach

If you prefer configuring in code, update your Program.cs:

using OpenTelemetry.Logs;
using OpenTelemetry.Resources;
using Serilog;

// Your existing Serilog configuration
Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .WriteTo.File("logs/app.log")
    .WriteTo.OpenTelemetry(opts =>
    {
        opts.Endpoint = "http://192.168.194.120:4318";
        opts.Protocol = OtlpProtocol.HttpProtobuf;
        opts.Headers = new Dictionary<string, string>
        {
            ["X-Source"] = "YourApp-to-Sawmills"
        };
        opts.ResourceAttributes = new Dictionary<string, object>
        {
            ["service.name"] = "your-service-name",
            ["service.version"] = "1.0.0",
            ["deployment.environment"] = "production"
        };
    })
    .CreateLogger();

// Also configure OpenTelemetry logging for .NET's ILogger
var host = Host.CreateDefaultBuilder(args)
    .UseSerilog()
    .ConfigureServices(services =>
    {
        services.AddOpenTelemetry()
            .WithLogging(logging =>
            {
                logging.SetResourceBuilder(ResourceBuilder.CreateDefault()
                    .AddService("your-service-name", "1.0.0"));
                    
                logging.AddOtlpExporter(options =>
                {
                    options.Endpoint = new Uri("http://192.168.194.120:4318");
                    options.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf;
                });
            });
    })
    .Build();

Step 4: Make the Configuration Flexible

Hard-coding IP addresses isn't ideal for production. Here are better approaches:

Environment Variables

Update your code to use environment variables:

logging.AddOtlpExporter(options =>
{
    var host = Environment.GetEnvironmentVariable("SAWMILLS_COLLECTOR_HOST") ?? "localhost";
    var port = Environment.GetEnvironmentVariable("SAWMILLS_COLLECTOR_PORT") ?? "4318";
    options.Endpoint = new Uri($"http://{host}:{port}");
    options.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf;
});

Environment-Specific Configuration Files

Create appsettings.Production.json:

{
  "Serilog": {
    "WriteTo": [
      {
        "Name": "OpenTelemetry",
        "Args": {
          "endpoint": "http://sawmills-collector.sawmills.svc.cluster.local:4318"
        }
      }
    ]
  }
}

Step 5: Test the Integration

Run your application and verify the connection:

# Set environment variables
export SAWMILLS_COLLECTOR_HOST=192.168.194.120
export SAWMILLS_COLLECTOR_PORT=4318

# Run your application
dotnet run

You should see your existing console/file logs plus successful transmission to Sawmills.

Step 6: Verify Logs in Sawmills

Check your Sawmills dashboard to confirm logs are flowing. Look for:

  • Service name: your-service-name
  • Custom headers: X-Source: YourApp-to-Sawmills
  • All your existing structured log data
  • Proper timestamps and log levels

Example Output

Here's what your logs will look like in Sawmills:

{
  "timestamp": "2025-09-23T18:42:12.530Z",
  "level": "Information",
  "message": "User {UserId} placed order {OrderId} for {Amount:C}",
  "properties": {
    "UserId": 12345,
    "OrderId": "8409a340-238a-4c53-ad58-3373d5b3c44e",
    "Amount": 99.99,
    "SourceContext": "YourApp.OrderService"
  },
  "resource": {
    "service.name": "your-service-name",
    "service.version": "1.0.0",
    "deployment.environment": "production"
  }
}

Conclusion

By following this guide, you've successfully:

  • Added OpenTelemetry forwarding to your existing Serilog setup
  • Configured flexible endpoint management
  • Maintained all your existing logging functionality
  • Gained centralized telemetry management through Sawmills

Your logs now flow seamlessly from Serilog → OpenTelemetry → Sawmills, giving you the best of structured logging and centralized observability.

Need help with your specific setup? The Sawmills team is here to help with implementation questions and best practices.