-
Notifications
You must be signed in to change notification settings - Fork 54
Log function script exception and error details #328
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| // | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
| // Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
| // | ||
|
|
||
| namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell | ||
| { | ||
| using System; | ||
| using System.Linq; | ||
| using System.Management.Automation; | ||
| using System.Text; | ||
|
|
||
| internal class ErrorRecordFormatter | ||
| { | ||
| private const string TruncationPostfix = "..."; | ||
| private const string Indent = " "; | ||
|
|
||
| private readonly PowerShell _pwsh = PowerShell.Create(); | ||
|
|
||
| /// <summary> | ||
| /// maxSize limits the maximum size of the formatted error string (in characters). | ||
| /// The rest will be truncated. This value should be high enough to allow the result | ||
| /// contain the most important and relevant information, but low enough to create | ||
| /// no problems for the communication channels used to propagate this data. | ||
| /// The default value is somewhat arbitrary but satisfies both conditions. | ||
| /// </summary> | ||
| public string Format(ErrorRecord errorRecord, int maxSize = 1 * 1024 * 1024) | ||
| { | ||
| var errorDetails = _pwsh.AddCommand("Microsoft.PowerShell.Utility\\Out-String") | ||
| .AddParameter("InputObject", errorRecord) | ||
| .InvokeAndClearCommands<string>(); | ||
|
|
||
| var result = new StringBuilder( | ||
| capacity: Math.Min(1024, maxSize), | ||
| maxCapacity: maxSize); | ||
|
|
||
| try | ||
| { | ||
| result.Append(errorDetails.Single()); | ||
| result.AppendLine("Script stack trace:"); | ||
| AppendStackTrace(result, errorRecord.ScriptStackTrace, Indent); | ||
| result.AppendLine(); | ||
|
|
||
| if (errorRecord.Exception != null) | ||
| { | ||
| AppendExceptionWithInners(result, errorRecord.Exception); | ||
| } | ||
|
|
||
| return result.ToString(); | ||
| } | ||
| catch (ArgumentOutOfRangeException) // exceeding StringBuilder max capacity | ||
| { | ||
| return Truncate(result, maxSize); | ||
| } | ||
| } | ||
|
|
||
| private static void AppendExceptionWithInners(StringBuilder result, Exception exception) | ||
| { | ||
| AppendExceptionInfo(result, exception); | ||
|
|
||
| if (exception is AggregateException aggregateException) | ||
| { | ||
| foreach (var innerException in aggregateException.Flatten().InnerExceptions) | ||
| { | ||
| AppendInnerExceptionIfNotNull(result, innerException); | ||
| } | ||
| } | ||
| else | ||
| { | ||
| AppendInnerExceptionIfNotNull(result, exception.InnerException); | ||
| } | ||
| } | ||
|
|
||
| private static void AppendInnerExceptionIfNotNull(StringBuilder result, Exception innerException) | ||
| { | ||
| if (innerException != null) | ||
| { | ||
| result.Append("Inner exception: "); | ||
| AppendExceptionWithInners(result, innerException); | ||
| } | ||
| } | ||
|
|
||
| private static void AppendExceptionInfo(StringBuilder stringBuilder, Exception exception) | ||
| { | ||
| stringBuilder.Append(exception.GetType().FullName); | ||
| stringBuilder.Append(": "); | ||
| stringBuilder.AppendLine(exception.Message); | ||
|
|
||
| AppendStackTrace(stringBuilder, exception.StackTrace, string.Empty); | ||
| stringBuilder.AppendLine(); | ||
| } | ||
|
|
||
| private static void AppendStackTrace(StringBuilder stringBuilder, string stackTrace, string indent) | ||
| { | ||
| if (stackTrace != null) | ||
| { | ||
| stringBuilder.Append(indent); | ||
| stringBuilder.AppendLine(stackTrace.Replace(Environment.NewLine, Environment.NewLine + indent)); | ||
| } | ||
| } | ||
|
|
||
| private static string Truncate(StringBuilder result, int maxSize) | ||
| { | ||
| var charactersToRemove = result.Length + TruncationPostfix.Length - maxSize; | ||
| if (charactersToRemove > 0) | ||
| { | ||
| result.Remove(result.Length - charactersToRemove, charactersToRemove); | ||
| } | ||
|
|
||
| return result + TruncationPostfix; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| // | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
| // Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
| // | ||
|
|
||
| namespace Microsoft.Azure.Functions.PowerShellWorker.Test | ||
| { | ||
| using System; | ||
| using System.Linq; | ||
| using System.Management.Automation; | ||
| using System.Text.RegularExpressions; | ||
|
|
||
| using Xunit; | ||
|
|
||
| using Microsoft.Azure.Functions.PowerShellWorker.PowerShell; | ||
|
|
||
| public class ErrorRecordFormatterTests | ||
| { | ||
| private static readonly ErrorRecordFormatter s_errorRecordFormatter = new ErrorRecordFormatter(); | ||
|
|
||
| [Fact] | ||
| public void FormattedStringContainsBasicErrorRecordData() | ||
| { | ||
| var exception = new RuntimeException("My exception"); | ||
| var errorRecord = new ErrorRecord(exception, "error id", ErrorCategory.InvalidOperation, null); | ||
|
|
||
| var result = s_errorRecordFormatter.Format(errorRecord); | ||
|
|
||
| Assert.StartsWith(exception.Message, result); | ||
| var resultLines = result.Split(Environment.NewLine); | ||
| Assert.Contains(resultLines, line => Regex.IsMatch(line, @"\bCategoryInfo\b[:\s]*?\bInvalidOperation\b")); | ||
| Assert.Contains(resultLines, line => Regex.IsMatch(line, @"\bFullyQualifiedErrorId\b[:\s]*?\berror id\b")); | ||
| Assert.Contains(resultLines, line => line == "System.Management.Automation.RuntimeException: My exception"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void FormattedStringContainsInnerExceptions() | ||
| { | ||
| var exception1 = new Exception("My exception 1"); | ||
| var exception2 = new Exception("My exception 2", exception1); | ||
| var exception3 = new Exception("My exception 3", exception2); | ||
| var errorRecord = new ErrorRecord(exception3, "error id", ErrorCategory.InvalidOperation, null); | ||
|
|
||
| var result = s_errorRecordFormatter.Format(errorRecord); | ||
|
|
||
| var resultLines = result.Split(Environment.NewLine); | ||
| Assert.Contains(resultLines, line => line == "Inner exception: System.Exception: My exception 1"); | ||
| Assert.Contains(resultLines, line => line == "Inner exception: System.Exception: My exception 2"); | ||
| Assert.Equal(2, resultLines.Count(line => line.Contains("Inner exception:"))); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void FormattedStringContainsAggregateExceptionMembers() | ||
| { | ||
| var exception1 = new Exception("My exception 1"); | ||
| var exception2 = new Exception("My exception 2"); | ||
| var exception3 = new AggregateException("My exception 3", exception1, exception2); | ||
| var exception4 = new Exception("My exception 4", exception3); | ||
| var exception5 = new Exception("My exception 5"); | ||
| var exception6 = new AggregateException("My exception 6", exception4, exception5); | ||
| var exception7 = new Exception("My exception 7", exception6); | ||
| var errorRecord = new ErrorRecord(exception7, "error id", ErrorCategory.InvalidOperation, null); | ||
|
|
||
| var result = s_errorRecordFormatter.Format(errorRecord); | ||
|
|
||
| var resultLines = result.Split(Environment.NewLine); | ||
| Assert.Contains(resultLines, line => line == "Inner exception: System.Exception: My exception 1"); | ||
| Assert.Contains(resultLines, line => line == "Inner exception: System.Exception: My exception 2"); | ||
| Assert.Contains(resultLines, line => line.StartsWith("Inner exception: System.AggregateException: My exception 3")); | ||
| Assert.Contains(resultLines, line => line == "Inner exception: System.Exception: My exception 4"); | ||
| Assert.Contains(resultLines, line => line == "Inner exception: System.Exception: My exception 5"); | ||
| Assert.Contains(resultLines, line => line.StartsWith("Inner exception: System.AggregateException: My exception 6")); | ||
| Assert.Contains(resultLines, line => line == "System.Exception: My exception 7"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void FormattedStringIsTruncatedIfTooLong() | ||
| { | ||
| var exception1 = new Exception("My exception 1"); | ||
| var exception2 = new Exception("My exception 2", exception1); | ||
| var exception3 = new Exception("My exception 3", exception2); | ||
| var errorRecord = new ErrorRecord(exception3, "error id", ErrorCategory.InvalidOperation, null); | ||
| var fullResult = s_errorRecordFormatter.Format(errorRecord); | ||
|
|
||
| var maxSize = fullResult.Length / 2; | ||
| var truncatedResult = new ErrorRecordFormatter().Format(errorRecord, maxSize); | ||
|
|
||
| const string ExpectedPostfix = "..."; | ||
| Assert.InRange(truncatedResult.Length, ExpectedPostfix.Length + 1, maxSize); | ||
| Assert.EndsWith(ExpectedPostfix, truncatedResult); | ||
| Assert.StartsWith(fullResult.Substring(0, truncatedResult.Length - ExpectedPostfix.Length), truncatedResult); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.