001/*
002 * Copyright 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.soklet;
018
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.NotThreadSafe;
023import javax.annotation.concurrent.ThreadSafe;
024import java.util.Objects;
025import java.util.Optional;
026
027import static java.lang.String.format;
028import static java.util.Objects.requireNonNull;
029
030/**
031 * An informational "loggable" event that occurs during Soklet's internal processing - for example, if an error occurs while handling a request.
032 * <p>
033 * These events are exposed via {@link LifecycleObserver#didReceiveLogEvent(LogEvent)}.
034 * <p>
035 * Instances can be acquired via the {@link #with(LogEventType, String)} builder factory method.
036 * <p>
037 * Documentation is available at <a href="https://www.soklet.com/docs/request-lifecycle#event-logging">https://www.soklet.com/docs/request-lifecycle#event-logging</a>.
038 *
039 * @author <a href="https://www.revetkn.com">Mark Allen</a>
040 */
041@ThreadSafe
042public final class LogEvent {
043        @NonNull
044        private final LogEventType logEventType;
045        @NonNull
046        private final String message;
047        @Nullable
048        private final Throwable throwable;
049        @Nullable
050        private final Request request;
051        @Nullable
052        private final ResourceMethod resourceMethod;
053        @Nullable
054        private final MarshaledResponse marshaledResponse;
055
056        /**
057         * Acquires a builder for {@link LogEvent} instances.
058         *
059         * @param logEventType what kind of log event this is
060         * @param message      the message for this log event
061         * @return the builder
062         */
063        @NonNull
064        public static Builder with(@NonNull LogEventType logEventType,
065                                                                                                                 @NonNull String message) {
066                requireNonNull(logEventType);
067                requireNonNull(message);
068
069                return new Builder(logEventType, message);
070        }
071
072        /**
073         * Vends a mutable copier seeded with this instance's data, suitable for building new instances.
074         *
075         * @return a copier for this instance
076         */
077        @NonNull
078        public Copier copy() {
079                return new Copier(this);
080        }
081
082        protected LogEvent(@NonNull Builder builder) {
083                requireNonNull(builder);
084
085                this.logEventType = builder.logEventType;
086                this.message = builder.message;
087                this.throwable = builder.throwable;
088                this.request = builder.request;
089                this.resourceMethod = builder.resourceMethod;
090                this.marshaledResponse = builder.marshaledResponse;
091        }
092
093        @Override
094        @NonNull
095        public String toString() {
096                return format("%s{logEventType=%s, message=%s, throwable=%s}", getClass().getSimpleName(),
097                                getLogEventType(), getMessage(), getThrowable().orElse(null));
098        }
099
100        @Override
101        public boolean equals(@Nullable Object object) {
102                if (this == object)
103                        return true;
104
105                if (!(object instanceof LogEvent logEvent))
106                        return false;
107
108                return Objects.equals(getLogEventType(), logEvent.getLogEventType())
109                                && Objects.equals(getMessage(), logEvent.getMessage())
110                                && Objects.equals(getThrowable(), logEvent.getThrowable())
111                                && Objects.equals(getRequest(), logEvent.getRequest())
112                                && Objects.equals(getResourceMethod(), logEvent.getResourceMethod())
113                                && Objects.equals(getMarshaledResponse(), logEvent.getMarshaledResponse());
114        }
115
116        @Override
117        public int hashCode() {
118                return Objects.hash(getLogEventType(), getMessage(), getThrowable(), getRequest(), getResourceMethod(), getMarshaledResponse());
119        }
120
121        /**
122         * The type of log event this is.
123         *
124         * @return the log event type
125         */
126        @NonNull
127        public LogEventType getLogEventType() {
128                return this.logEventType;
129        }
130
131        /**
132         * The message for this log event.
133         *
134         * @return the message
135         */
136        @NonNull
137        public String getMessage() {
138                return this.message;
139        }
140
141        /**
142         * The throwable for this log event, if available.
143         *
144         * @return the throwable, or {@link Optional#empty()} if not available
145         */
146        @NonNull
147        public Optional<Throwable> getThrowable() {
148                return Optional.ofNullable(this.throwable);
149        }
150
151        /**
152         * The request associated with this log event, if available.
153         *
154         * @return the request, or {@link Optional#empty()} if not available
155         */
156        @NonNull
157        public Optional<Request> getRequest() {
158                return Optional.ofNullable(this.request);
159        }
160
161        /**
162         * The <em>Resource Method</em> associated with this log event, if available.
163         *
164         * @return the <em>Resource Method</em>, or {@link Optional#empty()} if not available
165         */
166        @NonNull
167        public Optional<ResourceMethod> getResourceMethod() {
168                return Optional.ofNullable(this.resourceMethod);
169        }
170
171        /**
172         * The response associated with this log event, if available.
173         *
174         * @return the response, or {@link Optional#empty()} if not available
175         */
176        @NonNull
177        public Optional<MarshaledResponse> getMarshaledResponse() {
178                return Optional.ofNullable(this.marshaledResponse);
179        }
180
181        /**
182         * Builder used to construct instances of {@link LogEvent} via {@link LogEvent#with(LogEventType, String)}.
183         * <p>
184         * This class is intended for use by a single thread.
185         *
186         * @author <a href="https://www.revetkn.com">Mark Allen</a>
187         */
188        @NotThreadSafe
189        public static final class Builder {
190                @NonNull
191                private LogEventType logEventType;
192                @NonNull
193                private String message;
194                @Nullable
195                private Throwable throwable;
196                @Nullable
197                private Request request;
198                @Nullable
199                private ResourceMethod resourceMethod;
200                @Nullable
201                private MarshaledResponse marshaledResponse;
202
203                protected Builder(@NonNull LogEventType logEventType,
204                                                                                        @NonNull String message) {
205                        requireNonNull(logEventType);
206                        requireNonNull(message);
207
208                        this.logEventType = logEventType;
209                        this.message = message;
210                }
211
212                @NonNull
213                public Builder logEventType(@NonNull LogEventType logEventType) {
214                        requireNonNull(logEventType);
215                        this.logEventType = logEventType;
216                        return this;
217                }
218
219                @NonNull
220                public Builder message(@NonNull String message) {
221                        requireNonNull(message);
222                        this.message = message;
223                        return this;
224                }
225
226                @NonNull
227                public Builder throwable(@Nullable Throwable throwable) {
228                        this.throwable = throwable;
229                        return this;
230                }
231
232                @NonNull
233                public Builder request(@Nullable Request request) {
234                        this.request = request;
235                        return this;
236                }
237
238                @NonNull
239                public Builder resourceMethod(@Nullable ResourceMethod resourceMethod) {
240                        this.resourceMethod = resourceMethod;
241                        return this;
242                }
243
244                @NonNull
245                public Builder marshaledResponse(@Nullable MarshaledResponse marshaledResponse) {
246                        this.marshaledResponse = marshaledResponse;
247                        return this;
248                }
249
250                @NonNull
251                public LogEvent build() {
252                        return new LogEvent(this);
253                }
254        }
255
256        /**
257         * Builder used to copy instances of {@link LogEvent} via {@link LogEvent#copy()}.
258         * <p>
259         * This class is intended for use by a single thread.
260         *
261         * @author <a href="https://www.revetkn.com">Mark Allen</a>
262         */
263        @NotThreadSafe
264        public static final class Copier {
265                @NonNull
266                private final Builder builder;
267
268                Copier(@NonNull LogEvent logEvent) {
269                        requireNonNull(logEvent);
270
271                        this.builder = new Builder(logEvent.getLogEventType(), logEvent.getMessage())
272                                        .throwable(logEvent.getThrowable().orElse(null))
273                                        .request(logEvent.getRequest().orElse(null))
274                                        .resourceMethod(logEvent.getResourceMethod().orElse(null))
275                                        .marshaledResponse(logEvent.getMarshaledResponse().orElse(null));
276                }
277
278                @NonNull
279                public Copier logEventType(@NonNull LogEventType logEventType) {
280                        requireNonNull(logEventType);
281                        this.builder.logEventType(logEventType);
282                        return this;
283                }
284
285                @NonNull
286                public Copier message(@NonNull String message) {
287                        requireNonNull(message);
288                        this.builder.message(message);
289                        return this;
290                }
291
292                @NonNull
293                public Copier throwable(@Nullable Throwable throwable) {
294                        this.builder.throwable(throwable);
295                        return this;
296                }
297
298                @NonNull
299                public Copier request(@Nullable Request request) {
300                        this.builder.request(request);
301                        return this;
302                }
303
304                @NonNull
305                public Copier resourceMethod(@Nullable ResourceMethod resourceMethod) {
306                        this.builder.resourceMethod(resourceMethod);
307                        return this;
308                }
309
310                @NonNull
311                public Copier marshaledResponse(@Nullable MarshaledResponse marshaledResponse) {
312                        this.builder.marshaledResponse(marshaledResponse);
313                        return this;
314                }
315
316                @NonNull
317                public LogEvent finish() {
318                        return this.builder.build();
319                }
320        }
321}