Skip to content

Commit ffde9a7

Browse files
authored
#739 Logging (#749)
* Better error logging during ballot decryption * # per RC 8/15/22 log errors and continue processing even if it makes numbers incorrect * Ability to set the log level in logger for the stream handler * Combine EG logging with eel logging * cache clean in dockerfile * fix issue with recovering during ballot parsing errors * log to file * Use date in file name so that when run on single machine containers don't conflict * Log stack traces
1 parent 2a5cb07 commit ffde9a7

10 files changed

Lines changed: 81 additions & 33 deletions

File tree

src/electionguard/logs.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@ class ElectionGuardLog(Singleton):
1616
"""
1717

1818
__logger: logging.Logger
19+
__stream_handler: logging.StreamHandler
1920

2021
def __init__(self) -> None:
2122
super().__init__()
2223

2324
self.__logger = logging.getLogger("electionguard")
24-
self.__logger.addHandler(get_stream_handler())
25+
# Pythong's logger will use the most restrictive of the logger level and the handler level,
26+
# so set the logger to the lowest level the handler ever might log at
27+
self.__logger.setLevel(logging.DEBUG)
28+
self.__stream_handler = get_stream_handler(logging.INFO)
29+
self.__logger.addHandler(self.__stream_handler)
2530

2631
@staticmethod
2732
def __get_call_info() -> Tuple[str, str, int]:
@@ -44,6 +49,13 @@ def __formatted_message(self, message: str) -> str:
4449
message = f"{os.path.basename(filename)}.{funcname}:#L{line}: {message}"
4550
return message
4651

52+
def set_stream_log_level(self, Level: int) -> None:
53+
"""
54+
Sets the stream log level
55+
"""
56+
self.remove_handler(self.__stream_handler)
57+
self.add_handler(get_stream_handler(Level))
58+
4759
def add_handler(self, handler: logging.Handler) -> None:
4860
"""
4961
Adds a logger handler
@@ -93,17 +105,17 @@ def critical(self, message: str, *args: Any, **kwargs: Any) -> None:
93105
self.__logger.critical(self.__formatted_message(message), *args, **kwargs)
94106

95107

96-
def get_stream_handler() -> logging.StreamHandler:
108+
def get_stream_handler(log_level: int) -> logging.StreamHandler:
97109
"""
98110
Get a Stream Handler, sends only warnings and errors to stdout.
99111
"""
100112
stream_handler = logging.StreamHandler(sys.stdout)
101-
stream_handler.setLevel(logging.INFO)
113+
stream_handler.setLevel(log_level)
102114
stream_handler.setFormatter(logging.Formatter(FORMAT))
103115
return stream_handler
104116

105117

106-
def get_file_handler() -> logging.FileHandler:
118+
def get_file_handler(log_level: int, filename: str) -> logging.FileHandler:
107119
"""
108120
Get a File System Handler, sends verbose logging to a file, `electionguard.log`.
109121
When that file gets too large, the logs will rotate, creating files with names
@@ -113,9 +125,9 @@ def get_file_handler() -> logging.FileHandler:
113125
# TODO: add file compression, save a bunch of space.
114126
# https://medium.com/@rahulraghu94/overriding-pythons-timedrotatingfilehandler-to-compress-your-log-files-iot-c766a4ace240 # pylint: disable=line-too-long
115127
file_handler = RotatingFileHandler(
116-
"electionguard.log", "a", maxBytes=10_000_000, backupCount=10
128+
filename, "a", maxBytes=10_000_000, backupCount=10
117129
)
118-
file_handler.setLevel(logging.DEBUG)
130+
file_handler.setLevel(log_level)
119131
file_handler.setFormatter(logging.Formatter(FORMAT))
120132
return file_handler
121133

src/electionguard_gui/Dockerfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,17 @@ RUN poetry config virtualenvs.in-project true && \
5959
# Get Source
6060
##################################################################################
6161

62+
# cleanup first, the next layer will get invalidated easiliy
63+
RUN apt-get clean && \
64+
rm -rf /var/lib/apt/lists/*
65+
6266
COPY ./src ./src
6367
RUN rm src/electionguard_gui/__init__.py
6468

6569
##################################################################################
6670
# Run EGUI
6771
##################################################################################
6872

69-
# cleanup
70-
RUN apt-get clean && \
71-
rm -rf /var/lib/apt/lists/*
72-
7373
# the final poetry install runs fast, it activates the virtualenv and initializes the modules
7474
RUN poetry install
7575

src/electionguard_gui/components/component_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ def expose(self) -> None:
2727
fact that method names exposed must be globally unique."""
2828

2929
def handle_error(self, error: Exception) -> dict[str, Any]:
30-
self._log.error(error)
30+
self._log.error("error in component_base", error)
3131
traceback.print_exc()
3232
return eel_fail(str(error))

src/electionguard_gui/components/key_ceremony_details_component.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def on_key_ceremony_changed(self, _: str, key_ceremony_id: str) -> None:
109109
eel.refresh_key_ceremony(eel_success(result))
110110
# pylint: disable=broad-except
111111
except Exception as e:
112-
self._log.error(e)
112+
self._log.error("error on key ceremony changed", e)
113113
traceback.print_exc()
114114
# pylint: disable=no-member
115115
eel.refresh_key_ceremony(eel_fail(str(e)))

src/electionguard_gui/components/view_decryption_component.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def on_decryption_changed(self, _: str, decryption_id: str) -> None:
6565
refresh_decryption(eel_success())
6666
# pylint: disable=broad-except
6767
except Exception as e:
68-
self._log.error(e)
68+
self._log.error("error in on decryption changed", e)
6969
traceback.print_exc()
7070
refresh_decryption(eel_fail(str(e)))
7171

src/electionguard_gui/containers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class Container(containers.DeclarativeContainer):
5454
"""Responsible for dependency injection and how components are wired together"""
5555

5656
# services
57-
log_service: Factory[EelLogService] = providers.Factory(EelLogService)
57+
log_service: Singleton[EelLogService] = providers.Singleton(EelLogService)
5858
config_service: Factory[ConfigurationService] = providers.Factory(
5959
ConfigurationService
6060
)

src/electionguard_gui/main_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,6 @@ def start(self) -> None:
103103
self.log_service.debug(f"Starting eel port={port} mode={mode} host={host}")
104104
eel.start("index.html", size=(1024, 768), port=port, mode=mode, host=host)
105105
except Exception as e:
106-
self.log_service.error(e)
106+
self.log_service.error("error in main app start", e)
107107
traceback.print_exc()
108108
raise e

src/electionguard_gui/services/ballot_upload_service.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,18 @@ def add_ballot(
5959
)
6060

6161
def get_ballots(self, db: Database, election_id: str) -> list[SubmittedBallot]:
62-
self._log.trace(f"getting ballots for {election_id}")
62+
self._log.debug(f"getting ballots for {election_id}")
6363
ballot_uploads = db.ballot_uploads.find(
6464
{"election_id": election_id, "file_contents": {"$exists": True}}
6565
)
6666
ballots = []
6767
for ballot_obj in ballot_uploads:
6868
ballot_str = ballot_obj["file_contents"]
69-
ballot = from_raw(SubmittedBallot, ballot_str)
70-
ballots.append(ballot)
69+
try:
70+
ballot = from_raw(SubmittedBallot, ballot_str)
71+
ballots.append(ballot)
72+
# pylint: disable=broad-except
73+
except Exception as e:
74+
self._log.error("error deserializing ballot: {ballot_obj}", e)
75+
# per RC 8/15/22 log errors and continue processing even if it makes numbers incorrect
7176
return ballots
Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,59 @@
11
from datetime import datetime
2+
import logging
3+
from os import path, makedirs
4+
from typing import Any
5+
from electionguard.logs import (
6+
get_file_handler,
7+
log_critical,
8+
log_debug,
9+
log_error,
10+
log_info,
11+
log_warning,
12+
LOG,
13+
)
14+
from electionguard_gui.services.directory_service import get_data_dir
215
from electionguard_gui.services.service_base import ServiceBase
316

417

518
class EelLogService(ServiceBase):
619
"""A facade for logging. Currently this simply writes to the console without using log levels, but
720
this may eventually be used to log to a file or database."""
821

9-
# pylint: disable=no-self-use
10-
def _log(self, level: str, message: str) -> None:
11-
print(f"{datetime.now()} {level} {message}")
22+
def __init__(self) -> None:
23+
LOG.set_stream_log_level(logging.DEBUG)
24+
file_dir = path.join(get_data_dir(), "logs")
25+
makedirs(file_dir, exist_ok=True)
26+
now = datetime.now().strftime("%Y-%m-%d-%H-%M-%S-%f")
27+
file_name = path.join(file_dir, f"electionguard-{now}.log")
28+
LOG.add_handler(get_file_handler(logging.DEBUG, file_name))
1229

13-
def trace(self, message: str) -> None:
30+
def trace(self, message: str, *args: Any, **kwargs: Any) -> None:
1431
pass
1532

16-
def debug(self, message: str) -> None:
17-
self._log("DEBUG", message)
33+
# pylint: disable=no-self-use
34+
def debug(self, message: str, *args: Any, **kwargs: Any) -> None:
35+
log_debug(message, *args, **kwargs)
1836

19-
def info(self, message: str) -> None:
20-
self._log("INFO ", message)
37+
# pylint: disable=no-self-use
38+
def info(self, message: str, *args: Any, **kwargs: Any) -> None:
39+
log_info(message, *args, **kwargs)
2140

22-
def warn(self, message: str) -> None:
23-
self._log("WARN ", message)
41+
# pylint: disable=no-self-use
42+
def warn(self, message: str, *args: Any, **kwargs: Any) -> None:
43+
log_warning(message, *args, **kwargs)
2444

25-
def error(self, e: Exception) -> None:
26-
self._log("ERROR", str(e))
45+
# pylint: disable=no-self-use
46+
def error(self, message: str, exception: Exception) -> None:
47+
log_error(
48+
f"{message} '{exception}'",
49+
exc_info=1,
50+
extra={"exception": exception},
51+
)
2752

28-
def fatal(self, message: str) -> None:
29-
self._log("FATAL", message)
53+
# pylint: disable=no-self-use
54+
def fatal(self, message: str, exception: Exception) -> None:
55+
log_critical(
56+
f"{message} '{str(exception)}'",
57+
exc_info=1,
58+
extra={"exception": exception},
59+
)

tests/unit/electionguard/test_logs.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
from tests.base_test_case import BaseTestCase
23

34
from electionguard.logs import (
@@ -45,7 +46,7 @@ def test_log_handlers(self):
4546
self.assertEqual(len(empty_handlers), 0)
4647

4748
# Act
48-
log_add_handler(get_stream_handler())
49+
log_add_handler(get_stream_handler(logging.INFO))
4950
added_handlers = log_handlers()
5051

5152
# Assert

0 commit comments

Comments
 (0)