-
Notifications
You must be signed in to change notification settings - Fork 163
Add logfile actuator #1499
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add logfile actuator #1499
Changes from all commits
37d4b74
a0e8fe2
711c98f
ee6cf15
7f8d4f9
e48a003
d361f82
26511de
f1f808b
ee63559
88324e5
52cd3b0
5f98eca
04eb095
013e333
5367550
2b016ef
416957d
a65eb25
361636e
629afc2
5c1f8aa
5679892
fefff15
f106e48
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the Apache 2.0 License. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using Microsoft.Extensions.Configuration; | ||
using Steeltoe.Management.Endpoint.Configuration; | ||
|
||
namespace Steeltoe.Management.Endpoint.Actuators.LogFile; | ||
|
||
internal sealed class ConfigureLogFileEndpointOptions(IConfiguration configuration) | ||
: ConfigureEndpointOptions<LogFileEndpointOptions>(configuration, ManagementInfoPrefix, "logfile") | ||
{ | ||
private const string ManagementInfoPrefix = "management:endpoints:logfile"; | ||
|
||
public override void Configure(LogFileEndpointOptions options) | ||
{ | ||
options.AllowedVerbs.Add("Get"); | ||
options.AllowedVerbs.Add("Head"); | ||
base.Configure(options); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the Apache 2.0 License. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
namespace Steeltoe.Management.Endpoint.Actuators.LogFile; | ||
|
||
internal static class EndpointServiceCollectionExtensions | ||
{ | ||
/// <summary> | ||
/// Adds the logfile actuator to the service container and configures the ASP.NET Core middleware pipeline. | ||
/// </summary> | ||
/// <param name="services"> | ||
/// The <see cref="IServiceCollection" /> to add services to. | ||
/// </param> | ||
/// <returns> | ||
/// The incoming <paramref name="services" /> so that additional calls can be chained. | ||
/// </returns> | ||
public static IServiceCollection AddLogFileActuator(this IServiceCollection services) | ||
{ | ||
return AddLogFileActuator(services, true); | ||
} | ||
|
||
/// <summary> | ||
/// Adds the logfile actuator to the service container. | ||
/// </summary> | ||
/// <param name="services"> | ||
/// The <see cref="IServiceCollection" /> to add services to. | ||
/// </param> | ||
/// <param name="configureMiddleware"> | ||
/// When <c>false</c>, skips configuration of the ASP.NET Core middleware pipeline. While this provides full control over the pipeline order, it requires | ||
/// manual addition of the appropriate middleware for actuators to work correctly. | ||
/// </param> | ||
/// <returns> | ||
/// The incoming <paramref name="services" /> so that additional calls can be chained. | ||
/// </returns> | ||
public static IServiceCollection AddLogFileActuator(this IServiceCollection services, bool configureMiddleware) | ||
{ | ||
ArgumentNullException.ThrowIfNull(services); | ||
|
||
services.AddCoreActuatorServices<LogFileEndpointOptions, ConfigureLogFileEndpointOptions, LogFileEndpointMiddleware, | ||
ILogFileEndpointHandler, LogFileEndpointHandler, LogFileEndpointRequest?, LogFileEndpointResponse>(configureMiddleware); | ||
|
||
return services; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the Apache 2.0 License. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
namespace Steeltoe.Management.Endpoint.Actuators.LogFile; | ||
|
||
public interface ILogFileEndpointHandler : IEndpointHandler<LogFileEndpointRequest?, LogFileEndpointResponse>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the Apache 2.0 License. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Reflection; | ||
using System.Text; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
using Steeltoe.Management.Configuration; | ||
|
||
namespace Steeltoe.Management.Endpoint.Actuators.LogFile; | ||
|
||
internal sealed class LogFileEndpointHandler : ILogFileEndpointHandler | ||
{ | ||
private readonly IOptionsMonitor<LogFileEndpointOptions> _optionsMonitor; | ||
private readonly ILogger<LogFileEndpointHandler> _logger; | ||
|
||
public EndpointOptions Options => _optionsMonitor.CurrentValue; | ||
|
||
public LogFileEndpointHandler(IOptionsMonitor<LogFileEndpointOptions> optionsMonitor, ILoggerFactory loggerFactory) | ||
{ | ||
ArgumentNullException.ThrowIfNull(optionsMonitor); | ||
ArgumentNullException.ThrowIfNull(loggerFactory); | ||
|
||
_optionsMonitor = optionsMonitor; | ||
_logger = loggerFactory.CreateLogger<LogFileEndpointHandler>(); | ||
} | ||
|
||
public async Task<LogFileEndpointResponse> InvokeAsync(LogFileEndpointRequest? argument, CancellationToken cancellationToken) | ||
{ | ||
_logger.LogTrace("Invoking {Handler} with argument: {Argument}", nameof(LogFileEndpointHandler), argument); | ||
cancellationToken.ThrowIfCancellationRequested(); | ||
|
||
string logFilePath = GetLogFilePath(); | ||
|
||
if (!string.IsNullOrEmpty(logFilePath)) | ||
{ | ||
FileInfo logFile = new FileInfo(logFilePath); | ||
var logFileResult = new LogFileEndpointResponse(await File.ReadAllTextAsync(logFilePath, cancellationToken), | ||
logFile.Length, | ||
Encoding.Default, // StreamReader.CurrentEncoding is not reliable based on unit tests, so just use default for this .NET implementation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can't make assumptions about the file encoding. It may vary per logger provider, over which Steeltoe has no control. For example, in Serilog, it can be overridden. At the HTTP level, we're using bytes for ranges. The easiest would be to leave out |
||
logFile.LastWriteTimeUtc); | ||
return logFileResult; | ||
} | ||
|
||
return new LogFileEndpointResponse(); | ||
} | ||
|
||
internal string GetLogFilePath() | ||
{ | ||
_logger.LogTrace("Getting log file path"); | ||
|
||
if (!string.IsNullOrEmpty(_optionsMonitor.CurrentValue.FilePath)) | ||
{ | ||
string entryAssemblyDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)!; | ||
return Path.Combine(entryAssemblyDirectory, _optionsMonitor.CurrentValue.FilePath ?? string.Empty); | ||
} | ||
|
||
_logger.LogWarning("File path is not set"); | ||
return string.Empty; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the Apache 2.0 License. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Globalization; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
using Steeltoe.Management.Endpoint.Configuration; | ||
using Steeltoe.Management.Endpoint.Middleware; | ||
|
||
namespace Steeltoe.Management.Endpoint.Actuators.LogFile; | ||
|
||
internal sealed class LogFileEndpointMiddleware( | ||
ILogFileEndpointHandler endpointHandler, IOptionsMonitor<ManagementOptions> managementOptionsMonitor, ILoggerFactory loggerFactory) | ||
: EndpointMiddleware<LogFileEndpointRequest?, LogFileEndpointResponse>(endpointHandler, managementOptionsMonitor, loggerFactory) | ||
{ | ||
protected override async Task<LogFileEndpointResponse> InvokeEndpointHandlerAsync(LogFileEndpointRequest? request, CancellationToken cancellationToken) | ||
{ | ||
var logFileResponse = await EndpointHandler.InvokeAsync(request, cancellationToken); | ||
return logFileResponse; | ||
} | ||
|
||
protected override async Task<LogFileEndpointRequest?> ParseRequestAsync(HttpContext httpContext, CancellationToken cancellationToken) | ||
{ | ||
return httpContext.Request.Method.Equals(HttpMethod.Head.Method, StringComparison.OrdinalIgnoreCase) | ||
? await Task.FromResult(new LogFileEndpointRequest(null, null, false)) | ||
: null; | ||
} | ||
|
||
protected override async Task WriteResponseAsync(LogFileEndpointResponse response, HttpContext httpContext, CancellationToken cancellationToken) | ||
{ | ||
if (response.LogFileEncoding != null) | ||
{ | ||
httpContext.Response.ContentType = $"text/plain; charset={response.LogFileEncoding?.BodyName}"; | ||
} | ||
else | ||
{ | ||
httpContext.Response.ContentType = "text/plain;"; | ||
} | ||
|
||
httpContext.Response.ContentLength = response.ContentLength; | ||
httpContext.Response.StatusCode = StatusCodes.Status200OK; | ||
|
||
if (httpContext.Request.Method.Equals(HttpMethod.Get.Method, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
|
||
if (response.Content != null) | ||
{ | ||
await httpContext.Response.WriteAsync(response.Content, cancellationToken); | ||
} | ||
} | ||
else | ||
{ | ||
httpContext.Response.Headers.AcceptRanges = "bytes"; | ||
httpContext.Response.Headers.LastModified = response.LastModified?.ToString("R", CultureInfo.InvariantCulture); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the Apache 2.0 License. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using Steeltoe.Management.Configuration; | ||
|
||
namespace Steeltoe.Management.Endpoint.Actuators.LogFile; | ||
|
||
public sealed class LogFileEndpointOptions : EndpointOptions | ||
{ | ||
/// <summary> | ||
/// Gets or sets the path to the log file on disk. The path can be absolute, or relative to | ||
/// <see cref="System.Reflection.Assembly.GetEntryAssembly()" />. | ||
/// </summary> | ||
public string? FilePath { get; set; } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the Apache 2.0 License. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
namespace Steeltoe.Management.Endpoint.Actuators.LogFile; | ||
|
||
public class LogFileEndpointRequest(long? rangeStartByte, long? rangeLastByte, bool readContent) | ||
{ | ||
public long? RangeStartByte { get; } = rangeStartByte; | ||
public long? RangeLastByte { get; } = rangeLastByte; | ||
public bool ReadContent { get; } = readContent; | ||
Comment on lines
+9
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As described here, both the start and end positions are optional, so using nullable is good here. But let's make |
||
|
||
public LogFileEndpointRequest() | ||
: this(null, null, true) | ||
{ | ||
} | ||
|
||
public LogFileEndpointRequest(long? rangeStartByte, long? rangeLastByte) | ||
: this(rangeStartByte, rangeLastByte, true) | ||
{ | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the Apache 2.0 License. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Text; | ||
|
||
namespace Steeltoe.Management.Endpoint.Actuators.LogFile; | ||
|
||
public class LogFileEndpointResponse(string? content, long? contentLength, Encoding? logFileEncoding, DateTime? lastModified) | ||
{ | ||
public string? Content { get; } = content; | ||
public Encoding? LogFileEncoding { get; } = logFileEncoding; | ||
public long? ContentLength { get; } = contentLength; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on this, we may want to add the total file size, so it can be returned in the |
||
public DateTime? LastModified { get; } = lastModified; | ||
public LogFileEndpointResponse() | ||
: this(null, null, null, null) | ||
{ | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the Apache 2.0 License. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Runtime.CompilerServices; | ||
|
||
namespace Steeltoe.Management.Endpoint.Actuators.LogFile; | ||
|
||
internal static class LogFileStreamer | ||
{ | ||
public static async IAsyncEnumerable<byte[]> ReadLogFileAsync(string fullPath, [EnumeratorCancellation] CancellationToken cancellationToken) | ||
{ | ||
await using var fileStream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, useAsync: true); | ||
byte[] buffer = new byte[4096]; | ||
int bytesRead; | ||
|
||
while ((bytesRead = await fileStream.ReadAsync(buffer, cancellationToken)) > 0) | ||
{ | ||
yield return buffer[..bytesRead]; | ||
} | ||
} | ||
|
||
public static async IAsyncEnumerable<byte[]> ReadLogFileAsync(string fullPath, int startIndex, [EnumeratorCancellation] CancellationToken cancellationToken) | ||
{ | ||
await using var fileStream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, useAsync: true); | ||
byte[] buffer = new byte[4096]; | ||
int bytesRead; | ||
// Seek to the start index | ||
fileStream.Seek(startIndex, SeekOrigin.Begin); | ||
while ((bytesRead = await fileStream.ReadAsync(buffer, cancellationToken)) > 0) | ||
{ | ||
yield return buffer[..bytesRead]; | ||
} | ||
} | ||
|
||
public static async IAsyncEnumerable<byte[]> ReadLogFileAsync(string fullPath, int startIndex, int endIndex, [EnumeratorCancellation] CancellationToken cancellationToken) | ||
{ | ||
await using var fileStream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, useAsync: true); | ||
byte[] buffer = new byte[4096]; | ||
int bytesRead; | ||
int remainingBytes = endIndex - startIndex; | ||
|
||
// Seek to the start index | ||
fileStream.Seek(startIndex, SeekOrigin.Begin); | ||
|
||
while (remainingBytes > 0 && (bytesRead = await fileStream.ReadAsync(buffer.AsMemory(0, Math.Min(buffer.Length, remainingBytes)), cancellationToken)) > 0) | ||
{ | ||
yield return buffer[..bytesRead]; | ||
remainingBytes -= bytesRead; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EndpointOptions
provides a virtual methodGetDefaultAllowedVerbs
to set these. For example usage, seeLoggersEndpointOptions
.