An Embedded Engineer’s Blog

とある組み込みエンジニアの備忘録的なブログです。

C++でログ出力

まえがき

今回は、以下のような機能を持ったログ出力クラス(Logger)を実装します。

  • Infomation / Warning / Errorのログを使い分けられる
  • 各ログは分かりやすいように色付きで表示
  • 文字列フォーマッティング対応
  • マルチスレッド時の排他処理
  • 同時にファイルへのログ出力可能

なお、以前作成した文字列フォーマット関数StringFormatを使用しているので、必要に応じて参照してください。

前提条件

今回実装したソースは以下の環境でビルド、動作確認しています。

OS Ver Compiler Remarks
Windows 10 Visual Studio 2019

実装

以下の2つのクラスに分けて実装します。

  • File Logger : ファイルへのログ出力クラス
  • Logger : ログ出力インタフェースクラス

File Loggerクラスは、Loggerクラスで使用され、コンソールへのログ出力と同時にファイルへもログ出力を行います。

File Loggerクラス

ファイルへのログ出力は、標準ライブラリのfstreamクラスを使って、ファイルのオープン、クローズ、文字列出力を行う関数を実装するだけです。

【FileLogger.h】

#pragma once
#include <fstream>

/* File Loggerクラス宣言 */
class FileLogger
{
public:
    /* コンストラクタ */
    FileLogger();

    /* コンストラクタ */
    FileLogger(const std::string& file_path);

    /* デストラクタ */
    ~FileLogger();

    /* ファイルオープン */
    void Open(const std::string& file_path);

    /* ファイルクローズ */
    void Close();

    /* ファイルオープン確認 */
    bool IsOpened();

    /* ファイル書き込み */
    void Write(const std::string& log);

private:
    /* ログファイルパス */
    std::string m_FilePath;

    /* ファイル出力ストリーム */
    std::ofstream m_Stream;
};


【FileLogger.cpp】

#include "FileLogger.h"

/* コンストラクタ */
FileLogger::FileLogger()
    : m_FilePath("")
    , m_Stream()
{
    /* Nothing to do */
}

/* コンストラクタ */
FileLogger::FileLogger(const std::string& file_path)
    : m_FilePath(file_path)
    , m_Stream()
{
    /* ファイルオープン */
    this->Open(file_path);
}

/* デストラクタ */
FileLogger::~FileLogger()
{
    /* ファイルクローズ */
    this->Close();
}

/* ファイルオープン */
void FileLogger::Open(const std::string& file_path)
{
    /* ファイルが開いていなかったら */
    if (this->IsOpened() == false)
    {
        this->m_Stream.open(file_path, std::ios::out);
    }
}

/* ファイルクローズ */
void FileLogger::Close()
{
    /* ファイルが開いていたら */
    if (this->IsOpened() == true)
    {
        this->m_Stream.close();
    }
}

/* ファイルオープン確認 */
bool FileLogger::IsOpened()
{
    return this->m_Stream.is_open();
}

/* ファイル書き込み */
void FileLogger::Write(const std::string& log)
{
    /* ファイルが開いていたら */
    if (this->IsOpened() == true)
    {
        /* ファイルストリームに書き込み */
        this->m_Stream << log << std::endl;
    }
}


Loggerクラス

ログ出力は、前述の要件を満たすために、以下のように実装します。

  1. どこから呼んでも同じインスタンスにアクセスできるようにシングルトンで実装する
  2. StringFormat関数で文字列フォーマット可能なインタフェースを定義する
  3. ログ出力時にはMutexによる排他処理を行う
  4. コンソールへのログ出力時に制御コードで文字色を変更する
  5. コンソールへのログ出力と同時にFileLoggerクラスでファイルへのログ出力も行う
1. シングルトンで実装

まずは、どこから呼んでも同じインスタンスにアクセスできるように、シングルトンでLoggerクラスを定義します。

このとき、外部からインスタンスを取得できないように、シングルトンインスタンス取得関数をprivateにしておきます。

また、シングルトンにするためにコンストラクタ/デストラクタをprivateにし、コピー/ムーブを禁止します。

【Logger.h】

#include <string>

/* Loggerクラス宣言 */
class Logger
{
private:
    /* シングルトンインスタンス取得 */
    static Logger& GetInstance();

private:
    /* コンストラクタ */
    Logger();
    /* デストラクタ */
    ~Logger();

public:
    /* コピーコンストラクタを削除 */
    Logger(const Logger&) = delete;
    /* ムーブコンストラクタを削除 */
    Logger(Logger&&) = delete;
    /* コピー代入オペレータを削除 */
    Logger& operator=(const Logger&) = delete;
    /* ムーブ代入オペレータを削除 */
    Logger& operator=(Logger&&) = delete;

private:
    /* Informationログ出力 */
    void LogInfo(const std::string& message);
    /* Warningログ出力 */
    void LogWarn(const std::string& message);
    /* Errorログ出力 */
    void LogError(const std::string& message);
}


【Lgger.cpp】

/* シングルトンインスタンス取得 */
Logger& Logger::GetInstance()
{
    static Logger instance;
    return instance;
}


2. 文字列フォーマット可能なインタフェースを定義

書式付き文字列のフォーマットが可能なログ出力インタフェース関数を定義します。

インタフェース関数は、クラスメソッド(static)として定義し、Information / Warning / Errorそれぞれ用の関数を用意します。

各関数の処理としては、StringFormat関数を使って、書式付き文字列をフォーマットして、シングルトンインスタンスのInformation / Warning / Error出力関数を呼び出します。

#include "StringFormat.h"

/* Loggerクラス宣言 */
class Logger
{
public:
    /* 書式指定Informationログ */
    template<typename ... Args>
    static void Info(const std::string& format, Args&& ... args)
    {
        /* 書式フォーマットしてシングルトンインスタンスのInformationログ出力呼び出し */
        Logger::GetInstance().LogInfo(StringFormat(format, std::forward<Args>(args) ...));
    }

    /* 書式指定Warningログ */
    template<typename ... Args>
    static void Warn(const std::string& format, Args&& ... args)
    {
        /* 書式フォーマットしてシングルトンインスタンスのWarningログ出力呼び出し */
        Logger::GetInstance().LogWarn(StringFormat(format, std::forward<Args>(args) ...));
    }

    /* 書式指定Errorログ */
    template<typename ... Args>
    static void Error(const std::string& format, Args&& ... args)
    {
        /* 書式フォーマットしてシングルトンインスタンスのErrorログ出力呼び出し */
        Logger::GetInstance().LogError(StringFormat(format, std::forward<Args>(args) ...));
    }

    /* 省略 */
}


3. Mutexによる排他制御

std::coutstd::cerrといった文字列出力関数はマルチスレッドで出力した場合に、その出力順序が保証されないため、複数スレッドで同時に文字列を出力すると、出力結果が混ざってしまいます。

それを回避するために、Mutexによる排他制御を行います。
Mutexを使うことで、ログ出力関数が呼ばれると、呼び終わるまでは別スレッドからの呼び出しが待機されるため、同時に呼ばれることがなくなり、行単位ではログが混ざらなくなります。
なお、複数行でのログ出力では混ざる可能性があるので別途排他処理が必要になります。

【Logger.h】

#include <mutex>

/* Loggerクラス宣言 */
class Logger
{
    /* 省略 */
private:
    /* ミューテックス */
    std::mutex m_Mutex;
}

【Logger.cpp】

/* コンストラクタ */
Logger::Logger()
    : m_Mutex()
{
}

/* Informationログ出力 */
void Logger::LogInfo(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* ログ出力 */
}

/* Warningログ出力 */
void Logger::LogWarn(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* ログ出力 */
}

/* Errorログ出力 */
void Logger::LogError(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* ログ出力 */
}
4. 制御コードによる文字色変更

std::coutなどを使って、コンソール似文字列を出力する際には、制御コードを挟むことによって、文字色を変更することが出来ます。

制御コード 機能 備考
\x1b[0m 制御コードリセット
\x1b[30m 文字色を黒色に変更
\x1b[31m 文字色を赤色に変更
\x1b[32m 文字色を緑色に変更
\x1b[33m 文字色を黄色に変更
\x1b[34m 文字色を青色に変更
\x1b[35m 文字色をマゼンタに変更
\x1b[36m 文字色をシアンに変更
\x1b[37m 文字色を白色に変更


今回は、以下のように文字色を変更して出力するようにしています。

  • Information : シアン
  • Warning : 黄色
  • Error : 赤色


【Logger.cpp】

#include <iostream>

/* コンソール出力文字色制御コード */
constexpr const char* ConsoleColorReset     = "\x1b[0m";
constexpr const char* ConsoleColorBlack     = "\x1b[30m";
constexpr const char* ConsoleColorRed       = "\x1b[31m";
constexpr const char* ConsoleColorGreen     = "\x1b[32m";
constexpr const char* ConsoleColorYellow    = "\x1b[33m";
constexpr const char* ConsoleColorBlue      = "\x1b[34m";
constexpr const char* ConsoleColorMagenta   = "\x1b[35m";
constexpr const char* ConsoleColorCyan      = "\x1b[36m";
constexpr const char* ConsoleColorWhite     = "\x1b[37m";

/* Informationログ出力 */
void Logger::LogInfo(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Informationログを生成 */
    std::string log_message = StringFormat("[INFO] %s", message);

    /* Informationログを標準出力に出力(シアン) */
    std::cout << ConsoleColorCyan << log_message << ConsoleColorReset << std::endl;
}

/* Warningログ出力 */
void Logger::LogWarn(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Warningログを生成 */
    std::string log_message = StringFormat("[WARN] %s", message);

    /* Warningログを標準出力に出力(黄色) */
    std::cerr << ConsoleColorYellow << log_message << ConsoleColorReset << std::endl;
}

/* Errorログ出力 */
void Logger::LogError(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Errorログを生成 */
    std::string log_message = StringFormat("[ERROR] %s", message);

    /* Errorログを標準出力に出力(赤色) */
    std::cerr << ConsoleColorRed << log_message << ConsoleColorReset << std::endl;
}


5. ファイルへのログ出力

ファイルへのログ出力はFileLoggerクラス経由で行います。

【Logger.h】

#pragma once
#include "FileLogger.h"

/* Loggerクラス宣言 */
class Logger
{
    /* 省略 */
private:
    /* File Loggerクラスインスタンス */
    FileLogger m_FileLogger;
};


【Logger.cpp】

/* 省略 */

/* コンストラクタ */
Logger::Logger()
    : m_Mutex()
    , m_FileLogger()
{
    /* ログファイルオープン */
    this->m_FileLogger.Open("log.txt");
}

/* デストラクタ */
Logger::~Logger()
{
    /* ログファイルクローズ */
    this->m_FileLogger.Close();
}

/* Informationログ出力 */
void Logger::LogInfo(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Informationログを生成 */
    std::string log_message = StringFormat("[INFO] %s", message);

    /* Informationログを標準出力に出力(シアン) */
    std::cout << ConsoleColorCyan << log_message << ConsoleColorReset << std::endl;

    /* Informationログをファイルに出力 */
    this->m_FileLogger.Write(log_message);
}

/* Warningログ出力 */
void Logger::LogWarn(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Warningログを生成 */
    std::string log_message = StringFormat("[WARN] %s", message);

    /* Warningログを標準出力に出力(黄色) */
    std::cerr << ConsoleColorYellow << log_message << ConsoleColorReset << std::endl;

    /* Warningログをファイルに出力 */
    this->m_FileLogger.Write(log_message);
}

/* Errorログ出力 */
void Logger::LogError(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Errorログを生成 */
    std::string log_message = StringFormat("[ERROR] %s", message);

    /* Errorログを標準出力に出力(赤色) */
    std::cerr << ConsoleColorRed << log_message << ConsoleColorReset << std::endl;

    /* Errorログをファイルに出力 */
    this->m_FileLogger.Write(log_message);
}


コード全文

コード全文を示します。

【Logger.h】

#pragma once
#include "StringFormat.h"
#include "FileLogger.h"

#include <string>
#include <mutex>

/* Loggerクラス宣言 */
class Logger
{
public:
    /* 書式指定Informationログ */
    template<typename ... Args>
    static void Info(const std::string& format, Args&& ... args)
    {
        /* 書式フォーマットしてシングルトンインスタンスのInformationログ出力呼び出し */
        Logger::GetInstance().LogInfo(StringFormat(format, std::forward<Args>(args) ...));
    }

    /* 書式指定Warningログ */
    template<typename ... Args>
    static void Warn(const std::string& format, Args&& ... args)
    {
        /* 書式フォーマットしてシングルトンインスタンスのWarningログ出力呼び出し */
        Logger::GetInstance().LogWarn(StringFormat(format, std::forward<Args>(args) ...));
    }

    /* 書式指定Errorログ */
    template<typename ... Args>
    static void Error(const std::string& format, Args&& ... args)
    {
        /* 書式フォーマットしてシングルトンインスタンスのErrorログ出力呼び出し */
        Logger::GetInstance().LogError(StringFormat(format, std::forward<Args>(args) ...));
    }

private:
    /* シングルトンインスタンス取得 */
    static Logger& GetInstance();

private:
    /* コンストラクタ */
    Logger();
    /* デストラクタ */
    ~Logger();

public:
    /* コピーコンストラクタを削除 */
    Logger(const Logger&) = delete;
    /* ムーブコンストラクタを削除 */
    Logger(Logger&&) = delete;
    /* コピー代入オペレータを削除 */
    Logger& operator=(const Logger&) = delete;
    /* ムーブ代入オペレータを削除 */
    Logger& operator=(Logger&&) = delete;

private:
    /* Informationログ出力 */
    void LogInfo(const std::string& message);
    /* Warningログ出力 */
    void LogWarn(const std::string& message);
    /* Errorログ出力 */
    void LogError(const std::string& message);

private:
    /* ミューテックス */
    std::mutex m_Mutex;

    /* File Loggerクラスインスタンス */
    FileLogger m_FileLogger;
};

【Logger.cpp】

#include "Logger.h"
#include <iostream>

/* コンソール出力文字色制御コード */
constexpr const char* ConsoleColorReset     = "\x1b[0m";
constexpr const char* ConsoleColorBlack     = "\x1b[30m";
constexpr const char* ConsoleColorRed       = "\x1b[31m";
constexpr const char* ConsoleColorGreen     = "\x1b[32m";
constexpr const char* ConsoleColorYellow    = "\x1b[33m";
constexpr const char* ConsoleColorBlue      = "\x1b[34m";
constexpr const char* ConsoleColorMagenta   = "\x1b[35m";
constexpr const char* ConsoleColorCyan      = "\x1b[36m";
constexpr const char* ConsoleColorWhite     = "\x1b[37m";


/* シングルトンインスタンス取得 */
Logger& Logger::GetInstance()
{
    static Logger instance;
    return instance;
}

/* コンストラクタ */
Logger::Logger()
    : m_Mutex()
    , m_FileLogger()
{
    /* ログファイルオープン */
    this->m_FileLogger.Open("log.txt");
}

/* デストラクタ */
Logger::~Logger()
{
    /* ログファイルクローズ */
    this->m_FileLogger.Close();
}

/* Informationログ出力 */
void Logger::LogInfo(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Informationログを生成 */
    std::string log_message = StringFormat("[INFO] %s", message);

    /* Informationログを標準出力に出力(シアン) */
    std::cout << ConsoleColorCyan << log_message << ConsoleColorReset << std::endl;

    /* Informationログをファイルに出力 */
    this->m_FileLogger.Write(log_message);
}

/* Warningログ出力 */
void Logger::LogWarn(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Warningログを生成 */
    std::string log_message = StringFormat("[WARN] %s", message);

    /* Warningログを標準出力に出力(黄色) */
    std::cerr << ConsoleColorYellow << log_message << ConsoleColorReset << std::endl;

    /* Warningログをファイルに出力 */
    this->m_FileLogger.Write(log_message);
}

/* Errorログ出力 */
void Logger::LogError(const std::string& message)
{
    /* ミューテックスによる排他処理 */
    std::lock_guard<std::mutex> lock(this->m_Mutex);

    /* Errorログを生成 */
    std::string log_message = StringFormat("[ERROR] %s", message);

    /* Errorログを標準出力に出力(赤色) */
    std::cerr << ConsoleColorRed << log_message << ConsoleColorReset << std::endl;

    /* Errorログをファイルに出力 */
    this->m_FileLogger.Write(log_message);
}


使用サンプル

Loggerクラスを使用したサンプルコードを示します。

/* Informationログ出力 */
Logger::Info("Information Log");

std::string warning_message = "warning text";

/* Warningログ出力 */
Logger::Warn("Warning Log : %s", warning_message);

std::string error_message = "error text";

int error_code = -1;

/* Errorログ出力 */
Logger::Error("Error Log : %s %d", error_message, error_code);


実行結果は以下のようになります。

Information Log
Warning Log : warning text
Error Log : error text -1

実際にコンソールに出力される文字列は色が変更されていると思います。
また、同じ内容がファイルにも出力されていると思います。