Skip to content
This repository was archived by the owner on Oct 13, 2021. It is now read-only.

propagators: add trace-context validation service #5

Merged
merged 1 commit into from
Nov 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ jobs:
- *persist_to_workspace_step
- *save_cache_step

trace-context:
docker:
- *test_runner
resource_class: *resource_class
steps:
- checkout
- *restore_cache_step
- run: scripts/run-tox-scenario '^py..-trace-context'
- *persist_to_workspace_step
- *save_cache_step

boto:
docker:
- *test_runner
Expand Down Expand Up @@ -901,6 +912,9 @@ workflows:
- tracer:
requires:
- flake8
- trace-context:
requires:
- flake8
- unit_tests:
requires:
- flake8
Expand Down Expand Up @@ -960,6 +974,7 @@ workflows:
- test_logging
- tornado
- tracer
- trace-context
- unit_tests
- vertica
- deploy_dev:
Expand Down
88 changes: 86 additions & 2 deletions oteltrace/propagation/w3c.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,29 @@
from ..ext import priority


_KEY_WITHOUT_VENDOR_FORMAT = r'[a-z][_0-9a-z\-\*\/]{0,255}'
_KEY_WITH_VENDOR_FORMAT = (
r'[a-z][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}'
)

_KEY_FORMAT = _KEY_WITHOUT_VENDOR_FORMAT + '|' + _KEY_WITH_VENDOR_FORMAT
_VALUE_FORMAT = (
r'[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]'
)

_DELIMITER_FORMAT = '[ \t]*,[ \t]*'
_MEMBER_FORMAT = '({})(=)({})[ \t]*'.format(_KEY_FORMAT, _VALUE_FORMAT)

_DELIMITER_FORMAT_RE = re.compile(_DELIMITER_FORMAT)
_MEMBER_FORMAT_RE = re.compile(_MEMBER_FORMAT)

_TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS = 32


class W3CHTTPPropagator:
"""w3c compatible propagator"""
_TRACEPARENT_HEADER_NAME = 'traceparent'
_TRACESTATE_HEADER_NAME = 'tracestate'
_TRACEPARENT_HEADER_FORMAT = (
'^[ \t]*([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(-.*)?[ \t]*$'
)
Expand All @@ -29,6 +49,10 @@ def inject(self, span_context, headers):
)

headers[self._TRACEPARENT_HEADER_NAME] = traceparent_string
# is there is state in the context propagate it
if hasattr(span_context, 'tracestate'):
tracestate_string = _format_tracestate(span_context.tracestate)
headers[self._TRACESTATE_HEADER_NAME] = tracestate_string

def extract(self, headers):
if not headers:
Expand Down Expand Up @@ -57,7 +81,6 @@ def extract(self, headers):
trace_id = int(trace_id_str, 16)
span_id = int(span_id_str, 16)
trace_options = int(trace_options_str, 16)
# TODO: probably not needed
if trace_id == 0 or span_id == 0:
return Context()

Expand All @@ -68,8 +91,69 @@ def extract(self, headers):
else:
sampling_priority = priority.AUTO_REJECT

return Context(
tracestate_header = headers.get(self._TRACESTATE_HEADER_NAME)
tracestate = _parse_tracestate(tracestate_header)

ctx = Context(
trace_id=trace_id,
span_id=span_id,
sampling_priority=sampling_priority,
)

ctx.tracestate = tracestate

return ctx


def _parse_tracestate(header):
"""Parse one or more w3c tracestate header into a TraceState.

Args:
string: the value of the tracestate header.

Returns:
A valid TraceState that contains values extracted from
the tracestate header.

If the format of one headers is illegal, all values will
be discarded and an empty tracestate will be returned.

If the number of keys is beyond the maximum, all values
will be discarded and an empty tracestate will be returned.
"""
tracestate = {}
value_count = 0

if header is None:
return {}
for member in re.split(_DELIMITER_FORMAT_RE, header):
# empty members are valid, but no need to process further.
if not member:
continue
match = _MEMBER_FORMAT_RE.fullmatch(member)
if not match:
# TODO: log this?
return {}
key, _eq, value = match.groups()
if key in tracestate: # pylint:disable=E1135
# duplicate keys are not legal in
# the header, so we will remove
return {}
tracestate[key] = value
value_count += 1
if value_count > _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS:
return {}
return tracestate


def _format_tracestate(tracestate):
"""Parse a w3c tracestate header into a TraceState.

Args:
tracestate: the tracestate header to write

Returns:
A string that adheres to the w3c tracestate
header format.
"""
return ','.join(key + '=' + value for key, value in tracestate.items())
27 changes: 27 additions & 0 deletions tests/propagation/test_w3c.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/sh
set -e
pwd
# hard-coding the git tag to ensure stable builds.
TRACECONTEXT_GIT_TAG="98f210efd89c63593dce90e2bae0a1bdcb986f51"
# clone w3c tracecontext tests
mkdir -p target
rm -rf ./target/trace-context
git clone https://github.com/w3c/trace-context ./target/trace-context
cd ./target/trace-context && git checkout $TRACECONTEXT_GIT_TAG && cd -
# start example service, which propagates trace-context by default.
python tests/propagation/w3c_server_test.py 1>&2 &
EXAMPLE_SERVER_PID=$!
# give the app server a little time to start up. Not adding some sort
# of delay would cause many of the tracecontext tests to fail being
# unable to connect.
sleep 1
onshutdown()
{
# send a sigint, to ensure
# it is caught as a KeyboardInterrupt in the
# example service.
kill $EXAMPLE_SERVER_PID
}
trap onshutdown EXIT
cd ./target/trace-context/test
python test.py http://127.0.0.1:5000/verify-tracecontext
60 changes: 60 additions & 0 deletions tests/propagation/w3c_server_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
#
# Copyright 2019, OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This server is intended to be used with the W3C tracecontext validation
Service. It implements the APIs needed to be exercised by the test bed.
"""

from oteltrace import patch_all
patch_all()

import json # noqa: E402
import flask # noqa: E402
import requests # noqa: E402

from oteltrace.propagation.w3c import W3CHTTPPropagator # noqa: E402
from oteltrace import tracer # noqa: E402

tracer.configure(
http_propagator=W3CHTTPPropagator,
)

app = flask.Flask(__name__)


@app.route('/verify-tracecontext', methods=['POST'])
def verify_tracecontext():
"""Upon reception of some payload, sends a request back to the designated
url.

This route is designed to be testable with the w3c tracecontext server /
client test.
"""
for action in flask.request.json:
requests.post(
url=action['url'],
data=json.dumps(action['arguments']),
headers={
'Accept': 'application/json',
'Content-Type': 'application/json; charset=utf-8',
},
timeout=5.0,
)
return 'hello'


if __name__ == '__main__':
app.run(debug=True)
5 changes: 5 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ envlist =
{py34,py35,py36,py37}-oteltracerun
{py34,py35,py36,py37}-test_utils
{py34,py35,py36,py37}-test_logging
py37-trace-context
# Integrations environments
aiobotocore_contrib-py34-aiobotocore{02,03,04}
aiobotocore_contrib-{py35,py36}-aiobotocore{02,03,04,05,07,08,09,010}
Expand Down Expand Up @@ -331,6 +332,9 @@ deps =
tornado50: tornado>=5.0,<5.1
tornado51: tornado>=5.1,<5.2
tornado60: tornado>=6.0,<6.1
trace-context: aiohttp
trace-context: flask
trace-context: requests
vertica060: vertica-python>=0.6.0,<0.7.0
vertica070: vertica-python>=0.7.0,<0.8.0
webtest: WebTest
Expand All @@ -341,6 +345,7 @@ passenv=TEST_*
commands =
# run only essential tests related to the tracing client
tracer: pytest {posargs} --ignore="tests/contrib" --ignore="tests/commands" --ignore="tests/unit" --ignore="tests/internal" tests
trace-context: {toxinidir}/tests/propagation/test_w3c.sh
# run only the `oteltrace.internal` tests
internal: pytest {posargs} tests/internal
# Contribs
Expand Down