1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-07-19 13:19:41 +02:00

rewrite logger to support custom config files (#3104)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Docker Nightly Production / Backend Server Tests (push) Waiting to run
Docker Nightly Production / Frontend and End-to-End Tests (push) Waiting to run
Docker Nightly Production / Build Tagged Release (push) Blocked by required conditions
Docker Nightly Production / Notify Discord (push) Blocked by required conditions

This commit is contained in:
Hayden 2024-04-16 10:52:49 -05:00 committed by GitHub
parent cba076b6a4
commit 6bd5a82b92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 285 additions and 294 deletions

View file

@ -14,6 +14,7 @@ services:
- 9091:9000 - 9091:9000
environment: environment:
ALLOW_SIGNUP: "false" ALLOW_SIGNUP: "false"
LOG_LEVEL: "DEBUG"
DB_ENGINE: sqlite # Optional: 'sqlite', 'postgres' DB_ENGINE: sqlite # Optional: 'sqlite', 'postgres'
# ===================================== # =====================================

View file

@ -40,10 +40,11 @@ init
GUNICORN_PORT=${API_PORT:-9000} GUNICORN_PORT=${API_PORT:-9000}
# Start API # Start API
hostip=`/sbin/ip route|awk '/default/ { print $3 }'` HOST_IP=`/sbin/ip route|awk '/default/ { print $3 }'`
if [ "$WEB_GUNICORN" = 'true' ]; then if [ "$WEB_GUNICORN" = 'true' ]; then
echo "Starting Gunicorn" echo "Starting Gunicorn"
exec gunicorn mealie.app:app -b 0.0.0.0:$GUNICORN_PORT --forwarded-allow-ips=$hostip -k uvicorn.workers.UvicornWorker -c /app/gunicorn_conf.py --preload exec gunicorn mealie.app:app -b 0.0.0.0:$GUNICORN_PORT --forwarded-allow-ips=$HOST_IP -k uvicorn.workers.UvicornWorker -c /app/gunicorn_conf.py --preload
else else
exec uvicorn mealie.app:app --host 0.0.0.0 --forwarded-allow-ips=$hostip --port $GUNICORN_PORT exec python /app/mealie/main.py
fi fi

View file

@ -15,6 +15,8 @@
| API_DOCS | True | Turns on/off access to the API documentation locally. | | API_DOCS | True | Turns on/off access to the API documentation locally. |
| TZ | UTC | Must be set to get correct date/time on the server | | TZ | UTC | Must be set to get correct date/time on the server |
| ALLOW_SIGNUP<super>\*</super> | false | Allow user sign-up without token | | ALLOW_SIGNUP<super>\*</super> | false | Allow user sign-up without token |
| LOG_CONFIG_OVERRIDE | | Override the config for logging with a custom path |
| LOG_LEVEL | info | logging level configured |
<super>\*</super> Starting in v1.4.0 this was changed to default to `false` as apart of a security review of the application. <super>\*</super> Starting in v1.4.0 this was changed to default to `false` as apart of a security review of the application.
@ -27,15 +29,15 @@
### Database ### Database
| Variables | Default | Description | | Variables | Default | Description |
| ----------------- | :------: | -------------------------------- | | --------------------- | :------: | ----------------------------------------------------------------------- |
| DB_ENGINE | sqlite | Optional: 'sqlite', 'postgres' | | DB_ENGINE | sqlite | Optional: 'sqlite', 'postgres' |
| POSTGRES_USER | mealie | Postgres database user | | POSTGRES_USER | mealie | Postgres database user |
| POSTGRES_PASSWORD | mealie | Postgres database password | | POSTGRES_PASSWORD | mealie | Postgres database password |
| POSTGRES_SERVER | postgres | Postgres database server address | | POSTGRES_SERVER | postgres | Postgres database server address |
| POSTGRES_PORT | 5432 | Postgres database port | | POSTGRES_PORT | 5432 | Postgres database port |
| POSTGRES_DB | mealie | Postgres database name | | POSTGRES_DB | mealie | Postgres database name |
| POSTGRES_URL_OVERRIDE | None | Optional Postgres URL override to use instead of POSTGRES_* variables | | POSTGRES_URL_OVERRIDE | None | Optional Postgres URL override to use instead of POSTGRES\_\* variables |
### Email ### Email
@ -96,7 +98,7 @@ For usage, see [Usage - OpenID Connect](../authentication/oidc.md)
| OIDC_PROVIDER_NAME | OAuth | The provider name is shown in SSO login button. "Login with <OIDC_PROVIDER_NAME\>" | | OIDC_PROVIDER_NAME | OAuth | The provider name is shown in SSO login button. "Login with <OIDC_PROVIDER_NAME\>" |
| OIDC_REMEMBER_ME | False | Because redirects bypass the login screen, you cant extend your session by clicking the "Remember Me" checkbox. By setting this value to true, a session will be extended as if "Remember Me" was checked | | OIDC_REMEMBER_ME | False | Because redirects bypass the login screen, you cant extend your session by clicking the "Remember Me" checkbox. By setting this value to true, a session will be extended as if "Remember Me" was checked |
| OIDC_SIGNING_ALGORITHM | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) | | OIDC_SIGNING_ALGORITHM | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
| OIDC_USER_CLAIM | email | Optional: 'email', 'preferred_username' | OIDC_USER_CLAIM | email | Optional: 'email', 'preferred_username' |
### Themeing ### Themeing

View file

@ -0,0 +1,16 @@
# Logs
:octicons-tag-24: v1.5.0
## Highlighs
- Logs are written to `/app/data/mealie.log` by default in the container.
- Logs are also written to stdout and stderr.
- You can adjust the log level using the `LOG_LEVEL` environment variable.
## Configuration
Starting in v1.5.0 logging is now highly configurable. Using the `LOG_CONFIG_OVERRIDE` you can provide the application with a custom configuration to log however you'd like. This configuration file is based off the [Python Logging Config](https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig). It can be difficult to understand the configuration at first, so here are some resources to help get started.
- This [YouTube Video](https://www.youtube.com/watch?v=9L77QExPmI0) for a great walkthrough on the logging file format.
- Our [Logging Config](https://github.com/mealie-recipes/mealie/blob/mealie-next/mealie/core/logger/logconf.prod.json)

View file

@ -73,6 +73,7 @@ nav:
- PostgreSQL: "documentation/getting-started/installation/postgres.md" - PostgreSQL: "documentation/getting-started/installation/postgres.md"
- Backend Configuration: "documentation/getting-started/installation/backend-config.md" - Backend Configuration: "documentation/getting-started/installation/backend-config.md"
- Security: "documentation/getting-started/installation/security.md" - Security: "documentation/getting-started/installation/security.md"
- Logs: "documentation/getting-started/installation/logs.md"
- Usage: - Usage:
- Backup and Restoring: "documentation/getting-started/usage/backups-and-restoring.md" - Backup and Restoring: "documentation/getting-started/usage/backups-and-restoring.md"
- Permissions and Public Access: "documentation/getting-started/usage/permissions-and-public-access.md" - Permissions and Public Access: "documentation/getting-started/usage/permissions-and-public-access.md"

View file

@ -203,7 +203,6 @@ export interface MaintenanceStorageDetails {
} }
export interface MaintenanceSummary { export interface MaintenanceSummary {
dataDirSize: string; dataDirSize: string;
logFileSize: string;
cleanableImages: number; cleanableImages: number;
cleanableDirs: number; cleanableDirs: number;
} }

View file

@ -22,10 +22,6 @@
<template #title> {{ $t("admin.maintenance.page-title") }} </template> <template #title> {{ $t("admin.maintenance.page-title") }} </template>
</BasePageTitle> </BasePageTitle>
<div class="d-flex justify-end">
<ButtonLink to="/admin/maintenance/logs" text="Logs" :icon="$globals.icons.file" />
</div>
<section> <section>
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.wrench" :title="$tc('admin.maintenance.summary-title')"> <BaseCardSectionTitle class="pb-0" :icon="$globals.icons.wrench" :title="$tc('admin.maintenance.summary-title')">
</BaseCardSectionTitle> </BaseCardSectionTitle>
@ -110,7 +106,6 @@ export default defineComponent({
const infoResults = ref<MaintenanceSummary>({ const infoResults = ref<MaintenanceSummary>({
dataDirSize: i18n.tc("about.unknown-version"), dataDirSize: i18n.tc("about.unknown-version"),
logFileSize: i18n.tc("about.unknown-version"),
cleanableDirs: 0, cleanableDirs: 0,
cleanableImages: 0, cleanableImages: 0,
}); });
@ -121,7 +116,6 @@ export default defineComponent({
infoResults.value = data ?? { infoResults.value = data ?? {
dataDirSize: i18n.tc("about.unknown-version"), dataDirSize: i18n.tc("about.unknown-version"),
logFileSize: i18n.tc("about.unknown-version"),
cleanableDirs: 0, cleanableDirs: 0,
cleanableImages: 0, cleanableImages: 0,
}; };
@ -129,17 +123,12 @@ export default defineComponent({
state.fetchingInfo = false; state.fetchingInfo = false;
} }
const info = computed(() => { const info = computed(() => {
return [ return [
{ {
name: i18n.t("admin.maintenance.info-description-data-dir-size"), name: i18n.t("admin.maintenance.info-description-data-dir-size"),
value: infoResults.value.dataDirSize, value: infoResults.value.dataDirSize,
}, },
{
name: i18n.t("admin.maintenance.info-description-log-file-size"),
value: infoResults.value.logFileSize,
},
{ {
name: i18n.t("admin.maintenance.info-description-cleanable-directories"), name: i18n.t("admin.maintenance.info-description-cleanable-directories"),
value: infoResults.value.cleanableDirs, value: infoResults.value.cleanableDirs,
@ -184,12 +173,6 @@ export default defineComponent({
// ========================================================================== // ==========================================================================
// Actions // Actions
async function handleDeleteLogFile() {
state.actionLoading = true;
await adminApi.maintenance.cleanLogFile();
state.actionLoading = false;
}
async function handleCleanDirectories() { async function handleCleanDirectories() {
state.actionLoading = true; state.actionLoading = true;
await adminApi.maintenance.cleanRecipeFolders(); await adminApi.maintenance.cleanRecipeFolders();
@ -209,11 +192,6 @@ export default defineComponent({
} }
const actions = [ const actions = [
{
name: i18n.t("admin.maintenance.action-delete-log-files-name"),
handler: handleDeleteLogFile,
subtitle: i18n.t("admin.maintenance.action-delete-log-files-description"),
},
{ {
name: i18n.t("admin.maintenance.action-clean-directories-name"), name: i18n.t("admin.maintenance.action-clean-directories-name"),
handler: handleCleanDirectories, handler: handleCleanDirectories,

View file

@ -1,109 +0,0 @@
<template>
<v-container fluid>
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.cog" :title="$t('admin.maintenance.summary-title')">
</BaseCardSectionTitle>
<div class="mb-6 ml-2 d-flex" style="gap: 0.8rem">
<BaseButton color="info" :loading="state.loading" @click="refreshLogs">
<template #icon> {{ $globals.icons.refreshCircle }} </template>
{{ $t("admin.maintenance.logs-action-refresh") }}
</BaseButton>
<AppButtonCopy :copy-text="copyText" />
<div class="ml-auto" style="max-width: 150px">
<v-text-field
v-model="state.lines"
type="number"
:label="$t('admin.maintenance.logs-tail-lines-label')"
hide-details
dense
outlined
>
</v-text-field>
</div>
</div>
<v-card outlined>
<v-virtual-scroll
v-scroll="scrollOptions"
:bench="20"
:items="logs.logs"
height="800"
item-height="20"
class="keep-whitespace log-container"
>
<template #default="{ item }">
<p class="log-text">
{{ item }}
</p>
</template>
</v-virtual-scroll>
</v-card>
</v-container>
</template>
<script lang="ts">
import { defineComponent, ref, computed, onMounted, reactive } from "@nuxtjs/composition-api";
import { useAdminApi } from "~/composables/api";
export default defineComponent({
layout: "admin",
setup() {
const adminApi = useAdminApi();
const state = reactive({
loading: false,
lines: 500,
autoRefresh: true,
});
const scrollOptions = reactive({
enable: true,
always: false,
smooth: false,
notSmoothOnInit: true,
});
const logs = ref({
logs: [] as string[],
});
async function refreshLogs() {
state.loading = true;
const { data } = await adminApi.maintenance.logs(state.lines);
if (data) {
logs.value = data;
}
state.loading = false;
}
onMounted(() => {
refreshLogs();
});
const copyText = computed(() => {
return logs.value.logs.join("") || "";
});
return {
copyText,
scrollOptions,
state,
refreshLogs,
logs,
};
},
head() {
return {
title: this.$t("admin.maintenance.logs-page-title") as string,
};
},
});
</script>
<style>
.log-text {
font: 0.8rem Inconsolata, monospace;
}
.log-container {
background-color: var(--v-background-base) !important;
}
.keep-whitespace {
white-space: pre;
}
</style>

View file

@ -0,0 +1,67 @@
import json
import logging
import pathlib
import typing
from logging import config as logging_config
__dir = pathlib.Path(__file__).parent
__conf: dict[str, str] | None = None
def _load_config(path: pathlib.Path, substitutions: dict[str, str] | None = None) -> dict[str, typing.Any]:
with open(path) as file:
if substitutions:
contents = file.read()
for key, value in substitutions.items():
# Replaces the key matches
#
# Example:
# {"key": "value"}
# "/path/to/${key}/file" -> "/path/to/value/file"
contents = contents.replace(f"${{{key}}}", value)
json_data = json.loads(contents)
else:
json_data = json.load(file)
return json_data
def log_config() -> dict[str, str]:
if __conf is None:
raise ValueError("logger not configured, must call configured_logger first")
return __conf
def configured_logger(
*,
mode: str,
config_override: pathlib.Path | None = None,
substitutions: dict[str, str] | None = None,
) -> logging.Logger:
"""
Configure the logger based on the mode and return the root logger
Args:
mode (str): The mode to configure the logger for (production, development, testing)
config_override (pathlib.Path, optional): A path to a custom logging config. Defaults to None.
substitutions (dict[str, str], optional): A dictionary of substitutions to apply to the logging config.
"""
global __conf
if config_override:
__conf = _load_config(config_override, substitutions)
else:
if mode == "production":
__conf = _load_config(__dir / "logconf.prod.json", substitutions)
elif mode == "development":
__conf = _load_config(__dir / "logconf.dev.json", substitutions)
elif mode == "testing":
__conf = _load_config(__dir / "logconf.test.json", substitutions)
else:
raise ValueError(f"Invalid mode: {mode}")
logging_config.dictConfig(config=__conf)
return logging.getLogger()

View file

@ -0,0 +1,17 @@
{
"version": 1,
"disable_existing_loggers": false,
"handlers": {
"rich": {
"class": "rich.logging.RichHandler"
}
},
"loggers": {
"root": {
"level": "DEBUG",
"handlers": [
"rich"
]
}
}
}

View file

@ -0,0 +1,74 @@
{
"version": 1,
"disable_existing_loggers": false,
"formatters": {
"simple": {
"format": "%(levelname)-8s %(asctime)s - %(message)s",
"datefmt": "%Y-%m-%dT%H:%M:%S"
},
"detailed": {
"format": "[%(levelname)s|%(module)s|L%(lineno)d] %(asctime)s: %(message)s",
"datefmt": "%Y-%m-%dT%H:%M:%S"
},
"access": {
"()": "uvicorn.logging.AccessFormatter",
"fmt": "%(levelname)-8s %(asctime)s - [%(client_addr)s] %(status_code)s \"%(request_line)s\"",
"datefmt": "%Y-%m-%dT%H:%M:%S"
}
},
"handlers": {
"stderr": {
"class": "logging.StreamHandler",
"level": "WARNING",
"formatter": "simple",
"stream": "ext://sys.stderr"
},
"stdout": {
"class": "logging.StreamHandler",
"level": "${LOG_LEVEL}",
"formatter": "simple",
"stream": "ext://sys.stdout"
},
"access": {
"class": "logging.StreamHandler",
"level": "${LOG_LEVEL}",
"formatter": "access",
"stream": "ext://sys.stdout"
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "DEBUG",
"formatter": "detailed",
"filename": "${DATA_DIR}/mealie.log",
"maxBytes": 10000,
"backupCount": 3
}
},
"loggers": {
"root": {
"level": "${LOG_LEVEL}",
"handlers": [
"stderr",
"file",
"stdout"
]
},
"uvicorn.error": {
"handlers": [
"stderr",
"file",
"stdout"
],
"level": "${LOG_LEVEL}",
"propagate": false
},
"uvicorn.access": {
"handlers": [
"access",
"file"
],
"level": "${LOG_LEVEL}",
"propagate": false
}
}
}

View file

@ -0,0 +1,26 @@
{
"version": 1,
"disable_existing_loggers": false,
"formatters": {
"detailed": {
"format": "[%(levelname)s|%(module)s|L%(lineno)d] %(asctime)s: %(message)s",
"datefmt": "%Y-%m-%dT%H:%M:%S"
}
},
"handlers": {
"stdout": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"formatter": "detailed",
"stream": "ext://sys.stdout"
}
},
"loggers": {
"root": {
"level": "${LOG_LEVEL}",
"handlers": [
"stdout"
]
}
}
}

View file

@ -1,85 +1,46 @@
import logging import logging
import sys
from dataclasses import dataclass
from functools import lru_cache
from mealie.core.config import determine_data_dir from .config import get_app_dirs, get_app_settings
from .logger.config import configured_logger
DATA_DIR = determine_data_dir() __root_logger: None | logging.Logger = None
from .config import get_app_settings # noqa E402
LOGGER_FILE = DATA_DIR.joinpath("mealie.log")
DATE_FORMAT = "%d-%b-%y %H:%M:%S"
LOGGER_FORMAT = "%(levelname)s: %(asctime)s \t%(message)s"
@dataclass
class LoggerConfig:
handlers: list
format: str
date_format: str
logger_file: str
level: int = logging.INFO
@lru_cache
def get_logger_config():
settings = get_app_settings()
log_level = logging._nameToLevel[settings.LOG_LEVEL]
if not settings.PRODUCTION:
from rich.logging import RichHandler
return LoggerConfig(
handlers=[RichHandler(rich_tracebacks=True, tracebacks_show_locals=True)],
format=None,
date_format=None,
logger_file=None,
level=log_level,
)
output_file_handler = logging.FileHandler(LOGGER_FILE)
handler_format = logging.Formatter(LOGGER_FORMAT, datefmt=DATE_FORMAT)
output_file_handler.setFormatter(handler_format)
# Stdout
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(handler_format)
return LoggerConfig(
handlers=[output_file_handler, stdout_handler],
format="%(levelname)s: %(asctime)s \t%(message)s",
date_format="%d-%b-%y %H:%M:%S",
logger_file=LOGGER_FILE,
level=log_level,
)
logger_config = get_logger_config()
logging.basicConfig(
level=logger_config.level,
format=logger_config.format,
datefmt=logger_config.date_format,
handlers=logger_config.handlers,
)
def logger_init() -> logging.Logger:
"""Returns the Root Logging Object for Mealie"""
return logging.getLogger("mealie")
root_logger = logger_init()
def get_logger(module=None) -> logging.Logger: def get_logger(module=None) -> logging.Logger:
"""Returns a child logger for mealie""" """
global root_logger Get a logger instance for a module, in most cases module should not be
provided. Simply using the root logger is sufficient.
Cases where you would want to use a module specific logger might be a background
task or a long running process where you want to easily identify the source of
those messages
"""
global __root_logger
if __root_logger is None:
app_settings = get_app_settings()
mode = "development"
if app_settings.TESTING:
mode = "testing"
elif app_settings.PRODUCTION:
mode = "production"
dirs = get_app_dirs()
substitutions = {
"DATA_DIR": dirs.DATA_DIR.as_posix(),
"LOG_LEVEL": app_settings.LOG_LEVEL.upper(),
}
__root_logger = configured_logger(
mode=mode,
config_override=app_settings.LOG_CONFIG_OVERRIDE,
substitutions=substitutions,
)
if module is None: if module is None:
return root_logger return __root_logger
return root_logger.getChild(module) return __root_logger.getChild(module)

View file

@ -36,13 +36,21 @@ class AppSettings(BaseSettings):
"""path to static files directory (ex. `mealie/dist`)""" """path to static files directory (ex. `mealie/dist`)"""
IS_DEMO: bool = False IS_DEMO: bool = False
HOST_IP: str = "*"
API_HOST: str = "0.0.0.0"
API_PORT: int = 9000 API_PORT: int = 9000
API_DOCS: bool = True API_DOCS: bool = True
TOKEN_TIME: int = 48 TOKEN_TIME: int = 48
"""time in hours""" """time in hours"""
SECRET: str SECRET: str
LOG_LEVEL: str = "INFO"
LOG_CONFIG_OVERRIDE: Path | None = None
"""path to custom logging configuration file"""
LOG_LEVEL: str = "info"
"""corresponds to standard Python log levels""" """corresponds to standard Python log levels"""
GIT_COMMIT_HASH: str = "unknown" GIT_COMMIT_HASH: str = "unknown"

20
mealie/main.py Normal file
View file

@ -0,0 +1,20 @@
import uvicorn
from mealie.app import settings
from mealie.core.logger.config import log_config
def main():
uvicorn.run(
"app:app",
host=settings.API_HOST,
port=settings.API_PORT,
log_level=settings.LOG_LEVEL.lower(),
log_config=log_config(),
workers=1,
forwarded_allow_ips=settings.HOST_IP,
)
if __name__ == "__main__":
main()

View file

@ -5,7 +5,6 @@ from . import (
admin_analytics, admin_analytics,
admin_backups, admin_backups,
admin_email, admin_email,
admin_log,
admin_maintenance, admin_maintenance,
admin_management_groups, admin_management_groups,
admin_management_users, admin_management_users,
@ -15,7 +14,6 @@ from . import (
router = AdminAPIRouter(prefix="/admin") router = AdminAPIRouter(prefix="/admin")
router.include_router(admin_about.router, tags=["Admin: About"]) router.include_router(admin_about.router, tags=["Admin: About"])
router.include_router(admin_log.router, tags=["Admin: Log"])
router.include_router(admin_management_users.router, tags=["Admin: Manage Users"]) router.include_router(admin_management_users.router, tags=["Admin: Manage Users"])
router.include_router(admin_management_groups.router, tags=["Admin: Manage Groups"]) router.include_router(admin_management_groups.router, tags=["Admin: Manage Groups"])
router.include_router(admin_email.router, tags=["Admin: Email"]) router.include_router(admin_email.router, tags=["Admin: Email"])

View file

@ -1,44 +0,0 @@
from fastapi import APIRouter
from mealie.core.root_logger import LOGGER_FILE
from mealie.core.security import create_file_token
router = APIRouter(prefix="/logs")
@router.get("/{num}")
async def get_log(num: int):
"""Doc Str"""
with open(LOGGER_FILE, "rb") as f:
log_text = tail(f, num)
return log_text
@router.get("")
async def get_log_file():
"""Returns a token to download a file"""
return {"fileToken": create_file_token(LOGGER_FILE)}
def tail(f, lines=20):
total_lines_wanted = lines
BLOCK_SIZE = 1024
f.seek(0, 2)
block_end_byte = f.tell()
lines_to_go = total_lines_wanted
block_number = -1
blocks = []
while lines_to_go > 0 and block_end_byte > 0:
if block_end_byte - BLOCK_SIZE > 0:
f.seek(block_number * BLOCK_SIZE, 2)
blocks.append(f.read(BLOCK_SIZE))
else:
f.seek(0, 0)
blocks.append(f.read(block_end_byte))
lines_found = blocks[-1].count(b"\n")
lines_to_go -= lines_found
block_end_byte -= BLOCK_SIZE
block_number -= 1
all_read_text = b"".join(reversed(blocks))
return b"/n".join(all_read_text.splitlines()[-total_lines_wanted:])

View file

@ -1,16 +1,13 @@
import contextlib
import os
import shutil import shutil
import uuid import uuid
from pathlib import Path from pathlib import Path
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from mealie.core.root_logger import LOGGER_FILE
from mealie.pkgs.stats import fs_stats from mealie.pkgs.stats import fs_stats
from mealie.routes._base import BaseAdminController, controller from mealie.routes._base import BaseAdminController, controller
from mealie.schema.admin import MaintenanceSummary from mealie.schema.admin import MaintenanceSummary
from mealie.schema.admin.maintenance import MaintenanceLogs, MaintenanceStorageDetails from mealie.schema.admin.maintenance import MaintenanceStorageDetails
from mealie.schema.response import ErrorResponse, SuccessResponse from mealie.schema.response import ErrorResponse, SuccessResponse
router = APIRouter(prefix="/maintenance") router = APIRouter(prefix="/maintenance")
@ -72,21 +69,13 @@ class AdminMaintenanceController(BaseAdminController):
""" """
Get the maintenance summary Get the maintenance summary
""" """
log_file_size = 0
with contextlib.suppress(FileNotFoundError):
log_file_size = os.path.getsize(LOGGER_FILE)
return MaintenanceSummary( return MaintenanceSummary(
data_dir_size=fs_stats.pretty_size(fs_stats.get_dir_size(self.folders.DATA_DIR)), data_dir_size=fs_stats.pretty_size(fs_stats.get_dir_size(self.folders.DATA_DIR)),
log_file_size=fs_stats.pretty_size(log_file_size),
cleanable_images=clean_images(self.folders.RECIPE_DATA_DIR, dry_run=True), cleanable_images=clean_images(self.folders.RECIPE_DATA_DIR, dry_run=True),
cleanable_dirs=clean_recipe_folders(self.folders.RECIPE_DATA_DIR, dry_run=True), cleanable_dirs=clean_recipe_folders(self.folders.RECIPE_DATA_DIR, dry_run=True),
) )
@router.get("/logs", response_model=MaintenanceLogs)
def get_logs(self, lines: int = 200):
return MaintenanceLogs(logs=tail_log(LOGGER_FILE, lines))
@router.get("/storage", response_model=MaintenanceStorageDetails) @router.get("/storage", response_model=MaintenanceStorageDetails)
def get_storage_details(self): def get_storage_details(self):
return MaintenanceStorageDetails( return MaintenanceStorageDetails(
@ -130,16 +119,3 @@ class AdminMaintenanceController(BaseAdminController):
return SuccessResponse.respond(f"{cleaned_dirs} Recipe folders removed") return SuccessResponse.respond(f"{cleaned_dirs} Recipe folders removed")
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=ErrorResponse.respond("Failed to clean directories")) from e raise HTTPException(status_code=500, detail=ErrorResponse.respond("Failed to clean directories")) from e
@router.post("/clean/logs", response_model=SuccessResponse)
def clean_logs(self):
"""
Purges the logs
"""
try:
with contextlib.suppress(FileNotFoundError):
os.remove(LOGGER_FILE)
LOGGER_FILE.touch()
return SuccessResponse.respond("Logs cleaned")
except Exception as e:
raise HTTPException(status_code=500, detail=ErrorResponse.respond("Failed to clean logs")) from e

View file

@ -3,7 +3,6 @@ from mealie.schema._mealie import MealieModel
class MaintenanceSummary(MealieModel): class MaintenanceSummary(MealieModel):
data_dir_size: str data_dir_size: str
log_file_size: str
cleanable_images: int cleanable_images: int
cleanable_dirs: int cleanable_dirs: int