/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Metrics; using System.Globalization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using System.CommandLine; using System.CommandLine.Help; using Opc.Ua; using Serilog; using Serilog.Events; #if NET5_0_OR_GREATER using Microsoft.Extensions.Configuration; #endif namespace Quickstarts { /// /// Simple console based telemetry /// public sealed class ConsoleTelemetry : ITelemetryContext, IDisposable { private readonly Action? m_configure; public ConsoleTelemetry(Action? configure = null) { m_configure = configure; LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory .Create(builder => { builder.SetMinimumLevel(LogLevel.Information); m_configure?.Invoke(builder); }); try { LoggerFactory = LoggerFactory.AddSerilog(Log.Logger); } catch { LoggerFactory.Dispose(); throw; } ActivitySource = new ActivitySource("Quickstarts", "1.0.0"); m_logger = LoggerFactory.CreateLogger("Main"); AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; TaskScheduler.UnobservedTaskException += Unobserved_TaskException; } /// public ILoggerFactory LoggerFactory { get; internal set; } /// public Meter CreateMeter() { return new Meter("Quickstarts", "1.0.0"); } /// public Meter CreateMeter(System.Reflection.Assembly assembly) { assembly.ThrowIfNull(nameof(assembly)); return CreateMeter(); } /// public ActivitySource ActivitySource { get; } /// public ActivitySource GetActivitySource(System.Reflection.Assembly assembly) { assembly.ThrowIfNull(nameof(assembly)); return ActivitySource; } /// public void Dispose() { CreateMeter().Dispose(); ActivitySource.Dispose(); LoggerFactory.Dispose(); AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException; TaskScheduler.UnobservedTaskException -= Unobserved_TaskException; } /// /// Configure the logging providers. /// /// /// Replaces the Opc.Ua.Core default ILogger with a /// Microsoft.Extension.Logger with a Serilog file, debug and console logger. /// The debug logger is only enabled for debug builds. /// The console logger is enabled by the logConsole flag at the consoleLogLevel. /// The file logger uses the setting in the ApplicationConfiguration. /// The Trace logLevel is chosen if required by the Tracemasks. /// /// The application configuration. /// The context name for the logger. /// Enable logging to the console. /// Enable logging to a file. /// Enable application logging. /// The LogLevel to use for the console/debug.< /// /param> public void ConfigureLogging( ApplicationConfiguration configuration, string context, bool logConsole, bool logFile, bool logApp, LogLevel consoleLogLevel) { if (!logApp) { return; } LoggerConfiguration loggerConfiguration = new LoggerConfiguration().Enrich .FromLogContext(); if (logConsole) { loggerConfiguration.WriteTo.Console( restrictedToMinimumLevel: (LogEventLevel)consoleLogLevel, formatProvider: CultureInfo.InvariantCulture); } #if DEBUG else { loggerConfiguration.WriteTo.Debug( restrictedToMinimumLevel: (LogEventLevel)consoleLogLevel, formatProvider: CultureInfo.InvariantCulture); } #endif LogLevel fileLevel = LogLevel.Information; // switch for Trace/Verbose output int traceMasks = configuration.TraceConfiguration!.TraceMasks; if ((traceMasks & ~( Utils.TraceMasks.Information | Utils.TraceMasks.Error | Utils.TraceMasks.Security | Utils.TraceMasks.StartStop | Utils.TraceMasks.StackTrace )) != 0) { fileLevel = LogLevel.Trace; } // add file logging if configured if (logFile) { string? outputFilePath = configuration.TraceConfiguration!.OutputFilePath; if (!string.IsNullOrWhiteSpace(outputFilePath)) { #pragma warning disable CA1305 // Specify IFormatProvider loggerConfiguration.WriteTo.File( Utils.ReplaceSpecialFolderNames(outputFilePath)!, restrictedToMinimumLevel: (LogEventLevel)fileLevel, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}", rollOnFileSizeLimit: true ); #pragma warning restore CA1305 // Specify IFormatProvider } } // adjust minimum level if (fileLevel < LogLevel.Information || consoleLogLevel < LogLevel.Information) { loggerConfiguration.MinimumLevel.Verbose(); } // create the serilog logger Serilog.Core.Logger serilogger = loggerConfiguration.CreateLogger(); ILoggerFactory oldLoggerFactory = LoggerFactory; LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory .Create(builder => { builder.SetMinimumLevel(consoleLogLevel); m_configure?.Invoke(builder); }); try { LoggerFactory = LoggerFactory.AddSerilog(serilogger); } catch { LoggerFactory.Dispose(); throw; } m_logger = LoggerFactory.CreateLogger("Main"); oldLoggerFactory.Dispose(); } private void CurrentDomain_UnhandledException( object? sender, UnhandledExceptionEventArgs args) { m_logger.UnhandledException( args.ExceptionObject as Exception, args.IsTerminating); } private void Unobserved_TaskException( object? sender, UnobservedTaskExceptionEventArgs args) { m_logger.UnobservedTaskException( args.Exception, args.Observed); } private Microsoft.Extensions.Logging.ILogger m_logger; } /// /// The error code why the application exit. /// public enum ExitCode { Ok = 0, ErrorNotStarted = 0x80, ErrorRunning = 0x81, ErrorException = 0x82, ErrorStopping = 0x83, ErrorCertificate = 0x84, ErrorInvalidCommandLine = 0x100 } /// /// An exception that occured and caused an exit of the application. /// [Serializable] public class ErrorExitException : Exception { public ExitCode ExitCode { get; } public ErrorExitException(ExitCode exitCode) { ExitCode = exitCode; } public ErrorExitException() { ExitCode = ExitCode.Ok; } public ErrorExitException(string message) : base(message) { ExitCode = ExitCode.Ok; } public ErrorExitException(string message, ExitCode exitCode) : base(message) { ExitCode = exitCode; } public ErrorExitException(string message, Exception innerException) : base(message, innerException) { ExitCode = ExitCode.Ok; } public ErrorExitException(string message, Exception innerException, ExitCode exitCode) : base(message, innerException) { ExitCode = exitCode; } } /// /// Helper functions shared in various console applications. /// public static class ConsoleUtils { /// /// Merges environment variables into the argument list for System.CommandLine. /// /// /// Converts environment settings to command line flags /// because in some environments (e.g. docker cloud) it is /// the only supported way to pass arguments. /// public static string[] MergeEnvironmentArgs( string[] args, string environmentPrefix, Command command) { #if NET5_0_OR_GREATER IConfigurationRoot config = new ConfigurationBuilder() .AddEnvironmentVariables(environmentPrefix + "_") .Build(); List argslist = [.. args]; foreach (Option option in command.Options) { if (option is HelpOption or VersionOption) { continue; } string name = option.Name.TrimStart('-'); if (name.Length >= 3) { string? envKey = config[name.ToUpperInvariant()]; if (envKey != null) { if (string.IsNullOrWhiteSpace(envKey)) { argslist.Add("--" + name); } else { argslist.Add("--" + name); argslist.Add(envKey); } } } } return [.. argslist]; #else return args; #endif } /// /// Create an event which is set if a user /// enters the Ctrl-C key combination. /// public static ManualResetEvent CtrlCHandler(CancellationTokenSource cts) { var quitEvent = new ManualResetEvent(false); try { Console.CancelKeyPress += (_, eArgs) => { cts.Cancel(); quitEvent.Set(); eArgs.Cancel = true; }; } catch { // intentionally left blank } return quitEvent; } } internal static partial class ConsoleTelemetryLog { [LoggerMessage(EventId = 9000, Level = LogLevel.Critical, Message = "Unhandled Exception: (IsTerminating: {IsTerminating})")] public static partial void UnhandledException( this Microsoft.Extensions.Logging.ILogger logger, Exception? exception, bool isTerminating); [LoggerMessage(EventId = 9001, Level = LogLevel.Critical, Message = "Unobserved Task Exception (Observed: {Observed})")] public static partial void UnobservedTaskException( this Microsoft.Extensions.Logging.ILogger logger, AggregateException exception, bool observed); } }