프로그램이 서비스나 백그라운드 작업으로 돌 때는 화면에 오류를 띄울 수가 없으니 어딘가에 기록을 남겨야 합니다. 텍스트 파일 로그가 가장 흔하지만, 윈도우 이벤트 뷰어에 남기면 관리자가 다른 시스템 로그와 같은 화면에서 볼 수 있고 원격으로도 조회할 수 있다는 장점이 있습니다. .NET에서는 System.Diagnostics.EventLog 클래스가 이 일을 맡습니다.

CreateEventSource에서 source와 logName의 차이

처음 문서를 보면 CreateEventSource의 오버로드 설명이 잘 와닿지 않습니다. 오버로드는 세 가지입니다.

광고
오버로드 내용
CreateEventSource(EventSourceCreationData) 구성 속성을 담은 개체로 원본을 만듭니다. 지역화된 메시지 리소스 파일 등을 지정할 때 씁니다.
CreateEventSource(String, String) 원본 이름과 로그 이름을 받아 로컬 컴퓨터에 원본을 등록합니다. 로그가 없으면 새 사용자 지정 로그도 같이 만듭니다.
CreateEventSource(String, String, String) 컴퓨터 이름까지 받는 버전입니다. 사용되지 않음(Obsolete)으로 표시돼 있습니다.

C# 선언으로 보면 이해가 쉽습니다.

public static void CreateEventSource (string source, string logName);

logName은 이벤트 뷰어 왼쪽 트리에 보이는 로그(폴더) 이름이고, source는 그 로그 안의 각 항목에 "원본" 열로 찍히는 보내는 쪽 이름입니다. 한 로그 아래에 원본을 여러 개 둘 수 있습니다. 예를 들어 로그는 회사 이름으로 하나 만들고, 원본은 프로그램별로 나누는 식입니다.

전체 흐름을 보여 주는 예제

로그를 만들고, 항목을 쓰고, 읽고, 지우고, 시스템의 로그 목록을 찍는 과정을 한 번에 보여 주는 예제입니다. 다른 곳에서 가져온 코드이고, 그대로는 메서드 밖에 문장이 있어서 콘솔 프로그램의 Sub Main 같은 곳에 넣어 실행해야 합니다.

Imports System.Diagnostics    '// 출처

    Dim aLog As EventLog
    Dim myLog As New EventLog
    Dim aEventLogList() As EventLog
    Dim aLogEntry As EventLogEntry
    Dim aLogEntries As EventLogEntryCollection

    '
    ' Create a new log.
    '
    If Not EventLog.SourceExists("MyNewSource") Then
        EventLog.CreateEventSource("MyNewSource", "MyNewLog")
    End If

    '
    ' Add an event to fire when an entry is written.
    '
    AddHandler myLog.EntryWritten, AddressOf OnEntryWritten

    With myLog
        .Source = "MyNewSource"
        .Log = "MyNewLog"
        .EnableRaisingEvents = True

        '
        ' Write a few entries.
        '
        .WriteEntry("Writing Error entry to event log", EventLogEntryType.Error)
        .WriteEntry("Writing Information entry to event", EventLogEntryType.Information)
        .WriteEntry("Writing Warning entry to event", EventLogEntryType.Warning)

        '
        ' Output all events in the new log.
        '
        aLogEntries = .Entries()
        For Each aLogEntry In aLogEntries
            With aLogEntry
                Console.WriteLine( _
                    "Source: {0}" & vbCrLf & _
                    "Category: {1}" & vbCrLf & _
                    "Message: {2}" & vbCrLf & _
                    "EntryType: {3}" & vbCrLf & _
                    "EventID: {4}" & vbCrLf & _
                    "UserName: {5}", _
                    .Source, .Category, .Message, .EntryType, .EventID, .UserName)
            End With
        Next

        '
        ' Delete your new log.
        '
        .Clear()
        .Delete("MyNewLog")

        '
        ' Output the names of all logs on the system.
        '
        aEventLogList = .GetEventLogs()
        For Each aLog In aEventLogList
            Console.WriteLine("Log name: " & aLog.LogDisplayName)
        Next
    End With

    Public Sub OnEntryWritten(ByVal source As Object, ByVal e As EntryWrittenEventArgs)
        Console.WriteLine(("Written: " + e.Entry.Message))
    End Sub

WriteEntry의 두 번째 인자 EventLogEntryType으로 오류·경고·정보를 구분해 쓰면 이벤트 뷰어에서 아이콘이 달라져 한눈에 구분됩니다. EnableRaisingEvents = True로 두면 항목이 기록될 때마다 EntryWritten 이벤트가 발생합니다. Clear()는 로그 안의 항목만 비우고, Delete("MyNewLog")는 로그 자체와 거기 등록된 원본까지 지웁니다.

직접 만들어 본 버튼 예제

위 흐름을 이해하고 윈폼 버튼 하나에 최소한만 붙여 봤습니다. 원본은 program1472, 로그는 IT_HUB입니다.

Imports System.Security.Principal

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        If Not IsAdministrator() Then MsgBox("관리자 권한으로 실행해 주시기 바랍니다.")
        If Not EventLog.SourceExists("program1472") Then _
            EventLog.CreateEventSource("program1472", "IT_HUB")
        Dim myLog As EventLog = New EventLog()
        myLog.Source = "program1472"
        myLog.WriteEntry("Writing to event log.")
    End Sub

    Public Function IsAdministrator() As Boolean
        Dim identity As WindowsIdentity = WindowsIdentity.GetCurrent()
        If identity IsNot Nothing Then
            Dim principal As New WindowsPrincipal(identity)
            Return principal.IsInRole(WindowsBuiltInRole.Administrator)
        End If
        Return False
    End Function

여기서 제일 많이 막히는 부분이 권한입니다. SourceExists는 보안 로그까지 포함해 모든 로그를 뒤지고, CreateEventSource는 레지스트리에 원본을 등록하기 때문에 관리자 권한이 없으면 보안 예외가 납니다. 그래서 IsAdministrator로 먼저 확인하게 했습니다.

주의: 위 코드는 관리자가 아닐 때 메시지만 띄우고 그대로 아래로 내려가서 결국 예외가 납니다. 메시지 뒤에 빠져나가도록 고친 버전은 이렇습니다.

        If Not IsAdministrator() Then
            MsgBox("관리자 권한으로 실행해 주시기 바랍니다.")
            Exit Sub
        End If

원본 등록은 한 번만 하면 되므로, 실무에서는 설치 프로그램(관리자 권한으로 실행됨)에서 원본을 만들어 두고 프로그램 본체는 WriteEntry만 하는 구조가 일반적입니다. 등록이 끝난 원본에 쓰는 건 일반 사용자 권한으로도 됩니다.

이벤트 뷰어에서 확인

Win+R을 눌러 eventvwr.msc를 실행하면 "응용 프로그램 및 서비스 로그" 아래에 IT_HUB가 새로 생긴 걸 볼 수 있습니다.

이벤트 뷰어의 응용 프로그램 및 서비스 로그 아래에 IT_HUB 로그가 생긴 화면

클릭하면 버튼을 누를 때마다 남긴 항목이 쌓여 있습니다.

IT_HUB 로그를 선택해 기록된 이벤트 항목을 확인하는 화면

직접 해 보고 나서 저는 이 프로젝트에서는 이벤트 로그를 쓰지 않기로 했습니다. 원본 등록에 관리자 권한이 필요하고, 한 번 만든 로그는 Delete로 지우기 전까지 사용자 PC의 이벤트 뷰어에 계속 남기 때문에 배포 대상이 많은 일반 프로그램에는 부담이 됩니다. 서버에서 상주하는 서비스처럼 관리자가 이벤트 뷰어로 상태를 확인하는 환경이라면 여전히 쓸 만한 선택지입니다. 테스트하면서 만든 로그는 EventLog.Delete("IT_HUB")로 정리해 두세요.