Commit 8187443f authored by AntekZ's avatar AntekZ

initial

parents
# build output
bin/
obj/
# Visual Studio
.vs/
*.user
# logi
logs/
*.log.gz
\ No newline at end of file
<Solution>
<Project Path="LcdLogger/LcdLogger.csproj" />
</Solution>
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.8" />
</ItemGroup>
</Project>
@LcdLogger_HostAddress = http://localhost:5194
GET {{LcdLogger_HostAddress}}/weatherforecast/
Accept: application/json
###
using System.Text.Json.Serialization;
namespace LcdLogger.Models
{
public sealed class LogBatch
{
[JsonPropertyName("logs")]
public LogEntry[] Logs { get; set; } = [];
}
}
using System.Text.Json;
using System.Text.Json.Serialization;
namespace LcdLogger.Models
{
public sealed class LogEntry
{
[JsonPropertyName("timestamp")]
public long TimeStamp { get; set; }
//log level
[JsonPropertyName("level")]
public int Level { get; set; }
//service name/ component name that logged
[JsonPropertyName("source")]
public string Source { get; set; } = "";
[JsonPropertyName("message")]
public string Message { get; set; } = "";
[JsonPropertyName("data")]
public JsonElement? Data { get; set; }
}
}
namespace LcdLogger.Models
{
public sealed class LogOptions
{
public string Directory { get; set; } = "/var/log/lcd-logger";
public long RotateAtBytes { get; set; } = 20971520;
public long MaxTotalBytes { get; set; } = 104857600;
public int MinLevel { get; set; } = 2;
public int FlushEveryMs { get; set; } = 500;
public int MaxBatchEntries { get; set; } = 100;
public int QueueCapacity { get; set; } = 250;
}
}
using LcdLogger.Models;
using Microsoft.Extensions.Options;
using LcdLogger.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<LogOptions>(builder.Configuration.GetSection("Log"));
builder.Services.AddSingleton<LogQueue>();
builder.Services.AddSingleton<LogFileManager>();
builder.Services.AddHostedService<LogWorker>();
var app = builder.Build();
//sprawdzenie uzywanych appsettingsow przez serwis - moze useless i wywalic idk
app.MapGet("/debug/options", (IOptions<LogOptions> options) =>
Results.Ok(options.Value));
app.MapPost("/logs", (LogBatch batch, LogQueue queue, IOptions<LogOptions> opts) =>
{
if (batch.Logs.Length > opts.Value.MaxBatchEntries)
return Results.BadRequest($"Batch too large (max {opts.Value.MaxBatchEntries})");
queue.TryEnqueue(batch);
return Results.Accepted();
});
app.Run();
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5194",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7291;http://localhost:5194",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
using LcdLogger.Models;
using Microsoft.Extensions.Options;
using System.Text.Json;
using System.IO.Compression;
namespace LcdLogger.Services
{
public sealed class LogFileManager : IAsyncDisposable
{
private readonly ILogger<LogFileManager> _logger;
private readonly LogOptions _options;
private FileStream _fileStream = null!;
private string _currentPath = null!;
private long _size;
private static readonly byte[] _newline = new byte[] { (byte)'\n' };
public LogFileManager(IOptions<LogOptions> options, ILogger<LogFileManager> logger)
{
_options = options.Value;
_logger = logger;
}
public async Task InitializeAsync(CancellationToken cancellationToken = default)
{
Directory.CreateDirectory(_options.Directory);
await PruneOldFilesAsync(cancellationToken);
await OpenNewAsync(cancellationToken);
}
private Task OpenNewAsync(CancellationToken cancellationToken = default)
{
var timestamp = DateTime.UtcNow.ToString("yyyyMMddTHHmmssfffZ");
_currentPath = Path.Combine(_options.Directory, $"lcd-{timestamp}.log");
_fileStream = new FileStream(_currentPath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, bufferSize: 1, useAsync: true);
_size = 0;
return Task.CompletedTask;
}
public async Task WriteAsync(LogBatch batch, CancellationToken cancellationToken = default)
{
foreach (var entry in batch.Logs)
{
if (entry.Level < _options.MinLevel) continue;
var bytes = JsonSerializer.SerializeToUtf8Bytes(entry);
await _fileStream.WriteAsync(bytes, cancellationToken);
await _fileStream.WriteAsync(_newline, cancellationToken);
_size += bytes.Length + 1;
if(_size >= _options.RotateAtBytes)
{
await RotateFileAsync(cancellationToken);
}
}
}
private Task PruneOldFilesAsync(CancellationToken cancellationToken = default)
{
var DirectoryInfo = new DirectoryInfo(_options.Directory);
if(!DirectoryInfo.Exists)
{
_logger.LogError("Directory {Directory} does not exist", _options.Directory);
return Task.CompletedTask;
}
var files = DirectoryInfo.GetFiles("lcd-*.log*").OrderBy(f => f.Name).ToList();
long totalBytes = files.Sum(f => f.Length);
foreach (var file in files)
{
if (totalBytes <= _options.MaxTotalBytes) break;
try
{
file.Refresh();
if(file.Exists)
{
totalBytes -= file.Length;
file.Delete();
}
}
catch (Exception e)
{
_logger.LogError(e.Message);
}
}
return Task.CompletedTask;
}
private async Task RotateFileAsync(CancellationToken cancellationToken = default)
{
await _fileStream.FlushAsync(cancellationToken);
await _fileStream.DisposeAsync();
var justClosedFile = _currentPath;
_ = Task.Run(async () =>
{
await CompressAsync(justClosedFile);
});
await PruneOldFilesAsync(cancellationToken);
await OpenNewAsync(cancellationToken);
}
private async Task CompressAsync(string path)
{
try
{
var gzPath = path + ".gz";
using (var source = File.OpenRead(path))
using (var destination = File.Create(gzPath))
using (var gzip = new GZipStream(destination, CompressionLevel.SmallestSize))
{
await source.CopyToAsync(gzip);
}
File.Delete(path);
}
catch (Exception e)
{
_logger.LogError(e.Message, "Compress failed");
}
}
public async ValueTask DisposeAsync()
{
try
{
await _fileStream.FlushAsync();
await _fileStream.DisposeAsync();
}
catch(Exception e)
{
_logger.LogError(e.Message);
}
}
}
}
using LcdLogger.Models;
using Microsoft.Extensions.Options;
using System.Threading.Channels;
namespace LcdLogger.Services
{
public sealed class LogQueue
{
private readonly Channel<LogBatch> _channel;
//dostep do mechanizmu czytania buforu
public readonly ChannelReader<LogBatch> Reader;
public LogQueue(IOptions<LogOptions> options)
{
var channelOptions = new BoundedChannelOptions(capacity: options.Value.QueueCapacity)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = false
};
//bufor w pamieci Ram - trzyma kolejke batchy
_channel = Channel.CreateBounded<LogBatch>(channelOptions);
Reader = _channel.Reader;
}
// wpuszcza LogBatch z endpointu do kolejki
public bool TryEnqueue(LogBatch batch)
{
return _channel.Writer.TryWrite(batch);
}
}
}
namespace LcdLogger.Services
{
public sealed class LogWorker : BackgroundService
{
private readonly LogQueue _logQueue;
private readonly LogFileManager _logFileManager;
private readonly ILogger<LogWorker> _logger;
public LogWorker(ILogger<LogWorker> logger, LogQueue logQueue, LogFileManager logFileManager)
{
_logger = logger;
_logFileManager = logFileManager;
_logQueue = logQueue;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
await _logFileManager.InitializeAsync(stoppingToken);
_logger.LogInformation("LogWorker Started");
await foreach (var batch in _logQueue.Reader.ReadAllAsync(stoppingToken))
{
try
{
await _logFileManager.WriteAsync(batch);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to write batch with {Count} entries", batch.Logs.Length);
}
}
}
catch (Exception e)
{
_logger.LogCritical(e, "LogWriterWorker crashed");
}
finally
{
await _logFileManager.DisposeAsync();
_logger.LogInformation("LogWriterWorker stopped");
}
}
}
}
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
{
"Log": {
"Directory": "/var/log/lcd-logger",
"RotateAtBytes": 20971520,
"MaxTotalBytes": 104857600,
"MinLevel": 2,
"FlushEveryMs": 500,
"MaxBatchEntries": 100,
"QueueCapacity": 250
},
"Kestrel": {
"Endpoints": {
"Http": { "Url": "http://127.0.0.1:8082" }
},
"Limits": {
"MaxRequestBodySize": 2097152
}
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "127.0.0.1"
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment