Skip to content

Commit 2d9d69a

Browse files
committed
ReadMe
1 parent b277764 commit 2d9d69a

File tree

3 files changed

+197
-25
lines changed

3 files changed

+197
-25
lines changed

ReadMe.md

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,21 @@ The default timeout is 5 seconds, but it can be changed using `ConsoleApp.Timeou
659659

660660
The hooking behavior using `PosixSignalRegistration` is determined by the presence of a `CancellationToken` (or always takes effect if a filter is set). Therefore, even for synchronous methods, it is possible to change the behavior by including a `CancellationToken` as an argument.
661661

662+
In the case of `Run/RunAsync` from `ConsoleAppBuilder`, you can also pass a CancellationToken. This is combined with PosixSignalRegistration and passed to each method. This makes it possible to cancel at any arbitrary timing.
663+
664+
```csharp
665+
var cancellationTokenSource = new CancellationTokenSource();
666+
667+
var app = ConsoleApp.Create();
668+
669+
app.Add("", (CancellationToken cancellationToken) =>
670+
{
671+
// do anything...
672+
});
673+
674+
await app.RunAsync(args, cancellationTokenSource.Token); // pass external CancellationToken
675+
```
676+
662677
Exit Code
663678
---
664679
If the method returns `int` or `Task<int>`, `ConsoleAppFramework` will set the return value to the exit code. Due to the nature of code generation, when writing lambda expressions, you need to explicitly specify either `int` or `Task<int>`.
@@ -1107,7 +1122,167 @@ var app = Host.CreateApplicationBuilder()
11071122

11081123
In this case, it builds the HostBuilder, creates a Scope for the ServiceProvider, and disposes of all of them after execution.
11091124

1110-
ConsoleAppFramework has its own lifetime management (see the [CancellationToken(Gracefully Shutdown) and Timeout](#cancellationtokengracefully-shutdown-and-timeout) section), so Host's Start/Stop is not necessary.
1125+
ConsoleAppFramework has its own lifetime management (see the [CancellationToken(Gracefully Shutdown) and Timeout](#cancellationtokengracefully-shutdown-and-timeout) section), therefore it is handled correctly even without using `ConsoleLifetime`.
1126+
1127+
OpenTelemetry
1128+
---
1129+
It's important to be conscious of observability in console applications as well. Visualizing not just logging but also traces will be helpful for performance tuning and troubleshooting. In ConsoleAppFramework, you can use this smoothly by utilizing the OpenTelemetry support of HostApplicationBuilder.
1130+
1131+
```xml
1132+
<!-- csproj, reference OpenTelemetry packages -->
1133+
<ItemGroup>
1134+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" />
1135+
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
1136+
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
1137+
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.9.0" />
1138+
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.9.0" />
1139+
</ItemGroup>
1140+
```
1141+
1142+
For command tracing, you can set up the trace root by preparing a filter like the following. Also, when using multiple filters in an application, if you start all activities, it becomes very convenient as you can visualize the execution status of the filters.
1143+
1144+
```csharp
1145+
public static class ConsoleAppFrameworkSampleActivitySource
1146+
{
1147+
public const string Name = "ConsoleAppFrameworkSample";
1148+
1149+
public static ActivitySource Instance { get; } = new ActivitySource(Name);
1150+
}
1151+
1152+
public class CommandTracingFilter(ConsoleAppFilter next) : ConsoleAppFilter(next)
1153+
{
1154+
public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)
1155+
{
1156+
using var activity = ConsoleAppFrameworkSampleActivitySource.Instance.StartActivity("CommandStart");
1157+
1158+
if (activity == null) // Telemtry is not listened
1159+
{
1160+
await Next.InvokeAsync(context, cancellationToken);
1161+
}
1162+
else
1163+
{
1164+
activity.SetTag("console_app.command_name", context.CommandName);
1165+
activity.SetTag("console_app.command_args", string.Join(" ", context.EscapedArguments));
1166+
1167+
try
1168+
{
1169+
await Next.InvokeAsync(context, cancellationToken);
1170+
activity.SetStatus(ActivityStatusCode.Ok);
1171+
}
1172+
catch (Exception ex)
1173+
{
1174+
if (ex is OperationCanceledException)
1175+
{
1176+
activity.SetStatus(ActivityStatusCode.Error, "Canceled");
1177+
}
1178+
else
1179+
{
1180+
activity.AddException(ex);
1181+
activity.SetStatus(ActivityStatusCode.Error);
1182+
}
1183+
throw;
1184+
}
1185+
}
1186+
}
1187+
}
1188+
```
1189+
1190+
For visualization, if your solution includes a web application, using .NET Aspire would be convenient during development. For production environments, there are solutions like Datadog and New Relic, as well as OSS tools from Grafana Labs. However, especially for local development, I think OpenTelemetry native all-in-one solutions are convenient. Here, let's look at tracing using the OSS [SigNoz](https://signoz.io/).
1191+
1192+
```csharp
1193+
using ConsoleAppFramework;
1194+
using Microsoft.Extensions.DependencyInjection;
1195+
using Microsoft.Extensions.Hosting;
1196+
using Microsoft.Extensions.Logging;
1197+
using OpenTelemetry.Logs;
1198+
using OpenTelemetry.Metrics;
1199+
using OpenTelemetry.Resources;
1200+
using OpenTelemetry.Trace;
1201+
using System.Diagnostics;
1202+
1203+
// git clone https://github.com/SigNoz/signoz.git
1204+
// cd signoz/deploy/docker
1205+
// docker compose up
1206+
Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"); // 4317 or 4318
1207+
1208+
var builder = Host.CreateApplicationBuilder(args);
1209+
1210+
builder.Logging.AddOpenTelemetry(logging =>
1211+
{
1212+
logging.IncludeFormattedMessage = true;
1213+
logging.IncludeScopes = true;
1214+
});
1215+
1216+
builder.Services.AddOpenTelemetry()
1217+
.ConfigureResource(resource =>
1218+
{
1219+
resource.AddService("ConsoleAppFramework Telemetry Sample");
1220+
})
1221+
.WithMetrics(metrics =>
1222+
{
1223+
metrics.AddRuntimeInstrumentation()
1224+
.AddHttpClientInstrumentation()
1225+
.AddOtlpExporter();
1226+
})
1227+
.WithTracing(tracing =>
1228+
{
1229+
tracing.SetSampler(new AlwaysOnSampler())
1230+
.AddHttpClientInstrumentation()
1231+
.AddSource(ConsoleAppFrameworkSampleActivitySource.Name) // add trace source
1232+
.AddOtlpExporter();
1233+
})
1234+
.WithLogging(logging =>
1235+
{
1236+
logging.AddOtlpExporter();
1237+
});
1238+
1239+
var app = builder.ToConsoleAppBuilder();
1240+
1241+
var consoleAppLoger = ConsoleApp.ServiceProvider.GetRequiredService<ILogger<Program>>(); // already built service provider.
1242+
ConsoleApp.Log = msg => consoleAppLoger.LogDebug(msg);
1243+
ConsoleApp.LogError = msg => consoleAppLoger.LogError(msg);
1244+
1245+
app.UseFilter<CommandTracingFilter>(); // use root Trace filter
1246+
1247+
app.Add("", async ([FromServices] ILogger<Program> logger, CancellationToken cancellationToken) =>
1248+
{
1249+
using var httpClient = new HttpClient();
1250+
var ms = await httpClient.GetStringAsync("https://www.microsoft.com", cancellationToken);
1251+
logger.LogInformation(ms);
1252+
var google = await httpClient.GetStringAsync("https://www.google.com", cancellationToken);
1253+
logger.LogInformation(google);
1254+
1255+
var ms2 = httpClient.GetStringAsync("https://www.microsoft.com", cancellationToken);
1256+
var google2 = httpClient.GetStringAsync("https://www.google.com", cancellationToken);
1257+
var apple2 = httpClient.GetStringAsync("https://www.apple.com", cancellationToken);
1258+
await Task.WhenAll(ms2, google2, apple2);
1259+
1260+
logger.LogInformation(apple2.Result);
1261+
1262+
logger.LogInformation("OK");
1263+
});
1264+
1265+
await app.RunAsync(args); // Run
1266+
```
1267+
1268+
When you launch `SigNoz` with docker, the view will be available at `http://localhost:8080/` and the collector at `http://localhost:4317`.
1269+
1270+
If you configure OpenTelemetry-related settings with `Host.CreateApplicationBuilder` and convert it with `ToConsoleAppBuilder`, `ConsoleAppFramework` will naturally support OpenTelemetry. In the example above, HTTP communications are performed sequentially, then executed in 3 parallel operations.
1271+
1272+
![](https://github.com/user-attachments/assets/51009748-28f1-46c6-a70a-7300e825ae5e)
1273+
1274+
In `SigNoz`, besides Trace, Logs and Metrics can also be visualized in an easy-to-read manner.
1275+
1276+
Prevent ServiceProvider auto dispose
1277+
---
1278+
When executing commands with `Run/RunAsync`, the ServiceProvider is automatically disposed. This becomes a problem when executing commands multiple times or when you want to use the ServiceProvider after the command finishes. In Run/RunAsync from ConsoleAppBuilder, you can stop the automatic disposal of ServiceProvider by setting `bool disposeServiceProvider` to `false`.
1279+
1280+
```csharp
1281+
var app = ConsoleApp.Create();
1282+
await app.RunAsync(args, disposeServiceProvider: false); // default is true
1283+
```
1284+
1285+
When `Microsoft.Extensions.Hosting` is referenced, `bool startHost, bool stopHost, bool disposeServiceProvider` become controllable. The defaults are all `true`.
11111286

11121287
Colorize
11131288
---

sandbox/GeneratorSandbox/Program.cs

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,8 @@
5353

5454
app.UseFilter<CommandTracingFilter>();
5555

56-
app.Add("", async ([FromServices] ILogger<Program> logger/*, CancellationToken cancellationToken*/) =>
56+
app.Add("", async ([FromServices] ILogger<Program> logger, CancellationToken cancellationToken) =>
5757
{
58-
var cancellationToken = CancellationToken.None;
59-
6058
using var httpClient = new HttpClient();
6159
var ms = await httpClient.GetStringAsync("https://www.microsoft.com", cancellationToken);
6260
logger.LogInformation(ms);
@@ -73,7 +71,7 @@
7371
logger.LogInformation("OK");
7472
});
7573

76-
await app.RunAsync(args);
74+
await app.RunAsync(args); // Run
7775

7876
public static class ConsoleAppFrameworkSampleActivitySource
7977
{
@@ -88,23 +86,22 @@ internal class CommandTracingFilter(ConsoleAppFilter next) : ConsoleAppFilter(ne
8886
public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)
8987
{
9088
using var activity = ConsoleAppFrameworkSampleActivitySource.Instance.StartActivity("CommandStart");
91-
if (activity != null)
92-
{
93-
activity.SetTag("console_app.command_name", context.CommandName);
94-
activity.AddBaggage("console_app.command_args", string.Join(" ", context.EscapedArguments));
95-
}
9689

97-
try
90+
if (activity == null) // Telemtry is not listened
9891
{
9992
await Next.InvokeAsync(context, cancellationToken);
100-
if (activity != null)
93+
}
94+
else
95+
{
96+
activity.SetTag("console_app.command_name", context.CommandName);
97+
activity.SetTag("console_app.command_args", string.Join(" ", context.EscapedArguments));
98+
99+
try
101100
{
101+
await Next.InvokeAsync(context, cancellationToken);
102102
activity.SetStatus(ActivityStatusCode.Ok);
103103
}
104-
}
105-
catch (Exception ex)
106-
{
107-
if (activity != null)
104+
catch (Exception ex)
108105
{
109106
if (ex is OperationCanceledException)
110107
{
@@ -115,8 +112,8 @@ public override async Task InvokeAsync(ConsoleAppContext context, CancellationTo
115112
activity.AddException(ex);
116113
activity.SetStatus(ActivityStatusCode.Error);
117114
}
115+
throw;
118116
}
119-
throw;
120117
}
121118
}
122119
}

src/ConsoleAppFramework/ConsoleAppBaseCode.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ internal partial class ConsoleAppBuilder
549549
public void Run(string[] args) => Run(args, true);
550550
public void Run(string[] args, CancellationToken cancellationToken) => Run(args, true, cancellationToken);
551551
552-
public void Run(string[] args, bool disposeService, CancellationToken cancellationToken = default)
552+
public void Run(string[] args, bool disposeServiceProvider, CancellationToken cancellationToken = default)
553553
{
554554
BuildAndSetServiceProvider();
555555
try
@@ -558,7 +558,7 @@ public void Run(string[] args, bool disposeService, CancellationToken cancellati
558558
}
559559
finally
560560
{
561-
if (disposeService)
561+
if (disposeServiceProvider)
562562
{
563563
if (ServiceProvider is IDisposable d)
564564
{
@@ -571,7 +571,7 @@ public void Run(string[] args, bool disposeService, CancellationToken cancellati
571571
public Task RunAsync(string[] args) => RunAsync(args, true);
572572
public Task RunAsync(string[] args, CancellationToken cancellationToken) => RunAsync(args, true, cancellationToken);
573573
574-
public async Task RunAsync(string[] args, bool disposeService, CancellationToken cancellationToken = default)
574+
public async Task RunAsync(string[] args, bool disposeServiceProvider, CancellationToken cancellationToken = default)
575575
{
576576
BuildAndSetServiceProvider();
577577
try
@@ -585,7 +585,7 @@ public async Task RunAsync(string[] args, bool disposeService, CancellationToken
585585
}
586586
finally
587587
{
588-
if (disposeService)
588+
if (disposeServiceProvider)
589589
{
590590
if (ServiceProvider is IAsyncDisposable ad)
591591
{
@@ -626,7 +626,7 @@ internal partial class ConsoleAppBuilder
626626
public void Run(string[] args) => Run(args, true, true, true);
627627
public void Run(string[] args, CancellationToken cancellationToken) => Run(args, true, true, true, cancellationToken);
628628
629-
public void Run(string[] args, bool startHost, bool stopHost, bool disposeService, CancellationToken cancellationToken = default)
629+
public void Run(string[] args, bool startHost, bool stopHost, bool disposeServiceProvider, CancellationToken cancellationToken = default)
630630
{
631631
BuildAndSetServiceProvider();
632632
Microsoft.Extensions.Hosting.IHost? host = ConsoleApp.ServiceProvider?.GetService(typeof(Microsoft.Extensions.Hosting.IHost)) as Microsoft.Extensions.Hosting.IHost;
@@ -644,7 +644,7 @@ public void Run(string[] args, bool startHost, bool stopHost, bool disposeServic
644644
{
645645
host?.StopAsync().GetAwaiter().GetResult();
646646
}
647-
if (disposeService)
647+
if (disposeServiceProvider)
648648
{
649649
if (ServiceProvider is IDisposable d)
650650
{
@@ -657,7 +657,7 @@ public void Run(string[] args, bool startHost, bool stopHost, bool disposeServic
657657
public Task RunAsync(string[] args) => RunAsync(args, true, true, true);
658658
public Task RunAsync(string[] args, CancellationToken cancellationToken) => RunAsync(args, true, true, true, cancellationToken);
659659
660-
public async Task RunAsync(string[] args, bool startHost, bool stopHost, bool disposeService, CancellationToken cancellationToken = default)
660+
public async Task RunAsync(string[] args, bool startHost, bool stopHost, bool disposeServiceProvider, CancellationToken cancellationToken = default)
661661
{
662662
BuildAndSetServiceProvider();
663663
Microsoft.Extensions.Hosting.IHost? host = ConsoleApp.ServiceProvider?.GetService(typeof(Microsoft.Extensions.Hosting.IHost)) as Microsoft.Extensions.Hosting.IHost;
@@ -680,7 +680,7 @@ public async Task RunAsync(string[] args, bool startHost, bool stopHost, bool di
680680
{
681681
await host?.StopAsync();
682682
}
683-
if (disposeService)
683+
if (disposeServiceProvider)
684684
{
685685
if (ServiceProvider is IAsyncDisposable ad)
686686
{

0 commit comments

Comments
 (0)