Created
September 21, 2026 12:48
-
-
Save sunmeat/9a3a265a6e19ec800b077b478299d9a3 to your computer and use it in GitHub Desktop.
логування у фаєрбейс з використанням бібліотеки Serilog
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Soccer.WebAPI / Logging / FirestoreSink.cs: | |
| // FirestoreSink знаходиться у WebAPI, оскільки він відповідає не за роботу застосунку з Firestore, | |
| // а за збереження логів WebAPI у Firestore. Infrastructure містить доступ до даних, | |
| // а WebAPI.Logging відповідає за логування HTTP-застосунку | |
| using Google.Cloud.Firestore; | |
| using Serilog.Core; | |
| using Serilog.Events; | |
| namespace Soccer.WebAPI.Logging; | |
| public sealed class FirestoreSink : ILogEventSink | |
| { | |
| private readonly CollectionReference _collection; | |
| public FirestoreSink(FirestoreDb db) | |
| { | |
| // отримуємо посилання на колекцію logs, | |
| // у яку будуть записуватися всі вибрані події логування | |
| _collection = db.Collection("logs"); | |
| } | |
| public void Emit(LogEvent logEvent) | |
| { | |
| // створюємо документ Firestore з основними даними лог-події | |
| var document = new Dictionary<string, object?> | |
| { | |
| // зберігаємо точний час події у форматі Firestore Timestamp | |
| // UTC використовується для однозначного зберігання часу | |
| ["timestamp"] = Timestamp.FromDateTime( | |
| logEvent.Timestamp.UtcDateTime), | |
| // зберігаємо рівень логування: | |
| // Information, Warning, Error, Debug тощо | |
| ["level"] = logEvent.Level.ToString(), | |
| // RenderMessage() формує готовий текст повідомлення | |
| // з урахуванням параметрів, переданих у Log.Information(), Log.Error() тощо | |
| ["message"] = logEvent.RenderMessage(), | |
| // якщо під час операції виникла помилка, | |
| // зберігаємо повну інформацію про exception | |
| ["exception"] = logEvent.Exception?.ToString() | |
| }; | |
| // додаємо до документа всі structured properties, | |
| // які були передані разом із лог-подією | |
| // | |
| // наприклад: | |
| // Log.Information( | |
| // "Player {PlayerId} created", | |
| // player.Id); | |
| // | |
| // у Firestore додатково з'явиться: | |
| // PlayerId: ... | |
| foreach (var property in logEvent.Properties) | |
| { | |
| document[property.Key] = ConvertProperty(property.Value); | |
| } | |
| // перетворюємо UTC-час лог-події на локальний час України | |
| // "FLE Standard Time" використовується Windows для часового поясу | |
| // України з урахуванням переходу на літній/зимовий час | |
| var localTimestamp = TimeZoneInfo.ConvertTimeBySystemTimeZoneId( | |
| logEvent.Timestamp, | |
| "FLE Standard Time"); | |
| // формуємо читабельну частину ID документа: | |
| // рік.місяць.день_година:хвилина:секунда.мілісекунда | |
| // | |
| // приклад: | |
| // 2026.09.21_15:39:05.123 | |
| string timestamp = localTimestamp | |
| .ToString("yyyy.MM.dd_HH:mm:ss.fff"); | |
| // генеруємо короткий унікальний суфікс, | |
| // щоб два логи, створені в одну мілісекунду, | |
| // не отримали однаковий ID документа | |
| string suffix = Guid.NewGuid() | |
| .ToString("N")[..8]; | |
| // об'єднуємо час і унікальний суфікс в ID документа | |
| // | |
| // приклад: | |
| // 2026.09.21_15:39:05.123_a7f3c921 | |
| string documentId = $"{timestamp}_{suffix}"; | |
| // створюємо документ із заданим ID | |
| // і записуємо в нього сформовані дані лог-події | |
| // | |
| // _ = означає, що ми запускаємо асинхронний запис, | |
| // але не очікуємо його завершення в цьому методі | |
| _ = _collection | |
| .Document(documentId) | |
| .SetAsync(document); | |
| } | |
| private static object? ConvertProperty(LogEventPropertyValue value) | |
| { | |
| // Serilog використовує різні типи для structured properties. | |
| // тут перетворюємо їх у типи, які можна зберігати у Firestore. | |
| return value switch | |
| { | |
| // просте значення: | |
| // string, int, bool, double та інші scalar values | |
| ScalarValue scalar => scalar.Value, | |
| // послідовність значень: | |
| // перетворюємо її у звичайний список | |
| SequenceValue sequence => | |
| sequence.Elements | |
| .Select(ConvertProperty) | |
| .ToList(), | |
| // структурований об'єкт: | |
| // перетворюємо його properties у Dictionary | |
| StructureValue structure => | |
| structure.Properties.ToDictionary( | |
| p => p.Name, | |
| p => ConvertProperty(p.Value)), | |
| // словник: | |
| // перетворюємо ключі та значення у формат Dictionary, | |
| // який підтримує Firestore | |
| DictionaryValue dictionary => | |
| dictionary.Elements.ToDictionary( | |
| p => p.Key.Value?.ToString() ?? string.Empty, | |
| p => ConvertProperty(p.Value)), | |
| // резервний варіант для інших типів Serilog properties | |
| _ => value.ToString() | |
| }; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using Google.Apis.Auth.OAuth2; | |
| using Google.Cloud.Firestore; | |
| using Serilog; | |
| using Serilog.Context; | |
| using Soccer.Application.DependencyInjection; | |
| using Soccer.Infrastructure.DependencyInjection; | |
| using Soccer.Infrastructure.Persistence; | |
| using Soccer.WebAPI.Logging; | |
| using Soccer.WebAPI.Middleware; | |
| var builder = WebApplication.CreateBuilder(args); | |
| // шлях до файлу Firebase Service Account | |
| string firebasePath = Path.GetFullPath( | |
| Path.Combine( | |
| builder.Environment.ContentRootPath, | |
| "..", | |
| "Soccer.Infrastructure", | |
| "firebase.json")); | |
| // завантаження облікових даних Firebase з Service Account | |
| GoogleCredential credential = | |
| CredentialFactory | |
| .FromFile<ServiceAccountCredential>(firebasePath) | |
| .ToGoogleCredential(); | |
| // створення підключення до Google Cloud Firestore | |
| var firestoreDb = new FirestoreDbBuilder | |
| { | |
| // ідентифікатор Firebase / Google Cloud проєкту | |
| ProjectId = "alex-odesa", // !!! | |
| // облікові дані для доступу до Firestore | |
| Credential = credential | |
| }.Build(); | |
| Log.Logger = new LoggerConfiguration() | |
| .MinimumLevel.Information() | |
| .MinimumLevel.Override( | |
| "Microsoft.AspNetCore", | |
| Serilog.Events.LogEventLevel.Warning) | |
| .MinimumLevel.Override( | |
| "Microsoft.Hosting.Lifetime", | |
| Serilog.Events.LogEventLevel.Information) | |
| .Enrich.FromLogContext() | |
| .WriteTo.Console() | |
| .WriteTo.File( | |
| path: Path.Combine( | |
| builder.Environment.ContentRootPath, | |
| "logs", | |
| "log-.txt"), | |
| rollingInterval: RollingInterval.Day, | |
| retainedFileCountLimit: 14, | |
| outputTemplate: | |
| "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} " + | |
| "[{Level:u3}] {Message:lj}{NewLine}{Exception}") | |
| .WriteTo.Logger(lc => lc | |
| .Filter.ByIncludingOnly(e => | |
| e.Properties.ContainsKey("FirestoreOperation") || | |
| e.Properties.ContainsKey("RequestMethod") || | |
| e.Properties.ContainsKey("HttpRequest")) | |
| // передача логів у власний sink, який зберігає їх у Firestore | |
| .WriteTo.Sink(new FirestoreSink(firestoreDb))) | |
| .CreateLogger(); | |
| try | |
| { | |
| builder.Host.UseSerilog(); | |
| // передача шляху до Firebase credentials | |
| // для реєстрації Firestore-залежностей | |
| builder.Services.AddInfrastructure(firebasePath); | |
| builder.Services.AddApplication(); | |
| builder.Services.AddControllers(); | |
| var app = builder.Build(); | |
| app.UseSerilogRequestLogging(); | |
| app.UseMiddleware<RequestLoggingMiddleware>(); | |
| using (var scope = app.Services.CreateScope()) | |
| { | |
| var seeder = scope.ServiceProvider | |
| .GetRequiredService<FirestoreSeeder>(); | |
| // початкове наповнення колекцій Firestore тестовими даними | |
| await seeder.SeedAsync(); | |
| } | |
| app.MapControllers(); | |
| // створюємо окрему структуровану подію про підключення до Firestore | |
| using (LogContext.PushProperty( | |
| "FirestoreOperation", | |
| "Connection")) | |
| using (LogContext.PushProperty( | |
| "ProjectId", | |
| "alex-odesa")) | |
| { | |
| Log.Information( | |
| "Connected to Firestore project {ProjectId}", | |
| "alex-odesa"); | |
| } | |
| app.Run(); | |
| } | |
| catch (Exception ex) | |
| { | |
| Log.Fatal( | |
| ex, | |
| "Application terminated unexpectedly"); | |
| } | |
| finally | |
| { | |
| Log.CloseAndFlush(); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment