Added log level/filter settings to Logs page

pull/2403/head
JayZed 9 months ago committed by GitHub
parent ad8f116c78
commit d5466fff23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -2,9 +2,10 @@
import io
import os
import re
from flask_restx import Resource, Namespace, fields, marshal
from app.config import settings
from app.logger import empty_log
from app.get_args import args
@ -29,12 +30,62 @@ class SystemLogs(Resource):
def get(self):
"""List log entries"""
logs = []
include = str(settings.log.include_filter)
exclude = str(settings.log.exclude_filter)
ignore_case = settings.log.ignore_case
regex = settings.log.use_regex
if regex:
# pre-compile regular expressions for better performance
if ignore_case:
flags = re.IGNORECASE
else:
flags = 0
if len(include) > 0:
try:
include_compiled = re.compile(include, flags)
except:
include_compiled = None
if len(exclude) > 0:
try:
exclude_compiled = re.compile(exclude, flags)
except:
exclude_compiled = None
elif ignore_case:
include = include.casefold()
exclude = exclude.casefold()
with io.open(os.path.join(args.config_dir, 'log', 'bazarr.log'), encoding='UTF-8') as file:
raw_lines = file.read()
lines = raw_lines.split('|\n')
for line in lines:
if line == '':
continue
if ignore_case and not regex:
compare_line = line.casefold()
else:
compare_line = line
if len(include) > 0:
if regex:
if include_compiled is None:
# if invalid re, keep the line
keep = True
else:
keep = include_compiled.search(compare_line)
else:
keep = include in compare_line
if not keep:
continue
if len(exclude) > 0:
if regex:
if exclude_compiled is None:
# if invalid re, keep the line
skip = False
else:
skip = exclude_compiled.search(compare_line)
else:
skip = exclude in compare_line
if skip:
continue
raw_message = line.split('|')
raw_message_len = len(raw_message)
if raw_message_len > 3:

@ -4,6 +4,7 @@ import hashlib
import os
import ast
import logging
import re
from urllib.parse import quote_plus
from subliminal.cache import region
@ -122,6 +123,12 @@ validators = [
Validator('general.skip_hashing', must_exist=True, default=False, is_type_of=bool),
Validator('general.language_equals', must_exist=True, default=[], is_type_of=list),
# log section
Validator('log.include_filter', must_exist=True, default='', is_type_of=str, cast=str),
Validator('log.exclude_filter', must_exist=True, default='', is_type_of=str, cast=str),
Validator('log.ignore_case', must_exist=True, default=False, is_type_of=bool),
Validator('log.use_regex', must_exist=True, default=False, is_type_of=bool),
# auth section
Validator('auth.apikey', must_exist=True, default=hexlify(os.urandom(16)).decode(), is_type_of=str),
Validator('auth.type', must_exist=True, default=None, is_type_of=(NoneType, str),
@ -430,7 +437,7 @@ array_keys = ['excluded_tags',
empty_values = ['', 'None', 'null', 'undefined', None, []]
str_keys = ['chmod']
str_keys = ['chmod', 'log_include_filter', 'log_exclude_filter']
# Increase Sonarr and Radarr sync interval since we now use SignalR feed to update in real time
if settings.sonarr.series_sync < 15:
@ -477,6 +484,24 @@ def get_settings():
settings_to_return[k].update({subk: subv})
return settings_to_return
def validate_log_regex():
# handle bug in dynaconf that changes strings to numbers, so change them back to str
if not isinstance(settings.log.include_filter, str):
settings.log.include_filter = str(settings.log.include_filter)
if not isinstance(settings.log.exclude_filter, str):
settings.log.exclude_filter = str(settings.log.exclude_filter)
if (settings.log.use_regex):
# compile any regular expressions specified to see if they are valid
# if invalid, tell the user which one
try:
re.compile(settings.log.include_filter)
except:
raise ValidationError(f"Include filter: invalid regular expression: {settings.log.include_filter}")
try:
re.compile(settings.log.exclude_filter)
except:
raise ValidationError(f"Exclude filter: invalid regular expression: {settings.log.exclude_filter}")
def save_settings(settings_items):
configure_debug = False
@ -494,7 +519,8 @@ def save_settings(settings_items):
undefined_subtitles_track_default_changed = False
audio_tracks_parsing_changed = False
reset_providers = False
check_log_regex = False
# Subzero Mods
update_subzero = False
subzero_mods = get_array_from(settings.general.subzero_mods)
@ -630,12 +656,10 @@ def save_settings(settings_items):
if key != settings.opensubtitlescom.username:
reset_providers = True
region.delete('oscom_token')
region.delete('oscom_server')
elif key == 'settings-opensubtitlescom-password':
if key != settings.opensubtitlescom.password:
reset_providers = True
region.delete('oscom_token')
region.delete('oscom_server')
if key == 'settings-subscene-username':
if key != settings.subscene.username:
@ -718,6 +742,7 @@ def save_settings(settings_items):
try:
settings.validators.validate()
validate_log_regex()
except ValidationError:
settings.reload()
raise

@ -1,6 +1,6 @@
import { useIsLoading } from "@/contexts";
import { Group, Pagination, Text } from "@mantine/core";
import { FunctionComponent } from "react";
import { FunctionComponent, useEffect } from "react";
interface Props {
count: number;
index: number;
@ -22,6 +22,11 @@ const PageControl: FunctionComponent<Props> = ({
const isLoading = useIsLoading();
// Jump to first page if total page count changes
useEffect(() => {
goto(0);
}, [total, goto]);
return (
<Group p={16} position="apart">
<Text size="sm">

@ -1,4 +1,5 @@
import { ScrollToTop } from "@/utilities";
import { usePageSize } from "@/utilities/storage";
import { useEffect } from "react";
import { usePagination, useTable } from "react-table";
import BaseTable from "./BaseTable";
@ -22,6 +23,9 @@ export default function PageTable<T extends object>(props: Props<T>) {
...(plugins ?? [])
);
// use page size as specified in UI settings
instance.state.pageSize = usePageSize();
if (instanceRef) {
instanceRef.current = instance;
}

@ -177,6 +177,22 @@ const SettingsGeneralView: FunctionComponent = () => {
<Section header="Logging">
<Check label="Debug" settingKey="settings-general-debug"></Check>
<Message>Debug logging should only be enabled temporarily</Message>
<Text
label="Include Filter"
settingKey="settings-log-include_filter"
></Text>
<Text
label="Exclude Filter"
settingKey="settings-log-exclude_filter"
></Text>
<Check
label="Use Regular Expressions (Regex)"
settingKey="settings-log-use_regex"
></Check>
<Check
label="Ignore Case"
settingKey="settings-log-ignore_case"
></Check>
</Section>
<Section header="Backups">
<File

@ -0,0 +1,99 @@
import { useSettingsMutation, useSystemSettings } from "@/apis/hooks";
import { LoadingProvider } from "@/contexts";
import { useOnValueChange } from "@/utilities";
import { LOG } from "@/utilities/console";
import {
Button,
Container,
Divider,
Group,
LoadingOverlay,
Space,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { FunctionComponent, ReactNode, useCallback, useMemo } from "react";
import { FormContext, FormValues, runHooks } from "../utilities/FormValues";
import { SettingsProvider } from "../utilities/SettingsProvider";
interface Props {
children: ReactNode;
callbackModal: (value: boolean) => void;
}
const LayoutModal: FunctionComponent<Props> = (props) => {
const { children, callbackModal } = props;
const { data: settings, isLoading, isRefetching } = useSystemSettings();
const { mutate, isLoading: isMutating } = useSettingsMutation();
const form = useForm<FormValues>({
initialValues: {
settings: {},
hooks: {},
},
});
useOnValueChange(isRefetching, (value) => {
if (!value) {
form.reset();
}
});
const submit = useCallback(
(values: FormValues) => {
const { settings, hooks } = values;
if (Object.keys(settings).length > 0) {
const settingsToSubmit = { ...settings };
runHooks(hooks, settingsToSubmit);
LOG("info", "submitting settings", settingsToSubmit);
mutate(settingsToSubmit);
// wait for settings to be validated before callback
// let the user see the spinning indicator on the Save button before the modal closes
setTimeout(() => {
callbackModal(true);
}, 500);
}
},
[mutate, callbackModal]
);
const totalStagedCount = useMemo(() => {
return Object.keys(form.values.settings).length;
}, [form.values.settings]);
return (
<SettingsProvider value={settings ?? null}>
<LoadingProvider value={isLoading || isMutating}>
<form onSubmit={form.onSubmit(submit)} style={{ position: "relative" }}>
<LoadingOverlay visible={settings === undefined}></LoadingOverlay>
<FormContext.Provider value={form}>
<Container size="xl" mx={0}>
{children}
</Container>
</FormContext.Provider>
<Space h="md" />
<Divider></Divider>
<Space h="md" />
<Group position="right">
<Button
type="submit"
disabled={totalStagedCount === 0}
loading={isMutating}
>
Save
</Button>
<Button
onClick={() => {
callbackModal(true);
}}
>
Close
</Button>
</Group>
</form>
</LoadingProvider>
</SettingsProvider>
);
};
export default LayoutModal;

@ -65,6 +65,7 @@ export const URLTestButton: FunctionComponent<{
export * from "./Card";
export * from "./Layout";
export { default as Layout } from "./Layout";
export { default as LayoutModal } from "./LayoutModal";
export * from "./Message";
export * from "./Section";
export * from "./collapse";

@ -1,10 +1,17 @@
import { useDeleteLogs, useSystemLogs } from "@/apis/hooks";
import { useDeleteLogs, useSystemLogs, useSystemSettings } from "@/apis/hooks";
import { Toolbox } from "@/components";
import { QueryOverlay } from "@/components/async";
import { Check, LayoutModal, Message, Text } from "@/pages/Settings/components";
import { Environment } from "@/utilities";
import { faDownload, faSync, faTrash } from "@fortawesome/free-solid-svg-icons";
import { Container, Group } from "@mantine/core";
import {
faDownload,
faFilter,
faSync,
faTrash,
} from "@fortawesome/free-solid-svg-icons";
import { Badge, Container, Group, Stack } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
import { useModals } from "@mantine/modals";
import { FunctionComponent, useCallback } from "react";
import Table from "./table";
@ -20,6 +27,61 @@ const SystemLogsView: FunctionComponent = () => {
useDocumentTitle("Logs - Bazarr (System)");
const { data: settings } = useSystemSettings();
const modals = useModals();
const suffix = () => {
const include = settings?.log.include_filter;
const exclude = settings?.log.exclude_filter;
const includeIndex = include !== "" && include !== undefined ? 1 : 0;
const excludeIndex = exclude !== "" && exclude !== undefined ? 1 : 0;
const filters = [
["", "I"],
["E", "I/E"],
];
const filterStr = filters[excludeIndex][includeIndex];
const debugStr = settings?.general.debug ? "Debug" : "";
const spaceStr = debugStr !== "" && filterStr !== "" ? " " : "";
const suffixStr = debugStr + spaceStr + filterStr;
return suffixStr;
};
const openFilterModal = () => {
const callbackModal = (close: boolean) => {
if (close) {
modals.closeModal(id);
}
};
const id = modals.openModal({
title: "Set Log Debug and Filter Options",
children: (
<LayoutModal callbackModal={callbackModal}>
<Stack>
<Check label="Debug" settingKey="settings-general-debug"></Check>
<Message>Debug logging should only be enabled temporarily</Message>
<Text
label="Include Filter"
settingKey="settings-log-include_filter"
></Text>
<Text
label="Exclude Filter"
settingKey="settings-log-exclude_filter"
></Text>
<Check
label="Use Regular Expressions (Regex)"
settingKey="settings-log-use_regex"
></Check>
<Check
label="Ignore Case"
settingKey="settings-log-ignore_case"
></Check>
</Stack>
</LayoutModal>
),
});
};
return (
<Container fluid px={0}>
<QueryOverlay result={logs}>
@ -42,6 +104,22 @@ const SystemLogsView: FunctionComponent = () => {
>
Empty
</Toolbox.Button>
<Toolbox.Button
loading={isLoading}
icon={faFilter}
onClick={openFilterModal}
rightIcon={
suffix() !== "" ? (
<Badge size="xs" radius="sm">
{suffix()}
</Badge>
) : (
<></>
)
}
>
Filter
</Toolbox.Button>
</Group>
</Toolbox>
<Table logs={data ?? []}></Table>

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-empty-interface */
interface Settings {
general: Settings.General;
log: Settings.Log;
proxy: Settings.Proxy;
auth: Settings.Auth;
subsync: Settings.Subsync;
@ -82,6 +83,13 @@ declare namespace Settings {
wanted_search_frequency_movie: number;
}
interface Log {
include_filter: string;
exclude_filter: string;
ignore_case: boolean;
use_regex: boolean;
}
interface Proxy {
exclude: string[];
type?: string;

Loading…
Cancel
Save