Reduce and centralize message-sending metrics

This commit is contained in:
Jon Chambers 2025-04-07 11:08:53 -04:00 committed by GitHub
parent 6013d00654
commit ffa98e5b34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 152 additions and 282 deletions

View File

@ -10,7 +10,6 @@ import com.codahale.metrics.annotation.Timed;
import com.google.common.net.HttpHeaders; import com.google.common.net.HttpHeaders;
import io.dropwizard.auth.Auth; import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.Timer.Sample; import io.micrometer.core.instrument.Timer.Sample;
@ -151,31 +150,25 @@ public class MessageController {
private static final CompletableFuture<?>[] EMPTY_FUTURE_ARRAY = new CompletableFuture<?>[0]; private static final CompletableFuture<?>[] EMPTY_FUTURE_ARRAY = new CompletableFuture<?>[0];
private static final String SENT_MESSAGE_COUNTER_NAME = name(MessageController.class, "sentMessages");
private static final String OUTGOING_MESSAGE_LIST_SIZE_BYTES_DISTRIBUTION_NAME = name(MessageController.class, "outgoingMessageListSizeBytes"); private static final String OUTGOING_MESSAGE_LIST_SIZE_BYTES_DISTRIBUTION_NAME = name(MessageController.class, "outgoingMessageListSizeBytes");
private static final String RATE_LIMITED_MESSAGE_COUNTER_NAME = name(MessageController.class, "rateLimitedMessage");
private static final String SEND_MESSAGE_LATENCY_TIMER_NAME = MetricsUtil.name(MessageController.class, "sendMessageLatency"); private static final Timer INDIVIDUAL_MESSAGE_LATENCY_TIMER;
private static final Timer MULTI_RECIPIENT_MESSAGE_LATENCY_TIMER;
private static final String EPHEMERAL_TAG_NAME = "ephemeral"; static {
private static final String SENDER_TYPE_TAG_NAME = "senderType"; final String timerName = MetricsUtil.name(MessageController.class, "sendMessageLatency");
private static final String AUTH_TYPE_TAG_NAME = "authType"; final String multiRecipientTagName = "multiRecipient";
private static final String SENDER_COUNTRY_TAG_NAME = "senderCountry";
private static final String RATE_LIMIT_REASON_TAG_NAME = "rateLimitReason";
private static final String IDENTITY_TYPE_TAG_NAME = "identityType";
private static final String ENDPOINT_TYPE_TAG_NAME = "endpoint";
private static final String SENDER_TYPE_IDENTIFIED = "identified"; INDIVIDUAL_MESSAGE_LATENCY_TIMER = Timer.builder(timerName)
private static final String SENDER_TYPE_UNIDENTIFIED = "unidentified"; .tags(multiRecipientTagName, "false")
private static final String SENDER_TYPE_SELF = "self"; .publishPercentileHistogram(true)
.register(Metrics.globalRegistry);
private static final String AUTH_TYPE_IDENTIFIED = "identified"; MULTI_RECIPIENT_MESSAGE_LATENCY_TIMER = Timer.builder(timerName)
private static final String AUTH_TYPE_ACCESS_KEY = "accessKey"; .tags(multiRecipientTagName, "true")
private static final String AUTH_TYPE_GROUP_SEND_TOKEN = "groupSendToken"; .publishPercentileHistogram(true)
private static final String AUTH_TYPE_STORY = "story"; .register(Metrics.globalRegistry);
}
private static final String ENDPOINT_TYPE_SINGLE = "single";
private static final String ENDPOINT_TYPE_MULTI = "multi";
// The Signal desktop client (really, JavaScript in general) can handle message timestamps at most 100,000,000 days // The Signal desktop client (really, JavaScript in general) can handle message timestamps at most 100,000,000 days
// past the epoch; please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_epoch_timestamps_and_invalid_date // past the epoch; please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_epoch_timestamps_and_invalid_date
@ -282,13 +275,11 @@ public class MessageController {
} }
} }
final String senderType = source.map(
s -> s.getAccount().isIdentifiedBy(destinationIdentifier) ? SENDER_TYPE_SELF : SENDER_TYPE_IDENTIFIED)
.orElse(SENDER_TYPE_UNIDENTIFIED);
final Sample sample = Timer.start(); final Sample sample = Timer.start();
try { try {
final boolean isSyncMessage = senderType.equals(SENDER_TYPE_SELF); final boolean isSyncMessage =
source.map(s -> s.getAccount().isIdentifiedBy(destinationIdentifier)).orElse(false);
if (isSyncMessage && destinationIdentifier.identityType() == IdentityType.PNI) { if (isSyncMessage && destinationIdentifier.identityType() == IdentityType.PNI) {
throw new WebApplicationException(Status.FORBIDDEN); throw new WebApplicationException(Status.FORBIDDEN);
@ -359,7 +350,7 @@ public class MessageController {
final Account destination = maybeDestination.orElseThrow(); final Account destination = maybeDestination.orElseThrow();
if (source.isPresent() && !isSyncMessage) { if (source.isPresent() && !isSyncMessage) {
checkMessageRateLimit(source.get(), destination, userAgent); rateLimiters.getMessagesLimiter().validate(source.get().getAccount().getUuid(), destination.getUuid());
} }
if (isStory) { if (isStory) {
@ -387,27 +378,8 @@ public class MessageController {
final Map<Byte, Integer> registrationIdsByDeviceId = messages.messages().stream() final Map<Byte, Integer> registrationIdsByDeviceId = messages.messages().stream()
.collect(Collectors.toMap(IncomingMessage::destinationDeviceId, IncomingMessage::destinationRegistrationId)); .collect(Collectors.toMap(IncomingMessage::destinationDeviceId, IncomingMessage::destinationRegistrationId));
final String authType;
if (SENDER_TYPE_IDENTIFIED.equals(senderType)) {
authType = AUTH_TYPE_IDENTIFIED;
} else if (isStory) {
authType = AUTH_TYPE_STORY;
} else if (groupSendToken != null) {
authType = AUTH_TYPE_GROUP_SEND_TOKEN;
} else {
authType = AUTH_TYPE_ACCESS_KEY;
}
messageSender.sendMessages(destination, destinationIdentifier, messagesByDeviceId, registrationIdsByDeviceId, userAgent); messageSender.sendMessages(destination, destinationIdentifier, messagesByDeviceId, registrationIdsByDeviceId, userAgent);
Metrics.counter(SENT_MESSAGE_COUNTER_NAME, List.of(UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(ENDPOINT_TYPE_TAG_NAME, ENDPOINT_TYPE_SINGLE),
Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(messages.online())),
Tag.of(SENDER_TYPE_TAG_NAME, senderType),
Tag.of(AUTH_TYPE_TAG_NAME, authType),
Tag.of(IDENTITY_TYPE_TAG_NAME, destinationIdentifier.identityType().name())))
.increment(messagesByDeviceId.size());
return Response.ok(new SendMessageResponse(needsSync)).build(); return Response.ok(new SendMessageResponse(needsSync)).build();
} catch (final MismatchedDevicesException e) { } catch (final MismatchedDevicesException e) {
if (!e.getMismatchedDevices().staleDeviceIds().isEmpty()) { if (!e.getMismatchedDevices().staleDeviceIds().isEmpty()) {
@ -426,10 +398,7 @@ public class MessageController {
throw new WebApplicationException(Status.REQUEST_ENTITY_TOO_LARGE); throw new WebApplicationException(Status.REQUEST_ENTITY_TOO_LARGE);
} }
} finally { } finally {
sample.stop(Timer.builder(SEND_MESSAGE_LATENCY_TIMER_NAME) sample.stop(INDIVIDUAL_MESSAGE_LATENCY_TIMER);
.tags(SENDER_TYPE_TAG_NAME, senderType)
.publishPercentileHistogram(true)
.register(Metrics.globalRegistry));
} }
} }
@ -514,171 +483,144 @@ public class MessageController {
} }
} }
final SpamCheckResult<Response> spamCheckResult = spamChecker.checkForMultiRecipientSpamHttp( final Timer.Sample sample = Timer.start();
isStory ? MessageType.MULTI_RECIPIENT_STORY : MessageType.MULTI_RECIPIENT_SEALED_SENDER,
context);
if (spamCheckResult.response().isPresent()) { try {
return spamCheckResult.response().get(); final SpamCheckResult<Response> spamCheckResult = spamChecker.checkForMultiRecipientSpamHttp(
} isStory ? MessageType.MULTI_RECIPIENT_STORY : MessageType.MULTI_RECIPIENT_SEALED_SENDER,
context);
if (groupSendToken == null && accessKeys == null && !isStory) { if (spamCheckResult.response().isPresent()) {
throw new NotAuthorizedException("A group send endorsement token or unidentified access key is required for non-story messages"); return spamCheckResult.response().get();
}
if (groupSendToken != null) {
if (accessKeys != null) {
throw new BadRequestException("Only one of group send endorsement token and unidentified access key may be provided");
} else if (isStory) {
throw new BadRequestException("Stories should not provide a group send endorsement token");
} }
}
if (groupSendToken != null) { if (groupSendToken == null && accessKeys == null && !isStory) {
// Group send endorsements are checked before we even attempt to resolve any accounts, since throw new NotAuthorizedException("A group send endorsement token or unidentified access key is required for non-story messages");
// the lists of service IDs in the envelope are all that we need to check against }
checkGroupSendToken(multiRecipientMessage.getRecipients().keySet(), groupSendToken); if (groupSendToken != null) {
} if (accessKeys != null) {
throw new BadRequestException("Only one of group send endorsement token and unidentified access key may be provided");
} else if (isStory) {
throw new BadRequestException("Stories should not provide a group send endorsement token");
}
}
// At this point, the caller has at least superficially provided the information needed to send a multi-recipient if (groupSendToken != null) {
// message. Attempt to resolve the destination service identifiers to Signal accounts. // Group send endorsements are checked before we even attempt to resolve any accounts, since
final Map<SealedSenderMultiRecipientMessage.Recipient, Account> resolvedRecipients = // the lists of service IDs in the envelope are all that we need to check against
Flux.fromIterable(multiRecipientMessage.getRecipients().entrySet()) checkGroupSendToken(multiRecipientMessage.getRecipients().keySet(), groupSendToken);
.flatMap(serviceIdAndRecipient -> { }
final ServiceIdentifier serviceIdentifier =
ServiceIdentifier.fromLibsignal(serviceIdAndRecipient.getKey());
return Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(serviceIdentifier)) // At this point, the caller has at least superficially provided the information needed to send a multi-recipient
.flatMap(Mono::justOrEmpty) // message. Attempt to resolve the destination service identifiers to Signal accounts.
.switchIfEmpty(isStory || groupSendToken != null ? Mono.empty() : Mono.error(NotFoundException::new)) final Map<SealedSenderMultiRecipientMessage.Recipient, Account> resolvedRecipients =
.map(account -> Tuples.of(serviceIdAndRecipient.getValue(), account)); Flux.fromIterable(multiRecipientMessage.getRecipients().entrySet())
}, MAX_FETCH_ACCOUNT_CONCURRENCY) .flatMap(serviceIdAndRecipient -> {
.collectMap(Tuple2::getT1, Tuple2::getT2) final ServiceIdentifier serviceIdentifier =
.blockOptional() ServiceIdentifier.fromLibsignal(serviceIdAndRecipient.getKey());
.orElse(Collections.emptyMap());
// Access keys are checked against the UAK in the resolved accounts, so we have to check after resolving accounts above. return Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(serviceIdentifier))
// Group send endorsements are checked earlier; for stories, we don't check permissions at all because only clients check them .flatMap(Mono::justOrEmpty)
if (groupSendToken == null && !isStory) { .switchIfEmpty(isStory || groupSendToken != null ? Mono.empty() : Mono.error(NotFoundException::new))
checkAccessKeys(accessKeys, multiRecipientMessage, resolvedRecipients); .map(account -> Tuples.of(serviceIdAndRecipient.getValue(), account));
} }, MAX_FETCH_ACCOUNT_CONCURRENCY)
.collectMap(Tuple2::getT1, Tuple2::getT2)
.blockOptional()
.orElse(Collections.emptyMap());
// We might filter out all the recipients of a story (if none exist). // Access keys are checked against the UAK in the resolved accounts, so we have to check after resolving accounts above.
// In this case there is no error so we should just return 200 now. // Group send endorsements are checked earlier; for stories, we don't check permissions at all because only clients check them
if (isStory) { if (groupSendToken == null && !isStory) {
if (resolvedRecipients.isEmpty()) { checkAccessKeys(accessKeys, multiRecipientMessage, resolvedRecipients);
return Response.ok(new SendMultiRecipientMessageResponse(List.of())).build(); }
// We might filter out all the recipients of a story (if none exist).
// In this case there is no error so we should just return 200 now.
if (isStory) {
if (resolvedRecipients.isEmpty()) {
return Response.ok(new SendMultiRecipientMessageResponse(List.of())).build();
}
try {
CompletableFuture.allOf(resolvedRecipients.values()
.stream()
.map(account -> account.getIdentifier(IdentityType.ACI))
.map(accountIdentifier ->
rateLimiters.getStoriesLimiter().validateAsync(accountIdentifier).toCompletableFuture())
.toList()
.toArray(EMPTY_FUTURE_ARRAY))
.join();
} catch (final Exception e) {
if (ExceptionUtils.unwrap(e) instanceof RateLimitExceededException rateLimitExceededException) {
throw rateLimitExceededException;
} else {
throw ExceptionUtils.wrap(e);
}
}
} }
try { try {
CompletableFuture.allOf(resolvedRecipients.values() if (!resolvedRecipients.isEmpty()) {
.stream() messageSender.sendMultiRecipientMessage(multiRecipientMessage, resolvedRecipients, timestamp, isStory, online, isUrgent, userAgent).get();
.map(account -> account.getIdentifier(IdentityType.ACI)) }
.map(accountIdentifier ->
rateLimiters.getStoriesLimiter().validateAsync(accountIdentifier).toCompletableFuture()) final List<ServiceIdentifier> unresolvedRecipientServiceIds;
.toList() if (groupSendToken != null) {
.toArray(EMPTY_FUTURE_ARRAY)) unresolvedRecipientServiceIds = multiRecipientMessage.getRecipients().entrySet().stream()
.join(); .filter(entry -> !resolvedRecipients.containsKey(entry.getValue()))
} catch (final Exception e) { .map(entry -> ServiceIdentifier.fromLibsignal(entry.getKey()))
if (ExceptionUtils.unwrap(e) instanceof RateLimitExceededException rateLimitExceededException) { .toList();
throw rateLimitExceededException;
} else { } else {
throw ExceptionUtils.wrap(e); unresolvedRecipientServiceIds = List.of();
}
}
}
final String authType;
if (isStory) {
authType = AUTH_TYPE_STORY;
} else if (groupSendToken != null) {
authType = AUTH_TYPE_GROUP_SEND_TOKEN;
} else {
authType = AUTH_TYPE_ACCESS_KEY;
}
try {
if (!resolvedRecipients.isEmpty()) {
messageSender.sendMultiRecipientMessage(multiRecipientMessage, resolvedRecipients, timestamp, isStory, online, isUrgent, userAgent).get();
}
final List<ServiceIdentifier> unresolvedRecipientServiceIds;
if (AUTH_TYPE_GROUP_SEND_TOKEN.equals(authType)) {
unresolvedRecipientServiceIds = multiRecipientMessage.getRecipients().entrySet().stream()
.filter(entry -> !resolvedRecipients.containsKey(entry.getValue()))
.map(entry -> ServiceIdentifier.fromLibsignal(entry.getKey()))
.toList();
} else {
unresolvedRecipientServiceIds = List.of();
}
multiRecipientMessage.getRecipients().forEach((serviceId, recipient) -> {
if (!resolvedRecipients.containsKey(recipient)) {
// We skipped sending to this recipient because we couldn't resolve the recipient to an
// existing account; don't increment the counter for this recipient. If the client was
// using a GSE, track the missing recipients to include in the response.
return;
} }
final String identityType = switch (serviceId) { return Response.ok(new SendMultiRecipientMessageResponse(unresolvedRecipientServiceIds)).build();
case ServiceId.Aci ignored -> "ACI"; } catch (InterruptedException e) {
case ServiceId.Pni ignored -> "PNI"; logger.error("interrupted while delivering multi-recipient messages", e);
default -> "unknown"; throw new InternalServerErrorException("interrupted during delivery");
}; } catch (CancellationException e) {
logger.error("cancelled while delivering multi-recipient messages", e);
throw new InternalServerErrorException("delivery cancelled");
} catch (ExecutionException e) {
logger.error("partial failure while delivering multi-recipient messages", e.getCause());
throw new InternalServerErrorException("failure during delivery");
} catch (MultiRecipientMismatchedDevicesException e) {
final List<AccountMismatchedDevices> accountMismatchedDevices =
e.getMismatchedDevicesByServiceIdentifier().entrySet().stream()
.filter(entry -> !entry.getValue().missingDeviceIds().isEmpty() || !entry.getValue().extraDeviceIds().isEmpty())
.map(entry -> new AccountMismatchedDevices(entry.getKey(),
new MismatchedDevicesResponse(entry.getValue().missingDeviceIds(), entry.getValue().extraDeviceIds())))
.toList();
Metrics.counter(SENT_MESSAGE_COUNTER_NAME, Tags.of( if (!accountMismatchedDevices.isEmpty()) {
UserAgentTagUtil.getPlatformTag(userAgent), return Response
Tag.of(ENDPOINT_TYPE_TAG_NAME, ENDPOINT_TYPE_MULTI), .status(409)
Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(online)), .type(MediaType.APPLICATION_JSON_TYPE)
Tag.of(SENDER_TYPE_TAG_NAME, SENDER_TYPE_UNIDENTIFIED), .entity(accountMismatchedDevices)
Tag.of(AUTH_TYPE_TAG_NAME, authType), .build();
Tag.of(IDENTITY_TYPE_TAG_NAME, identityType))) }
.increment(recipient.getDevices().length);
});
return Response.ok(new SendMultiRecipientMessageResponse(unresolvedRecipientServiceIds)).build(); final List<AccountStaleDevices> accountStaleDevices =
} catch (InterruptedException e) { e.getMismatchedDevicesByServiceIdentifier().entrySet().stream()
logger.error("interrupted while delivering multi-recipient messages", e); .filter(entry -> !entry.getValue().staleDeviceIds().isEmpty())
throw new InternalServerErrorException("interrupted during delivery"); .map(entry -> new AccountStaleDevices(entry.getKey(),
} catch (CancellationException e) { new StaleDevicesResponse(entry.getValue().staleDeviceIds())))
logger.error("cancelled while delivering multi-recipient messages", e); .toList();
throw new InternalServerErrorException("delivery cancelled");
} catch (ExecutionException e) {
logger.error("partial failure while delivering multi-recipient messages", e.getCause());
throw new InternalServerErrorException("failure during delivery");
} catch (MultiRecipientMismatchedDevicesException e) {
final List<AccountMismatchedDevices> accountMismatchedDevices =
e.getMismatchedDevicesByServiceIdentifier().entrySet().stream()
.filter(entry -> !entry.getValue().missingDeviceIds().isEmpty() || !entry.getValue().extraDeviceIds().isEmpty())
.map(entry -> new AccountMismatchedDevices(entry.getKey(),
new MismatchedDevicesResponse(entry.getValue().missingDeviceIds(), entry.getValue().extraDeviceIds())))
.toList();
if (!accountMismatchedDevices.isEmpty()) { if (!accountStaleDevices.isEmpty()) {
return Response return Response
.status(409) .status(410)
.type(MediaType.APPLICATION_JSON_TYPE) .type(MediaType.APPLICATION_JSON)
.entity(accountMismatchedDevices) .entity(accountStaleDevices)
.build(); .build();
}
throw new RuntimeException(e);
} catch (final MessageTooLargeException e) {
throw new WebApplicationException(Status.REQUEST_ENTITY_TOO_LARGE);
} }
} finally {
final List<AccountStaleDevices> accountStaleDevices = sample.stop(MULTI_RECIPIENT_MESSAGE_LATENCY_TIMER);
e.getMismatchedDevicesByServiceIdentifier().entrySet().stream()
.filter(entry -> !entry.getValue().staleDeviceIds().isEmpty())
.map(entry -> new AccountStaleDevices(entry.getKey(),
new StaleDevicesResponse(entry.getValue().staleDeviceIds())))
.toList();
if (!accountStaleDevices.isEmpty()) {
return Response
.status(410)
.type(MediaType.APPLICATION_JSON)
.entity(accountStaleDevices)
.build();
}
throw new RuntimeException(e);
} catch (final MessageTooLargeException e) {
throw new WebApplicationException(Status.REQUEST_ENTITY_TOO_LARGE);
} }
} }
@ -869,21 +811,4 @@ public class MessageController {
return Response.status(Status.ACCEPTED) return Response.status(Status.ACCEPTED)
.build(); .build();
} }
private void checkMessageRateLimit(AuthenticatedDevice source, Account destination, String userAgent)
throws RateLimitExceededException {
final String senderCountryCode = Util.getCountryCode(source.getAccount().getNumber());
try {
rateLimiters.getMessagesLimiter().validate(source.getAccount().getUuid(), destination.getUuid());
} catch (final RateLimitExceededException e) {
Metrics.counter(RATE_LIMITED_MESSAGE_COUNTER_NAME,
Tags.of(
UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(SENDER_COUNTRY_TAG_NAME, senderCountryCode),
Tag.of(RATE_LIMIT_REASON_TAG_NAME, "singleDestinationRate"))).increment();
throw e;
}
}
} }

View File

@ -55,20 +55,18 @@ public class MessageSender {
// Note that these names deliberately reference `MessageController` for metric continuity // Note that these names deliberately reference `MessageController` for metric continuity
private static final String REJECT_OVERSIZE_MESSAGE_COUNTER_NAME = name(MessageController.class, "rejectOversizeMessage"); private static final String REJECT_OVERSIZE_MESSAGE_COUNTER_NAME = name(MessageController.class, "rejectOversizeMessage");
private static final String LARGE_BUT_NOT_OVERSIZE_MESSAGE_COUNTER_NAME = name(MessageController.class, "largeMessage");
private static final String CONTENT_SIZE_DISTRIBUTION_NAME = MetricsUtil.name(MessageController.class, "messageContentSize"); private static final String CONTENT_SIZE_DISTRIBUTION_NAME = MetricsUtil.name(MessageController.class, "messageContentSize");
private static final String SEND_COUNTER_NAME = name(MessageSender.class, "sendMessage"); private static final String SEND_COUNTER_NAME = name(MessageSender.class, "sendMessage");
private static final String CHANNEL_TAG_NAME = "channel";
private static final String EPHEMERAL_TAG_NAME = "ephemeral"; private static final String EPHEMERAL_TAG_NAME = "ephemeral";
private static final String CLIENT_ONLINE_TAG_NAME = "clientOnline"; private static final String CLIENT_ONLINE_TAG_NAME = "clientOnline";
private static final String URGENT_TAG_NAME = "urgent"; private static final String URGENT_TAG_NAME = "urgent";
private static final String STORY_TAG_NAME = "story"; private static final String STORY_TAG_NAME = "story";
private static final String SEALED_SENDER_TAG_NAME = "sealedSender"; private static final String SEALED_SENDER_TAG_NAME = "sealedSender";
private static final String MULTI_RECIPIENT_TAG_NAME = "multiRecipient";
@VisibleForTesting @VisibleForTesting
public static final int MAX_MESSAGE_SIZE = (int) DataSize.kibibytes(256).toBytes(); public static final int MAX_MESSAGE_SIZE = (int) DataSize.kibibytes(256).toBytes();
private static final long LARGE_MESSAGE_SIZE = DataSize.kibibytes(8).toBytes();
@VisibleForTesting @VisibleForTesting
static final byte NO_EXCLUDED_DEVICE_ID = -1; static final byte NO_EXCLUDED_DEVICE_ID = -1;
@ -137,14 +135,16 @@ public class MessageSender {
} }
} }
Metrics.counter(SEND_COUNTER_NAME, final Tags tags = Tags.of(
CHANNEL_TAG_NAME, destination.getDevice(deviceId).map(MessageSender::getDeliveryChannelName).orElse("unknown"),
EPHEMERAL_TAG_NAME, String.valueOf(message.getEphemeral()), EPHEMERAL_TAG_NAME, String.valueOf(message.getEphemeral()),
CLIENT_ONLINE_TAG_NAME, String.valueOf(destinationPresent), CLIENT_ONLINE_TAG_NAME, String.valueOf(destinationPresent),
URGENT_TAG_NAME, String.valueOf(message.getUrgent()), URGENT_TAG_NAME, String.valueOf(message.getUrgent()),
STORY_TAG_NAME, String.valueOf(message.getStory()), STORY_TAG_NAME, String.valueOf(message.getStory()),
SEALED_SENDER_TAG_NAME, String.valueOf(!message.hasSourceServiceId())) SEALED_SENDER_TAG_NAME, String.valueOf(!message.hasSourceServiceId()),
.increment(); MULTI_RECIPIENT_TAG_NAME, "false")
.and(UserAgentTagUtil.getPlatformTag(userAgent));
Metrics.counter(SEND_COUNTER_NAME, tags).increment();
}); });
} }
@ -219,32 +219,20 @@ public class MessageSender {
} }
} }
Metrics.counter(SEND_COUNTER_NAME, final Tags tags = Tags.of(
CHANNEL_TAG_NAME,
account.getDevice(deviceId).map(MessageSender::getDeliveryChannelName).orElse("unknown"),
EPHEMERAL_TAG_NAME, String.valueOf(isEphemeral), EPHEMERAL_TAG_NAME, String.valueOf(isEphemeral),
CLIENT_ONLINE_TAG_NAME, String.valueOf(clientPresent), CLIENT_ONLINE_TAG_NAME, String.valueOf(clientPresent),
URGENT_TAG_NAME, String.valueOf(isUrgent), URGENT_TAG_NAME, String.valueOf(isUrgent),
STORY_TAG_NAME, String.valueOf(isStory), STORY_TAG_NAME, String.valueOf(isStory),
SEALED_SENDER_TAG_NAME, String.valueOf(true)) SEALED_SENDER_TAG_NAME, "true",
.increment(); MULTI_RECIPIENT_TAG_NAME, "true")
.and(UserAgentTagUtil.getPlatformTag(userAgent));
Metrics.counter(SEND_COUNTER_NAME, tags).increment();
}))) })))
.thenRun(Util.NOOP); .thenRun(Util.NOOP);
} }
@VisibleForTesting
static String getDeliveryChannelName(final Device device) {
if (device.getGcmId() != null) {
return "gcm";
} else if (device.getApnId() != null) {
return "apn";
} else if (device.getFetchesMessages()) {
return "websocket";
} else {
return "none";
}
}
@VisibleForTesting @VisibleForTesting
static void validateContentLength(final int contentLength, static void validateContentLength(final int contentLength,
final boolean isMultiRecipientMessage, final boolean isMultiRecipientMessage,
@ -273,13 +261,6 @@ public class MessageSender {
throw new MessageTooLargeException(); throw new MessageTooLargeException();
} }
if (contentLength > LARGE_MESSAGE_SIZE) {
Metrics.counter(
LARGE_BUT_NOT_OVERSIZE_MESSAGE_COUNTER_NAME,
Tags.of(UserAgentTagUtil.getPlatformTag(userAgent), Tag.of("multiRecipientMessage", String.valueOf(isMultiRecipientMessage))))
.increment();
}
} }
@VisibleForTesting @VisibleForTesting

View File

@ -18,7 +18,6 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -254,41 +253,6 @@ class MessageSenderTest {
mismatchedDevicesException.getMismatchedDevicesByServiceIdentifier()); mismatchedDevicesException.getMismatchedDevicesByServiceIdentifier());
} }
@ParameterizedTest
@MethodSource
void getDeliveryChannelName(final Device device, final String expectedChannelName) {
assertEquals(expectedChannelName, MessageSender.getDeliveryChannelName(device));
}
private static List<Arguments> getDeliveryChannelName() {
final List<Arguments> arguments = new ArrayList<>();
{
final Device apnDevice = mock(Device.class);
when(apnDevice.getApnId()).thenReturn("apns-token");
arguments.add(Arguments.of(apnDevice, "apn"));
}
{
final Device fcmDevice = mock(Device.class);
when(fcmDevice.getGcmId()).thenReturn("fcm-token");
arguments.add(Arguments.of(fcmDevice, "gcm"));
}
{
final Device fetchesMessagesDevice = mock(Device.class);
when(fetchesMessagesDevice.getFetchesMessages()).thenReturn(true);
arguments.add(Arguments.of(fetchesMessagesDevice, "websocket"));
}
arguments.add(Arguments.of(mock(Device.class), "none"));
return arguments;
}
@Test @Test
void validateContentLength() { void validateContentLength() {
assertThrows(MessageTooLargeException.class, () -> assertThrows(MessageTooLargeException.class, () ->