Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NextcloudApp: setup_nextcloud_logging function for transparent logging #294

Merged
merged 4 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ All notable changes to this project will be documented in this file.

## [0.17.1 - 2024-09-06]

### Added

- NextcloudApp: `setup_nextcloud_logging` function to support transparently sending logs to Nextcloud. #294

### Fixed

- NextcloudApp: `nc.log` now suppresses all exceptions to safe call it anywhere in your app.
- NextcloudApp: `nc.log` now suppresses all exceptions to safe call it anywhere in your app. #293

## [0.17.0 - 2024-09-05]

Expand Down
1 change: 1 addition & 0 deletions nc_py_api/ex_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
set_handlers,
talk_bot_msg,
)
from .logging import setup_nextcloud_logging
from .misc import (
get_computation_device,
get_model_path,
Expand Down
46 changes: 46 additions & 0 deletions nc_py_api/ex_app/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Transparent logging support to store logs in the nextcloud.log."""

import logging
import threading

from ..nextcloud import NextcloudApp
from .defs import LogLvl

LOGLVL_MAP = {
logging.NOTSET: LogLvl.DEBUG,
logging.DEBUG: LogLvl.DEBUG,
logging.INFO: LogLvl.INFO,
logging.WARNING: LogLvl.WARNING,
logging.ERROR: LogLvl.ERROR,
logging.CRITICAL: LogLvl.FATAL,
}

THREAD_LOCAL = threading.local()


class _NextcloudLogsHandler(logging.Handler):
def __init__(self):
super().__init__()

def emit(self, record):
if THREAD_LOCAL.__dict__.get("nc_py_api.loghandler", False):
return

try:
THREAD_LOCAL.__dict__["nc_py_api.loghandler"] = True
log_entry = self.format(record)
log_level = record.levelno
NextcloudApp().log(LOGLVL_MAP.get(log_level, LogLvl.FATAL), log_entry, fast_send=True)
except Exception: # noqa pylint: disable=broad-exception-caught
self.handleError(record)
finally:
THREAD_LOCAL.__dict__["nc_py_api.loghandler"] = False


def setup_nextcloud_logging(logger_name: str | None = None, logging_level: int = logging.DEBUG):
"""Function to easily send all or selected log entries to Nextcloud."""
logger = logging.getLogger(logger_name)
nextcloud_handler = _NextcloudLogsHandler()
nextcloud_handler.setLevel(logging_level)
logger.addHandler(nextcloud_handler)
return nextcloud_handler
22 changes: 12 additions & 10 deletions nc_py_api/nextcloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,15 +348,16 @@ def enabled_state(self) -> bool:
return bool(self._session.ocs("GET", "/ocs/v1.php/apps/app_api/ex-app/state"))
return False

def log(self, log_lvl: LogLvl, content: str) -> None:
def log(self, log_lvl: LogLvl, content: str, fast_send: bool = False) -> None:
"""Writes log to the Nextcloud log file."""
if self.check_capabilities("app_api"):
return
int_log_lvl = int(log_lvl)
if int_log_lvl < 0 or int_log_lvl > 4:
raise ValueError("Invalid `log_lvl` value")
if int_log_lvl < self.capabilities["app_api"].get("loglevel", 0):
return
if not fast_send:
if self.check_capabilities("app_api"):
return
if int_log_lvl < self.capabilities["app_api"].get("loglevel", 0):
return
with contextlib.suppress(Exception):
self._session.ocs("POST", f"{self._session.ae_url}/log", json={"level": int_log_lvl, "message": content})

Expand Down Expand Up @@ -482,15 +483,16 @@ async def enabled_state(self) -> bool:
return bool(await self._session.ocs("GET", "/ocs/v1.php/apps/app_api/ex-app/state"))
return False

async def log(self, log_lvl: LogLvl, content: str) -> None:
async def log(self, log_lvl: LogLvl, content: str, fast_send: bool = False) -> None:
"""Writes log to the Nextcloud log file."""
if await self.check_capabilities("app_api"):
return
int_log_lvl = int(log_lvl)
if int_log_lvl < 0 or int_log_lvl > 4:
raise ValueError("Invalid `log_lvl` value")
if int_log_lvl < (await self.capabilities)["app_api"].get("loglevel", 0):
return
if not fast_send:
if await self.check_capabilities("app_api"):
return
if int_log_lvl < (await self.capabilities)["app_api"].get("loglevel", 0):
return
with contextlib.suppress(Exception):
await self._session.ocs(
"POST", f"{self._session.ae_url}/log", json={"level": int_log_lvl, "message": content}
Expand Down
23 changes: 22 additions & 1 deletion tests/actual_tests/logs_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import logging
from copy import deepcopy
from unittest import mock

import pytest

from nc_py_api.ex_app import LogLvl
from nc_py_api.ex_app import LogLvl, setup_nextcloud_logging


def test_loglvl_values():
Expand Down Expand Up @@ -113,3 +114,23 @@ async def test_log_without_app_api_async(anc_app):
):
await anc_app.log(log_lvl, "will not be sent")
ocs.assert_not_called()


def test_logging(nc_app):
log_handler = setup_nextcloud_logging("my_logger")
logger = logging.getLogger("my_logger")
logger.fatal("testing logging.fatal")
try:
a = b # noqa
except Exception: # noqa
logger.exception("testing logger.exception")
logger.removeHandler(log_handler)


def test_recursive_logging(nc_app):
logging.getLogger("httpx").setLevel(logging.DEBUG)
log_handler = setup_nextcloud_logging()
logger = logging.getLogger()
logger.fatal("testing logging.fatal")
logger.removeHandler(log_handler)
logging.getLogger("httpx").setLevel(logging.ERROR)