/// <summary> /// Process the input log information to remove all unauthorized information /// </summary> /// <param name="className">usually the class that is calling the method</param> /// <param name="method">usually denotes method calling this method</param> /// <param name="message">informational message</param> /// <param name="logCode">error code, usually empty</param> /// <param name="corpusPath">usually denotes corpus path of document</param> /// <param name="correlationId">corpus correlation id</param> /// <param name="apiCorrelationId">method correlation id</param> /// <param name="appId">app id assigned by user</param> /// <returns>A complete log entry</returns> private string ProcessLogEntry(string timestamp, string className, string method, string message, CdmLogCode logCode, string corpusPath, string correlationId, Guid apiCorrelationId, string appId) { // Remove user created contents if (this.config.IngestAtLevel == EnvironmentType.PROD || this.config.IngestAtLevel == EnvironmentType.TEST) { corpusPath = null; } if (message == null) { message = ""; } // Remove all commas from message to ensure the correct syntax of Kusto query message = message.Replace(",", ";"); string code = logCode.ToString(); // Additional properties associated with the log Dictionary <string, string> property = new Dictionary <string, string>(); property.Add("Environment", this.config.IngestAtLevel.ToString()); property.Add("SDKLanguage", "CSharp"); property.Add("Region", this.config.Region); string propertyJson = SerializeDictionary(property); string entry = $"{timestamp},{className},{method},{message},{code},{corpusPath},{correlationId},{apiCorrelationId},{appId},{propertyJson}\n"; return(entry); }
/// <summary> /// Enqueue the request queue with the information to be logged /// </summary> /// <param name="timestamp">The log timestamp</param> /// <param name="level">Logging status level</param> /// <param name="className">Usually the class that is calling the method</param> /// <param name="method">Usually denotes method calling this method</param> /// <param name="corpusPath">Usually denotes corpus path of document</param> /// <param name="message">Informational message</param> /// <param name="requireIngestion">(Optional) Whether the log needs to be ingested</param> /// <param name="code">(Optional) Error or warning code</param> public void AddToIngestionQueue(string timestamp, CdmStatusLevel level, string className, string method, string corpusPath, string message, bool requireIngestion = false, CdmLogCode code = CdmLogCode.None) { // Check if the Kusto config and the concurrent queue has been initialized if (this.config == null || this.requestQueue == null) { return; } // Not ingest logs from telemetry client to avoid cycling if (className == nameof(TelemetryKustoClient)) { return; } // If ingestion is not required and the level is Progress if (level == CdmStatusLevel.Progress && !requireIngestion) { // If the execution time needs to be logged if (logExecTimeMethods.Contains(method)) { // Check if the log contains execution time info string execTimeMessage = "Leaving scope. Time elapsed:"; // Skip if the log is not for execution time if (!message.StartsWith(execTimeMessage)) { return; } } // Skip if the method execution time doesn't need to be logged else { return; } } // Configured in case no user-created content can be ingested into Kusto due to compliance issue // Note: The RemoveUserContent property could be deleted in the if the compliance issue gets resolved if (this.config.RemoveUserContent) { corpusPath = null; if (level == CdmStatusLevel.Warning || level == CdmStatusLevel.Error) { message = null; } } string logEntry = ProcessLogEntry(timestamp, className, method, message, code, corpusPath, this.ctx.CorrelationId, this.ctx.Events.ApiCorrelationId, this.ctx.Corpus.AppId); // Add the status level and log entry to the queue to be ingested this.requestQueue.Enqueue(new Tuple <CdmStatusLevel, string>(level, logEntry)); }
/// <summary> /// Loads the string from resource file for particular enum and inserts arguments in it. /// </summary> /// <param name="code">The code, denotes the code enum for a message.</param> /// <param name="args">The args, denotes the arguments inserts into the messages.</param> private static string GetMessagefromResourceFile(CdmLogCode code, params string[] args) { StringBuilder builder = new StringBuilder(resManager.GetString(code.ToString())); int i = 0; foreach (string x in args) { string str = "{" + i + "}"; builder.Replace(str, x); i++; } return(builder.ToString()); }
/// <summary> /// Asserts in logcode, if expected log code is not in log codes recorded list. /// </summary> /// <param name="corpus">The corpus object.</param> /// <param name="expectedcode">The expectedcode cdmlogcode.</param> public static void AssertCdmLogCodeEquality(CdmCorpusDefinition corpus, CdmLogCode expectedCode) { bool toAssert = false; corpus.Ctx.Events.ForEach(logEntry => { if (((expectedCode.ToString().StartsWith("Warn") && logEntry["level"].Equals(CdmStatusLevel.Warning.ToString())) || (expectedCode.ToString().StartsWith("Err") && logEntry["level"].Equals(CdmStatusLevel.Error.ToString()))) && logEntry["code"].Equals(expectedCode.ToString())) { toAssert = true; } }); Assert.IsTrue(toAssert, $"The recorded log events should have contained message with log code {expectedCode} of appropriate level"); }
/// <summary> /// Log to the specified status level by using the status event on the corpus context (if it exists) or to the default logger. /// The log level, className, message and path values are also added as part of a new entry to the log recorder. /// </summary> /// <param name="level">The status level to log to.</param> /// <param name="ctx">The CDM corpus context.</param> /// <param name="className">The className, usually the class that is calling the method.</param> /// <param name="message">The message.</param> /// <param name="method">The path, usually denotes the class and method calling this method.</param> /// <param name="defaultStatusEvent">The default status event (log using the default logger).</param> /// <param name="code">The code(optional), denotes the code enum for a message.</param> private static void Log(CdmStatusLevel level, CdmCorpusContext ctx, string className, string message, string method, Action <string> defaultStatusEvent, string corpusPath, CdmLogCode code = CdmLogCode.None) { // Store a record of the event. // Save some dict init and string formatting cycles by checking // whether the recording is actually enabled. if (ctx.Events.IsRecording) { var theEvent = new Dictionary <string, string> { { "timestamp", TimeUtils.GetFormattedDateString(DateTimeOffset.UtcNow) }, { "level", level.ToString() }, { "class", className }, { "message", message }, { "method", method } }; if (level == CdmStatusLevel.Error || level == CdmStatusLevel.Warning) { theEvent.Add("code", code.ToString()); } if (ctx.CorrelationId != null) { theEvent.Add("correlationId", ctx.CorrelationId); } if (corpusPath != null) { theEvent.Add("corpuspath", corpusPath); } ctx.Events.Add(theEvent); } string formattedMessage = FormatMessage(className, message, method, ctx.CorrelationId, corpusPath); if (ctx.StatusEvent != null) { ctx.StatusEvent.Invoke(level, formattedMessage); } else { defaultStatusEvent(formattedMessage); } }
/// <summary> /// Log to ERROR level.This is extension to Error function for new logging. /// </summary> /// <param name="ctx">The CDM corpus context.</param> /// <param name="className">The className, usually the class that is calling the method.</param> /// <param name="method">The path, usually denotes the method calling this method.</param> /// <param name="corpusPath">The corpusPath, usually denotes corpus path of document.</param> /// <param name="code">The code, denotes the code enum for a message.</param> /// <param name="args">The args, denotes the arguments inserted into the messages.</param> public static void Error(CdmCorpusContext ctx, string className, string method, string corpusPath, CdmLogCode code, params string[] args) { if (CdmStatusLevel.Error >= ctx.ReportAtLevel) { // Get message from resource for the code enum. string message = GetMessagefromResourceFile(code, args); Log(CdmStatusLevel.Error, ctx, className, message, method, DefaultLogger.Error, corpusPath, code); } }
/// <summary> /// Log to ERROR level.This is extension to Error function for new logging. /// </summary> /// <param name="ctx">The CDM corpus context.</param> /// <param name="className">The className, usually the class that is calling the method.</param> /// <param name="method">The path, usually denotes the method calling this method.</param> /// <param name="corpusPath">The corpusPath, usually denotes corpus path of document.</param> /// <param name="code">The code, denotes the code enum for a message.</param> /// <param name="args">The args, denotes the arguments inserted into the messages.</param> public static void Error(CdmCorpusContext ctx, string className, string method, string corpusPath, CdmLogCode code, params string[] args) { // Get message from resource for the code enum. string message = GetMessageFromResourceFile(code, args); Log(CdmStatusLevel.Error, ctx, className, message, method, Console.Error.WriteLine, corpusPath, code, true); }
/// <summary> /// Log to the specified status level by using the status event on the corpus context (if it exists) or to the default logger. /// The log level, className, message and path values are also added as part of a new entry to the log recorder. /// </summary> /// <param name="level">The status level to log to.</param> /// <param name="ctx">The CDM corpus context.</param> /// <param name="className">The className, usually the class that is calling the method.</param> /// <param name="message">The message.</param> /// <param name="method">The path, usually denotes the class and method calling this method.</param> /// <param name="defaultStatusEvent">The default status event (log using the default logger).</param> /// <param name="code">The code(optional), denotes the code enum for a message.</param> private static void Log(CdmStatusLevel level, CdmCorpusContext ctx, string className, string message, string method, Action <string> defaultStatusEvent, string corpusPath, CdmLogCode code = CdmLogCode.None, bool ingestTelemetry = false) { if (ctx.SuppressedLogCodes.Contains(code)) { return; } // Store a record of the event. // Save some dict init and string formatting cycles by checking // whether the recording is actually enabled. if (level >= ctx.ReportAtLevel) { string timestamp = TimeUtils.GetFormattedDateString(DateTimeOffset.UtcNow); // Store a record of the event. // Save some dict init and string formatting cycles by checking // whether the recording is actually enabled. if (ctx.Events.IsRecording) { var theEvent = new Dictionary <string, string> { { "timestamp", TimeUtils.GetFormattedDateString(DateTimeOffset.UtcNow) }, { "level", level.ToString() }, { "class", className }, { "message", message }, { "method", method } }; if (level == CdmStatusLevel.Error || level == CdmStatusLevel.Warning) { theEvent.Add("code", code.ToString()); } if (ctx.CorrelationId != null) { theEvent.Add("cid", ctx.CorrelationId); } if (corpusPath != null) { theEvent.Add("path", corpusPath); } ctx.Events.Add(theEvent); } string formattedMessage = FormatMessage(className, message, method, ctx.CorrelationId, corpusPath); if (ctx.StatusEvent != null) { ctx.StatusEvent.Invoke(level, formattedMessage); } else { defaultStatusEvent(formattedMessage); } // Ingest the logs into telemetry database if (ctx.Corpus.TelemetryClient != null) { ctx.Corpus.TelemetryClient.AddToIngestionQueue(timestamp, level, className, method, corpusPath, message, ingestTelemetry, code); } } }