Skip to content

Commit 28a45de

Browse files
committed
add multiplexed transport
1 parent 6f23a76 commit 28a45de

6 files changed

Lines changed: 279 additions & 6 deletions

File tree

sentry/api/sentry.api

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,6 +1422,11 @@ public final class io/sentry/MonitorScheduleUnit : java/lang/Enum {
14221422
public static fun values ()[Lio/sentry/MonitorScheduleUnit;
14231423
}
14241424

1425+
public final class io/sentry/MultiplexedTransportFactory : io/sentry/ITransportFactory {
1426+
public fun <init> (Lio/sentry/ITransportFactory;Ljava/util/List;)V
1427+
public fun create (Lio/sentry/SentryOptions;Lio/sentry/RequestDetails;)Lio/sentry/transport/ITransport;
1428+
}
1429+
14251430
public final class io/sentry/NoOpCompositePerformanceCollector : io/sentry/CompositePerformanceCollector {
14261431
public fun close ()V
14271432
public static fun getInstance ()Lio/sentry/NoOpCompositePerformanceCollector;
@@ -6257,6 +6262,16 @@ public abstract interface class io/sentry/transport/ITransportGate {
62576262
public abstract fun isConnected ()Z
62586263
}
62596264

6265+
public final class io/sentry/transport/MultiplexedTransport : io/sentry/transport/ITransport {
6266+
public fun <init> (Ljava/util/List;)V
6267+
public fun close ()V
6268+
public fun close (Z)V
6269+
public fun flush (J)V
6270+
public fun getRateLimiter ()Lio/sentry/transport/RateLimiter;
6271+
public fun isHealthy ()Z
6272+
public fun send (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V
6273+
}
6274+
62606275
public final class io/sentry/transport/NoOpEnvelopeCache : io/sentry/cache/IEnvelopeCache {
62616276
public fun <init> ()V
62626277
public fun discard (Lio/sentry/SentryEnvelope;)V
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.sentry;
2+
3+
import io.sentry.transport.ITransport;
4+
import io.sentry.transport.MultiplexedTransport;
5+
import io.sentry.util.Objects;
6+
import java.util.List;
7+
import java.util.stream.Collectors;
8+
import org.jetbrains.annotations.ApiStatus;
9+
import org.jetbrains.annotations.NotNull;
10+
11+
@ApiStatus.Experimental
12+
public final class MultiplexedTransportFactory implements ITransportFactory {
13+
14+
private final @NotNull ITransportFactory transportFactory;
15+
private final @NotNull List<Dsn> dsns;
16+
17+
public MultiplexedTransportFactory(
18+
final @NotNull ITransportFactory transportFactory, final @NotNull List<String> dsns) {
19+
Objects.requireNonNull(transportFactory, "transportFactory is required");
20+
Objects.requireNonNull(dsns, "dsns is required");
21+
22+
this.transportFactory = transportFactory;
23+
this.dsns = dsns.stream().map(Dsn::new).collect(Collectors.toList());
24+
}
25+
26+
@Override
27+
public @NotNull ITransport create(
28+
final @NotNull SentryOptions options, final @NotNull RequestDetails requestDetails) {
29+
final List<ITransport> transports =
30+
dsns.stream().map(dsn -> createTransport(options, dsn)).collect(Collectors.toList());
31+
return new MultiplexedTransport(transports);
32+
}
33+
34+
private @NotNull ITransport createTransport(
35+
final @NotNull SentryOptions options, final @NotNull Dsn dsn) {
36+
final RequestDetails requestDetails =
37+
new RequestDetailsResolver(dsn, options.getSentryClientName()).resolve();
38+
return transportFactory.create(options, requestDetails);
39+
}
40+
}

sentry/src/main/java/io/sentry/RequestDetailsResolver.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.HashMap;
66
import java.util.Map;
77
import org.jetbrains.annotations.NotNull;
8+
import org.jetbrains.annotations.Nullable;
89

910
/** Resolves {@link RequestDetails}. */
1011
final class RequestDetailsResolver {
@@ -13,15 +14,23 @@ final class RequestDetailsResolver {
1314
/** HTTP Header for the authentication to Sentry. */
1415
private static final String SENTRY_AUTH = "X-Sentry-Auth";
1516

16-
private final @NotNull SentryOptions options;
17+
private final @NotNull Dsn dsn;
18+
private final @Nullable String sentryClientName;
19+
20+
public RequestDetailsResolver(final @NotNull Dsn dsn, final @Nullable String sentryClientName) {
21+
this.dsn = Objects.requireNonNull(dsn, "dsn is required");
22+
this.sentryClientName = sentryClientName;
23+
}
1724

1825
public RequestDetailsResolver(final @NotNull SentryOptions options) {
19-
this.options = Objects.requireNonNull(options, "options is required");
26+
Objects.requireNonNull(options, "options is required");
27+
28+
this.dsn = options.retrieveParsedDsn();
29+
this.sentryClientName = options.getSentryClientName();
2030
}
2131

2232
@NotNull
2333
RequestDetails resolve() {
24-
final Dsn dsn = options.retrieveParsedDsn();
2534
final URI sentryUri = dsn.getSentryUri();
2635
final String envelopeUrl = sentryUri.resolve(sentryUri.getPath() + "/envelope/").toString();
2736

@@ -33,15 +42,14 @@ RequestDetails resolve() {
3342
+ SentryClient.SENTRY_PROTOCOL_VERSION
3443
+ ","
3544
+ "sentry_client="
36-
+ options.getSentryClientName()
45+
+ sentryClientName
3746
+ ","
3847
+ "sentry_key="
3948
+ publicKey
4049
+ (secretKey != null && secretKey.length() > 0 ? (",sentry_secret=" + secretKey) : "");
41-
final String userAgent = options.getSentryClientName();
4250

4351
final Map<String, String> headers = new HashMap<>();
44-
headers.put(USER_AGENT, userAgent);
52+
headers.put(USER_AGENT, sentryClientName);
4553
headers.put(SENTRY_AUTH, authHeader);
4654

4755
return new RequestDetails(envelopeUrl, headers);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package io.sentry.transport;
2+
3+
import io.sentry.Hint;
4+
import io.sentry.SentryEnvelope;
5+
import io.sentry.util.Objects;
6+
import java.io.IOException;
7+
import java.util.List;
8+
import java.util.Optional;
9+
import java.util.stream.Collectors;
10+
import org.jetbrains.annotations.ApiStatus;
11+
import org.jetbrains.annotations.NotNull;
12+
import org.jetbrains.annotations.Nullable;
13+
14+
@ApiStatus.Internal
15+
public final class MultiplexedTransport implements ITransport {
16+
private final @NotNull List<ITransport> transports;
17+
18+
public MultiplexedTransport(final @NotNull List<ITransport> transports) {
19+
this.transports = Objects.requireNonNull(transports, "transports is required");
20+
}
21+
22+
@Override
23+
public void send(final @NotNull SentryEnvelope envelope, final @NotNull Hint hint)
24+
throws IOException {
25+
for (ITransport transport : transports) {
26+
transport.send(envelope, hint);
27+
}
28+
}
29+
30+
@Override
31+
public boolean isHealthy() {
32+
return transports.stream().allMatch(ITransport::isHealthy);
33+
}
34+
35+
@Override
36+
public void flush(final long timeoutMillis) {
37+
transports.forEach(transport -> transport.flush(timeoutMillis));
38+
}
39+
40+
@Override
41+
public @Nullable RateLimiter getRateLimiter() {
42+
// Prefer one with rate limit active, else fall back to arbitrary one
43+
final List<RateLimiter> rateLimiters =
44+
this.transports.stream().map(ITransport::getRateLimiter).collect(Collectors.toList());
45+
final Optional<RateLimiter> activeRateLimiter =
46+
rateLimiters.stream().filter(RateLimiter::isAnyRateLimitActive).findAny();
47+
return activeRateLimiter.orElse(rateLimiters.stream().findAny().orElse(null));
48+
}
49+
50+
@Override
51+
public void close(final boolean isRestarting) throws IOException {
52+
for (ITransport transport : transports) {
53+
transport.close(isRestarting);
54+
}
55+
}
56+
57+
@Override
58+
public void close() throws IOException {
59+
for (ITransport transport : transports) {
60+
transport.close();
61+
}
62+
}
63+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package io.sentry
2+
import org.junit.Test
3+
import org.mockito.kotlin.argumentCaptor
4+
import org.mockito.kotlin.eq
5+
import org.mockito.kotlin.mock
6+
import org.mockito.kotlin.times
7+
import org.mockito.kotlin.verify
8+
import kotlin.test.assertContains
9+
import kotlin.test.assertEquals
10+
import kotlin.test.assertNotNull
11+
12+
class MultiplexedTransportFactoryTest {
13+
private class Fixture {
14+
val dsn1 = "https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647"
15+
var dsn2 = "https://007508298f9048729e434faa9ae4f49@fake-sentry.io:65535/1234567890"
16+
var transportFactory = mock<ITransportFactory>()
17+
var sentryOptions: SentryOptions = SentryOptions().apply {
18+
dsn = dsn1
19+
}
20+
21+
fun getSUT(): MultiplexedTransportFactory {
22+
return MultiplexedTransportFactory(transportFactory, listOf(dsn1, dsn2))
23+
}
24+
}
25+
26+
private val fixture = Fixture()
27+
28+
@Test
29+
fun `create transport`() {
30+
val requestDetailsResolver = RequestDetailsResolver(fixture.sentryOptions)
31+
32+
val transport = fixture.getSUT()
33+
.create(fixture.sentryOptions, requestDetailsResolver.resolve())
34+
35+
assertNotNull(transport)
36+
37+
val captor = argumentCaptor<RequestDetails>()
38+
verify(fixture.transportFactory, times(2))
39+
.create(eq(fixture.sentryOptions), captor.capture())
40+
41+
assertEquals("/api/2147483647/envelope/", captor.firstValue.url.path)
42+
assertEquals("/api/1234567890/envelope/", captor.secondValue.url.path)
43+
assertContains(
44+
captor.firstValue.headers["X-Sentry-Auth"].toString(),
45+
"sentry_key=d4d82fc1c2c4032a83f3a29aa3a3aff"
46+
)
47+
assertContains(
48+
captor.secondValue.headers["X-Sentry-Auth"].toString(),
49+
"sentry_key=007508298f9048729e434faa9ae4f49"
50+
)
51+
}
52+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package io.sentry.transport
2+
3+
import io.sentry.Hint
4+
import io.sentry.SentryEnvelope
5+
import io.sentry.SentryOptions
6+
import io.sentry.Session
7+
import io.sentry.dsnString
8+
import io.sentry.protocol.User
9+
import org.junit.Test
10+
import org.mockito.kotlin.mock
11+
import org.mockito.kotlin.verify
12+
import org.mockito.kotlin.whenever
13+
import kotlin.test.assertEquals
14+
import kotlin.test.assertFalse
15+
import kotlin.test.assertTrue
16+
17+
class MultiplexedTransportTest {
18+
private class Fixture {
19+
var transport1 = mock<ITransport>()
20+
var transport2 = mock<ITransport>()
21+
var rateLimiter1 = mock<RateLimiter>()
22+
var rateLimiter2 = mock<RateLimiter>()
23+
var sentryOptions: SentryOptions = SentryOptions().apply {
24+
dsn = dsnString
25+
setSerializer(mock())
26+
setEnvelopeDiskCache(mock())
27+
}
28+
29+
init {
30+
whenever(transport1.rateLimiter).thenReturn(rateLimiter1)
31+
whenever(transport2.rateLimiter).thenReturn(rateLimiter2)
32+
}
33+
34+
fun getSUT(): MultiplexedTransport {
35+
return MultiplexedTransport(listOf(transport1, transport2))
36+
}
37+
}
38+
private val fixture = Fixture()
39+
40+
private fun createSession(): Session {
41+
return Session("123", User(), "env", "release")
42+
}
43+
44+
@Test
45+
fun `send sends to all transports`() {
46+
val envelope = SentryEnvelope
47+
.from(fixture.sentryOptions.serializer, createSession(), null)
48+
val hint = Hint()
49+
50+
fixture.getSUT().send(envelope, hint)
51+
52+
verify(fixture.transport1).send(envelope, hint)
53+
verify(fixture.transport2).send(envelope, hint)
54+
}
55+
56+
@Test
57+
fun `healthy if all transports are healthy`() {
58+
whenever(fixture.transport1.isHealthy).thenReturn(true)
59+
whenever(fixture.transport2.isHealthy).thenReturn(true)
60+
61+
assertTrue(fixture.getSUT().isHealthy)
62+
}
63+
64+
@Test
65+
fun `not healthy if one transport is unhealthy`() {
66+
whenever(fixture.transport1.isHealthy).thenReturn(true)
67+
whenever(fixture.transport2.isHealthy).thenReturn(false)
68+
69+
assertFalse(fixture.getSUT().isHealthy)
70+
}
71+
72+
@Test
73+
fun `close closes all transports`() {
74+
fixture.getSUT().close()
75+
76+
verify(fixture.transport1).close()
77+
verify(fixture.transport2).close()
78+
}
79+
80+
@Test
81+
fun `close with isRestarting closes all transports with isRestarting`() {
82+
fixture.getSUT().close(true)
83+
84+
verify(fixture.transport1).close(true)
85+
verify(fixture.transport2).close(true)
86+
}
87+
88+
@Test
89+
fun `getRateLimiter returns active rate limiter if exists`() {
90+
whenever(fixture.rateLimiter1.isAnyRateLimitActive).thenReturn(false)
91+
whenever(fixture.rateLimiter2.isAnyRateLimitActive).thenReturn(true)
92+
93+
assertEquals(fixture.rateLimiter2, fixture.getSUT().rateLimiter)
94+
}
95+
}

0 commit comments

Comments
 (0)