diff --git a/Apps/QueryLogsDuckDBApp/App.cs b/Apps/QueryLogsDuckDBApp/App.cs
new file mode 100644
index 00000000..976fac43
--- /dev/null
+++ b/Apps/QueryLogsDuckDBApp/App.cs
@@ -0,0 +1,811 @@
+/*
+Technitium DNS Server
+Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
+Copyright (C) 2025 Zafer Balkan (zafer@zaferbalkan.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 .
+*/
+
+using DnsServerCore.ApplicationCommon;
+using DuckDB.NET.Data;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+using TechnitiumLibrary.Net.Dns;
+using TechnitiumLibrary.Net.Dns.ResourceRecords;
+
+namespace QueryLogsDuckDB
+{
+ public sealed class App : IDnsApplication, IDnsQueryLogger, IDnsQueryLogs
+ {
+ #region variables
+ // Initial capacity for answer StringBuilder
+ private const int DEFAULT_ANSWER_SIZE = 256;
+
+ // To prevent excessive memory usage, answers larger than this will be truncated. Used the value of MySQL app.
+ private const int MAX_ANSWER_SIZE = 4000;
+
+ // Maximum number of log entries to process in a single batch
+ private const int MAX_BATCH_SIZE = 1000;
+
+ [ThreadStatic]
+ private static StringBuilder? _sb;
+ private readonly SemaphoreSlim _dbGate = new(1, 1);
+ private readonly JsonSerializerOptions _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+ private Channel? _channel;
+ Config? _config;
+ private DuckDBConnection? _conn;
+ private Task? _consumerTask;
+ private bool _disposed;
+ private IDnsServer? _dnsServer;
+ private Task? _retentionTask;
+ #endregion variables
+
+ #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 (!_disposed)
+ {
+ if (disposing)
+ {
+ // To prevent blocking shutdown, we attempt to complete the channel and wait for the consumer task to finish.
+ // This ensures that the application can shut down gracefully without being hindered by logging operations.
+ try { _channel?.Writer.TryComplete(); }
+ catch (Exception ex)
+ {
+ _dnsServer?.WriteLog("QueryLogsDuckDB.App: Error while completing log channel during Dispose: " + ex);
+ }
+ try
+ {
+ if (_consumerTask != null)
+ {
+ using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(5000));
+ try
+ {
+ _consumerTask.WaitAsync(cts.Token).GetAwaiter().GetResult();
+ }
+ catch (OperationCanceledException)
+ {
+ // Ignore timeout/cancellation during dispose to avoid blocking shutdown indefinitely.
+ }
+ _consumerTask.Dispose();
+ }
+ }
+ catch (Exception ex)
+ {
+ _dnsServer?.WriteLog("QueryLogsDuckDB.App: Error while waiting for consumer task during Dispose: " + ex);
+ }
+ try
+ {
+ if (_retentionTask != null)
+ {
+ using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(5000));
+ try
+ {
+ _retentionTask.WaitAsync(cts.Token).GetAwaiter().GetResult();
+ }
+ catch (OperationCanceledException)
+ {
+ // Ignore timeout/cancellation during dispose to avoid blocking shutdown indefinitely.
+ }
+ _retentionTask.Dispose();
+ }
+ }
+ catch (Exception ex)
+ {
+ _dnsServer?.WriteLog("QueryLogsDuckDB.App: Error while waiting for retention task during Dispose: " + ex);
+ }
+ try { _conn?.Close(); _conn?.Dispose(); }
+ catch (Exception ex)
+ {
+ _dnsServer?.WriteLog("QueryLogsDuckDB.App: Error while closing/disposing DuckDB connection during Dispose: " + ex);
+ }
+
+ _dbGate.Dispose();
+ }
+
+ _disposed = true;
+ }
+ }
+ #endregion IDisposable
+
+ #region private
+
+ private static StringBuilder GetStringBuilder()
+ {
+ StringBuilder? sb = _sb;
+
+ if (sb is null)
+ return _sb = new StringBuilder(DEFAULT_ANSWER_SIZE, MAX_ANSWER_SIZE);
+
+ sb.Clear();
+ return sb;
+ }
+
+ private async Task BulkInsertAsync(List logs)
+ {
+ await _dbGate.WaitAsync();
+ try
+ {
+ // We create a new appender for each batch to avoid issues with concurrent usage
+ // By default, the appender performs commits every 204,800 rows.
+ // Since we are using smaller batches, we are forcing appender to close after each batch.
+ // It makes the flush to disk more frequent, but ensures data integrity in case of crashes.
+ // Each batch flush is atomic.
+ using DuckDBAppender appender = _conn!.CreateAppender("dns_logs");
+ foreach (LogEntry log in logs
+ .Where(log => log.Request is not null && log.Response is not null))
+ {
+ DnsQuestionRecord? question =
+ log.Request.Question.Count > 0
+ ? log.Request.Question[0]
+ : null;
+
+ //Response Type(Aligned)
+ DnsServerResponseType responseType = log.Response.Tag is null
+ ? DnsServerResponseType.Recursive
+ : (DnsServerResponseType)log.Response.Tag;
+
+ //RTT
+ double? rtt = null;
+
+ if (responseType == DnsServerResponseType.Recursive &&
+ log.Response.Metadata is not null)
+ {
+ rtt = log.Response.Metadata.RoundTripTime;
+ }
+
+ // Answer (bounded, safe)
+ string? answer = null;
+
+ if (log.Response.Answer.Count > 0)
+ {
+ if (log.Response.IsZoneTransfer && log.Response.Answer.Count > 2)
+ {
+ answer = "[ZONE TRANSFER]";
+ }
+ else
+ {
+ StringBuilder sb = GetStringBuilder();
+ bool first = true;
+
+ foreach (DnsResourceRecord record in log.Response.Answer)
+ {
+ if (!first)
+ {
+ if (sb.Length + 2 >= MAX_ANSWER_SIZE)
+ break;
+
+ sb.Append(", ");
+ }
+
+ string part = $"{record.Type} {record.RDATA}";
+
+ int remaining =
+ MAX_ANSWER_SIZE - sb.Length;
+
+ if (remaining <= 0)
+ break;
+
+ if (part.Length > remaining)
+ {
+ sb.Append(part.AsSpan(0, remaining));
+ break;
+ }
+
+ sb.Append(part);
+ first = false;
+ }
+
+ answer = sb.Length == 0 ? null : sb.ToString();
+ }
+ }
+
+ //Insert Row
+ IDuckDBAppenderRow row = appender.CreateRow();
+
+ row.AppendValue(_dnsServer!.ServerDomain);
+ row.AppendValue(log.Timestamp);
+ row.AppendValue(log.RemoteEP.Address.ToString());
+ row.AppendValue((byte)log.Protocol);
+
+ row.AppendValue((byte)responseType);
+
+ if (rtt is null)
+ row.AppendNullValue();
+ else
+ row.AppendValue(rtt.Value);
+
+ row.AppendValue((byte)log.Response.RCODE);
+
+ if (question is null)
+ {
+ row.AppendNullValue();
+ row.AppendNullValue();
+ row.AppendNullValue();
+ }
+ else
+ {
+ row.AppendValue(question.Name.ToLowerInvariant());
+ row.AppendValue((ushort)question.Type);
+ row.AppendValue((ushort)question.Class);
+ }
+
+ if (answer is null)
+ row.AppendNullValue();
+ else
+ row.AppendValue(answer);
+
+ row.EndRow();
+ }
+ }
+ catch (Exception ex)
+ {
+ _dnsServer?.WriteLog(ex);
+ // No need for a wait as this is running synchronously in the background,
+ // we just log and continue.
+ // No risk of concurrency issues and waiting here would block the logging thread.
+ }
+ finally
+ {
+ _dbGate.Release();
+ }
+ }
+
+ private async Task CreateSchemaAsync()
+ {
+ using DuckDBCommand cmd = _conn!.CreateCommand();
+
+ cmd.CommandText = @"
+CREATE TABLE IF NOT EXISTS dns_logs (
+ server VARCHAR(255) NOT NULL,
+ timestamp TIMESTAMP NOT NULL,
+ client_ip VARCHAR(39) NOT NULL,
+ protocol UTINYINT NOT NULL,
+ response_type UTINYINT,
+ response_rtt DOUBLE,
+ rcode UTINYINT NOT NULL,
+ qname VARCHAR(255),
+ qtype USMALLINT,
+ qclass USMALLINT,
+ answer TEXT
+);";
+ await cmd.ExecuteNonQueryAsync();
+
+ string index = "CREATE INDEX IF NOT EXISTS idx_ts_srv_ip ON dns_logs(timestamp, server, client_ip);";
+ cmd.CommandText = index;
+ await cmd.ExecuteNonQueryAsync();
+ }
+
+ private async Task ProcessLogsAsync()
+ {
+ List batch = new List(MAX_BATCH_SIZE);
+
+ while (!_disposed && await _channel!.Reader.WaitToReadAsync())
+ {
+ while (batch.Count < MAX_BATCH_SIZE &&
+ _channel.Reader.TryRead(out LogEntry log))
+ {
+ batch.Add(log);
+ }
+
+ if (batch.Count > 0)
+ {
+ await BulkInsertAsync(batch);
+ batch.Clear();
+ }
+ }
+
+ if (batch.Count > 0)
+ await BulkInsertAsync(batch);
+ }
+
+ private async Task RetentionLoopAsync()
+ {
+ await Task.Delay(TimeSpan.FromMinutes(1));
+
+ while (!_disposed)
+ {
+ try
+ {
+ await Task.Delay(TimeSpan.FromMinutes(15));
+ if (_disposed) break;
+ await RunRetentionAsync();
+ }
+ catch (Exception ex)
+ {
+ _dnsServer?.WriteLog(ex);
+ }
+ }
+ }
+
+ private async Task RunRetentionAsync()
+ {
+ if (_conn is null || _config is null)
+ return;
+
+ await _dbGate.WaitAsync();
+ try
+ {
+ using DuckDBCommand cmd = _conn.CreateCommand();
+
+ long deleted = 0;
+
+ /* ---------------------------------
+ Max records
+ --------------------------------- */
+
+ if (_config.MaxLogRecords > 0)
+ {
+ // Count first to avoid running a DELETE when there is nothing to prune.
+ cmd.Parameters.Clear();
+ cmd.CommandText = "SELECT count() FROM dns_logs;";
+ long totalRows = Convert.ToInt64(await cmd.ExecuteScalarAsync());
+ if (totalRows > _config.MaxLogRecords)
+ {
+ // Keep newest N rows (by timestamp). The cutoff is the Nth newest row.
+ // NOTE: Timestamp collisions can still cause >N rows to be retained; strict N
+ // requires a stable tiebreaker column (handled in a separate PR if needed).
+ cmd.Parameters.Clear();
+
+
+ cmd.CommandText = @"
+WITH cutoff AS (
+ SELECT timestamp
+ FROM dns_logs
+ ORDER BY timestamp DESC
+ OFFSET ($limit - 1)
+ LIMIT 1
+)
+DELETE FROM dns_logs
+WHERE timestamp < (SELECT timestamp FROM cutoff);
+";
+ cmd.Parameters.Add(
+ new DuckDBParameter("limit", _config.MaxLogRecords));
+
+ deleted += await cmd.ExecuteNonQueryAsync();
+ }
+ }
+
+ /* ---------------------------------
+ Max days
+ --------------------------------- */
+
+ if (_config.MaxLogDays > 0)
+ {
+ DateTime cutoff =
+ DateTime.UtcNow.AddDays(-_config.MaxLogDays);
+
+ cmd.CommandText =
+ "DELETE FROM dns_logs WHERE timestamp < $cutoff;";
+
+ cmd.Parameters.Clear();
+ cmd.Parameters.Add(
+ new DuckDBParameter("cutoff", cutoff));
+
+ deleted += await cmd.ExecuteNonQueryAsync();
+ }
+
+ if (deleted > 0)
+ {
+ cmd.Parameters.Clear();
+ cmd.CommandText = "CHECKPOINT;";
+ await cmd.ExecuteNonQueryAsync();
+ _dnsServer?.WriteLog($"DuckDB retention removed {deleted} records.");
+ }
+ }
+ finally
+ {
+ _dbGate.Release();
+ }
+ }
+ #endregion private
+
+ #region public
+
+ public async Task InitializeAsync(IDnsServer dnsServer, string config)
+ {
+ _dnsServer = dnsServer;
+
+ _config = JsonSerializer.Deserialize(config, _options);
+ _config ??= new Config();
+ Validator.ValidateObject(_config, new ValidationContext(_config), validateAllProperties: true);
+
+ if (!System.IO.Path.IsPathRooted(_config.DbPath))
+ _config.DbPath = System.IO.Path.Combine(dnsServer.ApplicationFolder, _config.DbPath);
+
+ _channel = Channel.CreateBounded(
+ new BoundedChannelOptions(_config.MaxQueueSize)
+ {
+ SingleReader = true,
+ SingleWriter = true,
+ FullMode = BoundedChannelFullMode.DropWrite
+ });
+
+ _conn = new DuckDBConnection($"Data Source={_config.DbPath}");
+ await _conn.OpenAsync();
+ await CreateSchemaAsync();
+
+ _consumerTask = Task.Run(ProcessLogsAsync).ContinueWith(
+ t =>
+ {
+ if (t.Exception != null)
+ _dnsServer?.WriteLog(t.Exception);
+ },
+ TaskContinuationOptions.OnlyOnFaulted);
+ _retentionTask = Task.Run(RetentionLoopAsync).ContinueWith(
+ t =>
+ {
+ if (t.Exception != null)
+ _dnsServer?.WriteLog(t.Exception);
+ },
+ TaskContinuationOptions.OnlyOnFaulted);
+ }
+
+ public Task InsertLogAsync(
+ DateTime timestamp,
+ DnsDatagram request,
+ IPEndPoint remoteEP,
+ DnsTransportProtocol protocol,
+ DnsDatagram response)
+ {
+ if (_disposed) return Task.CompletedTask;
+ if (_config is null) return Task.CompletedTask;
+ if (_conn is null) return Task.CompletedTask;
+
+ if (_config.EnableLogging)
+ _channel!.Writer.TryWrite(
+ new LogEntry(timestamp, request, remoteEP, protocol, response));
+
+ return Task.CompletedTask;
+ }
+
+ public async Task 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)
+ {
+ if (entriesPerPage <= 0)
+ throw new ArgumentOutOfRangeException(
+ nameof(entriesPerPage),
+ "entriesPerPage must be greater than zero.");
+
+ // Prevent pathological page sizes (DoS / memory abuse)
+ const int MaxPageSize = 10_000;
+
+ if (entriesPerPage > MaxPageSize)
+ entriesPerPage = MaxPageSize;
+
+ if (pageNumber < 1)
+ pageNumber = 1;
+
+ // Normalize inverted time ranges
+ if (start is not null &&
+ end is not null &&
+ start > end)
+ {
+ (start, end) = (end, start);
+ }
+
+ using DuckDBCommand cmd = _conn!.CreateCommand();
+
+ List filters = new List();
+
+ /* ---------------------------------
+ Filters
+ --------------------------------- */
+
+ if (start is not null)
+ {
+ filters.Add("timestamp >= $s");
+ cmd.Parameters.Add(new DuckDBParameter("s", start));
+ }
+
+ if (end is not null)
+ {
+ filters.Add("timestamp <= $e");
+ cmd.Parameters.Add(new DuckDBParameter("e", end));
+ }
+
+ if (clientIpAddress is not null)
+ {
+ filters.Add("client_ip = $cip");
+ cmd.Parameters.Add(
+ new DuckDBParameter("cip", clientIpAddress.ToString()));
+ }
+
+ if (protocol is not null)
+ {
+ filters.Add("protocol = $proto");
+ cmd.Parameters.Add(
+ new DuckDBParameter("proto", (byte)protocol.Value));
+ }
+
+ if (responseType is not null)
+ {
+ filters.Add("response_type = $rtype");
+ cmd.Parameters.Add(
+ new DuckDBParameter("rtype", (byte)responseType.Value));
+ }
+
+ if (rcode is not null)
+ {
+ filters.Add("rcode = $rcode");
+ cmd.Parameters.Add(
+ new DuckDBParameter("rcode", (byte)rcode.Value));
+ }
+
+ if (!string.IsNullOrWhiteSpace(qname))
+ {
+ qname = qname.Trim().ToLowerInvariant();
+ if (qname.Contains('*'))
+ {
+ qname = qname.Replace('*', '%');
+ }
+ filters.Add("qname LIKE $qname");
+ cmd.Parameters.Add(
+ new DuckDBParameter("qname", qname));
+ }
+
+ if (qtype is not null)
+ {
+ filters.Add("qtype = $qtype");
+ cmd.Parameters.Add(
+ new DuckDBParameter("qtype", (ushort)qtype.Value));
+ }
+
+ if (qclass is not null)
+ {
+ filters.Add("qclass = $qclass");
+ cmd.Parameters.Add(
+ new DuckDBParameter("qclass", (ushort)qclass.Value));
+ }
+
+ string whereSql =
+ filters.Count > 0
+ ? " WHERE " + string.Join(" AND ", filters)
+ : string.Empty;
+
+ /* ---------------------------------
+ Count
+ --------------------------------- */
+
+ cmd.CommandText =
+ $"SELECT count() FROM dns_logs {whereSql}";
+
+ long totalEntries =
+ Convert.ToInt64(await cmd.ExecuteScalarAsync());
+
+ long totalPages =
+ totalEntries == 0
+ ? 1
+ : (long)Math.Ceiling(
+ (double)totalEntries / entriesPerPage);
+
+ pageNumber =
+ Math.Clamp(pageNumber, 1, totalPages);
+
+ /* ---------------------------------
+ Offset (overflow-safe)
+ --------------------------------- */
+
+ long offset;
+
+ try
+ {
+ checked
+ {
+ offset =
+ (pageNumber - 1) * entriesPerPage;
+ }
+ }
+ catch (OverflowException)
+ {
+ offset = 0;
+ pageNumber = 1;
+ }
+
+ /* ---------------------------------
+ Pagination parameters
+ --------------------------------- */
+
+ cmd.Parameters.Add(
+ new DuckDBParameter("limit", entriesPerPage));
+
+ cmd.Parameters.Add(
+ new DuckDBParameter("offset", offset));
+
+ /* ---------------------------------
+ Main query
+ --------------------------------- */
+
+ cmd.CommandText = $@"
+SELECT server,
+ timestamp,
+ client_ip,
+ protocol,
+ response_type,
+ response_rtt,
+ rcode,
+ qname,
+ qtype,
+ qclass,
+ answer
+FROM dns_logs
+{whereSql}
+ORDER BY timestamp {(descendingOrder ? "DESC" : "ASC")}
+LIMIT $limit
+OFFSET $offset";
+
+ List list = new List(entriesPerPage);
+
+ /* ---------------------------------
+ Read
+ --------------------------------- */
+ await _dbGate.WaitAsync();
+ try
+ {
+ using System.Data.Common.DbDataReader reader = await cmd.ExecuteReaderAsync();
+
+ while (await reader.ReadAsync())
+ {
+ DateTime ts = reader.GetDateTime(1);
+
+ IPAddress ip =
+ IPAddress.Parse(reader.GetString(2));
+
+ DnsTransportProtocol proto =
+ (DnsTransportProtocol)reader.GetByte(3);
+
+ DnsServerResponseType respType =
+ (DnsServerResponseType)reader.GetByte(4);
+
+ double? rtt =
+ reader.IsDBNull(5)
+ ? null
+ : reader.GetDouble(5);
+
+ DnsResponseCode rc =
+ (DnsResponseCode)reader.GetByte(6);
+
+ string? qn =
+ reader.IsDBNull(7)
+ ? null
+ : reader.GetString(7);
+
+ DnsQuestionRecord? question = null;
+
+ if (qn is not null &&
+ !reader.IsDBNull(8) &&
+ !reader.IsDBNull(9))
+ {
+ question = new DnsQuestionRecord(
+ qn,
+ (DnsResourceRecordType)reader.GetFieldValue(8),
+ (DnsClass)reader.GetFieldValue(9),
+ false);
+ }
+
+ string? ans =
+ reader.IsDBNull(10)
+ ? null
+ : reader.GetString(10);
+
+ list.Add(
+ new DnsLogEntry(
+ 0,
+ ts,
+ ip,
+ proto,
+ respType,
+ rtt,
+ rc,
+ question,
+ ans));
+ }
+
+ return new DnsLogPage(
+ pageNumber,
+ totalPages,
+ totalEntries,
+ list);
+ }
+ finally
+ {
+ _dbGate.Release();
+ }
+ }
+
+ #endregion public
+
+ #region properties
+
+ public string Description
+ { get { return "Logs all incoming DNS requests and their responses in a DuckDB database that can be queried from the DNS Server web console."; } }
+
+ #endregion properties
+
+ private readonly struct LogEntry
+ {
+ #region variables
+
+ public readonly DnsTransportProtocol Protocol;
+ public readonly IPEndPoint RemoteEP;
+ public readonly DnsDatagram Request;
+ public readonly DnsDatagram Response;
+ public readonly DateTime Timestamp;
+
+ #endregion variables
+
+ #region constructor
+
+ public LogEntry(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)
+ {
+ Timestamp = timestamp;
+ Request = request;
+ RemoteEP = remoteEP;
+ Protocol = protocol;
+ Response = response;
+ }
+
+ #endregion constructor
+ }
+
+ private class Config
+ {
+ [JsonPropertyName("dbPath")]
+ public string DbPath { get; set; } = "querylogs.db";
+
+ [JsonPropertyName("enableLogging")]
+ public bool EnableLogging { get; set; } = true;
+
+ [JsonPropertyName("maxLogDays")]
+ [Range(0, 365)]
+ public int MaxLogDays { get; set; } = 30;
+
+ [JsonPropertyName("maxLogRecords")]
+ [Range(0, 5_000_000)]
+ public long MaxLogRecords { get; set; } = 1_000_000;
+
+ [JsonPropertyName("maxQueueSize")]
+ [Range(1_000, 1_000_000)]
+ public int MaxQueueSize { get; set; } = 200_000;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Apps/QueryLogsDuckDBApp/QueryLogsDuckDBApp.csproj b/Apps/QueryLogsDuckDBApp/QueryLogsDuckDBApp.csproj
new file mode 100644
index 00000000..f706684f
--- /dev/null
+++ b/Apps/QueryLogsDuckDBApp/QueryLogsDuckDBApp.csproj
@@ -0,0 +1,48 @@
+
+
+
+ net9.0
+ false
+ true
+ 1.0
+ false
+ Technitium
+ Technitium DNS Server
+ Zafer Balkan
+ QueryLogsDuckDBApp
+ QueryLogsDuckDB
+ https://technitium.com/dns/
+ https://github.com/TechnitiumSoftware/DnsServer
+ Logs all incoming DNS requests and their responses in a DuckDB database that can be queried from the DNS Server web console.
+ false
+ Library
+ enable
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+ ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll
+ false
+
+
+ ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll
+ false
+
+
+
+
+
+ PreserveNewest
+
+
+
diff --git a/Apps/QueryLogsDuckDBApp/dnsApp.config b/Apps/QueryLogsDuckDBApp/dnsApp.config
new file mode 100644
index 00000000..081b6ccd
--- /dev/null
+++ b/Apps/QueryLogsDuckDBApp/dnsApp.config
@@ -0,0 +1,7 @@
+{
+ "enableLogging": true,
+ "dbPath": "querylogs.db",
+ "maxQueueSize": 200000,
+ "maxLogDays": 30,
+ "maxLogRecords": 1000000
+}
diff --git a/DnsServer.sln b/DnsServer.sln
index 0a2a6756..976abfe5 100644
--- a/DnsServer.sln
+++ b/DnsServer.sln
@@ -68,8 +68,11 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QueryLogsMySqlApp", "Apps\QueryLogsMySqlApp\QueryLogsMySqlApp.csproj", "{699E2A1D-D917-4825-939E-65CDB2B16A96}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MispConnectorApp", "Apps\MispConnectorApp\MispConnectorApp.csproj", "{83C8180A-0F86-F9A0-8F41-6FD61FAC41CB}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DnsServerCore.HttpApi", "DnsServerCore.HttpApi\DnsServerCore.HttpApi.csproj", "{1A49D371-D08C-475E-B7A2-6E8ECD181FD6}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QueryLogsDuckDBApp", "Apps\QueryLogsDuckDBApp\QueryLogsDuckDBApp.csproj", "{B4F714DB-B90F-467A-9DD1-4F944A84B4F2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -208,6 +211,10 @@ Global
{1A49D371-D08C-475E-B7A2-6E8ECD181FD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A49D371-D08C-475E-B7A2-6E8ECD181FD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A49D371-D08C-475E-B7A2-6E8ECD181FD6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B4F714DB-B90F-467A-9DD1-4F944A84B4F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4F714DB-B90F-467A-9DD1-4F944A84B4F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B4F714DB-B90F-467A-9DD1-4F944A84B4F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B4F714DB-B90F-467A-9DD1-4F944A84B4F2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -240,6 +247,7 @@ Global
{6F655C97-FD43-4FE1-B15A-6C783D2D91C9} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
{699E2A1D-D917-4825-939E-65CDB2B16A96} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
{83C8180A-0F86-F9A0-8F41-6FD61FAC41CB} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
+ {B4F714DB-B90F-467A-9DD1-4F944A84B4F2} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6747BB6D-2826-4356-A213-805FBCCF9201}