diff --git a/micrometer-jakarta9/build.gradle b/micrometer-jakarta9/build.gradle index d375e5eb28..6f97ed2522 100644 --- a/micrometer-jakarta9/build.gradle +++ b/micrometer-jakarta9/build.gradle @@ -6,6 +6,7 @@ jar { bnd '''\ Import-Package: \ jakarta.jms.*;resolution:=dynamic;version="${@}",\ + jakarta.mail.*;resolution:=dynamic;version="${@}",\ io.micrometer.observation.*;resolution:=dynamic;version="${@}",\ * '''.stripIndent() @@ -18,6 +19,8 @@ dependencies { api project(":micrometer-observation") optionalApi 'jakarta.jms:jakarta.jms-api' + // Jakarta 9 version of the Mail API + optionalApi 'jakarta.mail:jakarta.mail-api:2.0.1' testImplementation(libs.archunitJunit5) { // avoid transitively pulling in slf4j 2 @@ -25,5 +28,7 @@ dependencies { } testImplementation libs.slf4jApi testImplementation libs.mockitoCore5 + testImplementation project(':micrometer-observation-test') testImplementation 'org.assertj:assertj-core' + testRuntimeOnly('com.sun.mail:jakarta.mail:2.0.1') } diff --git a/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/DefaultMailSendObservationConvention.java b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/DefaultMailSendObservationConvention.java new file mode 100644 index 0000000000..326e731a68 --- /dev/null +++ b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/DefaultMailSendObservationConvention.java @@ -0,0 +1,63 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.micrometer.jakarta9.instrument.mail; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import jakarta.mail.Message; +import jakarta.mail.Message.RecipientType; + +import java.util.ArrayList; +import java.util.List; + +/** + * Default implementation for {@link MailSendObservationConvention}. + * + * @since 1.15.0 + * @author famaridon + */ +public class DefaultMailSendObservationConvention implements MailSendObservationConvention { + + @Override + public String getName() { + return "mail.send"; + } + + @Override + public String getContextualName(MailSendObservationContext context) { + return "mail send"; + } + + @Override + public KeyValues getLowCardinalityKeyValues(MailSendObservationContext context) { + return KeyValues.of(MailKeyValues.serverAddress(context), MailKeyValues.serverPort(context), + MailKeyValues.networkProtocolName(context)); + } + + @Override + public KeyValues getHighCardinalityKeyValues(MailSendObservationContext context) { + Message message = context.getCarrier(); + List values = new ArrayList<>(); + MailKeyValues.smtpMessageSubject(message).ifPresent(values::add); + MailKeyValues.smtpMessageFrom(message).ifPresent(values::add); + MailKeyValues.smtpMessageRecipients(message, RecipientType.TO).ifPresent(values::add); + MailKeyValues.smtpMessageRecipients(message, RecipientType.CC).ifPresent(values::add); + MailKeyValues.smtpMessageRecipients(message, RecipientType.BCC).ifPresent(values::add); + + return KeyValues.of(values); + } + +} diff --git a/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/InstrumentedTransport.java b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/InstrumentedTransport.java new file mode 100644 index 0000000000..5a26c92865 --- /dev/null +++ b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/InstrumentedTransport.java @@ -0,0 +1,114 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.micrometer.jakarta9.instrument.mail; + +import io.micrometer.common.lang.Nullable; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import jakarta.mail.*; + +/** + * Wraps a {@link Transport} so that it is instrumented with a Micrometer + * {@link Observation}. + * + * @since 1.15.0 + * @author famaridon + */ +public class InstrumentedTransport extends Transport { + + private static final DefaultMailSendObservationConvention DEFAULT_CONVENTION = new DefaultMailSendObservationConvention(); + + private final ObservationRegistry observationRegistry; + + private final Transport delegate; + + @Nullable + private final String protocol; + + @Nullable + private String host; + + @Nullable + private final ObservationConvention customConvention; + + private int port; + + /** + * Create an instrumented transport using the + * {@link DefaultMailSendObservationConvention default} {@link ObservationConvention}. + * @param session session for the delegate transport + * @param delegate transport to instrument + * @param observationRegistry registry for the observations + */ + public InstrumentedTransport(Session session, Transport delegate, ObservationRegistry observationRegistry) { + this(session, delegate, observationRegistry, null); + } + + /** + * Create an instrumented transport with a custom {@link MailSendObservationConvention + * convention}. + * @param session session for the delegate transport + * @param delegate transport to instrument + * @param observationRegistry registry for the observations + * @param customConvention override the convention to apply to the instrumentation + */ + public InstrumentedTransport(Session session, Transport delegate, ObservationRegistry observationRegistry, + @Nullable ObservationConvention customConvention) { + super(session, delegate.getURLName()); + this.protocol = this.url.getProtocol(); + this.delegate = delegate; + this.observationRegistry = observationRegistry; + this.customConvention = customConvention; + } + + @Override + public void connect(String host, int port, String user, String password) throws MessagingException { + this.delegate.connect(host, port, user, password); + this.host = host; + this.port = port; + } + + @Override + public void sendMessage(Message msg, Address[] addresses) throws MessagingException { + + Observation observation = MailObservationDocumentation.MAIL_SEND.observation(this.customConvention, + DEFAULT_CONVENTION, () -> new MailSendObservationContext(msg, this.protocol, this.host, this.port), + observationRegistry); + + observation.start(); + try (Observation.Scope ignore = observation.openScope()) { + // the Message-Id is set by the Transport (from the SMTP server) after sending + this.delegate.sendMessage(msg, addresses); + MailKeyValues.smtpMessageId(msg).ifPresent(observation::highCardinalityKeyValue); + } + catch (MessagingException error) { + observation.error(error); + throw error; + } + finally { + observation.stop(); + } + } + + @Override + public synchronized void close() throws MessagingException { + this.delegate.close(); + this.host = null; + this.port = 0; + } + +} diff --git a/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/MailKeyValues.java b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/MailKeyValues.java new file mode 100644 index 0000000000..ac51818fe7 --- /dev/null +++ b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/MailKeyValues.java @@ -0,0 +1,130 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.micrometer.jakarta9.instrument.mail; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.common.lang.Nullable; +import io.micrometer.jakarta9.instrument.mail.MailObservationDocumentation.LowCardinalityKeyNames; + +import java.util.Arrays; +import java.util.Locale; +import java.util.Optional; +import java.util.stream.Collectors; + +import io.micrometer.common.KeyValue; +import jakarta.mail.Address; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Message.RecipientType; + +/** + * Documented {@link io.micrometer.common.KeyValue KeyValues} for the observations on + * {@link jakarta.mail.Transport send} of mail messages. + * + * @since 1.15.0 + * @author famaridon + */ +class MailKeyValues { + + /** + * The value is when value can't be determined. + */ + public static final String UNKNOWN = "unknown"; + + private MailKeyValues() { + } + + static Optional smtpMessageFrom(Message message) { + return safeExtractValue(MailObservationDocumentation.HighCardinalityKeyNames.SMTP_MESSAGE_FROM, + () -> addressesToValue(message.getFrom())); + } + + static Optional smtpMessageRecipients(Message message, RecipientType recipientType) { + MailObservationDocumentation.HighCardinalityKeyNames key = MailObservationDocumentation.HighCardinalityKeyNames + .valueOf("SMTP_MESSAGE_" + recipientType.toString().toUpperCase(Locale.ROOT)); + return safeExtractValue(key, () -> addressesToValue(message.getRecipients(recipientType))); + } + + static Optional smtpMessageSubject(Message message) { + + return safeExtractValue(MailObservationDocumentation.HighCardinalityKeyNames.SMTP_MESSAGE_SUBJECT, + () -> Optional.ofNullable(message.getSubject())); + } + + static Optional smtpMessageId(Message message) { + return safeExtractValue(MailObservationDocumentation.HighCardinalityKeyNames.SMTP_MESSAGE_ID, () -> { + String[] header = message.getHeader("Message-ID"); + if (header == null || header.length == 0) { + return Optional.empty(); + } + return Optional.of(String.join(", ", header)); + }); + } + + static KeyValue serverAddress(MailSendObservationContext context) { + String host = context.getHost(); + if (host == null || host.isEmpty()) { + host = UNKNOWN; + } + return LowCardinalityKeyNames.SERVER_ADDRESS.withValue(host); + } + + static KeyValue serverPort(MailSendObservationContext context) { + String port = UNKNOWN; + if (context.getPort() > 0) { + port = String.valueOf(context.getPort()); + } + return LowCardinalityKeyNames.SERVER_PORT.withValue(port); + } + + static KeyValue networkProtocolName(MailSendObservationContext context) { + String protocol = context.getProtocol(); + if (protocol == null || protocol.isEmpty()) { + protocol = UNKNOWN; + } + return KeyValue.of(LowCardinalityKeyNames.NETWORK_PROTOCOL_NAME, protocol); + } + + private static Optional safeExtractValue(KeyName key, ValueExtractor extractor) { + String value; + try { + Optional extracted = extractor.extract(); + if (!extracted.isPresent()) { + return Optional.empty(); + } + value = extracted.get(); + } + catch (MessagingException exc) { + value = UNKNOWN; + } + return Optional.of(key.withValue(value)); + } + + private static Optional addressesToValue(@Nullable Address[] addresses) { + if (addresses == null || addresses.length == 0) { + return Optional.empty(); + } + String value = Arrays.stream(addresses).map(Address::toString).collect(Collectors.joining(", ")); + return Optional.of(value); + } + + private interface ValueExtractor { + + Optional extract() throws MessagingException; + + } + +} diff --git a/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/MailObservationDocumentation.java b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/MailObservationDocumentation.java new file mode 100644 index 0000000000..c66ce2bb84 --- /dev/null +++ b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/MailObservationDocumentation.java @@ -0,0 +1,144 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.micrometer.jakarta9.instrument.mail; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +/** + * Documented {@link io.micrometer.common.KeyValue KeyValues} for the observations on + * {@link jakarta.mail.Transport send} of mail messages. + * + * @since 1.15.0 + * @author famaridon + */ +public enum MailObservationDocumentation implements ObservationDocumentation { + + /** + * Observation for mail send operations. Measures the time spent sending a mail + * message. + */ + MAIL_SEND { + @Override + public Class> getDefaultConvention() { + return DefaultMailSendObservationConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return LowCardinalityKeyNames.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + }; + + public enum LowCardinalityKeyNames implements KeyName { + + /** + * (SMTP) Server address used for sending mails. + */ + SERVER_ADDRESS { + @Override + public String asString() { + return "server.address"; + } + }, + /** + * (SMTP) Server port used for sending mails. + */ + SERVER_PORT { + @Override + public String asString() { + return "server.port"; + } + }, + /** + * Network protocol used for sending mails. + */ + NETWORK_PROTOCOL_NAME { + @Override + public String asString() { + return "network.protocol.name"; + } + } + + } + + public enum HighCardinalityKeyNames implements KeyName { + + /** + * Sender of the mail. + */ + SMTP_MESSAGE_FROM { + @Override + public String asString() { + return "smtp.message.from"; + } + }, + /** + * Primary (TO) recipient(s) of the mail. + */ + SMTP_MESSAGE_TO { + @Override + public String asString() { + return "smtp.message.to"; + } + }, + /** + * Carbon copy (CC) recipient(s) of the mail. + */ + SMTP_MESSAGE_CC { + @Override + public String asString() { + return "smtp.message.cc"; + } + }, + /** + * Blind carbon copy (BCC) recipient(s) of the mail. + */ + SMTP_MESSAGE_BCC { + @Override + public String asString() { + return "smtp.message.bcc"; + } + }, + /** + * Subject line of the mail. + */ + SMTP_MESSAGE_SUBJECT { + @Override + public String asString() { + return "smtp.message.subject"; + } + }, + /** + * Message ID received from the SMTP server. + */ + SMTP_MESSAGE_ID { + @Override + public String asString() { + return "smtp.message.id"; + } + } + + } + +} diff --git a/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/MailSendObservationContext.java b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/MailSendObservationContext.java new file mode 100644 index 0000000000..441d1b09bb --- /dev/null +++ b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/MailSendObservationContext.java @@ -0,0 +1,77 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.micrometer.jakarta9.instrument.mail; + +import io.micrometer.common.lang.Nullable; +import io.micrometer.common.util.internal.logging.WarnThenDebugLogger; +import io.micrometer.observation.transport.SenderContext; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; + +/** + * Context that holds information for observation metadata collection during the + * {@link MailObservationDocumentation#MAIL_SEND sending of mail messages}. + *

+ * This propagates metadata with the message sent by + * {@link Message#setHeader(String, String) setting a message header}. + * + * @since 1.15.0 + * @author famaridon + */ +public class MailSendObservationContext extends SenderContext { + + private static final WarnThenDebugLogger logger = new WarnThenDebugLogger(MailSendObservationContext.class); + + @Nullable + private final String protocol; + + @Nullable + private final String host; + + private final int port; + + public MailSendObservationContext(Message msg, @Nullable String protocol, @Nullable String host, int port) { + super((message, key, value) -> { + if (message != null) { + try { + message.setHeader(key, value); + } + catch (MessagingException exc) { + logger.log("Failed to set message header.", exc); + } + } + }); + setCarrier(msg); + this.protocol = protocol; + this.host = host; + this.port = port; + } + + @Nullable + public String getProtocol() { + return protocol; + } + + @Nullable + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + +} diff --git a/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/MailSendObservationConvention.java b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/MailSendObservationConvention.java new file mode 100644 index 0000000000..0a4beff9ba --- /dev/null +++ b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/MailSendObservationConvention.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.micrometer.jakarta9.instrument.mail; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +/** + * {@link ObservationConvention} interface for + * {@link MailObservationDocumentation#MAIL_SEND mail send} operations. + * + * @since 1.15.0 + * @author famaridon + */ +public interface MailSendObservationConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof MailSendObservationContext; + } + +} diff --git a/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/package-info.java b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/package-info.java new file mode 100644 index 0000000000..1397ccf16d --- /dev/null +++ b/micrometer-jakarta9/src/main/java/io/micrometer/jakarta9/instrument/mail/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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. + */ + +/** + * Observation instrumentation for Jakarta Mail. + * + * @since 1.15.0 + */ +@NonNullFields +@NonNullApi +package io.micrometer.jakarta9.instrument.mail; + +import io.micrometer.common.lang.NonNullApi; +import io.micrometer.common.lang.NonNullFields; diff --git a/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/DefaultMailSendObservationConventionTests.java b/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/DefaultMailSendObservationConventionTests.java new file mode 100644 index 0000000000..f35891cdee --- /dev/null +++ b/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/DefaultMailSendObservationConventionTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.micrometer.jakarta9.instrument.mail; + +import io.micrometer.common.KeyValue; +import jakarta.mail.Address; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultMailSendObservationConvention}. + * + * @author famaridon + */ +class DefaultMailSendObservationConventionTests { + + private final DefaultMailSendObservationConvention convention = new DefaultMailSendObservationConvention(); + + @Test + void shouldHaveObservationName() { + assertThat(convention.getName()).isEqualTo("mail.send"); + } + + @Test + void shouldHaveContextualName() { + MailSendObservationContext context = new MailSendObservationContext(new MimeMessage((Session) null), "smtp", + "localhost", 25); + assertThat(convention.getContextualName(context)).isEqualTo("mail send"); + } + + @Test + void shouldHaveHighCardinalityKeyValues() throws MessagingException { + MimeMessage message = new MimeMessage((Session) null); + message.setFrom(new InternetAddress("from@example.com")); + message.addRecipient(Message.RecipientType.TO, new InternetAddress("to@example.com")); + message.setSubject("Test Subject"); + + MailSendObservationContext context = new MailSendObservationContext(message, "smtp", "localhost", 25); + assertThat(convention.getHighCardinalityKeyValues(context)).contains( + KeyValue.of("smtp.message.from", "from@example.com"), KeyValue.of("smtp.message.to", "to@example.com"), + KeyValue.of("smtp.message.subject", "Test Subject")); + } + + @Test + void shouldHaveLowCardinalityKeyValues() throws MessagingException { + MimeMessage message = new MimeMessage((Session) null); + message.setFrom(new InternetAddress("from@example.com")); + message.addRecipient(Message.RecipientType.TO, new InternetAddress("to@example.com")); + message.setSubject("Test Subject"); + + MailSendObservationContext context = new MailSendObservationContext(message, "smtp", "example.com", 587); + assertThat(convention.getLowCardinalityKeyValues(context)).contains( + KeyValue.of("server.address", "example.com"), KeyValue.of("server.port", "587"), + KeyValue.of("network.protocol.name", "smtp")); + } + + @Test + void shouldHandleMessagingException() { + MimeMessage message = new MimeMessage((Session) null) { + @Override + public Address[] getFrom() throws MessagingException { + throw new MessagingException("test exception"); + } + }; + + MailSendObservationContext context = new MailSendObservationContext(message, "smtp", "localhost", 25); + assertThat(convention.getHighCardinalityKeyValues(context)) + .contains(KeyValue.of("smtp.message.from", "unknown")); + } + + @Test + void shouldHandleMultipleFromAddresses() throws MessagingException { + MimeMessage message = new MimeMessage((Session) null); + message.setFrom(new InternetAddress("from1@example.com")); + message.addFrom(new InternetAddress[] { new InternetAddress("from2@example.com") }); + + MailSendObservationContext context = new MailSendObservationContext(message, "smtp", "localhost", 25); + assertThat(convention.getHighCardinalityKeyValues(context)) + .contains(KeyValue.of("smtp.message.from", "from1@example.com, from2@example.com")); + } + + @Test + void shouldHandleMultipleToAddresses() throws MessagingException { + MimeMessage message = new MimeMessage((Session) null); + message.addRecipient(Message.RecipientType.TO, new InternetAddress("to1@example.com")); + message.addRecipient(Message.RecipientType.TO, new InternetAddress("to2@example.com")); + + MailSendObservationContext context = new MailSendObservationContext(message, "smtp", "localhost", 25); + assertThat(convention.getHighCardinalityKeyValues(context)) + .contains(KeyValue.of("smtp.message.to", "to1@example.com, to2@example.com")); + } + +} diff --git a/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/InstrumentedTransportTest.java b/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/InstrumentedTransportTest.java new file mode 100644 index 0000000000..962ddd5afb --- /dev/null +++ b/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/InstrumentedTransportTest.java @@ -0,0 +1,126 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.micrometer.jakarta9.instrument.mail; + +import io.micrometer.observation.tck.TestObservationRegistry; +import jakarta.mail.*; + +import jakarta.mail.Message.RecipientType; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; + +import java.util.Properties; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link InstrumentedTransport}. + * + * @author famaridon + */ +class InstrumentedTransportTest { + + Session session; + + MockSMTPTransportListener listener = Mockito.mock(MockSMTPTransportListener.class); + + TestObservationRegistry registry = TestObservationRegistry.create(); + + Transport transport; + + @BeforeEach + void setUp() throws NoSuchProviderException { + // prepare properties for the session + Properties smtpProperties = new Properties(); + // default use of mock to simplify test + smtpProperties.put("mail.transport.protocol", "mocksmtp"); + + // avoid NPE + MockSMTPTransport.LISTENER = listener; + + // open a Session + this.session = Session.getInstance(smtpProperties); + transport = new InstrumentedTransport(session, this.session.getTransport("mocksmtp"), this.registry); + } + + @Test + void shouldDelegateConnect() throws MessagingException { + transport.connect("host", 123, "user", "password"); + verify(listener).onConnect("host", 123, "user", "password"); + } + + @Test + void shouldDelegateClose() throws MessagingException { + transport.close(); + verify(listener).onClose(); + } + + @Test + void shouldDelegateSendMessageEvenIfObservationRegistryIsNotInstalled() throws MessagingException { + Message msg = new MimeMessage(this.session); + Address[] to = new Address[0]; + transport.sendMessage(msg, to); + verify(listener).onSendMessage(msg, to); + } + + @Test + void shouldDelegateSendMessageIfObservationRegistryIsInstalled() throws MessagingException { + Message msg = new MimeMessage(this.session); + Address[] to = new Address[0]; + transport.sendMessage(msg, to); + verify(listener).onSendMessage(msg, to); + } + + @Test + void shouldCreateObservationWhenSendMessageIsCalled() throws MessagingException { + // arrange + when(listener.onSendMessage(Mockito.any(), Mockito.any())).thenAnswer(invocation -> { + Message message = (Message) invocation.getArguments()[0]; + message.setHeader("Message-Id", "message-id"); + return true; + }); + Message msg = new MimeMessage(this.session); + msg.setSubject("Hello world"); + Address[] to = new Address[0]; + msg.setFrom(new InternetAddress("from@example.com")); + msg.addRecipient(RecipientType.TO, new InternetAddress("to@example.com")); + transport.connect("example.com", 123, "user", "password"); + + // act + transport.sendMessage(msg, to); + + // assert + assertThat(registry).hasObservationWithNameEqualTo("mail.send") + .that() + .hasBeenStarted() + .hasBeenStopped() + .hasHighCardinalityKeyValue("smtp.message.id", "message-id") + .hasHighCardinalityKeyValue("smtp.message.subject", "Hello world") + .hasHighCardinalityKeyValue("smtp.message.to", "to@example.com") + .hasHighCardinalityKeyValue("smtp.message.from", "from@example.com") + .hasLowCardinalityKeyValue("server.address", "example.com") + .hasLowCardinalityKeyValue("server.port", "123") + .hasLowCardinalityKeyValue("network.protocol.name", "mocksmtp"); + } + +} diff --git a/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/MailKeyValuesTest.java b/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/MailKeyValuesTest.java new file mode 100644 index 0000000000..6ad792149a --- /dev/null +++ b/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/MailKeyValuesTest.java @@ -0,0 +1,270 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.micrometer.jakarta9.instrument.mail; + +import static io.micrometer.jakarta9.instrument.mail.MailKeyValues.UNKNOWN; +import static io.micrometer.jakarta9.instrument.mail.MailObservationDocumentation.HighCardinalityKeyNames.SMTP_MESSAGE_FROM; +import static io.micrometer.jakarta9.instrument.mail.MailObservationDocumentation.HighCardinalityKeyNames.SMTP_MESSAGE_SUBJECT; +import static io.micrometer.jakarta9.instrument.mail.MailObservationDocumentation.HighCardinalityKeyNames.SMTP_MESSAGE_ID; +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.mail.Message; +import jakarta.mail.Message.RecipientType; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; + +import java.util.Locale; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; + +/** + * Tests for {@link MailKeyValues}. + * + * @author famaridon + */ +class MailKeyValuesTest { + + @Test + @DisplayName("Should return UNKNOWN when 'from' address retrieval fails") + void smtpMessageFrom_WhenFromFails_ReturnsUnknown() throws MessagingException { + var message = Mockito.mock(Message.class); + Mockito.when(message.getFrom()).thenThrow(new MessagingException("test exception")); + var result = MailKeyValues.smtpMessageFrom(message); + assertThat(result).isPresent().hasValue(SMTP_MESSAGE_FROM.withValue(UNKNOWN)); + } + + @Test + @DisplayName("Should return EMPTY when 'from' address is unset") + void smtpMessageFrom_WhenFromUnset_ReturnsNone() { + var message = new MimeMessage((Session) null); + var result = MailKeyValues.smtpMessageFrom(message); + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("Should return EMPTY when 'from' address is empty") + void smtpMessageFrom_WhenFromEmpty_ReturnsNone() throws MessagingException { + var message = new MimeMessage((Session) null); + message.addFrom(new InternetAddress[0]); + var result = MailKeyValues.smtpMessageFrom(message); + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("Should return single 'from' address when one is set") + void smtpMessageFrom_WhenOneFromAddress_ReturnsAddress() throws MessagingException { + var message = new MimeMessage((Session) null); + message.addFrom(new InternetAddress[] { new InternetAddress("test@exemple.com") }); + var result = MailKeyValues.smtpMessageFrom(message); + assertThat(result).isPresent().hasValue(SMTP_MESSAGE_FROM.withValue("test@exemple.com")); + } + + @Test + @DisplayName("Should return multiple 'from' addresses when more than one is set") + void smtpMessageFrom_WhenMultipleFromAddresses_ReturnsAddresses() throws MessagingException { + var message = new MimeMessage((Session) null); + message.addFrom(new InternetAddress[] { new InternetAddress("test@exemple.com"), + new InternetAddress("other@example.com") }); + var result = MailKeyValues.smtpMessageFrom(message); + assertThat(result).isPresent().hasValue(SMTP_MESSAGE_FROM.withValue("test@exemple.com, other@example.com")); + } + + @ParameterizedTest + @MethodSource("recipientTypes") + @DisplayName("Should return EMPTY when recipients are unset") + void smtpMessageRecipients_WhenRecipientsUnset_ReturnsNone(RecipientType recipientType) { + var message = new MimeMessage((Session) null); + var result = MailKeyValues.smtpMessageRecipients(message, recipientType); + assertThat(result).isEmpty(); + } + + @ParameterizedTest + @MethodSource("recipientTypes") + @DisplayName("Should return UNKNOWN when recipient retrieval fails") + void smtpMessageRecipients_WhenRecipientsFail_ReturnsUnknown(RecipientType recipientType) + throws MessagingException { + MailObservationDocumentation.HighCardinalityKeyNames key = MailObservationDocumentation.HighCardinalityKeyNames + .valueOf("SMTP_MESSAGE_" + recipientType.toString().toUpperCase(Locale.ROOT)); + var message = Mockito.mock(Message.class); + Mockito.when(message.getRecipients(ArgumentMatchers.any())).thenThrow(new MessagingException("test exception")); + var result = MailKeyValues.smtpMessageRecipients(message, recipientType); + assertThat(result).isPresent().hasValue(key.withValue(UNKNOWN)); + } + + @ParameterizedTest + @MethodSource("recipientTypes") + @DisplayName("Should return EMPTY when recipients are empty") + void smtpMessageRecipients_WhenRecipientsEmpty_ReturnsNone(RecipientType recipientType) throws MessagingException { + var message = new MimeMessage((Session) null); + message.addRecipients(recipientType, new InternetAddress[0]); + var result = MailKeyValues.smtpMessageRecipients(message, recipientType); + assertThat(result).isEmpty(); + } + + @ParameterizedTest + @MethodSource("recipientTypes") + @DisplayName("Should return single recipient when one is set") + void smtpMessageRecipients_WhenOneRecipient_ReturnsRecipient(RecipientType recipientType) + throws MessagingException { + MailObservationDocumentation.HighCardinalityKeyNames key = MailObservationDocumentation.HighCardinalityKeyNames + .valueOf("SMTP_MESSAGE_" + recipientType.toString().toUpperCase(Locale.ROOT)); + var message = new MimeMessage((Session) null); + message.addRecipients(recipientType, new InternetAddress[] { new InternetAddress("test@exemple.com") }); + var result = MailKeyValues.smtpMessageRecipients(message, recipientType); + assertThat(result).isPresent().hasValue(key.withValue("test@exemple.com")); + } + + @ParameterizedTest + @MethodSource("recipientTypes") + @DisplayName("Should return multiple recipients when more than one is set") + void smtpMessageRecipients_WhenMultipleRecipients_ReturnsRecipients(RecipientType recipientType) + throws MessagingException { + MailObservationDocumentation.HighCardinalityKeyNames key = MailObservationDocumentation.HighCardinalityKeyNames + .valueOf("SMTP_MESSAGE_" + recipientType.toString().toUpperCase(Locale.ROOT)); + var message = new MimeMessage((Session) null); + message.addRecipients(recipientType, new InternetAddress[] { new InternetAddress("test@exemple.com"), + new InternetAddress("other@example.com") }); + var result = MailKeyValues.smtpMessageRecipients(message, recipientType); + + assertThat(result).isPresent().hasValue(key.withValue("test@exemple.com, other@example.com")); + } + + public static Stream recipientTypes() { + return Stream.of(Arguments.of(Message.RecipientType.TO), Arguments.of(Message.RecipientType.CC), + Arguments.of(Message.RecipientType.BCC)); + } + + @Test + @DisplayName("Should return subject when it is set") + void smtpMessageSubject_WhenSubjectSet_ReturnsSubject() throws MessagingException { + var message = new MimeMessage((Session) null); + message.setSubject("test subject"); + var result = MailKeyValues.smtpMessageSubject(message); + assertThat(result).isPresent().hasValue(SMTP_MESSAGE_SUBJECT.withValue("test subject")); + } + + @Test + @DisplayName("Should return UNKNOWN when subject retrieval fails") + void smtpMessageSubject_WhenSubjectFails_ReturnsUnknown() throws MessagingException { + var message = Mockito.mock(Message.class); + Mockito.when(message.getSubject()).thenThrow(new MessagingException("test exception")); + var result = MailKeyValues.smtpMessageSubject(message); + assertThat(result).isPresent().hasValue(SMTP_MESSAGE_SUBJECT.withValue(UNKNOWN)); + } + + @Test + @DisplayName("Should return EMPTY when subject is unset") + void smtpMessageSubject_WhenSubjectUnset_ReturnsNone() { + var message = new MimeMessage((Session) null); + var result = MailKeyValues.smtpMessageSubject(message); + assertThat(result).isEmpty(); + + } + + @Test + @DisplayName("Should return server address when it is provided") + void serverAddress_WhenProvided_ReturnsAddress() { + var msg = new MimeMessage((Session) null); + var ctx = new MailSendObservationContext(msg, "smtp", "localhost", 25); + + var result = MailKeyValues.serverAddress(ctx); + assertThat(result.getValue()).isEqualTo("localhost"); + } + + @Test + @DisplayName("Should return UNKNOWN when server address is not provided") + void serverAddress_WhenNotProvided_ReturnsUnknown() { + var msg = new MimeMessage((Session) null); + var ctx = new MailSendObservationContext(msg, "smtp", null, 25); + + var result = MailKeyValues.serverAddress(ctx); + assertThat(result.getValue()).isEqualTo(UNKNOWN); + } + + @Test + @DisplayName("Should return server port when it is valid") + void serverPort_WhenValid_ReturnsPort() { + var msg = new MimeMessage((Session) null); + var ctx = new MailSendObservationContext(msg, "smtp", "localhost", 25); + + var result = MailKeyValues.serverPort(ctx); + assertThat(result.getValue()).isEqualTo("25"); + } + + @Test + @DisplayName("Should return UNKNOWN when server port is zero or negative") + void serverPort_WhenZeroOrNegative_ReturnsUnknown() { + var msg = new MimeMessage((Session) null); + var ctx = new MailSendObservationContext(msg, "smtp", "localhost", 0); + + var result = MailKeyValues.serverPort(ctx); + assertThat(result.getValue()).isEqualTo(UNKNOWN); + } + + @Test + @DisplayName("Should return UNKNOWN when Message-ID header retrieval fails") + void smtpMessageId_WhenHeaderFails_ReturnsUnknown() throws MessagingException { + var message = Mockito.mock(Message.class); + Mockito.when(message.getHeader("Message-ID")).thenThrow(new MessagingException("test exception")); + var result = MailKeyValues.smtpMessageId(message); + assertThat(result).isPresent().hasValue(SMTP_MESSAGE_ID.withValue(UNKNOWN)); + } + + @Test + @DisplayName("Should return EMPTY when Message-ID header is missing") + void smtpMessageId_WhenHeaderMissing_ReturnsUnknown() { + var message = new MimeMessage((Session) null); + var result = MailKeyValues.smtpMessageId(message); + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("Should return Message-ID when it is set") + void smtpMessageId_WhenHeaderSet_ReturnsMessageId() throws MessagingException { + var message = new MimeMessage((Session) null); + message.addHeader("Message-ID", "12345@example.com"); + var result = MailKeyValues.smtpMessageId(message); + assertThat(result).isPresent().hasValue(SMTP_MESSAGE_ID.withValue("12345@example.com")); + } + + @Test + @DisplayName("Should return network protocol name when it is provided") + void networkProtocolName() { + var message = new MimeMessage((Session) null); + var ctx = new MailSendObservationContext(message, "smtp", "localhost", 25); + var result = MailKeyValues.networkProtocolName(ctx); + assertThat(result.getValue()).isEqualTo("smtp"); + } + + @Test + @DisplayName("Should return UNKNOWN when network protocol name is null") + void networkProtocolName_WhenNull() { + var message = new MimeMessage((Session) null); + var ctx = new MailSendObservationContext(message, null, "localhost", 25); + var result = MailKeyValues.networkProtocolName(ctx); + assertThat(result.getValue()).isEqualTo(UNKNOWN); + } + +} diff --git a/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/MailObservationDocumentationTest.java b/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/MailObservationDocumentationTest.java new file mode 100644 index 0000000000..4448ed445c --- /dev/null +++ b/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/MailObservationDocumentationTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.micrometer.jakarta9.instrument.mail; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MailObservationDocumentation}. + * + * @author famaridon + */ +class MailObservationDocumentationTest { + + @Test + void testMailSendObservation() { + MailObservationDocumentation mailSend = MailObservationDocumentation.MAIL_SEND; + + // Verify default convention + Class> defaultConvention = mailSend + .getDefaultConvention(); + assertThat(defaultConvention).isEqualTo(DefaultMailSendObservationConvention.class); + + // Verify low cardinality key names + KeyName[] lowCardinalityKeyNames = mailSend.getLowCardinalityKeyNames(); + assertThat(lowCardinalityKeyNames).containsExactly( + MailObservationDocumentation.LowCardinalityKeyNames.SERVER_ADDRESS, + MailObservationDocumentation.LowCardinalityKeyNames.SERVER_PORT, + MailObservationDocumentation.LowCardinalityKeyNames.NETWORK_PROTOCOL_NAME); + + // Verify high cardinality key names + KeyName[] highCardinalityKeyNames = mailSend.getHighCardinalityKeyNames(); + assertThat(highCardinalityKeyNames).containsExactly( + MailObservationDocumentation.HighCardinalityKeyNames.SMTP_MESSAGE_FROM, + MailObservationDocumentation.HighCardinalityKeyNames.SMTP_MESSAGE_TO, + MailObservationDocumentation.HighCardinalityKeyNames.SMTP_MESSAGE_CC, + MailObservationDocumentation.HighCardinalityKeyNames.SMTP_MESSAGE_BCC, + MailObservationDocumentation.HighCardinalityKeyNames.SMTP_MESSAGE_SUBJECT, + MailObservationDocumentation.HighCardinalityKeyNames.SMTP_MESSAGE_ID); + } + +} diff --git a/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/MockSMTPTransport.java b/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/MockSMTPTransport.java new file mode 100644 index 0000000000..3c0208e105 --- /dev/null +++ b/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/MockSMTPTransport.java @@ -0,0 +1,50 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.micrometer.jakarta9.instrument.mail; + +import jakarta.mail.*; + +/** + * Mock implementation of the SMTP Transport class for testing purposes. This class allows + * us to simulate the behavior of an SMTP transport without actually sending emails. + * + * @author famaridon + */ +public class MockSMTPTransport extends Transport { + + static MockSMTPTransportListener LISTENER; + + public MockSMTPTransport(Session session, URLName urlname) { + super(session, urlname); + LISTENER.onConstructor(session, urlname); + } + + @Override + public synchronized void connect(String host, int port, String user, String password) throws MessagingException { + LISTENER.onConnect(host, port, user, password); + } + + @Override + public void sendMessage(Message msg, Address[] addresses) throws MessagingException { + LISTENER.onSendMessage(msg, addresses); + } + + @Override + public synchronized void close() { + LISTENER.onClose(); + } + +} diff --git a/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/MockSMTPTransportListener.java b/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/MockSMTPTransportListener.java new file mode 100644 index 0000000000..ed93b3729e --- /dev/null +++ b/micrometer-jakarta9/src/test/java/io/micrometer/jakarta9/instrument/mail/MockSMTPTransportListener.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.micrometer.jakarta9.instrument.mail; + +import jakarta.mail.*; + +/** + * Listener interface for the MockSMTPTransport class. This interface defines methods that + * will be called during the lifecycle of the MockSMTPTransport. + * + * @author famaridon + */ +interface MockSMTPTransportListener { + + void onConstructor(Session session, URLName urlname); + + void onConnect(String host, int port, String user, String password) throws MessagingException; + + boolean onSendMessage(Message msg, Address[] addresses) throws MessagingException; + + void onClose(); + +} diff --git a/micrometer-jakarta9/src/test/resources/META-INF/javamail.default.providers b/micrometer-jakarta9/src/test/resources/META-INF/javamail.default.providers new file mode 100644 index 0000000000..34318e9877 --- /dev/null +++ b/micrometer-jakarta9/src/test/resources/META-INF/javamail.default.providers @@ -0,0 +1 @@ +protocol=mocksmtp; type=transport; class=io.micrometer.jakarta9.instrument.mail.MockSMTPTransport; vendor=Micrometer; diff --git a/samples/micrometer-samples-jakarta10/build.gradle b/samples/micrometer-samples-jakarta10/build.gradle new file mode 100644 index 0000000000..e53eac6ada --- /dev/null +++ b/samples/micrometer-samples-jakarta10/build.gradle @@ -0,0 +1,14 @@ +plugins { + id 'java' +} + +dependencies { + implementation project(":micrometer-jakarta9") + // test micrometer-jakarta9 works with Jakarta 10 Mail dependencies + implementation('jakarta.mail:jakarta.mail-api:2.1.3') + testRuntimeOnly('org.eclipse.angus:jakarta.mail:2.0.3') + + testImplementation(project(":micrometer-observation-test")) + testImplementation(libs.junitJupiter) + testRuntimeOnly(libs.junitPlatformLauncher) +} diff --git a/samples/micrometer-samples-jakarta10/src/test/java/io/micrometer/samples/jakarta10/mail/Jakarta10InstrumentedTransportTest.java b/samples/micrometer-samples-jakarta10/src/test/java/io/micrometer/samples/jakarta10/mail/Jakarta10InstrumentedTransportTest.java new file mode 100644 index 0000000000..5f565764da --- /dev/null +++ b/samples/micrometer-samples-jakarta10/src/test/java/io/micrometer/samples/jakarta10/mail/Jakarta10InstrumentedTransportTest.java @@ -0,0 +1,80 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.micrometer.samples.jakarta10.mail; + +import io.micrometer.jakarta9.instrument.mail.InstrumentedTransport; +import io.micrometer.observation.tck.TestObservationRegistry; +import jakarta.mail.*; +import jakarta.mail.Message.RecipientType; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Properties; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link InstrumentedTransport} on Jakarta 10 Mail dependencies. + */ +class Jakarta10InstrumentedTransportTest { + + Session session; + + TestObservationRegistry registry = TestObservationRegistry.create(); + + Transport transport; + + @BeforeEach + void setUp() throws NoSuchProviderException { + // prepare properties for the session + Properties smtpProperties = new Properties(); + // default use of mock to simplify test + smtpProperties.put("mail.transport.protocol", "mocksmtp"); + + // open a Session + this.session = Session.getInstance(smtpProperties); + transport = new InstrumentedTransport(session, this.session.getTransport("mocksmtp"), this.registry); + } + + @Test + void shouldCreateObservationWhenSendMessageIsCalled() throws MessagingException { + // arrange + Message msg = new MimeMessage(this.session); + msg.setSubject("Hello world"); + Address[] to = new Address[0]; + msg.setFrom(new InternetAddress("from@example.com")); + msg.addRecipient(RecipientType.TO, new InternetAddress("to@example.com")); + transport.connect("example.com", 123, "user", "password"); + + // act + transport.sendMessage(msg, to); + + // assert + assertThat(registry).hasObservationWithNameEqualTo("mail.send") + .that() + .hasBeenStarted() + .hasBeenStopped() + .hasHighCardinalityKeyValue("smtp.message.subject", "Hello world") + .hasHighCardinalityKeyValue("smtp.message.to", "to@example.com") + .hasHighCardinalityKeyValue("smtp.message.from", "from@example.com") + .hasLowCardinalityKeyValue("server.address", "example.com") + .hasLowCardinalityKeyValue("server.port", "123") + .hasLowCardinalityKeyValue("network.protocol.name", "mocksmtp"); + } + +} diff --git a/samples/micrometer-samples-jakarta10/src/test/java/io/micrometer/samples/jakarta10/mail/MockSMTPTransport.java b/samples/micrometer-samples-jakarta10/src/test/java/io/micrometer/samples/jakarta10/mail/MockSMTPTransport.java new file mode 100644 index 0000000000..6c35d79567 --- /dev/null +++ b/samples/micrometer-samples-jakarta10/src/test/java/io/micrometer/samples/jakarta10/mail/MockSMTPTransport.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.micrometer.samples.jakarta10.mail; + +import jakarta.mail.*; + +public class MockSMTPTransport extends Transport { + + public MockSMTPTransport(Session session, URLName urlname) { + super(session, urlname); + } + + @Override + public synchronized void connect(String host, int port, String user, String password) throws MessagingException { + } + + @Override + public void sendMessage(Message msg, Address[] addresses) throws MessagingException { + } + + @Override + public synchronized void close() { + } + +} diff --git a/samples/micrometer-samples-jakarta10/src/test/resources/META-INF/javamail.default.providers b/samples/micrometer-samples-jakarta10/src/test/resources/META-INF/javamail.default.providers new file mode 100644 index 0000000000..150f35a1a0 --- /dev/null +++ b/samples/micrometer-samples-jakarta10/src/test/resources/META-INF/javamail.default.providers @@ -0,0 +1 @@ +protocol=mocksmtp; type=transport; class=io.micrometer.samples.jakarta10.mail.MockSMTPTransport; vendor=Micrometer; diff --git a/settings.gradle b/settings.gradle index 57ccecfce1..1177f63d14 100644 --- a/settings.gradle +++ b/settings.gradle @@ -26,7 +26,7 @@ buildCache { include 'micrometer-commons', 'micrometer-core', 'micrometer-observation' -['core', 'boot2', 'boot2-reactive', 'spring-integration', 'hazelcast', 'hazelcast3', 'javalin', 'jersey3', 'jooq', 'kotlin', 'spring-framework6'].each { sample -> +['core', 'boot2', 'boot2-reactive', 'spring-integration', 'hazelcast', 'hazelcast3', 'jakarta10', 'javalin', 'jersey3', 'jooq', 'kotlin', 'spring-framework6'].each { sample -> include "micrometer-samples-$sample" project(":micrometer-samples-$sample").projectDir = new File(rootProject.projectDir, "samples/micrometer-samples-$sample") }