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
This commit is contained in:
2025-09-14 09:45:32 -04:00
parent f4c56f6cbe
commit 17ef94ec4f
5 changed files with 100 additions and 14 deletions
+41 -2
View File
@@ -210,6 +210,45 @@ Preserve movie and TV import dates in Emby/Jellyfin/Plex by preventing upgraded
--- ---
**Last Updated:** September 13, 2025 ### 🆕 v0.6.4 Timezone-Aware Logging Fix (September 2025)
**Version:** v0.6.3-dev
**🕰️ 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 **Status:** Enhanced Production Ready
+1 -4
View File
@@ -11,10 +11,7 @@ from urllib.parse import urlencode, quote
from urllib.request import Request as UrlRequest, urlopen from urllib.request import Request as UrlRequest, urlopen
from urllib.error import URLError, HTTPError from urllib.error import URLError, HTTPError
from core.logging import _log
def _log(level: str, msg: str):
"""Placeholder logging function - replace with your actual logger"""
print(f"[{datetime.now().isoformat()}] {level}: {msg}")
def _get_json(url: str, timeout: int = 20, headers: Dict[str, str] = None) -> Optional[Dict[str, Any]]: def _get_json(url: str, timeout: int = 20, headers: Dict[str, str] = None) -> Optional[Dict[str, Any]]:
+1 -4
View File
@@ -10,10 +10,7 @@ from urllib.parse import urlencode
from urllib.request import Request as UrlRequest, urlopen from urllib.request import Request as UrlRequest, urlopen
from urllib.error import URLError, HTTPError from urllib.error import URLError, HTTPError
from core.logging import _log
def _log(level: str, msg: str):
"""Placeholder logging function - replace with your actual logger"""
print(f"[{datetime.now().isoformat()}] {level}: {msg}")
class SonarrClient: class SonarrClient:
+23 -1
View File
@@ -1,7 +1,29 @@
"""Logging utilities for NFOguard""" """Logging utilities for NFOguard"""
from datetime import datetime, timezone 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): def _log(level: str, msg: str):
"""Basic logging function that writes to console""" """Basic logging function that writes to console"""
print(f"[{datetime.now(timezone.utc).isoformat(timespec='seconds')}] {level}: {msg}") tz = _get_local_timezone()
print(f"[{datetime.now(tz).isoformat(timespec='seconds')}] {level}: {msg}")
+34 -3
View File
@@ -12,6 +12,7 @@ import logging
import logging.handlers import logging.handlers
from pathlib import Path from pathlib import Path
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from zoneinfo import ZoneInfo
from fastapi import FastAPI, HTTPException, BackgroundTasks, Request from fastapi import FastAPI, HTTPException, BackgroundTasks, Request
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, Dict, Any, List, Set, Tuple from typing import Optional, Dict, Any, List, Set, Tuple
@@ -31,6 +32,37 @@ from clients.external_clients import ExternalClientManager
# Configuration & Logging # 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(): def _setup_file_logging():
"""Setup file logging for NFOGuard""" """Setup file logging for NFOGuard"""
log_dir = Path("/app/data/logs") log_dir = Path("/app/data/logs")
@@ -43,9 +75,8 @@ def _setup_file_logging():
log_dir / "nfoguard.log", maxBytes=50*1024*1024, backupCount=3 log_dir / "nfoguard.log", maxBytes=50*1024*1024, backupCount=3
) )
formatter = logging.Formatter( formatter = TimezoneAwareFormatter(
'[%(asctime)s] %(levelname)s: %(message)s', '[%(asctime)s] %(levelname)s: %(message)s'
datefmt='%Y-%m-%dT%H:%M:%S+00:00'
) )
file_handler.setFormatter(formatter) file_handler.setFormatter(formatter)
logger.addHandler(file_handler) logger.addHandler(file_handler)