-
Notifications
You must be signed in to change notification settings - Fork 53
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
Logging: support sending structured logs to stackdriver via stdlib 'logging'. #13
Comments
What is the status of this request? I know the root issue is that the formatter must know the attributes of the extra arguments passed into the stdlib logger, but is it too much of hassle to the point of being won't fix? My hesitation with the |
@parthmishra The request as it stands (extract the "extra" fields from stdlib's To deal just with the synchronous bit, you could interact directly with the "background thread handler" transport. Its main worker method just polls the queue for lists of dicts to be pushed to the API: https://github.com/googleapis/google-cloud-python/blob/a14ffbaa50f7823c2792e91413a37cbc3ce687f5/logging/google/cloud/logging/handlers/transports/background_thread.py#L134-L152 |
I wanted to do something similar in order to have errors show up in Stackdriver Error Reporting, which is possible according to this issue I opened. I ended up doing it like this, but I think it would be neater if you could subclass from google.cloud.logging.handlers.transports.background_thread import BackgroundThreadTransport, _Worker
def my_enqueue(
self, record, message, resource=None, labels=None, trace=None, span_id=None):
entry = {
"info": {
"message": message, "python_logger": record.name
},
"severity": record.levelname,
"resource": resource,
"labels": labels,
"trace": trace,
"span_id": span_id,
"timestamp": datetime.utcfromtimestamp(record.created),
}
if record.levelno >= logging.WARNING:
entry["info"]["context"] = {"reportLocation": {"filePath": record.filename,
"lineNumber": record.lineno,
"functionName": record.funcName}}
entry["info"]["serviceContext"] = {"service": "my_service", "version": "1"}
entry["info"]["@type"] = "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent"
self._queue.put_nowait(entry)
# monkeypatch enqueue function
_Worker.enqueue = my_enqueue
cloud_handler = CloudLoggingHandler(client,
transport=BackgroundThreadTransport,
name=...,
resource=...,
labels=...) |
thanks @crazystick, that almost worked for me... but I'm seeing logs from the background thread itself in my logs (ie. "submitted 1 logs" etc.) Is there any way to exclude that logger? google.cloud.logging.handlers.transports.background_thread |
There is a EXCLUDED_LOGGER_DEFAULTS = ("google.cloud", "google.auth", "google_auth_httplib2")
# don't propagate excluded loggers (and don't send them to stderr either)
for logger_name in EXCLUDED_LOGGER_DEFAULTS:
logging.getLogger(logger_name).propagate = False
logging.getLogger(logger_name).addHandler(logging.NullHandler())
# add cloud handler to root logger
logging.getLogger().addHandler(cloud_handler) |
Based on @crazystick 's answer, I use the following bodge to log custom objects as info payload. It derives more from the original from google.cloud.logging import _helpers
from google.cloud.logging.handlers.transports.background_thread import _Worker
def my_enqueue(self, record, message, resource=None, labels=None, trace=None, span_id=None):
queue_entry = {
"info": {"message": message, "python_logger": record.name},
"severity": _helpers._normalize_severity(record.levelno),
"resource": resource,
"labels": labels,
"trace": trace,
"span_id": span_id,
"timestamp": datetime.datetime.utcfromtimestamp(record.created),
}
if 'custom_fields' in record:
entry['info']['custom_fields'] = record.custom_fields
self._queue.put_nowait(queue_entry)
_Worker.enqueue = my_enqueue Then import logging
from google.cloud import logging as google_logging
logger = logging.getLogger('my_log_client')
logger.addHandler(CloudLoggingHandler(google_logging.Client(), 'my_log_client'))
logger.info('hello', extra={'custom_fields':{'foo': 1, 'bar':{'tzar':3}}}) Which then makes it much easier to filter according to these custom_fields. Naturally, it would be great if this would be naively supported - thanks in advance! |
I tried to implement @Voyz solution on a Cloud Function and I couldn't make it work. def my_enqueue(
self, record, message, resource=None, labels=None, trace=None, span_id=None
):
queue_entry = {
"info": {
"message": message,
"python_logger": record.name,
"error_fields": record.error_fields,
},
"severity": _helpers._normalize_severity(record.levelno),
"resource": resource,
"labels": labels,
"trace": trace,
"span_id": span_id,
"timestamp": datetime.datetime.utcfromtimestamp(record.created),
}
self._queue.put_nowait(queue_entry)
_Worker.enqueue = my_enqueue And the function... def index(request):
logger = logging.getLogger("my_log_client")
handler = CloudLoggingHandler(
logger_client, name="my_log_client", transport=BackgroundThreadTransport
)
logger.setLevel(logging.INFO)
logger.addHandler(handler)
logging.info("hello")
logging.info("hello", extra={"custom_one": 1})
logger.info("hello", extra={"error_fields": {"foo": 1, "bar": {"tzar": 3}}})
return ("", 202) I would expect some error if |
EDIT TO UPDATE, I've moved the gist into a repo and uploaded it to pypi:
ORIGINAL for historical purposes: I've extended @Voyz / @crazystick 's work, and built a pre-canned gist
Its used like so: import google_structlog
google_structlog.setup(log_name="here-is-mylilapp")
# Now you can use structlog to get searchable json details in stackdriver...
import structlog
logger = structlog.get_logger()
logger.error("Uhoh, something bad did", moreinfo="it was bad", years_back_luck=5)
# Of course, you can still use plain ol' logging stdlib to get { "message": ... } objects
import logging
logger = logging.getLogger("yoyo")
logger.error("Regular logging calls will work happily too")
# Now you can search stackdriver with the query:
# logName: 'here-is-mylilapp' Which will produce a nice structured / searchable log inside GCP stackdriver: Main difference with what @Voyz had posted is its somewhat structlog specific, and doesn't care what sort of extra values you pass. It also includes all the pre-canned bells and whistles to run out of the box, and shows you what to search for in GCP so you can work backwards to something that works for you but is working by default. If there's interest, I can polish it a little further and put something up on pypi. |
Seems like this would be something obvious and cough idiomatic to be included in this library. Stack Driver doesn't looks so hot next to Sentry without this capability out of the box. |
Yeah, its a little frustrating to have "structured logging" with no simple ability to get in on it and inject your own domain-specific fields. That's where structured starts paying for its "not as easy to scan" downsides. The system provided structure fields are sort of 🤷♂️ compared to domain specific info that's super easy to create in code, and frustratingly hard to get into the log analysis tools in GCP. |
In my case I'm deploying to GKE, so I needed to modify the GKE handler. I went ahead and copied the approach used by Sentry for extracting the "extra" dictionary, and incorporated things like the python file and line number into a separate dict as well. Works for now. Perhaps @mauricepoirrier , you need to monkeypatch the ContainerEngineHandler like I did, rather than modifying the transport, in order to get it to work with Cloud Functions? |
@numbsafari do you have an example of what you did to modify GKE handler? |
What's the solution here on cloud functions? Using the |
I dropped the gist into a repo here: Its uploaded to PyPi, so available as:
|
I'm not using cloud functions, so it'd be really cool if somebody who was could figure out how to make it work "least surprise" in that context too. I'm mostly using python apps running on GKE, for which this works reasonably well for me. Hopefully somebody at Google will take this off our hands, but until then I think starting with a repo and refining it with issues etc is probably warranted at this level of complexity. |
I took a look. It seems like most things the default stdout logger sends can be provided except for trace id. So it's not a huge deal -- the nodejs lib for gcp logging does it -- but it's not complete. |
@snickell your solution is not working for me on Google App Engine, runtime I really enjoy Some tests I made: import logging
import google_structlog
google_structlog.setup(setup_google=True)
logger = google_structlog.getLogger()
logger.info("Google setup 1")
logger = logging.getLogger()
logger.info("Google setup 2")
print("Google setup 3") Doing the proposed setup in here https://cloud.google.com/logging/docs/setup/python works, but not for structured data of course import google.cloud.logging
# Instantiates a client
client = google.cloud.logging.Client()
# Connects the logger to the root logging handler; by default this captures
# all logs at INFO level and higher
client.setup_logging()
logger = logging.getLogger(__name__)
logger.info("Google setup 4") |
Hi, I recently came across the issue with structured logging for Google Cloud. However when I ran it in app engine standard, nothing showed up in logs... 🤨 The official documentation says that everything logged to stdout will be send to Cloud Logging.
I used this example for writing structured logs, but nothing shows in the Cloud Logging Explorer. Did something change and the docs are no longer up-to-date? Any idea what could I be doing wrong? Thanks a lot! |
are we getting structured logging!? woohoo! |
This question here is not for a cloud function, there, you have to use google.cloud.logging instead of the stdlib Python logging. This question here is just about getting it to work in a local Python script, using the GOOGLE_APPLICATION_CREDENTIALS to connect to GCP and drop the logs there. |
google-cloud-logging version: 1.8.0
I want to send structured logs to stackdriver from Python using the standard logging module. You can integrate structured logging to the Python logging module with either https://github.com/madzak/python-json-logger or http://www.structlog.org/en/stable/. Would be great to preserve those structures as part of
jsonPayload
when sent to stackdriver. Currently, these get stringified and sent asjsonPayload.message
instead.The text was updated successfully, but these errors were encountered: