duplicati/Duplicati/Library/Main/NetworkTrafficLogger.cs
Kenneth Skovhede ba2ea61ca4 Add option to log HTTP and socket requests
This adds the options `--log-http-requests` and `--log-socket-data` which can be activated to capture diagnostics about HTTP requests and socket operations.
2025-08-08 19:18:30 +02:00

148 lines
5.9 KiB
C#

// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#nullable enable
using System;
using System.Diagnostics.Tracing;
using System.Text;
namespace Duplicati.Library.Main;
/// <summary>
/// Listener for HTTP diagnostics events
/// This listener captures events from System.Net.Http and System.Net.Sockets
/// and forwards them to the Duplicati logging system.
/// </summary>
/// <param name="enableHttpLogging">If true, enables logging of HTTP events.</param>
/// <param name="socketDataBytes">The number of bytes of socket data to log, or -1 to disable logging of socket data.</param>
internal sealed class NetworkTrafficLogger(bool enableHttpLogging, int socketDataBytes) : EventListener
{
/// <summary>
/// Log tag for HTTP events
/// </summary>
private static readonly string LogTagHttp = Logging.Log.LogTagFromType<NetworkTrafficLogger>() + ".Http";
/// <summary>
/// Log tag for socket events
/// </summary>
private static readonly string LogTagSocket = Logging.Log.LogTagFromType<NetworkTrafficLogger>() + ".Socket";
/// <inheritdoc/>
protected override void OnEventSourceCreated(EventSource eventSource)
{
if ((enableHttpLogging && eventSource.Name == "System.Net.Http") || (socketDataBytes >= 0 && eventSource.Name == "System.Net.Sockets"))
EnableEvents(eventSource, EventLevel.Verbose, EventKeywords.All);
}
/// <inheritdoc/>
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
if (enableHttpLogging && eventData.EventSource.Name == "System.Net.Http")
{
Logging.Log.WriteVerboseMessage(LogTagHttp, eventData.EventName, FormatPayload(eventData));
}
else if (socketDataBytes >= 0 && eventData.EventSource.Name == "System.Net.Sockets")
{
Logging.Log.WriteVerboseMessage(LogTagSocket, eventData.EventName, FormatSocketPayload(eventData));
}
}
/// <summary>
/// Formats the payload of an HTTP event for logging.
/// </summary>
/// <param name="eventData">The event data containing the payload.</param>
/// <returns>A formatted string representation of the payload.</returns>
private static string FormatPayload(EventWrittenEventArgs eventData)
{
var sb = new StringBuilder();
sb.AppendLine($"[{eventData.EventSource.Name}] Event: {eventData.EventName}");
if (eventData.Payload != null && eventData.Payload.Count > 0)
{
for (int i = 0; i < eventData.Payload.Count; i++)
{
var name = eventData.PayloadNames?[i] ?? $"Field{i}";
var value = FormatValue(eventData.Payload[i]);
sb.AppendLine($" {name,-20}: {value}");
}
}
return sb.ToString();
}
/// <summary>
/// Formats the payload of a socket event for logging.
/// </summary>
/// <param name="eventData">The event data containing the payload.</param>
/// <returns>A formatted string representation of the socket payload.</returns>
private string FormatSocketPayload(EventWrittenEventArgs eventData)
{
var sb = new StringBuilder();
sb.AppendLine($"[{eventData.EventSource.Name}] Event: {eventData.EventName}");
if (eventData.Payload != null && eventData.Payload.Count > 0)
{
for (int i = 0; i < eventData.Payload.Count; i++)
{
var name = eventData.PayloadNames?[i] ?? $"Field{i}";
var val = eventData.Payload[i];
if (val is byte[] bytes)
{
int len = socketDataBytes == 0 ? 0 : Math.Min(bytes.Length, socketDataBytes);
if (len > 0)
{
string hex = BitConverter.ToString(bytes, 0, len);
string ascii = Encoding.ASCII.GetString(bytes, 0, len).Replace('\0', '.');
sb.AppendLine($" {name,-20}: {hex} (ASCII: {ascii})");
}
else
{
sb.AppendLine($" {name,-20}: [Byte array suppressed]");
}
}
else
{
sb.AppendLine($" {name,-20}: {FormatValue(val)}");
}
}
}
return sb.ToString();
}
/// <summary>
/// Formats a value for logging.
/// </summary>
/// <param name="val">The value to format.</param>
/// <returns>A formatted string representation of the value.</returns>
private static string FormatValue(object? val)
{
if (val == null)
return "(null)";
var type = val.GetType();
if (type.IsEnum)
return $"{val} ({Convert.ToInt32(val)})";
return val.ToString() ?? string.Empty;
}
}