diff --git a/CHANGELOG.md b/CHANGELOG.md index d8dd46005..c3df69338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [11.1.0 + +- Adds hikari logs to opentelemetry + ## [11.0.5] - Adds all logs to telemetry which were logged with `io/supertokens/output/Logging.java` diff --git a/build.gradle b/build.gradle index b37778ccd..4502c5993 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ java { } } -version = "11.0.5" +version = "11.1.0" repositories { mavenCentral() diff --git a/pluginInterfaceSupported.json b/pluginInterfaceSupported.json index 06c8cdfaf..144107a2b 100644 --- a/pluginInterfaceSupported.json +++ b/pluginInterfaceSupported.json @@ -1,6 +1,6 @@ { "_comment": "contains a list of plugin interfaces branch names that this core supports", "versions": [ - "8.0" + "8.1" ] } \ No newline at end of file diff --git a/src/main/java/io/supertokens/Main.java b/src/main/java/io/supertokens/Main.java index 307c3d965..f26b918ef 100644 --- a/src/main/java/io/supertokens/Main.java +++ b/src/main/java/io/supertokens/Main.java @@ -44,6 +44,7 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.telemetry.TelemetryProvider; +import io.supertokens.telemetry.WebRequestTelemetryHandler; import io.supertokens.version.Version; import io.supertokens.webserver.Webserver; import org.jetbrains.annotations.TestOnly; @@ -170,6 +171,8 @@ private void init() throws IOException, StorageQueryException { Logging.info(this, TenantIdentifier.BASE_TENANT, "Completed config.yaml loading.", true); TelemetryProvider.initialize(this); + WebRequestTelemetryHandler.INSTANCE.initializeOtelProvider( + TelemetryProvider.getInstance(this)); //life would be much easier with DI.. // loading storage layer try { diff --git a/src/main/java/io/supertokens/dashboard/Dashboard.java b/src/main/java/io/supertokens/dashboard/Dashboard.java index 478de1621..bf29e876e 100644 --- a/src/main/java/io/supertokens/dashboard/Dashboard.java +++ b/src/main/java/io/supertokens/dashboard/Dashboard.java @@ -26,6 +26,7 @@ import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.StorageUtils; import io.supertokens.pluginInterface.dashboard.DashboardSessionInfo; +import io.supertokens.pluginInterface.dashboard.DashboardStorage; import io.supertokens.pluginInterface.dashboard.DashboardUser; import io.supertokens.pluginInterface.dashboard.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.dashboard.exceptions.DuplicateUserIdException; @@ -34,16 +35,15 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.telemetry.WebRequestTelemetryHandler; import io.supertokens.utils.Utils; import jakarta.annotation.Nullable; import org.jetbrains.annotations.TestOnly; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; +import java.util.*; import java.util.regex.Pattern; public class Dashboard { @@ -84,7 +84,24 @@ public static DashboardUser signUpDashboardUser(AppIdentifier appIdentifier, Sto } } - String hashedPassword = PasswordHashing.getInstance(main).createHashWithSalt(appIdentifier, password); + String hashedPassword = null; + try { + hashedPassword = WebRequestTelemetryHandler.INSTANCE.wrapInSpan(TenantIdentifier.BASE_TENANT, + "Hashing dashboard password", + Map.of("email", email), () -> + { + try { + return PasswordHashing.getInstance(main).createHashWithSalt(appIdentifier, password); + } catch (TenantOrAppNotFoundException e) { + throw new RuntimeException(e); + } + }); + } catch (RuntimeException e) { + if (e.getCause() instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.getCause(); + } + throw e; + } while (true) { String userId = Utils.getUUID(); @@ -92,10 +109,32 @@ public static DashboardUser signUpDashboardUser(AppIdentifier appIdentifier, Sto try { DashboardUser user = new DashboardUser(userId, email, hashedPassword, timeJoined); - StorageUtils.getDashboardStorage(storage).createNewDashboardUser(appIdentifier, user); + DashboardStorage dashboardStorage = StorageUtils.getDashboardStorage(storage); + WebRequestTelemetryHandler.INSTANCE.wrapInSpan(TenantIdentifier.BASE_TENANT, + "Creating user in dashboard storage", + Map.of("email", email), () -> { + try { + dashboardStorage.createNewDashboardUser(appIdentifier, user); + } catch (StorageQueryException | DuplicateUserIdException | DuplicateEmailException | + TenantOrAppNotFoundException e) { + throw new RuntimeException(e); + } + return null; + }); + return user; - } catch (DuplicateUserIdException ignored) { - // we retry with a new userId (while loop) + } catch (RuntimeException runtimeException) { + if (runtimeException.getCause() instanceof DuplicateUserIdException) { + // we retry with a new userId (while loop) + } else if (runtimeException.getCause() instanceof StorageQueryException) { + throw (StorageQueryException) runtimeException.getCause(); + } else if (runtimeException.getCause() instanceof DuplicateEmailException) { + throw (DuplicateEmailException) runtimeException.getCause(); + } else if (runtimeException.getCause() instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) runtimeException.getCause(); + } else { + throw runtimeException; + } } } } diff --git a/src/main/java/io/supertokens/inmemorydb/Start.java b/src/main/java/io/supertokens/inmemorydb/Start.java index 0cdad6c95..46f355cb8 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -65,6 +65,7 @@ import io.supertokens.pluginInterface.oauth.OAuthStorage; import io.supertokens.pluginInterface.oauth.exception.DuplicateOAuthLogoutChallengeException; import io.supertokens.pluginInterface.oauth.exception.OAuthClientNotFoundException; +import io.supertokens.pluginInterface.opentelemetry.OtelProvider; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; import io.supertokens.pluginInterface.passwordless.PasswordlessDevice; import io.supertokens.pluginInterface.passwordless.PasswordlessImportUser; @@ -202,7 +203,7 @@ public void assertThatConfigFromSameUserPoolIsNotConflicting(JsonObject otherCon } @Override - public void initFileLogging(String infoLogPath, String errorLogPath) { + public void initFileLogging(String infoLogPath, String errorLogPath, OtelProvider otelProvider) { // no op } diff --git a/src/main/java/io/supertokens/output/Logging.java b/src/main/java/io/supertokens/output/Logging.java index 8806b6569..4e0335b35 100644 --- a/src/main/java/io/supertokens/output/Logging.java +++ b/src/main/java/io/supertokens/output/Logging.java @@ -66,7 +66,7 @@ private Logging(Main main) { Storage storage = StorageLayer.getBaseStorage(main); if (storage != null) { storage.initFileLogging(Config.getBaseConfig(main).getInfoLogPath(main), - Config.getBaseConfig(main).getErrorLogPath(main)); + Config.getBaseConfig(main).getErrorLogPath(main), TelemetryProvider.getInstance(main)); } try { // we wait here for a bit so that the loggers can be properly initialised.. @@ -114,7 +114,7 @@ public static void debug(Main main, TenantIdentifier tenantIdentifier, String ms if (getInstance(main) != null) { String formattedMsg = getFormattedMessage(tenantIdentifier, msg); getInstance(main).infoLogger.debug(formattedMsg); - TelemetryProvider.createLogEvent(main, tenantIdentifier, formattedMsg, "debug"); + TelemetryProvider.getInstance(main).createLogEvent(tenantIdentifier, formattedMsg, "debug"); } } catch (NullPointerException e) { // sometimes logger.debug throws a null pointer exception... @@ -166,7 +166,7 @@ public static void info(Main main, TenantIdentifier tenantIdentifier, String msg getInstance(main).infoLogger.info(msg); } - TelemetryProvider.createLogEvent(main, tenantIdentifier, msg, "info"); + TelemetryProvider.getInstance(main).createLogEvent(tenantIdentifier, msg, "info"); } catch (NullPointerException ignored) { } } @@ -180,7 +180,7 @@ public static void warn(Main main, TenantIdentifier tenantIdentifier, String msg msg = getFormattedMessage(tenantIdentifier, msg); if (getInstance(main) != null) { getInstance(main).errorLogger.warn(msg); - TelemetryProvider.createLogEvent(main, tenantIdentifier, msg, "warn"); + TelemetryProvider.getInstance(main).createLogEvent(tenantIdentifier, msg, "warn"); } } catch (NullPointerException ignored) { } @@ -202,7 +202,7 @@ public static void error(Main main, TenantIdentifier tenantIdentifier, String er if (getInstance(main) != null) { String formattedMessage = getFormattedMessage(tenantIdentifier, err); getInstance(main).errorLogger.error(formattedMessage); - TelemetryProvider.createLogEvent(main, tenantIdentifier, formattedMessage, "error"); + TelemetryProvider.getInstance(main).createLogEvent(tenantIdentifier, formattedMessage, "error"); } if (toConsoleAsWell || getInstance(main) == null) { systemErr(prependTenantIdentifierToMessage(tenantIdentifier, err)); @@ -236,8 +236,8 @@ public static void error(Main main, TenantIdentifier tenantIdentifier, String me message = message.trim(); if (getInstance(main) != null) { getInstance(main).errorLogger.error(getFormattedMessage(tenantIdentifier, message, e)); - TelemetryProvider - .createLogEvent(main, tenantIdentifier, getFormattedMessage(tenantIdentifier, message, e), + TelemetryProvider.getInstance(main) + .createLogEvent(tenantIdentifier, getFormattedMessage(tenantIdentifier, message, e), "error"); } if (toConsoleAsWell || getInstance(main) == null) { diff --git a/src/main/java/io/supertokens/storageLayer/StorageLayer.java b/src/main/java/io/supertokens/storageLayer/StorageLayer.java index c054b7feb..c7ed9aeab 100644 --- a/src/main/java/io/supertokens/storageLayer/StorageLayer.java +++ b/src/main/java/io/supertokens/storageLayer/StorageLayer.java @@ -37,6 +37,8 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.useridmapping.UserIdMapping; +import io.supertokens.telemetry.TelemetryProvider; +import io.supertokens.telemetry.WebRequestTelemetryHandler; import io.supertokens.useridmapping.UserIdType; import jakarta.servlet.ServletException; import org.jetbrains.annotations.TestOnly; @@ -321,7 +323,8 @@ public static void loadAllTenantStorage(Main main, TenantConfig[] tenants) new ArrayList<>(storageToTenantIdentifiersMap.get(((StorageLayer) resource).storage))); ((StorageLayer) resource).storage.initFileLogging( Config.getBaseConfig(main).getInfoLogPath(main), - Config.getBaseConfig(main).getErrorLogPath(main)); + Config.getBaseConfig(main).getErrorLogPath(main), + TelemetryProvider.getInstance(main)); } catch (DbInitException e) { Logging.error(main, TenantIdentifier.BASE_TENANT, e.getMessage(), false, e); @@ -427,20 +430,32 @@ public static Storage[] getStoragesForApp(Main main, AppIdentifier appIdentifier throws TenantOrAppNotFoundException { Map userPoolToStorage = new HashMap<>(); - Map resources = - main.getResourceDistributor() - .getAllResourcesWithResourceKey(RESOURCE_KEY); - for (ResourceDistributor.KeyClass key : resources.keySet()) { - Storage storage = ((StorageLayer) resources.get(key)).storage; - if (key.getTenantIdentifier().toAppIdentifier().equals(appIdentifier)) { - userPoolToStorage.put(storage.getUserPoolId(), storage); + try { + return WebRequestTelemetryHandler.INSTANCE.wrapInSpan(appIdentifier.getAsPublicTenantIdentifier(), + "StorageLayer getStoragesForApp", + Map.of(), () -> { + Map resources = + main.getResourceDistributor() + .getAllResourcesWithResourceKey(RESOURCE_KEY); + for (ResourceDistributor.KeyClass key : resources.keySet()) { + Storage storage = ((StorageLayer) resources.get(key)).storage; + if (key.getTenantIdentifier().toAppIdentifier().equals(appIdentifier)) { + userPoolToStorage.put(storage.getUserPoolId(), storage); + } + } + Storage[] storages = userPoolToStorage.values().toArray(new Storage[0]); + if (storages.length == 0) { + throw new RuntimeException(new TenantOrAppNotFoundException(appIdentifier)); + } + return storages; + }); + } catch (RuntimeException e) { + if (e.getCause() instanceof TenantOrAppNotFoundException) { + throw (TenantOrAppNotFoundException) e.getCause(); + } else { + throw new RuntimeException(e); } } - Storage[] storages = userPoolToStorage.values().toArray(new Storage[0]); - if (storages.length == 0) { - throw new TenantOrAppNotFoundException(appIdentifier); - } - return storages; } public static StorageAndUserIdMapping findStorageAndUserIdMappingForUser( diff --git a/src/main/java/io/supertokens/telemetry/TelemetryProvider.java b/src/main/java/io/supertokens/telemetry/TelemetryProvider.java index 5a789d7c4..ce57dbe11 100644 --- a/src/main/java/io/supertokens/telemetry/TelemetryProvider.java +++ b/src/main/java/io/supertokens/telemetry/TelemetryProvider.java @@ -20,8 +20,10 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; @@ -36,19 +38,20 @@ import io.supertokens.config.Config; import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.opentelemetry.OtelProvider; +import io.supertokens.pluginInterface.opentelemetry.RunnableWithOtel; import org.jetbrains.annotations.TestOnly; +import java.util.Map; import java.util.concurrent.TimeUnit; import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; -public class TelemetryProvider extends ResourceDistributor.SingletonResource { - - private static final String RESOURCE_ID = "io.supertokens.telemetry.TelemetryProvider"; +public class TelemetryProvider extends ResourceDistributor.SingletonResource implements OtelProvider { private final OpenTelemetry openTelemetry; - private static synchronized TelemetryProvider getInstance(Main main) { + public static synchronized TelemetryProvider getInstance(Main main) { TelemetryProvider instance = null; try { instance = (TelemetryProvider) main.getResourceDistributor() @@ -63,15 +66,22 @@ public static void initialize(Main main) { .setResource(TenantIdentifier.BASE_TENANT, RESOURCE_ID, new TelemetryProvider(main)); } - public static void createLogEvent(Main main, TenantIdentifier tenantIdentifier, String logMessage, + @Override + public void createLogEvent(TenantIdentifier tenantIdentifier, String logMessage, String logLevel) { - getInstance(main).openTelemetry.getTracer("core-tracer") - .spanBuilder(logLevel) - .setParent(Context.current()) - .setAttribute("tenant.connectionUriDomain", tenantIdentifier.getConnectionUriDomain()) - .setAttribute("tenant.appId", tenantIdentifier.getAppId()) - .setAttribute("tenant.tenantId", tenantIdentifier.getTenantId()) - .startSpan() + createLogEvent(tenantIdentifier, logMessage, logLevel, Map.of()); + } + + @Override + public void createLogEvent(TenantIdentifier tenantIdentifier, String logMessage, + String logLevel, Map additionalAttributes) { + if (openTelemetry == null) { + throw new IllegalStateException("OpenTelemetry is not initialized. Call initialize() first."); + } + SpanBuilder spanBuilder = createSpanBuilder(tenantIdentifier, logLevel, additionalAttributes); + + + spanBuilder.startSpan() .addEvent("log", Attributes.builder() .put("message", logMessage) @@ -80,33 +90,72 @@ public static void createLogEvent(Main main, TenantIdentifier tenantIdentifier, .end(); } - public static Span startSpan(Main main, TenantIdentifier tenantIdentifier, String spanName) { - Span span = getInstance(main).openTelemetry.getTracer("core-tracer") - .spanBuilder(spanName) + @Override + public T wrapInSpanWithReturn(TenantIdentifier tenantIdentifier, String spanName, + Map additionalAttributes, RunnableWithOtel runnableWithOtel) { + if (openTelemetry == null) { + throw new IllegalStateException("OpenTelemetry is not initialized. Call initialize() first."); + } + + SpanBuilder spanBuilder = createSpanBuilder(tenantIdentifier, spanName, additionalAttributes); + + Span span = spanBuilder.startSpan(); + try (Scope scope = span.makeCurrent()) { + T returnValue = runnableWithOtel.runWithReturnValue(); + span.setAttribute("return.value.type", returnValue == null ? "null" : returnValue.getClass().getName()); + span.setAttribute("return.value.value", returnValue == null ? "null" : returnValue.toString()); + return returnValue; + } finally { + span.end(); + } + } + + @Override + public void createSpanWithAttributes(TenantIdentifier tenantIdentifier, String spanName, + Map additionalAttributes) { + Span span = createSpanBuilder(tenantIdentifier, spanName, additionalAttributes) .setParent(Context.current()) .setAttribute("tenant.connectionUriDomain", tenantIdentifier.getConnectionUriDomain()) .setAttribute("tenant.appId", tenantIdentifier.getAppId()) .setAttribute("tenant.tenantId", tenantIdentifier.getTenantId()) .startSpan(); - span.makeCurrent(); // Set the span as the current context - return span; + span.end(); } - public static Span endSpan(Span span) { - if (span != null) { - span.end(); - } - return span; + + private SpanBuilder createSpanBuilder(TenantIdentifier tenantIdentifier, String spanName, + Map additionalAttributes) { + SpanBuilder spanBuilder = openTelemetry.getTracer("core-tracer") + .spanBuilder(spanName) + .setParent(Context.current()); + + return addAttributesToSpanBuilder(spanBuilder, tenantIdentifier, additionalAttributes); } - public static Span addEventToSpan(Span span, String eventName, Attributes attributes) { - if (span != null) { - span.addEvent(eventName, attributes, System.currentTimeMillis(), TimeUnit.MILLISECONDS); + private SpanBuilder addAttributesToSpanBuilder(SpanBuilder spanBuilder, TenantIdentifier tenantIdentifier, + Map additionalAttributes) { + if (tenantIdentifier == null) { + spanBuilder + .setAttribute("tenant.connectionUriDomain", "unknown") + .setAttribute("tenant.appId", "unknown") + .setAttribute("tenant.tenantId", "unknown"); + } else { + spanBuilder + .setAttribute("tenant.connectionUriDomain", tenantIdentifier.getConnectionUriDomain()) + .setAttribute("tenant.appId", tenantIdentifier.getAppId()) + .setAttribute("tenant.tenantId", tenantIdentifier.getTenantId()); } - return span; - } + if (additionalAttributes != null && !additionalAttributes.isEmpty()) { + // Add additional attributes to the span + for (Map.Entry attribute : additionalAttributes.entrySet()) { + spanBuilder.setAttribute(attribute.getKey(), attribute.getValue()); + } + } + + return spanBuilder; + } private static OpenTelemetry initializeOpenTelemetry(Main main) { if (getInstance(main) != null && getInstance(main).openTelemetry != null) { diff --git a/src/main/java/io/supertokens/telemetry/WebRequestTelemetryHandler.java b/src/main/java/io/supertokens/telemetry/WebRequestTelemetryHandler.java new file mode 100644 index 000000000..2ad99e447 --- /dev/null +++ b/src/main/java/io/supertokens/telemetry/WebRequestTelemetryHandler.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.telemetry; + +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import io.supertokens.pluginInterface.opentelemetry.OtelProvider; +import io.supertokens.pluginInterface.opentelemetry.RunnableWithOtel; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +public enum WebRequestTelemetryHandler { + + INSTANCE; + + private OtelProvider telemetryProvider; + + public synchronized void initializeOtelProvider(OtelProvider otelProvider) { + if (this.telemetryProvider == null) { + this.telemetryProvider = otelProvider; + } + } + + public void wrapRequestInSpan(HttpServletRequest servletRequest, TenantIdentifier tenantIdentifier, + RunnableWithOtel requestHandler) { + Map requestAttributes = getRequestAttributes(servletRequest); + telemetryProvider.wrapInSpanWithReturn(tenantIdentifier, "httpRequest", requestAttributes, requestHandler); + } + + public void createSpan(TenantIdentifier tenantIdentifier, String name, Map attributes) { + telemetryProvider.createSpanWithAttributes(tenantIdentifier, name, attributes); + } + + public T wrapInSpan(TenantIdentifier tenantIdentifier, String name, Map attributes, + RunnableWithOtel runnable) { + return (T) telemetryProvider.wrapInSpanWithReturn(tenantIdentifier, name, attributes, runnable); + } + + + private Map getRequestAttributes(HttpServletRequest servletRequest) { + Map requestAttributes = new HashMap<>(); + requestAttributes.put("http.method", servletRequest.getMethod()); + requestAttributes.put("http.url", servletRequest.getRequestURL().toString()); + if (servletRequest.getQueryString() != null) { + requestAttributes.put("http.query_string", servletRequest.getQueryString()); + } + requestAttributes.put("http.user_agent", servletRequest.getHeader("User-Agent")); + requestAttributes.put("http.client_ip", servletRequest.getRemoteAddr()); + Enumeration headersEnumeration = servletRequest.getHeaderNames(); + while(headersEnumeration.hasMoreElements()) { + String headerName = headersEnumeration.nextElement(); + requestAttributes.put("http.header." + headerName.toLowerCase(), servletRequest.getHeader(headerName)); + } + return requestAttributes; + } + +} diff --git a/src/main/java/io/supertokens/webserver/PathRouter.java b/src/main/java/io/supertokens/webserver/PathRouter.java index 7bbc34099..c0100ddb5 100644 --- a/src/main/java/io/supertokens/webserver/PathRouter.java +++ b/src/main/java/io/supertokens/webserver/PathRouter.java @@ -17,6 +17,8 @@ package io.supertokens.webserver; import io.supertokens.Main; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; +import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -111,6 +113,29 @@ private WebserverAPI getAPIThatMatchesPath(HttpServletRequest req) { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { - getAPIThatMatchesPath(req).service(req, resp); + TenantIdentifier tenantIdentifier = null; + try { + tenantIdentifier = getTenantIdentifierWithoutVerifying(req); + } catch (ServletException e) { + //servlet exception can be thrown by getTenantIdentifierWithoutVerifying(req) + // we are going to ignore it + } + + try { + otelTelemetryWebHandler.wrapRequestInSpan(req, tenantIdentifier, () -> { + try { + getAPIThatMatchesPath(req).service(req, resp); + } catch (IOException e) { + throw new RuntimeException(e); + } + return null; + }); + } catch (RuntimeException exception) { + if (exception.getCause() instanceof IOException) { + throw (IOException) exception.getCause(); //unwrap the IOException so that it can be handled + // properly by the caller + } + throw new RuntimeException(exception); + } } } diff --git a/src/main/java/io/supertokens/webserver/WebserverAPI.java b/src/main/java/io/supertokens/webserver/WebserverAPI.java index 7be51494c..6f63257ce 100644 --- a/src/main/java/io/supertokens/webserver/WebserverAPI.java +++ b/src/main/java/io/supertokens/webserver/WebserverAPI.java @@ -33,6 +33,7 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storageLayer.StorageLayer; +import io.supertokens.telemetry.WebRequestTelemetryHandler; import io.supertokens.useridmapping.UserIdType; import io.supertokens.utils.SemVer; import jakarta.servlet.FilterChain; @@ -58,6 +59,8 @@ public abstract class WebserverAPI extends HttpServlet { public static final Set supportedVersions = new HashSet<>(); private String rid; + protected WebRequestTelemetryHandler otelTelemetryWebHandler; + static { supportedVersions.add(SemVer.v2_7); supportedVersions.add(SemVer.v2_8); @@ -107,6 +110,7 @@ public WebserverAPI(Main main, String rid) { super(); this.main = main; this.rid = rid; + otelTelemetryWebHandler = WebRequestTelemetryHandler.INSTANCE; } public String getRID() { @@ -344,7 +348,7 @@ private String getConnectionUriDomain(HttpServletRequest req) throws ServletExce return null; } - private TenantIdentifier getTenantIdentifierWithoutVerifying(HttpServletRequest req) throws ServletException { + protected TenantIdentifier getTenantIdentifierWithoutVerifying(HttpServletRequest req) throws ServletException { return new TenantIdentifier(this.getConnectionUriDomain(req), this.getAppId(req), this.getTenantId(req)); } diff --git a/src/main/java/io/supertokens/webserver/api/core/HelloAPI.java b/src/main/java/io/supertokens/webserver/api/core/HelloAPI.java index c0691863a..47d021969 100644 --- a/src/main/java/io/supertokens/webserver/api/core/HelloAPI.java +++ b/src/main/java/io/supertokens/webserver/api/core/HelloAPI.java @@ -17,9 +17,11 @@ package io.supertokens.webserver.api.core; import io.supertokens.Main; +import io.supertokens.pluginInterface.KeyValueInfo; import io.supertokens.pluginInterface.Storage; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.utils.RateLimiter; @@ -29,6 +31,7 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Map; // the point of this API is only to test that the server is up and running. @@ -77,13 +80,25 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO private void handleRequest(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { // API is app specific - try { AppIdentifier appIdentifier = getAppIdentifier(req); - Storage[] storages = StorageLayer.getStoragesForApp(main, - appIdentifier); // throws tenantOrAppNotFoundException + TenantIdentifier nonVerifiedTenant = getTenantIdentifierWithoutVerifying(req); + + Storage[] storages = + otelTelemetryWebHandler.wrapInSpan(getTenantIdentifierWithoutVerifying(req), + "HelloAPI getStoragesForApp", + Map.of(), () -> { + try { + return StorageLayer.getStoragesForApp(main, + appIdentifier); // throws tenantOrAppNotFoundException + } catch (TenantOrAppNotFoundException e) { + throw new RuntimeException(e); + } + }); RateLimiter rateLimiter = RateLimiter.getInstance(appIdentifier, super.main, 200); + otelTelemetryWebHandler.createSpan(nonVerifiedTenant, "HelloAPI handleRequest", + Map.of("rateLimited", String.valueOf(rateLimiter.checkRequest()))); if (!rateLimiter.checkRequest()) { if (Main.isTesting) { super.sendTextResponse(200, "RateLimitedHello", resp); @@ -96,12 +111,28 @@ private void handleRequest(HttpServletRequest req, HttpServletResponse resp) thr for (Storage storage : storages) { // even if the public tenant does not exist, the following function will return a null // idea here is to test that the storage is working - storage.getKeyValue(appIdentifier.getAsPublicTenantIdentifier(), "Test"); + KeyValueInfo storageResponse = otelTelemetryWebHandler.wrapInSpan(nonVerifiedTenant, + "HelloAPI storageGetKeyValue", + Map.of(), () -> { + try { + return storage.getKeyValue(appIdentifier.getAsPublicTenantIdentifier(), "Test"); + } catch (StorageQueryException e) { + throw new RuntimeException(e); + } + }); } super.sendTextResponse(200, "Hello", resp); - } catch (StorageQueryException | TenantOrAppNotFoundException e) { + } catch (TenantOrAppNotFoundException e) { // we send 500 status code throw new ServletException(e); + } catch (RuntimeException e) { + if (e.getCause() instanceof IOException) { + throw (IOException) e.getCause(); + } else if (e.getCause() instanceof StorageQueryException || + e.getCause() instanceof TenantOrAppNotFoundException) { + throw new ServletException(e.getCause()); + } + throw e; } } } diff --git a/src/main/java/io/supertokens/webserver/api/dashboard/DashboardSignInAPI.java b/src/main/java/io/supertokens/webserver/api/dashboard/DashboardSignInAPI.java index f8032715c..58745c3f2 100644 --- a/src/main/java/io/supertokens/webserver/api/dashboard/DashboardSignInAPI.java +++ b/src/main/java/io/supertokens/webserver/api/dashboard/DashboardSignInAPI.java @@ -32,6 +32,7 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Map; public class DashboardSignInAPI extends WebserverAPI { @@ -46,10 +47,14 @@ public String getPath() { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + // API is app specific JsonObject input = InputParser.parseJsonObjectOrThrowError(req); String email = InputParser.parseStringOrThrowError(input, "email", false); + otelTelemetryWebHandler.createSpan(getTenantIdentifierWithoutVerifying(req), "DashboardSignInAPI signIn", + Map.of("email", email)); + // normalize email email = Utils.normalizeAndValidateStringParam(email, "email"); email = io.supertokens.utils.Utils.normaliseEmail(email); @@ -60,30 +65,59 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I password = Utils.normalizeAndValidateStringParam(password, "password"); try { - String sessionId = Dashboard.signInDashboardUser( - getAppIdentifier(req), - enforcePublicTenantAndGetPublicTenantStorage(req), main, email, password); + String finalEmail = email; + String finalPassword = password; + String sessionId = otelTelemetryWebHandler.wrapInSpan(getTenantIdentifierWithoutVerifying(req), + "DashboardSignInAPI signIn", + Map.of("email", email), () -> { + try { + return Dashboard.signInDashboardUser( + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), main, finalEmail, finalPassword); + } catch (StorageQueryException | UserSuspendedException | TenantOrAppNotFoundException | + ServletException | BadPermissionException e) { + throw new RuntimeException(e); + } + } + ); + if (sessionId == null) { JsonObject response = new JsonObject(); response.addProperty("status", "INVALID_CREDENTIALS_ERROR"); + otelTelemetryWebHandler.createSpan(getTenantIdentifierWithoutVerifying(req), + "DashboardSignInAPI signIn result", + Map.of("status", response.get("status").getAsString())); super.sendJsonResponse(200, response, resp); return; } JsonObject response = new JsonObject(); response.addProperty("status", "OK"); response.addProperty("sessionId", sessionId); + otelTelemetryWebHandler.createSpan(getTenantIdentifierWithoutVerifying(req), + "DashboardSignInAPI signIn response", + Map.of("status", response.get("status").getAsString(), "sessionId", sessionId)); super.sendJsonResponse(200, response, resp); - } catch (UserSuspendedException e) { - JsonObject response = new JsonObject(); - response.addProperty("status", "USER_SUSPENDED_ERROR"); - // TODO: update message - response.addProperty("message", - "User is currently suspended, please sign in with another account, or reactivate the SuperTokens " + - "core license key"); - super.sendJsonResponse(200, response, resp); - } catch (StorageQueryException | TenantOrAppNotFoundException | BadPermissionException e) { - throw new ServletException(e); + } catch (RuntimeException runtimeException) { + if (runtimeException.getCause() instanceof StorageQueryException + || runtimeException.getCause() instanceof TenantOrAppNotFoundException + || runtimeException.getCause() instanceof BadPermissionException) { + throw new ServletException(runtimeException.getCause()); + } else if (runtimeException.getCause() instanceof UserSuspendedException) { + JsonObject response = new JsonObject(); + response.addProperty("status", "USER_SUSPENDED_ERROR"); + // TODO: update message + response.addProperty("message", + "User is currently suspended, please sign in with another account, or reactivate the " + + "SuperTokens " + + "core license key"); + otelTelemetryWebHandler.createSpan(getTenantIdentifierWithoutVerifying(req), + "DashboardSignInAPI signIn result", + Map.of("status", response.get("status").getAsString(), "message", + response.get("message").getAsString())); + super.sendJsonResponse(200, response, resp); + } else { + throw runtimeException; + } } - } } diff --git a/src/main/java/io/supertokens/webserver/api/dashboard/DashboardUserAPI.java b/src/main/java/io/supertokens/webserver/api/dashboard/DashboardUserAPI.java index c66444b74..201d234ad 100644 --- a/src/main/java/io/supertokens/webserver/api/dashboard/DashboardUserAPI.java +++ b/src/main/java/io/supertokens/webserver/api/dashboard/DashboardUserAPI.java @@ -31,6 +31,7 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.Utils; @@ -41,6 +42,7 @@ import java.io.IOException; import java.io.Serial; +import java.util.Map; public class DashboardUserAPI extends WebserverAPI { @@ -61,6 +63,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I // API is app specific try { + TenantIdentifier unverifiedTenantIdentifier = getTenantIdentifierWithoutVerifying(req); + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); String email = InputParser.parseStringOrThrowError(input, "email", false); @@ -68,10 +72,15 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I email = Utils.normalizeAndValidateStringParam(email, "email"); email = io.supertokens.utils.Utils.normaliseEmail(email); + otelTelemetryWebHandler.createSpan(unverifiedTenantIdentifier, "DashboardUserAPI", + Map.of("email", email)); + // check if input email is invalid if (!Dashboard.isValidEmail(email)) { JsonObject response = new JsonObject(); response.addProperty("status", "INVALID_EMAIL_ERROR"); + otelTelemetryWebHandler.createSpan(unverifiedTenantIdentifier, "DashboardUserAPI isValidEmail", + Map.of("response", response.get("status").getAsString())); super.sendJsonResponse(200, response, resp); return; } @@ -82,7 +91,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I password = Utils.normalizeAndValidateStringParam(password, "password"); // check if input password is a strong password - String passwordErrorMessage = Dashboard.validatePassword(password); + String finalPassword = password; + String passwordErrorMessage = + otelTelemetryWebHandler.wrapInSpan(unverifiedTenantIdentifier, "DashboardUserAPI validatePassword", + Map.of("email", email), () -> Dashboard.validatePassword(finalPassword)); if (passwordErrorMessage != null) { JsonObject response = new JsonObject(); response.addProperty("status", "PASSWORD_WEAK_ERROR"); @@ -91,24 +103,50 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I return; } - DashboardUser user = Dashboard.signUpDashboardUser( - getAppIdentifier(req), - enforcePublicTenantAndGetPublicTenantStorage(req), - main, email, password); + String finalEmail = email; + DashboardUser user = otelTelemetryWebHandler.wrapInSpan(unverifiedTenantIdentifier, + "DashboardUserAPI signUp", + Map.of("email", email), () -> + { + try { + return Dashboard.signUpDashboardUser( + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + main, finalEmail, finalPassword); + } catch (StorageQueryException | DuplicateEmailException | FeatureNotEnabledException | + TenantOrAppNotFoundException | ServletException | BadPermissionException e) { + throw new RuntimeException(e); + } + } + ); JsonObject userAsJsonObject = new JsonParser().parse(new Gson().toJson(user)).getAsJsonObject(); JsonObject response = new JsonObject(); response.addProperty("status", "OK"); response.add("user", userAsJsonObject); + otelTelemetryWebHandler.createSpan(unverifiedTenantIdentifier, "DashboardUserAPI signUp response", + Map.of("status", response.get("status").getAsString(), "userId", user.userId)); super.sendJsonResponse(200, response, resp); - } catch (DuplicateEmailException e) { - JsonObject response = new JsonObject(); - response.addProperty("status", "EMAIL_ALREADY_EXISTS_ERROR"); - super.sendJsonResponse(200, response, resp); - } catch (StorageQueryException | FeatureNotEnabledException | TenantOrAppNotFoundException | - BadPermissionException e) { - throw new ServletException(e); + } catch (RuntimeException e) { + if (e.getCause() instanceof DuplicateEmailException) { + JsonObject response = new JsonObject(); + response.addProperty("status", "EMAIL_ALREADY_EXISTS_ERROR"); + super.sendJsonResponse(200, response, resp); + } else if (e.getCause() instanceof StorageQueryException || + e.getCause() instanceof FeatureNotEnabledException || + e.getCause() instanceof TenantOrAppNotFoundException || + e.getCause() instanceof BadPermissionException) { + otelTelemetryWebHandler.createSpan(getTenantIdentifierWithoutVerifying(req), + "DashboardUserAPI doPost error", + Map.of("error", e.getCause().getMessage() == null ? "null" : e.getCause().getMessage())); + throw new ServletException(e.getCause()); + } else { + otelTelemetryWebHandler.createSpan(getTenantIdentifierWithoutVerifying(req), + "DashboardUserAPI doPost error", + Map.of("error", e.getMessage() == null ? "null" : e.getMessage())); + throw new RuntimeException(e); + } } } @@ -127,6 +165,9 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO if (!Dashboard.isValidEmail(newEmail)) { JsonObject response = new JsonObject(); response.addProperty("status", "INVALID_EMAIL_ERROR"); + otelTelemetryWebHandler.createSpan(getTenantIdentifierWithoutVerifying(req), + "DashboardUserAPI isValidEmail", + Map.of("email", newEmail, "response", response.get("status").getAsString())); super.sendJsonResponse(200, response, resp); return; }