Skip to content

Commit 2924701

Browse files
authored
NextcloudApp: setup_nextcloud_logging function for transparent logging (#294)
Usage: ```python3 import logging from pathlib import Path from os import environ import nc_py_api environ["APP_ID"] = "nc_py_api" environ["APP_VERSION"] = "1.0.0" environ["APP_SECRET"] = "12345" environ["NEXTCLOUD_URL"] = "http://nextcloud.local" if __name__ == "__main__": nc_app = nc_py_api.NextcloudApp() logging.basicConfig( level=logging.INFO, format="%(asctime)s: [%(funcName)s]:%(levelname)s: %(message)s", datefmt="%H:%M:%S", ) logging.getLogger("httpx").setLevel(logging.ERROR) # not needed, but better to hide spam to console nc_py_api.ex_app.setup_nextcloud_logging() # setup logging handler in one line of code logging.fatal("Fatal test") logging.error("Error test") logging.warning("Warning test") logging.info("Info test") logging.debug("Debug test") logging.fatal("Fatal test2") try: a = 0 b = z except Exception as e: logging.exception("Exception test") ``` ### setup_nextcloud_logging ```python3 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 = _NextcloudStorageHandler() nextcloud_handler.setLevel(logging_level) logger.addHandler(nextcloud_handler) return nextcloud_handler ``` --------- Signed-off-by: Alexander Piskun <[email protected]>
1 parent 1471e90 commit 2924701

File tree

5 files changed

+86
-12
lines changed

5 files changed

+86
-12
lines changed

CHANGELOG.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ All notable changes to this project will be documented in this file.
44

55
## [0.17.1 - 2024-09-06]
66

7+
### Added
8+
9+
- NextcloudApp: `setup_nextcloud_logging` function to support transparently sending logs to Nextcloud. #294
10+
711
### Fixed
812

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

1115
## [0.17.0 - 2024-09-05]
1216

nc_py_api/ex_app/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
set_handlers,
1111
talk_bot_msg,
1212
)
13+
from .logging import setup_nextcloud_logging
1314
from .misc import (
1415
get_computation_device,
1516
get_model_path,

nc_py_api/ex_app/logging.py

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Transparent logging support to store logs in the nextcloud.log."""
2+
3+
import logging
4+
import threading
5+
6+
from ..nextcloud import NextcloudApp
7+
from .defs import LogLvl
8+
9+
LOGLVL_MAP = {
10+
logging.NOTSET: LogLvl.DEBUG,
11+
logging.DEBUG: LogLvl.DEBUG,
12+
logging.INFO: LogLvl.INFO,
13+
logging.WARNING: LogLvl.WARNING,
14+
logging.ERROR: LogLvl.ERROR,
15+
logging.CRITICAL: LogLvl.FATAL,
16+
}
17+
18+
THREAD_LOCAL = threading.local()
19+
20+
21+
class _NextcloudLogsHandler(logging.Handler):
22+
def __init__(self):
23+
super().__init__()
24+
25+
def emit(self, record):
26+
if THREAD_LOCAL.__dict__.get("nc_py_api.loghandler", False):
27+
return
28+
29+
try:
30+
THREAD_LOCAL.__dict__["nc_py_api.loghandler"] = True
31+
log_entry = self.format(record)
32+
log_level = record.levelno
33+
NextcloudApp().log(LOGLVL_MAP.get(log_level, LogLvl.FATAL), log_entry, fast_send=True)
34+
except Exception: # noqa pylint: disable=broad-exception-caught
35+
self.handleError(record)
36+
finally:
37+
THREAD_LOCAL.__dict__["nc_py_api.loghandler"] = False
38+
39+
40+
def setup_nextcloud_logging(logger_name: str | None = None, logging_level: int = logging.DEBUG):
41+
"""Function to easily send all or selected log entries to Nextcloud."""
42+
logger = logging.getLogger(logger_name)
43+
nextcloud_handler = _NextcloudLogsHandler()
44+
nextcloud_handler.setLevel(logging_level)
45+
logger.addHandler(nextcloud_handler)
46+
return nextcloud_handler

nc_py_api/nextcloud.py

+12-10
Original file line numberDiff line numberDiff line change
@@ -348,15 +348,16 @@ def enabled_state(self) -> bool:
348348
return bool(self._session.ocs("GET", "/ocs/v1.php/apps/app_api/ex-app/state"))
349349
return False
350350

351-
def log(self, log_lvl: LogLvl, content: str) -> None:
351+
def log(self, log_lvl: LogLvl, content: str, fast_send: bool = False) -> None:
352352
"""Writes log to the Nextcloud log file."""
353-
if self.check_capabilities("app_api"):
354-
return
355353
int_log_lvl = int(log_lvl)
356354
if int_log_lvl < 0 or int_log_lvl > 4:
357355
raise ValueError("Invalid `log_lvl` value")
358-
if int_log_lvl < self.capabilities["app_api"].get("loglevel", 0):
359-
return
356+
if not fast_send:
357+
if self.check_capabilities("app_api"):
358+
return
359+
if int_log_lvl < self.capabilities["app_api"].get("loglevel", 0):
360+
return
360361
with contextlib.suppress(Exception):
361362
self._session.ocs("POST", f"{self._session.ae_url}/log", json={"level": int_log_lvl, "message": content})
362363

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

485-
async def log(self, log_lvl: LogLvl, content: str) -> None:
486+
async def log(self, log_lvl: LogLvl, content: str, fast_send: bool = False) -> None:
486487
"""Writes log to the Nextcloud log file."""
487-
if await self.check_capabilities("app_api"):
488-
return
489488
int_log_lvl = int(log_lvl)
490489
if int_log_lvl < 0 or int_log_lvl > 4:
491490
raise ValueError("Invalid `log_lvl` value")
492-
if int_log_lvl < (await self.capabilities)["app_api"].get("loglevel", 0):
493-
return
491+
if not fast_send:
492+
if await self.check_capabilities("app_api"):
493+
return
494+
if int_log_lvl < (await self.capabilities)["app_api"].get("loglevel", 0):
495+
return
494496
with contextlib.suppress(Exception):
495497
await self._session.ocs(
496498
"POST", f"{self._session.ae_url}/log", json={"level": int_log_lvl, "message": content}

tests/actual_tests/logs_test.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import logging
12
from copy import deepcopy
23
from unittest import mock
34

45
import pytest
56

6-
from nc_py_api.ex_app import LogLvl
7+
from nc_py_api.ex_app import LogLvl, setup_nextcloud_logging
78

89

910
def test_loglvl_values():
@@ -113,3 +114,23 @@ async def test_log_without_app_api_async(anc_app):
113114
):
114115
await anc_app.log(log_lvl, "will not be sent")
115116
ocs.assert_not_called()
117+
118+
119+
def test_logging(nc_app):
120+
log_handler = setup_nextcloud_logging("my_logger")
121+
logger = logging.getLogger("my_logger")
122+
logger.fatal("testing logging.fatal")
123+
try:
124+
a = b # noqa
125+
except Exception: # noqa
126+
logger.exception("testing logger.exception")
127+
logger.removeHandler(log_handler)
128+
129+
130+
def test_recursive_logging(nc_app):
131+
logging.getLogger("httpx").setLevel(logging.DEBUG)
132+
log_handler = setup_nextcloud_logging()
133+
logger = logging.getLogger()
134+
logger.fatal("testing logging.fatal")
135+
logger.removeHandler(log_handler)
136+
logging.getLogger("httpx").setLevel(logging.ERROR)

0 commit comments

Comments
 (0)