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:
+41
-2
@@ -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
|
||||||
@@ -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]]:
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user