mirror of
https://github.com/fergalmoran/DnsServer.git
synced 2025-12-22 09:29:50 +00:00
LogExporterApp: fixed multiple issues with the app. Code refactoring changes done.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
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
|
||||
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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TechnitiumLibrary;
|
||||
using TechnitiumLibrary.Net.Dns;
|
||||
using TechnitiumLibrary.Net.Dns.ResourceRecords;
|
||||
|
||||
namespace LogExporter
|
||||
{
|
||||
@@ -34,63 +34,57 @@ namespace LogExporter
|
||||
{
|
||||
#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;
|
||||
|
||||
private IDnsServer _dnsServer;
|
||||
|
||||
private BlockingCollection<LogEntry> _logBuffer;
|
||||
|
||||
private Timer _queueTimer;
|
||||
|
||||
private bool disposedValue;
|
||||
|
||||
#endregion variables
|
||||
#endregion
|
||||
|
||||
#region constructor
|
||||
|
||||
public App()
|
||||
{
|
||||
_queueTimer = new Timer(HandleExportLogCallback);
|
||||
}
|
||||
|
||||
#endregion constructor
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_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
|
||||
|
||||
@@ -99,66 +93,97 @@ namespace LogExporter
|
||||
_dnsServer = dnsServer;
|
||||
_config = BufferManagementConfig.Deserialize(config);
|
||||
|
||||
if (_config == null)
|
||||
{
|
||||
if (_config is null)
|
||||
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
|
||||
{
|
||||
_logBuffer = new BlockingCollection<LogEntry>(DEFAULT_QUEUE_CAPACITY);
|
||||
_exportManager.RemoveStrategy(typeof(FileExportStrategy));
|
||||
}
|
||||
|
||||
RegisterExportTargets();
|
||||
if (_exportManager.HasStrategy())
|
||||
if (_config.HttpTarget!.Enabled)
|
||||
{
|
||||
_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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return Task.FromResult(new DnsLogPage(0, 0, 0, _emptyList));
|
||||
}
|
||||
|
||||
#endregion public
|
||||
#endregion
|
||||
|
||||
#region private
|
||||
|
||||
private void ExportLogs()
|
||||
{
|
||||
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)
|
||||
private async Task ExportLogsAsync()
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -168,42 +193,22 @@ namespace LogExporter
|
||||
{
|
||||
try
|
||||
{
|
||||
_queueTimer.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite);
|
||||
_queueTimer?.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterExportTargets()
|
||||
{
|
||||
// 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
|
||||
#endregion
|
||||
|
||||
#region properties
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -25,8 +25,9 @@ namespace LogExporter
|
||||
{
|
||||
public class BufferManagementConfig
|
||||
{
|
||||
[JsonPropertyName("maxLogEntries")]
|
||||
public int? MaxLogEntries { get; set; }
|
||||
[JsonPropertyName("maxQueueSize")]
|
||||
public int MaxQueueSize
|
||||
{ get; set; }
|
||||
|
||||
[JsonPropertyName("file")]
|
||||
public FileTarget? FileTarget { get; set; }
|
||||
@@ -74,7 +75,7 @@ namespace LogExporter
|
||||
public string Endpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("headers")]
|
||||
public Dictionary<string, string>? Headers { get; set; }
|
||||
public Dictionary<string, string?>? Headers { get; set; }
|
||||
}
|
||||
|
||||
// Setup reusable options with a single instance
|
||||
|
||||
@@ -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.Linq;
|
||||
using System.Net;
|
||||
@@ -11,16 +31,14 @@ namespace LogExporter
|
||||
{
|
||||
public class LogEntry
|
||||
{
|
||||
public DateTime Timestamp { get; set; }
|
||||
public string ClientIp { get; set; }
|
||||
public int ClientPort { get; set; }
|
||||
public bool DnssecOk { get; set; }
|
||||
public DnsTransportProtocol Protocol { get; set; }
|
||||
public DnsResponseCode ResponseCode { get; set; }
|
||||
public List<Question> Questions { get; set; }
|
||||
public List<Answer> Answers { get; set; }
|
||||
public object? RequestTag { get; set; }
|
||||
public object? ResponseTag { get; set; }
|
||||
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)
|
||||
{
|
||||
@@ -29,65 +47,57 @@ namespace LogExporter
|
||||
|
||||
// Extract client information
|
||||
ClientIp = remoteEP.Address.ToString();
|
||||
ClientPort = remoteEP.Port;
|
||||
DnssecOk = request.DnssecOk;
|
||||
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
|
||||
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,
|
||||
QuestionType = questionRecord.Type,
|
||||
QuestionClass = questionRecord.Class,
|
||||
Size = questionRecord.UncompressedLength,
|
||||
}));
|
||||
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<Answer>(response.Answer.Count);
|
||||
if (response.Answer?.Count > 0)
|
||||
Answers = new List<DnsResourceRecord>(response.Answer.Count);
|
||||
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,
|
||||
RecordData = record.RDATA.ToString(),
|
||||
RecordClass = record.Class,
|
||||
RecordTtl = record.TTL,
|
||||
Size = record.UncompressedLength,
|
||||
RecordData = record.RDATA.ToString(),
|
||||
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 DnsResourceRecordType? QuestionType { get; set; }
|
||||
public DnsClass? QuestionClass { get; set; }
|
||||
public int Size { get; set; }
|
||||
public DnsResourceRecordType QuestionType { get; set; }
|
||||
public DnsClass QuestionClass { get; set; }
|
||||
}
|
||||
|
||||
public class Answer
|
||||
public class DnsResourceRecord
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public DnsResourceRecordType RecordType { get; set; }
|
||||
public string RecordData { get; set; }
|
||||
public DnsClass RecordClass { get; set; }
|
||||
public uint RecordTtl { get; set; }
|
||||
public int Size { get; set; }
|
||||
public string RecordData { get; set; }
|
||||
public DnssecStatus DnssecStatus { get; set; }
|
||||
}
|
||||
|
||||
@@ -101,7 +111,7 @@ namespace LogExporter
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<RootNamespace>LogExporter</RootNamespace>
|
||||
<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>
|
||||
<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>
|
||||
<OutputType>Library</OutputType>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
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
|
||||
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.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LogExporter.Strategy
|
||||
{
|
||||
public class ExportManager
|
||||
public sealed class ExportManager : IDisposable
|
||||
{
|
||||
#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
|
||||
|
||||
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()
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -23,15 +23,15 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace LogExporter.Strategy
|
||||
{
|
||||
public class FileExportStrategy : IExportStrategy
|
||||
public sealed class FileExportStrategy : IExportStrategy
|
||||
{
|
||||
#region variables
|
||||
|
||||
private readonly Serilog.Core.Logger _sender;
|
||||
readonly Serilog.Core.Logger _sender;
|
||||
|
||||
private bool disposedValue;
|
||||
bool _disposed;
|
||||
|
||||
#endregion variables
|
||||
#endregion
|
||||
|
||||
#region constructor
|
||||
|
||||
@@ -40,45 +40,32 @@ namespace LogExporter.Strategy
|
||||
_sender = new LoggerConfiguration().WriteTo.File(filePath, outputTemplate: "{Message:lj}{NewLine}{Exception}").CreateLogger();
|
||||
}
|
||||
|
||||
#endregion constructor
|
||||
|
||||
#region public
|
||||
|
||||
public Task ExportAsync(List<LogEntry> logs)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
foreach (LogEntry logEntry in logs)
|
||||
{
|
||||
_sender.Information(logEntry.ToString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#endregion public
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
System.GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
if (!_disposed)
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -29,19 +29,19 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace LogExporter.Strategy
|
||||
{
|
||||
public class HttpExportStrategy : IExportStrategy
|
||||
public sealed class HttpExportStrategy : IExportStrategy
|
||||
{
|
||||
#region variables
|
||||
|
||||
private readonly Serilog.Core.Logger _sender;
|
||||
readonly Serilog.Core.Logger _sender;
|
||||
|
||||
private bool disposedValue;
|
||||
bool _disposed;
|
||||
|
||||
#endregion variables
|
||||
#endregion
|
||||
|
||||
#region constructor
|
||||
|
||||
public HttpExportStrategy(string endpoint, Dictionary<string, string>? headers = null)
|
||||
public HttpExportStrategy(string endpoint, Dictionary<string, string?>? headers = null)
|
||||
{
|
||||
IConfigurationRoot? configuration = 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();
|
||||
}
|
||||
|
||||
#endregion constructor
|
||||
|
||||
#region public
|
||||
|
||||
public Task ExportAsync(List<LogEntry> logs)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
foreach (LogEntry logEntry in logs)
|
||||
{
|
||||
_sender.Information(logEntry.ToString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#endregion public
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
if (!_disposed)
|
||||
{
|
||||
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
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
readonly HttpClient _httpClient;
|
||||
|
||||
public CustomHttpClient() => httpClient = new HttpClient();
|
||||
public CustomHttpClient()
|
||||
{
|
||||
_httpClient = new HttpClient();
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
httpClient?.Dispose();
|
||||
_httpClient?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
return await httpClient
|
||||
return await _httpClient
|
||||
.PostAsync(requestUri, content, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Classes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -24,10 +24,10 @@ using System.Threading.Tasks;
|
||||
namespace LogExporter.Strategy
|
||||
{
|
||||
/// <summary>
|
||||
/// Strategu interface to decide the sinks for exporting the logs.
|
||||
/// Strategy interface to decide the sinks for exporting the logs.
|
||||
/// </summary>
|
||||
public interface IExportStrategy: IDisposable
|
||||
{
|
||||
Task ExportAsync(List<LogEntry> logs);
|
||||
Task ExportAsync(IReadOnlyList<LogEntry> logs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
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
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -28,27 +28,23 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace LogExporter.Strategy
|
||||
{
|
||||
public class SyslogExportStrategy : IExportStrategy
|
||||
public sealed class SyslogExportStrategy : IExportStrategy
|
||||
{
|
||||
#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;
|
||||
|
||||
private readonly Rfc5424Formatter _formatter;
|
||||
|
||||
private readonly Serilog.Core.Logger _sender;
|
||||
|
||||
private bool disposedValue;
|
||||
|
||||
#endregion variables
|
||||
#endregion
|
||||
|
||||
#region constructor
|
||||
|
||||
@@ -57,7 +53,7 @@ namespace LogExporter.Strategy
|
||||
port ??= DEFAULT_PORT;
|
||||
protocol ??= DEFAUL_PROTOCOL;
|
||||
|
||||
var conf = new LoggerConfiguration();
|
||||
LoggerConfiguration conf = new LoggerConfiguration();
|
||||
|
||||
_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(),
|
||||
"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 Exception("Invalid protocol specified"),
|
||||
_ => throw new NotSupportedException("Syslog protocol is not supported: " + protocol),
|
||||
};
|
||||
|
||||
_formatter = new Rfc5424Formatter(_facility, _appName, null, _sdId, Environment.MachineName);
|
||||
}
|
||||
|
||||
#endregion constructor
|
||||
|
||||
#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
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
if (!_disposed)
|
||||
{
|
||||
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
|
||||
|
||||
private LogEvent Convert(LogEntry log)
|
||||
private static LogEvent Convert(LogEntry log)
|
||||
{
|
||||
// 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("clientPort", new ScalarValue(log.ClientPort.ToString())),
|
||||
new LogEventProperty("dnssecOk", new ScalarValue(log.DnssecOk.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()))
|
||||
};
|
||||
|
||||
// Add each question as properties
|
||||
if (log.Questions?.Count > 0)
|
||||
if (log.Question != null)
|
||||
{
|
||||
for (int i = 0; i < log.Questions.Count; i++)
|
||||
{
|
||||
var question = log.Questions[i];
|
||||
properties.Add(new LogEventProperty($"qName_{i}", new ScalarValue(question.QuestionName)));
|
||||
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())));
|
||||
}
|
||||
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())));
|
||||
|
||||
// Generate questions summary
|
||||
var questionSummary = string.Join("; ", log.Questions.Select((q, i) =>
|
||||
$"QNAME_{i}: {q.QuestionName}, QTYPE: {q.QuestionType?.ToString() ?? "unknown"}, QCLASS: {q.QuestionClass?.ToString() ?? "unknown"}"));
|
||||
string questionSummary = $"QNAME: {question.QuestionName}, QTYPE: {question.QuestionType.ToString()}, QCLASS: {question.QuestionClass.ToString()}";
|
||||
properties.Add(new LogEventProperty("questionsSummary", new ScalarValue(questionSummary)));
|
||||
}
|
||||
else
|
||||
@@ -149,21 +127,22 @@ namespace LogExporter.Strategy
|
||||
}
|
||||
|
||||
// Add each answer as properties
|
||||
if (log.Answers?.Count > 0)
|
||||
if (log.Answers.Count > 0)
|
||||
{
|
||||
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($"aData_{i}", new ScalarValue(answer.RecordData)));
|
||||
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($"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())));
|
||||
}
|
||||
|
||||
// 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)));
|
||||
}
|
||||
else
|
||||
@@ -171,22 +150,11 @@ namespace LogExporter.Strategy
|
||||
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
|
||||
const string templateText = "{questionsSummary}; RCODE: {rCode}; ANSWER: [{answersSummary}]";
|
||||
|
||||
// Parse the template
|
||||
var template = new MessageTemplateParser().Parse(templateText);
|
||||
MessageTemplate template = new MessageTemplateParser().Parse(templateText);
|
||||
|
||||
// Create the LogEvent and return it
|
||||
return new LogEvent(
|
||||
@@ -198,6 +166,6 @@ namespace LogExporter.Strategy
|
||||
);
|
||||
}
|
||||
|
||||
#endregion private
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"maxLogEntries": 1000,
|
||||
"maxQueueSize": 1000000,
|
||||
"file": {
|
||||
"path": "./dns_logs.json",
|
||||
"enabled": false
|
||||
|
||||
Reference in New Issue
Block a user