Skip to content

Commit 264e6c3

Browse files
codebotenocelotlc24t
authored
Adding Correlation Context API and propagator (#471)
This change removes Distributed Context and replaces it with the Correlations Context API. This change also adds the Correlation Context Propagator to the global httptextformat propagator. Fixes #416 Co-authored-by: Diego Hurtado <[email protected]> Co-authored-by: Chris Kleinknecht <[email protected]>
1 parent f52468b commit 264e6c3

File tree

12 files changed

+506
-362
lines changed

12 files changed

+506
-362
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Copyright 2020, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import abc
16+
import typing
17+
18+
from opentelemetry.context import get_value, set_value
19+
from opentelemetry.context.context import Context
20+
21+
_CORRELATION_CONTEXT_KEY = "correlation-context"
22+
23+
24+
def get_correlations(
25+
context: typing.Optional[Context] = None,
26+
) -> typing.Dict[str, object]:
27+
""" Returns the name/value pairs in the CorrelationContext
28+
29+
Args:
30+
context: The Context to use. If not set, uses current Context
31+
32+
Returns:
33+
Name/value pairs in the CorrelationContext
34+
"""
35+
correlations = get_value(_CORRELATION_CONTEXT_KEY, context=context)
36+
if isinstance(correlations, dict):
37+
return correlations.copy()
38+
return {}
39+
40+
41+
def get_correlation(
42+
name: str, context: typing.Optional[Context] = None
43+
) -> typing.Optional[object]:
44+
""" Provides access to the value for a name/value pair in the CorrelationContext
45+
46+
Args:
47+
name: The name of the value to retrieve
48+
context: The Context to use. If not set, uses current Context
49+
50+
Returns:
51+
The value associated with the given name, or null if the given name is
52+
not present.
53+
"""
54+
return get_correlations(context=context).get(name)
55+
56+
57+
def set_correlation(
58+
name: str, value: object, context: typing.Optional[Context] = None
59+
) -> Context:
60+
"""Sets a value in the CorrelationContext
61+
62+
Args:
63+
name: The name of the value to set
64+
value: The value to set
65+
context: The Context to use. If not set, uses current Context
66+
67+
Returns:
68+
A Context with the value updated
69+
"""
70+
correlations = get_correlations(context=context)
71+
correlations[name] = value
72+
return set_value(_CORRELATION_CONTEXT_KEY, correlations, context=context)
73+
74+
75+
def remove_correlation(
76+
name: str, context: typing.Optional[Context] = None
77+
) -> Context:
78+
"""Removes a value from the CorrelationContext
79+
Args:
80+
name: The name of the value to remove
81+
context: The Context to use. If not set, uses current Context
82+
83+
Returns:
84+
A Context with the name/value removed
85+
"""
86+
correlations = get_correlations(context=context)
87+
correlations.pop(name, None)
88+
89+
return set_value(_CORRELATION_CONTEXT_KEY, correlations, context=context)
90+
91+
92+
def clear_correlations(context: typing.Optional[Context] = None) -> Context:
93+
"""Removes all values from the CorrelationContext
94+
Args:
95+
context: The Context to use. If not set, uses current Context
96+
97+
Returns:
98+
A Context with all correlations removed
99+
"""
100+
return set_value(_CORRELATION_CONTEXT_KEY, {}, context=context)
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Copyright 2020, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
import re
16+
import typing
17+
import urllib.parse
18+
19+
from opentelemetry import correlationcontext
20+
from opentelemetry.context import get_current
21+
from opentelemetry.context.context import Context
22+
from opentelemetry.trace.propagation import httptextformat
23+
24+
25+
class CorrelationContextPropagator(httptextformat.HTTPTextFormat):
26+
MAX_HEADER_LENGTH = 8192
27+
MAX_PAIR_LENGTH = 4096
28+
MAX_PAIRS = 180
29+
_CORRELATION_CONTEXT_HEADER_NAME = "otcorrelationcontext"
30+
31+
def extract(
32+
self,
33+
get_from_carrier: httptextformat.Getter[
34+
httptextformat.HTTPTextFormatT
35+
],
36+
carrier: httptextformat.HTTPTextFormatT,
37+
context: typing.Optional[Context] = None,
38+
) -> Context:
39+
""" Extract CorrelationContext from the carrier.
40+
41+
See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.extract`
42+
"""
43+
44+
if context is None:
45+
context = get_current()
46+
47+
header = _extract_first_element(
48+
get_from_carrier(carrier, self._CORRELATION_CONTEXT_HEADER_NAME)
49+
)
50+
51+
if not header or len(header) > self.MAX_HEADER_LENGTH:
52+
return context
53+
54+
correlations = header.split(",")
55+
total_correlations = self.MAX_PAIRS
56+
for correlation in correlations:
57+
if total_correlations <= 0:
58+
return context
59+
total_correlations -= 1
60+
if len(correlation) > self.MAX_PAIR_LENGTH:
61+
continue
62+
try:
63+
name, value = correlation.split("=", 1)
64+
except Exception: # pylint: disable=broad-except
65+
continue
66+
context = correlationcontext.set_correlation(
67+
urllib.parse.unquote(name).strip(),
68+
urllib.parse.unquote(value).strip(),
69+
context=context,
70+
)
71+
72+
return context
73+
74+
def inject(
75+
self,
76+
set_in_carrier: httptextformat.Setter[httptextformat.HTTPTextFormatT],
77+
carrier: httptextformat.HTTPTextFormatT,
78+
context: typing.Optional[Context] = None,
79+
) -> None:
80+
"""Injects CorrelationContext into the carrier.
81+
82+
See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject`
83+
"""
84+
correlations = correlationcontext.get_correlations(context=context)
85+
if not correlations:
86+
return
87+
88+
correlation_context_string = _format_correlations(correlations)
89+
set_in_carrier(
90+
carrier,
91+
self._CORRELATION_CONTEXT_HEADER_NAME,
92+
correlation_context_string,
93+
)
94+
95+
96+
def _format_correlations(correlations: typing.Dict[str, object]) -> str:
97+
return ",".join(
98+
key + "=" + urllib.parse.quote_plus(str(value))
99+
for key, value in correlations.items()
100+
)
101+
102+
103+
def _extract_first_element(
104+
items: typing.Iterable[httptextformat.HTTPTextFormatT],
105+
) -> typing.Optional[httptextformat.HTTPTextFormatT]:
106+
if items is None:
107+
return None
108+
return next(iter(items), None)

opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py

Lines changed: 0 additions & 145 deletions
This file was deleted.

opentelemetry-api/src/opentelemetry/propagators/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ def example_route():
5858
import opentelemetry.trace as trace
5959
from opentelemetry.context import get_current
6060
from opentelemetry.context.context import Context
61+
from opentelemetry.correlationcontext.propagation import (
62+
CorrelationContextPropagator,
63+
)
64+
from opentelemetry.propagators import composite
6165
from opentelemetry.trace.propagation import httptextformat
6266
from opentelemetry.trace.propagation.tracecontexthttptextformat import (
6367
TraceContextHTTPTextFormat,
@@ -106,8 +110,8 @@ def inject(
106110
get_global_httptextformat().inject(set_in_carrier, carrier, context)
107111

108112

109-
_HTTP_TEXT_FORMAT = (
110-
TraceContextHTTPTextFormat()
113+
_HTTP_TEXT_FORMAT = composite.CompositeHTTPPropagator(
114+
[TraceContextHTTPTextFormat(), CorrelationContextPropagator()],
111115
) # type: httptextformat.HTTPTextFormat
112116

113117

0 commit comments

Comments
 (0)