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}