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