Logging
As introduced in Overview, nanopie provides pluggable solutions for logging, in the form of logging handlers. You may add logging handlers to an endpoint or a service to log the beginning and the end of the processing of a request automatically, or use these handlers separately in your code.
nanopie logging handers support structured logging and integrate closely with the
standard Python logging module
so as to provide an idiomatic logging solution. At its core, a logging handler
configures a Python logger with a formatter
(logging.Formatter)
and a handler (logging.handler); the formatter accepts a dictionary (Dict) as
template and formats its key-value pairs using standard
Python log record attributes, and the handler is responsible for transiting
the formatted logs, either in the form of a Dict or a JSON string, to
their destinations.
LoggingHandler is the foundation for all logging handlers in nanopie; this
handler simply writes logs (formatted as JSON strings) to standard streams.
Other logging handlers extend LoggingHandler and send logs to a variety
of destinations over the network, such as
Fluentd,
Logstash, or
Stackdriver Logging.
Using logging handlers
To add a logging handler to an endpoint or a service, specify it when you create the endpoint or the service.
from flask import Flask
from nanopie import FlaskService, LoggingHandler
app = Flask(__name__)
svc = FlaskService(app=app)
logging_handler = LoggingHandler()
@svc.list(name="list_users",
rule="/users/",
logging_handler=logging_handler)
def list_users():
do_something()
When added to an endpoint, the logging handler will log a message
(e.g. Entering span list_users) when a request arrives at the endpoint,
and another message (e.g. Exiting span list_users) when the endpoint
completes processing the request, along with other contextual information.
If you add a logging handler to a service, it will run for all the
endpoints in the service.
Alternatively, you may use a logging handler separately by getting the logger it configures:
from nanopie import LoggingHandler
logging_handler = LoggingHandler()
logger = logging_handler.default_logger
logger.info("This is a test message.")
Note
See Python logging module for instructions on how to use Python loggers.
Logging handlers also include methods for setting up additional loggers.
To configure a non-root logger, call the getLogger method:
| Arguments | Description |
|---|---|
name |
The name of the logger. See logging.getLogger for more information. |
append_handlers |
If set to True, the logging handler will set up a logger even if the logger already exists and has a handler specified. This may cause duplicate log entries when configured inappropriately. Defaults to False. |
from nanopie import LoggingHandler
logging_handler = LoggingHandler()
logger = logging_handler.getLogger('example-logger')
logger.info("This is a test message.")
If you are feeling courageous, you may even use the setup_root_logger method
to set up the root logger with nanopie. This will process all log entries from a
Python program, including those who are emitted by loggers in other modules,
in the manner dictated by the nanopie logging handler.
Note
This may cause an infinite loop of failure if configured inappropriately
since all Python loggers by default propagate to the root logger. See
the excluded_loggers argument below for more information.
| Arguments | Description |
|---|---|
excluded_loggers |
A list of loggers whose propagation will be disabled. This prevents error logs produced by erred handlers themselves being redirected back to these handlers. |
append_handlers |
If set to True, the logging handler will set up the root logger even if it already has a handler specified. This may cause duplicate log entries when configured inappropriately. Defaults to False. |
Logs
Loggers configured by nanopie logging handlers by default logs messages
plus some contextual information in a structured manner using either the
Dict type or JSON strings.
For example, when running this code snippet
from nanopie import LoggingHandler
logging_handler = LoggingHandler()
logger = logging_handler.default_logger
logger.info("This is a test message.")
You should see the following output in the terminal (pretty-printed):
{
"host": "HOSTNAME",
"logger": "LOGGER",
"level": "INFO",
"module": "MODULE",
"func": "FUNC",
"message": "This is a test message."
}
The contextual information includes:
| Attribute | Description |
|---|---|
host |
The hostname of the machine running the microservice/API backend. |
logger |
The name of the logger that logs the message. |
level |
The level of the log message. |
module |
The module where the logger is called. |
func |
The function where the logger is called. |
You can configure what additional context logging handlers should log along with the message; it is even possible to ask these handlers to extract log contexts from incoming requests automatically and send them along with the log message. See the section below for instructions.
Configuring logging handlers
Logging handlers provide the following options for configuring its logging behavior:
| Argument | Description |
|---|---|
default_logger_name |
The name of the logger that the logging handler itself uses. Defaults to __name__. If you plan to use different logging handlers in the same service/API backend, you should assign each of them a different default_logger_name. |
span_name |
The name of the span. This value is used in the log messages that logging handlers output when being added to an endpoint or service. If not specified, logging handlers will try to use the name of the currently executing endpoint. |
level |
The level of the log message. See Python Logging Levels for more information. |
fmt |
The format of the log message, in the form of a Dict. See the instructions below in this section for more information. |
datefmt |
The format of date and time (if any) in the log. See Python Logging Date and Time Formatting for more information. |
style |
How the values in the fmt Dict are formatted. It can be one of the three values: %, {, and $. See Python Logging Formatting Styles for more information. |
mode |
The mode this logging handler uses. See the instructions below in this section for more information. |
log_ctx_extractor |
A log context extractor that automatically extracts log contexts from requests. See the instructions below in this section for more information. |
quiet |
If set to True, the logging handler runs quietly when extracting log contexts, i.e. it will not report any error should a log context cannot be extracted or processed. |
Formats
fmt is essentially a template of log entries that loggers configured
by nanopie logging handlers use to build structured logs. The default
fmt looks as follows:
{
# hostname is the return of the socket.gethostname() method
"host": "{}".format(hostname),
"logger": "%(name)s",
"level": "%(levelname)s",
"module": "%(module)s",
"func": "%(funcName)s",
}
Every time you call the configured logger to log a message, it will
replace the placeholders, such as %(name)s and %(levelname)s,
with available Python Log Record Attributes, in the same way as the str.format() method
works.
You can update the fmt to include additional contextual information
in your logs. For example, with the fmt below configured loggers will
include the time when a message is logged in the structured logs:
from nanopie import LoggingHandler
logging_handler = LoggingHandler(fmt={"time": "%(asctime)s"})
logger = logging_handler.default_logger
logger.info("This is a test message.")
# The output should look like
# {"time": "2020-09-01 08:54:51,060", "message": "This is a test message."}
Modes
mode controls how handlers in the configured loggers transmit log messages.
Three options are available:
SYNC: Transmit logs synchronously.BACKGROUND_THREAD: Transmit logs asynchronously with a background thread.ASYNC: Transmit logs asynchronously in an event loop. This option only works when you are using configured loggers with an asynchronous framework, such asquart, as transport.
The base LoggingHandler always work in the SYNC mode. This is
due to the fact that this logging handler only writes to the standard
streams on the local machine and it does not make sense to enable
asynchronous support.
Note
The available modes are listed in nanopie.LoggingHandlerModes. Instead
of the raw values, you may also use LoggingHandlerModes.SYNC,
LoggingHandlerModes.BACKGROUND_THREAD, or
LoggingHandlerModes.ASYNC.
Note
At this early stage of development, some nanopie logging handlers have limited support for modes.
Log context extractors
It is common for microservice and API backend developers to pass contextual information in requests, so as to track user activities and/or system workflow through services. A e-commerce service frontend, for example, may pass the IDs of customers in requests; when processing the requests, the backend will log the process with the IDs attached. The centralized logging system can then correlate log entries using the IDs, granting developers and operators a holistic view of interactions within a customer session.
To support this use case, nanopie logging handler accepts a
LogContextExtractor as argument. When formatting a log message,
the formatter will invoke the log context extractor, which extracts
contextual information from the request currently being processed. You can
create a log context extractor by subclassing the LogContextExtractor
base class and override its extract method; the method should return
a nanopie data model (Model) as output, which will be merged into
the structured log:
from nanopie import Model, StringField, LogContextExtractor, LoggingHandler
# This is the data model for the log contexts in requests
class LogContext(Model):
user_id = StringField()
class CustomLogContextExtractor(LogContextExtractor):
# The `request` is the global request object
# See the Services page of the documentation for more information
def extract(self, request):
user_id = request.headers.get('user_id')
log_context = LogContext(user_id=user_id)
return log_context
logging_handler = LoggingHandler(log_ctx_extractor=CustomLogContextExtractor())
Connecting logging handlers to log collection services
The LoggingHandler, as specified in the beginning of this document, writes
all logs to the standard streams. If you would like to transmit logs
to other destinations, such as log collectors, nanopie provides a number
of additional logging handlers; they accept the same set of logging options and
function in the same way as the foundation LoggingHandler, with the only
difference being the destinations of logs.
| Available Logging Handlers | Description |
|---|---|
FluentdLoggingHandler |
A logging handler that transmits logs to a Fluentd service. |
LogstashLoggingHandler |
A logging handler that transmits logs to a Logstash service. |
StackdriverLoggingHandler |
A logging handler that transmits logs to Stackdriver Logging. |
Using FluentdLoggingHandler
Aside from the options inherited from LoggingHandler, this logging handler
accepts the following additional arguments:
| Argument | Description |
|---|---|
host |
The hostname or address of the Fluentd service. |
port |
The port of the Fluentd service. |
tag |
The log entry tags. |
from nanopie import FluentdLoggingHandler
logging_handler = FluentdLoggingHandler(host="HOST",
port="PORT",
tag="TAG")
logger = logging_handler.default_logger
logger.info("This is a test message")
Using LogstashLoggingHandler
Aside from the options inherited from LoggingHandler, this logging handler
accepts the following additional arguments:
| Argument | Description |
|---|---|
host |
The hostname or address of the Logstash service. |
port |
The port of the Logstash service. |
use_udp |
If set to True, the logs will be transmitted using the UDP protocol. |
from nanopie import LogstashLoggingHandler
logging_handler = LogstashLoggingHandler(host="HOST",
port="PORT",
use_udp=False)
logger = logging_handler.default_logger
logger.info("This is a test message")
Using StackdriverLoggingHandler
Aside from the options inherited from LoggingHandler, this logging handler
accepts the following additional arguments:
| Argument | Description |
|---|---|
client |
A Stackdriver client. |
client_args |
Keyword arguments for the Stackdriver client. See Stackdriver Client for Python for more information. |
custom_log_name |
The name of the log entries in Stackdriver Logging. See Stackdriver Logging Python Logging Handlers for more information. |
resource |
The resource associated with the logs. |
labels |
The labels associated with the logs. |
stream |
The stream to use. |
from google.cloud import logging
from nanopie import StackdriverLoggingHandler
stackdriver_client = logging.Client()
logging_handler = StackdriverLoggingHandler(client=stackdriver_client)
logger = logging_handler.default_logger
logger.info("This is a test message")