/* ======================================================================== * 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.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Mono.Options; using Opc.Ua; using Serilog; using Serilog.Events; using Serilog.Templates; #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); }) .AddSerilog(Log.Logger); 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 ActivitySource ActivitySource { get; } /// 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 #pragma warning disable CS0618 // Type or member is obsolete int traceMasks = configuration.TraceConfiguration.TraceMasks; #pragma warning restore CS0618 // Type or member is obsolete 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) { #pragma warning disable CS0618 // Type or member is obsolete string outputFilePath = configuration.TraceConfiguration.OutputFilePath; #pragma warning restore CS0618 // Type or member is obsolete if (!string.IsNullOrWhiteSpace(outputFilePath)) { loggerConfiguration.WriteTo.File( new ExpressionTemplate( "{UtcDateTime(@t):yyyy-MM-dd HH:mm:ss.fff} [{@l:u3}] {@m}\n{@x}"), Utils.ReplaceSpecialFolderNames(outputFilePath), restrictedToMinimumLevel: (LogEventLevel)fileLevel, rollOnFileSizeLimit: true ); } } // adjust minimum level if (fileLevel < LogLevel.Information || consoleLogLevel < LogLevel.Information) { loggerConfiguration.MinimumLevel.Verbose(); } // create the serilog logger Serilog.Core.Logger serilogger = loggerConfiguration.CreateLogger(); // Dispose the old LoggerFactory and create a new one with the updated configuration ILoggerFactory oldLoggerFactory = LoggerFactory; LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory .Create(builder => { builder.SetMinimumLevel(consoleLogLevel); m_configure?.Invoke(builder); }) .AddSerilog(serilogger); m_logger = LoggerFactory.CreateLogger("Main"); oldLoggerFactory.Dispose(); } private void CurrentDomain_UnhandledException( object sender, UnhandledExceptionEventArgs args) { m_logger.LogCritical( args.ExceptionObject as Exception, "Unhandled Exception: (IsTerminating: {IsTerminating})", args.IsTerminating); } private void Unobserved_TaskException( object sender, UnobservedTaskExceptionEventArgs args) { m_logger.LogCritical( args.Exception, "Unobserved Task Exception (Observed: {Observed})", 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 { /// /// Process a command line of the console sample application. /// /// public static string ProcessCommandLine( string[] args, Mono.Options.OptionSet options, ref bool showHelp, string environmentPrefix, bool noExtraArgs = true, TextWriter output = null) { output ??= Console.Out; #if NET5_0_OR_GREATER // Convert environment settings to command line flags // because in some environments (e.g. docker cloud) it is // the only supported way to pass arguments. IConfigurationRoot config = new ConfigurationBuilder() .AddEnvironmentVariables(environmentPrefix + "_") .Build(); List argslist = [.. args]; foreach (Option option in options) { string[] names = option.GetNames(); string longest = names.MaxBy(s => s.Length); if (longest != null && longest.Length >= 3) { string envKey = config[longest.ToUpperInvariant()]; if (envKey != null) { if (string.IsNullOrWhiteSpace(envKey) || option.OptionValueType == OptionValueType.None) { argslist.Add("--" + longest); } else { argslist.Add("--" + longest + "=" + envKey); } } } } args = [.. argslist]; #endif IList extraArgs = null; try { extraArgs = options.Parse(args); if (noExtraArgs) { foreach (string extraArg in extraArgs) { output.WriteLine("Error: Unknown option: {0}", extraArg); showHelp = true; } } } catch (OptionException e) { output.WriteLine(e.Message); showHelp = true; } if (showHelp) { options.WriteOptionDescriptions(output); throw new ErrorExitException( "Invalid Commandline or help requested.", ExitCode.ErrorInvalidCommandLine ); } return extraArgs.FirstOrDefault(); } /// /// 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; } } }