-
Notifications
You must be signed in to change notification settings - Fork 133
Initial Hybrid Agent Trace implementation #1587
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
base: develop-hybrid-core-tracing
Are you sure you want to change the base?
Changes from all commits
ae0cb31
acc1607
b01ac4b
b45a7c8
bf075f1
c33134e
daa433f
9e03321
e2bf015
40a58f7
abf1a31
22f1906
04ceb65
08fed22
17aa20f
2aafac3
f8d6cf7
98b6285
ea958bb
ee66602
5deedb8
9d270d2
fbe86e2
af6a853
f7369dd
1bdd6f0
377ad06
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| # Copyright 2010 New Relic, Inc. | ||
| # | ||
| # 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 logging | ||
|
|
||
| from newrelic.api.application import application_instance | ||
| from newrelic.api.time_trace import add_custom_span_attribute, current_trace | ||
| from newrelic.api.transaction import Sentinel, current_transaction | ||
| from newrelic.common.object_wrapper import wrap_function_wrapper | ||
| from newrelic.common.signature import bind_args | ||
| from newrelic.core.config import global_settings | ||
|
|
||
| _logger = logging.getLogger(__name__) | ||
| _TRACER_PROVIDER = None | ||
|
|
||
| ########################################### | ||
| # Trace Instrumentation | ||
| ########################################### | ||
|
|
||
|
|
||
| def wrap_set_tracer_provider(wrapped, instance, args, kwargs): | ||
| settings = global_settings() | ||
|
|
||
| if not settings.otel_bridge.enabled: | ||
| return wrapped(*args, **kwargs) | ||
|
|
||
| global _TRACER_PROVIDER | ||
|
|
||
| if _TRACER_PROVIDER is None: | ||
| bound_args = bind_args(wrapped, args, kwargs) | ||
| tracer_provider = bound_args.get("tracer_provider") | ||
| _TRACER_PROVIDER = tracer_provider | ||
| else: | ||
| _logger.warning("TracerProvider has already been set.") | ||
|
|
||
|
|
||
| def wrap_get_tracer_provider(wrapped, instance, args, kwargs): | ||
| settings = global_settings() | ||
|
|
||
| if not settings.otel_bridge.enabled: | ||
| return wrapped(*args, **kwargs) | ||
|
|
||
| # This needs to act as a singleton, like the agent instance. | ||
| # We should initialize the agent here as well, if there is | ||
| # not an instance already. | ||
| application = application_instance(activate=False) | ||
| if not application or (application and not application.active): | ||
| application_instance().activate() | ||
|
|
||
| global _TRACER_PROVIDER | ||
|
|
||
| if _TRACER_PROVIDER is None: | ||
| from newrelic.api.opentelemetry import TracerProvider | ||
|
|
||
| hybrid_agent_tracer_provider = TracerProvider("hybrid_agent_tracer_provider") | ||
| _TRACER_PROVIDER = hybrid_agent_tracer_provider | ||
| return _TRACER_PROVIDER | ||
|
|
||
|
|
||
| def wrap_get_current_span(wrapped, instance, args, kwargs): | ||
| transaction = current_transaction() | ||
| trace = current_trace() | ||
|
|
||
| # If a NR trace does not exist (aside from the Sentinel | ||
| # trace), return the original function's result. | ||
| if not transaction or isinstance(trace, Sentinel): | ||
| return wrapped(*args, **kwargs) | ||
|
|
||
| # Do not allow the wrapper to continue if | ||
| # the Hybrid Agent setting is not enabled | ||
| settings = transaction.settings or global_settings() | ||
|
|
||
| if not settings.otel_bridge.enabled: | ||
| return wrapped(*args, **kwargs) | ||
|
|
||
| # If a NR trace does exist, check to see if the current | ||
| # OTel span corresponds to the current NR trace. If so, | ||
| # return the original function's result. | ||
| span = wrapped(*args, **kwargs) | ||
|
|
||
| if span.get_span_context().span_id == int(trace.guid, 16): | ||
| return span | ||
|
|
||
| # If the current OTel span does not match the current NR | ||
| # trace, this means that a NR trace was created either | ||
| # manually or through the NR agent. Either way, the OTel | ||
| # API was not used to create a span object. The Hybrid | ||
| # Agent's Span object creates a NR trace but since the NR | ||
| # trace has already been created, we just need a symbolic | ||
| # OTel span to represent it the span object. A LazySpan | ||
| # will be created. It will effectively be a NonRecordingSpan | ||
| # with the ability to add custom attributes. | ||
|
|
||
| from opentelemetry import trace as otel_api_trace | ||
|
|
||
| class LazySpan(otel_api_trace.NonRecordingSpan): | ||
| def set_attribute(self, key, value): | ||
| add_custom_span_attribute(key, value) | ||
|
|
||
| def set_attributes(self, attributes): | ||
| for key, value in attributes.items(): | ||
| add_custom_span_attribute(key, value) | ||
|
|
||
| otel_tracestate_headers = None | ||
|
|
||
| span_context = otel_api_trace.SpanContext( | ||
| trace_id=int(transaction.trace_id, 16), | ||
| span_id=int(trace.guid, 16), | ||
| is_remote=span.get_span_context().is_remote, | ||
| trace_flags=otel_api_trace.TraceFlags(span.get_span_context().trace_flags), | ||
| trace_state=otel_api_trace.TraceState(otel_tracestate_headers), | ||
| ) | ||
|
|
||
| return LazySpan(span_context) | ||
|
|
||
|
|
||
| def wrap_start_internal_or_server_span(wrapped, instance, args, kwargs): | ||
| # We want to take the NR version of the context_carrier | ||
| # and put that into the attributes. Keep the original | ||
| # context_carrier intact. | ||
|
|
||
| # Do not allow the wrapper to continue if | ||
| # the Hybrid Agent setting is not enabled | ||
| settings = global_settings() | ||
|
|
||
| if not settings.otel_bridge.enabled: | ||
| return wrapped(*args, **kwargs) | ||
|
|
||
| bound_args = bind_args(wrapped, args, kwargs) | ||
| context_carrier = bound_args.get("context_carrier") | ||
| attributes = bound_args.get("attributes", {}) | ||
|
|
||
| if context_carrier: | ||
| if ("HTTP_HOST" in context_carrier) or ("http_version" in context_carrier): | ||
| # This is an HTTP request (WSGI, ASGI, or otherwise) | ||
| nr_environ = context_carrier.copy() | ||
| attributes["nr.http.headers"] = nr_environ | ||
|
|
||
| else: | ||
| nr_headers = context_carrier.copy() | ||
| attributes["nr.nonhttp.headers"] = nr_headers | ||
|
|
||
| bound_args["attributes"] = attributes | ||
|
|
||
| return wrapped(**bound_args) | ||
|
|
||
|
|
||
| def instrument_trace_api(module): | ||
| if hasattr(module, "set_tracer_provider"): | ||
| wrap_function_wrapper(module, "set_tracer_provider", wrap_set_tracer_provider) | ||
|
|
||
| if hasattr(module, "get_tracer_provider"): | ||
| wrap_function_wrapper(module, "get_tracer_provider", wrap_get_tracer_provider) | ||
|
|
||
| if hasattr(module, "get_current_span"): | ||
| wrap_function_wrapper(module, "get_current_span", wrap_get_current_span) | ||
|
|
||
|
|
||
| def instrument_utils(module): | ||
| if hasattr(module, "_start_internal_or_server_span"): | ||
| wrap_function_wrapper(module, "_start_internal_or_server_span", wrap_start_internal_or_server_span) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| # Copyright 2010 New Relic, Inc. | ||
| # | ||
| # 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 pytest | ||
| from opentelemetry import trace | ||
| from testing_support.fixtures import collector_agent_registration_fixture, collector_available_fixture | ||
|
|
||
| from newrelic.api.opentelemetry import TracerProvider | ||
|
|
||
| _default_settings = { | ||
| "package_reporting.enabled": False, # Turn off package reporting for testing as it causes slow downs. | ||
| "transaction_tracer.explain_threshold": 0.0, | ||
| "transaction_tracer.transaction_threshold": 0.0, | ||
| "transaction_tracer.stack_trace_threshold": 0.0, | ||
| "debug.log_data_collector_payloads": True, | ||
| "debug.record_transaction_failure": True, | ||
| "otel_bridge.enabled": True, | ||
| } | ||
|
|
||
| collector_agent_registration = collector_agent_registration_fixture( | ||
| app_name="Python Agent Test (Hybrid Agent)", default_settings=_default_settings | ||
| ) | ||
|
|
||
| @pytest.fixture(scope="session") | ||
| def tracer(): | ||
| trace_provider = TracerProvider() | ||
| trace.set_tracer_provider(trace_provider) | ||
|
|
||
| return trace.get_tracer(__name__) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| # Copyright 2010 New Relic, Inc. | ||
| # | ||
| # 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. | ||
|
|
||
| from opentelemetry import trace as otel_api_trace | ||
| from testing_support.validators.validate_error_event_attributes import validate_error_event_attributes | ||
| from testing_support.validators.validate_span_events import validate_span_events | ||
| from testing_support.validators.validate_transaction_count import validate_transaction_count | ||
| from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics | ||
|
|
||
| from newrelic.api.application import application_instance | ||
| from newrelic.api.background_task import BackgroundTask | ||
| from newrelic.api.function_trace import FunctionTrace | ||
| from newrelic.api.time_trace import current_trace | ||
| from newrelic.api.transaction import current_transaction | ||
|
|
||
|
|
||
| # Does not create segment without a transaction | ||
| @validate_transaction_count(0) | ||
| def test_does_not_create_segment_without_a_transaction(tracer): | ||
| with tracer.start_as_current_span(name="Bar", kind=otel_api_trace.SpanKind.INTERNAL): | ||
| # The OpenTelmetry span should not be created | ||
| assert otel_api_trace.get_current_span() == otel_api_trace.INVALID_SPAN | ||
|
|
||
| # There should be no transaction | ||
| assert not current_transaction() | ||
|
|
||
|
|
||
| # Creates OpenTelemetry segment in a transaction | ||
| @validate_transaction_metrics(name="Foo", background_task=True) | ||
| @validate_span_events( | ||
| exact_intrinsics={"name": "Function/Bar", "category": "generic"}, expected_intrinsics=("parentId",) | ||
| ) | ||
| @validate_span_events(exact_intrinsics={"name": "Function/Foo", "category": "generic", "nr.entryPoint": True}) | ||
| def test_creates_opentelemetry_segment_in_a_transaction(tracer): | ||
| application = application_instance(activate=False) | ||
|
|
||
| with BackgroundTask(application, name="Foo"): | ||
| with tracer.start_as_current_span(name="Bar", kind=otel_api_trace.SpanKind.INTERNAL): | ||
| # OpenTelemetry API and New Relic API report the same traceId | ||
| assert otel_api_trace.get_current_span().get_span_context().trace_id == int( | ||
| current_transaction()._trace_id, 16 | ||
| ) | ||
|
|
||
| # OpenTelemetry API and New Relic API report the same spanId | ||
| assert otel_api_trace.get_current_span().get_span_context().span_id == int(current_trace().guid, 16) | ||
|
|
||
|
|
||
| # Creates New Relic span as child of OpenTelemetry span | ||
| @validate_transaction_metrics(name="Foo", background_task=True) | ||
| @validate_span_events( | ||
| exact_intrinsics={"name": "Function/Baz", "category": "generic"}, expected_intrinsics=("parentId",) | ||
| ) | ||
| @validate_span_events( | ||
| exact_intrinsics={"name": "Function/Bar", "category": "generic"}, expected_intrinsics=("parentId",) | ||
| ) | ||
| @validate_span_events(exact_intrinsics={"name": "Function/Foo", "category": "generic"}) | ||
| def test_creates_new_relic_span_as_child_of_open_telemetry_span(tracer): | ||
| application = application_instance(activate=False) | ||
|
|
||
| with BackgroundTask(application, name="Foo"): | ||
| with tracer.start_as_current_span(name="Bar", kind=otel_api_trace.SpanKind.INTERNAL): | ||
| with FunctionTrace(name="Baz"): | ||
| # OpenTelemetry API and New Relic API report the same traceId | ||
| assert otel_api_trace.get_current_span().get_span_context().trace_id == int( | ||
| current_transaction().trace_id, 16 | ||
| ) | ||
|
|
||
| # OpenTelemetry API and New Relic API report the same spanId | ||
| assert otel_api_trace.get_current_span().get_span_context().span_id == int(current_trace().guid, 16) | ||
|
|
||
|
|
||
| # OpenTelemetry API can add custom attributes to spans | ||
| @validate_transaction_metrics(name="Foo", background_task=True) | ||
| @validate_span_events(exact_intrinsics={"name": "Function/Baz"}, exact_users={"spanNumber": 2}) | ||
| @validate_span_events(exact_intrinsics={"name": "Function/Bar"}, exact_users={"spanNumber": 1}) | ||
| def test_opentelemetry_api_can_add_custom_attributes_to_spans(tracer): | ||
| application = application_instance(activate=False) | ||
|
|
||
| with BackgroundTask(application, name="Foo"): | ||
| with tracer.start_as_current_span(name="Bar", kind=otel_api_trace.SpanKind.INTERNAL): | ||
| with FunctionTrace(name="Baz"): | ||
| otel_api_trace.get_current_span().set_attribute("spanNumber", 2) | ||
|
|
||
| otel_api_trace.get_current_span().set_attribute("spanNumber", 1) | ||
|
|
||
|
|
||
| # OpenTelemetry API can record errors | ||
| @validate_transaction_metrics(name="Foo", background_task=True) | ||
| @validate_error_event_attributes( | ||
| exact_attrs={"agent": {}, "intrinsic": {"error.message": "Test exception message"}, "user": {}} | ||
|
Contributor
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 think there should be an error.class attribute on here too right?
Contributor
Author
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. For these cross agent tests, I just used what was in the JSON files, but once I work more on the status functionality, I will have more tests for errors as well. |
||
| ) | ||
| @validate_span_events(exact_intrinsics={"name": "Function/Bar"}) | ||
| def test_opentelemetry_api_can_record_errors(tracer): | ||
| application = application_instance(activate=False) | ||
|
|
||
| with BackgroundTask(application, name="Foo"): | ||
| with tracer.start_as_current_span(name="Bar", kind=otel_api_trace.SpanKind.INTERNAL): | ||
| try: | ||
| raise Exception("Test exception message") | ||
| except Exception as e: | ||
| otel_api_trace.get_current_span().record_exception(e) | ||
|
Contributor
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 have a test anywhere for making sure we don't do anything if hybrid agent is disabled?
Contributor
Author
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 was going to include that in the PR where I change the setting names, but I can do that in this one too (since it's literally just one test) |
||
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.
General question: are there cross agent tests for this? Seems like we might want some...
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.
Yes, and the tests in this file are directly from those. I have not finished the script to generate these on the fly for if/when they change, so I left the original JSON out of this PR. I'll just put it into the next PR anyway to make it clear where these came from