-
Notifications
You must be signed in to change notification settings - Fork 705
Adding Correlation Context API and propagator #471
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
Changes from 18 commits
4d595f8
764ee23
868987d
b2bf9d5
13ddb5c
bba033e
b6fc6cf
495c840
2a732f7
8449360
2e813af
778d8f4
1bc5a83
270ddc2
54ac2cc
2c5d20f
19342ce
09617ef
4fd969f
39192ab
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 |
---|---|---|
@@ -0,0 +1,100 @@ | ||
# Copyright 2020, 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. | ||
|
||
import abc | ||
import typing | ||
|
||
from opentelemetry.context import get_value, set_value | ||
from opentelemetry.context.context import Context | ||
|
||
CORRELATION_CONTEXT_KEY = "correlation-context" | ||
|
||
|
||
def get_correlations( | ||
context: typing.Optional[Context] = None, | ||
) -> typing.Dict[str, object]: | ||
""" Returns the name/value pairs in the CorrelationContext | ||
|
||
Args: | ||
context: The Context to use. If not set, uses current Context | ||
|
||
Returns: | ||
Name/value pairs in the CorrelationContext | ||
""" | ||
correlations = get_value(CORRELATION_CONTEXT_KEY, context=context) | ||
if isinstance(correlations, dict): | ||
return correlations.copy() | ||
return {} | ||
|
||
|
||
def get_correlation( | ||
name: str, context: typing.Optional[Context] = None | ||
) -> typing.Optional[object]: | ||
""" Provides access to the value for a name/value pair in the CorrelationContext | ||
|
||
Args: | ||
name: The name of the value to retrieve | ||
context: The Context to use. If not set, uses current Context | ||
|
||
Returns: | ||
The value associated with the given name, or null if the given name is | ||
not present. | ||
""" | ||
return get_correlations(context=context).get(name) | ||
|
||
|
||
def set_correlation( | ||
name: str, value: object, context: typing.Optional[Context] = None | ||
) -> Context: | ||
"""Sets a value in the CorrelationContext | ||
|
||
Args: | ||
name: The name of the value to set | ||
value: The value to set | ||
context: The Context to use. If not set, uses current Context | ||
|
||
Returns: | ||
A Context with the value updated | ||
""" | ||
correlations = get_correlations(context=context) | ||
correlations[name] = value | ||
return set_value(CORRELATION_CONTEXT_KEY, correlations, context=context) | ||
|
||
|
||
def remove_correlation( | ||
name: str, context: typing.Optional[Context] = None | ||
) -> Context: | ||
"""Removes a value from the CorrelationContext | ||
Args: | ||
name: The name of the value to remove | ||
context: The Context to use. If not set, uses current Context | ||
|
||
Returns: | ||
A Context with the name/value removed | ||
""" | ||
correlations = get_correlations(context=context) | ||
correlations.pop(name, None) | ||
|
||
return set_value(CORRELATION_CONTEXT_KEY, correlations, context=context) | ||
|
||
|
||
def clear_correlations(context: typing.Optional[Context] = None) -> Context: | ||
"""Removes all values from the CorrelationContext | ||
Args: | ||
context: The Context to use. If not set, uses current Context | ||
|
||
Returns: | ||
A Context with all correlations removed | ||
""" | ||
return set_value(CORRELATION_CONTEXT_KEY, {}, context=context) |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,117 @@ | ||||||
# Copyright 2020, 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. | ||||||
# | ||||||
import re | ||||||
import typing | ||||||
import urllib.parse | ||||||
|
||||||
from opentelemetry import correlationcontext | ||||||
from opentelemetry.context import get_current | ||||||
from opentelemetry.context.context import Context | ||||||
from opentelemetry.trace.propagation import httptextformat | ||||||
|
||||||
|
||||||
class CorrelationContextPropagator(httptextformat.HTTPTextFormat): | ||||||
MAX_HEADER_LENGTH = 8192 | ||||||
MAX_PAIR_LENGTH = 4096 | ||||||
MAX_PAIRS = 180 | ||||||
_CORRELATION_CONTEXT_HEADER_NAME = "otcorrelationcontext" | ||||||
|
||||||
def extract( | ||||||
self, | ||||||
get_from_carrier: httptextformat.Getter[ | ||||||
httptextformat.HTTPTextFormatT | ||||||
], | ||||||
carrier: httptextformat.HTTPTextFormatT, | ||||||
context: typing.Optional[Context] = None, | ||||||
) -> Context: | ||||||
""" Extract CorrelationContext from the carrier. | ||||||
|
||||||
See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.extract` | ||||||
""" | ||||||
|
||||||
if not context: | ||||||
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.
Suggested change
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. do you mean 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. changed this to check for |
||||||
context = get_current() | ||||||
|
||||||
header = _extract_first_element( | ||||||
get_from_carrier(carrier, self._CORRELATION_CONTEXT_HEADER_NAME) | ||||||
) | ||||||
|
||||||
if not header or len(header) > self.MAX_HEADER_LENGTH: | ||||||
ocelotl marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
return context | ||||||
|
||||||
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. The draft of the W3C Correlation Context specification has some limits on the header: https://w3c.github.io/correlation-context/#header-value. Are those checks missing on purpose here? 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. they were not, added them. |
||||||
correlations = header.split(",") | ||||||
total_correlations = self.MAX_PAIRS | ||||||
for correlation in correlations: | ||||||
if total_correlations <= 0: | ||||||
return context | ||||||
total_correlations -= 1 | ||||||
if len(correlation) > self.MAX_PAIR_LENGTH: | ||||||
continue | ||||||
try: | ||||||
name, value = correlation.split("=", 1) | ||||||
except Exception: # pylint: disable=broad-except | ||||||
continue | ||||||
context = correlationcontext.set_correlation( | ||||||
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. It could be nice to have a function that receives a list (or dict) of key-value peers and adds them to the correlation context at once. It'll avoid to create that many temporal contexts that are overwritten in the next iteration of the loop. 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. i agree, there was nothing in the spec for this but it could definitely be something we implement. |
||||||
urllib.parse.unquote(name).strip(), | ||||||
urllib.parse.unquote(value).strip(), | ||||||
context=context, | ||||||
) | ||||||
|
||||||
return context | ||||||
|
||||||
def inject( | ||||||
self, | ||||||
set_in_carrier: httptextformat.Setter[httptextformat.HTTPTextFormatT], | ||||||
carrier: httptextformat.HTTPTextFormatT, | ||||||
context: typing.Optional[Context] = None, | ||||||
) -> None: | ||||||
"""Injects CorrelationContext into the carrier. | ||||||
|
||||||
See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject` | ||||||
""" | ||||||
correlations = correlationcontext.get_correlations(context=context) | ||||||
if not correlations: | ||||||
return | ||||||
|
||||||
correlation_context_string = _format_correlations(correlations) | ||||||
set_in_carrier( | ||||||
carrier, | ||||||
self._CORRELATION_CONTEXT_HEADER_NAME, | ||||||
correlation_context_string, | ||||||
) | ||||||
|
||||||
|
||||||
def _format_correlations(correlations: typing.Dict[str, object]) -> str: | ||||||
ocelotl marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
"""Format correlations into a string. | ||||||
|
||||||
Args: | ||||||
correlations: The correlations to format | ||||||
|
||||||
Returns: | ||||||
A string that adheres to the w3c correlationcontext | ||||||
header format. | ||||||
""" | ||||||
return ",".join( | ||||||
key + "=" + urllib.parse.quote_plus(str(value)) | ||||||
for key, value in correlations.items() | ||||||
) | ||||||
|
||||||
|
||||||
def _extract_first_element( | ||||||
items: typing.Iterable[httptextformat.HTTPTextFormatT], | ||||||
) -> typing.Optional[httptextformat.HTTPTextFormatT]: | ||||||
if items is None: | ||||||
return None | ||||||
return next(iter(items), None) |
This file was deleted.
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.
Can this be made private?
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.
Done