syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

ログ用に時刻を取得して文字列化する (C#, Java, POSIX/Win32)

アプリケーションのデバッグや、デプロイ後の問題解析に最も重要な役割を果たすのはログです。ログの機能や精度・粒度によって、問題解決のしやすさがほぼすべて決まります。また、ログは単なるトラブルシューティングだけでなく、場合によってはユーザー操作記録などの簡易的なエビデンス(証拠)としても使えます*1
通例、動作ログにタイミング情報を記録する際、レコード行に時刻情報を含めることで、状態遷移の正しさの確認や速度性能解析に役立てることができます。いわゆるprintfデバッグです。レコードの時刻情報はログをファイルに記録するときだけでなく、デバッグ出力するときにも役立ちます。

また、秒単位ではなくミリ秒単位の時刻情報が記録できると性能解析に便利です*2。なお、時刻は協定世界時 (UTC) で記録しておくと逆シリアライズが簡単になるというメリットがあるのですが、現地時刻(ローカル時刻)とタイムゾーン情報(GMTからのオフセット)を記録する方式のほうが、人間がログレコードを見て判断しやすくなるというメリットがあります。

ログファイルのフォーマットはパフォーマンスの観点から、ファイルの全書き換えではなくプレーンテキストの末尾追加で済むCSVフォーマットやTSVフォーマットなどが好ましいです。また、後で回収できるように、日付ごとにファイルを分けて作成したり、直近数か月分を保持して古いものは自動削除する循環ログにしたり、といった機能を実装します。自分はロギング機能を自前で実装したことが何度かありますが、通例 log4jlog4net を使っているプロジェクトも多いのではないかと思います。

今回はロギングの実装までは踏み込まず、C#, Java, C/C++におけるそれぞれの時刻情報取得と文字列化(フォーマット)についてのみ説明します。
ちなみにISO 8601は日付と時刻の間にTを入れるルールになっているのですが、今回は見やすさ優先のためASCII空白0x20にしました。

C#

C# (.NET) は後発なだけあって一番簡単です。洗練されていて美しいですね。余計な解説をするようなことはほとんどありません。

using System;

public class DateTimeFormatTest {
    public static void Main() {
        const string format = "yyyy-MM-dd HH:mm:ss.fffzzz";
        // GMT からのオフセット値が 0 の場合、タイムゾーンは "+00:00" になる。 
        Console.WriteLine("Local = " + DateTime.Now.ToString(format));
        Console.WriteLine("UTC   = " + DateTime.UtcNow.ToString(format));
    }
}

もしローカル時刻とUTC時刻との間の変換が必要な場合、TimeZoneInfo.ConvertTimeFromUtc()TimeZoneInfo.ConvertTimeToUtc()を使います。

なお、ラウンドトリップ書式指定子 "O", "o" を用いることで、簡単にISO 8601形式の書式化(シリアライズ)と解析(逆シリアライズ)ができるようです。

Java

Javaも時刻関連ライブラリは最初期から実装・標準化されています。しかしタイムゾーンの扱いが若干厄介な印象を受けます。ログのパーサーを書くときに考慮する必要があります。
注意点として、SimpleDateFormatはスレッドセーフではないので、スレッド間でインスタンスを使いまわすのは厳禁です。スレッドごとにインスタンスを作成する必要があります。

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

public class Main {
    private static final String FORMAT = "yyyy-MM-dd HH:mm:ss.SSSXXX";
    public static void main(String[] args) {
        // GMT からのオフセット値が 0 の場合、タイムゾーンは "Z" になる。 
        System.out.println("Local = " + (new SimpleDateFormat(FORMAT, Locale.ROOT).format(new Date())));
        final SimpleDateFormat sdf = new SimpleDateFormat(FORMAT, Locale.ROOT);
        sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
        System.out.println("UTC   = " + sdf.format(new Date()));
    }
}

C/C++

.NETもJavaもプラットフォーム非依存の高水準環境であり、標準ライブラリも充実しているので特に困るようなことはありませんが、問題はC/C++です。こいつらは標準化がとにかく遅い言語で、時刻関連の標準ライブラリは貧弱の一言です。まともな情報を取得するには、プラットフォームごとのライブラリやAPIを使わざるを得ません。

POSIX

UNIX時刻を表すtime_t型からtm構造体への変換がミソです。ちなみに逆変換をする関数mktime(), timegm()も存在します。

なお、標準Cライブラリのlocaltime(), gmtime()はスレッドセーフでないので使わないようにします。代わりにリエントラントなlocaltime_r(), gmtime_r()を使います。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//#include <sys/time.h>

int main() {
    printf("sizeof(time_t) = %d\n", static_cast<int>(sizeof(time_t)));
    // 秒単位まででよければ、time() 関数を使う。
    //const time_t now = time(nullptr);
#if 0
    // C 標準では time_t の実際の型が整数であるかどうかは規定されていない。
    // POSIX では符号付き整数であることが保証される。
    if (time(&now) == -1) {
        puts("Failed to get time!!");
        return -1;
    }
#endif
    // gettimeofday() や clock_gettime() を使うと、マイクロ秒やナノ秒単位で時刻を求めることができる。
#if 0
    // timezone 構造体は廃止予定の非推奨機能。
    // gettimeofday() 関数は廃止予定の非推奨機能。
    struct timeval tvo = {};
    //struct timezone tzo = {};
    //gettimeofday(&tvo, &tzo);
    gettimeofday(&tvo, nullptr);
    const time_t now = tvo.tv_sec;
#endif

    struct timespec tso = {};
    if (clock_gettime(CLOCK_REALTIME, &tso) != 0) {
        puts("Failed to get clock time!!");
        return -1;
    }
    const time_t now = tso.tv_sec;

    // ctime_r() や asctime_r() は出力フォーマットがお粗末なので却下。
    struct tm tmo = {};
    if (localtime_r(&now, &tmo) != nullptr) {
        // tm_gmtoff, tm_zone は BSD 拡張。
        const int gmtOffsetHourPart = tmo.tm_gmtoff / 3600;
        const int gmtOffsetMinPart = abs(tmo.tm_gmtoff / 60) % 60;
        // ミリ秒部分を単純に "%03.0f" で四捨五入すると、例えば 999.9 の繰り上がりで破たんする。
        //const int msPart = static_cast<int>(tvo.tv_usec / 1000);
        const int msPart = static_cast<int>(tso.tv_nsec / 1000L / 1000L);
        printf("Local = %04d-%02d-%02d %02d:%02d:%02d.%03d%+03d:%02d\n",
            tmo.tm_year + 1900, tmo.tm_mon + 1, tmo.tm_mday, tmo.tm_hour, tmo.tm_min, tmo.tm_sec,
            msPart,
            gmtOffsetHourPart, gmtOffsetMinPart);
        // DST = Daylight Saving Time: 夏時間(サマータイム)。
        printf("WeekDay = %d, YearDay = %d, DST = %d, Zone = \"%s\"\n",
            tmo.tm_wday, tmo.tm_yday, tmo.tm_isdst, tmo.tm_zone);
        // +hhmm や -hhmm 形式でよければ strftime() を使うこともできる。
        char buf[128] = {};
        strftime(buf, sizeof(buf), "%F %T %z (%Z)", &tmo);
        printf("Local = %s\n", buf);
    }

    if (gmtime_r(&now, &tmo) != nullptr) {
        printf("UTC   = %04d-%02d-%02d %02d:%02d:%02d\n",
            tmo.tm_year + 1900, tmo.tm_mon + 1, tmo.tm_mday, tmo.tm_hour, tmo.tm_min, tmo.tm_sec);
    }
}

Win32

Visual C++のランタイムライブラリには、POSIXlocaltime_r(), gmtime_r()に相当するlocaltime_s(), gmtime_s()がMSVC 8.0以降のSecure CRTとして実装されていますが、引数や戻り値の仕様が異なるので注意が必要です。また、C11規格(C++11ではない)ではlocaltime_s(), gmtime_s()が追加されていますが、MSVCの同名関数とはインターフェイスが異なるので注意が必要です。ちなみに、MSVC 8.0以降のtime_tは64bit化されていて、32bitプラットフォームでも既定で64bit整数となり、2038年問題を回避できるようになっています*3

いずれにせよ、Windowsにはgettimeofday()clock_gettime()が実装されておらず、tm構造体のtm_gmtoff, tm_zoneメンバーも実装されていないので、ミリ秒単位で時刻を求めたり、タイムゾーン情報を取得したりする場合はWindows APIを直接叩くのがベストだと思います。

なお、TIME_ZONE_INFORMATION構造体のメンバーのうち、夏時間の扱いはよく分かりませんでした。日本はサマータイムとかいうクソシステムなんぞ採用していないのに、DaylightBias-60となります。

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
#include <Windows.h>

int main() {
    setlocale(LC_ALL, "");

    SYSTEMTIME loc = {};
    ::GetLocalTime(&loc);
    SYSTEMTIME utc = {};
    ::GetSystemTime(&utc);

    TIME_ZONE_INFORMATION tzInfo = {};
    if (::GetTimeZoneInformation(&tzInfo) != TIME_ZONE_ID_INVALID) {
        const int gmtOffsetHourPart = -tzInfo.Bias / 60;
        const int gmtOffsetMinPart = abs(tzInfo.Bias) % 60;
        printf("Local = %04hd-%02hd-%02hd %02hd:%02hd:%02hd.%03hd%+03d:%02d\n",
            loc.wYear, loc.wMonth, loc.wDay, loc.wHour, loc.wMinute, loc.wSecond,
            loc.wMilliseconds,
            gmtOffsetHourPart, gmtOffsetMinPart);
        printf("WeekDay = %hd, Zone = \"%S\"\n", loc.wDayOfWeek, tzInfo.StandardName);
    }
    printf("UTC   = %04hd-%02hd-%02hd %02hd:%02hd:%02hd.%03hd\n",
        utc.wYear, utc.wMonth, utc.wDay, utc.wHour, utc.wMinute, utc.wSecond,
        utc.wMilliseconds);
}

説明の簡略化のためprintf系を使いましたが、C/C++で文字列バッファにフォーマット出力するときはsnprintf系やstd::stringstreamを使います。もしWindows限定でよければATL::CString::Format()を使うと簡単です。

Boost C++ライブラリのboost::posix_timeboost::date_timeを使えば、一応プラットフォーム非依存なコードを書けそうですが、JavaC#のように言語標準の方法で手軽かつ簡単に、というわけにはいきません。

ところで、C++11では時刻を扱うstd::chronoが追加されましたが、このライブラリは設計が大仰なわりに機能が貧弱で、しかも使い方が直感的でなくかなり面倒です。C++20で日付フォーマット関連の機能が追加されるらしいのですが、今更ですね。標準化が遅すぎる。C/C++JavaC#に比べて言語機能もライブラリも10年どころか20年以上遅れをとっています。生産性や移植性の低いC/C++は、21世紀における新時代のアプリケーション開発に使うべき言語ではありません。引退すべき言語とまでは言いませんが、今後はハードウェア層に近いバックエンドの記述などにとどめるべきです。

*1:もちろんコンプライアンス上の問題がない限り、という条件は付きます。ログにユーザーIDやパスワードを平文で記録するようなことは絶対にしてはいけません。最近の事例だとFacebookがやらかしていたことが報じられています。Facebookが数億件分のパスワードを暗号化しないままサーバーに保存していたことが明らかに - GIGAZINE

*2:システム時刻を処理時間計測に使うと、アプリケーション稼働中にシステム時刻が変更された場合に破たんするので、単調増加することが保証されるmonotonic (steady) なタイマーを使って2点間の差を計算するのがセオリーです。とはいえ、稼働中にシステム時刻の変更がないという前提であれば、システム時刻を処理時間計測に使うことはできます。

*3:逆に言うと、古いコンパイラコンパイル&ビルドされたアプリケーションは2038年問題を回避できません。