Skip to content
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

feat: support span inference #267

Merged
merged 12 commits into from May 3, 2021
72 changes: 48 additions & 24 deletions google/cloud/logging_v2/handlers/_helpers.py
Expand Up @@ -16,6 +16,7 @@

import math
import json
import re

try:
import flask
Expand Down Expand Up @@ -55,12 +56,13 @@ def get_request_data_from_flask():
"""Get http_request and trace data from flask request headers.

Returns:
Tuple[Optional[dict], Optional[str]]:
Data related to the current http request and the trace_id for the
request. Both fields will be None if a flask request isn't found.
Tuple[Optional[dict], Optional[str], Optional[str]]:
Data related to the current http request, trace_id, and span_id for
the request. All fields will be None if a django request isn't
found.
"""
if flask is None or not flask.request:
return None, None
return None, None, None

# build http_request
http_request = {
Expand All @@ -73,27 +75,26 @@ def get_request_data_from_flask():
"protocol": flask.request.environ.get(_PROTOCOL_HEADER),
}

# find trace id
trace_id = None
# find trace id and span id
header = flask.request.headers.get(_FLASK_TRACE_HEADER)
if header:
trace_id = header.split("/", 1)[0]
trace_id, span_id = _parse_trace_span(header)

return http_request, trace_id
return http_request, trace_id, span_id


def get_request_data_from_django():
"""Get http_request and trace data from django request headers.

Returns:
Tuple[Optional[dict], Optional[str]]:
Data related to the current http request and the trace_id for the
request. Both fields will be None if a django request isn't found.
Tuple[Optional[dict], Optional[str], Optional[str]]:
Data related to the current http request, trace_id, and span_id for
the request. All fields will be None if a django request isn't
found.
"""
request = _get_django_request()

if request is None:
return None, None
return None, None, None

# convert content_length to int if it exists
content_length = None
Expand All @@ -112,32 +113,55 @@ def get_request_data_from_django():
"protocol": request.META.get(_PROTOCOL_HEADER),
}

# find trace id
trace_id = None
# find trace id and span id
header = request.META.get(_DJANGO_TRACE_HEADER)
if header:
trace_id = header.split("/", 1)[0]
trace_id, span_id = _parse_trace_span(header)

return http_request, trace_id
return http_request, trace_id, span_id


def _parse_trace_span(header):
"""Given an X_CLOUD_TRACE header, extract the trace and span ids.

Args:
header (str): the string extracted from the X_CLOUD_TRACE header
Returns:
Tuple[Optional[dict], Optional[str]]:
The trace_id and span_id extracted from the header
Each field will be None if not found.
"""
trace_id = None
span_id = None
if header:
try:
split_header = header.split("/", 1)
trace_id = split_header[0]
header_suffix = split_header[1]
# the span is the set of alphanumeric characters after the /
span_id = re.findall(r"^\w+", header_suffix)[0]
except IndexError:
pass
return trace_id, span_id


def get_request_data():
"""Helper to get http_request and trace data from supported web
frameworks (currently supported: Flask and Django).

Returns:
Tuple[Optional[dict], Optional[str]]:
Data related to the current http request and the trace_id for the
request. Both fields will be None if a supported web request isn't found.
Tuple[Optional[dict], Optional[str], Optional[str]]:
Data related to the current http request, trace_id, and span_id for
the request. All fields will be None if a django request isn't
found.
"""
checkers = (
get_request_data_from_django,
get_request_data_from_flask,
)

for checker in checkers:
http_request, trace_id = checker()
http_request, trace_id, span_id = checker()
if http_request is not None:
return http_request, trace_id
return http_request, trace_id, span_id

return None, None
return None, None, None
4 changes: 2 additions & 2 deletions google/cloud/logging_v2/handlers/app_engine.py
Expand Up @@ -90,7 +90,7 @@ def get_gae_labels(self):
"""
gae_labels = {}

_, trace_id = get_request_data()
_, trace_id, _ = get_request_data()
if trace_id is not None:
gae_labels[_TRACE_ID_LABEL] = trace_id

Expand All @@ -107,7 +107,7 @@ def emit(self, record):
record (logging.LogRecord): The record to be logged.
"""
message = super(AppEngineHandler, self).format(record)
inferred_http, inferred_trace = get_request_data()
inferred_http, inferred_trace, _ = get_request_data()
if inferred_trace is not None:
inferred_trace = f"projects/{self.project_id}/traces/{inferred_trace}"
# allow user overrides
Expand Down
3 changes: 2 additions & 1 deletion google/cloud/logging_v2/handlers/handlers.py
Expand Up @@ -59,7 +59,7 @@ def filter(self, record):
}
record.msg = "" if record.msg is None else record.msg
# find http request data
inferred_http, inferred_trace = get_request_data()
inferred_http, inferred_trace, inferred_span = get_request_data()
if inferred_trace is not None and self.project is not None:
inferred_trace = f"projects/{self.project}/traces/{inferred_trace}"
# set labels
Expand All @@ -70,6 +70,7 @@ def filter(self, record):
)

record.trace = getattr(record, "trace", inferred_trace) or ""
record.span_id = getattr(record, "span_id", inferred_span) or ""
record.http_request = getattr(record, "http_request", inferred_http) or {}
record.request_method = record.http_request.get("requestMethod", "")
record.request_url = record.http_request.get("requestUrl", "")
Expand Down
1 change: 1 addition & 0 deletions google/cloud/logging_v2/handlers/structured_log.py
Expand Up @@ -24,6 +24,7 @@
'"severity": "%(levelname)s", '
'"logging.googleapis.com/labels": { %(total_labels_str)s }, '
'"logging.googleapis.com/trace": "%(trace)s", '
'"logging.googleapis.com/spanId": "%(span_id)s", '
'"logging.googleapis.com/sourceLocation": { "file": "%(file)s", "line": "%(line)d", "function": "%(function)s"}, '
'"httpRequest": {"requestMethod": "%(request_method)s", "requestUrl": "%(request_url)s", "userAgent": "%(user_agent)s", "protocol": "%(protocol)s"} }'
)
Expand Down
2 changes: 1 addition & 1 deletion tests/environment