From 0108041c68913f89471c9f7b8d7683e0f7d4efab Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 1 Oct 2021 09:09:00 +1000 Subject: [PATCH] Add query log --- .../core/auto_generated/qgsapplication.sip.in | 7 + .../core/auto_generated/qgsdbquerylog.sip.in | 102 ++++++++ python/core/core_auto.sip | 1 + src/app/CMakeLists.txt | 4 + .../querylogger/qgsappquerylogger.cpp | 244 ++++++++++++++++++ .../devtools/querylogger/qgsappquerylogger.h | 147 +++++++++++ .../querylogger/qgsqueryloggernode.cpp | 201 +++++++++++++++ .../devtools/querylogger/qgsqueryloggernode.h | 197 ++++++++++++++ .../querylogger/qgsqueryloggerpanelwidget.cpp | 205 +++++++++++++++ .../querylogger/qgsqueryloggerpanelwidget.h | 84 ++++++ .../qgsqueryloggerwidgetfactory.cpp | 29 +++ .../querylogger/qgsqueryloggerwidgetfactory.h | 35 +++ src/app/qgisapp.cpp | 7 + src/app/qgisapp.h | 3 + src/core/CMakeLists.txt | 2 + src/core/qgsapplication.cpp | 13 + src/core/qgsapplication.h | 9 + src/core/qgsdbquerylog.cpp | 68 +++++ src/core/qgsdbquerylog.h | 170 ++++++++++++ src/providers/postgres/qgspostgresconn.cpp | 108 ++++++-- src/providers/postgres/qgspostgresconn.h | 3 +- .../postgres/qgspostgresdataitems.cpp | 20 +- .../postgres/qgspostgresfeatureiterator.cpp | 13 +- src/ui/qgsqueryloggerpanelbase.ui | 117 +++++++++ 24 files changed, 1766 insertions(+), 23 deletions(-) create mode 100644 python/core/auto_generated/qgsdbquerylog.sip.in create mode 100644 src/app/devtools/querylogger/qgsappquerylogger.cpp create mode 100644 src/app/devtools/querylogger/qgsappquerylogger.h create mode 100644 src/app/devtools/querylogger/qgsqueryloggernode.cpp create mode 100644 src/app/devtools/querylogger/qgsqueryloggernode.h create mode 100644 src/app/devtools/querylogger/qgsqueryloggerpanelwidget.cpp create mode 100644 src/app/devtools/querylogger/qgsqueryloggerpanelwidget.h create mode 100644 src/app/devtools/querylogger/qgsqueryloggerwidgetfactory.cpp create mode 100644 src/app/devtools/querylogger/qgsqueryloggerwidgetfactory.h create mode 100644 src/core/qgsdbquerylog.cpp create mode 100644 src/core/qgsdbquerylog.h create mode 100644 src/ui/qgsqueryloggerpanelbase.ui diff --git a/python/core/auto_generated/qgsapplication.sip.in b/python/core/auto_generated/qgsapplication.sip.in index f16bbd66913..106a2bef57c 100644 --- a/python/core/auto_generated/qgsapplication.sip.in +++ b/python/core/auto_generated/qgsapplication.sip.in @@ -854,6 +854,13 @@ Returns the application's bookmark manager, used for storing installation-wide b Returns the handler for recently used style items. .. versionadded:: 3.22 +%End + + static QgsDatabaseQueryLog *databaseQueryLog() /KeepReference/; +%Docstring +Returns the database query log. + +.. versionadded:: 3.24 %End static QgsStyleModel *defaultStyleModel(); diff --git a/python/core/auto_generated/qgsdbquerylog.sip.in b/python/core/auto_generated/qgsdbquerylog.sip.in new file mode 100644 index 00000000000..8f2efcb28d5 --- /dev/null +++ b/python/core/auto_generated/qgsdbquerylog.sip.in @@ -0,0 +1,102 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsdbquerylog.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + +class QgsDatabaseQueryLogEntry +{ +%Docstring(signature="appended") +Encapsulates a logged database query. + +.. versionadded:: 3.24 +%End + +%TypeHeaderCode +#include "qgsdbquerylog.h" +%End + public: + + QgsDatabaseQueryLogEntry( const QString &query = QString() ); +%Docstring +Constructor for QgsDatabaseQueryLogEntry. +%End + + QString query; + + QString initiatorClass; + + QString origin; +}; + + + +class QgsDatabaseQueryLog: QObject +{ +%Docstring(signature="appended") +Handles logging of database queries. + +:py:class:`QgsDatabaseQueryLog` is not usually directly created, but rather accessed through +:py:func:`QgsApplication.databaseQueryLog()`. Generally, clients should only access the +static :py:func:`~QgsDatabaseQueryLogEntry.log` method to register their queries. + +Example +------- + +.. code-block:: python + + # Log a database query + QgsDatabaseQueryLogger.log('SELECT * FROM my_table') + +.. versionadded:: 3.24 +%End + +%TypeHeaderCode +#include "qgsdbquerylog.h" +%End + public: + + QgsDatabaseQueryLog( QObject *parent = 0 ); +%Docstring +Creates a new query logger. + +:py:class:`QgsDatabaseQueryLogger` is not usually directly created, but rather accessed through +:py:func:`QgsApplication.queryLogger()`. +%End + + static void log( const QString &query ); +%Docstring +Logs a database query. + +Consider using the variant with a :py:class:`QgsDatabaseQueryLogEntry` argument instead, as that +method allows more context for logged queries. + +This method can be safely called from any thread. +%End + + static void log( const QgsDatabaseQueryLogEntry &entry ); +%Docstring +Logs a database query ``entry``. + +This method can be safely called from any thread. +%End + + public slots: + + + signals: + + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsdbquerylog.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 914ecb7793e..f149af7e2f9 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -38,6 +38,7 @@ %Include auto_generated/qgsdatasourceuri.sip %Include auto_generated/qgsdatetimestatisticalsummary.sip %Include auto_generated/qgsdbfilterproxymodel.sip +%Include auto_generated/qgsdbquerylog.sip %Include auto_generated/qgsdefaultvalue.sip %Include auto_generated/qgsdiagramrenderer.sip %Include auto_generated/qgsdistancearea.sip diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index afb2228fbac..4d3833a045c 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -169,6 +169,10 @@ set(QGIS_APP_SRCS devtools/networklogger/qgsnetworkloggerwidgetfactory.cpp devtools/profiler/qgsprofilerpanelwidget.cpp devtools/profiler/qgsprofilerwidgetfactory.cpp + devtools/querylogger/qgsappquerylogger.cpp + devtools/querylogger/qgsqueryloggernode.cpp + devtools/querylogger/qgsqueryloggerpanelwidget.cpp + devtools/querylogger/qgsqueryloggerwidgetfactory.cpp dwg/qgsdwgimportdialog.cpp dwg/qgsdwgimporter.cpp diff --git a/src/app/devtools/querylogger/qgsappquerylogger.cpp b/src/app/devtools/querylogger/qgsappquerylogger.cpp new file mode 100644 index 00000000000..0b412391178 --- /dev/null +++ b/src/app/devtools/querylogger/qgsappquerylogger.cpp @@ -0,0 +1,244 @@ +/*************************************************************************** + qgsappquerylogger.cpp + ------------------------- + begin : October 2021 + copyright : (C) 2021 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsappquerylogger.h" +#include "qgsqueryloggernode.h" +#include "qgsapplication.h" +#include "qgssettings.h" +#include "qgis.h" +#include +#include +#include + +QgsAppQueryLogger::QgsAppQueryLogger( QObject *parent ) + : QAbstractItemModel( parent ) + , mRootNode( std::make_unique< QgsDatabaseQueryLoggerRootNode >() ) +{ + // logger must be created on the main thread + Q_ASSERT( QThread::currentThread() == QApplication::instance()->thread() ); + + if ( QgsSettings().value( QStringLiteral( "logQueries" ), false, QgsSettings::App ).toBool() ) + enableLogging( true ); +} + +bool QgsAppQueryLogger::isLogging() const +{ + return mIsLogging; +} + +QgsAppQueryLogger::~QgsAppQueryLogger() = default; + +void QgsAppQueryLogger::enableLogging( bool enabled ) +{ + if ( enabled ) + { + connect( QgsApplication::databaseQueryLog(), &QgsDatabaseQueryLog::queryStarted, this, &QgsAppQueryLogger::queryLogged, Qt::UniqueConnection ); + } + else + { + disconnect( QgsApplication::databaseQueryLog(), &QgsDatabaseQueryLog::queryStarted, this, &QgsAppQueryLogger::queryLogged ); + } + mIsLogging = enabled; +} + +void QgsAppQueryLogger::clear() +{ + beginResetModel(); + mRequestGroups.clear(); + mRootNode->clear(); + endResetModel(); +} + +void QgsAppQueryLogger::queryLogged( const QgsDatabaseQueryLogEntry &query ) +{ + const int childCount = mRootNode->childCount(); + + beginInsertRows( QModelIndex(), childCount, childCount ); + + std::unique_ptr< QgsDatabaseQueryLoggerGroup > group = std::make_unique< QgsDatabaseQueryLoggerGroup >( query.query ); +// mRequestGroups.insert( parameters.requestId(), group.get() ); + mRootNode->addChild( std::move( group ) ); + endInsertRows(); +} + +QgsDatabaseQueryLoggerNode *QgsAppQueryLogger::index2node( const QModelIndex &index ) const +{ + if ( !index.isValid() ) + return mRootNode.get(); + + return reinterpret_cast( index.internalPointer() ); +} + +QList QgsAppQueryLogger::actions( const QModelIndex &index, QObject *parent ) +{ + QgsDatabaseQueryLoggerNode *node = index2node( index ); + if ( !node ) + return QList< QAction * >(); + + return node->actions( parent ); +} + +QModelIndex QgsAppQueryLogger::node2index( QgsDatabaseQueryLoggerNode *node ) const +{ + if ( !node || !node->parent() ) + return QModelIndex(); // this is the only root item -> invalid index + + QModelIndex parentIndex = node2index( node->parent() ); + + int row = node->parent()->indexOf( node ); + Q_ASSERT( row >= 0 ); + return index( row, 0, parentIndex ); +} + +QModelIndex QgsAppQueryLogger::indexOfParentLayerTreeNode( QgsDatabaseQueryLoggerNode *parentNode ) const +{ + Q_ASSERT( parentNode ); + + QgsDatabaseQueryLoggerGroup *grandParentNode = parentNode->parent(); + if ( !grandParentNode ) + return QModelIndex(); // root node -> invalid index + + int row = grandParentNode->indexOf( parentNode ); + Q_ASSERT( row >= 0 ); + + return createIndex( row, 0, parentNode ); +} + +void QgsAppQueryLogger::removeRequestRows( const QList &rows ) +{ + QList< int > res = rows; + std::sort( res.begin(), res.end(), std::greater< int >() ); + + for ( int row : std::as_const( res ) ) + { + int popId = data( index( row, 0, QModelIndex() ), QgsDatabaseQueryLoggerNode::RoleId ).toInt(); + mRequestGroups.remove( popId ); + + beginRemoveRows( QModelIndex(), row, row ); + mRootNode->removeRow( row ); + endRemoveRows(); + } +} + +QgsDatabaseQueryLoggerRootNode *QgsAppQueryLogger::rootGroup() +{ + return mRootNode.get(); +} + +int QgsAppQueryLogger::rowCount( const QModelIndex &parent ) const +{ + QgsDatabaseQueryLoggerNode *n = index2node( parent ); + if ( !n ) + return 0; + + return n->childCount(); +} + +int QgsAppQueryLogger::columnCount( const QModelIndex &parent ) const +{ + Q_UNUSED( parent ) + return 1; +} + +QModelIndex QgsAppQueryLogger::index( int row, int column, const QModelIndex &parent ) const +{ + if ( column < 0 || column >= columnCount( parent ) || + row < 0 || row >= rowCount( parent ) ) + return QModelIndex(); + + QgsDatabaseQueryLoggerGroup *n = dynamic_cast< QgsDatabaseQueryLoggerGroup * >( index2node( parent ) ); + if ( !n ) + return QModelIndex(); // have no children + + return createIndex( row, column, n->childAt( row ) ); +} + +QModelIndex QgsAppQueryLogger::parent( const QModelIndex &child ) const +{ + if ( !child.isValid() ) + return QModelIndex(); + + if ( QgsDatabaseQueryLoggerNode *n = index2node( child ) ) + { + return indexOfParentLayerTreeNode( n->parent() ); // must not be null + } + else + { + Q_ASSERT( false ); + return QModelIndex(); + } +} + +QVariant QgsAppQueryLogger::data( const QModelIndex &index, int role ) const +{ + if ( !index.isValid() || index.column() > 1 ) + return QVariant(); + + QgsDatabaseQueryLoggerNode *node = index2node( index ); + if ( !node ) + return QVariant(); + + return node->data( role ); +} + +Qt::ItemFlags QgsAppQueryLogger::flags( const QModelIndex &index ) const +{ + if ( !index.isValid() ) + { + Qt::ItemFlags rootFlags = Qt::ItemFlags(); + return rootFlags; + } + + Qt::ItemFlags f = Qt::ItemIsEnabled | Qt::ItemIsSelectable; + return f; +} + +QVariant QgsAppQueryLogger::headerData( int section, Qt::Orientation orientation, int role ) const +{ + if ( section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole ) + return tr( "Requests" ); + return QVariant(); +} + + +// +// QgsDatabaseQueryLoggerProxyModel +// + +QgsDatabaseQueryLoggerProxyModel::QgsDatabaseQueryLoggerProxyModel( QgsAppQueryLogger *logger, QObject *parent ) + : QSortFilterProxyModel( parent ) + , mLogger( logger ) +{ + setSourceModel( mLogger ); +} + +void QgsDatabaseQueryLoggerProxyModel::setFilterString( const QString &string ) +{ + mFilterString = string; + invalidateFilter(); +} + +bool QgsDatabaseQueryLoggerProxyModel::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const +{ + QgsDatabaseQueryLoggerNode *node = mLogger->index2node( mLogger->index( source_row, 0, source_parent ) ); +#if 0 + if ( QgsDatabaseQueryLoggerRequestGroup *request = dynamic_cast< QgsDatabaseQueryLoggerRequestGroup * >( node ) ) + { + return mFilterString.isEmpty() || request->url().url().contains( mFilterString, Qt::CaseInsensitive ); + } +#endif + + return true; +} diff --git a/src/app/devtools/querylogger/qgsappquerylogger.h b/src/app/devtools/querylogger/qgsappquerylogger.h new file mode 100644 index 00000000000..ede6e92a692 --- /dev/null +++ b/src/app/devtools/querylogger/qgsappquerylogger.h @@ -0,0 +1,147 @@ +/*************************************************************************** + qgsappquerylogger.h + ------------------------- + begin : October 2021 + copyright : (C) 2021 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGSAPPQUERYLOGGER_H +#define QGSAPPQUERYLOGGER_H + +#include +#include +#include +#include "qgsdbquerylog.h" +#include + +class QgsDatabaseQueryLoggerNode; +class QgsDatabaseQueryLoggerRequestGroup; +class QgsDatabaseQueryLoggerRootNode; +class QAction; + +/** + * \ingroup app + * \class QgsAppQueryLogger + * \brief Logs sql queries, converting them + * to a QAbstractItemModel representing the request and response details. + */ +class QgsAppQueryLogger : public QAbstractItemModel +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsAppQueryLogger, logging requests from the specified \a manager. + * + * \warning QgsAppQueryLogger must be created on the main thread. + */ + QgsAppQueryLogger( QObject *parent ); + ~QgsAppQueryLogger() override; + + /** + * Returns TRUE if the logger is currently logging activity. + */ + bool isLogging() const; + + // Implementation of virtual functions from QAbstractItemModel + + int rowCount( const QModelIndex &parent = QModelIndex() ) const override; + int columnCount( const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex parent( const QModelIndex &child ) const override; + QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const override; + Qt::ItemFlags flags( const QModelIndex &index ) const override; + QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const override; + + /** + * Returns node for given index. Returns root node for invalid index. + */ + QgsDatabaseQueryLoggerNode *index2node( const QModelIndex &index ) const; + + /** + * Returns a list of actions corresponding to the item at the specified \a index. + * + * The actions should be parented to \a parent. + */ + QList< QAction * > actions( const QModelIndex &index, QObject *parent ); + + /** + * Removes a list of request \a rows from the log. + */ + void removeRequestRows( const QList< int > &rows ); + + /** + * Returns the root node of the log. + */ + QgsDatabaseQueryLoggerRootNode *rootGroup(); + + static constexpr int MAX_LOGGED_REQUESTS = 1000; + + public slots: + + /** + * Enables or disables logging, depending on the value of \a enabled. + */ + void enableLogging( bool enabled ); + + /** + * Clears all logged entries. + */ + void clear(); + + private slots: + void queryLogged( const QgsDatabaseQueryLogEntry &query ); + + private: + + //! Returns index for a given node + QModelIndex node2index( QgsDatabaseQueryLoggerNode *node ) const; + QModelIndex indexOfParentLayerTreeNode( QgsDatabaseQueryLoggerNode *parentNode ) const; + + bool mIsLogging = false; + + std::unique_ptr< QgsDatabaseQueryLoggerRootNode > mRootNode; + + QHash< int, QgsDatabaseQueryLoggerRequestGroup * > mRequestGroups; + +}; + +/** + * \ingroup app + * \class QgsDatabaseQueryLoggerProxyModel + * \brief A proxy model for filtering QgsNetworkLogger models by url string subsets + * or request status. + */ +class QgsDatabaseQueryLoggerProxyModel : public QSortFilterProxyModel +{ + public: + + /** + * Constructor for QgsDatabaseQueryLoggerProxyModel, filtering the specified network \a logger. + */ + QgsDatabaseQueryLoggerProxyModel( QgsAppQueryLogger *logger, QObject *parent ); + + /** + * Sets a filter \a string to apply to request queries. + */ + void setFilterString( const QString &string ); + + protected: + bool filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const override; + + private: + + QgsAppQueryLogger *mLogger = nullptr; + + QString mFilterString; +}; + +#endif // QGSAPPQUERYLOGGER_H diff --git a/src/app/devtools/querylogger/qgsqueryloggernode.cpp b/src/app/devtools/querylogger/qgsqueryloggernode.cpp new file mode 100644 index 00000000000..ed25255682c --- /dev/null +++ b/src/app/devtools/querylogger/qgsqueryloggernode.cpp @@ -0,0 +1,201 @@ +/*************************************************************************** + QgsDatabaseQueryLoggernode.cpp + ------------------------- + begin : October 2021 + copyright : (C) 2021 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsqueryloggernode.h" +#include "qgis.h" +#include "qgsjsonutils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// +// QgsDatabaseQueryLoggerNode +// + +QgsDatabaseQueryLoggerNode::QgsDatabaseQueryLoggerNode() = default; +QgsDatabaseQueryLoggerNode::~QgsDatabaseQueryLoggerNode() = default; + + +// +// QgsDatabaseQueryLoggerGroup +// + +QgsDatabaseQueryLoggerGroup::QgsDatabaseQueryLoggerGroup( const QString &title ) + : mGroupTitle( title ) +{ +} + +void QgsDatabaseQueryLoggerGroup::addChild( std::unique_ptr child ) +{ + if ( !child ) + return; + + Q_ASSERT( !child->mParent ); + child->mParent = this; + + mChildren.emplace_back( std::move( child ) ); +} + +int QgsDatabaseQueryLoggerGroup::indexOf( QgsDatabaseQueryLoggerNode *child ) const +{ + Q_ASSERT( child->mParent == this ); + auto it = std::find_if( mChildren.begin(), mChildren.end(), [&]( const std::unique_ptr &p ) + { + return p.get() == child; + } ); + if ( it != mChildren.end() ) + return std::distance( mChildren.begin(), it ); + return -1; +} + +QgsDatabaseQueryLoggerNode *QgsDatabaseQueryLoggerGroup::childAt( int index ) +{ + Q_ASSERT( static_cast< std::size_t >( index ) < mChildren.size() ); + return mChildren[ index ].get(); +} + +void QgsDatabaseQueryLoggerGroup::clear() +{ + mChildren.clear(); +} + +QVariant QgsDatabaseQueryLoggerGroup::data( int role ) const +{ + switch ( role ) + { + case Qt::DisplayRole: + return mGroupTitle; + + default: + break; + } + return QVariant(); +} + +QVariant QgsDatabaseQueryLoggerGroup::toVariant() const +{ + QVariantMap res; + for ( const std::unique_ptr< QgsDatabaseQueryLoggerNode > &child : mChildren ) + { + if ( const QgsDatabaseQueryLoggerValueNode *valueNode = dynamic_cast< const QgsDatabaseQueryLoggerValueNode *>( child.get() ) ) + { + res.insert( valueNode->key(), valueNode->value() ); + } + } + return res; +} + +// +// QgsDatabaseQueryLoggerRootNode +// + +QgsDatabaseQueryLoggerRootNode::QgsDatabaseQueryLoggerRootNode() + : QgsDatabaseQueryLoggerGroup( QString() ) +{ + +} + +QVariant QgsDatabaseQueryLoggerRootNode::data( int ) const +{ + return QVariant(); +} + +void QgsDatabaseQueryLoggerRootNode::removeRow( int row ) +{ + mChildren.erase( mChildren.begin() + row ); +} + +QVariant QgsDatabaseQueryLoggerRootNode::toVariant() const +{ + QVariantList res; + for ( const std::unique_ptr< QgsDatabaseQueryLoggerNode > &child : mChildren ) + res << child->toVariant(); + return res; +} + + +// +// QgsDatabaseQueryLoggerValueNode +// +QgsDatabaseQueryLoggerValueNode::QgsDatabaseQueryLoggerValueNode( const QString &key, const QString &value, const QColor &color ) + : mKey( key ) + , mValue( value ) + , mColor( color ) +{ + +} + +QVariant QgsDatabaseQueryLoggerValueNode::data( int role ) const +{ + switch ( role ) + { + case Qt::DisplayRole: + case Qt::ToolTipRole: + { + return QStringLiteral( "%1: %2" ).arg( mKey.leftJustified( 30, ' ' ), mValue ); + } + + case Qt::ForegroundRole: + { + if ( mColor.isValid() ) + return QBrush( mColor ); + break; + } + default: + break; + } + return QVariant(); +} + +QList QgsDatabaseQueryLoggerValueNode::actions( QObject *parent ) +{ + QList< QAction * > res; + + QAction *copyAction = new QAction( QObject::tr( "Copy" ), parent ); + QObject::connect( copyAction, &QAction::triggered, copyAction, [ = ] + { + QApplication::clipboard()->setText( QStringLiteral( "%1: %2" ).arg( mKey, mValue ) ); + } ); + + res << copyAction; + + return res; +} + +// +// QgsDatabaseQueryLoggerGroup +// + +void QgsDatabaseQueryLoggerGroup::addKeyValueNode( const QString &key, const QString &value, const QColor &color ) +{ + addChild( std::make_unique< QgsDatabaseQueryLoggerValueNode >( key, value, color ) ); +} + + +QList QgsDatabaseQueryLoggerNode::actions( QObject * ) +{ + return QList< QAction * >(); +} + +QVariant QgsDatabaseQueryLoggerNode::toVariant() const +{ + return QVariant(); +} diff --git a/src/app/devtools/querylogger/qgsqueryloggernode.h b/src/app/devtools/querylogger/qgsqueryloggernode.h new file mode 100644 index 00000000000..494fd4c2b1f --- /dev/null +++ b/src/app/devtools/querylogger/qgsqueryloggernode.h @@ -0,0 +1,197 @@ +/*************************************************************************** + QgsDatabaseQueryLoggernode.h + ------------------------- + begin : October 2021 + copyright : (C) 2021 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QgsDatabaseQueryLoggerNODE_H +#define QgsDatabaseQueryLoggerNODE_H + +#include +#include +#include +#include +#include +#include + +class QAction; +class QgsDatabaseQueryLoggerGroup; + +/** + * \ingroup app + * \class QgsDatabaseQueryLoggerNode + * \brief Base class for nodes in the query logger model. + */ +class QgsDatabaseQueryLoggerNode +{ + public: + + //! Custom node data roles + enum Roles + { + RoleStatus = Qt::UserRole + 1, //!< Request status role + RoleId, //!< Request ID role + }; + + virtual ~QgsDatabaseQueryLoggerNode(); + + /** + * Returns the node's parent node. + * + * If parent is NULLPTR, the node is a root node + */ + QgsDatabaseQueryLoggerGroup *parent() { return mParent; } + + /** + * Returns the node's data for the specified model \a role. + */ + virtual QVariant data( int role = Qt::DisplayRole ) const = 0; + + /** + * Returns the number of child nodes owned by this node. + */ + virtual int childCount() const = 0; + + /** + * Returns a list of actions relating to the node. + * + * The actions should be parented to \a parent. + */ + virtual QList< QAction * > actions( QObject *parent ); + + /** + * Converts the node's contents to a variant. + */ + virtual QVariant toVariant() const; + + protected: + + QgsDatabaseQueryLoggerNode(); + + private: + + QgsDatabaseQueryLoggerGroup *mParent = nullptr; + friend class QgsDatabaseQueryLoggerGroup; +}; + +/** + * \ingroup app + * \class QgsDatabaseQueryLoggerGroup + * \brief Base class for query logger model "group" nodes, which contain children of their own. + */ +class QgsDatabaseQueryLoggerGroup : public QgsDatabaseQueryLoggerNode +{ + public: + + /** + * Adds a \a child node to this node. + */ + void addChild( std::unique_ptr< QgsDatabaseQueryLoggerNode > child ); + + /** + * Returns the index of the specified \a child node. + * + * \warning \a child must be a valid child of this node. + */ + int indexOf( QgsDatabaseQueryLoggerNode *child ) const; + + /** + * Returns the child at the specified \a index. + */ + QgsDatabaseQueryLoggerNode *childAt( int index ); + + /** + * Clears the group, removing all its children. + */ + void clear(); + + int childCount() const override final { return mChildren.size(); } + QVariant data( int role = Qt::DisplayRole ) const override; + QVariant toVariant() const override; + +// protected: + + /** + * Constructor for a QgsDatabaseQueryLoggerGroup, with the specified \a title. + */ + QgsDatabaseQueryLoggerGroup( const QString &title ); + + /** + * Adds a simple \a key: \a value node to the group. + */ + void addKeyValueNode( const QString &key, const QString &value, const QColor &color = QColor() ); + + private: + + std::deque< std::unique_ptr< QgsDatabaseQueryLoggerNode > > mChildren; + QString mGroupTitle; + friend class QgsDatabaseQueryLoggerRootNode; + friend class QgsAppQueryLogger; + +}; + +/** + * \ingroup app + * \class QgsDatabaseQueryLoggerValueNode + * \brief A "key: value" style node for the query logger model. + */ +class QgsDatabaseQueryLoggerValueNode : public QgsDatabaseQueryLoggerNode +{ + public: + + /** + * Constructor for QgsDatabaseQueryLoggerValueNode, with the specified \a key (usually translated) and \a value. + */ + QgsDatabaseQueryLoggerValueNode( const QString &key, const QString &value, const QColor &color = QColor() ); + + /** + * Returns the node's key. + */ + QString key() const { return mKey; } + + /** + * Returns the node's value. + */ + QString value() const { return mValue; } + + QVariant data( int role = Qt::DisplayRole ) const override final; + int childCount() const override final { return 0; } + QList< QAction * > actions( QObject *parent ) override final; + + private: + + QString mKey; + QString mValue; + QColor mColor; +}; + +/** + * \ingroup app + * \class QgsDatabaseQueryLoggerRootNode + * \brief Root node for the query logger model. + */ +class QgsDatabaseQueryLoggerRootNode final : public QgsDatabaseQueryLoggerGroup +{ + public: + + QgsDatabaseQueryLoggerRootNode(); + QVariant data( int role = Qt::DisplayRole ) const override final; + + /** + * Removes a \a row from the root group. + */ + void removeRow( int row ); + + QVariant toVariant() const override; +}; + + +#endif // QgsDatabaseQueryLoggerNODE_H diff --git a/src/app/devtools/querylogger/qgsqueryloggerpanelwidget.cpp b/src/app/devtools/querylogger/qgsqueryloggerpanelwidget.cpp new file mode 100644 index 00000000000..37c8b8580b6 --- /dev/null +++ b/src/app/devtools/querylogger/qgsqueryloggerpanelwidget.cpp @@ -0,0 +1,205 @@ +/*************************************************************************** + QgsDatabaseQueryLoggerpanelwidget.cpp + ------------------------- + begin : October 2021 + copyright : (C) 2021 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsapplication.h" +#include "qgsguiutils.h" +#include "qgsjsonutils.h" +#include "qgsqueryloggerpanelwidget.h" +#include "qgsqueryloggernode.h" +#include "qgsappquerylogger.h" +#include "qgssettings.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +// +// QgsDatabaseQueryLoggerTreeView +// + +QgsDatabaseQueryLoggerTreeView::QgsDatabaseQueryLoggerTreeView( QgsAppQueryLogger *logger, QWidget *parent ) + : QTreeView( parent ) + , mLogger( logger ) +{ + connect( this, &QTreeView::expanded, this, &QgsDatabaseQueryLoggerTreeView::itemExpanded ); + + setFont( QFontDatabase::systemFont( QFontDatabase::FixedFont ) ); + + mProxyModel = new QgsDatabaseQueryLoggerProxyModel( mLogger, this ); + setModel( mProxyModel ); + + setContextMenuPolicy( Qt::CustomContextMenu ); + connect( this, &QgsDatabaseQueryLoggerTreeView::customContextMenuRequested, this, &QgsDatabaseQueryLoggerTreeView::contextMenu ); + + connect( verticalScrollBar(), &QAbstractSlider::sliderMoved, this, [this]( int value ) + { + if ( value == verticalScrollBar()->maximum() ) + mAutoScroll = true; + else + mAutoScroll = false; + } ); + + connect( mLogger, &QAbstractItemModel::rowsInserted, this, [ = ] + { + if ( mLogger->rowCount() > ( QgsAppQueryLogger::MAX_LOGGED_REQUESTS * 1.2 ) ) // 20 % more as buffer + { + // never trim expanded nodes + const int toTrim = mLogger->rowCount() - QgsAppQueryLogger::MAX_LOGGED_REQUESTS; + int trimmed = 0; + QList< int > rowsToTrim; + rowsToTrim.reserve( toTrim ); + for ( int i = 0; i < mLogger->rowCount(); ++i ) + { + const QModelIndex proxyIndex = mProxyModel->mapFromSource( mLogger->index( i, 0 ) ); + if ( !proxyIndex.isValid() || !isExpanded( proxyIndex ) ) + { + rowsToTrim << i; + trimmed++; + } + if ( trimmed == toTrim ) + break; + } + + mLogger->removeRequestRows( rowsToTrim ); + } + + if ( mAutoScroll ) + scrollToBottom(); + } ); + + mMenu = new QMenu( this ); +} + +void QgsDatabaseQueryLoggerTreeView::setFilterString( const QString &string ) +{ + mProxyModel->setFilterString( string ); +} + +void QgsDatabaseQueryLoggerTreeView::itemExpanded( const QModelIndex &index ) +{ + // if the item is a QgsNetworkLoggerRequestGroup item, open all children (show ALL info of it) + // we want to scroll to last request + + // only expand all children on QgsNetworkLoggerRequestGroup nodes (which don't have a valid parent!) + if ( !index.parent().isValid() ) + expandChildren( index ); + + // make ALL request information visible by scrolling view to it + scrollTo( index ); +} + +void QgsDatabaseQueryLoggerTreeView::contextMenu( QPoint point ) +{ + const QModelIndex viewModelIndex = indexAt( point ); + const QModelIndex modelIndex = mProxyModel->mapToSource( viewModelIndex ); + + if ( modelIndex.isValid() ) + { + mMenu->clear(); + + const QList< QAction * > actions = mLogger->actions( modelIndex, mMenu ); + mMenu->addActions( actions ); + if ( !mMenu->actions().empty() ) + { + mMenu->exec( viewport()->mapToGlobal( point ) ); + } + } +} + +void QgsDatabaseQueryLoggerTreeView::expandChildren( const QModelIndex &index ) +{ + if ( !index.isValid() ) + return; + + const int count = model()->rowCount( index ); + for ( int i = 0; i < count; ++i ) + { + const QModelIndex childIndex = model()->index( i, 0, index ); + expandChildren( childIndex ); + } + if ( !isExpanded( index ) ) + expand( index ); +} + + +// +// QgsDatabaseQueryLoggerPanelWidget +// + +QgsDatabaseQueryLoggerPanelWidget::QgsDatabaseQueryLoggerPanelWidget( QgsAppQueryLogger *logger, QWidget *parent ) + : QgsDevToolWidget( parent ) + , mLogger( logger ) +{ + setupUi( this ); + + mTreeView = new QgsDatabaseQueryLoggerTreeView( mLogger ); + verticalLayout->addWidget( mTreeView ); + mToolbar->setIconSize( QgsGuiUtils::iconSize( true ) ); + + mFilterLineEdit->setShowClearButton( true ); + mFilterLineEdit->setShowSearchIcon( true ); + mFilterLineEdit->setPlaceholderText( tr( "Filter requests" ) ); + + mActionRecord->setChecked( mLogger->isLogging() ); + + connect( mFilterLineEdit, &QgsFilterLineEdit::textChanged, mTreeView, &QgsDatabaseQueryLoggerTreeView::setFilterString ); + connect( mActionClear, &QAction::triggered, mLogger, &QgsAppQueryLogger::clear ); + connect( mActionRecord, &QAction::toggled, this, [ = ]( bool enabled ) + { + QgsSettings().setValue( QStringLiteral( "logNetworkRequests" ), enabled, QgsSettings::App ); + mLogger->enableLogging( enabled ); + } ); + connect( mActionSaveLog, &QAction::triggered, this, [ = ]() + { + if ( QMessageBox::warning( this, tr( "Save Network Log" ), + tr( "Security warning: network logs may contain sensitive data including usernames or passwords. Treat this log as confidential and be careful who you share it with. Continue?" ), QMessageBox::Yes | QMessageBox::No ) == QMessageBox::No ) + return; + + const QString saveFilePath = QFileDialog::getSaveFileName( this, tr( "Save Network Log" ), QDir::homePath(), tr( "Log files" ) + " (*.json)" ); + if ( saveFilePath.isEmpty() ) + { + return; + } + + QFile exportFile( saveFilePath ); + if ( !exportFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) ) + { + return; + } + QTextStream fout( &exportFile ); + + const QVariant value = mLogger->rootGroup()->toVariant(); + const QString json = QString::fromStdString( QgsJsonUtils::jsonFromVariant( value ).dump( 2 ) ); + + fout << json; + } ); + + + QMenu *settingsMenu = new QMenu( this ); + QToolButton *settingsButton = new QToolButton(); + settingsButton->setAutoRaise( true ); + settingsButton->setToolTip( tr( "Settings" ) ); + settingsButton->setMenu( settingsMenu ); + settingsButton->setPopupMode( QToolButton::InstantPopup ); + settingsButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionOptions.svg" ) ) ); + mToolbar->addWidget( settingsButton ); +} diff --git a/src/app/devtools/querylogger/qgsqueryloggerpanelwidget.h b/src/app/devtools/querylogger/qgsqueryloggerpanelwidget.h new file mode 100644 index 00000000000..90ed89dd7ae --- /dev/null +++ b/src/app/devtools/querylogger/qgsqueryloggerpanelwidget.h @@ -0,0 +1,84 @@ +/*************************************************************************** + QgsDatabaseQueryLoggerpanelwidget.h + ------------------------- + begin : October 2021 + copyright : (C) 2021 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QgsDatabaseQueryLoggerPANELWIDGET_H +#define QgsDatabaseQueryLoggerPANELWIDGET_H + +#include "qgsdevtoolwidget.h" +#include "ui_qgsqueryloggerpanelbase.h" +#include + +class QgsAppQueryLogger; +class QgsDatabaseQueryLoggerProxyModel; + +/** + * \ingroup app + * \class QgsDatabaseQueryLoggerTreeView + * \brief A custom QTreeView subclass for showing logged database queries. + */ +class QgsDatabaseQueryLoggerTreeView: public QTreeView +{ + Q_OBJECT + public: + + /** + * Constructor for QgsDatabaseQueryLoggerTreeView, attached to the specified \a logger. + */ + QgsDatabaseQueryLoggerTreeView( QgsAppQueryLogger *logger, QWidget *parent = nullptr ); + + public slots: + + /** + * Sets a filter \a string to apply to request URLs. + */ + void setFilterString( const QString &string ); + + private slots: + void itemExpanded( const QModelIndex &index ); + void contextMenu( QPoint point ); + + private: + + void expandChildren( const QModelIndex &index ); + QMenu *mMenu = nullptr; + QgsAppQueryLogger *mLogger = nullptr; + QgsDatabaseQueryLoggerProxyModel *mProxyModel = nullptr; + bool mAutoScroll = true; +}; + + +/** + * \ingroup app + * \class QgsDatabaseQueryLoggerPanelWidget + * \brief A panel widget showing logged network requests. + */ +class QgsDatabaseQueryLoggerPanelWidget : public QgsDevToolWidget, private Ui::QgsDatabaseQueryLoggerPanelBase +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsDatabaseQueryLoggerPanelWidget, linked with the specified \a logger. + */ + QgsDatabaseQueryLoggerPanelWidget( QgsAppQueryLogger *logger, QWidget *parent ); + + private: + + QgsDatabaseQueryLoggerTreeView *mTreeView = nullptr; + QgsAppQueryLogger *mLogger = nullptr; +}; + + +#endif // QgsDatabaseQueryLoggerPANELWIDGET_H diff --git a/src/app/devtools/querylogger/qgsqueryloggerwidgetfactory.cpp b/src/app/devtools/querylogger/qgsqueryloggerwidgetfactory.cpp new file mode 100644 index 00000000000..134f3059697 --- /dev/null +++ b/src/app/devtools/querylogger/qgsqueryloggerwidgetfactory.cpp @@ -0,0 +1,29 @@ +/*************************************************************************** + QgsDatabaseQueryLoggerwidgetfactory.cpp + ------------------------- + begin : October 2021 + copyright : (C) 2021 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsqueryloggerwidgetfactory.h" +#include "qgsqueryloggerpanelwidget.h" +#include "qgsapplication.h" + +QgsDatabaseQueryLoggerWidgetFactory::QgsDatabaseQueryLoggerWidgetFactory( QgsAppQueryLogger *logger ) + : QgsDevToolWidgetFactory( QObject::tr( "Query Logger" ), QgsApplication::getThemeIcon( QStringLiteral( "propertyicons/network_and_proxy.svg" ) ) ) + , mLogger( logger ) +{ +} + +QgsDevToolWidget *QgsDatabaseQueryLoggerWidgetFactory::createWidget( QWidget *parent ) const +{ + return new QgsDatabaseQueryLoggerPanelWidget( mLogger, parent ); +} diff --git a/src/app/devtools/querylogger/qgsqueryloggerwidgetfactory.h b/src/app/devtools/querylogger/qgsqueryloggerwidgetfactory.h new file mode 100644 index 00000000000..7e46d37c946 --- /dev/null +++ b/src/app/devtools/querylogger/qgsqueryloggerwidgetfactory.h @@ -0,0 +1,35 @@ +/*************************************************************************** + QgsDatabaseQueryLoggerwidgetfactory.h + ------------------------- + begin : October 2021 + copyright : (C) 2021 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QgsDatabaseQueryLoggerWIDGETFACTORY_H +#define QgsDatabaseQueryLoggerWIDGETFACTORY_H + +#include "qgsdevtoolwidgetfactory.h" + +class QgsAppQueryLogger; + +class QgsDatabaseQueryLoggerWidgetFactory: public QgsDevToolWidgetFactory +{ + public: + + QgsDatabaseQueryLoggerWidgetFactory( QgsAppQueryLogger *logger ); + QgsDevToolWidget *createWidget( QWidget *parent = nullptr ) const override; + + private: + + QgsAppQueryLogger *mLogger = nullptr; +}; + + +#endif // QgsDatabaseQueryLoggerWIDGETFACTORY_H diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 01a197e8d3d..130e7e491c1 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -437,6 +437,8 @@ Q_GUI_EXPORT extern int qt_defaultDpiX(); #include "qgsuserprofilemanager.h" #include "qgsuserprofile.h" #include "qgsnetworkloggerwidgetfactory.h" +#include "devtools/querylogger/qgsappquerylogger.h" +#include "devtools/querylogger/qgsqueryloggerwidgetfactory.h" #include "devtools/profiler/qgsprofilerwidgetfactory.h" #include "qgsabstractdatabaseproviderconnection.h" #include "qgszipitem.h" @@ -989,6 +991,10 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipBadLayers mNetworkLogger = new QgsNetworkLogger( QgsNetworkAccessManager::instance(), this ); endProfile(); + startProfile( tr( "Create database query logger" ) ); + mQueryLogger = new QgsAppQueryLogger( this ); + endProfile(); + // load GUI: actions, menus, toolbars startProfile( tr( "Setting up UI" ) ); setupUi( this ); @@ -1829,6 +1835,7 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipBadLayers mBearingNumericFormat.reset( QgsLocalDefaultSettings::bearingFormat() ); mNetworkLoggerWidgetFactory.reset( std::make_unique< QgsNetworkLoggerWidgetFactory >( mNetworkLogger ) ); + mQueryLoggerWidgetFactory.reset( std::make_unique< QgsDatabaseQueryLoggerWidgetFactory >( mQueryLogger ) ); // update windows qApp->processEvents(); diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index a623f2c8acd..3ada37a8afa 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -151,6 +151,7 @@ class QgsDevToolsPanelWidget; class QgsDevToolWidgetFactory; class QgsNetworkLogger; class QgsNetworkLoggerWidgetFactory; +class QgsAppQueryLogger; class QgsMapToolCapture; class QgsElevationProfileWidget; @@ -2742,6 +2743,8 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow QgsNetworkLogger *mNetworkLogger = nullptr; QgsScopedDevToolWidgetFactory mNetworkLoggerWidgetFactory; QgsScopedDevToolWidgetFactory mStartupProfilerWidgetFactory; + QgsAppQueryLogger *mQueryLogger = nullptr; + QgsScopedDevToolWidgetFactory mQueryLoggerWidgetFactory; QgsScopedOptionsWidgetFactory mCodeEditorWidgetFactory; QgsScopedOptionsWidgetFactory mBabelGpsDevicesWidgetFactory; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 9b97946218a..79d39f59c82 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -355,6 +355,7 @@ set(QGIS_CORE_SRCS qgsdataprovidertemporalcapabilities.cpp qgsdatetimestatisticalsummary.cpp qgsdbfilterproxymodel.cpp + qgsdbquerylog.cpp qgsdefaultvalue.cpp qgsdiagramrenderer.cpp qgsdistancearea.cpp @@ -1004,6 +1005,7 @@ set(QGIS_CORE_HDRS qgsdatasourceuri.h qgsdatetimestatisticalsummary.h qgsdbfilterproxymodel.h + qgsdbquerylog.h qgsdefaultvalue.h qgsdiagramrenderer.h qgsdistancearea.h diff --git a/src/core/qgsapplication.cpp b/src/core/qgsapplication.cpp index 9164a1f1e2c..b2c5032a2dc 100644 --- a/src/core/qgsapplication.cpp +++ b/src/core/qgsapplication.cpp @@ -77,6 +77,7 @@ #include "qgslocator.h" #include "qgsreadwritelocker.h" #include "qgsbabelformatregistry.h" +#include "qgsdbquerylog.h" #include "gps/qgsgpsconnectionregistry.h" #include "processing/qgsprocessingregistry.h" @@ -265,6 +266,7 @@ void QgsApplication::init( QString profileFolder ) std::call_once( sMetaTypesRegistered, [] { qRegisterMetaType( "QgsGeometry::Error" ); + qRegisterMetaType( "QgsDatabaseQueryLogEntry" ); qRegisterMetaType( "QgsProcessingFeatureSourceDefinition" ); qRegisterMetaType( "QgsProcessingOutputLayerDefinition" ); qRegisterMetaType( "QgsUnitTypes::LayoutUnit" ); @@ -2433,6 +2435,11 @@ QgsRecentStyleHandler *QgsApplication::recentStyleHandler() return members()->mRecentStyleHandler; } +QgsDatabaseQueryLog *QgsApplication::databaseQueryLog() +{ + return members()->mQueryLogger; +} + QgsStyleModel *QgsApplication::defaultStyleModel() { return members()->mStyleModel; @@ -2512,6 +2519,11 @@ QgsApplication::ApplicationMembers::ApplicationMembers() mMessageLog = new QgsMessageLog(); QgsRuntimeProfiler *profiler = QgsRuntimeProfiler::threadLocalInstance(); + { + profiler->start( tr( "Create query logger" ) ); + mQueryLogger = new QgsDatabaseQueryLog(); + profiler->end(); + } { profiler->start( tr( "Setup coordinate reference system registry" ) ); mCrsRegistry = new QgsCoordinateReferenceSystemRegistry(); @@ -2726,6 +2738,7 @@ QgsApplication::ApplicationMembers::~ApplicationMembers() delete mConnectionRegistry; delete mLocalizedDataPathRegistry; delete mCrsRegistry; + delete mQueryLogger; delete mSettingsRegistryCore; } diff --git a/src/core/qgsapplication.h b/src/core/qgsapplication.h index 040dcc2462c..efed5c9d70a 100644 --- a/src/core/qgsapplication.h +++ b/src/core/qgsapplication.h @@ -72,6 +72,7 @@ class QgsPointCloudRendererRegistry; class QgsTileDownloadManager; class QgsCoordinateReferenceSystemRegistry; class QgsRecentStyleHandler; +class QgsDatabaseQueryLog; /** * \ingroup core @@ -820,6 +821,13 @@ class CORE_EXPORT QgsApplication : public QApplication */ static QgsRecentStyleHandler *recentStyleHandler() SIP_KEEPREFERENCE; + /** + * Returns the database query log. + * + * \since QGIS 3.24 + */ + static QgsDatabaseQueryLog *databaseQueryLog() SIP_KEEPREFERENCE; + /** * Returns a shared QgsStyleModel containing the default style library (see QgsStyle::defaultStyle()). * @@ -1136,6 +1144,7 @@ class CORE_EXPORT QgsApplication : public QApplication QgsTileDownloadManager *mTileDownloadManager = nullptr; QgsStyleModel *mStyleModel = nullptr; QgsRecentStyleHandler *mRecentStyleHandler = nullptr; + QgsDatabaseQueryLog *mQueryLogger = nullptr; QString mNullRepresentation; QStringList mSvgPathCache; bool mSvgPathCacheValid = false; diff --git a/src/core/qgsdbquerylog.cpp b/src/core/qgsdbquerylog.cpp new file mode 100644 index 00000000000..523b6887942 --- /dev/null +++ b/src/core/qgsdbquerylog.cpp @@ -0,0 +1,68 @@ +/*************************************************************************** + qgsdbquerylog.cpp + ------------ + Date : October 2021 + Copyright : (C) 2021 by Nyall Dawson + Email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsdbquerylog.h" +#include "qgsapplication.h" +#include + +// +// QgsDatabaseQueryLogEntry +// + +QAtomicInt QgsDatabaseQueryLogEntry::sQueryId = 0; + +QgsDatabaseQueryLogEntry::QgsDatabaseQueryLogEntry( const QString &query ) + : queryId( ++sQueryId ) + , query( query ) + , startedTime( QDateTime::currentMSecsSinceEpoch() ) +{} + + +// +// QgsDatabaseQueryLog +// + +QgsDatabaseQueryLog::QgsDatabaseQueryLog( QObject *parent ) + : QObject( parent ) +{ + +} + +void QgsDatabaseQueryLog::log( const QgsDatabaseQueryLogEntry &query ) +{ + QMetaObject::invokeMethod( QgsApplication::databaseQueryLog(), "queryStartedPrivate", Qt::QueuedConnection, Q_ARG( QgsDatabaseQueryLogEntry, query ) ); +} + +void QgsDatabaseQueryLog::finished( const QgsDatabaseQueryLogEntry &query ) +{ + // record time of completion + QgsDatabaseQueryLogEntry finishedQuery = query; + finishedQuery.finishedTime = QDateTime::currentMSecsSinceEpoch(); + + QMetaObject::invokeMethod( QgsApplication::databaseQueryLog(), "queryFinishedPrivate", Qt::QueuedConnection, Q_ARG( QgsDatabaseQueryLogEntry, finishedQuery ) ); +} + +void QgsDatabaseQueryLog::queryStartedPrivate( const QgsDatabaseQueryLogEntry &query ) +{ + QgsDebugMsg( query.query ); + emit queryStarted( query ); +} + +void QgsDatabaseQueryLog::queryFinishedPrivate( const QgsDatabaseQueryLogEntry &query ) +{ + QgsDebugMsg( query.query ); + emit queryFinished( query ); +} + diff --git a/src/core/qgsdbquerylog.h b/src/core/qgsdbquerylog.h new file mode 100644 index 00000000000..be10542f030 --- /dev/null +++ b/src/core/qgsdbquerylog.h @@ -0,0 +1,170 @@ +/*************************************************************************** + qgsdbquerylog.h + ------------ + Date : October 2021 + Copyright : (C) 2021 by Nyall Dawson + Email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program 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 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSDBQUERYLOG_H +#define QGSDBQUERYLOG_H + +#include "qgis_core.h" +#include "qgis.h" +#include +#include + +/** + * \ingroup core + * \class QgsDatabaseQueryLogEntry + * \brief Encapsulates a logged database query. + * + * \since QGIS 3.24 + */ +class CORE_EXPORT QgsDatabaseQueryLogEntry +{ + public: + + /** + * Constructor for QgsDatabaseQueryLogEntry. + */ + QgsDatabaseQueryLogEntry( const QString &query = QString() ); + + /** + * Unique query ID. + * + * This ID will automatically be set on creation of a new QgsDatabaseQueryLogEntry object. + */ + int queryId = 0; + + //! The logged database query (e.g. the SQL query) + QString query; + + /** + * Time when the query started (in milliseconds since epoch). + * + * This will be automatically recorded on creation of a new QgsDatabaseQueryLogEntry object. + */ + quint64 startedTime = 0; + + /** + * Time when the query finished (in milliseconds since epoch), if available. + */ + quint64 finishedTime = 0; + + /** + * The QGIS class which initiated the query. + * + * c++ code can automatically populate this through the QgsSetQueryLogClass macro. + */ + QString initiatorClass; + + /** + * Code file location for the query origin. + * + * c++ code can automatically populate this through the QgsSetQueryLogClass macro. + */ + QString origin; + + private: + + static QAtomicInt sQueryId; +}; + +Q_DECLARE_METATYPE( QgsDatabaseQueryLogEntry ); + +#ifndef SIP_RUN +#include "qgsconfig.h" +constexpr int sQueryLoggerFilePrefixLength = CMAKE_SOURCE_DIR[sizeof( CMAKE_SOURCE_DIR ) - 1] == '/' ? sizeof( CMAKE_SOURCE_DIR ) + 1 : sizeof( CMAKE_SOURCE_DIR ); +#define QgsSetQueryLogClass(entry, _class) entry.initiatorClass = _class; entry.origin = QString(QString( __FILE__ ).mid( sQueryLoggerFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + __FUNCTION__ + ")"); +#endif + +/** + * \ingroup core + * \class QgsDatabaseQueryLog + * \brief Handles logging of database queries. + * + * QgsDatabaseQueryLog is not usually directly created, but rather accessed through + * QgsApplication::databaseQueryLog(). Generally, clients should only access the + * static log() method to register their queries. + * + * ### Example + * + * \code{.py} + * # Log a database query + * QgsDatabaseQueryLog.log('SELECT * FROM my_table') + * \endcode + * + * + * \since QGIS 3.24 + */ +class CORE_EXPORT QgsDatabaseQueryLog: public QObject +{ + Q_OBJECT + + public: + + /** + * Creates a new query log. + * + * QgsDatabaseQueryLog is not usually directly created, but rather accessed through + * QgsApplication::databaseQueryLog(). + */ + QgsDatabaseQueryLog( QObject *parent = nullptr ); + + /** + * Logs a database \a query as starting. + * + * This method can be safely called from any thread. + */ + static void log( const QgsDatabaseQueryLogEntry &query ); + + /** + * Records that the database \a query as finished. + * + * This method can be safely called from any thread. + */ + static void finished( const QgsDatabaseQueryLogEntry &query ); + + public slots: + + /** + * Internal slot for logging queries as start. + * + * \note Not available in Python bindings. + */ + void queryStartedPrivate( const QgsDatabaseQueryLogEntry &query ) SIP_SKIP; + + /** + * Internal slot for logging queries as finished. + * + * \note Not available in Python bindings. + */ + void queryFinishedPrivate( const QgsDatabaseQueryLogEntry &query ) SIP_SKIP; + + signals: + + /** + * Emitted whenever a database query is started. + * + * \note Not available in Python bindings + */ + void queryStarted( const QgsDatabaseQueryLogEntry &query ) SIP_SKIP; + + /** + * Emitted whenever a database query has finished executing. + * + * \note Not available in Python bindings + */ + void queryFinished( const QgsDatabaseQueryLogEntry &query ) SIP_SKIP; + +}; + +#endif // QGSDBQUERYLOG_H diff --git a/src/providers/postgres/qgspostgresconn.cpp b/src/providers/postgres/qgspostgresconn.cpp index edd52477421..3f9d40adb71 100644 --- a/src/providers/postgres/qgspostgresconn.cpp +++ b/src/providers/postgres/qgspostgresconn.cpp @@ -31,6 +31,7 @@ #include "qgspostgresstringutils.h" #include "qgspostgresconnpool.h" #include "qgsvariantutils.h" +#include "qgsdbquerylog.h" #include #include @@ -49,6 +50,14 @@ const int PG_DEFAULT_TIMEOUT = 30; +#include "qgsconfig.h" +constexpr int sPostgresConQueryLogFilePrefixLength = CMAKE_SOURCE_DIR[sizeof( CMAKE_SOURCE_DIR ) - 1] == '/' ? sizeof( CMAKE_SOURCE_DIR ) + 1 : sizeof( CMAKE_SOURCE_DIR ); +#define LoggedPQExecNR(query) { QgsDatabaseQueryLogEntry logEntry( query ); entry.initiatorClass = _class; entry.origin = QString(QString( __FILE__ ).mid( sPostgresConQueryLogFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + __FUNCTION__ + ")"); \ + QgsDatabaseQueryLog::log( entry ); \ + PQexecNR( query ); \ + QgsDatabaseQueryLog::finished( entry ); } + + QgsPostgresResult::~QgsPostgresResult() { if ( mRes ) @@ -412,8 +421,17 @@ QgsPostgresConn::QgsPostgresConn( const QString &conninfo, bool readOnly, bool s if ( mPostgresqlVersion >= 90000 ) { - PQexecNR( QStringLiteral( "SET application_name='QGIS'" ) ); - PQexecNR( QStringLiteral( "SET extra_float_digits=3" ) ); + QString query = QStringLiteral( "SET application_name='QGIS'" ); + QgsDatabaseQueryLogEntry entry( query ); + QgsSetQueryLogClass( entry, "QgsPostgresFeatureIterator" ); + QgsDatabaseQueryLog::log( entry ); + PQexecNR( query ); + + query = QStringLiteral( "SET extra_float_digits=3" ); + entry = QgsDatabaseQueryLogEntry( query ); + QgsSetQueryLogClass( entry, "QgsPostgresFeatureIterator" ); + QgsDatabaseQueryLog::log( entry ); + PQexecNR( query ); } PQsetNoticeProcessor( mConn, noticeProcessor, nullptr ); @@ -817,7 +835,11 @@ bool QgsPostgresConn::getTableInfo( bool searchGeometryColumnsOnly, bool searchP QgsMessageLog::logMessage( tr( "Database connection was successful, but the accessible tables could not be determined. The error message from the database was:\n%1\n" ) .arg( result.PQresultErrorMessage() ), tr( "PostGIS" ) ); - PQexecNR( QStringLiteral( "COMMIT" ) ); + QString query = QStringLiteral( "COMMIT" ); + QgsDatabaseQueryLogEntry entry( query ); + QgsSetQueryLogClass( entry, "QgsPostgresConn" ); + QgsDatabaseQueryLog::log( entry ); + PQexecNR( query ); return false; } @@ -1021,7 +1043,11 @@ bool QgsPostgresConn::getSchemas( QList &schemas ) result = PQexec( sql, true ); if ( result.PQresultStatus() != PGRES_TUPLES_OK ) { - PQexecNR( QStringLiteral( "COMMIT" ) ); + QString query = QStringLiteral( "COMMIT" ); + QgsDatabaseQueryLogEntry entry( query ); + QgsSetQueryLogClass( entry, "QgsPostgresConn" ); + QgsDatabaseQueryLog::log( entry ); + PQexecNR( query ); return false; } @@ -1428,7 +1454,7 @@ int QgsPostgresConn::PQCancel() return result; } -bool QgsPostgresConn::openCursor( const QString &cursorName, const QString &sql ) +bool QgsPostgresConn::openCursor( const QString &cursorName, const QString &sql, const QgsDatabaseQueryLogEntry &logEntry ) { QMutexLocker locker( &mLock ); // to protect access to mOpenCursors QString preStr; @@ -1442,8 +1468,12 @@ bool QgsPostgresConn::openCursor( const QString &cursorName, const QString &sql preStr = QStringLiteral( "BEGIN;" ); } QgsDebugMsgLevel( QStringLiteral( "Binary cursor %1 for %2" ).arg( cursorName, sql ), 3 ); - return PQexecNR( QStringLiteral( "%1DECLARE %2 BINARY CURSOR%3 FOR %4" ). - arg( preStr, cursorName, !mTransaction ? QString() : QStringLiteral( " WITH HOLD" ), sql ) ); + const QString query = QStringLiteral( "%1DECLARE %2 BINARY CURSOR%3 FOR %4" ). + arg( preStr, cursorName, !mTransaction ? QString() : QStringLiteral( " WITH HOLD" ), sql ); + QgsDatabaseQueryLogEntry entry = logEntry; + entry.query = query; + QgsDatabaseQueryLog::log( entry ); + return PQexecNR( query ); } bool QgsPostgresConn::closeCursor( const QString &cursorName ) @@ -1457,7 +1487,11 @@ bool QgsPostgresConn::closeCursor( const QString &cursorName ) postStr = QStringLiteral( ";COMMIT" ); } - if ( !PQexecNR( QStringLiteral( "CLOSE %1%2" ).arg( cursorName, postStr ) ) ) + QString query = QStringLiteral( "CLOSE %1%2" ).arg( cursorName, postStr ); + QgsDatabaseQueryLogEntry entry( query ); + QgsSetQueryLogClass( entry, "QgsPostgresConn" ); + QgsDatabaseQueryLog::log( entry ); + if ( !PQexecNR( query ) ) return false; return true; @@ -1495,7 +1529,11 @@ bool QgsPostgresConn::PQexecNR( const QString &query ) if ( PQstatus() == CONNECTION_OK ) { - PQexecNR( QStringLiteral( "ROLLBACK" ) ); + QString query = QStringLiteral( "ROLLBACK" ); + QgsDatabaseQueryLogEntry entry( query ); + QgsSetQueryLogClass( entry, "QgsPostgresConn" ); + QgsDatabaseQueryLog::log( entry ); + PQexecNR( query ); } return false; @@ -1575,11 +1613,19 @@ bool QgsPostgresConn::begin() QMutexLocker locker( &mLock ); if ( mTransaction ) { - return PQexecNR( QStringLiteral( "SAVEPOINT transaction_savepoint" ) ); + QString query = QStringLiteral( "SAVEPOINT transaction_savepoint" ); + QgsDatabaseQueryLogEntry entry( query ); + QgsSetQueryLogClass( entry, "QgsPostgresConn" ); + QgsDatabaseQueryLog::log( entry ); + return PQexecNR( query ); } else { - return PQexecNR( QStringLiteral( "BEGIN" ) ); + QString query = QStringLiteral( "BEGIN" ); + QgsDatabaseQueryLogEntry entry( query ); + QgsSetQueryLogClass( entry, "QgsPostgresConn" ); + QgsDatabaseQueryLog::log( entry ); + return PQexecNR( query ); } } @@ -1588,11 +1634,19 @@ bool QgsPostgresConn::commit() QMutexLocker locker( &mLock ); if ( mTransaction ) { - return PQexecNR( QStringLiteral( "RELEASE SAVEPOINT transaction_savepoint" ) ); + QString query = QStringLiteral( "RELEASE SAVEPOINT transaction_savepoint" ); + QgsDatabaseQueryLogEntry entry( query ); + QgsSetQueryLogClass( entry, "QgsPostgresConn" ); + QgsDatabaseQueryLog::log( entry ); + return PQexecNR( query ); } else { - return PQexecNR( QStringLiteral( "COMMIT" ) ); + QString query = QStringLiteral( "COMMIT" ); + QgsDatabaseQueryLogEntry entry( query ); + QgsSetQueryLogClass( entry, "QgsPostgresConn" ); + QgsDatabaseQueryLog::log( entry ); + return PQexecNR( query ); } } @@ -1601,12 +1655,29 @@ bool QgsPostgresConn::rollback() QMutexLocker locker( &mLock ); if ( mTransaction ) { - return PQexecNR( QStringLiteral( "ROLLBACK TO SAVEPOINT transaction_savepoint" ) ) - && PQexecNR( QStringLiteral( "RELEASE SAVEPOINT transaction_savepoint" ) ); + QString query = QStringLiteral( "ROLLBACK TO SAVEPOINT transaction_savepoint" ); + QgsDatabaseQueryLogEntry entry( query ); + QgsSetQueryLogClass( entry, "QgsPostgresConn" ); + QgsDatabaseQueryLog::log( entry ); + bool res = false; + if ( PQexecNR( query ) ) + { + query = QStringLiteral( "RELEASE SAVEPOINT transaction_savepoint" ); + QgsDatabaseQueryLogEntry entry( query ); + QgsSetQueryLogClass( entry, "QgsPostgresConn" ); + QgsDatabaseQueryLog::log( entry ); + if ( PQexecNR( query ) ) + res = true; + } + return res; } else { - return PQexecNR( QStringLiteral( "ROLLBACK" ) ); + QString query = QStringLiteral( "ROLLBACK" ); + QgsDatabaseQueryLogEntry entry( query ); + QgsSetQueryLogClass( entry, "QgsPostgresConn" ); + QgsDatabaseQueryLog::log( entry ); + return PQexecNR( query ); } } @@ -1898,7 +1969,10 @@ void QgsPostgresConn::deduceEndian() QgsDebugMsgLevel( QStringLiteral( "Creating binary cursor" ), 2 ); // get the same value using a binary cursor - openCursor( QStringLiteral( "oidcursor" ), QStringLiteral( "select regclass('pg_class')::oid" ) ); + QString query = QStringLiteral( "select regclass('pg_class')::oid" ); + QgsDatabaseQueryLogEntry entry( query ); + QgsSetQueryLogClass( entry, "QgsPostgresConn" ); + openCursor( QStringLiteral( "oidcursor" ), query, entry ); QgsDebugMsgLevel( QStringLiteral( "Fetching a record and attempting to get check endian-ness" ), 2 ); diff --git a/src/providers/postgres/qgspostgresconn.h b/src/providers/postgres/qgspostgresconn.h index 50b25511d84..0eca63ce538 100644 --- a/src/providers/postgres/qgspostgresconn.h +++ b/src/providers/postgres/qgspostgresconn.h @@ -36,6 +36,7 @@ extern "C" } class QgsField; +class QgsDatabaseQueryLogEntry; //! Spatial column types enum QgsPostgresGeometryColumnType @@ -250,7 +251,7 @@ class QgsPostgresConn : public QObject bool PQexecNR( const QString &query ); //! cursor handling - bool openCursor( const QString &cursorName, const QString &declare ); + bool openCursor( const QString &cursorName, const QString &declare, const QgsDatabaseQueryLogEntry &logEntry ); bool closeCursor( const QString &cursorName ); QString uniqueCursorName(); diff --git a/src/providers/postgres/qgspostgresdataitems.cpp b/src/providers/postgres/qgspostgresdataitems.cpp index 66384a75f34..dfaa00e90f7 100644 --- a/src/providers/postgres/qgspostgresdataitems.cpp +++ b/src/providers/postgres/qgspostgresdataitems.cpp @@ -29,6 +29,7 @@ #include "qgsvectorlayerexporter.h" #include "qgsprojectitem.h" #include "qgsfieldsitem.h" +#include "qgsdbquerylog.h" #include #include @@ -58,11 +59,17 @@ bool QgsPostgresUtils::deleteLayer( const QString &uri, QString &errCause ) // handle deletion of views QString sqlViewCheck = QStringLiteral( "SELECT relkind FROM pg_class WHERE oid=regclass(%1)::oid" ) .arg( QgsPostgresConn::quotedValue( schemaTableName ) ); + QgsDatabaseQueryLogEntry entry( sqlViewCheck ); + QgsSetQueryLogClass( entry, "QgsPostgresUtils" ); + QgsDatabaseQueryLog::log( entry ); QgsPostgresResult resViewCheck( conn->PQexec( sqlViewCheck ) ); QString type = resViewCheck.PQgetvalue( 0, 0 ); if ( type == QLatin1String( "v" ) || type == QLatin1String( "m" ) ) { QString sql = QStringLiteral( "DROP %1VIEW %2" ).arg( type == QLatin1String( "m" ) ? QStringLiteral( "MATERIALIZED " ) : QString(), schemaTableName ); + QgsDatabaseQueryLogEntry entry( sql ); + QgsSetQueryLogClass( entry, "QgsPostgresUtils" ); + QgsDatabaseQueryLog::log( entry ); QgsPostgresResult result( conn->PQexec( sql ) ); if ( result.PQresultStatus() != PGRES_COMMAND_OK ) { @@ -85,6 +92,9 @@ bool QgsPostgresUtils::deleteLayer( const QString &uri, QString &errCause ) "AND f_table_schema=%1 AND f_table_name=%2" ) .arg( QgsPostgresConn::quotedValue( schemaName ), QgsPostgresConn::quotedValue( tableName ) ); + entry = QgsDatabaseQueryLogEntry( sql ); + QgsSetQueryLogClass( entry, "QgsPostgresUtils" ); + QgsDatabaseQueryLog::log( entry ); QgsPostgresResult result( conn->PQexec( sql ) ); if ( result.PQresultStatus() != PGRES_TUPLES_OK ) { @@ -113,6 +123,9 @@ bool QgsPostgresUtils::deleteLayer( const QString &uri, QString &errCause ) QgsPostgresConn::quotedValue( tableName ) ); } + entry = QgsDatabaseQueryLogEntry( sql ); + QgsSetQueryLogClass( entry, "QgsPostgresUtils" ); + QgsDatabaseQueryLog::log( entry ); result = conn->PQexec( sql ); if ( result.PQresultStatus() != PGRES_TUPLES_OK ) { @@ -147,6 +160,9 @@ bool QgsPostgresUtils::deleteSchema( const QString &schema, const QgsDataSourceU QString sql = QStringLiteral( "DROP SCHEMA %1 %2" ) .arg( schemaName, cascade ? QStringLiteral( "CASCADE" ) : QString() ); + QgsDatabaseQueryLogEntry entry( sql ); + QgsSetQueryLogClass( entry, "QgsPostgresUtils" ); + QgsDatabaseQueryLog::log( entry ); QgsPostgresResult result( conn->PQexec( sql ) ); if ( result.PQresultStatus() != PGRES_COMMAND_OK ) { @@ -262,7 +278,7 @@ bool QgsPGConnectionItem::handleDrop( const QMimeData *data, const QString &toSc if ( srcLayer->isValid() ) { uri.setDataSource( QString(), u.name, srcLayer->geometryType() != QgsWkbTypes::NullGeometry ? QStringLiteral( "geom" ) : QString() ); - QgsDebugMsgLevel( "URI " + uri.uri( false ), 2 ); + QgsDebugMsgLevel( "URI " + uri.uri( false ), 3 ); if ( !toSchema.isNull() ) { @@ -365,7 +381,7 @@ QString QgsPGLayerItem::createUri() if ( uri.wkbType() != QgsWkbTypes::NoGeometry && mLayerProperty.srids.at( 0 ) != std::numeric_limits::min() ) uri.setSrid( QString::number( mLayerProperty.srids.at( 0 ) ) ); - QgsDebugMsgLevel( QStringLiteral( "layer uri: %1" ).arg( uri.uri( false ) ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "layer uri: %1" ).arg( uri.uri( false ) ), 3 ); return uri.uri( false ); } diff --git a/src/providers/postgres/qgspostgresfeatureiterator.cpp b/src/providers/postgres/qgspostgresfeatureiterator.cpp index ee00f330c0e..8247d66c8bc 100644 --- a/src/providers/postgres/qgspostgresfeatureiterator.cpp +++ b/src/providers/postgres/qgspostgresfeatureiterator.cpp @@ -23,7 +23,7 @@ #include "qgssettings.h" #include "qgsexception.h" #include "qgsgeometryengine.h" - +#include "qgsdbquerylog.h" #include #include @@ -422,8 +422,13 @@ bool QgsPostgresFeatureIterator::rewind() return false; // move cursor to first record + const QString query = QStringLiteral( "move absolute 0 in %1" ).arg( mCursorName ); + + QgsDatabaseQueryLogEntry entry( query ); + QgsSetQueryLogClass( entry, "QgsPostgresFeatureIterator" ); + QgsDatabaseQueryLog::log( entry ); + mConn->PQexecNR( query ); - mConn->PQexecNR( QStringLiteral( "move absolute 0 in %1" ).arg( mCursorName ) ); mFeatureQueue.clear(); mFetched = 0; mLastFetch = false; @@ -763,7 +768,9 @@ bool QgsPostgresFeatureIterator::declareCursor( const QString &whereClause, long if ( !orderBy.isEmpty() ) query += QStringLiteral( " ORDER BY %1 " ).arg( orderBy ); - if ( !mConn->openCursor( mCursorName, query ) ) + QgsDatabaseQueryLogEntry entry( query ); + QgsSetQueryLogClass( entry, "QgsPostgresFeatureIterator" ); + if ( !mConn->openCursor( mCursorName, query, entry ) ) { // reloading the fields might help next time around // TODO how to cleanly force reload of fields? P->loadFields(); diff --git a/src/ui/qgsqueryloggerpanelbase.ui b/src/ui/qgsqueryloggerpanelbase.ui new file mode 100644 index 00000000000..7fff61bf082 --- /dev/null +++ b/src/ui/qgsqueryloggerpanelbase.ui @@ -0,0 +1,117 @@ + + + QgsDatabaseQueryLoggerPanelBase + + + + 0 + 0 + 700 + 629 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 24 + 24 + + + + false + + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 6 + + + + + + + + + :/images/themes/default/mActionDeleteSelected.svg:/images/themes/default/mActionDeleteSelected.svg + + + Clear + + + Clear Log + + + + + true + + + + :/images/themes/default/mActionRecord.svg:/images/themes/default/mActionRecord.svg + + + Record Log + + + + + + :/images/themes/default/mActionFileSave.svg:/images/themes/default/mActionFileSave.svg + + + Save Log… + + + + + + QgsPanelWidget + QWidget +
qgspanelwidget.h
+ 1 +
+ + QgsFilterLineEdit + QLineEdit +
qgsfilterlineedit.h
+
+
+ + + + +