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}