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}