-
Notifications
You must be signed in to change notification settings - Fork 0
WIP: Introduce experimental JSON logging #16
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,6 @@ | |
from localstack.http import Response | ||
from localstack.http.request import restore_payload | ||
from localstack.logging.format import AwsTraceLoggingFormatter, TraceLoggingFormatter | ||
from localstack.logging.setup import create_default_handler | ||
|
||
LOG = logging.getLogger(__name__) | ||
|
||
|
@@ -72,11 +71,12 @@ def internal_http_logger(self): | |
|
||
# make sure loggers are loaded after logging config is loaded | ||
def _prepare_logger(self, logger: logging.Logger, formatter: Type): | ||
if logger.isEnabledFor(logging.DEBUG): | ||
logger.propagate = False | ||
handler = create_default_handler(logger.level) | ||
handler.setFormatter(formatter()) | ||
logger.addHandler(handler) | ||
# TODO: uncommenting this will block .http and .aws logs from being propagated | ||
# if logger.isEnabledFor(logging.DEBUG): | ||
# logger.propagate = False | ||
# handler = create_default_handler(logger.level) | ||
# handler.setFormatter(formatter()) | ||
# logger.addHandler(handler) | ||
return logger | ||
Comment on lines
73
to
80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Commented-out code block prevents propagation of .http and .aws logs. Consider removing or implementing a configuration option to control this behavior. |
||
|
||
def _log(self, context: RequestContext, response: Response): | ||
|
@@ -95,6 +95,10 @@ def _log(self, context: RequestContext, response: Response): | |
response.status_code, | ||
context.service_exception.code, | ||
extra={ | ||
"request_id": context.request_id, | ||
"service": context.service.service_name, | ||
"operation": context.operation.name, | ||
"status_code": response.status_code, | ||
# context | ||
"account_id": context.account_id, | ||
"region": context.region, | ||
|
@@ -117,6 +121,10 @@ def _log(self, context: RequestContext, response: Response): | |
context.operation.name, | ||
response.status_code, | ||
extra={ | ||
"request_id": context.request_id, | ||
"service": context.service.service_name, | ||
"operation": context.operation.name, | ||
"status_code": response.status_code, | ||
# context | ||
"account_id": context.account_id, | ||
"region": context.region, | ||
|
@@ -142,6 +150,9 @@ def _log(self, context: RequestContext, response: Response): | |
context.request.path, | ||
response.status_code, | ||
extra={ | ||
"request_id": context.request_id or "NOREQUESTID", | ||
"http_method": context.request.method, | ||
"status_code": response.status_code, | ||
# request | ||
"input_type": "Request", | ||
"input": restore_payload(context.request), | ||
|
@@ -152,3 +163,25 @@ def _log(self, context: RequestContext, response: Response): | |
"response_headers": dict(response.headers), | ||
}, | ||
) | ||
|
||
|
||
class RequestLogger: | ||
def __call__(self, _: HandlerChain, context: RequestContext, response: Response): | ||
if context.request.path == "/health" or context.request.path == "/_localstack/health": | ||
# special case so the health check doesn't spam the logs | ||
return | ||
Comment on lines
+170
to
+172
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Duplicate health check logic. Consider extracting this into a separate method to avoid repetition. |
||
|
||
logger = logging.getLogger("localstack.request.raw_http") | ||
logger.info( | ||
"%s %s", | ||
context.request.method, | ||
context.request.path, | ||
extra={ | ||
"request_id": context.request_id or "NOREQUESTID", | ||
"http_method": context.request.method, | ||
# request | ||
"input_type": "Request", | ||
"input": restore_payload(context.request), # TODO: remove? | ||
"request_headers": dict(context.request.headers), | ||
}, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,10 @@ | ||
import json | ||
import logging | ||
import sys | ||
import warnings | ||
|
||
from localstack_snapshot.util.encoding import CustomJsonEncoder | ||
|
||
from localstack import config, constants | ||
|
||
from .format import AddFormattedAttributes, DefaultFormatter | ||
|
@@ -87,6 +90,32 @@ def create_default_handler(log_level: int): | |
return log_handler | ||
|
||
|
||
class JsonFormatter(logging.Formatter): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: JsonFormatter implementation is naive and may not handle all data types correctly. Consider using a more robust JSON serialization method. |
||
def format(self, record: logging.LogRecord) -> str: | ||
# TODO: extras are currently flat at root level and not nested | ||
data = {} | ||
skip_kw = ["msg", "message", "args", "exc_info"] | ||
for k, v in record.__dict__.items(): | ||
if k in skip_kw: | ||
continue | ||
if v is not None: | ||
data[k] = v | ||
data["message"] = record.getMessage() | ||
|
||
if record.exc_info: | ||
data["error_type"] = record.exc_info[0].__name__ | ||
data["error_msg"] = str(record.exc_info[1]) | ||
if record.exc_text: | ||
data["exc_text"] = record.exc_text | ||
|
||
# import re | ||
# reg = re.compile(r"./localstack/services/([^/])") | ||
# if "localstack/services/" in record.pathname: | ||
# service = record.pathname.split("") | ||
# data["service"] = | ||
Comment on lines
+111
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Commented-out code for service extraction. Either implement or remove this block. |
||
return json.dumps(data, cls=CustomJsonEncoder) | ||
|
||
|
||
def setup_logging(log_level=logging.INFO) -> None: | ||
""" | ||
Configures the python logging environment for LocalStack. | ||
|
@@ -109,6 +138,20 @@ def setup_logging(log_level=logging.INFO) -> None: | |
for logger, level in default_log_levels.items(): | ||
logging.getLogger(logger).setLevel(level) | ||
|
||
# Configure JSON logs | ||
# TODO: make configurable/opt-in | ||
logging.basicConfig(level=logging.DEBUG) | ||
file_handler = logging.FileHandler("/tmp/localstack.log", mode="w", encoding="utf-8") | ||
Comment on lines
+142
to
+144
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: JSON logging is not configurable and uses a hardcoded file path. Make this configurable as mentioned in the TODO. |
||
|
||
file_handler.setFormatter(JsonFormatter()) | ||
logging.root.addHandler(file_handler) | ||
logging.root.setLevel(logging.DEBUG) # FIXME | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Setting root logger to DEBUG level may be too verbose. Implement proper log level configuration. |
||
|
||
# silence noisy libs by default | ||
logging.getLogger("werkzeug").setLevel(logging.CRITICAL) | ||
logging.getLogger("stevedore").setLevel(logging.CRITICAL) | ||
logging.getLogger("botocore").setLevel(logging.CRITICAL) | ||
|
||
|
||
def setup_hypercorn_logger(hypercorn_config) -> None: | ||
""" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Consider adding a comment explaining the purpose of this new logger