diff --git a/examples/.vscode/launch.json b/examples/.vscode/launch.json index d285367..5c74779 100644 --- a/examples/.vscode/launch.json +++ b/examples/.vscode/launch.json @@ -5,14 +5,14 @@ "version": "0.2.0", "configurations": [ { - "name": ".NET Core Launch (console)", + "name": ".NET Core Launch - .NET Generic Host", "type": "coreclr", "request": "launch", - "preLaunchTask": "build", + "preLaunchTask": "Build Example - .NET Generic Host", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/BasicConsoleAppILogger/bin/Debug/netcoreapp2.2/BasicConsoleAppILogger.dll", + "program": "${workspaceFolder}/GenericHost/src/bin/Debug/netcoreapp2.2/AIK8sGenericHost.dll", "args": [], - "cwd": "${workspaceFolder}/BasicConsoleAppILogger", + "cwd": "${workspaceFolder}/GenericHost/src", // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window "console": "internalConsole", "stopAtEntry": false diff --git a/examples/.vscode/tasks.json b/examples/.vscode/tasks.json index 2d37e05..b80f84f 100644 --- a/examples/.vscode/tasks.json +++ b/examples/.vscode/tasks.json @@ -79,5 +79,18 @@ "cwd": "${workspaceFolder}/WindowsContainer/" } }, + { + "label": "Build Example - .NET Generic Host", + "command": "dotnet", + "type": "shell", + "group": "build", + "problemMatcher":"$msCompile", + "args": [ + "build" + ], + "options": { + "cwd": "${workspaceFolder}/GenericHost/src/" + } + } ] } \ No newline at end of file diff --git a/examples/GenericHost/Readme.md b/examples/GenericHost/Readme.md new file mode 100644 index 0000000..17544a1 --- /dev/null +++ b/examples/GenericHost/Readme.md @@ -0,0 +1,150 @@ +# Use Application Insights for Kubernetes in .NET Generic Host project + +This is an example of enabling Application Insights for Kubernetes in .NET Generic Host project. If you are looking into enabling profiler on a plain Console Application, refer to [Enable Application Insights for Kubernetes in .NET Core Console Application](../BasicConsoleAppILogger/Readme.md). + +## Why .NET Generic Host + +According to [.NET Generic Host](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-2.2): + +> "The purpose of Generic Host is to decouple the HTTP pipeline from the Web Host API to enable a wider array of host scenarios. Messaging, background tasks, and other non-HTTP workloads based on Generic Host benefit from cross-cutting capabilities, such as configuration, dependency injection (DI), and logging." + +And also notice: +> Generic Host is new in ASP.NET Core 2.1 and isn't suitable for web hosting scenarios. For web hosting scenarios, use the Web Host. Generic Host will replace Web Host in a future release and act as the primary host API in both HTTP and non-HTTP scenarios. + +Now that you know what the .NET Generic Host is used for, lets dive in. + +## Walk-through + +### Create an empty console app + +```bash +dotnet new console -n AIK8sGenericHost +``` + +### Adding necessary packages + +```bash +dotnet add package Microsoft.Extensions.Hosting +dotnet add package Microsoft.Extensions.DependencyInjection +dotnet add package Microsoft.Extensions.Logging.ApplicationInsights +dotnet add package Microsoft.Extensions.Logging.Console +``` + +Reference to [AIK8sGenericHost.csproj](./src/AIK8sGenericHost.csproj) for the final results. + +### Set up a host + +```csharp +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace AIK8sGenericHost +{ + class Program + { + public static async Task Main(string[] args) + { + var host = new HostBuilder() + .Build(); + + await host.RunAsync(); + } + } +} +``` + +### Enable Application Insights and its Kubernetes enricher + +```csharp + var host = new HostBuilder() + .ConfigureServices((context, services) => + { + var channel = new InMemoryChannel(); + + // Add application insights for Kubernetes. Making sure this is called before services.Configure(). + services.AddApplicationInsightsKubernetesEnricher(); + + services.Configure( + (config) => + { + config.TelemetryChannel = channel; + } + ); + + // Add the logging pipelines to use. We are using Application Insights only here. + services.AddLogging(builder => + { + // Optional: Apply filters to configure LogLevel Trace or above is sent to + // Application Insights for all categories. + builder.AddFilter + ("", LogLevel.Trace); + builder.AddApplicationInsights("--YourAIKeyHere--"); + }); + + // Register your services that implemented IHostedService interface. For example, SendAIEventService. You will need to uncomment this later. + // services.AddHostedService(); + }).Build(); +``` + +Largely, those implementations come from [ApplicationInsightsLoggerProvider for .NET Core ILogger logs](https://docs.microsoft.com/en-us/azure/azure-monitor/app/ilogger). + +### Create a hosted service + +Refer to [SendAIEventService](./src/SendAIEventService.cs) for the implementation. + +Now that the service is created, it could be registered into the dependency injection container: + +```csharp +// Register your services that implemented IHostedService interface. For example, SendAIEventService. +services.AddHostedService(); +``` + +### Flush the channel with lifetime methods + +Since the telemetry won't necessary be flushed, we should flush the channel at the end of the life cycle of the host. +One way of doing that is to register to the application lifetime: + +```csharp + IApplicationLifetime lifetime = host.Services.GetRequiredService(); + lifetime.ApplicationStopping.Register(() => + { + channel.Flush(); + // Work around Application Insights issue: + // https://github.com/Microsoft/ApplicationInsights-dotnet/issues/407 + System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2)); + }); +``` + +### Containerize the application + +* Create a docker file like [this](./src/Dockerfile). +* Build and push the images: + +```bash +docker build -t dockeraccount/aik8sgenerichost . +docker push dockeraccount/aik8sgenerichost:latest +``` + +### Deploy it to Kubernetes + +* Create a Kubernetes deployment file like [this](./src/K8s.yaml). +* Deploy it: + +```bash +kubectl create -f K8s.yaml +``` + +For RBAC enabled Kubernetes cluster (AKS for example), you will need to have proper service account bindings. Refer to [here](../BasicUsage_clr21_RBAC/README.MD#setup-the-default-service-account-for-rbac-enabled-cluster) to see how to do it. + +### Result + +Go to the application insights and you will see traces with kubernetes properties. + +![Traces in Application Insights](./media/result.png) + +## References + +* [.NET Generic Host](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-2.2) +* [USING HOSTBUILDER AND THE GENERIC HOST IN .NET CORE MICROSERVICES](https://www.stevejgordon.co.uk/using-generic-host-in-dotnet-core-console-based-microservices) +* [ApplicationInsightsLoggerProvider for .NET Core ILogger logs](https://docs.microsoft.com/en-us/azure/azure-monitor/app/ilogger) + * Specifically, [Console application](https://docs.microsoft.com/en-us/azure/azure-monitor/app/ilogger#console-application) part. diff --git a/examples/GenericHost/media/result.png b/examples/GenericHost/media/result.png new file mode 100644 index 0000000..db77db6 Binary files /dev/null and b/examples/GenericHost/media/result.png differ diff --git a/examples/GenericHost/src/AIK8sGenericHost.csproj b/examples/GenericHost/src/AIK8sGenericHost.csproj new file mode 100644 index 0000000..92924b6 --- /dev/null +++ b/examples/GenericHost/src/AIK8sGenericHost.csproj @@ -0,0 +1,16 @@ + + + + Exe + netcoreapp2.2 + + + + + + + + + + + diff --git a/examples/GenericHost/src/Dockerfile b/examples/GenericHost/src/Dockerfile new file mode 100644 index 0000000..2b7f9a9 --- /dev/null +++ b/examples/GenericHost/src/Dockerfile @@ -0,0 +1,16 @@ +FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS build-env +WORKDIR /app + +# Copy csproj and restore as distinct layers +COPY *.csproj ./ +RUN dotnet restore + +# Copy everything else and build +COPY . ./ +RUN dotnet publish -c Release -o out + +# Build runtime image +FROM mcr.microsoft.com/dotnet/core/runtime:2.2 +WORKDIR /app +COPY --from=build-env /app/out . +ENTRYPOINT ["dotnet", "AIK8sGenericHost.dll"] \ No newline at end of file diff --git a/examples/GenericHost/src/K8s.yaml b/examples/GenericHost/src/K8s.yaml new file mode 100644 index 0000000..12b0ddf --- /dev/null +++ b/examples/GenericHost/src/K8s.yaml @@ -0,0 +1,14 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: ai-k8s-generic-host +spec: + replicas: 1 + template: + metadata: + labels: + app: consoleapp + spec: + containers: + - name: ai-k8s-generic-host-container + image: dockeraccount/aik8sgenerichost:latest \ No newline at end of file diff --git a/examples/GenericHost/src/Program.cs b/examples/GenericHost/src/Program.cs new file mode 100644 index 0000000..e38b36a --- /dev/null +++ b/examples/GenericHost/src/Program.cs @@ -0,0 +1,63 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.ApplicationInsights.Channel; +using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.ApplicationInsights.Kubernetes.Debugging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +namespace AIK8sGenericHost +{ + class Program + { + public static async Task Main(string[] args) + { + // Channel is explicitly configured to do flush on it later. + var channel = new InMemoryChannel(); + + var host = new HostBuilder() + .ConfigureServices((context, services) => + { + // Uncomment the following lines for debugging AI.K8s. + // Refer to https://github.com/microsoft/ApplicationInsights-Kubernetes/blob/develop/docs/SelfDiagnostics.MD for details. + // var observer = new ApplicationInsightsKubernetesDiagnosticObserver(DiagnosticLogLevel.Trace); + // ApplicationInsightsKubernetesDiagnosticSource.Instance.Observable.SubscribeWithAdapter(observer); + + // Add application insights for Kubernetes. Making sure this is called before services.Configure(). + services.AddApplicationInsightsKubernetesEnricher(); + + services.Configure( + (config) => + { + config.TelemetryChannel = channel; + } + ); + + // Add the logging pipelines to use. We are using Application Insights only here. + services.AddLogging(builder => + { + // Optional: Apply filters to configure LogLevel Trace or above is sent to + // Application Insights for all categories. + builder.AddFilter + ("", LogLevel.Trace); + builder.AddApplicationInsights("----Your instrumentation key----"); + builder.AddConsole(); + }); + + // Register your services that implemented IHostedService interface. For example, SendAIEventService + services.AddHostedService(); + }).Build(); + + IApplicationLifetime lifetime = host.Services.GetRequiredService(); + lifetime.ApplicationStopping.Register(() => + { + channel.Flush(); + // Work around Application Insights issue: + // https://github.com/Microsoft/ApplicationInsights-dotnet/issues/407 + System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2)); + }); + await host.RunAsync(); + } + } +} diff --git a/examples/GenericHost/src/SendAIEventService.cs b/examples/GenericHost/src/SendAIEventService.cs new file mode 100644 index 0000000..b7261bc --- /dev/null +++ b/examples/GenericHost/src/SendAIEventService.cs @@ -0,0 +1,37 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace AIK8sGenericHost +{ + public class SendAIEventService : IHostedService + { + private readonly ILogger _logger; + + public SendAIEventService(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + public Task StartAsync(CancellationToken cancellationToken) + { + Task _ = Task.Run(async () => await RunUntilCancelledAsync(cancellationToken)); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public async Task RunUntilCancelledAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + _logger.LogInformation("Application Insights for Kubernetes is working . . ."); + await Task.Delay(5000).ConfigureAwait(false); + } + } + } +} \ No newline at end of file