added json logger

This commit is contained in:
2024-09-16 22:18:36 +02:00
parent a95a6e1722
commit 8ec9701aa9
30 changed files with 183 additions and 15 deletions

View File

@@ -4,6 +4,8 @@ import os
import argparse
from rich_argparse import RichHelpFormatter, HelpPreviewAction
from modules.logger import logger
if __package__ is None:
PACKAGE = ""
else:
@@ -129,4 +131,5 @@ def parse_arguments(version: str) -> Args:
use_fancy_folders=parsed_args.use_fancy_folders,
web_root_url=parsed_args.web_root_url,
)
logger.debug("parsed arguments", extra={"args": _args.to_dict()})
return _args

View File

@@ -2,6 +2,8 @@ import re
import colorsys
from typing import Dict
from modules.logger import logger
def extract_colorscheme(theme_path: str) -> Dict[str, str]:
"""
@@ -17,6 +19,7 @@ def extract_colorscheme(theme_path: str) -> Dict[str, str]:
Dict[str, str]
Dictionary containing color scheme variables and their hexadecimal values.
"""
logger.info("extracting color scheme from theme file", extra={"theme_path": theme_path})
pattern = r"--(color[1-4]|bcolor1):\s*(#[0-9a-fA-F]+|rgba?\([^)]*\)|hsla?\([^)]*\)|[a-zA-Z]+);"
colorscheme = {}
@@ -30,6 +33,8 @@ def extract_colorscheme(theme_path: str) -> Dict[str, str]:
color_value = match[1]
hex_color_value = css_color_to_hex(color_value)
colorscheme[variable_name] = hex_color_value
logger.debug("extracted variable", extra={"variable": variable_name, "value": hex_color_value})
logger.info("extracted color scheme", extra={"colorscheme": colorscheme})
return colorscheme
@@ -86,10 +91,12 @@ def css_color_to_hex(css_color: str) -> str:
# Helper function to convert RGB tuple to hexadecimal string
def rgb_to_hex(rgb: tuple[int, int, int]) -> str:
logger.debug("converting rgb tuple to hex string", extra={"rgb": rgb})
return "#{:02x}{:02x}{:02x}".format(*rgb)
# Helper function to convert HSL tuple to RGB tuple
def hsl_to_rgb(hsl: tuple[int, float, float]) -> tuple[int, int, int]:
logger.debug("converting hsl tuple to rgb tuple", extra={"hsl": hsl})
return tuple(round(c * 255) for c in colorsys.hls_to_rgb(hsl[0] / 360, hsl[1] / 100, hsl[2] / 100))
# Regular expression pattern to match CSS colors
@@ -103,6 +110,7 @@ def css_color_to_hex(css_color: str) -> str:
match = color_pattern.match(css_color.strip())
if not match:
logger.error("invalid CSS color format", extra={"css_color": css_color})
raise ValueError("Invalid CSS color format")
groups = match.groupdict()
@@ -119,8 +127,10 @@ def css_color_to_hex(css_color: str) -> str:
b = int(groups["b"].rstrip("%")) * 255 // 100 if "%" in groups["b"] else int(groups["b"])
a = float(groups["a"]) if groups["a"] else 1.0
if a < 1.0:
logger.debug("converting rgba color to hex", extra={"color": css_color, "r": r, "g": g, "b": b, "a": a})
return rgb_to_hex((r, g, b)) + "{:02x}".format(round(a * 255))
else:
logger.debug("converting rgb color to hex", extra={"color": css_color, "r": r, "g": g, "b": b})
return rgb_to_hex((r, g, b))
elif groups["hsl"]:
@@ -130,8 +140,10 @@ def css_color_to_hex(css_color: str) -> str:
a = float(groups["a"]) if groups["a"] else 1.0
rgb_color = hsl_to_rgb((h, s, l))
if a < 1.0:
logger.debug("converting hsla color to hex", extra={"color": css_color, "hsl": (h, s, l), "a": a})
return rgb_to_hex(rgb_color) + "{:02x}".format(round(a * 255))
else:
logger.debug("converting hsl color to hex", extra={"color": css_color, "hsl": (h, s, l)})
return rgb_to_hex(rgb_color)
# fmt: off
@@ -182,7 +194,9 @@ def css_color_to_hex(css_color: str) -> str:
'turquoise': '#40e0d0', 'violet': '#ee82ee', 'wheat': '#f5deb3', 'white': '#ffffff',
'whitesmoke': '#f5f5f5', 'yellow': '#ffff00', 'yellowgreen': '#9acd32'
}
logger.debug("parsing css color string", extra={"css_color": css_color})
return named_colors[groups['name'].lower()]
# fmt: on
logger.error("invalid CSS color format", extra={"css_color": css_color})
raise ValueError("Invalid CSS color format")

View File

@@ -9,6 +9,7 @@ from tqdm.auto import tqdm
from PIL import Image, ExifTags, TiffImagePlugin
from jinja2 import Environment, FileSystemLoader
from modules.logger import logger
import modules.cclicense as cclicense
from modules.argumentparser import Args
@@ -45,12 +46,15 @@ def initialize_sizelist(folder: str) -> Dict[str, Dict[str, int]]:
sizelist = {}
sizelist_path = os.path.join(folder, ".sizelist.json")
if not os.path.exists(sizelist_path):
logger.info("creating new size list file", extra={"file": sizelist_path})
with open(sizelist_path, "x", encoding="utf-8") as sizelistfile:
sizelistfile.write("{}")
with open(sizelist_path, "r+", encoding="utf-8") as sizelistfile:
logger.info("reading size list file", extra={"file": sizelist_path})
try:
sizelist = json.loads(sizelistfile.read())
except json.decoder.JSONDecodeError:
logger.warning("invalid JSON in size list file", extra={"file": sizelist_path})
sizelist = {}
return sizelist
@@ -64,11 +68,13 @@ def update_sizelist(sizelist: Dict[str, Dict[str, Any]], folder: str) -> None:
folder (str): The folder in which the size list file is located.
"""
sizelist_path = os.path.join(folder, ".sizelist.json")
if sizelist != {}:
if sizelist:
with open(sizelist_path, "w", encoding="utf-8") as sizelistfile:
logger.info("writing size list file", extra={"file": sizelist_path})
sizelistfile.write(json.dumps(sizelist, indent=4))
else:
if os.path.exists(sizelist_path):
logger.info("deleting empty size list file", extra={"file": sizelist_path})
os.remove(sizelist_path)
@@ -84,11 +90,12 @@ def get_image_info(item: str, folder: str) -> Dict[str, Any]:
Dict[str, Any]: A dictionary containing image width, height, and EXIF data.
"""
with Image.open(os.path.join(folder, item)) as img:
logger.info("extracting image information", extra={"file": item})
exif = img.getexif()
width, height = img.size
if exif:
ifd = exif.get_ifd(ExifTags.IFD.Exif)
exifdatas = {key: val for key, val in exif.items()} | ifd
exifdatas = dict(exif.items()) | ifd
exifdata = {}
for tag_id in exifdatas:
tag = ExifTags.TAGS.get(tag_id, tag_id)
@@ -167,6 +174,10 @@ def generate_html(folder: str, title: str, _args: Args, raw: List[str], version:
_args (Args): Parsed command line arguments.
raw (List[str]): Raw image file names.
"""
if _args.regenerate_thumbnails:
if os.path.exists(os.path.join(folder, ".sizelist.json")):
logger.info("removing .sizelist.json", extra={"folder": folder})
os.remove(os.path.join(folder, ".sizelist.json"))
sizelist = initialize_sizelist(folder)
items = sorted(os.listdir(folder))
@@ -205,6 +216,7 @@ def generate_html(folder: str, title: str, _args: Args, raw: List[str], version:
create_html_file(folder, title, foldername, images, subfolders, _args, version)
else:
if os.path.exists(os.path.join(folder, "index.html")):
logger.info("removing existing index.html", extra={"folder": folder})
os.remove(os.path.join(folder, "index.html"))
if not _args.non_interactive_mode:
@@ -221,6 +233,7 @@ def create_thumbnail_folder(foldername: str, root_directory: str) -> None:
"""
thumbnails_path = os.path.join(root_directory, ".thumbnails", foldername)
if not os.path.exists(thumbnails_path):
logger.info("creating thumbnail folder", extra={"path": thumbnails_path})
os.mkdir(thumbnails_path)
@@ -252,6 +265,7 @@ def process_info_file(folder: str, item: str) -> None:
item (str): The info file name.
"""
with open(os.path.join(folder, item), encoding="utf-8") as f:
logger.info("processing info file", extra={"path": os.path.join(folder, item)})
info[urllib.parse.quote(folder)] = f.read()
@@ -281,6 +295,8 @@ def create_html_file(folder: str, title: str, foldername: str, images: List[Dict
subfolders (List[Dict[str, str]]): A list of subfolders to include in the HTML.
_args (Args): Parsed command line arguments.
"""
html_file = os.path.join(folder, "index.html")
logger.info("generating html file with jinja2", extra={"path": html_file})
image_chunks = np.array_split(images, 8) if images else []
header = os.path.basename(folder) or title
parent = None if not foldername else f"{_args.web_root_url}{urllib.parse.quote(foldername.removesuffix(folder.split('/')[-1] + '/'))}"
@@ -320,7 +336,8 @@ def create_html_file(folder: str, title: str, foldername: str, images: List[Dict
version=version,
)
with open(os.path.join(folder, "index.html"), "w", encoding="utf-8") as f:
with open(html_file, "w", encoding="utf-8") as f:
logger.info("writing html file", extra={"path": html_file})
f.write(content)

96
modules/logger.py Normal file
View File

@@ -0,0 +1,96 @@
"""
loggerdabn.py
This module provides functionality for setting up a centralized logging system using the
`logging` library and the `python-json-logger` to output logs in JSON format. It handles
log rotation by renaming old log files and saving them based on the first timestamp entry.
Functions:
- log_format(keys): Generates the logging format string based on the list of keys.
- rotate_log_file(): Handles renaming the existing log file to a timestamp-based name.
- setup_logger(): Configures the logging system, applies a JSON format, and returns a logger instance.
"""
import logging
import os
import json
from datetime import datetime
from pythonjsonlogger import jsonlogger
# Constants for file paths and exclusions
if __package__ is None:
PACKAGE = ""
else:
PACKAGE = __package__
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
LOG_DIR = os.path.join(SCRIPTDIR, "logs")
LATEST_LOG_FILE = os.path.join(LOG_DIR, "latest.jsonl")
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
def log_format(keys):
"""
Generates a list of format strings based on the given keys.
Args:
keys (list): A list of string keys that represent the log attributes (e.g., 'asctime', 'levelname').
Returns:
list: A list of formatted strings for each key, in the format "%(key)s".
"""
return [f"%({i})s" for i in keys]
def rotate_log_file():
"""
Renames the existing 'latest.jsonl' file to a timestamped file based on the first log entry's asctime.
If 'latest.jsonl' exists, it's renamed to the first timestamp found in the log entry.
"""
if os.path.exists(LATEST_LOG_FILE):
with open(LATEST_LOG_FILE, "r", encoding="utf-8") as f:
first_line = f.readline()
try:
first_log = json.loads(first_line)
first_timestamp = first_log.get("asctime")
first_timestamp = first_timestamp.split(",")[0]
except (json.JSONDecodeError, KeyError):
first_timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
safe_timestamp = first_timestamp.replace(":", "-").replace(" ", "_")
old_log_filename = os.path.join(LOG_DIR, f"{safe_timestamp}.jsonl")
os.rename(LATEST_LOG_FILE, old_log_filename)
def setup_logger():
"""
Configures the logging system with a custom format and outputs logs in JSON format.
The logger will write to the 'logs/latest.jsonl' file, and it will include
multiple attributes such as the time of logging, the filename, function name, log level, etc.
If 'latest.jsonl' already exists, it will be renamed to a timestamped file before creating a new one.
Returns:
logging.Logger: A configured logger instance that can be used to log messages.
"""
rotate_log_file()
app_logger = logging.getLogger()
supported_keys = ["asctime", "created", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "name", "pathname", "process", "processName", "relativeCreated", "thread", "threadName", "taskName"]
custom_format = " ".join(log_format(supported_keys))
formatter = jsonlogger.JsonFormatter(custom_format)
log_handler = logging.FileHandler(LATEST_LOG_FILE)
log_handler.setFormatter(formatter)
app_logger.addHandler(log_handler)
app_logger.setLevel(logging.INFO)
return app_logger
logger = setup_logger()

View File

@@ -13,6 +13,7 @@ try:
except ImportError:
SVGSUPPORT = False
from modules.logger import logger
from modules.argumentparser import Args
from modules.css_color import extract_theme_color, extract_colorscheme
@@ -55,6 +56,7 @@ def render_svg_icon(colorscheme: Dict[str, str], iconspath: str) -> str:
svg = env.get_template("icon.svg.j2")
content = svg.render(colorscheme=colorscheme)
with open(os.path.join(iconspath, "icon.svg"), "w+", encoding="utf-8") as f:
logger.info("writing svg icon", extra={"iconspath": iconspath})
f.write(content)
return content
@@ -73,6 +75,7 @@ def save_png_icon(content: str, iconspath: str) -> None:
tmpimg = BytesIO()
cairosvg.svg2png(bytestring=content, write_to=tmpimg)
with Image.open(tmpimg) as iconfile:
logger.info("saving png icon", extra={"iconspath": iconspath})
iconfile.save(os.path.join(iconspath, "icon.png"))
@@ -87,9 +90,11 @@ def generate_favicon(iconspath: str, root_directory: str) -> None:
root_directory : str
Root directory of the project where the favicon will be saved.
"""
command = f'magick {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {os.path.join(root_directory, ".static", "favicon.ico")}'
favicon = os.path.join(root_directory, ".static", "favicon.ico")
logger.info("generating favicon with imagemagick", extra={"iconspath": iconspath, "favicon": favicon})
command = f'magick {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {favicon}'
if not shutil.which("magick"):
command = f'convert {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {os.path.join(root_directory, ".static", "favicon.ico")}'
command = f'convert {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {favicon}'
os.system(command)
@@ -102,12 +107,14 @@ def icons(_args: Args) -> None:
_args : Args
Parsed command-line arguments.
"""
print("Generating icons...")
iconspath = os.path.join(_args.root_directory, ".static", "icons")
logger.info("generating icons", extra={"iconspath": iconspath})
print("Generating icons...")
colorscheme = extract_colorscheme(_args.theme_path)
content = render_svg_icon(colorscheme, iconspath)
if not SVGSUPPORT:
print("Please install cairosvg to generate favicon from svg icon.")
logger.error("svg support not available")
return
save_png_icon(content, iconspath)
generate_favicon(iconspath, _args.root_directory)
@@ -135,6 +142,7 @@ def render_manifest_json(_args: Args, icon_list: List[Icon], colors: Dict[str, s
theme_color=colors["theme_color"],
)
with open(os.path.join(_args.root_directory, ".static", "manifest.json"), "w", encoding="utf-8") as f:
logger.info("rendering manifest.json", extra={"path": os.path.join(_args.root_directory, ".static", "manifest.json")})
f.write(content)
@@ -156,6 +164,7 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List
List[Icon]
List of icons created from the SVG file.
"""
logger.info("creating icons for web application", extra={"iconspath": iconspath})
svg = [file for file in files if file.endswith(".svg")][0]
icon_list = [
{"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "maskable"},
@@ -165,6 +174,7 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List
tmpimg = BytesIO()
sizes = size.split("x")
iconpath = os.path.join(iconspath, os.path.splitext(svg)[0] + "-" + size + ".png")
logger.info("converting svg to png", extra={"iconpath": iconpath, "size": size})
cairosvg.svg2png(
url=os.path.join(iconspath, svg),
write_to=tmpimg,
@@ -173,6 +183,7 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List
scale=1,
)
with Image.open(tmpimg) as iconfile:
logger.info("saving png file", extra={"iconpath": iconpath})
iconfile.save(iconpath, format="PNG")
icon_list.append(
{
@@ -215,6 +226,7 @@ def create_icons_from_png(iconspath: str, web_root_url: str) -> List[Icon]:
continue
with Image.open(os.path.join(iconspath, icon)) as iconfile:
iconsize = f"{iconfile.size[0]}x{iconfile.size[1]}"
logger.info("using icon", extra={"icon": icon, "size": iconsize})
icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "maskable"})
icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "any"})
return icon_list
@@ -231,14 +243,11 @@ def webmanifest(_args: Args) -> None:
"""
iconspath = os.path.join(_args.root_directory, ".static", "icons")
files = os.listdir(iconspath)
icon_list = (
create_icons_from_svg(files, iconspath, _args)
if SVGSUPPORT and any(file.endswith(".svg") for file in files)
else create_icons_from_png(iconspath, _args.web_root_url)
)
icon_list = create_icons_from_svg(files, iconspath, _args) if SVGSUPPORT and any(file.endswith(".svg") for file in files) else create_icons_from_png(iconspath, _args.web_root_url)
if not icon_list:
print("No icons found in the static/icons folder!")
logger.error("no icons found in the static/icons folder", extra={"iconspath": iconspath})
return
colorscheme = extract_colorscheme(os.path.join(_args.root_directory, ".static", "theme.css"))