はじめに
UTC で動いているサーバーで、Python スクリプトのログ時刻(%(asctime)s)を日本時間にそろえたい、という場面は多いと思います。サーバーのタイムゾーン自体は変えられない(変えたくない)前提です。
AI(Claude)に十数本のスクリプトの修正を任せたところ、最初に入った「よく見る1行」が、ログを日本時間にするどころかログを1行も出さなくしていました。しかも終了コードは 0。この記事では、そのとき分かった書き方の選び方と落とし穴をまとめます。出力はすべて Windows の Python 3.13 で再現確認したものです。
結論:どれを使うか
| 方法 | 向いている場面 | 注意点 |
|---|---|---|
Formatter.converter を差し替える |
既存スクリプトに1行足したい |
staticmethod で包む/%z は信用しない |
Formatter を継承して formatTime を書く |
新規コード・%z も正しく出したい |
数行増える |
環境変数 TZ を変える |
プロセス全体を JST 前提で書いている | ログ以外の日付計算も変わる |
既存コードは1つ目、新規コードは2つ目、で迷いません。
方法1:converter を差し替える(1行で済む)
logging.Formatter は record.created(UNIX 秒)を converter に渡して時刻を作ります。既定は time.localtime なので、UTC サーバーなら UTC で出ます。ここに「UTC + 9時間」を返す関数を入れます。
import logging
import time
logging.basicConfig(format="%(asctime)s %(message)s", level=logging.INFO)
logging.Formatter.converter = staticmethod(
lambda secs: time.gmtime(secs + 9 * 3600)
)
logging.info("hello")
localtime ではなく gmtime を起点にしているので、サーバーが JST 設定だった場合でも 18 時間ずれることがありません。
落とし穴1:staticmethod を外すとログが消える
logging.Formatter.converter = lambda secs: time.gmtime(secs + 9 * 3600) # NG
これだと標準エラーにこう出て、本来のログは残りません。
--- Logging error ---
Traceback (most recent call last):
...
ct = self.converter(record.created)
TypeError: <lambda>() takes 1 positional argument but 2 were given
クラス属性に入れた Python 関数はメソッドとして束縛され、self が第1引数に入るためです。既定の time.localtime や公式ドキュメント例の time.gmtime は C 実装の組み込み関数なので束縛されず、問題になりません。自作の lambda / def だけがメソッドに化けます。
なお、Formatter のインスタンス属性に入れる場合は束縛されないので動きます。
formatter = logging.Formatter("%(asctime)s %(message)s")
formatter.converter = lambda secs: time.gmtime(secs + 9 * 3600) # これは動く
クラスかインスタンスかで挙動が変わるので、どちらでも staticmethod で包んでおくのが無難です。
厄介なのは、logging が出力時の例外を握りつぶす設計のため、スクリプトは止まらず終了コードも 0 という点です。cron などで標準エラーを見ていないと「処理は成功、ログファイルは空」という状態だけが残ります。
落とし穴2:同じ関数内の import time で NameError
def setup():
logging.basicConfig(format="%(asctime)s %(message)s", level=logging.INFO)
logging.Formatter.converter = staticmethod(
lambda secs: time.gmtime(secs + 9 * 3600)
)
logging.info("inside setup")
import time # 関数の後半にある import
NameError: cannot access free variable 'time' where it is not associated with a value in enclosing scope. Did you forget to import 'time'?
関数内のどこかに import time があると、その関数全体で time はローカル変数扱いになり、lambda もそれを参照します。ファイル冒頭で import time していても防げません。継ぎ足しを重ねたスクリプトほど関数の途中に import が紛れているので、converter を足す前に同じ関数内を検索しておくと安全です。
落とし穴3:%z は当てにならない
converter 方式は「9時間足した UTC」を作っているだけで、タイムゾーン情報を持ちません。datefmt に %z を入れて +0900 と出ても、それは実行マシンのローカル設定を拾っただけの可能性があります。時差表記まで正しく出したいなら方法2を使います。
方法2:formatTime を書き換える(依存なしで確実)
import logging
from datetime import datetime, timedelta, timezone
JST = timezone(timedelta(hours=9), "JST")
class JSTFormatter(logging.Formatter):
def formatTime(self, record, datefmt=None):
dt = datetime.fromtimestamp(record.created, JST)
if datefmt:
return dt.strftime(datefmt)
return dt.strftime("%Y-%m-%d %H:%M:%S") + f",{int(record.msecs):03d}"
handler = logging.StreamHandler()
handler.setFormatter(JSTFormatter("%(asctime)s %(levelname)s %(message)s"))
logging.getLogger().addHandler(handler)
UTC 20:12 に実行した結果:
2026-09-29 05:12:24,788 INFO default datefmt
2026-09-29 05:12:24 +0900 with %z
ZoneInfo を使う場合の注意(Windows)
zoneinfo.ZoneInfo("Asia/Tokyo") も使えますが、Windows では tzdata が無いと失敗し、ここでもログが出なくなります。
zoneinfo._common.ZoneInfoNotFoundError: 'No time zone found with key Asia/Tokyo'
pip install tzdata するか、日本には夏時間が無いので timezone(timedelta(hours=9)) 固定で十分です。
方法3:TZ 環境変数を使わなかった理由
TZ=Asia/Tokyo で起動すればコード変更ゼロで済みます。ただし影響はプロセス全体に及び、datetime.now() や time.localtime() で「今日」を決めている集計処理まで JST に変わります。JST 0〜9 時の間は UTC と日付が食い違うため、「昨日分を集計するつもりが今日分になる」といったずれが起こりえます。ログの見た目だけ変えたいなら、変更範囲も Formatter に閉じておく方が安全です。
まとめ
| 症状・目的 | 原因 | 対処 |
|---|---|---|
| ログ時刻を JST にしたい(既存コード) | converter の既定が time.localtime
|
staticmethod(lambda secs: time.gmtime(secs + 9 * 3600)) |
takes 1 positional argument but 2 were given |
クラス属性の lambda がメソッド扱い |
staticmethod で包む |
cannot access free variable 'time' |
同じ関数内の import time
|
関数内 import を消してファイル冒頭へ |
%z が信用できない |
converter 方式は TZ 情報を持たない |
formatTime を書き換える |
ZoneInfoNotFoundError(Windows) |
tzdata 未導入 |
pip install tzdata か固定オフセット |
TZ で集計日付までずれた |
TZ はプロセス全体に効く |
Formatter 側で対応 |
どれも失敗してもスクリプトは止まらず終了コード 0 です。ログ設定を変えたら、実際に1行出ることを目で確かめてから本番に入れるのがおすすめです。
元になった記事(AI側の視点で書いたもの): https://kujiragames.com/2026/09/python-logging-jst/