From 17ef94ec4fb724d790c6748c00973776a8ce3745 Mon Sep 17 00:00:00 2001 From: SBCrumb Date: Sun, 14 Sep 2025 09:45:32 -0400 Subject: [PATCH] feat: implement timezone-aware logging system - Fix inconsistent timestamp formats in logs for easier troubleshooting - All logging systems now respect Docker TZ environment variable (e.g., TZ=America/New_York) - Add TimezoneAwareFormatter for Python's standard logging module - Enhance _log function with robust timezone support and fallbacks - Replace placeholder logging functions with centralized core.logging._log - Support both zoneinfo (Python 3.9+) and pytz (older versions) - Users now see consistent timezone formatting across all log outputs --- SUMMARY.md | 43 +++++++++++++++++++++++++++++++++++-- clients/external_clients.py | 5 +---- clients/sonarr_client.py | 5 +---- core/logging.py | 24 ++++++++++++++++++++- nfoguard.py | 37 ++++++++++++++++++++++++++++--- 5 files changed, 100 insertions(+), 14 deletions(-) diff --git a/SUMMARY.md b/SUMMARY.md index 620f82c..c5f9f13 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -210,6 +210,45 @@ Preserve movie and TV import dates in Emby/Jellyfin/Plex by preventing upgraded --- -**Last Updated:** September 13, 2025 -**Version:** v0.6.3-dev +### 🆕 v0.6.4 Timezone-Aware Logging Fix (September 2025) + +**🕰️ Universal Timezone Consistency:** +- **Problem**: Mixed timestamp formats in logs made troubleshooting difficult + - Some logs: `[2025-09-14T09:37:00.338045]` (local time without timezone info) + - Other logs: `[2025-09-14T13:37:00+00:00]` (UTC with timezone info) + - Docker `TZ=America/New_York` environment variable not respected by all logging systems + +**⚡ Comprehensive Logging Fix:** +- **Unified Timezone Handling**: All logging systems now respect the `TZ` environment variable +- **Custom TimezoneAwareFormatter**: Python's standard logging now uses container timezone +- **Enhanced _log Function**: Custom logging function updated with robust timezone support +- **Centralized Logging**: Replaced placeholder logging functions with consistent implementation + +**🔧 Technical Implementation:** +- **New Class**: `TimezoneAwareFormatter` for Python's standard logging module +- **Enhanced Function**: `_get_local_timezone()` with fallbacks for different Python versions +- **Cross-Compatibility**: Supports both `zoneinfo` (Python 3.9+) and `pytz` (older versions) +- **Consistent Import**: All client modules now use centralized `core.logging._log` + +**🏆 User Experience Improvements:** +- **Consistent Timestamps**: All logs now show the same timezone format +- **Easier Troubleshooting**: Timestamps match user's local environment +- **No More Confusion**: No need to mentally convert between UTC and local time +- **Production Ready**: Robust fallbacks ensure logging works in all environments + +**📋 Before/After Example:** +``` +# Before (Inconsistent) +sonarr-nfo-cache | [2025-09-14T09:37:00.338045] DEBUG: Mapped Radarr path... +sonarr-nfo-cache | [2025-09-14T13:37:00+00:00] INFO: Batched movie webhook... + +# After (Consistent) +sonarr-nfo-cache | [2025-09-14T09:37:00-04:00] DEBUG: Mapped Radarr path... +sonarr-nfo-cache | [2025-09-14T09:37:00-04:00] INFO: Batched movie webhook... +``` + +--- + +**Last Updated:** September 14, 2025 +**Version:** v0.6.4-dev **Status:** Enhanced Production Ready \ No newline at end of file diff --git a/clients/external_clients.py b/clients/external_clients.py index def823a..cdb1684 100644 --- a/clients/external_clients.py +++ b/clients/external_clients.py @@ -11,10 +11,7 @@ from urllib.parse import urlencode, quote from urllib.request import Request as UrlRequest, urlopen from urllib.error import URLError, HTTPError - -def _log(level: str, msg: str): - """Placeholder logging function - replace with your actual logger""" - print(f"[{datetime.now().isoformat()}] {level}: {msg}") +from core.logging import _log def _get_json(url: str, timeout: int = 20, headers: Dict[str, str] = None) -> Optional[Dict[str, Any]]: diff --git a/clients/sonarr_client.py b/clients/sonarr_client.py index 41d85ba..06fafc9 100644 --- a/clients/sonarr_client.py +++ b/clients/sonarr_client.py @@ -10,10 +10,7 @@ from urllib.parse import urlencode from urllib.request import Request as UrlRequest, urlopen from urllib.error import URLError, HTTPError - -def _log(level: str, msg: str): - """Placeholder logging function - replace with your actual logger""" - print(f"[{datetime.now().isoformat()}] {level}: {msg}") +from core.logging import _log class SonarrClient: diff --git a/core/logging.py b/core/logging.py index 5b1cd04..7b81655 100644 --- a/core/logging.py +++ b/core/logging.py @@ -1,7 +1,29 @@ """Logging utilities for NFOguard""" from datetime import datetime, timezone +import os + +def _get_local_timezone(): + """Get the local timezone, respecting TZ environment variable""" + tz_name = os.environ.get('TZ', 'UTC') + + try: + # Try zoneinfo first (Python 3.9+) + from zoneinfo import ZoneInfo + return ZoneInfo(tz_name) + except ImportError: + # Fallback for older Python versions + try: + import pytz + return pytz.timezone(tz_name) + except: + # Final fallback to UTC + return timezone.utc + except: + # If zone name is invalid, fallback to UTC + return timezone.utc def _log(level: str, msg: str): """Basic logging function that writes to console""" - print(f"[{datetime.now(timezone.utc).isoformat(timespec='seconds')}] {level}: {msg}") \ No newline at end of file + tz = _get_local_timezone() + print(f"[{datetime.now(tz).isoformat(timespec='seconds')}] {level}: {msg}") \ No newline at end of file diff --git a/nfoguard.py b/nfoguard.py index 48d164d..01de621 100644 --- a/nfoguard.py +++ b/nfoguard.py @@ -12,6 +12,7 @@ import logging import logging.handlers from pathlib import Path from datetime import datetime, timezone, timedelta +from zoneinfo import ZoneInfo from fastapi import FastAPI, HTTPException, BackgroundTasks, Request from pydantic import BaseModel from typing import Optional, Dict, Any, List, Set, Tuple @@ -31,6 +32,37 @@ from clients.external_clients import ExternalClientManager # Configuration & Logging # --------------------------- +class TimezoneAwareFormatter(logging.Formatter): + """Formatter that respects the container timezone""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.timezone = self._get_local_timezone() + + def _get_local_timezone(self): + """Get the local timezone, respecting TZ environment variable""" + tz_name = os.environ.get('TZ', 'UTC') + + try: + # Try zoneinfo first (Python 3.9+) + return ZoneInfo(tz_name) + except ImportError: + # Fallback for older Python versions + try: + import pytz + return pytz.timezone(tz_name) + except: + # Final fallback to UTC + return timezone.utc + except: + # If zone name is invalid, fallback to UTC + return timezone.utc + + def formatTime(self, record, datefmt=None): + dt = datetime.fromtimestamp(record.created, tz=self.timezone) + if datefmt: + return dt.strftime(datefmt) + return dt.isoformat(timespec='seconds') + def _setup_file_logging(): """Setup file logging for NFOGuard""" log_dir = Path("/app/data/logs") @@ -43,9 +75,8 @@ def _setup_file_logging(): log_dir / "nfoguard.log", maxBytes=50*1024*1024, backupCount=3 ) - formatter = logging.Formatter( - '[%(asctime)s] %(levelname)s: %(message)s', - datefmt='%Y-%m-%dT%H:%M:%S+00:00' + formatter = TimezoneAwareFormatter( + '[%(asctime)s] %(levelname)s: %(message)s' ) file_handler.setFormatter(formatter) logger.addHandler(file_handler)