Merge branch 'log-exporter' into develop

This commit is contained in:
Shreyas Zare
2025-01-18 13:03:38 +05:30
11 changed files with 1007 additions and 0 deletions

214
Apps/LogExporterApp/App.cs Normal file
View File

@@ -0,0 +1,214 @@
/*
Technitium DNS Server
Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using DnsServerCore.ApplicationCommon;
using LogExporter.Strategy;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using TechnitiumLibrary;
using TechnitiumLibrary.Net.Dns;
namespace LogExporter
{
public sealed class App : IDnsApplication, IDnsQueryLogger
{
#region variables
IDnsServer? _dnsServer;
BufferManagementConfig? _config;
readonly ExportManager _exportManager = new ExportManager();
bool _enableLogging;
readonly ConcurrentQueue<LogEntry> _queuedLogs = new ConcurrentQueue<LogEntry>();
readonly Timer _queueTimer;
const int QUEUE_TIMER_INTERVAL = 10000;
const int BULK_INSERT_COUNT = 1000;
bool _disposed;
#endregion
#region constructor
public App()
{
_queueTimer = new Timer(HandleExportLogCallback);
}
#endregion
#region IDisposable
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_queueTimer?.Dispose();
ExportLogsAsync().Sync(); //flush any pending logs
_exportManager.Dispose();
}
_disposed = true;
}
}
#endregion
#region public
public Task InitializeAsync(IDnsServer dnsServer, string config)
{
_dnsServer = dnsServer;
_config = BufferManagementConfig.Deserialize(config);
if (_config is null)
throw new DnsClientException("Invalid application configuration.");
if (_config.FileTarget!.Enabled)
{
_exportManager.RemoveStrategy(typeof(FileExportStrategy));
_exportManager.AddStrategy(new FileExportStrategy(_config.FileTarget!.Path));
}
else
{
_exportManager.RemoveStrategy(typeof(FileExportStrategy));
}
if (_config.HttpTarget!.Enabled)
{
_exportManager.RemoveStrategy(typeof(HttpExportStrategy));
_exportManager.AddStrategy(new HttpExportStrategy(_config.HttpTarget.Endpoint, _config.HttpTarget.Headers));
}
else
{
_exportManager.RemoveStrategy(typeof(HttpExportStrategy));
}
if (_config.SyslogTarget!.Enabled)
{
_exportManager.RemoveStrategy(typeof(SyslogExportStrategy));
_exportManager.AddStrategy(new SyslogExportStrategy(_config.SyslogTarget.Address, _config.SyslogTarget.Port, _config.SyslogTarget.Protocol));
}
else
{
_exportManager.RemoveStrategy(typeof(SyslogExportStrategy));
}
_enableLogging = _exportManager.HasStrategy();
if (_enableLogging)
_queueTimer.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite);
else
_queueTimer.Change(Timeout.Infinite, Timeout.Infinite);
return Task.CompletedTask;
}
public Task InsertLogAsync(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)
{
if (_enableLogging)
{
if (_queuedLogs.Count < _config!.MaxQueueSize)
_queuedLogs.Enqueue(new LogEntry(timestamp, remoteEP, protocol, request, response));
}
return Task.CompletedTask;
}
#endregion
#region private
private async Task ExportLogsAsync()
{
try
{
List<LogEntry> logs = new List<LogEntry>(BULK_INSERT_COUNT);
while (true)
{
while (logs.Count < BULK_INSERT_COUNT && _queuedLogs.TryDequeue(out LogEntry? log))
{
logs.Add(log);
}
if (logs.Count < 1)
break;
await _exportManager.ImplementStrategyAsync(logs);
logs.Clear();
}
}
catch (Exception ex)
{
_dnsServer?.WriteLog(ex);
}
}
private async void HandleExportLogCallback(object? state)
{
try
{
// Process logs within the timer interval, then let the timer reschedule
await ExportLogsAsync();
}
catch (Exception ex)
{
_dnsServer?.WriteLog(ex);
}
finally
{
try
{
_queueTimer?.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite);
}
catch (ObjectDisposedException)
{ }
}
}
#endregion
#region properties
public string Description
{
get { return "Allows exporting query logs to third party sinks. It supports exporting to File, HTTP endpoint, and Syslog (UDP, TCP, TLS, and Local protocols)."; }
}
#endregion
}
}

View File

@@ -0,0 +1,94 @@
/*
Technitium DNS Server
Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace LogExporter
{
public class BufferManagementConfig
{
[JsonPropertyName("maxQueueSize")]
public int MaxQueueSize
{ get; set; }
[JsonPropertyName("file")]
public FileTarget? FileTarget { get; set; }
[JsonPropertyName("http")]
public HttpTarget? HttpTarget { get; set; }
[JsonPropertyName("syslog")]
public SyslogTarget? SyslogTarget { get; set; }
// Load configuration from JSON
public static BufferManagementConfig? Deserialize(string json)
{
return JsonSerializer.Deserialize<BufferManagementConfig>(json, DnsConfigSerializerOptions.Default);
}
}
public class TargetBase
{
[JsonPropertyName("enabled")]
public bool Enabled { get; set; }
}
public class SyslogTarget : TargetBase
{
[JsonPropertyName("address")]
public string Address { get; set; }
[JsonPropertyName("port")]
public int? Port { get; set; }
[JsonPropertyName("protocol")]
public string? Protocol { get; set; }
}
public class FileTarget : TargetBase
{
[JsonPropertyName("path")]
public string Path { get; set; }
}
public class HttpTarget : TargetBase
{
[JsonPropertyName("endpoint")]
public string Endpoint { get; set; }
[JsonPropertyName("headers")]
public Dictionary<string, string?>? Headers { get; set; }
}
// Setup reusable options with a single instance
public static class DnsConfigSerializerOptions
{
public static readonly JsonSerializerOptions Default = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // Convert properties to camelCase
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // For safe encoding
NumberHandling = JsonNumberHandling.Strict,
AllowTrailingCommas = true, // Allow trailing commas in JSON
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, // Convert dictionary keys to camelCase
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull // Ignore null values
};
}
}

View File

@@ -0,0 +1,138 @@
/*
Technitium DNS Server
Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using DnsServerCore.ApplicationCommon;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
namespace LogExporter
{
public class LogEntry
{
public DateTime Timestamp { get; private set; }
public string ClientIp { get; private set; }
public DnsTransportProtocol Protocol { get; private set; }
public DnsServerResponseType ResponseType { get; private set; }
public double? ResponseRtt { get; private set; }
public DnsResponseCode ResponseCode { get; private set; }
public DnsQuestion? Question { get; private set; }
public List<DnsResourceRecord> Answers { get; private set; }
public LogEntry(DateTime timestamp, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram request, DnsDatagram response)
{
// Assign timestamp and ensure it's in UTC
Timestamp = timestamp.Kind == DateTimeKind.Utc ? timestamp : timestamp.ToUniversalTime();
// Extract client information
ClientIp = remoteEP.Address.ToString();
Protocol = protocol;
ResponseType = response.Tag == null ? DnsServerResponseType.Recursive : (DnsServerResponseType)response.Tag;
if ((ResponseType == DnsServerResponseType.Recursive) && (response.Metadata is not null))
ResponseRtt = response.Metadata.RoundTripTime;
ResponseCode = response.RCODE;
// Extract request information
if (request.Question.Count > 0)
{
DnsQuestionRecord query = request.Question[0];
Question = new DnsQuestion
{
QuestionName = query.Name,
QuestionType = query.Type,
QuestionClass = query.Class,
};
}
// Convert answer section into a simple string summary (comma-separated for multiple answers)
Answers = new List<DnsResourceRecord>(response.Answer.Count);
if (response.Answer.Count > 0)
{
Answers.AddRange(response.Answer.Select(record => new DnsResourceRecord
{
Name = record.Name,
RecordType = record.Type,
RecordClass = record.Class,
RecordTtl = record.TTL,
RecordData = record.RDATA.ToString(),
DnssecStatus = record.DnssecStatus,
}));
}
}
public class DnsQuestion
{
public string QuestionName { get; set; }
public DnsResourceRecordType QuestionType { get; set; }
public DnsClass QuestionClass { get; set; }
}
public class DnsResourceRecord
{
public string Name { get; set; }
public DnsResourceRecordType RecordType { get; set; }
public DnsClass RecordClass { get; set; }
public uint RecordTtl { get; set; }
public string RecordData { get; set; }
public DnssecStatus DnssecStatus { get; set; }
}
public override string ToString()
{
return JsonSerializer.Serialize(this, DnsLogSerializerOptions.Default);
}
// Custom DateTime converter to handle UTC serialization in ISO 8601 format
public class JsonDateTimeConverter : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string? dts = reader.GetString();
return dts == null ? DateTime.MinValue : DateTime.Parse(dts);
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
}
}
// Setup reusable options with a single instance
public static class DnsLogSerializerOptions
{
public static readonly JsonSerializerOptions Default = new JsonSerializerOptions
{
WriteIndented = false, // Newline delimited logs should not be multiline
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // Convert properties to camelCase
Converters = { new JsonStringEnumConverter(), new JsonDateTimeConverter() }, // Handle enums and DateTime conversion
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // For safe encoding
NumberHandling = JsonNumberHandling.Strict,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull // Ignore null values
};
}
}
}

View File

@@ -0,0 +1,53 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<Version>1.0</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Company>Technitium</Company>
<Product>Technitium DNS Server</Product>
<Authors>Zafer Balkan</Authors>
<AssemblyName>LogExporterApp</AssemblyName>
<RootNamespace>LogExporter</RootNamespace>
<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>
<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>
<Description>Allows exporting query logs to third party sinks. It supports exporting to File, HTTP endpoint, and Syslog (UDP, TCP, TLS, and Local protocols).</Description>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<OutputType>Library</OutputType>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Http" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.PeriodicBatching" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="4.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\DnsServerCore.ApplicationCommon\DnsServerCore.ApplicationCommon.csproj">
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Reference Include="TechnitiumLibrary.Net">
<HintPath>..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="TechnitiumLibrary">
<HintPath>..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<None Update="dnsApp.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,82 @@
/*
Technitium DNS Server
Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace LogExporter.Strategy
{
public sealed class ExportManager : IDisposable
{
#region variables
readonly ConcurrentDictionary<Type, IExportStrategy> _exportStrategies = new ConcurrentDictionary<Type, IExportStrategy>();
#endregion
#region IDisposable
public void Dispose()
{
foreach (KeyValuePair<Type, IExportStrategy> exportStrategy in _exportStrategies)
exportStrategy.Value.Dispose();
}
#endregion
#region public
public void AddStrategy(IExportStrategy strategy)
{
if (!_exportStrategies.TryAdd(strategy.GetType(), strategy))
throw new InvalidOperationException();
}
public void RemoveStrategy(Type type)
{
if (_exportStrategies.TryRemove(type, out IExportStrategy? existing))
existing?.Dispose();
}
public bool HasStrategy()
{
return !_exportStrategies.IsEmpty;
}
public async Task ImplementStrategyAsync(IReadOnlyList<LogEntry> logs)
{
List<Task> tasks = new List<Task>(_exportStrategies.Count);
foreach (KeyValuePair<Type, IExportStrategy> strategy in _exportStrategies)
{
tasks.Add(Task.Factory.StartNew(delegate (object? state)
{
return strategy.Value.ExportAsync(logs);
}, null, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current));
}
await Task.WhenAll(tasks);
}
#endregion
}
}

View File

@@ -0,0 +1,71 @@
/*
Technitium DNS Server
Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using Serilog;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace LogExporter.Strategy
{
public sealed class FileExportStrategy : IExportStrategy
{
#region variables
readonly Serilog.Core.Logger _sender;
bool _disposed;
#endregion
#region constructor
public FileExportStrategy(string filePath)
{
_sender = new LoggerConfiguration().WriteTo.File(filePath, outputTemplate: "{Message:lj}{NewLine}{Exception}").CreateLogger();
}
#endregion
#region IDisposable
public void Dispose()
{
if (!_disposed)
{
_sender.Dispose();
_disposed = true;
}
}
#endregion
#region public
public Task ExportAsync(IReadOnlyList<LogEntry> logs)
{
foreach (LogEntry logEntry in logs)
_sender.Information(logEntry.ToString());
return Task.CompletedTask;
}
#endregion
}
}

View File

@@ -0,0 +1,119 @@
/*
Technitium DNS Server
Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using Microsoft.Extensions.Configuration;
using Serilog;
using Serilog.Sinks.Http;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace LogExporter.Strategy
{
public sealed class HttpExportStrategy : IExportStrategy
{
#region variables
readonly Serilog.Core.Logger _sender;
bool _disposed;
#endregion
#region constructor
public HttpExportStrategy(string endpoint, Dictionary<string, string?>? headers = null)
{
IConfigurationRoot? configuration = null;
if (headers != null)
{
configuration = new ConfigurationBuilder()
.AddInMemoryCollection(headers)
.Build();
}
_sender = new LoggerConfiguration().WriteTo.Http(endpoint, null, httpClient: new CustomHttpClient(), configuration: configuration).Enrich.FromLogContext().CreateLogger();
}
#endregion
#region IDisposable
public void Dispose()
{
if (!_disposed)
{
_sender.Dispose();
_disposed = true;
}
}
#endregion
#region public
public Task ExportAsync(IReadOnlyList<LogEntry> logs)
{
foreach (LogEntry logEntry in logs)
_sender.Information(logEntry.ToString());
return Task.CompletedTask;
}
#endregion
public class CustomHttpClient : IHttpClient
{
readonly HttpClient _httpClient;
public CustomHttpClient()
{
_httpClient = new HttpClient();
}
public void Configure(IConfiguration configuration)
{
foreach (IConfigurationSection pair in configuration.GetChildren())
{
_httpClient.DefaultRequestHeaders.Add(pair.Key, pair.Value);
}
}
public void Dispose()
{
_httpClient?.Dispose();
GC.SuppressFinalize(this);
}
public async Task<HttpResponseMessage> PostAsync(string requestUri, Stream contentStream, CancellationToken cancellationToken)
{
StreamContent content = new StreamContent(contentStream);
content.Headers.Add("Content-Type", "application/json");
return await _httpClient
.PostAsync(requestUri, content, cancellationToken)
.ConfigureAwait(false);
}
}
}
}

View File

@@ -0,0 +1,33 @@
/*
Technitium DNS Server
Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace LogExporter.Strategy
{
/// <summary>
/// Strategy interface to decide the sinks for exporting the logs.
/// </summary>
public interface IExportStrategy: IDisposable
{
Task ExportAsync(IReadOnlyList<LogEntry> logs);
}
}

View File

@@ -0,0 +1,171 @@
/*
Technitium DNS Server
Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using Serilog;
using Serilog.Events;
using Serilog.Parsing;
using Serilog.Sinks.Syslog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LogExporter.Strategy
{
public sealed class SyslogExportStrategy : IExportStrategy
{
#region variables
const string _appName = "Technitium DNS Server";
const string _sdId = "meta";
const string DEFAUL_PROTOCOL = "udp";
const int DEFAULT_PORT = 514;
readonly Facility _facility = Facility.Local6;
readonly Rfc5424Formatter _formatter;
readonly Serilog.Core.Logger _sender;
bool _disposed;
#endregion
#region constructor
public SyslogExportStrategy(string address, int? port, string? protocol)
{
port ??= DEFAULT_PORT;
protocol ??= DEFAUL_PROTOCOL;
LoggerConfiguration conf = new LoggerConfiguration();
_sender = protocol.ToLowerInvariant() switch
{
"tls" => conf.WriteTo.TcpSyslog(address, port.Value, _appName, FramingType.OCTET_COUNTING, SyslogFormat.RFC5424, _facility, useTls: true).Enrich.FromLogContext().CreateLogger(),
"tcp" => conf.WriteTo.TcpSyslog(address, port.Value, _appName, FramingType.OCTET_COUNTING, SyslogFormat.RFC5424, _facility, useTls: false).Enrich.FromLogContext().CreateLogger(),
"udp" => conf.WriteTo.UdpSyslog(address, port.Value, _appName, SyslogFormat.RFC5424, _facility).Enrich.FromLogContext().CreateLogger(),
"local" => conf.WriteTo.LocalSyslog(_appName, _facility).Enrich.FromLogContext().CreateLogger(),
_ => throw new NotSupportedException("Syslog protocol is not supported: " + protocol),
};
_formatter = new Rfc5424Formatter(_facility, _appName, null, _sdId, Environment.MachineName);
}
#endregion
#region IDisposable
public void Dispose()
{
if (!_disposed)
{
_sender.Dispose();
_disposed = true;
}
}
#endregion
#region public
public Task ExportAsync(IReadOnlyList<LogEntry> logs)
{
foreach (LogEntry log in logs)
_sender.Information(_formatter.FormatMessage((LogEvent?)Convert(log)));
return Task.CompletedTask;
}
#endregion
#region private
private static LogEvent Convert(LogEntry log)
{
// Initialize properties with base log details
List<LogEventProperty> properties = new List<LogEventProperty>
{
new LogEventProperty("timestamp", new ScalarValue(log.Timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"))),
new LogEventProperty("clientIp", new ScalarValue(log.ClientIp)),
new LogEventProperty("protocol", new ScalarValue(log.Protocol.ToString())),
new LogEventProperty("responseType", new ScalarValue(log.ResponseType.ToString())),
new LogEventProperty("responseRtt", new ScalarValue(log.ResponseRtt?.ToString())),
new LogEventProperty("rCode", new ScalarValue(log.ResponseCode.ToString()))
};
// Add each question as properties
if (log.Question != null)
{
LogEntry.DnsQuestion question = log.Question;
properties.Add(new LogEventProperty("qName", new ScalarValue(question.QuestionName)));
properties.Add(new LogEventProperty("qType", new ScalarValue(question.QuestionType.ToString())));
properties.Add(new LogEventProperty("qClass", new ScalarValue(question.QuestionClass.ToString())));
string questionSummary = $"QNAME: {question.QuestionName}, QTYPE: {question.QuestionType.ToString()}, QCLASS: {question.QuestionClass.ToString()}";
properties.Add(new LogEventProperty("questionsSummary", new ScalarValue(questionSummary)));
}
else
{
properties.Add(new LogEventProperty("questionsSummary", new ScalarValue(string.Empty)));
}
// Add each answer as properties
if (log.Answers.Count > 0)
{
for (int i = 0; i < log.Answers.Count; i++)
{
LogEntry.DnsResourceRecord answer = log.Answers[i];
properties.Add(new LogEventProperty($"aName_{i}", new ScalarValue(answer.Name)));
properties.Add(new LogEventProperty($"aType_{i}", new ScalarValue(answer.RecordType.ToString())));
properties.Add(new LogEventProperty($"aClass_{i}", new ScalarValue(answer.RecordClass.ToString())));
properties.Add(new LogEventProperty($"aTtl_{i}", new ScalarValue(answer.RecordTtl.ToString())));
properties.Add(new LogEventProperty($"aRData_{i}", new ScalarValue(answer.RecordData)));
properties.Add(new LogEventProperty($"aDnssecStatus_{i}", new ScalarValue(answer.DnssecStatus.ToString())));
}
// Generate answers summary
string answerSummary = string.Join(", ", log.Answers.Select(a => a.RecordData));
properties.Add(new LogEventProperty("answersSummary", new ScalarValue(answerSummary)));
}
else
{
properties.Add(new LogEventProperty("answersSummary", new ScalarValue(string.Empty)));
}
// Define the message template to match the original summary format
const string templateText = "{questionsSummary}; RCODE: {rCode}; ANSWER: [{answersSummary}]";
// Parse the template
MessageTemplate template = new MessageTemplateParser().Parse(templateText);
// Create the LogEvent and return it
return new LogEvent(
timestamp: log.Timestamp,
level: LogEventLevel.Information,
exception: null,
messageTemplate: template,
properties: properties
);
}
#endregion
}
}

View File

@@ -0,0 +1,20 @@
{
"maxQueueSize": 1000000,
"file": {
"path": "./dns_logs.json",
"enabled": false
},
"http": {
"endpoint": "http://localhost:5000/logs",
"headers": {
"Authorization": "Bearer abc123"
},
"enabled": false
},
"syslog": {
"address": "127.0.0.1",
"port": 514,
"protocol": "UDP",
"enabled": false
}
}

View File

@@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DropRequestsApp", "Apps\Dro
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryLogsSqliteApp", "Apps\QueryLogsSqliteApp\QueryLogsSqliteApp.csproj", "{186DEF23-863E-4954-BE16-5E5FCA75ECA2}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryLogsSqliteApp", "Apps\QueryLogsSqliteApp\QueryLogsSqliteApp.csproj", "{186DEF23-863E-4954-BE16-5E5FCA75ECA2}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogExporterApp", "Apps\LogExporterApp\LogExporterApp.csproj", "{6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdvancedBlockingApp", "Apps\AdvancedBlockingApp\AdvancedBlockingApp.csproj", "{A4C31093-CA65-42D4-928A-11907076C0DE}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdvancedBlockingApp", "Apps\AdvancedBlockingApp\AdvancedBlockingApp.csproj", "{A4C31093-CA65-42D4-928A-11907076C0DE}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NxDomainApp", "Apps\NxDomainApp\NxDomainApp.csproj", "{BB0010FC-20E9-4397-BF9B-C9955D9AD339}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NxDomainApp", "Apps\NxDomainApp\NxDomainApp.csproj", "{BB0010FC-20E9-4397-BF9B-C9955D9AD339}"
@@ -61,6 +63,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DnsRebindingProtectionApp",
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FilterAaaaApp", "Apps\FilterAaaaApp\FilterAaaaApp.csproj", "{0A9B7F39-80DA-4084-AD47-8707576927ED}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FilterAaaaApp", "Apps\FilterAaaaApp\FilterAaaaApp.csproj", "{0A9B7F39-80DA-4084-AD47-8707576927ED}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3746EF13-91C5-4858-9DC2-D3C2504BD135}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -119,6 +126,10 @@ Global
{186DEF23-863E-4954-BE16-5E5FCA75ECA2}.Debug|Any CPU.Build.0 = Debug|Any CPU {186DEF23-863E-4954-BE16-5E5FCA75ECA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{186DEF23-863E-4954-BE16-5E5FCA75ECA2}.Release|Any CPU.ActiveCfg = Release|Any CPU {186DEF23-863E-4954-BE16-5E5FCA75ECA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{186DEF23-863E-4954-BE16-5E5FCA75ECA2}.Release|Any CPU.Build.0 = Release|Any CPU {186DEF23-863E-4954-BE16-5E5FCA75ECA2}.Release|Any CPU.Build.0 = Release|Any CPU
{6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}.Release|Any CPU.Build.0 = Release|Any CPU
{A4C31093-CA65-42D4-928A-11907076C0DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A4C31093-CA65-42D4-928A-11907076C0DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A4C31093-CA65-42D4-928A-11907076C0DE}.Debug|Any CPU.Build.0 = Debug|Any CPU {A4C31093-CA65-42D4-928A-11907076C0DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4C31093-CA65-42D4-928A-11907076C0DE}.Release|Any CPU.ActiveCfg = Release|Any CPU {A4C31093-CA65-42D4-928A-11907076C0DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -192,6 +203,7 @@ Global
{099D27AF-3AEB-495A-A5D0-46DA59CC9213} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {099D27AF-3AEB-495A-A5D0-46DA59CC9213} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
{738079D1-FA5A-40CD-8A27-D831919EE209} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {738079D1-FA5A-40CD-8A27-D831919EE209} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
{186DEF23-863E-4954-BE16-5E5FCA75ECA2} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {186DEF23-863E-4954-BE16-5E5FCA75ECA2} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
{6F9BCCA9-6422-484B-A065-EF8AF9DA74B5} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
{A4C31093-CA65-42D4-928A-11907076C0DE} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {A4C31093-CA65-42D4-928A-11907076C0DE} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
{BB0010FC-20E9-4397-BF9B-C9955D9AD339} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {BB0010FC-20E9-4397-BF9B-C9955D9AD339} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
{45C6F9AD-57D6-4D6D-9498-10B5C828E47E} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2} {45C6F9AD-57D6-4D6D-9498-10B5C828E47E} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}