Skip to content

Commit 7796bf5

Browse files
authored
Merge pull request #404 from benjaminysmith/main
Add structured logger to epidata, and log metadata generation
2 parents 46822ce + 49e786b commit 7796bf5

File tree

3 files changed

+114
-5
lines changed

3 files changed

+114
-5
lines changed

src/acquisition/covidcast/covidcast_meta_cache_updater.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,38 @@
33
# standard library
44
import argparse
55
import sys
6+
import time
67

78
# first party
89
from delphi.epidata.acquisition.covidcast.database import Database
10+
from delphi.epidata.acquisition.covidcast.logger import get_structured_logger
911
from delphi.epidata.client.delphi_epidata import Epidata
1012

11-
1213
def get_argument_parser():
1314
"""Define command line arguments."""
1415

15-
# there are no flags, but --help will still work
16-
return argparse.ArgumentParser()
16+
parser = argparse.ArgumentParser()
17+
parser.add_argument("--log_file", help="filename for log output")
18+
return parser
1719

1820

1921
def main(args, epidata_impl=Epidata, database_impl=Database):
2022
"""Update the covidcast metadata cache.
2123
2224
`args`: parsed command-line arguments
2325
"""
26+
logger = get_structured_logger(
27+
"metadata_cache_updater",
28+
filename=args.log_file)
29+
start_time = time.time()
2430
database = database_impl()
2531
database.connect()
2632

2733
# fetch metadata
2834
try:
35+
metadata_calculation_start_time = time.time()
2936
metadata = database.get_covidcast_meta()
37+
metadata_calculation_interval_in_seconds = metadata_calculation_start_time - start_time
3038
except:
3139
# clean up before failing
3240
database.disconnect(True)
@@ -44,13 +52,22 @@ def main(args, epidata_impl=Epidata, database_impl=Database):
4452

4553
# update the cache
4654
try:
55+
metadata_update_start_time = time.time()
4756
database.update_covidcast_meta_cache(metadata)
57+
metadata_update_interval_in_seconds = time.time() - metadata_update_start_time
4858
print('successfully cached epidata')
4959
finally:
5060
# no catch block so that an exception above will cause the program to
5161
# fail after the following cleanup
5262
database.disconnect(True)
5363

64+
logger.info(
65+
"Generated and updated covidcast metadata",
66+
metadata_calculation_interval_in_seconds=round(
67+
metadata_calculation_interval_in_seconds, 2),
68+
metadata_update_interval_in_seconds=round(
69+
metadata_update_interval_in_seconds, 2),
70+
total_runtime_in_seconds=round(time.time() - start_time, 2))
5471
return True
5572

5673

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""Structured logger utility for creating JSON logs in Delphi pipelines."""
2+
import logging
3+
import sys
4+
import threading
5+
import structlog
6+
7+
8+
def handle_exceptions(logger):
9+
"""Handle exceptions using the provided logger."""
10+
def exception_handler(etype, value, traceback):
11+
logger.exception("Top-level exception occurred",
12+
exc_info=(etype, value, traceback))
13+
14+
def multithread_exception_handler(args):
15+
exception_handler(args.exc_type, args.exc_value, args.exc_traceback)
16+
17+
sys.excepthook = exception_handler
18+
threading.excepthook = multithread_exception_handler
19+
20+
21+
def get_structured_logger(name=__name__,
22+
filename=None,
23+
log_exceptions=True):
24+
"""Create a new structlog logger.
25+
26+
Use the logger returned from this in indicator code using the standard
27+
wrapper calls, e.g.:
28+
29+
logger = get_structured_logger(__name__)
30+
logger.warning("Error", type="Signal too low").
31+
32+
The output will be rendered as JSON which can easily be consumed by logs
33+
processors.
34+
35+
See the structlog documentation for details.
36+
37+
Parameters
38+
---------
39+
name: Name to use for logger (included in log lines), __name__ from caller
40+
is a good choice.
41+
filename: An (optional) file to write log output.
42+
"""
43+
# Configure the underlying logging configuration
44+
handlers = [logging.StreamHandler()]
45+
if filename:
46+
handlers.append(logging.FileHandler(filename))
47+
48+
logging.basicConfig(
49+
format="%(message)s",
50+
level=logging.INFO,
51+
handlers=handlers
52+
)
53+
54+
# Configure structlog. This uses many of the standard suggestions from
55+
# the structlog documentation.
56+
structlog.configure(
57+
processors=[
58+
# Filter out log levels we are not tracking.
59+
structlog.stdlib.filter_by_level,
60+
# Include logger name in output.
61+
structlog.stdlib.add_logger_name,
62+
# Include log level in output.
63+
structlog.stdlib.add_log_level,
64+
# Allow formatting into arguments e.g., logger.info("Hello, %s",
65+
# name)
66+
structlog.stdlib.PositionalArgumentsFormatter(),
67+
# Add timestamps.
68+
structlog.processors.TimeStamper(fmt="iso"),
69+
# Match support for exception logging in the standard logger.
70+
structlog.processors.StackInfoRenderer(),
71+
structlog.processors.format_exc_info,
72+
# Decode unicode characters
73+
structlog.processors.UnicodeDecoder(),
74+
# Render as JSON
75+
structlog.processors.JSONRenderer()
76+
],
77+
# Use a dict class for keeping track of data.
78+
context_class=dict,
79+
# Use a standard logger for the actual log call.
80+
logger_factory=structlog.stdlib.LoggerFactory(),
81+
# Use a standard wrapper class for utilities like log.warning()
82+
wrapper_class=structlog.stdlib.BoundLogger,
83+
# Cache the logger
84+
cache_logger_on_first_use=True,
85+
)
86+
87+
logger = structlog.get_logger(name)
88+
89+
if log_exceptions:
90+
handle_exceptions(logger)
91+
92+
return logger

tests/acquisition/covidcast/test_covidcast_meta_cache_updater.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def test_main_successful(self):
3434
'epidata': [{'foo': 'bar'}],
3535
}
3636

37-
args = None
37+
args = MagicMock(log_file="log")
3838
mock_epidata_impl = MagicMock()
3939
mock_epidata_impl.covidcast_meta.return_value = api_response
4040
mock_database = MagicMock()
@@ -64,7 +64,7 @@ def test_main_failure(self):
6464
'message': 'no',
6565
}
6666

67-
args = None
67+
args = MagicMock(log_file="log")
6868
mock_database = MagicMock()
6969
mock_database.get_covidcast_meta.return_value = list()
7070
fake_database_impl = lambda: mock_database

0 commit comments

Comments
 (0)