Skip to content

Commit 760b4c0

Browse files
committed
backport: logs to otel
1 parent c189651 commit 760b4c0

6 files changed

Lines changed: 214 additions & 4 deletions

File tree

build.gradle

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,6 @@ dependencies {
4141
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core
4242
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.18.2'
4343

44-
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
45-
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.14'
46-
4744
// https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core
4845
implementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.1.18'
4946

@@ -80,6 +77,20 @@ dependencies {
8077
// https://mvnrepository.com/artifact/com.webauthn4j/webauthn4j-core
8178
implementation group: 'com.webauthn4j', name: 'webauthn4j-core', version: '0.28.6.RELEASE'
8279

80+
implementation platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha:2.17.0-alpha")
81+
82+
implementation("ch.qos.logback:logback-core:1.5.18")
83+
implementation("ch.qos.logback:logback-classic:1.5.18")
84+
85+
// OpenTelemetry core
86+
implementation("io.opentelemetry:opentelemetry-sdk")
87+
implementation("io.opentelemetry:opentelemetry-exporter-otlp")
88+
implementation("io.opentelemetry:opentelemetry-exporter-logging")
89+
implementation("io.opentelemetry:opentelemetry-api")
90+
91+
implementation("io.opentelemetry.semconv:opentelemetry-semconv")
92+
93+
8394
compileOnly project(":supertokens-plugin-interface")
8495

8596
// this is so that we can find plugin-interface jar while testing

src/main/java/io/supertokens/Main.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
4444
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
4545
import io.supertokens.storageLayer.StorageLayer;
46+
import io.supertokens.telemetry.TelemetryProvider;
4647
import io.supertokens.version.Version;
4748
import io.supertokens.webserver.Webserver;
4849
import org.jetbrains.annotations.TestOnly;
@@ -166,6 +167,8 @@ private void init() throws IOException, StorageQueryException {
166167

167168
Logging.info(this, TenantIdentifier.BASE_TENANT, "Completed config.yaml loading.", true);
168169

170+
TelemetryProvider.initialize(this);
171+
169172
// loading storage layer
170173
try {
171174
StorageLayer.initPrimary(this, CLIOptions.get(this).getInstallationPath() + "plugin/",
@@ -441,6 +444,7 @@ private void stopApp() {
441444
StorageLayer.close(this);
442445
removeDotStartedFileForThisProcess();
443446
Logging.stopLogging(this);
447+
TelemetryProvider.closeTelemetry(this);
444448
// uncomment this when you want to confirm that processes are actually shut.
445449
// printRunningThreadNames();
446450

src/main/java/io/supertokens/config/CoreConfig.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,14 @@ public class CoreConfig {
361361
@ConfigDescription("Time in milliseconds for how long a webauthn account recovery token is valid for. [Default: 3600000 (1 hour)]")
362362
private long webauthn_recover_account_token_lifetime = 3600000; // in MS;
363363

364+
@ConfigYamlOnly
365+
@JsonProperty
366+
@ConfigDescription(
367+
"The URL of the OpenTelemetry collector to which the core will send telemetry data. " +
368+
"This should be in the format http://<host>:<port> or https://<host>:<port>. (Default: " +
369+
"http://localhost:4317)")
370+
private String otel_collector_connection_uri = "http://localhost:4317";
371+
364372
@IgnoreForAnnotationCheck
365373
private static boolean disableOAuthValidationForTest = false;
366374

@@ -608,6 +616,10 @@ public int getBulkMigrationBatchSize() {
608616
return bulk_migration_batch_size;
609617
}
610618

619+
public String getOtelCollectorConnectionURI() {
620+
return otel_collector_connection_uri;
621+
}
622+
611623
private String getConfigFileLocation(Main main) {
612624
return new File(CLIOptions.get(main).getConfigFilePath() == null
613625
? CLIOptions.get(main).getInstallationPath() + "config.yaml"

src/main/java/io/supertokens/output/Logging.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
3333
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
3434
import io.supertokens.storageLayer.StorageLayer;
35+
import io.supertokens.telemetry.TelemetryProvider;
3536
import io.supertokens.utils.Utils;
3637
import io.supertokens.version.Version;
3738
import io.supertokens.webserver.Webserver;
@@ -112,6 +113,9 @@ public static void debug(Main main, TenantIdentifier tenantIdentifier, String ms
112113
msg = msg.trim();
113114
if (getInstance(main) != null) {
114115
getInstance(main).infoLogger.debug(getFormattedMessage(tenantIdentifier, msg));
116+
String formattedMsg = getFormattedMessage(tenantIdentifier, msg);
117+
getInstance(main).infoLogger.debug(formattedMsg);
118+
TelemetryProvider.createLogEvent(main, tenantIdentifier, formattedMsg, "debug");
115119
}
116120
} catch (NullPointerException e) {
117121
// sometimes logger.debug throws a null pointer exception...
@@ -162,6 +166,8 @@ public static void info(Main main, TenantIdentifier tenantIdentifier, String msg
162166
if (getInstance(main) != null) {
163167
getInstance(main).infoLogger.info(msg);
164168
}
169+
170+
TelemetryProvider.createLogEvent(main, tenantIdentifier, msg, "info");
165171
} catch (NullPointerException ignored) {
166172
}
167173
}
@@ -175,6 +181,8 @@ public static void warn(Main main, TenantIdentifier tenantIdentifier, String msg
175181
msg = getFormattedMessage(tenantIdentifier, msg);
176182
if (getInstance(main) != null) {
177183
getInstance(main).errorLogger.warn(msg);
184+
TelemetryProvider.createLogEvent(main, tenantIdentifier, msg, "warn");
185+
178186
}
179187
} catch (NullPointerException ignored) {
180188
}
@@ -194,7 +202,9 @@ public static void error(Main main, TenantIdentifier tenantIdentifier, String er
194202
try {
195203
err = err.trim();
196204
if (getInstance(main) != null) {
197-
getInstance(main).errorLogger.error(getFormattedMessage(tenantIdentifier, err));
205+
String formattedMessage = getFormattedMessage(tenantIdentifier, err);
206+
getInstance(main).errorLogger.error(formattedMessage);
207+
TelemetryProvider.createLogEvent(main, tenantIdentifier, formattedMessage, "error");
198208
}
199209
if (toConsoleAsWell || getInstance(main) == null) {
200210
systemErr(prependTenantIdentifierToMessage(tenantIdentifier, err));
@@ -228,6 +238,9 @@ public static void error(Main main, TenantIdentifier tenantIdentifier, String me
228238
message = message.trim();
229239
if (getInstance(main) != null) {
230240
getInstance(main).errorLogger.error(getFormattedMessage(tenantIdentifier, message, e));
241+
TelemetryProvider
242+
.createLogEvent(main, tenantIdentifier, getFormattedMessage(tenantIdentifier, message, e),
243+
"error");
231244
}
232245
if (toConsoleAsWell || getInstance(main) == null) {
233246
systemErr(prependTenantIdentifierToMessage(tenantIdentifier, message));
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved.
3+
*
4+
* This software is licensed under the Apache License, Version 2.0 (the
5+
* "License") as published by the Apache Software Foundation.
6+
*
7+
* You may not use this file except in compliance with the License. You may
8+
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package io.supertokens.telemetry;
18+
19+
import io.opentelemetry.api.GlobalOpenTelemetry;
20+
import io.opentelemetry.api.OpenTelemetry;
21+
import io.opentelemetry.api.common.Attributes;
22+
import io.opentelemetry.api.trace.Span;
23+
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
24+
import io.opentelemetry.context.Context;
25+
import io.opentelemetry.context.propagation.ContextPropagators;
26+
import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter;
27+
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
28+
import io.opentelemetry.sdk.OpenTelemetrySdk;
29+
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
30+
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
31+
import io.opentelemetry.sdk.resources.Resource;
32+
import io.opentelemetry.sdk.trace.SdkTracerProvider;
33+
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
34+
import io.supertokens.Main;
35+
import io.supertokens.ResourceDistributor;
36+
import io.supertokens.config.Config;
37+
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
38+
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
39+
import org.jetbrains.annotations.TestOnly;
40+
41+
import java.util.concurrent.TimeUnit;
42+
43+
import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME;
44+
45+
public class TelemetryProvider extends ResourceDistributor.SingletonResource {
46+
47+
private static final String RESOURCE_ID = "io.supertokens.telemetry.TelemetryProvider";
48+
49+
private final OpenTelemetry openTelemetry;
50+
51+
private static synchronized TelemetryProvider getInstance(Main main) {
52+
TelemetryProvider instance = null;
53+
try {
54+
instance = (TelemetryProvider) main.getResourceDistributor()
55+
.getResource(TenantIdentifier.BASE_TENANT, RESOURCE_ID);
56+
} catch (TenantOrAppNotFoundException ignored) {
57+
}
58+
return instance;
59+
}
60+
61+
public static void initialize(Main main) {
62+
main.getResourceDistributor()
63+
.setResource(TenantIdentifier.BASE_TENANT, RESOURCE_ID, new TelemetryProvider(main));
64+
}
65+
66+
public static void createLogEvent(Main main, TenantIdentifier tenantIdentifier, String logMessage,
67+
String logLevel) {
68+
getInstance(main).openTelemetry.getTracer("core-tracer")
69+
.spanBuilder(logLevel)
70+
.setParent(Context.current())
71+
.setAttribute("tenant.connectionUriDomain", tenantIdentifier.getConnectionUriDomain())
72+
.setAttribute("tenant.appId", tenantIdentifier.getAppId())
73+
.setAttribute("tenant.tenantId", tenantIdentifier.getTenantId())
74+
.startSpan()
75+
.addEvent("log",
76+
Attributes.builder()
77+
.put("message", logMessage)
78+
.build(),
79+
System.currentTimeMillis(), TimeUnit.MILLISECONDS)
80+
.end();
81+
}
82+
83+
public static Span startSpan(Main main, TenantIdentifier tenantIdentifier, String spanName) {
84+
Span span = getInstance(main).openTelemetry.getTracer("core-tracer")
85+
.spanBuilder(spanName)
86+
.setParent(Context.current())
87+
.setAttribute("tenant.connectionUriDomain", tenantIdentifier.getConnectionUriDomain())
88+
.setAttribute("tenant.appId", tenantIdentifier.getAppId())
89+
.setAttribute("tenant.tenantId", tenantIdentifier.getTenantId())
90+
.startSpan();
91+
92+
span.makeCurrent(); // Set the span as the current context
93+
return span;
94+
}
95+
96+
public static Span endSpan(Span span) {
97+
if (span != null) {
98+
span.end();
99+
}
100+
return span;
101+
}
102+
103+
public static Span addEventToSpan(Span span, String eventName, Attributes attributes) {
104+
if (span != null) {
105+
span.addEvent(eventName, attributes, System.currentTimeMillis(), TimeUnit.MILLISECONDS);
106+
}
107+
return span;
108+
}
109+
110+
111+
private static OpenTelemetry initializeOpenTelemetry(Main main) {
112+
if (getInstance(main) != null && getInstance(main).openTelemetry != null) {
113+
return getInstance(main).openTelemetry; // already initialized
114+
}
115+
116+
Resource resource = Resource.getDefault().toBuilder()
117+
.put(SERVICE_NAME, "supertokens-core")
118+
.build();
119+
120+
String collectorUri = Config.getBaseConfig(main).getOtelCollectorConnectionURI();
121+
122+
SdkTracerProvider sdkTracerProvider =
123+
SdkTracerProvider.builder()
124+
.setResource(resource)
125+
.addSpanProcessor(SimpleSpanProcessor.create(OtlpGrpcSpanExporter.builder()
126+
.setEndpoint(collectorUri) // otel collector
127+
.build()))
128+
.build();
129+
130+
OpenTelemetrySdk sdk =
131+
OpenTelemetrySdk.builder()
132+
.setTracerProvider(sdkTracerProvider)
133+
.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
134+
.setLoggerProvider(
135+
SdkLoggerProvider.builder()
136+
.setResource(resource)
137+
.addLogRecordProcessor(
138+
BatchLogRecordProcessor.builder(
139+
OtlpGrpcLogRecordExporter.builder()
140+
.setEndpoint(collectorUri)
141+
.build())
142+
143+
.build())
144+
.build())
145+
.build();
146+
147+
// Add hook to close SDK, which flushes logs
148+
Runtime.getRuntime().addShutdownHook(new Thread(sdk::close));
149+
return sdk;
150+
}
151+
152+
@TestOnly
153+
public static void resetForTest() {
154+
GlobalOpenTelemetry.resetForTest();
155+
}
156+
157+
public static void closeTelemetry(Main main) {
158+
OpenTelemetry telemetry = getInstance(main).openTelemetry;
159+
if (telemetry instanceof OpenTelemetrySdk) {
160+
((OpenTelemetrySdk) telemetry).close();
161+
}
162+
}
163+
164+
private TelemetryProvider(Main main) {
165+
openTelemetry = initializeOpenTelemetry(main);
166+
}
167+
}

src/test/java/io/supertokens/test/Utils.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import io.supertokens.pluginInterface.PluginInterfaceTesting;
2424
import io.supertokens.pluginInterface.useridmapping.UserIdMapping;
2525
import io.supertokens.storageLayer.StorageLayer;
26+
import io.supertokens.telemetry.TelemetryProvider;
2627
import io.supertokens.test.httpRequest.HttpRequestForTesting;
2728
import io.supertokens.test.httpRequest.HttpResponseException;
2829
import io.supertokens.useridmapping.UserIdType;
@@ -72,6 +73,8 @@ public static void afterTesting() {
7273
} catch (Exception ignored) {
7374
}
7475

76+
TelemetryProvider.resetForTest();
77+
7578
} catch (Exception e) {
7679
e.printStackTrace();
7780
}

0 commit comments

Comments
 (0)