From 958ddbd2b913c6a2a448a9c866953a079d9a6c96 Mon Sep 17 00:00:00 2001 From: Magne Sjaastad Date: Sun, 25 Oct 2026 09:24:22 +0100 Subject: [PATCH] #15033 Telemetry: Add graphics environment to events Most Linux crash reports are aborts inside the GL driver during a widget repaint, and the reports carry no information about the driver or display setup. Attach GL vendor/renderer/version, Qt platform, display server and remote display server (e.g. HP RGS) to crash and regular events, and report the OpenGL driver once per session in app.opengl_initialized. Co-Authored-By: Claude Opus 5.5 --- ApplicationExeCode/RiaMain.cpp | 7 + .../Telemetry/RiaGraphicsEnvironmentTools.cpp | 199 ++++++++++++++++++ .../Telemetry/RiaGraphicsEnvironmentTools.h | 35 +++ .../Telemetry/RiaOpenTelemetryManager.cpp | 28 ++- .../Tools/Telemetry/RiaOpenTelemetryManager.h | 4 + ApplicationLibCode/CMakeLists.txt | 2 + .../UserInterface/RiuViewer.cpp | 22 ++ ApplicationLibCode/UserInterface/RiuViewer.h | 1 + 8 files changed, 295 insertions(+), 3 deletions(-) create mode 100644 ApplicationLibCode/Application/Tools/Telemetry/RiaGraphicsEnvironmentTools.cpp create mode 100644 ApplicationLibCode/Application/Tools/Telemetry/RiaGraphicsEnvironmentTools.h diff --git a/ApplicationExeCode/RiaMain.cpp b/ApplicationExeCode/RiaMain.cpp index c1fb320b96..7b59b38da5 100644 --- a/ApplicationExeCode/RiaMain.cpp +++ b/ApplicationExeCode/RiaMain.cpp @@ -18,6 +18,7 @@ #include "RiaApplication.h" #include "RiaArgumentParser.h" +#include "RiaGraphicsEnvironmentTools.h" #include "RiaMainTools.h" #include "RiaOpenTelemetryManager.h" #include "RiaPreferences.h" @@ -157,6 +158,12 @@ int main( int argc, char* argv[] ) bool isConsoleMode = ( dynamic_cast( app.get() ) == nullptr ); attributes["app.console_mode"] = RiaStdStringTools::boolToString( isConsoleMode ); attributes["app.version"] = RiaApplication::getVersionStringApp( false ); + + if ( !isConsoleMode ) + { + RiaOpenTelemetryManager::instance().setGraphicsInfo( RiaGraphicsEnvironmentTools::displayEnvironment() ); + } + RiaOpenTelemetryManager::instance().reportEventAsync( "app.started", attributes ); } diff --git a/ApplicationLibCode/Application/Tools/Telemetry/RiaGraphicsEnvironmentTools.cpp b/ApplicationLibCode/Application/Tools/Telemetry/RiaGraphicsEnvironmentTools.cpp new file mode 100644 index 0000000000..27c49ae2c5 --- /dev/null +++ b/ApplicationLibCode/Application/Tools/Telemetry/RiaGraphicsEnvironmentTools.cpp @@ -0,0 +1,199 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RiaGraphicsEnvironmentTools.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ +//-------------------------------------------------------------------------------------------------- +/// Classify the X11 DISPLAY without reporting the host name +//-------------------------------------------------------------------------------------------------- +std::string displayKind( const QString& display ) +{ + if ( display.isEmpty() ) return "none"; + + const QString host = display.section( ':', 0, 0 ); + if ( host.isEmpty() || host == "unix" ) return "local"; + + // ssh -X/-Y forwarding uses localhost with a display number of 10 and above + if ( host == "localhost" || host == "127.0.0.1" ) return "localhost"; + + return "remote"; +} + +//-------------------------------------------------------------------------------------------------- +/// Names of known remote display servers running on this host. Remote graphics solutions such as HP RGS +/// capture the local console display, so they are not visible in the environment of the application. +//-------------------------------------------------------------------------------------------------- +std::string runningRemoteDisplayServers() +{ +#ifdef Q_OS_LINUX + // Process names as shown in /proc//comm, truncated by the kernel to 15 characters + const QStringList knownServers = { "rgsender", "pcoip-server", "Xvnc", "x0vncserver", "vncagent", "nxnode", "nxserver.bin" }; + + QStringList found; + for ( const auto& pid : QDir( "/proc" ).entryList( QDir::Dirs | QDir::NoDotAndDotDot ) ) + { + if ( !pid.front().isDigit() ) continue; + + QFile commFile( "/proc/" + pid + "/comm" ); + if ( !commFile.open( QIODevice::ReadOnly ) ) continue; + + const QString name = QString::fromLatin1( commFile.readAll() ).trimmed(); + if ( knownServers.contains( name ) && !found.contains( name ) ) found.push_back( name ); + } + + return found.join( "," ).toStdString(); +#else + return {}; +#endif +} + +//-------------------------------------------------------------------------------------------------- +/// Driver strings are free text from the driver. Keep printable ASCII only and limit the length. +//-------------------------------------------------------------------------------------------------- +std::string sanitizeGlString( const std::string& text ) +{ + const size_t maxLength = 100; + + std::string result; + for ( const char c : text ) + { + if ( result.size() >= maxLength ) break; + if ( c >= 0x20 && c < 0x7f ) result.push_back( c ); + } + return result; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::string glString( QOpenGLFunctions* functions, GLenum name ) +{ + const auto* str = reinterpret_cast( functions->glGetString( name ) ); + return str ? sanitizeGlString( str ) : std::string( "unknown" ); +} +} // namespace + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::map RiaGraphicsEnvironmentTools::displayEnvironment() +{ + std::map attributes; + + attributes["graphics.qt_runtime_version"] = qVersion(); + attributes["graphics.qpa_platform"] = QGuiApplication::platformName().toStdString(); + attributes["graphics.session_type"] = qEnvironmentVariable( "XDG_SESSION_TYPE" ).toStdString(); + attributes["graphics.desktop"] = qEnvironmentVariable( "XDG_CURRENT_DESKTOP" ).toStdString(); + attributes["graphics.x11_display"] = displayKind( qEnvironmentVariable( "DISPLAY" ) ); + + // Set when running under a Wayland compositor, also when Qt uses xcb through XWayland + attributes["graphics.wayland_display"] = qEnvironmentVariableIsSet( "WAYLAND_DISPLAY" ) ? "true" : "false"; + + // Presence of variables set by remote access tools. Only the names are reported, never the values. + { + const QStringList remoteHintVariables = { "SSH_CONNECTION", "VGL_ISACTIVE", "TLSESSIONDATA", "VNCDESKTOP" }; + + QStringList present; + for ( const auto& name : remoteHintVariables ) + { + if ( qEnvironmentVariableIsSet( name.toLatin1().constData() ) ) present.push_back( name ); + } + + // VirtualGL can also be injected without vglrun + if ( qEnvironmentVariable( "LD_PRELOAD" ).contains( "vglfaker" ) ) present.push_back( "LD_PRELOAD=vglfaker" ); + + attributes["graphics.remote_hints"] = present.join( "," ).toStdString(); + } + + attributes["graphics.remote_display_servers"] = runningRemoteDisplayServers(); + + // Overrides that change the OpenGL code path in Qt or the driver + { + const QStringList overrideVariables = { "QT_QPA_PLATFORM", + "QT_XCB_GL_INTEGRATION", + "QT_WIDGETS_RHI", + "QT_WIDGETS_RHI_BACKEND", + "QT_OPENGL", + "LIBGL_ALWAYS_SOFTWARE", + "LIBGL_ALWAYS_INDIRECT", + "MESA_LOADER_DRIVER_OVERRIDE", + "__GLX_VENDOR_LIBRARY_NAME", + "__GL_SYNC_TO_VBLANK", + "RESINSIGHT_ENABLE_NATIVE_GL_WIDGET" }; + + const QStringList knownValues = { "0", "1", "true", "false", "xcb", "wayland", "offscreen", + "minimal", "eglfs", "linuxfb", "software", "desktop", "angle", "mesa", + "nvidia", "egl", "glx", "egl_x11", "egl_wayland", "glx_nvidia", "swrast", + "llvmpipe", "radeonsi", "iris", "nouveau" }; + + QStringList overrides; + for ( const auto& name : overrideVariables ) + { + const QByteArray latinName = name.toLatin1(); + if ( qEnvironmentVariableIsSet( latinName.constData() ) ) + { + // Values are free text set by the user. Only report values from a fixed list, never arbitrary text. + const QString value = qEnvironmentVariable( latinName.constData() ).toLower(); + const QString reported = knownValues.contains( value ) ? value : QString( "other" ); + overrides.push_back( name + "=" + reported ); + } + } + + attributes["graphics.env_overrides"] = overrides.join( "," ).toStdString(); + } + + return attributes; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::map RiaGraphicsEnvironmentTools::currentOpenGLContextInfo() +{ + std::map attributes; + + QOpenGLContext* context = QOpenGLContext::currentContext(); + if ( !context ) return attributes; + + QOpenGLFunctions* functions = context->functions(); + + attributes["graphics.gl_vendor"] = glString( functions, GL_VENDOR ); + attributes["graphics.gl_renderer"] = glString( functions, GL_RENDERER ); + attributes["graphics.gl_version"] = glString( functions, GL_VERSION ); + attributes["graphics.gl_glsl_version"] = glString( functions, GL_SHADING_LANGUAGE_VERSION ); + + const QSurfaceFormat format = context->format(); + + std::string profile = "none"; + if ( format.profile() == QSurfaceFormat::CoreProfile ) profile = "core"; + if ( format.profile() == QSurfaceFormat::CompatibilityProfile ) profile = "compatibility"; + + attributes["graphics.gl_context"] = std::to_string( format.majorVersion() ) + "." + std::to_string( format.minorVersion() ) + " " + + profile + ( context->isOpenGLES() ? " es" : "" ); + + return attributes; +} diff --git a/ApplicationLibCode/Application/Tools/Telemetry/RiaGraphicsEnvironmentTools.h b/ApplicationLibCode/Application/Tools/Telemetry/RiaGraphicsEnvironmentTools.h new file mode 100644 index 0000000000..d911f6fbbd --- /dev/null +++ b/ApplicationLibCode/Application/Tools/Telemetry/RiaGraphicsEnvironmentTools.h @@ -0,0 +1,35 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +//================================================================================================== +/// Collects graphics environment information for telemetry, used to correlate rendering crashes +/// (e.g. aborts inside the GL driver) with driver, display server and remote desktop setup. +//================================================================================================== +namespace RiaGraphicsEnvironmentTools +{ +// Display server and environment information. Requires a QGuiApplication, but no OpenGL context. +std::map displayEnvironment(); + +// Vendor, renderer and version strings of the current OpenGL context. Returns an empty map if no context is current. +std::map currentOpenGLContextInfo(); +} // namespace RiaGraphicsEnvironmentTools diff --git a/ApplicationLibCode/Application/Tools/Telemetry/RiaOpenTelemetryManager.cpp b/ApplicationLibCode/Application/Tools/Telemetry/RiaOpenTelemetryManager.cpp index 6d95d2f8c5..cba53b16f2 100644 --- a/ApplicationLibCode/Application/Tools/Telemetry/RiaOpenTelemetryManager.cpp +++ b/ApplicationLibCode/Application/Tools/Telemetry/RiaOpenTelemetryManager.cpp @@ -305,6 +305,9 @@ void RiaOpenTelemetryManager::reportEventAsync( const std::string& eventName, co enrichedAttributes["user.hash"] = m_userHash; } + // Do not overwrite attributes given by the caller + enrichedAttributes.insert( m_graphicsInfo.begin(), m_graphicsInfo.end() ); + addOsInfo( enrichedAttributes ); } @@ -391,11 +394,18 @@ void RiaOpenTelemetryManager::reportCrash( int signalCode, const std::stacktrace addOsInfo( attributes ); // Add hashed username so crashes can be grouped per user without revealing the cleartext name. + // Add graphics environment, as many crashes are aborts inside the OpenGL driver. { - std::lock_guard lock( m_configMutex ); - if ( !m_userHash.empty() ) + // try_lock: a crash on a thread holding the mutex would otherwise deadlock the handler + std::unique_lock lock( m_configMutex, std::try_to_lock ); + if ( lock.owns_lock() ) { - attributes["user.hash"] = m_userHash; + if ( !m_userHash.empty() ) + { + attributes["user.hash"] = m_userHash; + } + + attributes.insert( m_graphicsInfo.begin(), m_graphicsInfo.end() ); } } @@ -480,6 +490,18 @@ void RiaOpenTelemetryManager::setUsername( const std::string& username ) m_userHash = hashUsername( username ); } +//-------------------------------------------------------------------------------------------------- +/// Merge graphics information into the attributes attached to all events. Existing keys are replaced. +//-------------------------------------------------------------------------------------------------- +void RiaOpenTelemetryManager::setGraphicsInfo( const std::map& graphicsInfo ) +{ + std::lock_guard lock( m_configMutex ); + for ( const auto& [key, value] : graphicsInfo ) + { + m_graphicsInfo[key] = value; + } +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/Application/Tools/Telemetry/RiaOpenTelemetryManager.h b/ApplicationLibCode/Application/Tools/Telemetry/RiaOpenTelemetryManager.h index f5c81233cb..c0e40994e2 100644 --- a/ApplicationLibCode/Application/Tools/Telemetry/RiaOpenTelemetryManager.h +++ b/ApplicationLibCode/Application/Tools/Telemetry/RiaOpenTelemetryManager.h @@ -114,6 +114,7 @@ class RiaOpenTelemetryManager : public QObject static bool isReportingCrash(); void setErrorCallback( ErrorCallback callback ); void setUsername( const std::string& username ); + void setGraphicsInfo( const std::map& graphicsInfo ); // Performance and memory management void setMaxQueueSize( size_t maxEvents ); @@ -194,6 +195,9 @@ class RiaOpenTelemetryManager : public QObject size_t m_memoryThresholdMB{ 50 }; std::string m_userHash; + // Graphics environment attached to all events, see RiaGraphicsEnvironmentTools + std::map m_graphicsInfo; + // Error handling ErrorCallback m_errorCallback; std::atomic m_consecutiveFailures{ 0 }; diff --git a/ApplicationLibCode/CMakeLists.txt b/ApplicationLibCode/CMakeLists.txt index 6e6190ef82..9447d3baf0 100644 --- a/ApplicationLibCode/CMakeLists.txt +++ b/ApplicationLibCode/CMakeLists.txt @@ -265,6 +265,8 @@ set(OPENTELEMETRY_FILES Application/RiaPreferencesOpenTelemetry.cpp Application/Tools/Telemetry/RiaOpenTelemetryManager.h Application/Tools/Telemetry/RiaOpenTelemetryManager.cpp + Application/Tools/Telemetry/RiaGraphicsEnvironmentTools.h + Application/Tools/Telemetry/RiaGraphicsEnvironmentTools.cpp ) list(APPEND CPP_SOURCES ${OPENTELEMETRY_FILES}) diff --git a/ApplicationLibCode/UserInterface/RiuViewer.cpp b/ApplicationLibCode/UserInterface/RiuViewer.cpp index 02354b4ba8..3a7f37d147 100644 --- a/ApplicationLibCode/UserInterface/RiuViewer.cpp +++ b/ApplicationLibCode/UserInterface/RiuViewer.cpp @@ -23,7 +23,9 @@ #include "RiaApplication.h" #include "RiaBaseDefs.h" #include "RiaColorTools.h" +#include "RiaGraphicsEnvironmentTools.h" #include "RiaGuiApplication.h" +#include "RiaOpenTelemetryManager.h" #include "RiaPreferences.h" #include "RiaRegressionTestRunner.h" @@ -1144,6 +1146,26 @@ RimViewWindow* RiuViewer::ownerViewWindow() const return dynamic_cast( m_rimView.p() ); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RiuViewer::onWidgetOpenGLReady() +{ + caf::Viewer::onWidgetOpenGLReady(); + + // Report the OpenGL driver once per session. The context is current here, so no extra context is created. + static bool isOpenGLInfoReported = false; + if ( !isOpenGLInfoReported ) + { + auto glInfo = RiaGraphicsEnvironmentTools::currentOpenGLContextInfo(); + if ( glInfo.empty() ) return; + + isOpenGLInfoReported = true; + RiaOpenTelemetryManager::instance().setGraphicsInfo( glInfo ); + RiaOpenTelemetryManager::instance().reportEventAsync( "app.opengl_initialized", glInfo ); + } +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/UserInterface/RiuViewer.h b/ApplicationLibCode/UserInterface/RiuViewer.h index 8e84253438..5f9a0486e1 100644 --- a/ApplicationLibCode/UserInterface/RiuViewer.h +++ b/ApplicationLibCode/UserInterface/RiuViewer.h @@ -151,6 +151,7 @@ public slots: protected: void optimizeClippingPlanes() override; + void onWidgetOpenGLReady() override; void mouseMoveEvent( QMouseEvent* e ) override; void enterEvent( QEnterEvent* e ) override; void leaveEvent( QEvent* ) override;