From df9701c3576e966e296931e87243430a1d2f4e22 Mon Sep 17 00:00:00 2001 From: Timothy Pansino <11214426+TimPansino@users.noreply.github.com> Date: Mon, 20 Mar 2023 12:44:17 -0700 Subject: [PATCH] Add GraphQL Introspection Setting (#783) * Add graphql introspection setting * Sort settings object hierarchy * Add test for introspection queries setting * Expand introspection queries testing * [Mega-Linter] Apply linters fixes * Adjust introspection detection for graphql --------- Co-authored-by: TimPansino --- newrelic/core/config.py | 70 +++++++++++-------- newrelic/hooks/framework_graphql.py | 6 +- tests/framework_ariadne/test_application.py | 21 ++++-- tests/framework_graphene/test_application.py | 29 +++++--- tests/framework_graphql/test_application.py | 41 +++++++---- .../framework_strawberry/test_application.py | 39 +++++++---- 6 files changed, 136 insertions(+), 70 deletions(-) diff --git a/newrelic/core/config.py b/newrelic/core/config.py index 72c4de03d2..80e9ccec05 100644 --- a/newrelic/core/config.py +++ b/newrelic/core/config.py @@ -334,6 +334,14 @@ def _can_enable_infinite_tracing(): return True +class InstrumentationSettings(Settings): + pass + + +class InstrumentationGraphQLSettings(Settings): + pass + + class EventHarvestConfigSettings(Settings): nested = True _lock = threading.Lock() @@ -355,50 +363,52 @@ class EventHarvestConfigHarvestLimitSettings(Settings): _settings = TopLevelSettings() +_settings.agent_limits = AgentLimitsSettings() _settings.application_logging = ApplicationLoggingSettings() _settings.application_logging.forwarding = ApplicationLoggingForwardingSettings() -_settings.application_logging.metrics = ApplicationLoggingMetricsSettings() _settings.application_logging.local_decorating = ApplicationLoggingLocalDecoratingSettings() +_settings.application_logging.metrics = ApplicationLoggingMetricsSettings() _settings.attributes = AttributesSettings() -_settings.gc_runtime_metrics = GCRuntimeMetricsSettings() -_settings.code_level_metrics = CodeLevelMetricsSettings() -_settings.thread_profiler = ThreadProfilerSettings() -_settings.transaction_tracer = TransactionTracerSettings() -_settings.transaction_tracer.attributes = TransactionTracerAttributesSettings() -_settings.error_collector = ErrorCollectorSettings() -_settings.error_collector.attributes = ErrorCollectorAttributesSettings() _settings.browser_monitoring = BrowserMonitorSettings() _settings.browser_monitoring.attributes = BrowserMonitorAttributesSettings() -_settings.transaction_name = TransactionNameSettings() -_settings.transaction_metrics = TransactionMetricsSettings() -_settings.event_loop_visibility = EventLoopVisibilitySettings() -_settings.rum = RumSettings() -_settings.slow_sql = SlowSqlSettings() -_settings.agent_limits = AgentLimitsSettings() +_settings.code_level_metrics = CodeLevelMetricsSettings() _settings.console = ConsoleSettings() -_settings.debug = DebugSettings() _settings.cross_application_tracer = CrossApplicationTracerSettings() -_settings.transaction_events = TransactionEventsSettings() -_settings.transaction_events.attributes = TransactionEventsAttributesSettings() _settings.custom_insights_events = CustomInsightsEventsSettings() -_settings.process_host = ProcessHostSettings() -_settings.synthetics = SyntheticsSettings() -_settings.message_tracer = MessageTracerSettings() -_settings.utilization = UtilizationSettings() -_settings.strip_exception_messages = StripExceptionMessageSettings() _settings.datastore_tracer = DatastoreTracerSettings() -_settings.datastore_tracer.instance_reporting = DatastoreTracerInstanceReportingSettings() _settings.datastore_tracer.database_name_reporting = DatastoreTracerDatabaseNameReportingSettings() +_settings.datastore_tracer.instance_reporting = DatastoreTracerInstanceReportingSettings() +_settings.debug = DebugSettings() +_settings.distributed_tracing = DistributedTracingSettings() +_settings.error_collector = ErrorCollectorSettings() +_settings.error_collector.attributes = ErrorCollectorAttributesSettings() +_settings.event_harvest_config = EventHarvestConfigSettings() +_settings.event_harvest_config.harvest_limits = EventHarvestConfigHarvestLimitSettings() +_settings.event_loop_visibility = EventLoopVisibilitySettings() +_settings.gc_runtime_metrics = GCRuntimeMetricsSettings() _settings.heroku = HerokuSettings() +_settings.infinite_tracing = InfiniteTracingSettings() +_settings.instrumentation = InstrumentationSettings() +_settings.instrumentation.graphql = InstrumentationGraphQLSettings() +_settings.message_tracer = MessageTracerSettings() +_settings.process_host = ProcessHostSettings() +_settings.rum = RumSettings() +_settings.serverless_mode = ServerlessModeSettings() +_settings.slow_sql = SlowSqlSettings() _settings.span_events = SpanEventSettings() _settings.span_events.attributes = SpanEventAttributesSettings() +_settings.strip_exception_messages = StripExceptionMessageSettings() +_settings.synthetics = SyntheticsSettings() +_settings.thread_profiler = ThreadProfilerSettings() +_settings.transaction_events = TransactionEventsSettings() +_settings.transaction_events.attributes = TransactionEventsAttributesSettings() +_settings.transaction_metrics = TransactionMetricsSettings() +_settings.transaction_name = TransactionNameSettings() _settings.transaction_segments = TransactionSegmentSettings() _settings.transaction_segments.attributes = TransactionSegmentAttributesSettings() -_settings.distributed_tracing = DistributedTracingSettings() -_settings.serverless_mode = ServerlessModeSettings() -_settings.infinite_tracing = InfiniteTracingSettings() -_settings.event_harvest_config = EventHarvestConfigSettings() -_settings.event_harvest_config.harvest_limits = EventHarvestConfigHarvestLimitSettings() +_settings.transaction_tracer = TransactionTracerSettings() +_settings.transaction_tracer.attributes = TransactionTracerAttributesSettings() +_settings.utilization = UtilizationSettings() _settings.log_file = os.environ.get("NEW_RELIC_LOG", None) @@ -735,6 +745,10 @@ def default_host(license_key): _settings.infinite_tracing.ssl = True _settings.infinite_tracing.span_queue_size = _environ_as_int("NEW_RELIC_INFINITE_TRACING_SPAN_QUEUE_SIZE", 10000) +_settings.instrumentation.graphql.capture_introspection_queries = os.environ.get( + "NEW_RELIC_INSTRUMENTATION_GRAPHQL_CAPTURE_INTROSPECTION_QUERIES", False +) + _settings.event_harvest_config.harvest_limits.analytic_event_data = _environ_as_int( "NEW_RELIC_ANALYTICS_EVENTS_MAX_SAMPLES_STORED", DEFAULT_RESERVOIR_SIZE ) diff --git a/newrelic/hooks/framework_graphql.py b/newrelic/hooks/framework_graphql.py index 378b714b8b..30d8a2e193 100644 --- a/newrelic/hooks/framework_graphql.py +++ b/newrelic/hooks/framework_graphql.py @@ -105,6 +105,7 @@ def bind_operation_v2(exe_context, operation, root_value): def wrap_execute_operation(wrapped, instance, args, kwargs): transaction = current_transaction() trace = current_trace() + settings = transaction.settings if not transaction: return wrapped(*args, **kwargs) @@ -135,8 +136,9 @@ def wrap_execute_operation(wrapped, instance, args, kwargs): if operation.selection_set is not None: fields = operation.selection_set.selections # Ignore transactions for introspection queries - for field in fields: - if get_node_value(field, "name") in GRAPHQL_INTROSPECTION_FIELDS: + if not settings.instrumentation.graphql.capture_introspection_queries: + # If all selected fields are introspection fields + if all(get_node_value(field, "name") in GRAPHQL_INTROSPECTION_FIELDS for field in fields): ignore_transaction() fragments = execution_context.fragments diff --git a/tests/framework_ariadne/test_application.py b/tests/framework_ariadne/test_application.py index 0f16da4491..cf8501a7af 100644 --- a/tests/framework_ariadne/test_application.py +++ b/tests/framework_ariadne/test_application.py @@ -13,7 +13,7 @@ # limitations under the License. import pytest -from testing_support.fixtures import dt_enabled +from testing_support.fixtures import dt_enabled, override_application_settings from testing_support.validators.validate_span_events import validate_span_events from testing_support.validators.validate_transaction_count import ( validate_transaction_count, @@ -520,8 +520,17 @@ def _test(): _test() -@validate_transaction_count(0) -@background_task() -def test_ignored_introspection_transactions(app, graphql_run): - ok, response = graphql_run(app, "{ __schema { types { name } } }") - assert ok and not response.get("errors") +@pytest.mark.parametrize("capture_introspection_setting", (True, False)) +def test_introspection_transactions(app, graphql_run, capture_introspection_setting): + txn_ct = 1 if capture_introspection_setting else 0 + + @override_application_settings( + {"instrumentation.graphql.capture_introspection_queries": capture_introspection_setting} + ) + @validate_transaction_count(txn_ct) + @background_task() + def _test(): + ok, response = graphql_run(app, "{ __schema { types { name } } }") + assert ok and not response.get("errors") + + _test() diff --git a/tests/framework_graphene/test_application.py b/tests/framework_graphene/test_application.py index b9d374a3ce..fd02d992a9 100644 --- a/tests/framework_graphene/test_application.py +++ b/tests/framework_graphene/test_application.py @@ -14,13 +14,17 @@ import pytest import six -from testing_support.fixtures import dt_enabled +from testing_support.fixtures import dt_enabled, override_application_settings 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_errors import validate_transaction_errors -from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics +from testing_support.validators.validate_transaction_errors import ( + validate_transaction_errors, +) +from testing_support.validators.validate_transaction_metrics import ( + validate_transaction_metrics, +) from newrelic.api.background_task import background_task from newrelic.common.object_names import callable_name @@ -510,8 +514,17 @@ def _test(): _test() -@validate_transaction_count(0) -@background_task() -def test_ignored_introspection_transactions(app, graphql_run): - response = graphql_run(app, "{ __schema { types { name } } }") - assert not response.errors +@pytest.mark.parametrize("capture_introspection_setting", (True, False)) +def test_introspection_transactions(app, graphql_run, capture_introspection_setting): + txn_ct = 1 if capture_introspection_setting else 0 + + @override_application_settings( + {"instrumentation.graphql.capture_introspection_queries": capture_introspection_setting} + ) + @validate_transaction_count(txn_ct) + @background_task() + def _test(): + response = graphql_run(app, "{ __schema { types { name } } }") + assert not response.errors + + _test() diff --git a/tests/framework_graphql/test_application.py b/tests/framework_graphql/test_application.py index 56dc3a7382..65d8cee3a5 100644 --- a/tests/framework_graphql/test_application.py +++ b/tests/framework_graphql/test_application.py @@ -13,14 +13,20 @@ # limitations under the License. import pytest -from testing_support.fixtures import dt_enabled +from testing_support.fixtures import dt_enabled, override_application_settings +from testing_support.validators.validate_code_level_metrics import ( + validate_code_level_metrics, +) 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_code_level_metrics import validate_code_level_metrics -from testing_support.validators.validate_transaction_errors import validate_transaction_errors -from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics +from testing_support.validators.validate_transaction_errors import ( + validate_transaction_errors, +) +from testing_support.validators.validate_transaction_metrics import ( + validate_transaction_metrics, +) from newrelic.api.background_task import background_task from newrelic.common.object_names import callable_name @@ -99,9 +105,9 @@ def test_basic(app, graphql_run): ) @background_task() def _test(): - response = graphql_run(app, '{ hello }') + response = graphql_run(app, "{ hello }") assert not response.errors - + _test() @@ -376,9 +382,7 @@ def test_operation_metrics_and_attrs(app, graphql_run): @validate_span_events(exact_agents=operation_attrs) @background_task() def _test(): - response = graphql_run( - app, "query MyQuery { library(index: 0) { branch, book { id, name } } }" - ) + response = graphql_run(app, "query MyQuery { library(index: 0) { branch, book { id, name } } }") assert not response.errors _test() @@ -507,8 +511,17 @@ def _test(): _test() -@validate_transaction_count(0) -@background_task() -def test_ignored_introspection_transactions(app, graphql_run): - response = graphql_run(app, "{ __schema { types { name } } }") - assert not response.errors +@pytest.mark.parametrize("capture_introspection_setting", (True, False)) +def test_introspection_transactions(app, graphql_run, capture_introspection_setting): + txn_ct = 1 if capture_introspection_setting else 0 + + @override_application_settings( + {"instrumentation.graphql.capture_introspection_queries": capture_introspection_setting} + ) + @validate_transaction_count(txn_ct) + @background_task() + def _test(): + response = graphql_run(app, "{ __schema { types { name } } }") + assert not response.errors + + _test() diff --git a/tests/framework_strawberry/test_application.py b/tests/framework_strawberry/test_application.py index d57de74f44..ac60a33e0c 100644 --- a/tests/framework_strawberry/test_application.py +++ b/tests/framework_strawberry/test_application.py @@ -13,11 +13,17 @@ # limitations under the License. import pytest -from testing_support.fixtures import dt_enabled +from testing_support.fixtures import dt_enabled, override_application_settings from testing_support.validators.validate_span_events import validate_span_events -from testing_support.validators.validate_transaction_errors import validate_transaction_errors -from testing_support.validators.validate_transaction_metrics import validate_transaction_metrics -from testing_support.validators.validate_transaction_count import validate_transaction_count +from testing_support.validators.validate_transaction_count import ( + validate_transaction_count, +) +from testing_support.validators.validate_transaction_errors import ( + validate_transaction_errors, +) +from testing_support.validators.validate_transaction_metrics import ( + validate_transaction_metrics, +) from newrelic.api.background_task import background_task from newrelic.common.object_names import callable_name @@ -61,12 +67,12 @@ def delay_import(): return delay_import -def example_middleware(next, root, info, **args): #pylint: disable=W0622 +def example_middleware(next, root, info, **args): # pylint: disable=W0622 return_value = next(root, info, **args) return return_value -def error_middleware(next, root, info, **args): #pylint: disable=W0622 +def error_middleware(next, root, info, **args): # pylint: disable=W0622 raise RuntimeError("Runtime Error!") @@ -248,7 +254,7 @@ def test_exception_in_validation(app, graphql_run, is_graphql_2, query, exc_clas exc_class = callable_name(GraphQLError) _test_exception_scoped_metrics = [ - ('GraphQL/operation/Strawberry///', 1), + ("GraphQL/operation/Strawberry///", 1), ] _test_exception_rollup_metrics = [ ("Errors/all", 1), @@ -434,8 +440,17 @@ def _test(): _test() -@validate_transaction_count(0) -@background_task() -def test_ignored_introspection_transactions(app, graphql_run): - response = graphql_run(app, "{ __schema { types { name } } }") - assert not response.errors +@pytest.mark.parametrize("capture_introspection_setting", (True, False)) +def test_introspection_transactions(app, graphql_run, capture_introspection_setting): + txn_ct = 1 if capture_introspection_setting else 0 + + @override_application_settings( + {"instrumentation.graphql.capture_introspection_queries": capture_introspection_setting} + ) + @validate_transaction_count(txn_ct) + @background_task() + def _test(): + response = graphql_run(app, "{ __schema { types { name } } }") + assert not response.errors + + _test()