001/* 002 * Copyright 2015-2022 Transmogrify LLC, 2022-2026 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 org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.NotThreadSafe; 023import javax.annotation.concurrent.ThreadSafe; 024import java.time.Duration; 025import java.util.ArrayList; 026import java.util.List; 027import java.util.Objects; 028import java.util.Optional; 029import java.util.stream.Collectors; 030 031import static java.lang.String.format; 032import static java.util.Objects.requireNonNull; 033 034/** 035 * A collection of SQL statement execution diagnostics. 036 * 037 * @author <a href="https://www.revetkn.com">Mark Allen</a> 038 * @since 1.0.0 039 */ 040@ThreadSafe 041public final class StatementLog<T> { 042 @NonNull 043 private final StatementContext<T> statementContext; 044 @NonNull 045 private final Duration totalDuration; 046 @Nullable 047 private final Duration connectionAcquisitionDuration; 048 @Nullable 049 private final Duration preparationDuration; 050 @Nullable 051 private final Duration executionDuration; 052 @Nullable 053 private final Duration resultSetMappingDuration; 054 @Nullable 055 private final Integer batchSize; 056 @Nullable 057 private final Exception exception; 058 059 /** 060 * Creates a {@code StatementLog} for the given {@code builder}. 061 * 062 * @param builder the builder used to construct this {@code StatementLog} 063 */ 064 private StatementLog(@NonNull Builder builder) { 065 requireNonNull(builder); 066 067 this.statementContext = requireNonNull(builder.statementContext); 068 this.connectionAcquisitionDuration = builder.connectionAcquisitionDuration; 069 this.preparationDuration = builder.preparationDuration; 070 this.executionDuration = builder.executionDuration; 071 this.resultSetMappingDuration = builder.resultSetMappingDuration; 072 this.batchSize = builder.batchSize; 073 this.exception = builder.exception; 074 075 Duration totalDuration = Duration.ZERO; 076 077 if (this.connectionAcquisitionDuration != null) 078 totalDuration = totalDuration.plus(this.connectionAcquisitionDuration); 079 080 if (this.preparationDuration != null) 081 totalDuration = totalDuration.plus(this.preparationDuration); 082 083 if (this.executionDuration != null) 084 totalDuration = totalDuration.plus(this.executionDuration); 085 086 if (this.resultSetMappingDuration != null) 087 totalDuration = totalDuration.plus(this.resultSetMappingDuration); 088 089 this.totalDuration = totalDuration; 090 } 091 092 /** 093 * Creates a {@link StatementLog} builder for the given {@code statementContext}. 094 * 095 * @param statementContext current SQL context 096 * @return a {@link StatementLog} builder 097 */ 098 @NonNull 099 public static <T> Builder withStatementContext(@NonNull StatementContext<T> statementContext) { 100 requireNonNull(statementContext); 101 return new Builder(statementContext); 102 } 103 104 @Override 105 public String toString() { 106 List<String> components = new ArrayList<>(8); 107 108 components.add(format("statementContext=%s", getStatementContext())); 109 components.add(format("totalDuration=%s", getTotalDuration())); 110 111 Duration connectionAcquisitionDuration = getConnectionAcquisitionDuration().orElse(null); 112 113 if (connectionAcquisitionDuration != null) 114 components.add(format("connectionAcquisitionDuration=%s", connectionAcquisitionDuration)); 115 116 Duration preparationDuration = getPreparationDuration().orElse(null); 117 118 if (preparationDuration != null) 119 components.add(format("preparationDuration=%s", preparationDuration)); 120 121 Duration executionDuration = getExecutionDuration().orElse(null); 122 123 if (executionDuration != null) 124 components.add(format("executionDuration=%s", executionDuration)); 125 126 Duration resultSetMappingDuration = getResultSetMappingDuration().orElse(null); 127 128 if (resultSetMappingDuration != null) 129 components.add(format("resultSetMappingDuration=%s", resultSetMappingDuration)); 130 131 Integer batchSize = getBatchSize().orElse(null); 132 133 if (batchSize != null) 134 components.add(format("batchSize=%s", batchSize)); 135 136 Exception exception = getException().orElse(null); 137 138 if (exception != null) 139 components.add(format("exception=%s", exception)); 140 141 return format("%s{%s}", getClass().getSimpleName(), components.stream().collect(Collectors.joining(", "))); 142 } 143 144 @Override 145 public boolean equals(Object object) { 146 if (this == object) 147 return true; 148 149 if (!(object instanceof StatementLog)) 150 return false; 151 152 StatementLog statementLog = (StatementLog) object; 153 154 return Objects.equals(getStatementContext(), statementLog.getStatementContext()) 155 && Objects.equals(getConnectionAcquisitionDuration(), statementLog.getConnectionAcquisitionDuration()) 156 && Objects.equals(getPreparationDuration(), statementLog.getPreparationDuration()) 157 && Objects.equals(getExecutionDuration(), statementLog.getExecutionDuration()) 158 && Objects.equals(getResultSetMappingDuration(), statementLog.getResultSetMappingDuration()) 159 && Objects.equals(getBatchSize(), statementLog.getBatchSize()) 160 && Objects.equals(getException(), statementLog.getException()); 161 } 162 163 @Override 164 public int hashCode() { 165 return Objects.hash(getStatementContext(), getConnectionAcquisitionDuration(), getPreparationDuration(), 166 getExecutionDuration(), getResultSetMappingDuration(), getBatchSize(), getException()); 167 } 168 169 /** 170 * How long did it take to acquire a {@link java.sql.Connection} from the {@link javax.sql.DataSource}? 171 * 172 * @return how long it took to acquire a {@link java.sql.Connection}, if available 173 */ 174 @NonNull 175 public Optional<Duration> getConnectionAcquisitionDuration() { 176 return Optional.ofNullable(this.connectionAcquisitionDuration); 177 } 178 179 /** 180 * How long did it take to bind data to the {@link java.sql.PreparedStatement}? 181 * 182 * @return how long it took to bind data to the {@link java.sql.PreparedStatement}, if available 183 */ 184 @NonNull 185 public Optional<Duration> getPreparationDuration() { 186 return Optional.ofNullable(this.preparationDuration); 187 } 188 189 /** 190 * How long did it take to execute the SQL statement? 191 * 192 * @return how long it took to execute the SQL statement, if available 193 */ 194 public Optional<Duration> getExecutionDuration() { 195 return Optional.ofNullable(this.executionDuration); 196 } 197 198 /** 199 * How long did it take to extract data from the {@link java.sql.ResultSet}? 200 * 201 * @return how long it took to extract data from the {@link java.sql.ResultSet}, if available 202 */ 203 public Optional<Duration> getResultSetMappingDuration() { 204 return Optional.ofNullable(this.resultSetMappingDuration); 205 } 206 207 /** 208 * How long did it take to perform the database operation in total? 209 * <p> 210 * This is the sum of {@link #getConnectionAcquisitionDuration()} + {@link #getPreparationDuration()} + 211 * {@link #getExecutionDuration()} + {@link #getResultSetMappingDuration()}. 212 * 213 * @return how long the database operation took in total 214 */ 215 @NonNull 216 public Duration getTotalDuration() { 217 return this.totalDuration; 218 } 219 220 /** 221 * The SQL statement that was executed. 222 * 223 * @return the SQL statement that was executed. 224 */ 225 @NonNull 226 public StatementContext<T> getStatementContext() { 227 return this.statementContext; 228 } 229 230 /** 231 * The size of the batch operation. 232 * 233 * @return how many records were processed as part of the batch operation, if available 234 */ 235 @NonNull 236 public Optional<Integer> getBatchSize() { 237 return Optional.ofNullable(this.batchSize); 238 } 239 240 /** 241 * The exception that occurred during SQL statement execution. 242 * 243 * @return the exception that occurred during SQL statement execution, if available 244 */ 245 @NonNull 246 public Optional<Exception> getException() { 247 return Optional.ofNullable(this.exception); 248 } 249 250 /** 251 * Builder used to construct instances of {@link StatementLog}. 252 * <p> 253 * This class is intended for use by a single thread. 254 * 255 * @author <a href="https://www.revetkn.com">Mark Allen</a> 256 * @since 1.0.0 257 */ 258 @NotThreadSafe 259 public static class Builder<T> { 260 @NonNull 261 private final StatementContext<T> statementContext; 262 @Nullable 263 private Duration connectionAcquisitionDuration; 264 @Nullable 265 private Duration preparationDuration; 266 @Nullable 267 private Duration executionDuration; 268 @Nullable 269 private Duration resultSetMappingDuration; 270 @Nullable 271 private Integer batchSize; 272 @Nullable 273 private Exception exception; 274 275 /** 276 * Creates a {@code Builder} for the given {@code statementContext}. 277 * 278 * @param statementContext current SQL context 279 */ 280 private Builder(@NonNull StatementContext<T> statementContext) { 281 requireNonNull(statementContext); 282 this.statementContext = statementContext; 283 } 284 285 /** 286 * Specifies how long it took to acquire a {@link java.sql.Connection} from the {@link javax.sql.DataSource}. 287 * 288 * @param connectionAcquisitionDuration how long it took to acquire a {@link java.sql.Connection}, if available 289 * @return this {@code Builder}, for chaining 290 */ 291 @NonNull 292 public Builder connectionAcquisitionDuration(@Nullable Duration connectionAcquisitionDuration) { 293 this.connectionAcquisitionDuration = connectionAcquisitionDuration; 294 return this; 295 } 296 297 /** 298 * Specifies how long it took to bind data to a {@link java.sql.PreparedStatement}. 299 * 300 * @param preparationDuration how long it took to bind data to a {@link java.sql.PreparedStatement}, if available 301 * @return this {@code Builder}, for chaining 302 */ 303 public Builder preparationDuration(@Nullable Duration preparationDuration) { 304 this.preparationDuration = preparationDuration; 305 return this; 306 } 307 308 /** 309 * Specifies how long it took to execute a SQL statement. 310 * 311 * @param executionDuration how long it took to execute a SQL statement, if available 312 * @return this {@code Builder}, for chaining 313 */ 314 public Builder executionDuration(@Nullable Duration executionDuration) { 315 this.executionDuration = executionDuration; 316 return this; 317 } 318 319 /** 320 * Specifies how long it took to extract data from a {@link java.sql.ResultSet}. 321 * 322 * @param resultSetMappingDuration how long it took to extract data from a {@link java.sql.ResultSet}, if available 323 * @return this {@code Builder}, for chaining 324 */ 325 public Builder resultSetMappingDuration(@Nullable Duration resultSetMappingDuration) { 326 this.resultSetMappingDuration = resultSetMappingDuration; 327 return this; 328 } 329 330 /** 331 * Specifies the size of the batch operation. 332 * 333 * @param batchSize how many records were processed as part of the batch operation, if available 334 * @return this {@code Builder}, for chaining 335 */ 336 public Builder batchSize(@Nullable Integer batchSize) { 337 this.batchSize = batchSize; 338 return this; 339 } 340 341 /** 342 * Specifies the exception that occurred during SQL statement execution. 343 * 344 * @param exception the exception that occurred during SQL statement execution, if available 345 * @return this {@code Builder}, for chaining 346 */ 347 public Builder exception(@Nullable Exception exception) { 348 this.exception = exception; 349 return this; 350 } 351 352 /** 353 * Constructs a {@code StatementLog} instance. 354 * 355 * @return a {@code StatementLog} instance 356 */ 357 @NonNull 358 public StatementLog build() { 359 return new StatementLog(this); 360 } 361 } 362}