dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.36k stars 4.74k forks source link

Exception Handling / Error Logging in English #40427

Open Chiramisu opened 4 years ago

Chiramisu commented 4 years ago

I have an application that is used in other countries. When receiving exception / error logs, they're invariably in other languages that I can't understand. I've looked into this, but had no luck. How do we force our C# .NET programs to log errors in English? If this currently isn't supported, please add it. I've seen requests for this around the Interwebz going back over a decade.

The user doesn't care about error logs. They are for developers; the vast majority of whom speak English.

danmoseley commented 2 years ago

BTW, probably everyone knows this, but if it helps anyone -- net helpmsg is a quick way to call FormatMessage(..FORMAT_MESSAGE_FROM_SYSTEM..) manually. eg

C:\>net helpmsg 10060

A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.
TobiasKnauss commented 2 years ago

@danmoseley

[...] I do not see how it would work for Win32Exception though, there is no enum (such an enum would be vast, eg., it would be all of winerror.h, etc)

Your assumption is correct, this enum contains about 2840 values: I created my own workaround and copied all names and values including their descriptions from the child pages of https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes into an Excel spreadsheet, added some concat functions and copied the results into an enum. It took about an hour. This enum looks like:

public enum EnumSystemErrorCode
{
  [Description ("The operation completed successfully.")]
  ERROR_SUCCESS = 0,
  [Description ("Incorrect function.")]
  ERROR_INVALID_FUNCTION = 1,
  [Description ("The system cannot find the file specified.")]
  ERROR_FILE_NOT_FOUND = 2,
  [Description ("The system cannot find the path specified.")]
  ERROR_PATH_NOT_FOUND = 3,
  [Description ("The system cannot open the file.")]
  ERROR_TOO_MANY_OPEN_FILES = 4,
  [Description ("Access is denied.")]
  ERROR_ACCESS_DENIED = 5,
...
  [Description ("The length of the state manager setting name has exceeded the limit.")]
  ERROR_STATE_SETTING_NAME_SIZE_LIMIT_EXCEEDED = 15817,
  [Description ("The length of the state manager container name has exceeded the limit.")]
  ERROR_STATE_CONTAINER_NAME_SIZE_LIMIT_EXCEEDED = 15818,
  [Description ("This API cannot be used in the context of the caller's application type.")]
  ERROR_API_UNAVAILABLE = 15841,
}

If somebody wants the complete file, I can post it here or upload it to my github account.

The problem in using the descriptions from the websites is, that they contain placeholders like:

  [Description ("{Missing System File} The required system file %hs is bad or missing.")]
  ERROR_MISSING_SYSTEMFILE = 573,
  [Description ("{Application Error} The exception %s (0x%08lx) occurred in the application at location 0x%08lx.")]
  ERROR_UNHANDLED_EXCEPTION = 574,

Therefore I had to combine the original exception message, which contains the data that was inserted to the placeholders, with the English exception message from the description.

The final message looks like:

A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond. (original message: 'Ein Verbindungsversuch ist fehlgeschlagen, da die Gegenstelle nach einer bestimmten Zeitspanne nicht richtig reagiert hat, oder die hergestellte Verbindung war fehlerhaft, da der verbundene Host nicht reagiert hat. [::ffff:100.111.1.2]:55900'), Socket Error = TimedOut, Socket Error Code = 10060 (0x0000274C)

The code to create this message is:

public static class EnumSystemErrorCodeHelper
{
  public static string? GetDescription (this EnumSystemErrorCode i_systemErrorCode)
  {
    return DescriptionAttributeHelper.GetDescription (i_systemErrorCode);
  }
}

public static class DescriptionAttributeHelper
{
  public static string? GetDescription (Enum? i_member)
  {
    string? memberName = i_member?.ToString ();
    if (i_member == null
     || string.IsNullOrEmpty (memberName))
      throw new ArgumentNullException (nameof (i_member));

    var memberInfos = i_member.GetType ().GetMember (memberName);
    if (!memberInfos.Any ())
      return null;

    object[] attributes = memberInfos[0].GetCustomAttributes (typeof (DescriptionAttribute), false);
    return attributes.Any ()
             ? ((DescriptionAttribute)attributes[0]).Description
             : null;
  }
}

public static class ExceptionHelper
{
  /// ------------------------------------------------------------------
  /// <summary>
  ///   Create a message text from the exception.
  /// </summary>
  /// <param name="i_exception"> The exception from which the message will be created. </param>
  /// <param name="i_separator"> The separator text that will be inserted between different parts of the message. </param>
  /// <param name="i_withInnerExceptions"> A flag that specifies whether the messages of inner exceptions should be added to the created message. </param>
  /// <returns> A message text from the exception. </returns>
  /// ------------------------------------------------------------------
  public static string? GetMessage (this Exception? i_exception,
                                    string          i_separator           = ", ",
                                    bool            i_withInnerExceptions = true)
  {
    if (i_exception == null)
      return null;

    string? newlineSeparator = i_separator.EqualsAny (CONSTS.CR, CONSTS.LF, CONSTS.CRLF)
                                 ? i_separator
                                 : null;

    string exceptionMessage = i_exception.Message;
    var    sbMessage        = new StringBuilder (exceptionMessage);
    if (!string.Equals (System.Threading.Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName, "en", StringComparison.InvariantCultureIgnoreCase))
    {
      if (i_exception is System.ComponentModel.Win32Exception win32Exception)
      {
        var systemErrorCode = (EnumSystemErrorCode)win32Exception.ErrorCode;
        if (!Enum.IsDefined (systemErrorCode))
          systemErrorCode = (EnumSystemErrorCode)win32Exception.NativeErrorCode;
        if (Enum.IsDefined (systemErrorCode))
        {
          string? description = DescriptionAttributeHelper.GetDescription (systemErrorCode);
          if (!string.IsNullOrEmpty (description))
          {
            _ = sbMessage.Clear ()
                         .Append (description)
                         .Append (newlineSeparator ?? " ")
                         .Append ("(original message: '")
                         .Append (exceptionMessage)
                         .Append ("')");
          }
        }
      }
    }

    switch (i_exception) // Do NOT change the order of the cases.
    {
    case System.Net.Sockets.SocketException socketException:
      _ = sbMessage.Append ($"{i_separator}Socket Error = {socketException.SocketErrorCode}")
                   .Append ($"{i_separator}Socket Error Code = {socketException.ErrorCode} (0x{socketException.ErrorCode:X8})");
      break;

    case System.ComponentModel.Win32Exception win32Exception:
      _ = sbMessage.Append ($"{i_separator}Error Code = {win32Exception.ErrorCode} (0x{win32Exception.ErrorCode:X8})")
                   .Append ($"{i_separator}Native Error Code = {win32Exception.NativeErrorCode} (0x{win32Exception.NativeErrorCode:X8})");
      break;

    case System.Runtime.InteropServices.ExternalException externalException:
      _ = sbMessage.Append ($"{i_separator}Error Code = {externalException.ErrorCode} (0x{externalException.ErrorCode:X8})");
      break;

    case System.Net.Http.HttpRequestException httpRequestException:
      _ = sbMessage.Append ($"{i_separator}Status Code = {httpRequestException.StatusCode}");
      break;
    }

    if (newlineSeparator is null)
      _ = sbMessage.Replace (CONSTS.CRLF, CONSTS.CommaSpace)
                   .Replace (CONSTS.CR, CONSTS.CommaSpace)
                   .Replace (CONSTS.LF, CONSTS.CommaSpace);

    if (i_exception.InnerException != null
     && i_withInnerExceptions)
    {
      _ = sbMessage.Append (" Inner exception: {")
                   .Append (i_exception.InnerException.GetMessage (i_separator, i_withInnerExceptions))
                   .Append ("}");
    }

    return sbMessage.ToString ();
  }

  /// ------------------------------------------------------------------
  /// <summary>
  ///   Create a detailed text from the exception. The text contains message, source and stacktrace from the given exception and all inner exceptions.
  /// </summary>
  /// <param name="i_exception"> The exception from which the text will be created. </param>
  /// <param name="i_separator"> The separator text that will be inserted between different parts of the exception text. </param>
  /// <returns> A detailed text from the exception. </returns>
  /// ------------------------------------------------------------------
  public static string GetText (this Exception? i_exception,
                                string          i_separator = ", ")
  {
    if (i_exception == null)
      return s_text_exceptionObjectMissing;

    string? newlineSeparator = i_separator.EqualsAny (CONSTS.CR, CONSTS.LF, CONSTS.CRLF)
                                 ? i_separator
                                 : null;

    var exception = i_exception;
    var sb        = new StringBuilder ();

    int levelOfInnerException = 0;
    do
    {
      if (levelOfInnerException == 0)
      {
        _ = sb.Append (">>>>> Exception: ");
      }
      else
      {
        _ = sb.Append (i_separator);
        _ = sb.Append ($">>>>> Inner Exception #{levelOfInnerException}: ");
      }

      _ = sb.Append (newlineSeparator);
      _ = sb.AppendWithSeparator (exception.GetType ().FullName,             i_separator);
      _ = sb.AppendWithSeparator (exception.GetMessage (i_separator, false), i_separator);

      if (exception.Source != null)
      {
        _ = sb.Append (">>> Source: ");
        _ = sb.Append (newlineSeparator);
        _ = sb.AppendWithSeparator (exception.Source, i_separator);
      }

      if (exception.StackTrace != null)
      {
        string stackTrace = exception.StackTrace;
        if (newlineSeparator is null)
          stackTrace = stackTrace.Replace (CONSTS.CRLF, CONSTS.CommaSpace)
                                 .Replace (CONSTS.CR, CONSTS.CommaSpace)
                                 .Replace (CONSTS.LF, CONSTS.CommaSpace);
        _ = sb.Append (">>> Stack Trace: ");
        _ = sb.Append (newlineSeparator);
        _ = sb.Append (stackTrace);
      }

      exception = exception.InnerException;
      levelOfInnerException++;
    }
    while (exception != null);

    return sb.ToString ();
  }
}
danmoseley commented 2 years ago

@tfenise Are we expected to know or look up the error codes? If yes, is there a central page of all possible error codes?

copied all names and values including their descriptions from the child pages of https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes

In case it is useful to others -- what I do is just grep the SDK headers, which you will have if you have installed the C++ workload in Visual Studio, eg

C:\Program Files (x86)\Windows Kits\10\Include>findstr /sipc:"10060" *h
10.0.19041.0\shared\winerror.h:#define WSAETIMEDOUT                     10060L

the message is in a comment next to there.

Of course, the numbers in those are not always in decimal. However the ERRLOOK.EXE tool that installs with Visual Studio at "C:\Program Files\Microsoft Visual Studio\2022\Preview\Common7\Tools\errlook.exe" can handle hex, etc. image

I am not sure which headers ERRLOOK.EXE has aggregated. I do not know where the sources are.

danmoseley commented 2 years ago

The only concrete step identified in this issue so far is the suggested change to SocketsException. We can pass feedback on to Windows, but I do not thing we will change how they create and deploy Windows for this.

mlsomers commented 7 months ago

At least this issue did not get closed yet... They closed mine and this one and this one

When can we stop the need to translate something that resembles this (in dutch):

The surgery is crippled while the opening is not switched on.

And figure out it meant something like

This operation is invalid while the window is disabled.

Or even worse (an example from @macmade)

개체 참조가 개체의 인스턴스로 설정되지 않았습니다

Which according to him means

Object reference not set to an instance of an object.

Maybe .Net 9 will finally give us Exception.ToString(CultureInfo.InvariantCulture) or a setting System.Environment.ExceptionCulture or something equivalent?... Please?

juwens commented 1 month ago

Exception.ToString(CultureInfo.InvariantCulture) +1

For our localized app, we want english logs and localized error messages for the user. So only specifying System.Environment.ExceptionCulture would be insufficient (though better than the status quo).

Clockwork-Muse commented 1 month ago

Exception.ToString(CultureInfo.InvariantCulture) +1

For our localized app, we want english logs and localized error messages for the user. So only specifying System.Environment.ExceptionCulture would be insufficient (though better than the status quo).

The catch is that you should rarely be displaying "raw" exception messages to the user (and never a stack trace), regardless of the language, but instead be displaying something that would be actionable for them. At that point, you'd be constructing messages and dialogs, so the language/culture of the exception should be irrelevant.