Skip to content

Commit dd69615

Browse files
authored
Merge pull request #46 from tgiachi/feature/lifecycle-logging
feat(bootstrap): lifecycle logging with AppName, versions and registration summary
2 parents 35301ac + 0818df2 commit dd69615

5 files changed

Lines changed: 403 additions & 5 deletions

File tree

docs/articles/concepts/bootstrap-lifecycle.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ builder.UseSquidStd(options => options.ConfigName = "myapp", c => c.RegisterCore
5555

5656
Services implementing `ISquidStdService` participate in the lifecycle. On `StartAsync` they are started in registration order; on `StopAsync` they are stopped in reverse order, so dependencies remain available while their dependents shut down.
5757

58+
The bootstrap logs its whole lifecycle: a startup banner with the application name and version (set `SquidStdOptions.AppName`; it defaults to the entry assembly name and is attached to every event as the `Application` / `ApplicationVersion` properties), a registration summary (per-registration detail at Debug), one line per service started with its duration, and the shutdown sequence. A service that fails to stop is logged as a warning and the remaining services are still stopped. Extra Serilog sinks can be plugged by registering `ILogEventSink` instances in the container before start.
59+
5860
```mermaid
5961
sequenceDiagram
6062
participant App

src/SquidStd.Core/Data/Bootstrap/SquidStdOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,10 @@ public sealed class SquidStdOptions
1414
/// Gets or sets the logical configuration name or YAML file name.
1515
/// </summary>
1616
public string ConfigName { get; set; } = "squidstd";
17+
18+
/// <summary>
19+
/// Gets or sets the application name used in the startup banner and as the Serilog
20+
/// "Application" property. When null or empty, the entry assembly name is used.
21+
/// </summary>
22+
public string? AppName { get; set; }
1723
}

src/SquidStd.Services.Core/Services/Bootstrap/SquidStdBootstrap.cs

Lines changed: 147 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
using System.Diagnostics;
2+
using System.Reflection;
13
using DryIoc;
24
using Serilog;
5+
using Serilog.Core;
6+
using Serilog.Events;
7+
using SquidStd.Abstractions.Data.Internal.Config;
38
using SquidStd.Abstractions.Data.Internal.Services;
49
using SquidStd.Abstractions.Interfaces.Services;
510
using SquidStd.Core.Data.Bootstrap;
@@ -159,8 +164,25 @@ public async ValueTask StartAsync(CancellationToken cancellationToken = default)
159164

160165
try
161166
{
167+
ConfigureLogging();
168+
169+
var logger = Log.ForContext<SquidStdBootstrap>();
170+
var appName = ResolveAppName(Options);
171+
172+
logger.Information(
173+
"{Application:l} {ApplicationVersion:l} starting (SquidStd {SquidStdVersion:l}, config {ConfigName}, root {RootDirectory})",
174+
appName,
175+
ResolveVersion(Assembly.GetEntryAssembly()),
176+
ResolveVersion(typeof(SquidStdBootstrap).Assembly),
177+
Options.ConfigName,
178+
Options.RootDirectory
179+
);
180+
162181
var registrations = GetServiceRegistrations();
182+
LogRegistrations(logger, registrations);
183+
163184
var startedInstances = new HashSet<ISquidStdService>(ReferenceEqualityComparer.Instance);
185+
var totalStopwatch = Stopwatch.StartNew();
164186

165187
for (var i = 0; i < registrations.Length; i++)
166188
{
@@ -173,14 +195,33 @@ public async ValueTask StartAsync(CancellationToken cancellationToken = default)
173195
continue;
174196
}
175197

176-
await service.StartAsync(cancellationToken);
177-
_startedServices.Add(service);
198+
var serviceStopwatch = Stopwatch.StartNew();
178199

179-
if (service is IConfigManagerService)
200+
try
180201
{
181-
ConfigureLogger();
202+
await service.StartAsync(cancellationToken);
182203
}
204+
catch (Exception ex)
205+
{
206+
logger.Error(ex, "Service {Service:l} failed to start", service.GetType().Name);
207+
208+
throw;
209+
}
210+
211+
_startedServices.Add(service);
212+
logger.Information(
213+
"Started {Service:l} in {Elapsed:0.#}ms",
214+
service.GetType().Name,
215+
serviceStopwatch.Elapsed.TotalMilliseconds
216+
);
183217
}
218+
219+
logger.Information(
220+
"{Application:l} started: {Count} service(s) in {TotalElapsed:0.#}ms",
221+
appName,
222+
_startedServices.Count,
223+
totalStopwatch.Elapsed.TotalMilliseconds
224+
);
184225
}
185226
catch
186227
{
@@ -198,13 +239,33 @@ public async ValueTask StopAsync(CancellationToken cancellationToken = default)
198239
return;
199240
}
200241

242+
var logger = Log.ForContext<SquidStdBootstrap>();
243+
var appName = ResolveAppName(Options);
244+
logger.Information("{Application:l} stopping ({Count} service(s))", appName, _startedServices.Count);
245+
201246
try
202247
{
203248
for (var i = _startedServices.Count - 1; i >= 0; i--)
204249
{
205250
cancellationToken.ThrowIfCancellationRequested();
206-
await _startedServices[i].StopAsync(cancellationToken);
251+
var service = _startedServices[i];
252+
253+
try
254+
{
255+
await service.StopAsync(cancellationToken);
256+
logger.Information("Stopped {Service:l}", service.GetType().Name);
257+
}
258+
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
259+
{
260+
throw;
261+
}
262+
catch (Exception ex)
263+
{
264+
logger.Warning(ex, "Service {Service:l} failed to stop", service.GetType().Name);
265+
}
207266
}
267+
268+
logger.Information("{Application:l} shutdown complete", appName);
208269
}
209270
finally
210271
{
@@ -251,6 +312,26 @@ public static SquidStdBootstrap Create(Action<SquidStdOptions> configure)
251312
return Create(options);
252313
}
253314

315+
private static string ResolveAppName(SquidStdOptions options)
316+
=> !string.IsNullOrWhiteSpace(options.AppName)
317+
? options.AppName
318+
: Assembly.GetEntryAssembly()?.GetName().Name ?? "SquidStd";
319+
320+
private static string ResolveVersion(Assembly? assembly)
321+
{
322+
var informational = assembly?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
323+
?.InformationalVersion;
324+
325+
if (string.IsNullOrWhiteSpace(informational))
326+
{
327+
return assembly?.GetName().Version?.ToString() ?? "0.0.0";
328+
}
329+
330+
var metadataStart = informational.IndexOf('+');
331+
332+
return metadataStart > 0 ? informational[..metadataStart] : informational;
333+
}
334+
254335
private void ConfigureLogger()
255336
{
256337
if (_loggerConfigured || !Container.IsRegistered<SquidStdLoggerOptions>())
@@ -261,6 +342,10 @@ private void ConfigureLogger()
261342
var options = Container.Resolve<SquidStdLoggerOptions>();
262343
var loggerConfiguration = new LoggerConfiguration();
263344

345+
loggerConfiguration
346+
.Enrich.WithProperty("Application", ResolveAppName(Options))
347+
.Enrich.WithProperty("ApplicationVersion", ResolveVersion(Assembly.GetEntryAssembly()));
348+
264349
if (options.MinimumLevel != LogLevelType.None)
265350
{
266351
var minimumLevel = options.MinimumLevel.ToSerilogLogLevel();
@@ -279,6 +364,11 @@ private void ConfigureLogger()
279364
restrictedToMinimumLevel: minimumLevel
280365
);
281366
}
367+
368+
foreach (var sink in Container.ResolveMany<ILogEventSink>())
369+
{
370+
loggerConfiguration.WriteTo.Sink(sink, minimumLevel);
371+
}
282372
}
283373

284374
var logger = loggerConfiguration.CreateLogger();
@@ -287,6 +377,58 @@ private void ConfigureLogger()
287377
_loggerConfigured = true;
288378
}
289379

380+
private void LogRegistrations(ILogger logger, ServiceRegistrationData[] lifecycleRegistrations)
381+
{
382+
List<ConfigRegistrationData> sections = Container.IsRegistered<List<ConfigRegistrationData>>()
383+
? Container.Resolve<List<ConfigRegistrationData>>()
384+
: [];
385+
var containerRegistrations = Container.GetServiceRegistrations().ToArray();
386+
387+
logger.Information(
388+
"Registered {LifecycleCount} lifecycle service(s), {SectionCount} config section(s), {ContainerCount} container registration(s)",
389+
lifecycleRegistrations.Length,
390+
sections.Count,
391+
containerRegistrations.Length
392+
);
393+
394+
if (!logger.IsEnabled(LogEventLevel.Debug))
395+
{
396+
return;
397+
}
398+
399+
foreach (var registration in lifecycleRegistrations)
400+
{
401+
logger.Debug(
402+
"Lifecycle service {Service} -> {Implementation} (priority {Priority})",
403+
registration.ServiceType.Name,
404+
registration.ImplementationType.Name,
405+
registration.Priority
406+
);
407+
}
408+
409+
foreach (var section in sections)
410+
{
411+
logger.Debug("Config section '{Section}' -> {Type}", section.SectionName, section.ConfigType.Name);
412+
}
413+
414+
foreach (var group in containerRegistrations
415+
.GroupBy(registration => registration.ServiceType.Assembly.GetName().Name ?? "unknown")
416+
.OrderBy(group => group.Key, StringComparer.Ordinal))
417+
{
418+
logger.Debug(
419+
"{Assembly}: {Count} registration(s): {Services}",
420+
group.Key,
421+
group.Count(),
422+
string.Join(
423+
", ",
424+
group.Select(registration => registration.ServiceType.Name)
425+
.Distinct()
426+
.Order(StringComparer.Ordinal)
427+
)
428+
);
429+
}
430+
}
431+
290432
private ServiceRegistrationData[] GetServiceRegistrations()
291433
{
292434
if (!Container.IsRegistered<List<ServiceRegistrationData>>())

0 commit comments

Comments
 (0)