001/* 002 * Copyright 2015-2022 Transmogrify LLC, 2022-2024 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.pyranid; 018 019import javax.annotation.Nonnull; 020import javax.annotation.concurrent.ThreadSafe; 021import java.util.ArrayList; 022import java.util.List; 023import java.util.logging.Level; 024import java.util.logging.Logger; 025 026import static java.lang.String.format; 027import static java.util.Objects.requireNonNull; 028import static java.util.stream.Collectors.joining; 029 030/** 031 * Basic implementation of {@link StatementLogger} which logs via <a href="https://docs.oracle.com/en/java/javase/20/docs/api/java.logging/java/util/logging/package-summary.html">java.util.logging</a>. 032 * 033 * @author <a href="https://www.revetkn.com">Mark Allen</a> 034 * @since 1.0.0 035 */ 036@ThreadSafe 037public class DefaultStatementLogger implements StatementLogger { 038 @Nonnull 039 public static final String DEFAULT_LOGGER_NAME = "com.pyranid.SQL"; 040 @Nonnull 041 public static final Level DEFAULT_LOGGER_LEVEL = Level.FINE; 042 043 /** 044 * The point at which we ellipsize output for parameters. 045 */ 046 @Nonnull 047 private static final int MAXIMUM_PARAMETER_LOGGING_LENGTH = 100; 048 049 @Nonnull 050 private final Logger logger; 051 @Nonnull 052 private final Level loggerLevel; 053 054 /** 055 * Creates a new statement logger with the default logger name <code>{@value #DEFAULT_LOGGER_NAME}</code> and level. 056 */ 057 public DefaultStatementLogger() { 058 this(DEFAULT_LOGGER_NAME, DEFAULT_LOGGER_LEVEL); 059 } 060 061 /** 062 * Creates a new statement logger with the given logger name and level. 063 * 064 * @param loggerName the logger name to use 065 * @param loggerLevel the logger level to use 066 */ 067 public DefaultStatementLogger(@Nonnull String loggerName, 068 @Nonnull Level loggerLevel) { 069 requireNonNull(loggerName); 070 requireNonNull(loggerLevel); 071 072 this.logger = Logger.getLogger(loggerName); 073 this.loggerLevel = loggerLevel; 074 } 075 076 @Override 077 public void log(@Nonnull StatementLog statementLog) { 078 requireNonNull(statementLog); 079 080 if (getLogger().isLoggable(getLoggerLevel())) 081 getLogger().log(getLoggerLevel(), formatStatementLog(statementLog)); 082 } 083 084 @Nonnull 085 protected String formatStatementLog(@Nonnull StatementLog statementLog) { 086 requireNonNull(statementLog); 087 088 List<String> timingEntries = new ArrayList<>(4); 089 090 if (statementLog.getConnectionAcquisitionDuration().isPresent()) 091 timingEntries.add(format("%s acquiring connection", statementLog.getConnectionAcquisitionDuration().get())); 092 093 if (statementLog.getPreparationDuration().isPresent()) 094 timingEntries.add(format("%s preparing statement", statementLog.getPreparationDuration().get())); 095 096 if (statementLog.getExecutionDuration().isPresent()) 097 timingEntries.add(format("%s executing statement", statementLog.getExecutionDuration().get())); 098 099 if (statementLog.getResultSetMappingDuration().isPresent()) 100 timingEntries.add(format("%s processing resultset", statementLog.getResultSetMappingDuration().get())); 101 102 String parameterLine = null; 103 104 if (statementLog.getStatementContext().getParameters().size() > 0) { 105 StringBuilder parameterLineBuilder = new StringBuilder(); 106 parameterLineBuilder.append("Parameters: "); 107 parameterLineBuilder.append(statementLog.getStatementContext().getParameters().stream().map(parameter -> { 108 if (parameter == null) 109 return "null"; 110 111 if (parameter instanceof Number) 112 return format("%s", parameter); 113 114 if (parameter.getClass().isArray()) { 115 // TODO: cap size of arrays 116 117 if (parameter instanceof byte[]) 118 return format("[byte array of length %d]", ((byte[]) parameter).length); 119 } 120 121 return format("'%s'", ellipsize(parameter.toString(), MAXIMUM_PARAMETER_LOGGING_LENGTH)); 122 }).collect(joining(", "))); 123 124 parameterLine = parameterLineBuilder.toString(); 125 } 126 127 List<String> lines = new ArrayList<>(4); 128 129 lines.add(statementLog.getStatementContext().getStatement().getSql()); 130 131 if (parameterLine != null) 132 lines.add(parameterLine); 133 134 if (timingEntries.size() > 0) 135 lines.add(timingEntries.stream().collect(joining(", "))); 136 137 Throwable exception = (Exception) statementLog.getException().orElse(null); 138 139 if (exception != null) { 140 if (exception instanceof DatabaseException && exception.getCause() != null) 141 exception = exception.getCause(); 142 143 lines.add(format("Failed due to %s", exception.toString())); 144 } 145 146 return lines.stream().collect(joining("\n")); 147 } 148 149 /** 150 * Ellipsizes the given {@code string}, capping at {@code maximumLength}. 151 * 152 * @param string the string to ellipsize 153 * @param maximumLength the maximum length of the ellipsized string, not including ellipsis 154 * @return an ellipsized version of {@code string} 155 */ 156 @Nonnull 157 protected String ellipsize(@Nonnull String string, 158 int maximumLength) { 159 requireNonNull(string); 160 161 string = string.trim(); 162 163 if (string.length() <= maximumLength) 164 return string; 165 166 return format("%s...", string.substring(0, maximumLength)); 167 } 168 169 @Nonnull 170 protected Logger getLogger() { 171 return this.logger; 172 } 173 174 @Nonnull 175 protected Level getLoggerLevel() { 176 return this.loggerLevel; 177 } 178}