Skip to content

Commit 48df656

Browse files
authored
Merge branch 'main' into pymemcache_instruments_version_bump
2 parents 1cb28a6 + c60a7e4 commit 48df656

File tree

15 files changed

+519
-7
lines changed

15 files changed

+519
-7
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.9.1-0.28b1...HEAD)
99

10+
- `opentelemetry-instrumentation-wsgi` Capture custom request/response headers in span attributes
11+
([#925])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/925)
12+
13+
- `opentelemetry-instrumentation-flask` Flask: Capture custom request/response headers in span attributes
14+
([#952])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/952)
15+
1016
### Added
1117

18+
- `opentelemetry-instrumentation-sqlalchemy` added experimental sql commenter capability
19+
([#924](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/924))
1220
- `opentelemetry-instrumentation-dbapi` add experimental sql commenter capability
1321
([#908](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/908))
1422
- `opentelemetry-instrumentation-requests` make span attribute available to samplers

instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ def process_response(self, request, response):
275275
add_response_attributes(
276276
span,
277277
f"{response.status_code} {response.reason_phrase}",
278-
response,
278+
response.items(),
279279
)
280280

281281
propagator = get_global_response_propagator()

instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ def _start_response(status, response_headers, *args, **kwargs):
153153
otel_wsgi.add_response_attributes(
154154
span, status, response_headers
155155
)
156+
if span.kind == trace.SpanKind.SERVER:
157+
otel_wsgi.add_custom_response_headers(
158+
span, response_headers
159+
)
156160
else:
157161
_logger.warning(
158162
"Flask environ's OpenTelemetry span "
@@ -200,6 +204,10 @@ def _before_request():
200204
] = flask.request.url_rule.rule
201205
for key, value in attributes.items():
202206
span.set_attribute(key, value)
207+
if span.kind == trace.SpanKind.SERVER:
208+
otel_wsgi.add_custom_request_headers(
209+
span, flask_request_environ
210+
)
203211

204212
activation = trace.use_span(span, end_on_exit=True)
205213
activation.__enter__() # pylint: disable=E1101

instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from flask import Response
1516
from werkzeug.test import Client
1617
from werkzeug.wrappers import BaseResponse
1718

@@ -23,6 +24,16 @@ def _hello_endpoint(helloid):
2324
raise ValueError(":-(")
2425
return "Hello: " + str(helloid)
2526

27+
@staticmethod
28+
def _custom_response_headers():
29+
resp = Response("test response")
30+
resp.headers["content-type"] = "text/plain; charset=utf-8"
31+
resp.headers["content-length"] = "13"
32+
resp.headers[
33+
"my-custom-header"
34+
] = "my-custom-value-1,my-custom-header-2"
35+
return resp
36+
2637
def _common_initialization(self):
2738
def excluded_endpoint():
2839
return "excluded"
@@ -35,6 +46,9 @@ def excluded2_endpoint():
3546
self.app.route("/excluded/<int:helloid>")(self._hello_endpoint)
3647
self.app.route("/excluded")(excluded_endpoint)
3748
self.app.route("/excluded2")(excluded2_endpoint)
49+
self.app.route("/test_custom_response_headers")(
50+
self._custom_response_headers
51+
)
3852

3953
# pylint: disable=attribute-defined-outside-init
4054
self.client = Client(self.app, BaseResponse)

instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,3 +442,101 @@ def test_mark_span_internal_in_presence_of_span_from_other_framework(self):
442442
self.assertEqual(
443443
span_list[0].parent.span_id, span_list[1].context.span_id
444444
)
445+
446+
447+
class TestCustomRequestResponseHeaders(
448+
InstrumentationTest, TestBase, WsgiTestBase
449+
):
450+
def setUp(self):
451+
super().setUp()
452+
453+
self.env_patch = patch.dict(
454+
"os.environ",
455+
{
456+
"OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST": "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3",
457+
"OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE": "content-type,content-length,my-custom-header,invalid-header",
458+
},
459+
)
460+
self.env_patch.start()
461+
self.app = Flask(__name__)
462+
FlaskInstrumentor().instrument_app(self.app)
463+
464+
self._common_initialization()
465+
466+
def tearDown(self):
467+
super().tearDown()
468+
self.env_patch.stop()
469+
with self.disable_logging():
470+
FlaskInstrumentor().uninstrument_app(self.app)
471+
472+
def test_custom_request_header_added_in_server_span(self):
473+
headers = {
474+
"Custom-Test-Header-1": "Test Value 1",
475+
"Custom-Test-Header-2": "TestValue2,TestValue3",
476+
}
477+
resp = self.client.get("/hello/123", headers=headers)
478+
self.assertEqual(200, resp.status_code)
479+
span = self.memory_exporter.get_finished_spans()[0]
480+
expected = {
481+
"http.request.header.custom_test_header_1": ("Test Value 1",),
482+
"http.request.header.custom_test_header_2": (
483+
"TestValue2,TestValue3",
484+
),
485+
}
486+
self.assertEqual(span.kind, trace.SpanKind.SERVER)
487+
self.assertSpanHasAttributes(span, expected)
488+
489+
def test_custom_request_header_not_added_in_internal_span(self):
490+
tracer = trace.get_tracer(__name__)
491+
with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER):
492+
headers = {
493+
"Custom-Test-Header-1": "Test Value 1",
494+
"Custom-Test-Header-2": "TestValue2,TestValue3",
495+
}
496+
resp = self.client.get("/hello/123", headers=headers)
497+
self.assertEqual(200, resp.status_code)
498+
span = self.memory_exporter.get_finished_spans()[0]
499+
not_expected = {
500+
"http.request.header.custom_test_header_1": ("Test Value 1",),
501+
"http.request.header.custom_test_header_2": (
502+
"TestValue2,TestValue3",
503+
),
504+
}
505+
self.assertEqual(span.kind, trace.SpanKind.INTERNAL)
506+
for key, _ in not_expected.items():
507+
self.assertNotIn(key, span.attributes)
508+
509+
def test_custom_response_header_added_in_server_span(self):
510+
resp = self.client.get("/test_custom_response_headers")
511+
self.assertEqual(resp.status_code, 200)
512+
span = self.memory_exporter.get_finished_spans()[0]
513+
expected = {
514+
"http.response.header.content_type": (
515+
"text/plain; charset=utf-8",
516+
),
517+
"http.response.header.content_length": ("13",),
518+
"http.response.header.my_custom_header": (
519+
"my-custom-value-1,my-custom-header-2",
520+
),
521+
}
522+
self.assertEqual(span.kind, trace.SpanKind.SERVER)
523+
self.assertSpanHasAttributes(span, expected)
524+
525+
def test_custom_response_header_not_added_in_internal_span(self):
526+
tracer = trace.get_tracer(__name__)
527+
with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER):
528+
resp = self.client.get("/test_custom_response_headers")
529+
self.assertEqual(resp.status_code, 200)
530+
span = self.memory_exporter.get_finished_spans()[0]
531+
not_expected = {
532+
"http.response.header.content_type": (
533+
"text/plain; charset=utf-8",
534+
),
535+
"http.response.header.content_length": ("13",),
536+
"http.response.header.my_custom_header": (
537+
"my-custom-value-1,my-custom-header-2",
538+
),
539+
}
540+
self.assertEqual(span.kind, trace.SpanKind.INTERNAL)
541+
for key, _ in not_expected.items():
542+
self.assertNotIn(key, span.attributes)

instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ def trace_tween(request):
161161
otel_wsgi.add_response_attributes(
162162
span,
163163
response_or_exception.status,
164-
response_or_exception.headers,
164+
response_or_exception.headerlist,
165165
)
166166

167167
propagator = get_global_response_propagator()

instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def _instrument(self, **kwargs):
106106
return EngineTracer(
107107
_get_tracer(kwargs.get("engine"), tracer_provider),
108108
kwargs.get("engine"),
109+
kwargs.get("enable_commenter", False),
109110
)
110111
return None
111112

instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717

1818
from opentelemetry import trace
1919
from opentelemetry.instrumentation.sqlalchemy.version import __version__
20+
from opentelemetry.instrumentation.utils import (
21+
_generate_opentelemetry_traceparent,
22+
_generate_sql_comment,
23+
)
2024
from opentelemetry.semconv.trace import NetTransportValues, SpanAttributes
25+
from opentelemetry.trace import Span
2126
from opentelemetry.trace.status import Status, StatusCode
2227

2328

@@ -70,12 +75,15 @@ def _wrap_create_engine_internal(func, module, args, kwargs):
7075

7176

7277
class EngineTracer:
73-
def __init__(self, tracer, engine):
78+
def __init__(self, tracer, engine, enable_commenter=False):
7479
self.tracer = tracer
7580
self.engine = engine
7681
self.vendor = _normalize_vendor(engine.name)
82+
self.enable_commenter = enable_commenter
7783

78-
listen(engine, "before_cursor_execute", self._before_cur_exec)
84+
listen(
85+
engine, "before_cursor_execute", self._before_cur_exec, retval=True
86+
)
7987
listen(engine, "after_cursor_execute", _after_cur_exec)
8088
listen(engine, "handle_error", _handle_error)
8189

@@ -115,6 +123,18 @@ def _before_cur_exec(
115123
span.set_attribute(key, value)
116124

117125
context._otel_span = span
126+
if self.enable_commenter:
127+
statement = statement + EngineTracer._generate_comment(span=span)
128+
129+
return statement, params
130+
131+
@staticmethod
132+
def _generate_comment(span: Span) -> str:
133+
span_context = span.get_span_context()
134+
meta = {}
135+
if span_context.is_valid:
136+
meta.update(_generate_opentelemetry_traceparent(span))
137+
return _generate_sql_comment(**meta)
118138

119139

120140
# pylint: disable=unused-argument

instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import asyncio
15+
import logging
1516
from unittest import mock
1617

1718
import pytest
@@ -20,12 +21,17 @@
2021

2122
from opentelemetry import trace
2223
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
24+
from opentelemetry.instrumentation.sqlalchemy.engine import EngineTracer
2325
from opentelemetry.sdk.resources import Resource
2426
from opentelemetry.sdk.trace import TracerProvider, export
2527
from opentelemetry.test.test_base import TestBase
2628

2729

2830
class TestSqlalchemyInstrumentation(TestBase):
31+
@pytest.fixture(autouse=True)
32+
def inject_fixtures(self, caplog):
33+
self.caplog = caplog # pylint: disable=attribute-defined-outside-init
34+
2935
def tearDown(self):
3036
super().tearDown()
3137
SQLAlchemyInstrumentor().uninstrument()
@@ -150,3 +156,22 @@ async def run():
150156
self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)
151157

152158
asyncio.get_event_loop().run_until_complete(run())
159+
160+
def test_generate_commenter(self):
161+
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
162+
engine = create_engine("sqlite:///:memory:")
163+
SQLAlchemyInstrumentor().instrument(
164+
engine=engine,
165+
tracer_provider=self.tracer_provider,
166+
enable_commenter=True,
167+
)
168+
169+
cnx = engine.connect()
170+
cnx.execute("SELECT 1 + 1;").fetchall()
171+
spans = self.memory_exporter.get_finished_spans()
172+
self.assertEqual(len(spans), 1)
173+
span = spans[0]
174+
self.assertIn(
175+
EngineTracer._generate_comment(span),
176+
self.caplog.records[-2].getMessage(),
177+
)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Copyright The 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 pytest
16+
from sqlalchemy import create_engine
17+
18+
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
19+
from opentelemetry.test.test_base import TestBase
20+
21+
22+
class TestSqlalchemyInstrumentationWithSQLCommenter(TestBase):
23+
@pytest.fixture(autouse=True)
24+
def inject_fixtures(self, caplog):
25+
self.caplog = caplog # pylint: disable=attribute-defined-outside-init
26+
27+
def tearDown(self):
28+
super().tearDown()
29+
SQLAlchemyInstrumentor().uninstrument()
30+
31+
def test_sqlcommenter_enabled(self):
32+
engine = create_engine("sqlite:///:memory:")
33+
SQLAlchemyInstrumentor().instrument(
34+
engine=engine,
35+
tracer_provider=self.tracer_provider,
36+
enable_commenter=True,
37+
)
38+
cnx = engine.connect()
39+
cnx.execute("SELECT 1;").fetchall()
40+
self.assertRegex(
41+
self.caplog.records[-2].getMessage(),
42+
r"SELECT 1; /\*traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/",
43+
)
44+
45+
def test_sqlcommenter_disabled(self):
46+
engine = create_engine("sqlite:///:memory:", echo=True)
47+
SQLAlchemyInstrumentor().instrument(
48+
engine=engine, tracer_provider=self.tracer_provider
49+
)
50+
cnx = engine.connect()
51+
cnx.execute("SELECT 1;").fetchall()
52+
53+
self.assertEqual(self.caplog.records[-2].getMessage(), "SELECT 1;")

0 commit comments

Comments
 (0)