Add NLog/File endpoint (#2639)

* Add log endpoint

* Update LogController.cs

* Address netf breaking

* Fixes & feedback

* THIS IS MADNESS

* Revert "THIS IS MADNESS"

This reverts commit 8359960314.

* Solve netf madness differently
This commit is contained in:
Łukasz Domeradzki 2022-07-03 01:20:43 +02:00 committed by GitHub
parent 04e14293ef
commit d899dbc18c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 110 additions and 2 deletions

View file

@ -32,6 +32,7 @@ using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.IPC.Responses;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.NLog;
using ArchiSteamFarm.NLog.Targets;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Mvc;
@ -43,6 +44,43 @@ namespace ArchiSteamFarm.IPC.Controllers.Api;
public sealed class NLogController : ArchiController {
private static readonly ConcurrentDictionary<WebSocket, (SemaphoreSlim Semaphore, CancellationToken CancellationToken)> ActiveLogWebSockets = new();
/// <summary>
/// Fetches ASF log file, this works on assumption that the log file is in fact generated, as user could disable it through custom configuration.
/// </summary>
/// <param name="count">Maximum amount of lines from the log file returned. The respone naturally might have less amount than specified, if you've read whole file already.</param>
/// <param name="lastAt">Ending index, used for pagination. Omit it for the first request, then initialize to TotalLines returned, and on every following request subtract count that you've used in the previous request from it until you hit 0 or less, which means you've read whole file already.</param>
[HttpGet("File")]
[ProducesResponseType(typeof(GenericResponse<GenericResponse<LogResponse>>), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.ServiceUnavailable)]
public async Task<ActionResult<GenericResponse>> FileGet(int count = 100, int lastAt = 0) {
if (count <= 0) {
return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(count))));
}
if (lastAt < 0) {
return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(lastAt))));
}
if (!Logging.LogFileExists) {
return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(SharedInfo.LogFile))));
}
string[]? lines = await Logging.ReadLogFileLines().ConfigureAwait(false);
if ((lines == null) || (lines.Length == 0)) {
return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(SharedInfo.LogFile))));
}
if ((lastAt == 0) || (lastAt > lines.Length)) {
lastAt = lines.Length;
}
int startFrom = Math.Max(lastAt - count, 0);
return Ok(new GenericResponse<LogResponse>(new LogResponse(lines.Length, lines[startFrom..lastAt])));
}
/// <summary>
/// Fetches ASF log in realtime.
/// </summary>
@ -52,7 +90,7 @@ public sealed class NLogController : ArchiController {
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<GenericResponse<string>>), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
public async Task<ActionResult> NLogGet(CancellationToken cancellationToken) {
public async Task<ActionResult> Get(CancellationToken cancellationToken) {
if (HttpContext == null) {
throw new InvalidOperationException(nameof(HttpContext));
}

View file

@ -0,0 +1,52 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
namespace ArchiSteamFarm.IPC.Responses;
public sealed class LogResponse {
/// <summary>
/// Content of the log file which consists of lines read from it - in chronological order.
/// </summary>
[JsonProperty(Required = Required.Always)]
[Required]
public IReadOnlyList<string> Content { get; private set; }
/// <summary>
/// Total number of lines of the log file returned, can be used as an index for future requests.
/// </summary>
[JsonProperty(Required = Required.Always)]
[Required]
public int TotalLines { get; private set; }
internal LogResponse(int totalLines, IReadOnlyList<string> content) {
if (totalLines < 0) {
throw new ArgumentOutOfRangeException(nameof(totalLines));
}
TotalLines = totalLines;
Content = content ?? throw new ArgumentNullException(nameof(content));
}
}

View file

@ -47,6 +47,8 @@ internal static class Logging {
private const string GeneralLayout = $@"${{date:format=yyyy-MM-dd HH\:mm\:ss}}|${{processname}}-${{processid}}|${{level:uppercase=true}}|{LayoutMessage}";
private const string LayoutMessage = @"${logger}|${message}${onexception:inner= ${exception:format=toString,Data}}";
internal static bool LogFileExists => File.Exists(SharedInfo.LogFile);
private static readonly ConcurrentHashSet<LoggingRule> ConsoleLoggingRules = new();
private static readonly SemaphoreSlim ConsoleSemaphore = new(1, 1);
@ -181,6 +183,10 @@ internal static class Logging {
CleanupFileName = false,
DeleteOldFileOnStartup = true,
FileName = Path.Combine("${currentdir}", SharedInfo.LogFile),
// For GET /Api/NLog/File ASF API usage on Windows (sigh)
KeepFileOpen = !OperatingSystem.IsWindows(),
Layout = GeneralLayout,
MaxArchiveFiles = 10
};
@ -235,6 +241,16 @@ internal static class Logging {
ArchiKestrel.OnNewHistoryTarget(historyTarget);
}
internal static async Task<string[]?> ReadLogFileLines() {
try {
return await File.ReadAllLinesAsync(SharedInfo.LogFile).ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
return null;
}
}
internal static void StartInteractiveConsole() {
Utilities.InBackground(HandleConsoleInteractively, true);
ASF.ArchiLogger.LogGenericInfo(Strings.InteractiveConsoleEnabled);

View file

@ -33,6 +33,7 @@
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
<PackageReference Include="JustArchiNET.Madness" />
<PackageReference Include="TA.System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray" />
<Using Include="JustArchiNET.Madness" />
<Using Include="JustArchiNET.Madness.ArgumentNullExceptionMadness.ArgumentNullException" Alias="ArgumentNullException" />

View file

@ -27,7 +27,7 @@
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
<PackageVersion Include="JustArchiNET.Madness" Version="3.6.0" />
<PackageVersion Include="JustArchiNET.Madness" Version="3.7.0" />
<PackageVersion Include="Microsoft.AspNetCore.Cors" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
@ -38,5 +38,6 @@
<PackageVersion Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="3.1.26" />
<PackageVersion Include="Microsoft.Extensions.Logging.Configuration" Version="3.1.26" />
<PackageVersion Include="TA.System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray" Version="1.0.1" />
</ItemGroup>
</Project>