Core Concepts
Logging and Diagnostics
Pyranid provides tooling for runtime insight into SQL execution.
For many applications, it's important to understand exactly what queries are being run to keep on top of potential performance problems.
Further, it's often useful to mark specific queries for special processing - for example, you might want to log particularly slow or "special" SQL, or you might want to flag a query as needing Custom ResultSet Mapping.
Statement Logging
You may customize your Database
with a StatementLogger
.
Reasons you might do this include:
- Writing queries and timing information to your logging system
- Picking out slow queries for special logging/reporting
- Collecting a set of queries executed across a unit of work for bulk analysis (e.g. a
ThreadLocal<T>
orScopedValue<T>
scoped to a single web request)
Database database = Database.forDataSource(dataSource)
.statementLogger(new StatementLogger() {
Duration SLOW_QUERY_THRESHOLD = Duration.ofMillis(500);
@Override
public void log(@Nonnull StatementLog statementLog) {
if(statementLog.getTotalDuration().compareTo(SLOW_QUERY_THRESHOLD) > 0)
out.printf("Slow query: %s\n", statementLog);
}
}).build();
Database database = Database.forDataSource(dataSource)
.statementLogger(new StatementLogger() {
Duration SLOW_QUERY_THRESHOLD = Duration.ofMillis(500);
@Override
public void log(@Nonnull StatementLog statementLog) {
if(statementLog.getTotalDuration().compareTo(SLOW_QUERY_THRESHOLD) > 0)
out.printf("Slow query: %s\n", statementLog);
}
}).build();
StatementLog
instances give you access to the following for each SQL statement executed:
statementContext
connectionAcquisitionDuration
(optional)preparationDuration
(optional)executionDuration
(optional)resultSetMappingDuration
(optional)batchSize
(optional)exception
(optional)
References:
Statement Identifiers
For any data access method that accepts a sql
parameter, you may alternatively provide a Statement
, which permits you to specify an arbitrary identifier for the SQL.
If you do not explicitly provide a Statement
, Pyranid will create one for you and generate its own identifier.
// Regular SQL
database.queryForObject("SELECT * FROM car LIMIT 1", Car.class);
// SQL in a Statement so we can give it an identifier
database.queryForObject(Statement.of(
"random-car",
"SELECT * FROM car LIMIT 1"
), Car.class);
// Regular SQL
database.queryForObject("SELECT * FROM car LIMIT 1", Car.class);
// SQL in a Statement so we can give it an identifier
database.queryForObject(Statement.of(
"random-car",
"SELECT * FROM car LIMIT 1"
), Car.class);
This is useful for tagging queries that should be handled specially. Some examples are:
- Marking a query as "hot" so we don't pollute logs with it
- Marking a query as "known to be slow" so we don't flag slow query alerts for it
- Your
InstanceProvider
might provide custom instances based on resultset data
// Custom tagging system
enum QueryTag {
HOT_QUERY,
SLOW_QUERY
};
// This query fires every 3 seconds -
// let's mark it HOT_QUERY so we know not to log it.
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> {
database.transaction(() -> {
List<Message> messages = database.queryForList(
Statement.of(QueryTag.HOT_QUERY, """
SELECT *
FROM message_queue
WHERE message_status_id=?
LIMIT ?
FOR UPDATE
SKIP LOCKED
"""), Message.class, MessageStatusId.UNPROCESSED, BATCH_SIZE);
// Implementation not shown
processMessages(messages);
});
}, 0, 3, TimeUnit.SECONDS);
// Custom tagging system
enum QueryTag {
HOT_QUERY,
SLOW_QUERY
};
// This query fires every 3 seconds -
// let's mark it HOT_QUERY so we know not to log it.
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> {
database.transaction(() -> {
List<Message> messages = database.queryForList(
Statement.of(QueryTag.HOT_QUERY, """
SELECT *
FROM message_queue
WHERE message_status_id=?
LIMIT ?
FOR UPDATE
SKIP LOCKED
"""), Message.class, MessageStatusId.UNPROCESSED, BATCH_SIZE);
// Implementation not shown
processMessages(messages);
});
}, 0, 3, TimeUnit.SECONDS);
A corresponding Database
setup:
// Ensure our StatementLogger implementation takes HOT_QUERY into account
Database database = Database.forDataSource(dataSource)
.statementLogger(new StatementLogger() {
@Override
public void log(@Nonnull StatementLog statementLog) {
// Log everything except HOT_QUERY
Statement statement = statementLog.getStatementContext().getStatement();
if(statement.getId() != HOT_QUERY)
out.println(statementLog);
}
}).build();
// Ensure our StatementLogger implementation takes HOT_QUERY into account
Database database = Database.forDataSource(dataSource)
.statementLogger(new StatementLogger() {
@Override
public void log(@Nonnull StatementLog statementLog) {
// Log everything except HOT_QUERY
Statement statement = statementLog.getStatementContext().getStatement();
if(statement.getId() != HOT_QUERY)
out.println(statementLog);
}
}).build();
References:
Internal Logging
Pyranid uses java.util.Logging
internally for diagnostics. This is not normally useful for Pyranid users, but the usual way to hook into this is with SLF4J, which can funnel all the different logging mechanisms in your app through a single one, normally Logback. Your Maven configuration might look like this:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>2.0.7</version>
</dependency>
You might have code like this which runs at startup:
// Bridge all java.util.logging to SLF4J
java.util.logging.Logger rootLogger =
java.util.logging.LogManager.getLogManager().getLogger("");
for (Handler handler : rootLogger.getHandlers())
rootLogger.removeHandler(handler);
SLF4JBridgeHandler.install();
// Bridge all java.util.logging to SLF4J
java.util.logging.Logger rootLogger =
java.util.logging.LogManager.getLogManager().getLogger("");
for (Handler handler : rootLogger.getHandlers())
rootLogger.removeHandler(handler);
SLF4JBridgeHandler.install();
Don't forget to uninstall the bridge at shutdown time:
// Sometime later
SLF4JBridgeHandler.uninstall();
// Sometime later
SLF4JBridgeHandler.uninstall();
Note: SLF4JBridgeHandler
can impact performance. You can mitigate that with Logback's LevelChangePropagator
configuration option as described here.