Adds OpenTelemetry integration

Adds support for OpenTelemetry to enable telemetry and crash reporting.

This change introduces:
- New RiaPreferencesOpenTelemetry class for managing OpenTelemetry configuration.
- RiaOpenTelemetryManager class for handling the OpenTelemetry lifecycle,
  event processing, and error handling.
- Configuration options in preferences to enable, disable, and configure
  OpenTelemetry.
- Asynchronous event reporting.
- Crash reporting capabilities.
- Health monitoring features.
This commit is contained in:
Magne Sjaastad
2025-12-18 08:40:30 +01:00
parent b8141fbbe5
commit 6c11e53f21
7 changed files with 1284 additions and 7 deletions
@@ -27,6 +27,7 @@
#include "RiaLogging.h"
#include "RiaPreferencesGeoMech.h"
#include "RiaPreferencesGrid.h"
#include "RiaPreferencesOpenTelemetry.h"
#include "RiaPreferencesOpm.h"
#include "RiaPreferencesOsdu.h"
#include "RiaPreferencesSummary.h"
@@ -284,6 +285,9 @@ RiaPreferences::RiaPreferences()
caf::PdmUiPushButtonEditor::configureEditorLabelHidden( &m_deleteSumoToken );
m_deleteSumoToken.xmlCapability()->disableIO();
CAF_PDM_InitFieldNoDefault( &m_openTelemetryPreferences, "openTelemetryPreferences", "openTelemetryPreferences" );
m_openTelemetryPreferences = new RiaPreferencesOpenTelemetry;
CAF_PDM_InitFieldNoDefault( &m_importPreferences, "importPreferences", "Import From File" );
caf::PdmUiPushButtonEditor::configureEditorLabelHidden( &m_importPreferences );
@@ -522,6 +526,14 @@ void RiaPreferences::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering&
sumoGroup->setCollapsedByDefault();
m_sumoPreferences()->uiOrdering( uiConfigName, *sumoGroup );
sumoGroup->add( &m_deleteSumoToken );
caf::PdmUiGroup* openTelemetryGroup = uiOrdering.addNewGroup( "OpenTelemetry" );
openTelemetryGroup->setCollapsedByDefault();
#ifdef RESINSIGHT_OPENTELEMETRY_ENABLED
m_openTelemetryPreferences()->uiOrdering( uiConfigName, *openTelemetryGroup );
#else
openTelemetryGroup->addNewLabel( "OpenTelemetry support is not enabled in this build of ResInsight. " );
#endif
}
else if ( RiaApplication::enableDevelopmentFeatures() && uiConfigName == RiaPreferences::tabNameSystem() )
{
@@ -1078,6 +1090,14 @@ RiaPreferencesSumo* RiaPreferences::sumoPreferences() const
return m_sumoPreferences();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiaPreferencesOpenTelemetry* RiaPreferences::openTelemetryPreferences() const
{
return m_openTelemetryPreferences();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -47,6 +47,7 @@ class RiaPreferencesOsdu;
class RiaPreferencesGrid;
class RiaPreferencesSumo;
class RiaPreferencesOpm;
class RiaPreferencesOpenTelemetry;
//--------------------------------------------------------------------------------------------------
///
@@ -122,13 +123,14 @@ public:
bool storeBackupOfProjectFiles() const;
RiaPreferencesGeoMech* geoMechPreferences() const;
RiaPreferencesSummary* summaryPreferences() const;
RiaPreferencesSystem* systemPreferences() const;
RiaPreferencesOsdu* osduPreferences() const;
RiaPreferencesSumo* sumoPreferences() const;
RiaPreferencesGrid* gridPreferences() const;
RiaPreferencesOpm* opmPreferences() const;
RiaPreferencesGeoMech* geoMechPreferences() const;
RiaPreferencesSummary* summaryPreferences() const;
RiaPreferencesSystem* systemPreferences() const;
RiaPreferencesOsdu* osduPreferences() const;
RiaPreferencesSumo* sumoPreferences() const;
RiaPreferencesGrid* gridPreferences() const;
RiaPreferencesOpm* opmPreferences() const;
RiaPreferencesOpenTelemetry* openTelemetryPreferences() const;
void importPreferenceValuesFromFile( const QString& fileName );
void exportPreferenceValuesToFile( const QString& fileName );
@@ -241,6 +243,9 @@ private:
caf::PdmChildField<RiaPreferencesSumo*> m_sumoPreferences;
caf::PdmField<bool> m_deleteSumoToken;
// OpenTelemetry settings
caf::PdmChildField<RiaPreferencesOpenTelemetry*> m_openTelemetryPreferences;
// 3d view
caf::PdmField<caf::AppEnum<RiaDefines::MeshModeType>> m_defaultMeshModeType;
caf::PdmField<caf::AppEnum<RiaDefines::RINavigationPolicy>> m_navigationPolicy;
@@ -0,0 +1,234 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2025- 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RiaPreferencesOpenTelemetry.h"
#include "RiaApplication.h"
#include "RiaLogging.h"
#include "RiaPreferences.h"
#include "RiaVersionInfo.h"
#include "cafPdmUiTextEditor.h"
namespace caf
{
template <>
void RiaPreferencesOpenTelemetry::LoggingStateType::setUp()
{
addItem( RiaPreferencesOpenTelemetry::LoggingState::DISABLED, "DISABLED", "Disabled" );
addItem( RiaPreferencesOpenTelemetry::LoggingState::DEFAULT, "DEFAULT", "Default" );
addItem( RiaPreferencesOpenTelemetry::LoggingState::ALL, "ALL", "All" );
setDefault( RiaPreferencesOpenTelemetry::LoggingState::DEFAULT );
}
} // namespace caf
CAF_PDM_SOURCE_INIT( RiaPreferencesOpenTelemetry, "RiaPreferencesOpenTelemetry" );
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiaPreferencesOpenTelemetry::RiaPreferencesOpenTelemetry()
{
CAF_PDM_InitObject( "OpenTelemetry Configuration", "", "", "Configuration for OpenTelemetry crash reporting and telemetry" );
CAF_PDM_InitField( &m_loggingState, "loggingState", LoggingStateType( LoggingState::DISABLED ), "Logging State" );
CAF_PDM_InitField( &m_connectionString, "connectionString", QString(), "Azure Connection String" );
m_connectionString.uiCapability()->setUiEditorTypeName( caf::PdmUiTextEditor::uiEditorTypeName() );
CAF_PDM_InitField( &m_batchTimeoutMs, "batchTimeoutMs", 5000, "Batch Timeout (ms)" );
CAF_PDM_InitField( &m_maxBatchSize, "maxBatchSize", 512, "Max Batch Size" );
CAF_PDM_InitField( &m_maxQueueSize, "maxQueueSize", 10000, "Max Queue Size" );
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)" );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiaPreferencesOpenTelemetry::~RiaPreferencesOpenTelemetry()
{
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiaPreferencesOpenTelemetry* RiaPreferencesOpenTelemetry::current()
{
return RiaApplication::instance()->preferences()->openTelemetryPreferences();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaPreferencesOpenTelemetry::setData( const std::map<QString, QString>& keyValuePairs )
{
for ( const auto& [key, value] : keyValuePairs )
{
if ( key == "connection_string" )
{
m_connectionString = value;
}
else if ( key == "batch_timeout_ms" )
{
m_batchTimeoutMs = value.toInt();
}
else if ( key == "max_batch_size" )
{
m_maxBatchSize = value.toInt();
}
else if ( key == "max_queue_size" )
{
m_maxQueueSize = value.toInt();
}
else if ( key == "memory_threshold_mb" )
{
m_memoryThresholdMb = value.toInt();
}
else if ( key == "sampling_rate" )
{
m_samplingRate = value.toDouble();
}
else if ( key == "connection_timeout_ms" )
{
m_connectionTimeoutMs = value.toInt();
}
else
{
RiaLogging::warning( QString( "Unknown OpenTelemetry config key: '%1'" ).arg( key ) );
}
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaPreferencesOpenTelemetry::setFieldsReadOnly()
{
std::vector<caf::PdmFieldHandle*> fields = this->fields();
for ( auto field : fields )
{
// Keep logging state editable
if ( field == &m_loggingState )
{
continue;
}
field->uiCapability()->setUiReadOnly( true );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaPreferencesOpenTelemetry::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
{
uiOrdering.add( &m_loggingState );
// Only show configuration fields if not disabled
if ( m_loggingState() != LoggingState::DISABLED )
{
uiOrdering.add( &m_connectionString );
uiOrdering.add( &m_batchTimeoutMs );
uiOrdering.add( &m_maxBatchSize );
uiOrdering.add( &m_maxQueueSize );
uiOrdering.add( &m_memoryThresholdMb );
uiOrdering.add( &m_samplingRate );
uiOrdering.add( &m_connectionTimeoutMs );
}
uiOrdering.skipRemainingFields();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RiaPreferencesOpenTelemetry::serviceName() const
{
return QStringLiteral( "ResInsight" );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RiaPreferencesOpenTelemetry::serviceVersion() const
{
return QString( STRPRODUCTVER );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RiaPreferencesOpenTelemetry::connectionString() const
{
return m_connectionString;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
int RiaPreferencesOpenTelemetry::batchTimeoutMs() const
{
return m_batchTimeoutMs;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
int RiaPreferencesOpenTelemetry::maxBatchSize() const
{
return m_maxBatchSize;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
int RiaPreferencesOpenTelemetry::maxQueueSize() const
{
return m_maxQueueSize;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
int RiaPreferencesOpenTelemetry::memoryThresholdMb() const
{
return m_memoryThresholdMb;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
double RiaPreferencesOpenTelemetry::samplingRate() const
{
return m_samplingRate;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
int RiaPreferencesOpenTelemetry::connectionTimeoutMs() const
{
return m_connectionTimeoutMs;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiaPreferencesOpenTelemetry::LoggingState RiaPreferencesOpenTelemetry::loggingState() const
{
return m_loggingState();
}
@@ -0,0 +1,83 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2025- 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "cafAppEnum.h"
#include "cafPdmField.h"
#include "cafPdmObject.h"
#include <QString>
#include <map>
#include <vector>
//==================================================================================================
//
// OpenTelemetry preferences for ResInsight
// Follows the same pattern as RiaPreferencesOsdu
//
//==================================================================================================
class RiaPreferencesOpenTelemetry : public caf::PdmObject
{
CAF_PDM_HEADER_INIT;
public:
enum class LoggingState
{
DISABLED,
DEFAULT,
ALL
};
using LoggingStateType = caf::AppEnum<LoggingState>;
RiaPreferencesOpenTelemetry();
~RiaPreferencesOpenTelemetry() override;
static RiaPreferencesOpenTelemetry* current();
void setData( const std::map<QString, QString>& keyValuePairs );
void setFieldsReadOnly();
// Service name and version are hardcoded, not configurable
QString serviceName() const;
QString serviceVersion() const; // Read from ResInsightVersion.cmake
// Getters for configuration values
QString connectionString() const;
int batchTimeoutMs() const;
int maxBatchSize() const;
int maxQueueSize() const;
int memoryThresholdMb() const;
double samplingRate() const;
int connectionTimeoutMs() const;
LoggingState loggingState() const;
protected:
void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override;
private:
caf::PdmField<LoggingStateType> m_loggingState;
caf::PdmField<QString> m_connectionString;
caf::PdmField<int> m_batchTimeoutMs;
caf::PdmField<int> m_maxBatchSize;
caf::PdmField<int> m_maxQueueSize;
caf::PdmField<int> m_memoryThresholdMb;
caf::PdmField<double> m_samplingRate;
caf::PdmField<int> m_connectionTimeoutMs;
};
@@ -0,0 +1,729 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2025- 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RiaOpenTelemetryManager.h"
#include "RiaLogging.h"
#include "RiaPreferencesOpenTelemetry.h"
#include "RifJsonEncodeDecode.h"
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QString>
#include <QSysInfo>
#include <QTimer>
#include <algorithm>
#include <random>
#include <sstream>
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
double RiaOpenTelemetryManager::HealthSnapshot::getSuccessRate() const
{
uint64_t total = eventsSent + eventsDropped;
if ( total == 0 ) return 1.0;
return static_cast<double>( eventsSent ) / total;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RiaOpenTelemetryManager::HealthSnapshot::isHealthy() const
{
// Consider healthy if success rate > 90% and we've had recent successful sends
const auto now = std::chrono::steady_clock::now();
const auto timeSinceLastSuccess = now - lastSuccessfulSend;
return getSuccessRate() > 0.9 && timeSinceLastSuccess < std::chrono::minutes( 5 );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiaOpenTelemetryManager& RiaOpenTelemetryManager::instance()
{
static RiaOpenTelemetryManager instance;
return instance;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiaOpenTelemetryManager::RiaOpenTelemetryManager()
: QObject( nullptr )
{
m_healthMetrics.systemStartTime = std::chrono::steady_clock::now();
m_healthMetrics.lastSuccessfulSend = m_healthMetrics.systemStartTime;
// Initialize Qt networking components
m_networkAccessManager = new QNetworkAccessManager( this );
// Initialize timers
m_processTimer = new QTimer( this );
connect( m_processTimer, &QTimer::timeout, this, &RiaOpenTelemetryManager::onProcessEventTimer );
m_healthTimer = new QTimer( this );
connect( m_healthTimer, &QTimer::timeout, this, &RiaOpenTelemetryManager::sendHealthSpan );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiaOpenTelemetryManager::~RiaOpenTelemetryManager()
{
shutdown();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RiaOpenTelemetryManager::initialize()
{
std::lock_guard<std::mutex> lock( m_configMutex );
// Check if OpenTelemetry is disabled in preferences
auto* prefs = RiaPreferencesOpenTelemetry::current();
if ( prefs && prefs->loggingState() == RiaPreferencesOpenTelemetry::LoggingState::DISABLED )
{
RiaLogging::info( "OpenTelemetry is disabled in preferences" );
return false;
}
if ( m_initialized.load() )
{
return true;
}
if ( !initializeProvider() )
{
return false;
}
// Start event processing timer (100ms interval for responsive processing)
m_isShuttingDown = false;
m_processTimer->start( 100 );
// Start health monitoring timer (5 minutes interval)
if ( m_healthMonitoringEnabled )
{
m_healthTimer->start( 5 * 60 * 1000 ); // 5 minutes in milliseconds
sendHealthSpan();
}
m_initialized = true;
m_enabled = true;
RiaLogging::info( "OpenTelemetry initialized successfully" );
return true;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::shutdown( std::chrono::seconds timeout )
{
if ( !m_initialized.load() )
{
return;
}
RiaLogging::info( "Shutting down OpenTelemetry" );
m_isShuttingDown = true;
m_enabled = false;
// Stop timers
if ( m_processTimer )
{
m_processTimer->stop();
}
if ( m_healthTimer )
{
m_healthTimer->stop();
}
// Flush pending events
flushPendingEvents();
m_initialized = false;
RiaLogging::info( "OpenTelemetry shutdown complete" );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::reportEventAsync( const std::string& eventName, const std::map<std::string, std::string>& attributes )
{
if ( !isEnabled() || isCircuitBreakerOpen() || !shouldSampleEvent() )
{
return;
}
std::unique_lock<std::mutex> lock( m_queueMutex );
// Check queue size and apply backpressure
if ( m_backpressureEnabled && m_eventQueue.size() >= m_maxQueueSize )
{
m_healthMetrics.eventsDropped++;
return;
}
m_eventQueue.emplace( eventName, attributes );
m_healthMetrics.eventsQueued++;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::reportCrash( int signalCode, const std::stacktrace& trace )
{
if ( !isEnabled() )
{
return;
}
// Format stack trace using existing ResInsight formatter
std::stringstream ss;
int frame = 0;
for ( const auto& entry : trace )
{
ss << " [" << frame++ << "] " << entry.description() << " at " << entry.source_file() << ":" << entry.source_line() << "\n";
}
std::string rawStackTrace = ss.str();
std::map<std::string, std::string> attributes;
attributes["crash.signal"] = std::to_string( signalCode );
attributes["crash.thread_id"] = std::to_string( std::hash<std::thread::id>{}( std::this_thread::get_id() ) );
attributes["crash.stack_trace"] = rawStackTrace;
attributes["service.name"] = RiaPreferencesOpenTelemetry::current()->serviceName().toStdString();
attributes["service.version"] = RiaPreferencesOpenTelemetry::current()->serviceVersion().toStdString();
// Report with high priority (bypass sampling)
std::unique_lock<std::mutex> lock( m_queueMutex );
m_eventQueue.emplace( "crash.signal_handler", attributes );
m_healthMetrics.eventsQueued++;
lock.unlock();
RiaLogging::error( QString( "Crash reported to OpenTelemetry (signal: %1)" ).arg( signalCode ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::reportTestCrash( const std::stacktrace& trace )
{
if ( !isEnabled() )
{
return;
}
// Format stack trace
std::stringstream ss;
int frame = 0;
for ( const auto& entry : trace )
{
ss << " [" << frame++ << "] " << entry.description() << " at " << entry.source_file() << ":" << entry.source_line() << "\n";
}
std::string rawStackTrace = ss.str();
std::map<std::string, std::string> attributes;
attributes["test.type"] = "manual_stack_trace";
attributes["test.thread_id"] = std::to_string( std::hash<std::thread::id>{}( std::this_thread::get_id() ) );
attributes["test.stack_trace"] = rawStackTrace;
attributes["service.name"] = RiaPreferencesOpenTelemetry::current()->serviceName().toStdString();
attributes["service.version"] = RiaPreferencesOpenTelemetry::current()->serviceVersion().toStdString();
reportEventAsync( "test.stack_trace", attributes );
RiaLogging::info( "Test stack trace reported to OpenTelemetry" );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RiaOpenTelemetryManager::isEnabled() const
{
return m_enabled.load() && m_initialized.load();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RiaOpenTelemetryManager::isInitialized() const
{
return m_initialized.load();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::setErrorCallback( ErrorCallback callback )
{
std::lock_guard<std::mutex> lock( m_configMutex );
m_errorCallback = callback;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::setMaxQueueSize( size_t maxEvents )
{
std::lock_guard<std::mutex> lock( m_configMutex );
m_maxQueueSize = maxEvents;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::enableBackpressure( bool enable )
{
std::lock_guard<std::mutex> lock( m_configMutex );
m_backpressureEnabled = enable;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::setMemoryThreshold( size_t maxMemoryMB )
{
std::lock_guard<std::mutex> lock( m_configMutex );
m_memoryThresholdMB = maxMemoryMB;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::setSamplingRate( double rate )
{
std::lock_guard<std::mutex> lock( m_configMutex );
m_samplingRate = std::clamp( rate, 0.0, 1.0 );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
size_t RiaOpenTelemetryManager::getCurrentQueueSize() const
{
std::lock_guard<std::mutex> lock( m_queueMutex );
return m_eventQueue.size();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiaOpenTelemetryManager::HealthSnapshot RiaOpenTelemetryManager::getHealthMetrics() const
{
HealthSnapshot result;
result.eventsQueued = m_healthMetrics.eventsQueued.load();
result.eventsSent = m_healthMetrics.eventsSent.load();
result.eventsDropped = m_healthMetrics.eventsDropped.load();
result.networkFailures = m_healthMetrics.networkFailures.load();
result.lastSuccessfulSend = m_healthMetrics.lastSuccessfulSend;
result.systemStartTime = m_healthMetrics.systemStartTime;
return result;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RiaOpenTelemetryManager::isHealthy() const
{
return getHealthMetrics().isHealthy() && !isCircuitBreakerOpen();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::enableHealthMonitoring( bool enable )
{
std::lock_guard<std::mutex> lock( m_configMutex );
m_healthMonitoringEnabled = enable;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RiaOpenTelemetryManager::initializeProvider()
{
try
{
if ( !createExporter() )
{
return false;
}
setupResourceAttributes();
return true;
}
catch ( const std::exception& e )
{
handleError( TelemetryError::InternalError, QString( "Failed to initialize provider: %1" ).arg( e.what() ) );
return false;
}
}
//--------------------------------------------------------------------------------------------------
/// Parse Azure Application Insights connection string
/// Format: InstrumentationKey=<key>;IngestionEndpoint=<endpoint>;...
//--------------------------------------------------------------------------------------------------
static std::map<QString, QString> parseAzureConnectionString( const QString& connectionString )
{
std::map<QString, QString> result;
QStringList parts = connectionString.split( ';', Qt::SkipEmptyParts );
for ( const QString& part : parts )
{
int equalPos = part.indexOf( '=' );
if ( equalPos > 0 )
{
QString key = part.left( equalPos ).trimmed();
QString value = part.mid( equalPos + 1 ).trimmed();
result[key] = value;
}
}
return result;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RiaOpenTelemetryManager::createExporter()
{
try
{
auto* prefs = RiaPreferencesOpenTelemetry::current();
if ( !prefs )
{
handleError( TelemetryError::ConfigurationError, "Failed to get OpenTelemetry Preferences" );
return false;
}
// Parse and validate connection string
auto connectionParams = parseAzureConnectionString( prefs->connectionString() );
if ( !connectionParams.contains( "InstrumentationKey" ) )
{
handleError( TelemetryError::ConfigurationError, "InstrumentationKey not found in connection string" );
return false;
}
if ( !connectionParams.contains( "IngestionEndpoint" ) )
{
handleError( TelemetryError::ConfigurationError, "IngestionEndpoint not found in connection string" );
return false;
}
// Using Application Insights REST API for telemetry
RiaLogging::info( QString( "Application Insights REST API configured for production environment" ) );
return true;
}
catch ( const std::exception& e )
{
handleError( TelemetryError::NetworkError, QString( "Failed to create exporter: %1" ).arg( e.what() ) );
return false;
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::setupResourceAttributes()
{
// Resource attributes are typically set during provider creation
// This would be expanded with system information, process details, etc.
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::onProcessEventTimer()
{
if ( !m_isShuttingDown.load() )
{
processEvents();
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::processEvents()
{
std::unique_lock<std::mutex> lock( m_queueMutex );
if ( m_eventQueue.empty() )
{
return;
}
// Process a batch of events
std::queue<Event> batch;
auto* prefs = RiaPreferencesOpenTelemetry::current();
int maxBatchSize = prefs ? prefs->maxBatchSize() : 100;
for ( int i = 0; i < maxBatchSize && !m_eventQueue.empty(); ++i )
{
batch.push( m_eventQueue.front() );
m_eventQueue.pop();
}
lock.unlock();
// Process events outside of lock
while ( !batch.empty() )
{
processEvent( batch.front() );
batch.pop();
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::processEvent( const Event& event )
{
try
{
auto* prefs = RiaPreferencesOpenTelemetry::current();
if ( !prefs )
{
handleError( TelemetryError::InternalError, QString( "Failed to access Open Telemetry Preferences." ) );
updateHealthMetrics( false );
return;
}
// Use Application Insights REST API
auto connectionParams = parseAzureConnectionString( prefs->connectionString() );
if ( !connectionParams.contains( "InstrumentationKey" ) || !connectionParams.contains( "IngestionEndpoint" ) )
{
updateHealthMetrics( false );
return;
}
// Format timestamp - must match Application Insights format exactly
auto time_t = std::chrono::system_clock::to_time_t( event.timestamp );
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>( event.timestamp.time_since_epoch() ) % 1000;
char timeBuffer[100];
std::strftime( timeBuffer, sizeof( timeBuffer ), "%Y-%m-%dT%H:%M:%S", std::gmtime( &time_t ) );
// Pad milliseconds to 3 digits
char msBuffer[8];
std::snprintf( msBuffer, sizeof( msBuffer ), ".%03dZ", static_cast<int>( ms.count() ) );
std::string timestamp = std::string( timeBuffer ) + msBuffer;
// Convert attributes to JSON properties
QMap<QString, QVariant> properties;
for ( const auto& [key, value] : event.attributes )
{
properties[QString::fromStdString( key )] = QString::fromStdString( value );
}
// Add system information
properties["os.type"] = QSysInfo::productType();
properties["os.version"] = QSysInfo::productVersion();
properties["os.name"] = QSysInfo::prettyProductName();
// Create Application Insights telemetry item
QMap<QString, QVariant> baseData;
baseData["name"] = QString::fromStdString( event.name );
baseData["properties"] = properties;
QMap<QString, QVariant> data;
data["baseType"] = "EventData";
data["baseData"] = baseData;
QMap<QString, QVariant> telemetryItem;
telemetryItem["time"] = QString::fromStdString( timestamp );
telemetryItem["iKey"] = connectionParams["InstrumentationKey"];
telemetryItem["name"] = "Microsoft.ApplicationInsights.Event";
telemetryItem["data"] = data;
// Convert to JSON string
QString jsonPayload = ResInsightInternalJson::Json::encode( telemetryItem, false );
// Send to Application Insights using QNetworkAccessManager
QString url = connectionParams["IngestionEndpoint"] + "/v2/track";
QNetworkRequest request;
request.setUrl( QUrl( url ) );
request.setHeader( QNetworkRequest::ContentTypeHeader, "application/json" );
request.setHeader( QNetworkRequest::KnownHeaders( QNetworkRequest::UserAgentHeader ), "ResInsight-OpenTelemetry" );
request.setTransferTimeout( prefs->connectionTimeoutMs() );
QNetworkReply* reply = m_networkAccessManager->post( request, jsonPayload.toUtf8() );
// Handle response asynchronously
connect( reply,
&QNetworkReply::finished,
this,
[this, reply]()
{
if ( reply->error() == QNetworkReply::NoError )
{
updateHealthMetrics( true );
resetCircuitBreaker();
}
else
{
QString errorMsg = QString( "HTTP %1: %2" )
.arg( reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt() )
.arg( reply->errorString() );
handleError( TelemetryError::NetworkError, QString( "Failed to send telemetry: %1" ).arg( errorMsg ) );
updateHealthMetrics( false );
}
reply->deleteLater();
} );
}
catch ( const std::exception& e )
{
handleError( TelemetryError::InternalError, QString( "Failed to process event: %1" ).arg( e.what() ) );
updateHealthMetrics( false );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RiaOpenTelemetryManager::shouldSampleEvent() const
{
if ( m_samplingRate >= 1.0 )
{
return true;
}
static thread_local std::mt19937 gen( std::random_device{}() );
static thread_local std::uniform_real_distribution<double> dis( 0.0, 1.0 );
return dis( gen ) < m_samplingRate;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::flushPendingEvents()
{
// Process remaining events in the queue
processEvents();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::handleError( TelemetryError error, const QString& context )
{
m_consecutiveFailures++;
if ( m_consecutiveFailures >= 3 )
{
m_circuitBreakerOpen = true;
RiaLogging::warning( "OpenTelemetry circuit breaker opened due to consecutive failures" );
}
if ( m_errorCallback )
{
m_errorCallback( error, context );
}
RiaLogging::warning( QString( "OpenTelemetry error: %1" ).arg( context ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::attemptReconnection()
{
auto now = std::chrono::steady_clock::now();
if ( now - m_lastReconnectAttempt < std::chrono::minutes( 5 ) )
{
return; // Don't retry too frequently
}
m_lastReconnectAttempt = now;
// Try to reinitialize connection
if ( createExporter() )
{
resetCircuitBreaker();
RiaLogging::info( "OpenTelemetry reconnection successful" );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RiaOpenTelemetryManager::isCircuitBreakerOpen() const
{
return m_circuitBreakerOpen.load();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::resetCircuitBreaker()
{
m_consecutiveFailures = 0;
m_circuitBreakerOpen = false;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::updateHealthMetrics( bool success )
{
if ( success )
{
m_healthMetrics.eventsSent++;
m_healthMetrics.lastSuccessfulSend = std::chrono::steady_clock::now();
}
else
{
m_healthMetrics.networkFailures++;
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaOpenTelemetryManager::sendHealthSpan()
{
if ( !isEnabled() )
{
return;
}
auto metrics = getHealthMetrics();
std::map<std::string, std::string> attributes;
attributes["health.events_queued"] = std::to_string( metrics.eventsQueued );
attributes["health.events_sent"] = std::to_string( metrics.eventsSent );
attributes["health.events_dropped"] = std::to_string( metrics.eventsDropped );
attributes["health.network_failures"] = std::to_string( metrics.networkFailures );
attributes["health.success_rate"] = std::to_string( metrics.getSuccessRate() );
attributes["health.queue_size"] = std::to_string( getCurrentQueueSize() );
reportEventAsync( "health.status", attributes );
}
@@ -0,0 +1,183 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2025- 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include <QObject>
#include <atomic>
#include <chrono>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <queue>
#include <stacktrace>
#include <string>
class QString;
class QNetworkAccessManager;
class QTimer;
//==================================================================================================
//
// OpenTelemetry Manager for ResInsight
// Handles async telemetry reporting with privacy filtering and resilience
//
//==================================================================================================
class RiaOpenTelemetryManager : public QObject
{
public:
// Error handling
enum class TelemetryError
{
ConfigurationError,
NetworkError,
AuthenticationError,
QuotaExceeded,
PrivacyViolation,
InternalError
};
using ErrorCallback = std::function<void( TelemetryError, const QString& details )>;
// Health monitoring
struct HealthSnapshot
{
uint64_t eventsQueued{ 0 };
uint64_t eventsSent{ 0 };
uint64_t eventsDropped{ 0 };
uint64_t networkFailures{ 0 };
std::chrono::steady_clock::time_point lastSuccessfulSend;
std::chrono::steady_clock::time_point systemStartTime;
double getSuccessRate() const;
bool isHealthy() const;
};
struct HealthMetrics
{
std::atomic<uint64_t> eventsQueued{ 0 };
std::atomic<uint64_t> eventsSent{ 0 };
std::atomic<uint64_t> eventsDropped{ 0 };
std::atomic<uint64_t> networkFailures{ 0 };
std::chrono::steady_clock::time_point lastSuccessfulSend;
std::chrono::steady_clock::time_point systemStartTime;
};
static RiaOpenTelemetryManager& instance();
bool initialize();
void shutdown( std::chrono::seconds timeout = std::chrono::seconds( 30 ) );
// Event reporting
void reportEventAsync( const std::string& eventName, const std::map<std::string, std::string>& attributes );
void reportCrash( int signalCode, const std::stacktrace& trace );
void reportTestCrash( const std::stacktrace& trace );
// Configuration
bool isEnabled() const;
bool isInitialized() const;
void setErrorCallback( ErrorCallback callback );
// Performance and memory management
void setMaxQueueSize( size_t maxEvents );
void enableBackpressure( bool enable );
void setMemoryThreshold( size_t maxMemoryMB );
void setSamplingRate( double rate );
size_t getCurrentQueueSize() const;
// Health monitoring
HealthSnapshot getHealthMetrics() const;
bool isHealthy() const;
void enableHealthMonitoring( bool enable );
private:
RiaOpenTelemetryManager();
~RiaOpenTelemetryManager();
RiaOpenTelemetryManager( const RiaOpenTelemetryManager& ) = delete;
RiaOpenTelemetryManager& operator=( const RiaOpenTelemetryManager& ) = delete;
struct Event
{
std::string name;
std::map<std::string, std::string> attributes;
std::chrono::system_clock::time_point timestamp;
Event( const std::string& eventName, const std::map<std::string, std::string>& attrs )
: name( eventName )
, attributes( attrs )
, timestamp( std::chrono::system_clock::now() )
{
}
};
// Initialization
bool initializeProvider();
bool createExporter();
void setupResourceAttributes();
// Event processing
void processEvents();
void processEvent( const Event& event );
bool shouldSampleEvent() const;
void flushPendingEvents();
void onProcessEventTimer();
void onNetworkReplyFinished();
// Circuit breaker and resilience
void handleError( TelemetryError error, const QString& context );
void attemptReconnection();
bool isCircuitBreakerOpen() const;
void resetCircuitBreaker();
// Health monitoring
void updateHealthMetrics( bool success );
void sendHealthSpan();
// Thread safety
mutable std::mutex m_configMutex;
mutable std::mutex m_queueMutex;
std::queue<Event> m_eventQueue;
// State
std::atomic<bool> m_initialized{ false };
std::atomic<bool> m_enabled{ false };
std::atomic<bool> m_isShuttingDown{ false };
std::atomic<bool> m_circuitBreakerOpen{ false };
// Qt networking and timer
QNetworkAccessManager* m_networkAccessManager{ nullptr };
QTimer* m_processTimer{ nullptr };
QTimer* m_healthTimer{ nullptr };
// Configuration
size_t m_maxQueueSize{ 10000 };
bool m_backpressureEnabled{ true };
size_t m_memoryThresholdMB{ 50 };
double m_samplingRate{ 1.0 };
// Error handling
ErrorCallback m_errorCallback;
std::atomic<int> m_consecutiveFailures{ 0 };
std::chrono::steady_clock::time_point m_lastReconnectAttempt;
// Health monitoring
mutable HealthMetrics m_healthMetrics;
bool m_healthMonitoringEnabled{ true };
};
+23
View File
@@ -214,6 +214,28 @@ if(RESINSIGHT_FOUND_HDF5)
endif() # MSVC
endif()
#
# OpenTelemetry
#
option(RESINSIGHT_ENABLE_OPENTELEMETRY "Enable OpenTelemetry integration" OFF)
if(RESINSIGHT_ENABLE_OPENTELEMETRY)
message(STATUS "OpenTelemetry support enabled")
add_definitions(-DRESINSIGHT_OPENTELEMETRY_ENABLED)
# Create telemetry source files list
set(OPENTELEMETRY_FILES
Application/Tools/Telemetry/RiaOpenTelemetryManager.h
Application/Tools/Telemetry/RiaOpenTelemetryManager.cpp
)
list(APPEND CPP_SOURCES ${OPENTELEMETRY_FILES})
endif()
# Always include OpenTelemetry preferences
list(APPEND CPP_SOURCES Application/RiaPreferencesOpenTelemetry.h
Application/RiaPreferencesOpenTelemetry.cpp
)
qt_add_resources(QRC_FILES Application/Resources/ApplicationLibCode.qrc)
list(APPEND ALL_SOURCE_FILES ${CPP_SOURCES} ${MOC_SOURCE_FILES}
@@ -353,6 +375,7 @@ target_include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/Application/Tools
${CMAKE_CURRENT_SOURCE_DIR}/Application/Tools/WellPathTools
${CMAKE_CURRENT_SOURCE_DIR}/Application/Tools/KeyValueStore
${CMAKE_CURRENT_SOURCE_DIR}/Application/Tools/Telemetry
${CMAKE_CURRENT_SOURCE_DIR}/CommandFileInterface
${CMAKE_CURRENT_SOURCE_DIR}/CommandFileInterface/Core
${CMAKE_CURRENT_SOURCE_DIR}/FileInterface