Skip to content

Commit

Permalink
Merge branch 'release/0.77.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
Jericho committed Jun 7, 2024
2 parents 111a2e1 + 9e14a38 commit 436e835
Show file tree
Hide file tree
Showing 18 changed files with 812 additions and 217 deletions.
6 changes: 3 additions & 3 deletions Source/ZoomNet.IntegrationTests/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,14 @@ private static LoggingConfiguration GetNLogConfiguration()
logzioTarget.ContextProperties.Add(new TargetPropertyWithContext("ZoomNet-Version", ZoomNet.ZoomClient.Version));

nLogConfig.AddTarget("Logzio", logzioTarget);
nLogConfig.AddRule(NLog.LogLevel.Info, NLog.LogLevel.Fatal, logzioTarget, "*");
nLogConfig.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, logzioTarget, "*"); // Send all logs to logz.io, no matther the 'level'
}

// Send logs to console
var consoleTarget = new ColoredConsoleTarget();
nLogConfig.AddTarget("ColoredConsole", consoleTarget);
nLogConfig.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, consoleTarget, "*");
nLogConfig.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, consoleTarget, "ZoomNet.ZoomWebSocketClient");
nLogConfig.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, consoleTarget, "*"); // Only display logs with 'Debug' level or higher in the console
nLogConfig.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, consoleTarget, "ZoomNet.ZoomWebSocketClient"); // Display all logs to console when testing the WebSocket client, no matther the 'level'

return nLogConfig;
}
Expand Down
9 changes: 8 additions & 1 deletion Source/ZoomNet.IntegrationTests/TestSuite.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading;
using System.Threading.Tasks;
using ZoomNet.Models;
using ZoomNet.Utilities;

namespace ZoomNet.IntegrationTests
{
Expand Down Expand Up @@ -37,7 +38,13 @@ public TestSuite(IConnectionInfo connectionInfo, IWebProxy proxy, ILoggerFactory
public virtual async Task<ResultCodes> RunTestsAsync(CancellationToken cancellationToken)
{
// Configure ZoomNet client
var client = new ZoomClient(ConnectionInfo, Proxy, null, LoggerFactory.CreateLogger<ZoomClient>());
var options = new ZoomClientOptions
{
// A successful API call will trigger a 'Trace' log (rather than the default 'Debug').
// This is to ensure that we don't get overwhelmed by too many debug messages in the console.
LogLevelSuccessfulCalls = LogLevel.Trace
};
var client = new ZoomClient(ConnectionInfo, Proxy, options, LoggerFactory.CreateLogger<ZoomClient>());

// Get my user and permisisons
User currentUser = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,12 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Logzio.DotNet.NLog" Version="1.1.0" />
<PackageReference Include="Logzio.DotNet.NLog" Version="1.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.8" />

<!-- This is a workaround for the problem described here: https://github.com/logzio/logzio-dotnet/issues/72 -->
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="8.0.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.11" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion Source/ZoomNet.UnitTests/MockFluentHttpResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public async Task<Stream> AsStream()
#endif
)
.ConfigureAwait(false);
stream.Position = 0;
if (stream.CanSeek) stream.Position = 0;
return stream;
}

Expand Down
6 changes: 3 additions & 3 deletions Source/ZoomNet.UnitTests/ZoomNet.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="NSubstitute.Analyzers.CSharp" Version="1.0.17">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
<PackageReference Include="Shouldly" Version="4.2.1" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
Expand Down
49 changes: 17 additions & 32 deletions Source/ZoomNet/Resources/CloudRecordings.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using Pathoschild.Http.Client;
using Pathoschild.Http.Client.Extensibility;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -361,41 +361,26 @@ public Task RejectRegistrantsAsync(long meetingId, IEnumerable<string> registran
/// <inheritdoc/>
public async Task<Stream> DownloadFileAsync(string downloadUrl, CancellationToken cancellationToken = default)
{
/*
* PLEASE NOTE:
*
* The HttpRequestMessage in this method is dispatched with its completion option set to "ResponseHeadersRead".
* This ensures the content of the response is streamed rather than buffered in memory.
* This is important in cases where the downloaded file is quite large.
* In this scenario, we don't want the entirety of the file to be buffered in a MemoryStream because
* it could lead to "out of memory" exceptions if the file is large enough.
* See https://github.com/Jericho/ZoomNet/pull/342 for a discussion on this topic.
*
* Forthermore, as of this writing, the FluentHttp library does not allow us to stream the content of responses
* which means that the code in this method cannot be simplified like so:
return _client
.GetAsync(downloadUrl)
.WithCancellationToken(cancellationToken)
.AsStream();
*
* The downside of not using the FluentHttp library to dispatch the request is that we lose automatic retries,
* error handling, logging, etc.
*/
// Prepare the request
var request = _client
.GetAsync(downloadUrl)
.WithOptions(completeWhen: HttpCompletionOption.ResponseHeadersRead)
.WithCancellationToken(cancellationToken);

using (var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl))
{
var tokenHandler = _client.Filters.OfType<ITokenHandler>().SingleOrDefault();
if (tokenHandler != null)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenHandler.Token);
}
// Remove our custom error handler because it reads the content of the response to check for error messages returned from the Zoom API.
// This is problematic because we want the content of the response to be streamed.
request = request.WithoutFilter<ZoomErrorHandler>();

var response = await _client.BaseClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
// We need to add the default error filter to throw an exception if the request fails.
// The error handler is safe to use with streaming responses because it does not read the content to determine if an error occured.
request = request.WithFilter(new DefaultErrorFilter());

response.EnsureSuccessStatusCode();
// Dispatch the request
var response = await request
.AsStream()
.ConfigureAwait(false);

return await response.Content.ReadAsStreamAsync();
}
return response;
}

private Task UpdateRegistrantsStatusAsync(long meetingId, IEnumerable<string> registrantIds, string status, CancellationToken cancellationToken = default)
Expand Down
152 changes: 3 additions & 149 deletions Source/ZoomNet/Utilities/DiagnosticHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@
using Pathoschild.Http.Client.Extensibility;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;

namespace ZoomNet.Utilities
{
Expand All @@ -19,149 +16,6 @@ namespace ZoomNet.Utilities
/// <seealso cref="Pathoschild.Http.Client.Extensibility.IHttpFilter" />
internal class DiagnosticHandler : IHttpFilter
{
internal class DiagnosticInfo
{
public WeakReference<HttpRequestMessage> RequestReference { get; set; }

public long RequestTimestamp { get; set; }

public WeakReference<HttpResponseMessage> ResponseReference { get; set; }

public long ResponseTimestamp { get; set; }

public DiagnosticInfo(WeakReference<HttpRequestMessage> requestReference, long requestTimestamp, WeakReference<HttpResponseMessage> responseReference, long responseTimestamp)
{
RequestReference = requestReference;
RequestTimestamp = requestTimestamp;
ResponseReference = responseReference;
ResponseTimestamp = responseTimestamp;
}

public string GetLoggingTemplate()
{
RequestReference.TryGetTarget(out HttpRequestMessage request);
ResponseReference.TryGetTarget(out HttpResponseMessage response);

var logTemplate = new StringBuilder();

if (request != null)
{
logTemplate.AppendLine("REQUEST SENT BY ZOOMNET: {Request_HttpMethod} {Request_Uri} HTTP/{Request_HttpVersion}");
logTemplate.AppendLine("REQUEST HEADERS:");

var requestHeaders = response?.RequestMessage?.Headers ?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>();
if (!requestHeaders.Any(kvp => string.Equals(kvp.Key, "Content-Length", StringComparison.OrdinalIgnoreCase)))
{
requestHeaders = requestHeaders.Append(new KeyValuePair<string, IEnumerable<string>>("Content-Length", new[] { "0" }));
}

foreach (var header in requestHeaders.OrderBy(kvp => kvp.Key))
{
logTemplate.AppendLine(" " + header.Key + ": {Request_Header_" + header.Key + "}");
}

logTemplate.AppendLine("REQUEST: {Request_Content}");
logTemplate.AppendLine();
}

if (response != null)
{
logTemplate.AppendLine("RESPONSE FROM ZOOM: HTTP/{Response_HttpVersion} {Response_StatusCode} {Response_ReasonPhrase}");
logTemplate.AppendLine("RESPONSE HEADERS:");

var responseHeaders = response?.Headers ?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>();
if (!responseHeaders.Any(kvp => string.Equals(kvp.Key, "Content-Length", StringComparison.OrdinalIgnoreCase)))
{
responseHeaders = responseHeaders.Append(new KeyValuePair<string, IEnumerable<string>>("Content-Length", new[] { "0" }));
}

foreach (var header in responseHeaders.OrderBy(kvp => kvp.Key))
{
logTemplate.AppendLine(" " + header.Key + ": {Response_Header_" + header.Key + "}");
}

logTemplate.AppendLine("RESPONSE: {Response_Content}");
logTemplate.AppendLine();
}

logTemplate.AppendLine("DIAGNOSTIC: The request took {Diagnostic_Elapsed:N} milliseconds");

return logTemplate.ToString();
}

public object[] GetLoggingParameters()
{
RequestReference.TryGetTarget(out HttpRequestMessage request);
ResponseReference.TryGetTarget(out HttpResponseMessage response);

// Get the content to the request/response and calculate how long it took to get the response
var elapsed = TimeSpan.FromTicks(ResponseTimestamp - RequestTimestamp);
var requestContent = request?.Content?.ReadAsStringAsync(null).GetAwaiter().GetResult();
var responseContent = response?.Content?.ReadAsStringAsync(null).GetAwaiter().GetResult();

// Calculate the content size
var requestContentLength = requestContent?.Length ?? 0;
var responseContentLength = responseContent?.Length ?? 0;

// Get the request headers (please note: intentionally getting headers from "response.RequestMessage" rather than "request")
var requestHeaders = response?.RequestMessage?.Headers ?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>();
if (!requestHeaders.Any(kvp => string.Equals(kvp.Key, "Content-Length", StringComparison.OrdinalIgnoreCase)))
{
requestHeaders = requestHeaders.Append(new KeyValuePair<string, IEnumerable<string>>("Content-Length", new[] { requestContentLength.ToString() }));
}

// Get the response headers
var responseHeaders = response?.Headers ?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>();
if (!responseHeaders.Any(kvp => string.Equals(kvp.Key, "Content-Length", StringComparison.OrdinalIgnoreCase)))
{
responseHeaders = responseHeaders.Append(new KeyValuePair<string, IEnumerable<string>>("Content-Length", new[] { responseContentLength.ToString() }));
}

// The order of these values must match the order in which they appear in the logging template
var logParams = new List<object>();

if (request != null)
{
logParams.AddRange([request.Method.Method, request.RequestUri, request.Version]);
logParams.AddRange(requestHeaders
.OrderBy(kvp => kvp.Key)
.Select(kvp => kvp.Key.Equals("authorization", StringComparison.OrdinalIgnoreCase) ? "... omitted for security reasons ..." : string.Join(", ", kvp.Value))
.ToArray());
logParams.Add(requestContent?.TrimEnd('\r', '\n'));
}

if (response != null)
{
logParams.AddRange([response.Version, (int)response.StatusCode, response.ReasonPhrase]);
logParams.AddRange(responseHeaders
.OrderBy(kvp => kvp.Key)
.Select(kvp => kvp.Key.Equals("authorization", StringComparison.OrdinalIgnoreCase) ? "... omitted for security reasons ..." : string.Join(", ", kvp.Value))
.ToList());
logParams.Add(responseContent?.TrimEnd('\r', '\n'));
}

logParams.Add(elapsed.TotalMilliseconds);

return logParams.ToArray();
}

public string GetFormattedLog()
{
var formattedLog = GetLoggingTemplate();
var args = GetLoggingParameters();

var pattern = @"(.*?{)(\w+?.+?)(}.*)";
for (var i = 0; i < args.Length; i++)
{
formattedLog = Regex.Replace(formattedLog, pattern, $"$1 {i} $3", RegexOptions.None);
}

formattedLog = formattedLog.Replace("{ ", "{").Replace(" }", "}");

return string.Format(formattedLog, args);
}
}

#region FIELDS

internal const string DIAGNOSTIC_ID_HEADER_NAME = "ZoomNet-Diagnostic-Id";
Expand Down Expand Up @@ -199,7 +53,7 @@ public void OnRequest(IRequest request)
request.WithHeader(DIAGNOSTIC_ID_HEADER_NAME, diagnosticId);

// Add the diagnostic info to our cache
DiagnosticsInfo.TryAdd(diagnosticId, new DiagnosticInfo(new WeakReference<HttpRequestMessage>(request.Message), Stopwatch.GetTimestamp(), null, long.MinValue));
DiagnosticsInfo.TryAdd(diagnosticId, new DiagnosticInfo(new WeakReference<HttpRequestMessage>(request.Message), Stopwatch.GetTimestamp(), null, long.MinValue, request.Options));
}

/// <summary>Method invoked just after the HTTP response is received. This method can modify the incoming HTTP response.</summary>
Expand Down Expand Up @@ -240,9 +94,9 @@ private static void Cleanup()
try
{
// Remove diagnostic information for requests that have been garbage collected
foreach (string key in DiagnosticHandler.DiagnosticsInfo.Keys.ToArray())
foreach (string key in DiagnosticsInfo.Keys.ToArray())
{
if (DiagnosticHandler.DiagnosticsInfo.TryGetValue(key, out DiagnosticInfo diagnosticInfo))
if (DiagnosticsInfo.TryGetValue(key, out DiagnosticInfo diagnosticInfo))
{
if (!diagnosticInfo.RequestReference.TryGetTarget(out HttpRequestMessage request))
{
Expand Down
Loading

0 comments on commit 436e835

Please sign in to comment.