Skip to content

Commit b6f1402

Browse files
ckaipflkuchenb
andauthored
Add json logger (#497)
* add dependency * add json formatter * update format for formatter_json * update log messages * change formatter to json * Update datameta/api/files.py Co-authored-by: Leon Kuchenbecker <[email protected]> * Update datameta/api/files.py Co-authored-by: Leon Kuchenbecker <[email protected]> * Update datameta/api/files.py Co-authored-by: Leon Kuchenbecker <[email protected]> * Update datameta/api/files.py Co-authored-by: Leon Kuchenbecker <[email protected]> * Update datameta/api/files.py Co-authored-by: Leon Kuchenbecker <[email protected]> * Update datameta/siteid.py Co-authored-by: Leon Kuchenbecker <[email protected]> * Update datameta/siteid.py Co-authored-by: Leon Kuchenbecker <[email protected]> * Update datameta/siteid.py Co-authored-by: Leon Kuchenbecker <[email protected]> * Update datameta/views/login.py Co-authored-by: Leon Kuchenbecker <[email protected]> * Update datameta/views/tfa.py Co-authored-by: Leon Kuchenbecker <[email protected]> * Update datameta/api/register.py Co-authored-by: Leon Kuchenbecker <[email protected]> * Apply suggestions from code review log messages Co-authored-by: Leon Kuchenbecker <[email protected]> * remove setting `datameta.logging.log_token_urls` * fix linting Co-authored-by: Leon Kuchenbecker <[email protected]>
1 parent 6d0b917 commit b6f1402

20 files changed

+67
-57
lines changed

conf/ci.ini

+6-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ datameta.site_id_prefix.metadatasets = DMR-
2929
datameta.site_id_prefix.files = DMF-
3030
datameta.site_id_prefix.services = DMP-
3131

32-
datameta.logging.log_token_urls = false
3332

3433
# ApiKeys:
3534
datameta.apikeys.max_expiration_period = 30
@@ -92,7 +91,7 @@ keys = root, datameta, sqlalchemy
9291
keys = console
9392

9493
[formatters]
95-
keys = generic
94+
keys = generic, json
9695

9796
[logger_root]
9897
level = INFO
@@ -115,7 +114,11 @@ qualname = sqlalchemy.engine
115114
class = StreamHandler
116115
args = (sys.stderr,)
117116
level = NOTSET
118-
formatter = generic
117+
formatter = json
118+
119+
[formatter_json]
120+
format = %(asctime)s %(levelname)s %(name)s %(threadName)s %(message)s
121+
class = pythonjsonlogger.jsonlogger.JsonFormatter
119122

120123
[formatter_generic]
121124
format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s

conf/development.ini

+6-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ datameta.site_id_prefix.metadatasets = DMR-
2929
datameta.site_id_prefix.files = DMF-
3030
datameta.site_id_prefix.services = DMP-
3131

32-
datameta.logging.log_token_urls = false
3332

3433
# ApiKeys:
3534
datameta.apikeys.max_expiration_period = 30
@@ -93,7 +92,7 @@ keys = root, datameta, sqlalchemy
9392
keys = console
9493

9594
[formatters]
96-
keys = generic
95+
keys = generic, json
9796

9897
[logger_root]
9998
level = INFO
@@ -116,7 +115,11 @@ qualname = sqlalchemy.engine
116115
class = StreamHandler
117116
args = (sys.stderr,)
118117
level = NOTSET
119-
formatter = generic
118+
formatter = json
119+
120+
[formatter_json]
121+
format = %(asctime)s %(levelname)s %(name)s %(threadName)s %(message)s
122+
class = pythonjsonlogger.jsonlogger.JsonFormatter
120123

121124
[formatter_generic]
122125
format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s

conf/docker_development.ini

+6-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ datameta.site_id_prefix.metadatasets = DMR-
2929
datameta.site_id_prefix.files = DMF-
3030
datameta.site_id_prefix.services = DMP-
3131

32-
datameta.logging.log_token_urls = false
3332

3433
# ApiKeys:
3534
datameta.apikeys.max_expiration_period = 30
@@ -87,7 +86,7 @@ keys = root, datameta, sqlalchemy
8786
keys = console
8887

8988
[formatters]
90-
keys = generic
89+
keys = generic, json
9190

9291
[logger_root]
9392
level = WARN
@@ -110,7 +109,11 @@ qualname = sqlalchemy.engine
110109
class = StreamHandler
111110
args = (sys.stderr,)
112111
level = NOTSET
113-
formatter = generic
112+
formatter = json
113+
114+
[formatter_json]
115+
format = %(asctime)s %(levelname)s %(name)s %(threadName)s %(message)s
116+
class = pythonjsonlogger.jsonlogger.JsonFormatter
114117

115118
[formatter_generic]
116119
format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s

conf/docker_production.ini

+6-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ datameta.site_id_prefix.metadatasets = $DATAMETA_SITE_ID_PREFIX_METADATASETS
2929
datameta.site_id_prefix.files = $DATAMETA_SITE_ID_PREFIX_FILES
3030
datameta.site_id_prefix.services = $DATAMETA_SITE_ID_PREFIX_SERVICES
3131

32-
datameta.logging.log_token_urls = false
3332

3433
# ApiKeys:
3534
datameta.apikeys.max_expiration_period = $DATAMETA_API_KEYS_MAX_EXPIRE_DAYS
@@ -91,7 +90,7 @@ keys = root, datameta, sqlalchemy
9190
keys = console
9291

9392
[formatters]
94-
keys = generic
93+
keys = generic, json
9594

9695
[logger_root]
9796
level = WARN
@@ -114,7 +113,11 @@ qualname = sqlalchemy.engine
114113
class = StreamHandler
115114
args = (sys.stderr,)
116115
level = NOTSET
117-
formatter = generic
116+
formatter = json
117+
118+
[formatter_json]
119+
format = %(asctime)s %(levelname)s %(name)s %(threadName)s %(message)s
120+
class = pythonjsonlogger.jsonlogger.JsonFormatter
118121

119122
[formatter_generic]
120123
format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s

conf/production.ini

+7-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ datameta.site_id_prefix.metadatasets = DMR-
2929
datameta.site_id_prefix.files = DMF-
3030
datameta.site_id_prefix.services = DMP-
3131

32-
datameta.logging.log_token_urls = false
3332

3433
# ApiKeys:
3534
datameta.apikeys.max_expiration_period = 30
@@ -82,11 +81,12 @@ listen = *:6543
8281

8382
[loggers]
8483
keys = root, datameta, sqlalchemy
84+
8585
[handlers]
8686
keys = console
8787

8888
[formatters]
89-
keys = generic
89+
keys = generic, json
9090

9191
[logger_root]
9292
level = WARN
@@ -109,7 +109,11 @@ qualname = sqlalchemy.engine
109109
class = StreamHandler
110110
args = (sys.stderr,)
111111
level = NOTSET
112-
formatter = generic
112+
formatter = json
113+
114+
[formatter_json]
115+
format = %(asctime)s %(levelname)s %(name)s %(threadName)s %(message)s
116+
class = pythonjsonlogger.jsonlogger.JsonFormatter
113117

114118
[formatter_generic]
115119
format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s

datameta/api/files.py

+5-7
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,11 @@ def delete_files(request: Request) -> HTTPNoContent:
119119

120120
# Log the deletions from db
121121
for user_uuid, file_uuid, storage_uri in deleted_files:
122-
log.info(f"[DB_FILE][DELETE][user={user_uuid}][file={file_uuid}]")
123-
122+
log.info("File record deleted from the database.", extra={"user_uuid": user_uuid, "file_uuid": file_uuid})
124123
# Delete the files from storage
125124
for user_uuid, file_uuid, storage_uri in deleted_files:
126125
storage.rm(request, storage_uri)
127-
log.info(f"[STORAGE][DELETE][user={user_uuid}][file={file_uuid}]")
126+
log.info("File deleted from storage.", extra={"user_uuid": user_uuid, "file_uuid": file_uuid})
128127

129128
return HTTPNoContent()
130129

@@ -261,7 +260,7 @@ def update_file(request: Request) -> HTTPOk:
261260
except storage.ChecksumMismatchError:
262261
# Checksum of uploaded data does not match announcement
263262
raise HTTPConflict(json=None) # 409
264-
log.info(f"[STORAGE][FREEZE][user={db_file.user.uuid}][file={db_file.uuid}]")
263+
log.info("Storage file frozen.", extra={"user_uuid": db_file.user.uuid, "file_uuid": db_file.uuid})
265264

266265
return FileResponse(
267266
id = resource.get_identifier(db_file),
@@ -317,10 +316,9 @@ def delete_file(request: Request) -> HTTPNoContent:
317316
# Commit transaction
318317
request.tm.commit()
319318
request.tm.begin()
320-
log.info(f"[DB_FILE][DELETE][user={user_uuid}][file={file_uuid}]")
321-
319+
log.info("File record deleted from the database.", extra={"user_uuid": user_uuid, "file_uuid": file_uuid})
322320
# Delete the file in storage if exists
323321
storage.rm(request, storage_uri)
324-
log.info(f"[STORAGE][DELETE][user={user_uuid}][file={file_uuid}]")
322+
log.info("File deleted from storage.", extra={"user_uuid": user_uuid, "file_uuid": file_uuid})
325323

326324
return HTTPNoContent()

datameta/api/register.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,7 @@ def post(request):
153153
)
154154

155155
if org_create:
156-
log.info(f"REGISTRATION REQUEST [email='{req_email}', name='{name}', new_org='{org_new_name}']")
156+
log.info("New registration request.", extra={"email": req_email, "name": name, "group": org_new_name, "is_new_group": True})
157157
else:
158-
log.info(f"REGISTRATION REQUEST [email='{req_email}', name='{name}', group='{group.name}']")
159-
158+
log.info("New registration request.", extra={"email": req_email, "name": name, "group": group.name, "is_new_group": False})
160159
return HTTPNoContent()

datameta/api/ui/admin.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def v_admin_put_request(request):
5353
else:
5454
raise SyntaxError()
5555
except Exception as e:
56-
log.error(f"MALFORMED API REQUEST AT /api/ui/admin/request: {e}")
56+
log.error("Malformed API request.", extra={"error": e})
5757
raise HTTPBadRequest()
5858

5959
# Check if the request exists
@@ -113,8 +113,7 @@ def v_admin_put_request(request):
113113
'token_url' : token_url
114114
}
115115
)
116-
log.info(f"NEW USER '{new_user.email}' [GROUP '{new_user.group.name}'] CONFIRMED BY '{req_user.email}'")
117-
log.info(f"NEW USER PWTOKEN URL '{token_url}'")
116+
log.info("Registation request accepted.", extra={"email": new_user.email, "group": new_user.group.name, "admin_email": req_user.email})
118117
else:
119118
# Denote email and name before request deletion
120119
reg_req_email = reg_req.email
@@ -132,7 +131,7 @@ def v_admin_put_request(request):
132131
'fullname' : reg_req_fullname,
133132
}
134133
)
135-
log.info(f"REGISTRATION REQUEST BY '{reg_req_email}' REJECTED BY '{req_user.email}'")
134+
log.info("Registation request rejected.", extra={"email": reg_req_email, "admin_email": req_user.email})
136135

137136
return HTTPNoContent()
138137

datameta/api/ui/convert.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def convert_samplesheet(db, file_like_obj, filename, user):
6161
reader = get_samplesheet_reader(file_like_obj)
6262
submitted_metadata = reader(file_like_obj)
6363
except Exception as e:
64-
log.info(f"submitted sample sheet '{filename}' triggered exception {e}")
64+
log.info("Sample sheet conversion failed.", extra={"file_name": filename, "error": e})
6565
raise samplesheet.SampleSheetReadError("Unable to parse the sample sheet.")
6666

6767
# Query column names that we expect to see in the sample sheet (intra-submission duplicates)
@@ -85,7 +85,7 @@ def convert_samplesheet(db, file_like_obj, filename, user):
8585
# string value is not possible through the convert API.
8686
return [ { mdname : None if not row[mdname] else formatted_mrec_value_str(row[mdname], metadata_datetimefmt[mdname]) for mdname in metadata_names } for _, row in submitted_metadata.iterrows() ]
8787
except Exception as e:
88-
log.error(e)
88+
log.error("Unexpected error during sample sheet conversion.", extra={"file_name": filename, "error": e})
8989
raise samplesheet.SampleSheetReadError("Unknown error")
9090

9191

@@ -113,5 +113,5 @@ def post(request: Request) -> HTTPOk:
113113
try:
114114
return convert_samplesheet(db, input_file.file, input_file.filename, auth_user)
115115
except samplesheet.SampleSheetReadError as e:
116-
log.warning(f"Sample sheet '{input_file.filename}' could not be read: {e}")
116+
log.warning("Unable to read sample sheet.", extra={"file_name": input_file.filename, "error": e})
117117
raise errors.get_validation_error(messages = [ str(e) ])

datameta/api/ui/forgot.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
# limitations under the License.
1414

1515
from pyramid.view import view_config
16-
from pyramid import threadlocal
17-
from pyramid.settings import asbool
1816

1917
from ... import security, email
2018
from ...settings import get_setting
@@ -39,8 +37,7 @@ def send_forgot_token(request, db_token_obj, clear_token):
3937
}
4038
)
4139

42-
if asbool(threadlocal.get_current_registry().settings['datameta.logging.log_token_urls']):
43-
log.debug(f"USER REQUESTED RECOVERY TOKEN: '{token_url}'")
40+
log.debug("User requested recovery token.", extra={"clear_token": clear_token})
4441

4542

4643
@view_config(route_name='forgot_api', renderer='json')
@@ -60,10 +57,10 @@ def v_forgot_api(request):
6057
db_token_obj, clear_token = security.get_new_password_reset_token_from_email(db, req_email)
6158
except KeyError:
6259
# User not found
63-
log.debug(f"DURING RECOVERY TOKEN REQUEST: USER COULD NOT BE RESOLVED FROM EMAIL: {req_email}")
60+
log.debug("Recovery token request. User could not be resolved.", extra={"req_email": req_email})
6461
else:
6562
# Generate a new forgot token and send it to the user
6663
send_forgot_token(request, db_token_obj, clear_token)
67-
log.debug(f"USER REQUESTED RECOVERY TOKEN: {clear_token}")
64+
log.debug("User requested recovery token.", extra={"clear_token": clear_token})
6865

6966
return { 'success' : True }

datameta/email/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,4 @@ def send(recipients, subject, template, values, bcc=None, rec_header_only=False)
4949
try:
5050
__smtp.sendMessage(__smtp_from, recipients, subject, message, bcc=bcc, rec_header_only=rec_header_only)
5151
except Exception as e:
52-
log.error(f"An error occurred when sending an email: {e}")
52+
log.error("SMTP error.", extra={"error": e})

datameta/samplesheet.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def datetime_iso_or_empty(x):
4747
try:
4848
return "" if pd.isna(x) else x.isoformat()
4949
except Exception as e:
50-
log.error(f"Unexpected exception: {e}")
50+
log.error("Failed to convert datetime.", extra={"error": e})
5151
return ""
5252

5353

datameta/security/__init__.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,11 @@ def register_failed_login_attempt(db, user):
176176
for attempt in db.query(LoginAttempt).filter(LoginAttempt.user_id == user.id).all()
177177
)
178178

179-
log.warning(f"FAILED LOGIN ATTEMPT USER id={user.id} n={n_failed_logins} within one hour.")
179+
log.warning("Failed login attempt.", extra={"user_id": user.id, "failed_logins_last_hour": n_failed_logins})
180180
if n_failed_logins >= max_allowed_failed_logins:
181181
db.query(User).filter(user.id == User.id).update({User.enabled: False})
182182
db.flush()
183-
log.warning(f"BLOCKED USER id={user.id} enabled={user.enabled} reason={n_failed_logins} failed login attempts within one hour.")
183+
log.warning("User blocked due to repeated failed login attempts.", extra={"user_id": user.id, "user_enabled": user.enabled, "failed_logins_last_hour": n_failed_logins})
184184

185185
return None
186186

@@ -192,7 +192,7 @@ def get_user_by_credentials(request, email: str, password: str):
192192
if user:
193193
if check_password_by_hash(password, user.pwhash):
194194
if not is_2fa_enabled():
195-
log.warning(f"CLEARING FAILED LOGIN ATTEMPTS FOR gubc USER {user}")
195+
log.warning("Clearing failed login attempts.", extra={"user_id": user.id, "credential": "password"})
196196
user.login_attempts.clear()
197197
return user
198198

@@ -247,7 +247,7 @@ def revalidate_user_token_based(request, token):
247247
request.tm.commit()
248248
request.tm.begin()
249249
else:
250-
log.warning(f"CLEARING FAILED LOGIN ATTEMPTS FOR APIKEY.USER {user.id}")
250+
log.warning("Clearing failed login attempts.", extra={"user_id": user.id, "credential": "api_key"})
251251
user.login_attempts.clear()
252252
return user
253253

@@ -277,7 +277,7 @@ def revalidate_user_session_based(request):
277277
request.session['site_admin'] = user.site_admin
278278
request.session['group_admin'] = user.group_admin
279279

280-
log.warning(f"CLEARING FAILED LOGIN ATTEMPTS FOR USER {user}")
280+
log.warning("Clearing failed login attempts.", extra={"user_id": user.id})
281281
user.login_attempts.clear()
282282
return user
283283

datameta/settings.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,4 @@ def includeme(config):
131131
for key, value in defaults.items():
132132
if key not in existing:
133133
db.add(ApplicationSetting(key=key, **value))
134-
log.info(f"No application setting found for '{key}'. Inserting default value.")
134+
log.info("No application setting found, inserting default value", extra={"key": key})

datameta/siteid.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@
2828
try:
2929
_prefix[entity] = threadlocal.get_current_registry().settings[f'datameta.site_id_prefix.{entity}']
3030
except Exception as e:
31-
raise
32-
log.warning(f"Site ID prefix for {entity} not found in configuration file ({e}).")
31+
log.warning("Site ID prefix not found in configuration file", extra={"entity": entity, "error": e})
32+
raise e
3333
try:
3434
_digits[entity] = int(threadlocal.get_current_registry().settings[f'datameta.site_id_digits.{entity}'])
3535
except Exception as e:
36-
raise
37-
log.warning(f"Site ID digits for {entity} not found in configuration file ({e}).")
36+
log.warning("Site ID prefix not found in configuration file", extra={"entity": entity, "error": e})
37+
raise e
3838

3939

4040
def generate(request, BaseClass):
@@ -44,6 +44,6 @@ def generate(request, BaseClass):
4444
for _ in range(10):
4545
new_id = prefix + str(random.randint(0, pow(10, digits))).rjust(digits, "0")
4646
if request.dbsession.query(BaseClass).filter(BaseClass.site_id == new_id).first():
47-
log.warning(f"Site ID collision for {BaseClass.__tablename__}. Your ID space may be saturating.")
47+
log.warning("Site ID collision, ID space may be saturating.", extra={"tablename": BaseClass.__tablename__})
4848
else:
4949
return new_id

datameta/storage.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def rm(request, storage_path):
5050
else:
5151
raise NotImplementedError()
5252
else:
53-
log.debug("DID NOT DELETE. DEMO MODE.")
53+
log.debug("Did not delete, demo mode.")
5454

5555

5656
def get_local_storage_path(request, storage_uri):
@@ -99,9 +99,9 @@ def write_file(request, db_file, file):
9999
file.seek(0)
100100
with open(out_path, 'wb') as outfile:
101101
shutil.copyfileobj(file, outfile)
102-
log.info(f"[STORAGE][NEWFILE][user={db_file.user.uuid}][file={db_file.uuid}]")
102+
log.info("New file in storage.", extra={"user_uuid": db_file.user.uuid, "file_uuid": db_file.uuid})
103103
else:
104-
log.info(f"[!!DEMOMODE!!][STORAGE][NEWFILE][user={db_file.user.uuid}][file={db_file.uuid}]")
104+
log.info("Demomode. New file in storage.", extra={"user_uuid": db_file.user.uuid, "file_uuid": db_file.uuid})
105105

106106

107107
def _freeze_local(request, db_file):

0 commit comments

Comments
 (0)