Add query log

This commit is contained in:
Nyall Dawson 2021-10-01 09:09:00 +10:00 committed by Alessandro Pasotti
parent 4c88bc54b0
commit 0108041c68
24 changed files with 1766 additions and 23 deletions

View File

@ -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();

View File

@ -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 *
************************************************************************/

View File

@ -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

View File

@ -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

View File

@ -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 <QThread>
#include <QApplication>
#include <QUrlQuery>
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<QgsDatabaseQueryLoggerNode *>( index.internalPointer() );
}
QList<QAction *> 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<int> &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;
}

View File

@ -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 <QAbstractItemModel>
#include <QSortFilterProxyModel>
#include <QElapsedTimer>
#include "qgsdbquerylog.h"
#include <memory>
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

View File

@ -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 <QUrlQuery>
#include <QColor>
#include <QBrush>
#include <QFont>
#include <QAction>
#include <QDesktopServices>
#include <QApplication>
#include <QClipboard>
#include <nlohmann/json.hpp>
//
// QgsDatabaseQueryLoggerNode
//
QgsDatabaseQueryLoggerNode::QgsDatabaseQueryLoggerNode() = default;
QgsDatabaseQueryLoggerNode::~QgsDatabaseQueryLoggerNode() = default;
//
// QgsDatabaseQueryLoggerGroup
//
QgsDatabaseQueryLoggerGroup::QgsDatabaseQueryLoggerGroup( const QString &title )
: mGroupTitle( title )
{
}
void QgsDatabaseQueryLoggerGroup::addChild( std::unique_ptr<QgsDatabaseQueryLoggerNode> 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<QgsDatabaseQueryLoggerNode> &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<QAction *> 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<QAction *> QgsDatabaseQueryLoggerNode::actions( QObject * )
{
return QList< QAction * >();
}
QVariant QgsDatabaseQueryLoggerNode::toVariant() const
{
return QVariant();
}

View File

@ -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 <QElapsedTimer>
#include <QVariant>
#include <QColor>
#include <QUrl>
#include <memory>
#include <deque>
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

View File

@ -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 <QFileDialog>
#include <QFontDatabase>
#include <QMenu>
#include <QMessageBox>
#include <QScrollBar>
#include <QToolButton>
#include <QCheckBox>
#include <QTextStream>
#include <nlohmann/json.hpp>
//
// 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 );
}

View File

@ -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 <QTreeView>
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

View File

@ -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 );
}

View File

@ -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

View File

@ -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();

View File

@ -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;

View File

@ -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

View File

@ -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>( "QgsGeometry::Error" );
qRegisterMetaType<QgsDatabaseQueryLogEntry>( "QgsDatabaseQueryLogEntry" );
qRegisterMetaType<QgsProcessingFeatureSourceDefinition>( "QgsProcessingFeatureSourceDefinition" );
qRegisterMetaType<QgsProcessingOutputLayerDefinition>( "QgsProcessingOutputLayerDefinition" );
qRegisterMetaType<QgsUnitTypes::LayoutUnit>( "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;
}

View File

@ -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;

View File

@ -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 <QDateTime>
//
// 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 );
}

170
src/core/qgsdbquerylog.h Normal file
View File

@ -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 <QString>
#include <QDateTime>
/**
* \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

View File

@ -31,6 +31,7 @@
#include "qgspostgresstringutils.h"
#include "qgspostgresconnpool.h"
#include "qgsvariantutils.h"
#include "qgsdbquerylog.h"
#include <QApplication>
#include <QStringList>
@ -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<QgsPostgresSchemaProperty> &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 );

View File

@ -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();

View File

@ -29,6 +29,7 @@
#include "qgsvectorlayerexporter.h"
#include "qgsprojectitem.h"
#include "qgsfieldsitem.h"
#include "qgsdbquerylog.h"
#include <QMessageBox>
#include <climits>
@ -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<int>::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 );
}

View File

@ -23,7 +23,7 @@
#include "qgssettings.h"
#include "qgsexception.h"
#include "qgsgeometryengine.h"
#include "qgsdbquerylog.h"
#include <QElapsedTimer>
#include <QObject>
@ -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();

View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QgsDatabaseQueryLoggerPanelBase</class>
<widget class="QgsPanelWidget" name="QgsDatabaseQueryLoggerPanelBase">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>700</width>
<height>629</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QToolBar" name="mToolbar">
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="floatable">
<bool>false</bool>
</property>
<addaction name="mActionRecord"/>
<addaction name="mActionClear"/>
<addaction name="mActionSaveLog"/>
<addaction name="separator"/>
</widget>
</item>
<item>
<widget class="QgsFilterLineEdit" name="mFilterLineEdit"/>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>6</height>
</size>
</property>
</spacer>
</item>
</layout>
<action name="mActionClear">
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/mActionDeleteSelected.svg</normaloff>:/images/themes/default/mActionDeleteSelected.svg</iconset>
</property>
<property name="text">
<string>Clear</string>
</property>
<property name="toolTip">
<string>Clear Log</string>
</property>
</action>
<action name="mActionRecord">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/mActionRecord.svg</normaloff>:/images/themes/default/mActionRecord.svg</iconset>
</property>
<property name="text">
<string>Record Log</string>
</property>
</action>
<action name="mActionSaveLog">
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/mActionFileSave.svg</normaloff>:/images/themes/default/mActionFileSave.svg</iconset>
</property>
<property name="text">
<string>Save Log…</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>QgsPanelWidget</class>
<extends>QWidget</extends>
<header>qgspanelwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QgsFilterLineEdit</class>
<extends>QLineEdit</extends>
<header>qgsfilterlineedit.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../../images/images.qrc"/>
</resources>
<connections/>
</ui>