【问题标题】:multi file C program, how best to implement optional logging?多文件C程序,如何最好地实现可选日志记录?
【发布时间】:2008-11-29 18:46:36
【问题描述】:

我有一个多文件 C 程序。我希望用户能够在运行时指定不同的调试级别。

实现这一点的最佳方法是什么?

我正在考虑将 debug(level, "message") 类型的函数导出并在任何地方使用。有更好的/其他想法吗?

【问题讨论】:

  • 另请参阅C #define macro for debug printing,了解有关此主题的更多想法。那里的讨论更多是关于处理任何与不进行调试/日志记录,但这构成了此处讨论的多级调试/日志记录思想的核心。

标签: c debugging


【解决方案1】:

Jonathan 的建议很好,但是从 C99 开始,我们有了可变参数宏,所以不需要使用双括号来调试宏。

我使用了一个精简版的日志记录头:

#define LOG_FATAL    (1)
#define LOG_ERR      (2)
#define LOG_WARN     (3)
#define LOG_INFO     (4)
#define LOG_DBG      (5)

#define LOG(level, ...) do {  \
                            if (level <= debug_level) { \
                                fprintf(dbgstream,"%s:%d:", __FILE__, __LINE__); \
                                fprintf(dbgstream, __VA_ARGS__); \
                                fprintf(dbgstream, "\n"); \
                                fflush(dbgstream); \
                            } \
                        } while (0)
extern FILE *dbgstream;
extern int  debug_level;

所以无论我需要记录什么,我只需添加一行

LOG(LOG_ERR, "I/O error %s occured while opening file %s", strerror(errno), filename);

在程序初始化期间,您需要为dbgstream(通常默认为stderr)和debug_level指定值。

对于实际项目而不是多次调用fprintf,我只是从LOG 宏中调用我的函数并传递__FILE____LINE____VA_ARGS_ 作为参数——该函数还打印日期、时间和pid日志行,并且不要每次都做fflush() - 只有当缓冲计数器超过预设值时 - 它会显着提高日志记录性能。

但请注意,某些编译器可能不支持可变参数宏,因为它仅在 C99 中引入。

【讨论】:

    【解决方案2】:

    有一个非常好的log4j C口,log4c

    【讨论】:

    • 您能否指出使用 log4c 与拥有自己的自定义宏+prinf 组合相比的优势/劣势。
    【解决方案3】:

    我使用了两个密切相关的调试系统(在 hysterical raisins 的单个标头中声明)。更简单的有一个调试级别和类似 printf 的函数,它们采用调试级别并且仅在调试级别设置得足够高时才发出输出。更复杂的系统提供不同的调试子系统,每个子系统的行为都像更简单的系统(因此,例如,我可以在与输入调试或规则调试不同的级别进行宏调试,或...)。

    您的问题未解决的另一个问题是如何在运行时启用调试。我一直使用命令行选项——通常“-d”用于“3 级基本调试”,“-D nn”用于 nn 级调试。或者,使用复杂系统:'-D input=3,macros=5,rules=1'。拥有一个具有相同语义的环境变量并不难。

    从实现这些的标题中:

    /*
    ** Usage:  TRACE((level, fmt, ...))
    ** "level" is the debugging level which must be operational for the output
    ** to appear. "fmt" is a printf format string. "..." is whatever extra
    ** arguments fmt requires (possibly nothing).
    ** The non-debug macro means that the code is validated but never called.
    ** -- See chapter 8 of 'The Practice of Programming', by Kernighan and Pike.
    */
    #ifdef DEBUG
    #define TRACE(x)    db_print x
    #else
    #define TRACE(x)    do { if (0) db_print x; } while (0)
    #endif /* DEBUG */
    
    #ifndef lint
    #ifdef DEBUG
    /* This string can't be made extern - multiple definition in general */
    static const char jlss_id_debug_enabled[] = "@(#)*** DEBUG ***";
    #endif /* DEBUG */
    #ifdef MAIN_PROGRAM
    const char jlss_id_debug_h[] = "@(#)$Id: debug.h,v 3.6 2008/02/11 06:46:37 jleffler Exp $";
    #endif /* MAIN_PROGRAM */
    #endif /* lint */
    
    #include <stdio.h>
    
    extern int      db_getdebug(void);
    extern int      db_newindent(void);
    extern int      db_oldindent(void);
    extern int      db_setdebug(int level);
    extern int      db_setindent(int i);
    extern void     db_print(int level, const char *fmt,...);
    extern void     db_setfilename(const char *fn);
    extern void     db_setfileptr(FILE *fp);
    extern FILE    *db_getfileptr(void);
    
    /* Semi-private function */
    extern const char *db_indent(void);
    
    /**************************************\
    ** MULTIPLE DEBUGGING SUBSYSTEMS CODE **
    \**************************************/
    
    /*
    ** Usage:  MDTRACE((subsys, level, fmt, ...))
    ** "subsys" is the debugging system to which this statement belongs.
    ** The significance of the subsystems is determined by the programmer,
    ** except that the functions such as db_print refer to subsystem 0.
    ** "level" is the debugging level which must be operational for the
    ** output to appear. "fmt" is a printf format string. "..." is
    ** whatever extra arguments fmt requires (possibly nothing).
    ** The non-debug macro means that the code is validated but never called.
    */
    #ifdef DEBUG
    #define MDTRACE(x)  db_mdprint x
    #else
    #define MDTRACE(x)  do { if (0) db_mdprint x; } while (0)
    #endif /* DEBUG */
    
    extern int      db_mdgetdebug(int subsys);
    extern int      db_mdparsearg(char *arg);
    extern int      db_mdsetdebug(int subsys, int level);
    extern void     db_mdprint(int subsys, int level, const char *fmt,...);
    extern void     db_mdsubsysnames(char const * const *names);
    

    【讨论】:

    • 请注意,C #define for debug printing 在宏中讨论了 C99 和 __VA_ARGS__,这意味着您不必使用此处使用的双括号符号 — 如果您的所有编译器都支持它。
    【解决方案4】:

    在 Windows(以及整个 Microsoft)中,我们广泛使用 Event Tracing for Windows (ETW)。 ETW 是一种高效的静态日志记录机制。 NT 内核和许多组件都被很好地检测了。 ETW 有很多优点:

    • 可以在运行时动态启用/禁用任何 ETW 事件提供程序 - 无需重新启动或进程重新启动。大多数 ETW 提供程序提供对单个事件或事件组的精细控制。
    • 事件数据的格式化不会在运行时完成(这可能非常昂贵)。它在事件跟踪进行后处理时完成。
    • ETW 是Windows Event Log 的底层机制。如果您正确构建仪器,您将免费获得应用级日志记录。
    • 您的组件可以支持非常精细的事件启用,可以单独、按级别或分组(或任何组合)。您还可以在我们的代码中放置多个提供程序。
    • 可以将来自任何提供程序(最重要的是内核)的事件合并到单个跟踪中,以便关联所有事件。
    • 可以从盒子中复制合并的跟踪并进行完全处理 - 使用符号。
    • NT 内核样本配置文件中断可以生成 ETW 事件 - 这会产生一个非常轻量级的样本配置文件,可以随时使用
    • 在 Vista 和 Windows Server 2008 上,记录事件是无锁且完全支持多核的 - 每个处理器上的线程可以独立记录事件,而无需在它们之间进行同步。
    • ETW 在日志记录关闭时也很有效 - 它只是一个简单的布尔检查(ETW 执行此操作,或者您可以显式执行此操作)。请注意,这需要内核模式转换。一切都在进行中。

    这对我们来说非常有价值,也可以用于您的 Windows 代码 - ETW 可用于任何组件 - 包括用户模式、驱动程序和其他内核组件。

    许多人喜欢在他们的程序或组件运行时观察他们的日志输出。使用 ETW 很容易做到这一点。我们经常做的是编写一个流式 ETW 消费者。我没有将 printfs 放在代码中,而是将 ETW 事件放在有趣的地方。当我的组件运行时,我可以随时运行我的 ETW 观察器 - 观察器接收事件并显示它们、计算它们或用它们做其他有趣的事情。

    大多数代码都可以从良好实现的日志记录中受益。实施良好的静态运行时日志记录可以直接发现许多问题 - 没有它,您对组件中发生的事情的可见性为零。您可以查看输入、输出和猜测——仅此而已。

    这里的关键是“实施良好”这一术语。仪表必须在正确的位置。像其他任何事情一样,这需要一些思考和计划。如果它不在有用/有趣的地方,那么它不会帮助您在开发、测试或部署场景中发现问题。您还可能有太多的仪器在打开时导致性能问题 - 甚至关闭!

    Windows 中有多种工具可帮助您使用基于 ETW 的记录器和事件。其中包括 logman.exe 和 tracerpt.exe。还有xperf tools,它专注于性能,但也可以控制任何 ETW 提供程序和转储日志文件。

    【讨论】:

    • 不是每个人都在 Windows 上工作...但是 +1 因为我今天只是在寻找一个 Windowsy 解决方案!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-06
    • 2020-12-29
    相关资源
    最近更新 更多