Skip to content

Instantly share code, notes, and snippets.

@sunmeat
Created September 21, 2026 12:48
Show Gist options
  • Select an option

  • Save sunmeat/9a3a265a6e19ec800b077b478299d9a3 to your computer and use it in GitHub Desktop.

Select an option

Save sunmeat/9a3a265a6e19ec800b077b478299d9a3 to your computer and use it in GitHub Desktop.
логування у фаєрбейс з використанням бібліотеки Serilog
// 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()
};
}
}
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