每当我需要.net中的日志记录功能时,我都会使用日志记录框架,例如NLog。显然,没有用于vba的日志记录框架,至少我不知道。

尽管我喜欢使用NLog,但它与目标,规则和记录器一起工作的方式也会变得如此。

所以我保留了记录器的思想,并在LogManager静态类中集中记录日志消息的格式–该类提供了整个客户端API;客户端代码不会直接处理ILogger的实现,而是调用LogManager.Log方法,该方法会将日志消息“分发”给所有相关的记录器。


LogManager类模块


Option Explicit

Public Enum LogLevel
    TraceLevel = 0
    DebugLevel
    InfoLevel
    WarnLevel
    ErrorLevel
    FatalLevel
End Enum

Private Type TLogManager
    Formatter As ILogMessageFormatter
    Loggers As New Dictionary
End Type

Private this As TLogManager

Public Property Get Formatter() As ILogMessageFormatter
    Set Formatter = this.Formatter
End Property

Public Property Set Formatter(ByVal value As ILogMessageFormatter)
    Set this.Formatter = value
End Property

Public Sub Register(ByVal logger As ILogger)
    If Not this.Loggers.Exists(logger.Name) Then
        this.Loggers.Add logger.Name, logger
    Else
        Err.Raise vbObjectError + 1098, "LogManager.Register", "There is already a logger registered with name '" & logger.Name & "'."
    End If
End Sub

Public Function IsEnabled(ByVal level As LogLevel) As Boolean

    Dim logger As ILogger
    Dim item As Variant
    For Each item In this.Loggers.Items
        Set logger = item
        If level >= logger.MinLevel Then
            IsEnabled = True
            Exit Function
        End If
    Next

End Function

Public Sub Log(ByVal level As LogLevel, ByVal message As String, Optional ByVal loggerName As String)

    Dim logger As ILogger
    If loggerName = vbNullString Then

        Dim item As Variant
        For Each item In this.Loggers.Items
            Set logger = item
            LogWith logger, level, message
        Next

    ElseIf this.Loggers.Exists(loggerName) Then

        LogWith this.Loggers(loggerName), level, message

    Else
        Err.Raise vbObjectError + 1099, "LogManager.Log", "There is no registered logger named '" & loggerName & "'."
    End If

End Sub

Private Sub LogWith(ByVal logger As ILogger, ByVal level As LogLevel, ByVal message As String)
    If level >= logger.MinLevel Then
        logger.Log FormatMessage(level, logger.Name, message)
    End If
End Sub

Friend Function FormatMessage(ByVal level As LogLevel, ByVal loggerName As String, ByVal message As String)
    FormatMessage = this.Formatter.FormatMessage(level, loggerName, message)
End Function

Private Sub Class_Initialize()
    Set this.Formatter = New DefaultLogMessageFormatter
End Sub


我不喜欢*Manager的名称,因此建议不胜枚举... OTOH NLog确实具有LogManager类。


ILogMessageFormatter类模块(接口)


Option Explicit

Public Function FormatMessage(ByVal level As LogLevel, ByVal loggerName As String, ByVal message As String) As String
End Function


此接口与LogManager.Formatter属性一起,允许配置日志消息的输出方式。这是默认的实现:


DefaultLogMessageFormatter类模块


Option Explicit
Implements ILogMessageFormatter

Private Function ILogMessageFormatter_FormatMessage(ByVal level As LogLevel, ByVal loggerName As String, ByVal message As String) As String
    ILogMessageFormatter_FormatMessage = Framework.Strings.Format("{0:s}\t{1}\t[{2}]\t{3}", Now, loggerName, FormatLogLevel(level), message)
End Function

Private Function FormatLogLevel(ByVal level As LogLevel) As String

    Select Case level

        Case LogLevel.DebugLevel
            FormatLogLevel = "DEBUG"

        Case LogLevel.ErrorLevel
            FormatLogLevel = "ERROR"

        Case LogLevel.FatalLevel
            FormatLogLevel = "FATAL"

        Case LogLevel.InfoLevel
            FormatLogLevel = "INFO"

        Case LogLevel.TraceLevel
            FormatLogLevel = "TRACE"

        Case LogLevel.WarnLevel
            FormatLogLevel = "WARNING"

    End Select

End Function


ILogger接口仅规定记录器具有一个名称和最小级别属性,以及一个接受格式化日志输出的Log方法:


ILogger类模块(接口)


Option Explicit <我已经为ILogger接口编写了2种实现,都在带有默认实例PublicNotCreatable的类中,并且公开了Create方法以返回实例。一个写入立即窗格,并称为DebugLogger:DebugLogger类模块


Public Sub Log(ByVal output As String)
End Sub

Public Property Get Name() As String
End Property

Public Property Get MinLevel() As LogLevel
End Property


另一个写入指定的文本文件,并称为FileLogger


FileLogger类模块


Option Explicit

Private Type TDebugLogger
    Name As String
    MinLevel As LogLevel
End Type
Private this As TDebugLogger

Implements ILogger

Public Function Create(ByVal loggerName As String, ByVal loggerMinLevel As LogLevel) As ILogger

    Dim result As New DebugLogger
    result.Name = loggerName
    result.MinLevel = loggerMinLevel
    Set Create = result

End Function

Friend Property Get Name() As String
    Name = this.Name
End Property

Friend Property Let Name(ByVal value As String)
    this.Name = value
End Property

Friend Property Get MinLevel() As LogLevel
    MinLevel = this.MinLevel
End Property

Friend Property Let MinLevel(ByVal value As LogLevel)
    this.MinLevel = value
End Property

Private Sub ILogger_Log(ByVal output As String)
    Debug.Print output
End Sub

Private Property Get ILogger_MinLevel() As LogLevel
    ILogger_MinLevel = this.MinLevel
End Property

Private Property Get ILogger_Name() As String
    ILogger_Name = this.Name
End Property


FileLogger使用一个处理打开,写入和关闭文件的TextWriter对象-超出了本文的范围,但可以在此处进行评论。


客户代码

客户端代码可以以不同的方式实现ILogger,并使该代码将日志条目写入例如数据库中。提供FileLoggerDebugLogger是为了方便起见,但没有禁止在客户端上实现和注册自定义ILogger的信息。
Option Explicit

Private Type TFileLogger
    Name As String
    MinLevel As LogLevel
    LogFile As String
    Writer As TextWriter
End Type
Private this As TFileLogger

Implements ILogger

Public Function Create(ByVal loggerName As String, ByVal loggerMinLevel As LogLevel, ByVal path As String) As ILogger

    Dim result As New FileLogger
    result.Name = loggerName
    result.MinLevel = loggerMinLevel
    Set result.Writer = TextWriter.Create
    result.LogFile = path
    Set Create = result

End Function

Friend Property Get Name() As String
    Name = this.Name
End Property

Friend Property Let Name(ByVal value As String)
    this.Name = value
End Property

Friend Property Get MinLevel() As LogLevel
    MinLevel = this.MinLevel
End Property

Friend Property Let MinLevel(ByVal value As LogLevel)
    this.MinLevel = value
End Property

Friend Property Get LogFile() As String
    LogFile = this.LogFile
End Property

Friend Property Let LogFile(ByVal value As String)
    this.LogFile = value
End Property

Friend Property Get Writer() As TextWriter
    Set Writer = this.Writer
End Property

Friend Property Set Writer(ByVal value As TextWriter)
    Set this.Writer = value
End Property

Private Sub ILogger_Log(ByVal output As String)
    If this.Writer.OpenFile(this.LogFile) Then

        this.Writer.WriteLine output
        this.Writer.CloseFile

    Else
        Err.Raise vbObjectError + 1092, "ILogger.Log", "FileLogger.LogFile could not be opened."
    End If
End Sub

Private Property Get ILogger_MinLevel() As LogLevel
    ILogger_MinLevel = this.MinLevel
End Property

Private Property Get ILogger_Name() As String
    ILogger_Name = this.Name
End Property


这段代码在Logging.xlam中生成以下输出:

 C:\Dev\VBA\log.txt 


以及即时窗格中的以下输出:

 2014-09-28 19:42:44 TestLogger  [ERROR] Division by zero
 


因为在TestLogger 2014-09-28 19:42:44 MyLogger [INFO] it works! False 2014-09-28 19:42:44 MyLogger [ERROR] Division by zero 处没有MinLevel的记录器,所有TraceLevel日志条目都将被忽略;客户端代码可以使用TraceLevel函数检查当前是否启用了给定的日志级别,并有条件地注册记录器。

评论

太棒了有一个小项,但没有代码:我不知道在发布后它是否被更改,但是在提供的链接中找不到FileLogger类中引用的TextWriter类。也许FileWriter曾经被称为TextWriter?无论哪种方式,我都假定当时使用的类具有将Create,Open,Close和WriteLine转换为带有loggerFile名称的文本文件的方法。

这有点超出了您在此处编写代码的范围,但是我不知道还有什么可以联系到您的。我一直在阅读您的大量代码审查,以尝试通过使用vba获得更多的OOP。当您编写这样的组件(记录器,文件编写器等)时,通常是否将它们保存在自己的.xlam文件中,然后像对待库一样引用它们?

@ArcherBird,直到有人提出针对VBA代码的软件包管理器解决方案(例如nuget,npm等),这才是使这些组件保持在附近的好方法。另一种方法是在\ Dev \ VBA文件夹中包含所有模块(在git源代码控制下),可以从其中轻松地将文件导入到任何新的VBA项目中。

@ArcherBird(如果您使用的是Rubberduck),则可以在模块顶部使用@PredeclaredId注释轻松添加为类提供默认实例的VB_PredeclaredId = True隐藏属性,并且可以将“ predeclared class”模板添加到您的代码浏览器工具窗口中的项目,以便您免费获得该属性。该记录器代码使用默认实例之外的工厂方法(并且早于Rubberduck!)。实际上,我发现工厂方法(与显式接口结合)非常棒,尤其是在外接程序设置中:您可以真正构建公共API。

@ArcherBird这是一个很难发现的功能。请参见存储库Wiki上的VB_Attribute注释和“使用文件夹”注释。

#1 楼



真是挑剔,但除非您不想从零开始,否则没有理由声明枚举的起点。


Public Enum LogLevel
  TraceLevel = 0
  DebugLevel
  '.....



您还在这里重复很多。几乎是匈牙利人。它们都是LogLevel。我认为您可以将它们缩短为TraceDebugInfo等。当然,vba对于何时可以调用和不能调用带有LogLevel.Trace这样的完整引用的枚举成员可能会有些奇怪,因此您可以或可能不想这样做。我本人对此不感兴趣。但是您不能这样做,因为Debug是保留关键字...。


我真的建议为您的自定义错误编号使用Enum。如果它们都放在一个地方,维护起来会容易得多。


Err.Raise vbObjectError + 1098, "LogManager.Register", "There is already a logger registered with name '" & logger.Name & "'."
'.......
Err.Raise vbObjectError + 1099, "LogManager.Log", "There is no registered logger named '" & loggerName & "'."



使用枚举看起来像这样的枚举,它们变得更加简单。

Public Enum LogMangerError
    SomeError = vbObjectError + 1098
    SomeOtherError 
End Enum

'......

Err.Raise SomeError, "LogManager.Register", "There is already a logger registered with name '" & logger.Name & "'."



这是阅读的绝对乐趣。大量使用空白。


Select Case level

    Case LogLevel.DebugLevel
        FormatLogLevel = "DEBUG"

    Case LogLevel.ErrorLevel
        FormatLogLevel = "ERROR"

    Case LogLevel.FatalLevel
        FormatLogLevel = "FATAL"

    Case LogLevel.InfoLevel
        FormatLogLevel = "INFO"

    Case LogLevel.TraceLevel
        FormatLogLevel = "TRACE"

    Case LogLevel.WarnLevel
        FormatLogLevel = "WARNING"

End Select





也许我累了,或者也许很好。我目前无法确定,但是我认为您这里有一些非常可靠的代码。良好的命名和格式,以及非常好的抽象和使用接口。