LogExporterApp: fixed multiple issues with the app. Code refactoring changes done.

This commit is contained in:
Shreyas Zare
2025-01-18 13:02:31 +05:30
parent c0ae6dbf35
commit 7bd0b7ca6b
10 changed files with 294 additions and 319 deletions

View File

@@ -1,6 +1,6 @@
/* /*
Technitium DNS Server Technitium DNS Server
Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com) Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -25,8 +25,8 @@ using System.Collections.Generic;
using System.Net; using System.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using TechnitiumLibrary;
using TechnitiumLibrary.Net.Dns; using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
namespace LogExporter namespace LogExporter
{ {
@@ -34,63 +34,57 @@ namespace LogExporter
{ {
#region variables #region variables
private const int BULK_INSERT_COUNT = 1000; IDnsServer? _dnsServer;
BufferManagementConfig? _config;
private const int DEFAULT_QUEUE_CAPACITY = 1000; readonly ExportManager _exportManager = new ExportManager();
private const int QUEUE_TIMER_INTERVAL = 10000; bool _enableLogging;
private readonly IReadOnlyList<DnsLogEntry> _emptyList = []; readonly ConcurrentQueue<LogEntry> _queuedLogs = new ConcurrentQueue<LogEntry>();
readonly Timer _queueTimer;
const int QUEUE_TIMER_INTERVAL = 10000;
const int BULK_INSERT_COUNT = 1000;
private readonly ExportManager _exportManager = new ExportManager(); bool _disposed;
private BufferManagementConfig? _config; #endregion
private IDnsServer _dnsServer;
private BlockingCollection<LogEntry> _logBuffer;
private Timer _queueTimer;
private bool disposedValue;
#endregion variables
#region constructor #region constructor
public App() public App()
{ {
_queueTimer = new Timer(HandleExportLogCallback);
} }
#endregion constructor #endregion
#region IDisposable #region IDisposable
public void Dispose() public void Dispose()
{ {
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true); Dispose(disposing: true);
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
private void Dispose(bool disposing) private void Dispose(bool disposing)
{ {
if (!disposedValue) if (!_disposed)
{ {
if (disposing) if (disposing)
{ {
_queueTimer?.Dispose(); _queueTimer?.Dispose();
ExportLogs(); //flush any pending logs ExportLogsAsync().Sync(); //flush any pending logs
_logBuffer.Dispose(); _exportManager.Dispose();
} }
disposedValue = true; _disposed = true;
} }
} }
#endregion IDisposable #endregion
#region public #region public
@@ -99,66 +93,97 @@ namespace LogExporter
_dnsServer = dnsServer; _dnsServer = dnsServer;
_config = BufferManagementConfig.Deserialize(config); _config = BufferManagementConfig.Deserialize(config);
if (_config == null) if (_config is null)
{
throw new DnsClientException("Invalid application configuration."); throw new DnsClientException("Invalid application configuration.");
}
if (_config.MaxLogEntries != null) if (_config.FileTarget!.Enabled)
{ {
_logBuffer = new BlockingCollection<LogEntry>(_config.MaxLogEntries.Value); _exportManager.RemoveStrategy(typeof(FileExportStrategy));
_exportManager.AddStrategy(new FileExportStrategy(_config.FileTarget!.Path));
} }
else else
{ {
_logBuffer = new BlockingCollection<LogEntry>(DEFAULT_QUEUE_CAPACITY); _exportManager.RemoveStrategy(typeof(FileExportStrategy));
} }
RegisterExportTargets(); if (_config.HttpTarget!.Enabled)
if (_exportManager.HasStrategy())
{ {
_queueTimer = new Timer(HandleExportLogCallback, state: null, QUEUE_TIMER_INTERVAL, Timeout.Infinite); _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; return Task.CompletedTask;
} }
public Task InsertLogAsync(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) public Task InsertLogAsync(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)
{ {
_logBuffer.Add(new LogEntry(timestamp, remoteEP, protocol, request, response)); if (_enableLogging)
{
if (_queuedLogs.Count < _config!.MaxQueueSize)
_queuedLogs.Enqueue(new LogEntry(timestamp, remoteEP, protocol, request, response));
}
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task<DnsLogPage> QueryLogsAsync(long pageNumber, int entriesPerPage, bool descendingOrder, DateTime? start, DateTime? end, IPAddress clientIpAddress, DnsTransportProtocol? protocol, DnsServerResponseType? responseType, DnsResponseCode? rcode, string qname, DnsResourceRecordType? qtype, DnsClass? qclass) #endregion
{
return Task.FromResult(new DnsLogPage(0, 0, 0, _emptyList));
}
#endregion public
#region private #region private
private void ExportLogs() private async Task ExportLogsAsync()
{
var logs = new List<LogEntry>(BULK_INSERT_COUNT);
// Process logs within the timer interval, then let the timer reschedule
while (logs.Count <= BULK_INSERT_COUNT && _logBuffer.TryTake(out var log))
{
logs.Add(log);
}
// If we have any logs to process, export them
if (logs.Count > 0)
{
_exportManager.ImplementStrategy(logs);
}
}
private void HandleExportLogCallback(object? state)
{ {
try try
{ {
ExportLogs(); 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) catch (Exception ex)
{ {
@@ -168,42 +193,22 @@ namespace LogExporter
{ {
try try
{ {
_queueTimer.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite); _queueTimer?.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite);
} }
catch (ObjectDisposedException) catch (ObjectDisposedException)
{ } { }
} }
} }
private void RegisterExportTargets() #endregion
{
// Helper function to register an export strategy if the target is enabled
void RegisterIfEnabled<TTarget, TStrategy>(TTarget target, Func<TTarget, TStrategy> strategyFactory)
where TTarget : TargetBase
where TStrategy : IExportStrategy
{
if (target?.Enabled == true)
{
var strategy = strategyFactory(target);
_exportManager.AddOrReplaceStrategy(strategy);
}
}
// Register the different strategies using the helper
RegisterIfEnabled(_config!.FileTarget!, target => new FileExportStrategy(target.Path));
RegisterIfEnabled(_config!.HttpTarget!, target => new HttpExportStrategy(target.Endpoint, target.Headers));
RegisterIfEnabled(_config!.SyslogTarget!, target => new SyslogExportStrategy(target.Address, target.Port, target.Protocol));
}
#endregion private
#region properties #region properties
public string Description public string Description
{ {
get { return "The app allows exporting logs to a third party sink using an internal buffer."; } 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 properties #endregion
} }
} }

View File

@@ -1,6 +1,6 @@
/* /*
Technitium DNS Server Technitium DNS Server
Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com) Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -25,8 +25,9 @@ namespace LogExporter
{ {
public class BufferManagementConfig public class BufferManagementConfig
{ {
[JsonPropertyName("maxLogEntries")] [JsonPropertyName("maxQueueSize")]
public int? MaxLogEntries { get; set; } public int MaxQueueSize
{ get; set; }
[JsonPropertyName("file")] [JsonPropertyName("file")]
public FileTarget? FileTarget { get; set; } public FileTarget? FileTarget { get; set; }
@@ -74,7 +75,7 @@ namespace LogExporter
public string Endpoint { get; set; } public string Endpoint { get; set; }
[JsonPropertyName("headers")] [JsonPropertyName("headers")]
public Dictionary<string, string>? Headers { get; set; } public Dictionary<string, string?>? Headers { get; set; }
} }
// Setup reusable options with a single instance // Setup reusable options with a single instance

View File

@@ -1,4 +1,24 @@
using System; /*
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.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@@ -11,16 +31,14 @@ namespace LogExporter
{ {
public class LogEntry public class LogEntry
{ {
public DateTime Timestamp { get; set; } public DateTime Timestamp { get; private set; }
public string ClientIp { get; set; } public string ClientIp { get; private set; }
public int ClientPort { get; set; } public DnsTransportProtocol Protocol { get; private set; }
public bool DnssecOk { get; set; } public DnsServerResponseType ResponseType { get; private set; }
public DnsTransportProtocol Protocol { get; set; } public double? ResponseRtt { get; private set; }
public DnsResponseCode ResponseCode { get; set; } public DnsResponseCode ResponseCode { get; private set; }
public List<Question> Questions { get; set; } public DnsQuestion? Question { get; private set; }
public List<Answer> Answers { get; set; } public List<DnsResourceRecord> Answers { get; private set; }
public object? RequestTag { get; set; }
public object? ResponseTag { get; set; }
public LogEntry(DateTime timestamp, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram request, DnsDatagram response) public LogEntry(DateTime timestamp, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram request, DnsDatagram response)
{ {
@@ -29,65 +47,57 @@ namespace LogExporter
// Extract client information // Extract client information
ClientIp = remoteEP.Address.ToString(); ClientIp = remoteEP.Address.ToString();
ClientPort = remoteEP.Port;
DnssecOk = request.DnssecOk;
Protocol = protocol; 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; ResponseCode = response.RCODE;
// Extract request information // Extract request information
Questions = new List<Question>(request.Question.Count); if (request.Question.Count > 0)
if (request.Question?.Count > 0)
{ {
Questions.AddRange(request.Question.Select(questionRecord => new Question DnsQuestionRecord query = request.Question[0];
Question = new DnsQuestion
{ {
QuestionName = questionRecord.Name, QuestionName = query.Name,
QuestionType = questionRecord.Type, QuestionType = query.Type,
QuestionClass = questionRecord.Class, QuestionClass = query.Class,
Size = questionRecord.UncompressedLength, };
}));
} }
// Convert answer section into a simple string summary (comma-separated for multiple answers) // Convert answer section into a simple string summary (comma-separated for multiple answers)
Answers = new List<Answer>(response.Answer.Count); Answers = new List<DnsResourceRecord>(response.Answer.Count);
if (response.Answer?.Count > 0) if (response.Answer.Count > 0)
{ {
Answers.AddRange(response.Answer.Select(record => new Answer Answers.AddRange(response.Answer.Select(record => new DnsResourceRecord
{ {
Name = record.Name,
RecordType = record.Type, RecordType = record.Type,
RecordData = record.RDATA.ToString(),
RecordClass = record.Class, RecordClass = record.Class,
RecordTtl = record.TTL, RecordTtl = record.TTL,
Size = record.UncompressedLength, RecordData = record.RDATA.ToString(),
DnssecStatus = record.DnssecStatus, DnssecStatus = record.DnssecStatus,
})); }));
} }
if (request.Tag != null)
{
RequestTag = request.Tag;
}
if (response.Tag != null)
{
ResponseTag = response.Tag;
}
} }
public class Question public class DnsQuestion
{ {
public string QuestionName { get; set; } public string QuestionName { get; set; }
public DnsResourceRecordType? QuestionType { get; set; } public DnsResourceRecordType QuestionType { get; set; }
public DnsClass? QuestionClass { get; set; } public DnsClass QuestionClass { get; set; }
public int Size { get; set; }
} }
public class Answer public class DnsResourceRecord
{ {
public string Name { get; set; }
public DnsResourceRecordType RecordType { get; set; } public DnsResourceRecordType RecordType { get; set; }
public string RecordData { get; set; }
public DnsClass RecordClass { get; set; } public DnsClass RecordClass { get; set; }
public uint RecordTtl { get; set; } public uint RecordTtl { get; set; }
public int Size { get; set; } public string RecordData { get; set; }
public DnssecStatus DnssecStatus { get; set; } public DnssecStatus DnssecStatus { get; set; }
} }
@@ -101,7 +111,7 @@ namespace LogExporter
{ {
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
var dts = reader.GetString(); string? dts = reader.GetString();
return dts == null ? DateTime.MinValue : DateTime.Parse(dts); return dts == null ? DateTime.MinValue : DateTime.Parse(dts);
} }

View File

@@ -13,7 +13,7 @@
<RootNamespace>LogExporter</RootNamespace> <RootNamespace>LogExporter</RootNamespace>
<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl> <PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>
<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl> <RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>
<Description>The app allows exporting logs to a third party sink using an internal buffer.</Description> <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> <GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>

View File

@@ -1,6 +1,6 @@
/* /*
Technitium DNS Server Technitium DNS Server
Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com) Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -18,48 +18,65 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace LogExporter.Strategy namespace LogExporter.Strategy
{ {
public class ExportManager public sealed class ExportManager : IDisposable
{ {
#region variables #region variables
private readonly Dictionary<Type, IExportStrategy> _exportStrategies; readonly ConcurrentDictionary<Type, IExportStrategy> _exportStrategies = new ConcurrentDictionary<Type, IExportStrategy>();
#endregion variables #endregion
#region constructor #region IDisposable
public ExportManager() public void Dispose()
{ {
_exportStrategies = new Dictionary<Type, IExportStrategy>(); foreach (KeyValuePair<Type, IExportStrategy> exportStrategy in _exportStrategies)
exportStrategy.Value.Dispose();
} }
#endregion constructor #endregion
#region public #region public
public void AddOrReplaceStrategy(IExportStrategy strategy) public void AddStrategy(IExportStrategy strategy)
{ {
_exportStrategies[strategy.GetType()] = 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() public bool HasStrategy()
{ {
return _exportStrategies.Count > 0; return !_exportStrategies.IsEmpty;
} }
public async Task ImplementStrategy(List<LogEntry> logs) public async Task ImplementStrategyAsync(IReadOnlyList<LogEntry> logs)
{ {
foreach (var strategy in _exportStrategies.Values) List<Task> tasks = new List<Task>(_exportStrategies.Count);
foreach (KeyValuePair<Type, IExportStrategy> strategy in _exportStrategies)
{ {
await strategy.ExportAsync(logs).ConfigureAwait(false); 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 public #endregion
} }
} }

View File

@@ -1,6 +1,6 @@
/* /*
Technitium DNS Server Technitium DNS Server
Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com) Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -23,15 +23,15 @@ using System.Threading.Tasks;
namespace LogExporter.Strategy namespace LogExporter.Strategy
{ {
public class FileExportStrategy : IExportStrategy public sealed class FileExportStrategy : IExportStrategy
{ {
#region variables #region variables
private readonly Serilog.Core.Logger _sender; readonly Serilog.Core.Logger _sender;
private bool disposedValue; bool _disposed;
#endregion variables #endregion
#region constructor #region constructor
@@ -40,45 +40,32 @@ namespace LogExporter.Strategy
_sender = new LoggerConfiguration().WriteTo.File(filePath, outputTemplate: "{Message:lj}{NewLine}{Exception}").CreateLogger(); _sender = new LoggerConfiguration().WriteTo.File(filePath, outputTemplate: "{Message:lj}{NewLine}{Exception}").CreateLogger();
} }
#endregion constructor #endregion
#region public
public Task ExportAsync(List<LogEntry> logs)
{
return Task.Run(() =>
{
foreach (LogEntry logEntry in logs)
{
_sender.Information(logEntry.ToString());
}
});
}
#endregion public
#region IDisposable #region IDisposable
public void Dispose() public void Dispose()
{ {
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method if (!_disposed)
Dispose(disposing: true);
System.GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{ {
if (disposing) _sender.Dispose();
{
_sender.Dispose();
}
disposedValue = true; _disposed = true;
} }
} }
#endregion IDisposable #endregion
#region public
public Task ExportAsync(IReadOnlyList<LogEntry> logs)
{
foreach (LogEntry logEntry in logs)
_sender.Information(logEntry.ToString());
return Task.CompletedTask;
}
#endregion
} }
} }

View File

@@ -1,6 +1,6 @@
/* /*
Technitium DNS Server Technitium DNS Server
Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com) Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -29,19 +29,19 @@ using System.Threading.Tasks;
namespace LogExporter.Strategy namespace LogExporter.Strategy
{ {
public class HttpExportStrategy : IExportStrategy public sealed class HttpExportStrategy : IExportStrategy
{ {
#region variables #region variables
private readonly Serilog.Core.Logger _sender; readonly Serilog.Core.Logger _sender;
private bool disposedValue; bool _disposed;
#endregion variables #endregion
#region constructor #region constructor
public HttpExportStrategy(string endpoint, Dictionary<string, string>? headers = null) public HttpExportStrategy(string endpoint, Dictionary<string, string?>? headers = null)
{ {
IConfigurationRoot? configuration = null; IConfigurationRoot? configuration = null;
if (headers != null) if (headers != null)
@@ -54,79 +54,66 @@ namespace LogExporter.Strategy
_sender = new LoggerConfiguration().WriteTo.Http(endpoint, null, httpClient: new CustomHttpClient(), configuration: configuration).Enrich.FromLogContext().CreateLogger(); _sender = new LoggerConfiguration().WriteTo.Http(endpoint, null, httpClient: new CustomHttpClient(), configuration: configuration).Enrich.FromLogContext().CreateLogger();
} }
#endregion constructor #endregion
#region public
public Task ExportAsync(List<LogEntry> logs)
{
return Task.Run(() =>
{
foreach (LogEntry logEntry in logs)
{
_sender.Information(logEntry.ToString());
}
});
}
#endregion public
#region IDisposable #region IDisposable
public void Dispose() public void Dispose()
{ {
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method if (!_disposed)
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{ {
if (disposing) _sender.Dispose();
{
_sender.Dispose();
}
disposedValue = true; _disposed = true;
} }
} }
#endregion IDisposable #endregion
#region Classes #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 public class CustomHttpClient : IHttpClient
{ {
private readonly HttpClient httpClient; readonly HttpClient _httpClient;
public CustomHttpClient() => httpClient = new HttpClient(); public CustomHttpClient()
{
_httpClient = new HttpClient();
}
public void Configure(IConfiguration configuration) public void Configure(IConfiguration configuration)
{ {
foreach (var pair in configuration.GetChildren()) foreach (IConfigurationSection pair in configuration.GetChildren())
{ {
httpClient.DefaultRequestHeaders.Add(pair.Key, pair.Value); _httpClient.DefaultRequestHeaders.Add(pair.Key, pair.Value);
} }
} }
public void Dispose() public void Dispose()
{ {
httpClient?.Dispose(); _httpClient?.Dispose();
GC.SuppressFinalize(this);
} }
public async Task<HttpResponseMessage> PostAsync(string requestUri, Stream contentStream, CancellationToken cancellationToken) public async Task<HttpResponseMessage> PostAsync(string requestUri, Stream contentStream, CancellationToken cancellationToken)
{ {
using var content = new StreamContent(contentStream); StreamContent content = new StreamContent(contentStream);
content.Headers.Add("Content-Type", "application/json"); content.Headers.Add("Content-Type", "application/json");
return await httpClient return await _httpClient
.PostAsync(requestUri, content, cancellationToken) .PostAsync(requestUri, content, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
} }
#endregion Classes
} }
} }

View File

@@ -1,6 +1,6 @@
/* /*
Technitium DNS Server Technitium DNS Server
Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com) Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -24,10 +24,10 @@ using System.Threading.Tasks;
namespace LogExporter.Strategy namespace LogExporter.Strategy
{ {
/// <summary> /// <summary>
/// Strategu interface to decide the sinks for exporting the logs. /// Strategy interface to decide the sinks for exporting the logs.
/// </summary> /// </summary>
public interface IExportStrategy: IDisposable public interface IExportStrategy: IDisposable
{ {
Task ExportAsync(List<LogEntry> logs); Task ExportAsync(IReadOnlyList<LogEntry> logs);
} }
} }

View File

@@ -1,6 +1,6 @@
/* /*
Technitium DNS Server Technitium DNS Server
Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com) Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -28,27 +28,23 @@ using System.Threading.Tasks;
namespace LogExporter.Strategy namespace LogExporter.Strategy
{ {
public class SyslogExportStrategy : IExportStrategy public sealed class SyslogExportStrategy : IExportStrategy
{ {
#region variables #region variables
private const string _appName = "Technitium DNS Server"; const string _appName = "Technitium DNS Server";
const string _sdId = "meta";
const string DEFAUL_PROTOCOL = "udp";
const int DEFAULT_PORT = 514;
private const string _sdId = "meta"; readonly Facility _facility = Facility.Local6;
private const string DEFAUL_PROTOCOL = "udp"; readonly Rfc5424Formatter _formatter;
readonly Serilog.Core.Logger _sender;
private const int DEFAULT_PORT = 514; bool _disposed;
private readonly Facility _facility = Facility.Local6; #endregion
private readonly Rfc5424Formatter _formatter;
private readonly Serilog.Core.Logger _sender;
private bool disposedValue;
#endregion variables
#region constructor #region constructor
@@ -57,7 +53,7 @@ namespace LogExporter.Strategy
port ??= DEFAULT_PORT; port ??= DEFAULT_PORT;
protocol ??= DEFAUL_PROTOCOL; protocol ??= DEFAUL_PROTOCOL;
var conf = new LoggerConfiguration(); LoggerConfiguration conf = new LoggerConfiguration();
_sender = protocol.ToLowerInvariant() switch _sender = protocol.ToLowerInvariant() switch
{ {
@@ -65,82 +61,64 @@ namespace LogExporter.Strategy
"tcp" => conf.WriteTo.TcpSyslog(address, port.Value, _appName, FramingType.OCTET_COUNTING, SyslogFormat.RFC5424, _facility, useTls: false).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(), "udp" => conf.WriteTo.UdpSyslog(address, port.Value, _appName, SyslogFormat.RFC5424, _facility).Enrich.FromLogContext().CreateLogger(),
"local" => conf.WriteTo.LocalSyslog(_appName, _facility).Enrich.FromLogContext().CreateLogger(), "local" => conf.WriteTo.LocalSyslog(_appName, _facility).Enrich.FromLogContext().CreateLogger(),
_ => throw new Exception("Invalid protocol specified"), _ => throw new NotSupportedException("Syslog protocol is not supported: " + protocol),
}; };
_formatter = new Rfc5424Formatter(_facility, _appName, null, _sdId, Environment.MachineName); _formatter = new Rfc5424Formatter(_facility, _appName, null, _sdId, Environment.MachineName);
} }
#endregion constructor #endregion
#region public
public Task ExportAsync(List<LogEntry> logs)
{
return Task.Run(() =>
{
foreach (var log in logs)
{
_sender.Information((string?)_formatter.FormatMessage((LogEvent?)Convert(log)));
}
});
}
#endregion public
#region IDisposable #region IDisposable
public void Dispose() public void Dispose()
{ {
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method if (!_disposed)
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{ {
if (disposing) _sender.Dispose();
{
_sender.Dispose();
}
disposedValue = true; _disposed = true;
} }
} }
#endregion IDisposable #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 #region private
private LogEvent Convert(LogEntry log) private static LogEvent Convert(LogEntry log)
{ {
// Initialize properties with base log details // Initialize properties with base log details
var properties = new List<LogEventProperty> List<LogEventProperty> properties = new List<LogEventProperty>
{ {
new LogEventProperty("timestamp", new ScalarValue(log.Timestamp.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"))), new LogEventProperty("timestamp", new ScalarValue(log.Timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"))),
new LogEventProperty("clientIp", new ScalarValue(log.ClientIp)), new LogEventProperty("clientIp", new ScalarValue(log.ClientIp)),
new LogEventProperty("clientPort", new ScalarValue(log.ClientPort.ToString())),
new LogEventProperty("dnssecOk", new ScalarValue(log.DnssecOk.ToString())),
new LogEventProperty("protocol", new ScalarValue(log.Protocol.ToString())), 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())) new LogEventProperty("rCode", new ScalarValue(log.ResponseCode.ToString()))
}; };
// Add each question as properties // Add each question as properties
if (log.Questions?.Count > 0) if (log.Question != null)
{ {
for (int i = 0; i < log.Questions.Count; i++) LogEntry.DnsQuestion question = log.Question;
{ properties.Add(new LogEventProperty("qName", new ScalarValue(question.QuestionName)));
var question = log.Questions[i]; properties.Add(new LogEventProperty("qType", new ScalarValue(question.QuestionType.ToString())));
properties.Add(new LogEventProperty($"qName_{i}", new ScalarValue(question.QuestionName))); properties.Add(new LogEventProperty("qClass", new ScalarValue(question.QuestionClass.ToString())));
properties.Add(new LogEventProperty($"qType_{i}", new ScalarValue(question.QuestionType?.ToString() ?? "unknown")));
properties.Add(new LogEventProperty($"qClass_{i}", new ScalarValue(question.QuestionClass?.ToString() ?? "unknown")));
properties.Add(new LogEventProperty($"qSize_{i}", new ScalarValue(question.Size.ToString())));
}
// Generate questions summary string questionSummary = $"QNAME: {question.QuestionName}, QTYPE: {question.QuestionType.ToString()}, QCLASS: {question.QuestionClass.ToString()}";
var questionSummary = string.Join("; ", log.Questions.Select((q, i) =>
$"QNAME_{i}: {q.QuestionName}, QTYPE: {q.QuestionType?.ToString() ?? "unknown"}, QCLASS: {q.QuestionClass?.ToString() ?? "unknown"}"));
properties.Add(new LogEventProperty("questionsSummary", new ScalarValue(questionSummary))); properties.Add(new LogEventProperty("questionsSummary", new ScalarValue(questionSummary)));
} }
else else
@@ -149,21 +127,22 @@ namespace LogExporter.Strategy
} }
// Add each answer as properties // Add each answer as properties
if (log.Answers?.Count > 0) if (log.Answers.Count > 0)
{ {
for (int i = 0; i < log.Answers.Count; i++) for (int i = 0; i < log.Answers.Count; i++)
{ {
var answer = log.Answers[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($"aType_{i}", new ScalarValue(answer.RecordType.ToString())));
properties.Add(new LogEventProperty($"aData_{i}", new ScalarValue(answer.RecordData)));
properties.Add(new LogEventProperty($"aClass_{i}", new ScalarValue(answer.RecordClass.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($"aTtl_{i}", new ScalarValue(answer.RecordTtl.ToString())));
properties.Add(new LogEventProperty($"aSize_{i}", new ScalarValue(answer.Size.ToString()))); properties.Add(new LogEventProperty($"aRData_{i}", new ScalarValue(answer.RecordData)));
properties.Add(new LogEventProperty($"aDnssecStatus_{i}", new ScalarValue(answer.DnssecStatus.ToString()))); properties.Add(new LogEventProperty($"aDnssecStatus_{i}", new ScalarValue(answer.DnssecStatus.ToString())));
} }
// Generate answers summary // Generate answers summary
var answerSummary = string.Join(", ", log.Answers.Select(a => a.RecordData)); string answerSummary = string.Join(", ", log.Answers.Select(a => a.RecordData));
properties.Add(new LogEventProperty("answersSummary", new ScalarValue(answerSummary))); properties.Add(new LogEventProperty("answersSummary", new ScalarValue(answerSummary)));
} }
else else
@@ -171,22 +150,11 @@ namespace LogExporter.Strategy
properties.Add(new LogEventProperty("answersSummary", new ScalarValue(string.Empty))); properties.Add(new LogEventProperty("answersSummary", new ScalarValue(string.Empty)));
} }
// Add request and response tags if present
if (log.RequestTag != null)
{
properties.Add(new LogEventProperty("requestTag", new ScalarValue(log.RequestTag.ToString())));
}
if (log.ResponseTag != null)
{
properties.Add(new LogEventProperty("responseTag", new ScalarValue(log.ResponseTag.ToString())));
}
// Define the message template to match the original summary format // Define the message template to match the original summary format
const string templateText = "{questionsSummary}; RCODE: {rCode}; ANSWER: [{answersSummary}]"; const string templateText = "{questionsSummary}; RCODE: {rCode}; ANSWER: [{answersSummary}]";
// Parse the template // Parse the template
var template = new MessageTemplateParser().Parse(templateText); MessageTemplate template = new MessageTemplateParser().Parse(templateText);
// Create the LogEvent and return it // Create the LogEvent and return it
return new LogEvent( return new LogEvent(
@@ -198,6 +166,6 @@ namespace LogExporter.Strategy
); );
} }
#endregion private #endregion
} }
} }

View File

@@ -1,5 +1,5 @@
{ {
"maxLogEntries": 1000, "maxQueueSize": 1000000,
"file": { "file": {
"path": "./dns_logs.json", "path": "./dns_logs.json",
"enabled": false "enabled": false