diff --git a/ApplicationLibCode/Application/RiaPreferencesOpenTelemetry.cpp b/ApplicationLibCode/Application/RiaPreferencesOpenTelemetry.cpp index e5f813d860..76bdce9ae8 100644 --- a/ApplicationLibCode/Application/RiaPreferencesOpenTelemetry.cpp +++ b/ApplicationLibCode/Application/RiaPreferencesOpenTelemetry.cpp @@ -56,6 +56,8 @@ RiaPreferencesOpenTelemetry::RiaPreferencesOpenTelemetry() CAF_PDM_InitField( &m_memoryThresholdMb, "memoryThresholdMb", 50, "Memory Threshold (MB)" ); CAF_PDM_InitField( &m_samplingRate, "samplingRate", 1.0, "Sampling Rate" ); CAF_PDM_InitField( &m_connectionTimeoutMs, "connectionTimeoutMs", 10000, "Connection Timeout (ms)" ); + CAF_PDM_InitField( &m_eventAllowlist, "eventAllowlist", QString(), "Event Allowlist" ); + CAF_PDM_InitField( &m_eventDenylist, "eventDenylist", QString(), "Event Denylist" ); setFieldStates(); } @@ -105,6 +107,14 @@ void RiaPreferencesOpenTelemetry::setData( const std::map& key { m_connectionTimeoutMs = value.toInt(); } + else if ( key == "event_allowlist" ) + { + m_eventAllowlist = value; + } + else if ( key == "event_denylist" ) + { + m_eventDenylist = value; + } else { RiaLogging::warning( QString( "Unknown OpenTelemetry config key: '%1'" ).arg( key ) ); @@ -151,6 +161,8 @@ void RiaPreferencesOpenTelemetry::defineUiOrdering( QString uiConfigName, caf::P group->add( &m_memoryThresholdMb ); group->add( &m_samplingRate ); group->add( &m_connectionTimeoutMs ); + group->add( &m_eventAllowlist ); + group->add( &m_eventDenylist ); } uiOrdering.skipRemainingFields(); } @@ -234,3 +246,19 @@ RiaPreferencesOpenTelemetry::LoggingState RiaPreferencesOpenTelemetry::loggingSt { return m_loggingState(); } + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QStringList RiaPreferencesOpenTelemetry::eventAllowlist() const +{ + return m_eventAllowlist().split( ',', Qt::SkipEmptyParts ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QStringList RiaPreferencesOpenTelemetry::eventDenylist() const +{ + return m_eventDenylist().split( ',', Qt::SkipEmptyParts ); +} diff --git a/ApplicationLibCode/Application/RiaPreferencesOpenTelemetry.h b/ApplicationLibCode/Application/RiaPreferencesOpenTelemetry.h index cdc90b8a8d..c2f2e9c87d 100644 --- a/ApplicationLibCode/Application/RiaPreferencesOpenTelemetry.h +++ b/ApplicationLibCode/Application/RiaPreferencesOpenTelemetry.h @@ -64,6 +64,8 @@ class RiaPreferencesOpenTelemetry : public caf::PdmObject double samplingRate() const; int connectionTimeoutMs() const; LoggingState loggingState() const; + QStringList eventAllowlist() const; + QStringList eventDenylist() const; protected: void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; @@ -81,4 +83,6 @@ class RiaPreferencesOpenTelemetry : public caf::PdmObject caf::PdmField m_memoryThresholdMb; caf::PdmField m_samplingRate; caf::PdmField m_connectionTimeoutMs; + caf::PdmField m_eventAllowlist; + caf::PdmField m_eventDenylist; }; \ No newline at end of file diff --git a/ApplicationLibCode/Application/Tools/Cloud/RiaConnectorTools.cpp b/ApplicationLibCode/Application/Tools/Cloud/RiaConnectorTools.cpp index 00fab17817..aa45c88ce4 100644 --- a/ApplicationLibCode/Application/Tools/Cloud/RiaConnectorTools.cpp +++ b/ApplicationLibCode/Application/Tools/Cloud/RiaConnectorTools.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -141,6 +142,14 @@ std::map RiaConnectorTools::readKeyValuePairs( const QString& { valueStr = value.toBool() ? "true" : "false"; } + else if ( value.isArray() ) + { + QJsonArray arr = value.toArray(); + QStringList parts; + for ( const auto& elem : arr ) + if ( elem.isString() ) parts << elem.toString(); + valueStr = parts.join( "," ); + } keyValuePairs[it.key()] = valueStr; } diff --git a/ApplicationLibCode/Application/Tools/Telemetry/RiaOpenTelemetryManager.cpp b/ApplicationLibCode/Application/Tools/Telemetry/RiaOpenTelemetryManager.cpp index b36ce71eeb..1b8c5cd3b9 100644 --- a/ApplicationLibCode/Application/Tools/Telemetry/RiaOpenTelemetryManager.cpp +++ b/ApplicationLibCode/Application/Tools/Telemetry/RiaOpenTelemetryManager.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -223,20 +224,59 @@ static std::string hashUsername( const std::string& username ) return hash.toHex().toStdString(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RiaOpenTelemetryManager::isEventAllowed( const std::string& eventName ) const +{ + // Crash events are always reported regardless of filter settings + if ( eventName.starts_with( "crash." ) ) return true; + + auto* prefs = RiaPreferencesOpenTelemetry::current(); + if ( !prefs ) return true; + + const QString name = QString::fromStdString( eventName ); + + auto matches = []( const QString& name, const QStringList& patterns ) -> bool + { + for ( const QString& pattern : patterns ) + { + QRegularExpression re( QRegularExpression::wildcardToRegularExpression( pattern.trimmed() ) ); + if ( re.match( name ).hasMatch() ) return true; + } + return false; + }; + + const QStringList allowlist = prefs->eventAllowlist(); + if ( !allowlist.isEmpty() && !matches( name, allowlist ) ) return false; + + const QStringList denylist = prefs->eventDenylist(); + if ( !denylist.isEmpty() && matches( name, denylist ) ) return false; + + return true; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RiaOpenTelemetryManager::reportEventAsync( const std::string& eventName, const std::map& attributes ) { - if ( !isEnabled() || isCircuitBreakerOpen() ) + const bool isCrashEvent = eventName.starts_with( "crash." ); + + if ( !isCrashEvent && ( !isEnabled() || isCircuitBreakerOpen() ) ) + { + return; + } + + if ( !isEventAllowed( eventName ) ) { return; } std::unique_lock lock( m_queueMutex ); - // Check queue size and apply backpressure - if ( m_backpressureEnabled && m_eventQueue.size() >= m_maxQueueSize ) + // Check queue size and apply backpressure (crash events always bypass the queue limit) + if ( !isCrashEvent && m_backpressureEnabled && m_eventQueue.size() >= m_maxQueueSize ) { m_healthMetrics.eventsDropped++; return; @@ -246,7 +286,6 @@ void RiaOpenTelemetryManager::reportEventAsync( const std::string& eventName, co // Note: crash events already have real username added in reportCrash() std::map enrichedAttributes = attributes; - bool isCrashEvent = ( eventName == "crash.signal_handler" ); if ( !isCrashEvent ) { std::lock_guard configLock( m_configMutex ); diff --git a/ApplicationLibCode/Application/Tools/Telemetry/RiaOpenTelemetryManager.h b/ApplicationLibCode/Application/Tools/Telemetry/RiaOpenTelemetryManager.h index 11c014471a..f80a74b045 100644 --- a/ApplicationLibCode/Application/Tools/Telemetry/RiaOpenTelemetryManager.h +++ b/ApplicationLibCode/Application/Tools/Telemetry/RiaOpenTelemetryManager.h @@ -145,6 +145,9 @@ class RiaOpenTelemetryManager : public QObject bool isCircuitBreakerOpen() const; void resetCircuitBreaker(); + // Event filtering + bool isEventAllowed( const std::string& eventName ) const; + // Health monitoring void updateHealthMetrics( bool success ); void sendHealthSpan(); diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index cc09345ba8..f60b3a9fae 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -131,6 +131,7 @@ set(SOURCE_UNITTEST_FILES ${CMAKE_CURRENT_LIST_DIR}/RimMockSummaryCase.h ${CMAKE_CURRENT_LIST_DIR}/RimMockSummaryCase-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RimSummaryCalculation-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RiaConnectorTools-Test.cpp ) if(RESINSIGHT_ENABLE_GRPC) diff --git a/ApplicationLibCode/UnitTests/RiaConnectorTools-Test.cpp b/ApplicationLibCode/UnitTests/RiaConnectorTools-Test.cpp new file mode 100644 index 0000000000..20e7aa8933 --- /dev/null +++ b/ApplicationLibCode/UnitTests/RiaConnectorTools-Test.cpp @@ -0,0 +1,181 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2026- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "gtest/gtest.h" + +#include "Cloud/RiaConnectorTools.h" + +#include +#include + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaConnectorToolsTest, readKeyValuePairs_nonExistentFile ) +{ + auto result = RiaConnectorTools::readKeyValuePairs( "/this/path/does/not/exist.json" ); + EXPECT_TRUE( result.empty() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaConnectorToolsTest, readKeyValuePairs_emptyFile ) +{ + QTemporaryFile file; + ASSERT_TRUE( file.open() ); + + auto result = RiaConnectorTools::readKeyValuePairs( file.fileName() ); + EXPECT_TRUE( result.empty() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaConnectorToolsTest, readKeyValuePairs_stringValue ) +{ + QTemporaryFile file; + ASSERT_TRUE( file.open() ); + + { + QTextStream out( &file ); + out << R"({"connection_string": "InstrumentationKey=abc123"})"; + } + + auto result = RiaConnectorTools::readKeyValuePairs( file.fileName() ); + ASSERT_EQ( 1u, result.size() ); + EXPECT_EQ( QString( "InstrumentationKey=abc123" ), result["connection_string"] ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaConnectorToolsTest, readKeyValuePairs_numericValue ) +{ + QTemporaryFile file; + ASSERT_TRUE( file.open() ); + + { + QTextStream out( &file ); + out << R"({"batch_timeout_ms": 5000, "sampling_rate": 0.5})"; + } + + auto result = RiaConnectorTools::readKeyValuePairs( file.fileName() ); + ASSERT_EQ( 2u, result.size() ); + EXPECT_EQ( QString( "5000" ), result["batch_timeout_ms"] ); + EXPECT_EQ( QString( "0.5" ), result["sampling_rate"] ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaConnectorToolsTest, readKeyValuePairs_boolValue ) +{ + QTemporaryFile file; + ASSERT_TRUE( file.open() ); + + { + QTextStream out( &file ); + out << R"({"enabled": true, "verbose": false})"; + } + + auto result = RiaConnectorTools::readKeyValuePairs( file.fileName() ); + ASSERT_EQ( 2u, result.size() ); + EXPECT_EQ( QString( "true" ), result["enabled"] ); + EXPECT_EQ( QString( "false" ), result["verbose"] ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaConnectorToolsTest, readKeyValuePairs_arrayValue ) +{ + QTemporaryFile file; + ASSERT_TRUE( file.open() ); + + { + QTextStream out( &file ); + out << R"({"event_allowlist": ["app.started", "crash.*", "grpc.*"]})"; + } + + auto result = RiaConnectorTools::readKeyValuePairs( file.fileName() ); + ASSERT_EQ( 1u, result.size() ); + EXPECT_EQ( QString( "app.started,crash.*,grpc.*" ), result["event_allowlist"] ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaConnectorToolsTest, readKeyValuePairs_emptyArray ) +{ + QTemporaryFile file; + ASSERT_TRUE( file.open() ); + + { + QTextStream out( &file ); + out << R"({"event_denylist": []})"; + } + + auto result = RiaConnectorTools::readKeyValuePairs( file.fileName() ); + ASSERT_EQ( 1u, result.size() ); + EXPECT_EQ( QString( "" ), result["event_denylist"] ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaConnectorToolsTest, readKeyValuePairs_arrayIgnoresNonStringElements ) +{ + QTemporaryFile file; + ASSERT_TRUE( file.open() ); + + { + QTextStream out( &file ); + out << R"({"mixed_array": ["health.status", 42, true, "test.*"]})"; + } + + auto result = RiaConnectorTools::readKeyValuePairs( file.fileName() ); + ASSERT_EQ( 1u, result.size() ); + EXPECT_EQ( QString( "health.status,test.*" ), result["mixed_array"] ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaConnectorToolsTest, readKeyValuePairs_mixedTypes ) +{ + QTemporaryFile file; + ASSERT_TRUE( file.open() ); + + { + QTextStream out( &file ); + out << R"({ + "connection_string": "InstrumentationKey=key1", + "max_batch_size": 512, + "enabled": true, + "event_denylist": ["health.status", "test.*"] + })"; + } + + auto result = RiaConnectorTools::readKeyValuePairs( file.fileName() ); + ASSERT_EQ( 4u, result.size() ); + EXPECT_EQ( QString( "InstrumentationKey=key1" ), result["connection_string"] ); + EXPECT_EQ( QString( "512" ), result["max_batch_size"] ); + EXPECT_EQ( QString( "true" ), result["enabled"] ); + EXPECT_EQ( QString( "health.status,test.*" ), result["event_denylist"] ); +}