使用全局唯一的消息ID使代码可查找


39

查找脚本的常见模式遵循以下脚本:

  1. 观察奇怪的地方,例如没有输出或挂起的程序。
  2. 在日志或程序输出中找到相关消息,例如“找不到Foo”。(以下内容仅在找到错误所在的路径时才有意义。如果堆栈跟踪或其他调试信息容易获得,则是另一回事了。)
  3. 找到打印消息的代码。
  4. 调试Foo输入(或应该输入)图片到消息打印的第一处之间的代码。

第三步是调试过程经常停止的地方,因为在代码中有很多地方Could not find {name}都打印了“找不到Foo”(或模板字符串)。实际上,几次拼写错误使我找到实际位置的速度比我原本要快得多-它使消息在整个系统中(通常在整个世界)都是唯一的,从而导致相关搜索引擎立即受到攻击。

由此得出的明显结论是,我们应该在代码中使用全局唯一的消息ID,将其作为消息字符串的一部分进行硬编码,并可能验证代码库中每个ID仅出现一次。在可维护性方面,该社区认为此方法最重要的利弊是什么,您将如何实施此方法或以其他方式确保永远不必实施它(假设该软件将始终存在错误)?


54
而是使用堆栈跟踪。堆栈跟踪不仅会告诉您错误的发生位置,而且还会告诉每个调用该错误的函数。如有必要,请在发生异常时记录整个跟踪。如果您使用的语言没有例外,例如C,那就是另一回事了。
罗伯特·哈维

6
@ l0b0关于措辞的小建议。“这个社区怎么看……优缺点”是可能被认为太宽泛的短语。这是一个允许“好的主观”问题的站点,作为允许此类问题的回报,OP希望您做“牧养”评论和答案以达成有意义的共识的工作。
rwong

@rwong谢谢!我认为这个问题已经收到了很好的即时答复,尽管在论坛上可能会更好。在阅读了JohnWu的明确答复后,我撤消了对RobertHarvey评论的答复,以防您指的是这种情况。如果没有,您是否有任何具体的牧羊技巧?
l0b0

1
我的消息看起来像“在调用bar()期间找不到Foo”。问题解决了。耸耸肩。缺点是客户看不到它,但我们还是倾向于向他们隐藏错误消息的详细信息,从而使该错误仅提供给无法让猴子看到某些函数名称的系统管理员。没错,是的,一个不错的唯一ID /代码会解决问题。
与莫妮卡(Monica)进行的轻度比赛

1
当客户给您打电话,而他们的计算机未以英语运行时,这非常有用!如今这些问题已不多了,因为我们现在拥有电子邮件和日志文件.....
Ian

Answers:


12

总体而言,这是一种有效且有价值的策略。这里有一些想法。

在将所有此类信息组合在一起时,该策略也被称为“遥测”,它们可以帮助“三角剖分”执行跟踪,并允许疑难解答人员了解用户/应用程序试图完成的工作以及实际发生的情况。

必须收集(我们都知道)的一些重要数据是:

  • 代码的位置,即调用堆栈和大概的代码行
    • 如果将函数合理地分解为适当的小单位,则不需要“代码的近似行”。
  • 与功能成功/失败有关的任何数据
  • 一个高级“命令”,可以确定人工用户/外部代理/ API用户要完成的工作。
    • 想法是,软件将接受并处理来自某处的命令。
    • 在此过程中,可能发生了数十到数百至数千个函数调用。
    • 我们希望在此过程中生成的任何遥测都可以追溯到触发该过程的最高级别命令。
    • 对于基于Web的系统,原始HTTP请求及其数据将是此类“高级请求信息”的示例
    • 对于GUI系统,用户单击某些内容将符合此描述。

通常,由于无法将低级别的日志消息追溯到触发它的最高级别的命令,传统的日志记录方法往往不够用。堆栈跟踪仅捕获有助于处理最高级别命令的上级函数的名称,而不捕获有时需要表征该命令的细节(数据)。

通常,没有编写软件来实现这种可追溯性要求。这使得将低级消息与高级命令关联起来更加困难。在许多请求和响应可能重叠的自由多线程系统中,该问题尤其严重,并且处理可能会转移到与原始请求接收线程不同的线程上。

因此,为了从遥测中获得最大价值,将需要对整个软件架构进行更改。大多数接口和函数调用都需要进行修改,以接受和传播“ tracer”参数。

甚至实用程序功能都将需要添加“ tracer”参数,这样,如果确实失败,则日志消息将允许其自身与某个高级命令相关联。

使遥测跟踪变得困难的另一个失败是缺少对象引用(空指针或引用)。当某些关键数据丢失时,可能无法报告任何对故障有用的信息。

在编写日志消息方面:

  • 某些软件项目可能需要本地化(翻译成外语),即使仅用于管理员的日志消息也是如此。
  • 某些软件项目可能需要区分敏感数据和非敏感数据,甚至出于记录目的,而且管理员也不会偶然看到某些敏感数据。
  • 不要尝试混淆错误消息。那会破坏客户的信任。客户的管理员希望阅读并理解这些日志。不要让他们觉得必须向客户管理员隐藏某些专有秘密。
  • 一定希望客户带来一条遥测日志,并请教您的技术支持人员。他们希望知道。培训您的技术支持人员以正确解释遥测日志。

1
确实,AOP主要吹捧其解决此问题的固有能力-将Tracer添加到每个相关的调用中-对代码库的入侵最少。
主教

我还要在“编写日志消息”列表中添加一个重要的信息,即用“为什么”和“如何修复”来描述失败,而不只是“发生了什么”。
主教

58

假设您有一个琐碎的实用程序函数,该函数在代码中的数百个地方使用:

decimal Inverse(decimal input)
{
    return 1 / input;
}

如果我们按照您的建议去做,我们可能会写

decimal Inverse(decimal input)
{
    try 
    {
        return 1 / input;
    }
    catch(Exception ex)
    {
        log.Write("Error 27349262 occurred.");
    }
}

如果输入为零,则可能发生错误。这将导致被零除的异常。

假设您在输出或日志中看到27349262。您在哪里寻找通过零值的代码?请记住,具有唯一ID的功能已在数百个地方使用。因此,尽管您可能知道零被除,但是您不知道0它是谁。

在我看来,如果您要麻烦记录消息ID,则最好记录堆栈跟踪。

如果堆栈跟踪的冗长困扰您,那么您不必像运行时将其提供给您那样将其转储为字符串。您可以自定义它。例如,如果您只需要一个简短的堆栈跟踪,仅用于n级别,则可以编写如下内容(如果使用c#):

static class ExtensionMethods
{
    public static string LimitedStackTrace(this Exception input, int layers)
    {
        return string.Join
        (
            ">",
            new StackTrace(input)
                .GetFrames()
                .Take(layers)
                .Select
                (
                    f => f.GetMethod()
                )
                .Select
                (
                    m => string.Format
                    (
                        "{0}.{1}", 
                        m.DeclaringType, 
                        m.Name
                    )
                )
                .Reverse()
        );
    }
}

并像这样使用它:

public class Haystack
{
    public static void Needle()
    {
        throw new Exception("ZOMG WHERE DID I GO WRONG???!");
    }

    private static void Test()
    {
        Needle();
    }

    public static void Main()
    {
        try
        {
            Test();
        }
        catch(System.Exception e)
        {
            //Get 3 levels of stack trace
            Console.WriteLine
            (
                "Error '{0}' at {1}", 
                e.Message, 
                e.LimitedStackTrace(3)
            );  
        }
    }
}

输出:

Error 'ZOMG WHERE DID I GO WRONG???!' at Haystack.Main>Haystack.Test>Haystack.Needle

可能比维护消息ID更容易,而且更灵活。

从DotNetFiddle窃取我的代码


32
嗯,我想我的观点不够清楚。我知道每个代码位置它们都是唯一的Robert 它们不是每个代码路径唯一的知道位置通常是无用的,例如,如果真正的问题是输入设置不正确。我已经略微编辑了我的语言以强调。
John Wu,

1
大家好。堆栈跟踪有一个不同的问题,根据情况的不同,它可能会或可能不是一个破坏交易的事情:它们的大小可能会导致它们淹没消息,尤其是如果您想包括整个堆栈跟踪而不是像某些语言那样是简短的版本默认情况下执行。也许一个替代方案将是写堆栈跟踪日志分开,并且包括索引编号到日志中的应用程序的输出。
l0b0

12
如果您收到了太多的这些信息,以至于担心您的I / O泛滥,那就是严重的错误。还是您只是小气?真正的性能损失可能是堆栈逐渐消失。
John Wu,

9
使用缩短堆栈跟踪的解决方案进行编辑,以防万一您将日志写入3.5软盘;)
John Wu,

7
@JohnWu也不要忘记“ IOException'未找到文件',它告诉您调用堆栈约有五十层,但没有告诉您没有找到确切的血腥文件。
Joker_vD

6

SAP NetWeaver数十年来一直在这样做。

在对作为典型SAP ERP系统的大量代码庞然大物中的错误进行故障诊断时,它已被证明是一种有价值的工具。

错误消息在中央存储库中进行管理,其中每个消息均通过其消息类别和消息编号来标识。

当您要输出错误消息时,只需声明类,编号,严重性和特定于消息的变量。消息的文本表示是在运行时创建的。您通常会在出现消息的任何上下文中看到消息类别和编号。这具有几个整洁的效果:

  • 您可以在ABAP代码库中自动找到创建特定错误消息的任何代码行。

  • 您可以设置动态调试器断点,该断点将在生成特定错误消息时触发。

  • 与寻找“找不到Foo”相比,您可以在SAP知识库文章中查找错误并获得更多相关的搜索结果。

  • 消息的文本表示形式是可翻译的。因此,通过鼓励使用消息而不是字符串,您还可以获得i18n功能。

带有消息号的错误弹出窗口的示例:

错误1

在错误存储库中查找该错误:

错误2

在代码库中找到它:

错误3

但是,有缺点。如您所见,这些代码行不再是自我记录的。当您阅读源代码并看到MESSAGE上面的屏幕截图中的语句时,您只能从上下文中推断出其实际含义。同样,有时人们实现自定义错误处理程序,这些错误处理程序在运行时接收消息类和编号。在这种情况下,无法自动找到错误,或者无法在实际发生错误的位置找到错误。第一个问题的解决方法是养成一种习惯,始终在源代码中添加一条注释,告诉读者消息的含义。第二种方法是添加一些无效代码以确保自动消息查找有效。例:

" Do not use special characters
my_custom_error_handler->post_error( class = 'EU' number = '271').
IF 1 = 2.
   MESSAGE e271(eu).
ENDIF.    

但是在某些情况下这是不可能的。例如,有些基于UI的业务流程建模工具可在其中配置错误消息,以在违反业务规则时显示。这些工具的实现完全由数据驱动,因此这些错误不会显示在使用位置列表中。这意味着在试图查找错误原因时过多地依赖于哪里使用的列表可能会引起麻烦。


一段时间以来,消息目录也已经成为GNU / Linux的一部分,而UNIX通常作为POSIX标准
主教

@bishop我通常不是专门为POSIX系统编程的,所以我并不熟悉它。也许您可以发布另一个答案,该答案解释POSIX消息目录以及OP可以从其实现中学到什么。
菲利普

3
我参与了一个项目,该项目应运而生。我们遇到的一个问题是,连同其他所有内容,我们在数据库中放置了“无法连接到数据库”的人工消息。
JimmyJames

5

这种方法的问题在于,它导致了更详细的日志记录。您永远不会看的99.9999%。

相反,我建议在过程开始时捕获状态,并确定过程的成功/失败。

这使您可以在本地重现该错误,逐步执行代码并将每个进程的日志记录限制在两个位置。例如。

OrderPlaced {id:xyz; ...order data..}
OrderPlaced {id:xyz; ...Fail, ErrorMessage..}

现在,我可以在开发机上使用完全相同的状态来重现错误,逐步执行调试器中的代码并编写新的单元测试以确认修复。

此外,如果需要,我可以通过仅记录故障或将状态保留在其他位置(数据库?消息队列?)来避免更多日志记录

显然,我们在记录敏感数据时必须格外小心。因此,如果您的解决方案使用消息队列或事件存储模式,则此方法特别有效。由于日志只需要说“消息xyz失败”


将敏感数据放入队列仍在对其进行记录。这是不明智的做法,就像在没有某种形式的加密的情况下将敏感输入存储在数据库中一样。
jpmc26

如果系统运行在队列或数据库之外,则数据已经存在,安全性也应该存在。记录过多的日志仅是不好的,因为日志往往超出了安全控制范围。
伊万

是的,但这就是重点。不建议这样做,因为这些数据会永久保存在该位置,并且通常以完全明文形式保留。对于敏感数据,最好不要冒险,并尽量减少存储数据的时间,然后要非常了解并非常谨慎地存储数据。
jpmc26 '18

它传统上是永久的,因为您正在写入文件。但是错误队列是暂时的。
伊万

我会说这可能取决于队列的实现(甚至可能取决于设置)。您不能只是将其转储到任何队列中并期望它是安全的。队列耗尽后会发生什么?日志必须仍位于某人可以查看的地方。此外,这不是我想暂时打开的额外攻击媒介。如果攻击发现那里有敏感数据,那么即使是最新的条目也可能很有价值。然后就有可能有人不知道并翻转开关,因此它也开始记录到磁盘。只是一罐蠕虫。
jpmc26 '18

1

我建议不要通过日志记录来解决此问题,而应该将这种情况视为例外(它会锁定程序),并且应该引发异常。说你的代码是:

public Foo GetFoo() {

     //Expecting that this should never by null.
     var aFoo = ....;

     if (aFoo == null) Log("Could not find Foo.");

     return aFoo;
}

听起来您未设置调用代码来处理Foo不存在的事实,而您可能应该是:

public Foo GetFooById(int id) {
     var aFoo = ....;

     if (aFoo == null) throw new ApplicationException("Could not find Foo for ID: " + id);

     return aFoo;
}

这将返回堆栈跟踪以及可用于帮助调试的异常。

或者,如果我们期望Foo在检索时可以为null,这很好,那么我们需要修复调用站点:

void DoSomeFoo(Foo aFoo) {

    //Guard checks on your input - complete with stack trace!
    if (aFoo == null) throw new ArgumentNullException(nameof(aFoo));

    ... operations on Foo...
}

您的软件在意外情况下挂起或“异常”运行的事实在我看来是错误的-如果您需要Foo并且无法处理不存在的Foo,那么崩溃最好比尝试沿着可能会发生的路径进行崩溃破坏您的系统。


0

正确的日志库确实提供了扩展机制,因此,如果您想知道日志消息的起源方法,则可以立即使用。这确实对执行有影响,因为该过程需要生成堆栈跟踪并遍历它,直到您退出日志记录库为止。

也就是说,这实际上取决于您希望自己的ID为您做什么:

  • 将提供给用户的错误消息与您的日志相关联?
  • 提供有关生成消息时正在执行什么代码的注释?
  • 跟踪机器名称和服务实例?
  • 跟踪线程ID?

所有这些事情都可以使用适当的日志记录软件(即,不是Console.WriteLine()Debug.WriteLine())直接进行。

就个人而言,更重要的是重构执行路径的能力。这就是Zipkin之类的工具旨在完成的任务。一个ID,用于跟踪整个系统中一项用户操作的行为。通过将日志放在中央搜索引擎中,您不仅可以找到运行时间最长的动作,还可以调用适用于该动作的日志(例如ELK堆栈)。

每条消息都会更改的不透明ID并不是很有用。一个一致的ID,用于通过整个微服务套件跟踪行为...非常有用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.