diff --git a/build.gradle.kts b/build.gradle.kts index 5a3d6880..35d13dd8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,11 +14,11 @@ * limitations under the License. */ -plugins{ +plugins { id("io.github.gradle-nexus.publish-plugin") version "2.0.0" } -val swoAgentVersion = "3.0.3" +val swoAgentVersion = "3.0.3-profile" extra["swoAgentVersion"] = swoAgentVersion group = "com.solarwinds" version = if (System.getenv("SNAPSHOT_BUILD").toBoolean()) "$swoAgentVersion-SNAPSHOT" else swoAgentVersion diff --git a/custom/shared/src/main/java/com/solarwinds/opentelemetry/extensions/InboundMeasurementMetricsGenerator.java b/custom/shared/src/main/java/com/solarwinds/opentelemetry/extensions/InboundMeasurementMetricsGenerator.java index ff49d3b1..0b58a802 100644 --- a/custom/shared/src/main/java/com/solarwinds/opentelemetry/extensions/InboundMeasurementMetricsGenerator.java +++ b/custom/shared/src/main/java/com/solarwinds/opentelemetry/extensions/InboundMeasurementMetricsGenerator.java @@ -100,6 +100,9 @@ public void onEnd(ReadableSpan span) { responseTimeAttr.put(errorKey, hasError); responseTime.record( duration, responseTimeAttr.put(TRANSACTION_NAME_KEY, transactionName).build()); + logger.debug( + String.format( + "Inbound measurement metrics generated with context: %s", Context.current())); } } diff --git a/custom/src/main/java/com/solarwinds/opentelemetry/extensions/SolarwindsProfilingSpanProcessor.java b/custom/src/main/java/com/solarwinds/opentelemetry/extensions/SolarwindsProfilingSpanProcessor.java index 72eb8417..269c5f12 100644 --- a/custom/src/main/java/com/solarwinds/opentelemetry/extensions/SolarwindsProfilingSpanProcessor.java +++ b/custom/src/main/java/com/solarwinds/opentelemetry/extensions/SolarwindsProfilingSpanProcessor.java @@ -27,6 +27,7 @@ import com.solarwinds.joboe.sampling.Metadata; import com.solarwinds.joboe.shaded.javax.annotation.Nonnull; import com.solarwinds.opentelemetry.core.Util; +import com.solarwinds.opentelemetry.extensions.profile.SampleEmitter; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.context.Context; @@ -49,7 +50,10 @@ public void onStart(@Nonnull Context parentContext, ReadWriteSpan span) { Metadata metadata = Util.buildMetadata(spanContext); if (metadata.isValid()) { Profiler.addProfiledThread( - Thread.currentThread(), metadata, Metadata.bytesToHex(metadata.getTaskID())); + Thread.currentThread(), + metadata, + Metadata.bytesToHex(metadata.getTaskID()), + new SampleEmitter(span)); } } else { span.setAttribute(SW_KEY_PREFIX + "profile.spans", -1); // profiler disabled diff --git a/custom/src/main/java/com/solarwinds/opentelemetry/extensions/profile/SampleEmitter.java b/custom/src/main/java/com/solarwinds/opentelemetry/extensions/profile/SampleEmitter.java new file mode 100644 index 00000000..1c4d5520 --- /dev/null +++ b/custom/src/main/java/com/solarwinds/opentelemetry/extensions/profile/SampleEmitter.java @@ -0,0 +1,153 @@ +/* + * © SolarWinds Worldwide, LLC. All rights reserved. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.solarwinds.opentelemetry.extensions.profile; + +import com.solarwinds.joboe.core.profiler.ProfileSampleEmitter; +import com.solarwinds.joboe.logging.LoggerFactory; +import com.solarwinds.joboe.shaded.google.gson.Gson; +import com.solarwinds.joboe.shaded.google.gson.GsonBuilder; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import java.util.List; +import java.util.Map; + +public class SampleEmitter implements ProfileSampleEmitter { + private static final Gson gson = new GsonBuilder().create(); + + private AttributesBuilder entryAttributesBuilder; + + private AttributesBuilder exitAttributesBuilder; + + private AttributesBuilder sampleAttributesBuilder; + + private ReadWriteSpan readWriteSpan; + + public SampleEmitter(ReadWriteSpan readWriteSpan) { + this.readWriteSpan = readWriteSpan; + } + + @Override + public void addAttribute(String key, T value) { + if (value instanceof String) { + if (entryAttributesBuilder != null) { + entryAttributesBuilder.put(AttributeKey.stringKey(key), (String) value); + } + + if (sampleAttributesBuilder != null) { + sampleAttributesBuilder.put(AttributeKey.stringKey(key), (String) value); + } + + if (exitAttributesBuilder != null) { + exitAttributesBuilder.put(AttributeKey.stringKey(key), (String) value); + } + } + + if (value instanceof Long) { + if (entryAttributesBuilder != null) { + entryAttributesBuilder.put(AttributeKey.longKey(key), (Long) value); + } + + if (sampleAttributesBuilder != null) { + sampleAttributesBuilder.put(AttributeKey.longKey(key), (Long) value); + } + + if (exitAttributesBuilder != null) { + exitAttributesBuilder.put(AttributeKey.longKey(key), (Long) value); + } + } + + if (value instanceof List) { + boolean match = ((List) value).stream().anyMatch(item -> item instanceof Long); + Long[] longs = null; + if (match) { + longs = ((List) value).stream().map(Long.class::cast).toArray(Long[]::new); + } + + if (entryAttributesBuilder != null && match) { + entryAttributesBuilder.put(AttributeKey.longArrayKey(key), longs); + } + + if (sampleAttributesBuilder != null && match) { + sampleAttributesBuilder.put(AttributeKey.longArrayKey(key), longs); + } + + if (exitAttributesBuilder != null && match) { + exitAttributesBuilder.put(AttributeKey.longArrayKey(key), longs); + } + + match = ((List) value).stream().anyMatch(item -> item instanceof Map); + String[] strings = null; + if (match) { + strings = ((List) value).stream().map(gson::toJson).toArray(String[]::new); + } + + if (entryAttributesBuilder != null && match) { + entryAttributesBuilder.put(AttributeKey.stringArrayKey(key), strings); + } + + if (sampleAttributesBuilder != null && match) { + sampleAttributesBuilder.put(AttributeKey.stringArrayKey(key), strings); + } + + if (exitAttributesBuilder != null && match) { + exitAttributesBuilder.put(AttributeKey.stringArrayKey(key), strings); + } + } + } + + @Override + public void beginEntryEmit() { + entryAttributesBuilder = Attributes.builder(); + } + + @Override + public void beginExitEmit() { + exitAttributesBuilder = Attributes.builder(); + } + + @Override + public void beginSampleEmit() { + sampleAttributesBuilder = Attributes.builder(); + } + + @Override + public boolean endEntryEmit() { + readWriteSpan.addEvent("sw.profile", entryAttributesBuilder.build()); + entryAttributesBuilder = null; + LoggerFactory.getLogger().debug("Entry emit has been ended: span = " + readWriteSpan); + return true; + } + + @Override + public boolean endExitEmit() { + readWriteSpan.addEvent("sw.profile", exitAttributesBuilder.build()); + LoggerFactory.getLogger().debug("Exit emit has been ended: span = " + readWriteSpan); + exitAttributesBuilder = null; + readWriteSpan = null; + return true; + } + + @Override + public boolean endSampleEmit() { + readWriteSpan.addEvent("sw.profile", sampleAttributesBuilder.build()); + LoggerFactory.getLogger().debug("Sample emit has been ended: span = " + readWriteSpan); + sampleAttributesBuilder = null; + return true; + } +} diff --git a/custom/src/test/java/com/solarwinds/opentelemetry/extensions/SolarwindsProfilingSpanProcessorTest.java b/custom/src/test/java/com/solarwinds/opentelemetry/extensions/SolarwindsProfilingSpanProcessorTest.java index aa5e4eb1..0b1306fa 100644 --- a/custom/src/test/java/com/solarwinds/opentelemetry/extensions/SolarwindsProfilingSpanProcessorTest.java +++ b/custom/src/test/java/com/solarwinds/opentelemetry/extensions/SolarwindsProfilingSpanProcessorTest.java @@ -161,7 +161,8 @@ void onStartWhenProfilingIsEnabledOnRootSpanWithValidMetadataShouldAddProfiledTh processor.onStart(mockParentContext, mockSpan); profilerMock.verify( - () -> Profiler.addProfiledThread(eq(Thread.currentThread()), any(), eq(traceId)), times(1)); + () -> Profiler.addProfiledThread(eq(Thread.currentThread()), any(), eq(traceId), any()), + times(1)); verify(mockSpan, never()).setAttribute(anyString(), anyInt()); } diff --git a/custom/src/test/java/com/solarwinds/opentelemetry/extensions/profile/SampleEmitterTest.java b/custom/src/test/java/com/solarwinds/opentelemetry/extensions/profile/SampleEmitterTest.java new file mode 100644 index 00000000..117c0f84 --- /dev/null +++ b/custom/src/test/java/com/solarwinds/opentelemetry/extensions/profile/SampleEmitterTest.java @@ -0,0 +1,372 @@ +/* + * © SolarWinds Worldwide, LLC. All rights reserved. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.solarwinds.opentelemetry.extensions.profile; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SampleEmitterTest { + + private SampleEmitter sampleEmitter; + + @Mock private ReadWriteSpan mockSpan; + + @Captor private ArgumentCaptor eventNameCaptor; + + @Captor private ArgumentCaptor attributesCaptor; + + @BeforeEach + void setUp() { + sampleEmitter = new SampleEmitter(mockSpan); + } + + @Test + @DisplayName("beginEntryEmit should initialize entry attributes builder") + void beginEntryEmitShouldInitializeBuilder() { + sampleEmitter.beginEntryEmit(); + sampleEmitter.endEntryEmit(); + + verify(mockSpan).addEvent(eventNameCaptor.capture(), attributesCaptor.capture()); + assertEquals("sw.profile", eventNameCaptor.getValue()); + assertNotNull(attributesCaptor.getValue()); + } + + @Test + @DisplayName("beginExitEmit should initialize exit attributes builder") + void beginExitEmitShouldInitializeBuilder() { + sampleEmitter.beginExitEmit(); + sampleEmitter.endExitEmit(); + + verify(mockSpan).addEvent(eventNameCaptor.capture(), attributesCaptor.capture()); + assertEquals("sw.profile", eventNameCaptor.getValue()); + assertNotNull(attributesCaptor.getValue()); + } + + @Test + @DisplayName("beginSampleEmit should initialize sample attributes builder") + void beginSampleEmitShouldInitializeBuilder() { + sampleEmitter.beginSampleEmit(); + sampleEmitter.endSampleEmit(); + + verify(mockSpan).addEvent(eventNameCaptor.capture(), attributesCaptor.capture()); + assertEquals("sw.profile", eventNameCaptor.getValue()); + assertNotNull(attributesCaptor.getValue()); + } + + @Test + @DisplayName("addAttribute should add string attribute to entry builder") + void addAttributeShouldAddStringToEntry() { + sampleEmitter.beginEntryEmit(); + + sampleEmitter.addAttribute("stringKey", "stringValue"); + sampleEmitter.endEntryEmit(); + + verify(mockSpan).addEvent(eventNameCaptor.capture(), attributesCaptor.capture()); + Attributes attributes = attributesCaptor.getValue(); + + assertEquals("sw.profile", eventNameCaptor.getValue()); + assertEquals("stringValue", attributes.get(AttributeKey.stringKey("stringKey"))); + } + + @Test + @DisplayName("addAttribute should add long attribute to entry builder") + void addAttributeShouldAddLongToEntry() { + sampleEmitter.beginEntryEmit(); + + sampleEmitter.addAttribute("longKey", 12345L); + sampleEmitter.endEntryEmit(); + + verify(mockSpan).addEvent(eventNameCaptor.capture(), attributesCaptor.capture()); + Attributes attributes = attributesCaptor.getValue(); + assertEquals(12345L, attributes.get(AttributeKey.longKey("longKey"))); + } + + @Test + @DisplayName("addAttribute should add list of longs as long array to entry builder") + void addAttributeShouldAddListOfLongsToEntry() { + sampleEmitter.beginEntryEmit(); + List longList = Arrays.asList(1L, 2L, 3L, 4L, 5L); + + sampleEmitter.addAttribute("longListKey", longList); + sampleEmitter.endEntryEmit(); + + verify(mockSpan).addEvent(eventNameCaptor.capture(), attributesCaptor.capture()); + Attributes attributes = attributesCaptor.getValue(); + + List result = attributes.get(AttributeKey.longArrayKey("longListKey")); + assertNotNull(result); + assertEquals(5, result.size()); + assertArrayEquals(new Long[] {1L, 2L, 3L, 4L, 5L}, result.toArray(new Long[0])); + } + + @Test + @DisplayName("addAttribute should add list of maps as JSON string array to entry builder") + void addAttributeShouldAddListOfMapsToEntry() { + sampleEmitter.beginEntryEmit(); + Map map1 = new HashMap<>(); + map1.put("key1", "value1"); + + Map map2 = new HashMap<>(); + map2.put("key2", "value2"); + List> mapList = Arrays.asList(map1, map2); + + sampleEmitter.addAttribute("mapListKey", mapList); + sampleEmitter.endEntryEmit(); + + verify(mockSpan).addEvent(eventNameCaptor.capture(), attributesCaptor.capture()); + Attributes attributes = attributesCaptor.getValue(); + + List result = attributes.get(AttributeKey.stringArrayKey("mapListKey")); + assertNotNull(result); + assertEquals(2, result.size()); + assertTrue(result.get(0).contains("key1")); + assertTrue(result.get(1).contains("key2")); + } + + @Test + @DisplayName("addAttribute should add string attribute to sample builder") + void addAttributeShouldAddStringToSample() { + sampleEmitter.beginSampleEmit(); + + sampleEmitter.addAttribute("sampleKey", "sampleValue"); + sampleEmitter.endSampleEmit(); + + verify(mockSpan).addEvent(eventNameCaptor.capture(), attributesCaptor.capture()); + Attributes attributes = attributesCaptor.getValue(); + assertEquals("sampleValue", attributes.get(AttributeKey.stringKey("sampleKey"))); + } + + @Test + @DisplayName("addAttribute should add string attribute to exit builder") + void addAttributeShouldAddStringToExit() { + sampleEmitter.beginExitEmit(); + + sampleEmitter.addAttribute("exitKey", "exitValue"); + sampleEmitter.endExitEmit(); + + verify(mockSpan).addEvent(eventNameCaptor.capture(), attributesCaptor.capture()); + Attributes attributes = attributesCaptor.getValue(); + assertEquals("exitValue", attributes.get(AttributeKey.stringKey("exitKey"))); + } + + @Test + @DisplayName("addAttribute should handle multiple attributes") + void addAttributeShouldHandleMultipleAttributes() { + sampleEmitter.beginEntryEmit(); + + sampleEmitter.addAttribute("string", "value"); + sampleEmitter.addAttribute("long", 999L); + sampleEmitter.addAttribute("list", Arrays.asList(1L, 2L)); + sampleEmitter.endEntryEmit(); + + verify(mockSpan).addEvent(eventNameCaptor.capture(), attributesCaptor.capture()); + Attributes attributes = attributesCaptor.getValue(); + + assertEquals("value", attributes.get(AttributeKey.stringKey("string"))); + assertEquals(999L, attributes.get(AttributeKey.longKey("long"))); + assertNotNull(attributes.get(AttributeKey.longArrayKey("list"))); + } + + @Test + @DisplayName("addAttribute should not add attribute when no builder is initialized") + void addAttributeShouldNotAddWhenNoBuilderInitialized() { + sampleEmitter.addAttribute("key", "value"); + verifyNoInteractions(mockSpan); + } + + @Test + @DisplayName("endEntryEmit should return true and add event") + void endEntryEmitShouldReturnTrueAndAddEvent() { + sampleEmitter.beginEntryEmit(); + boolean result = sampleEmitter.endEntryEmit(); + + assertTrue(result); + verify(mockSpan).addEvent(eq("sw.profile"), any(Attributes.class)); + } + + @Test + @DisplayName("endExitEmit should return true and add event") + void endExitEmitShouldReturnTrueAndAddEvent() { + sampleEmitter.beginExitEmit(); + + boolean result = sampleEmitter.endExitEmit(); + assertTrue(result); + verify(mockSpan).addEvent(eq("sw.profile"), any(Attributes.class)); + } + + @Test + @DisplayName("endSampleEmit should return true and add event") + void endSampleEmitShouldReturnTrueAndAddEvent() { + sampleEmitter.beginSampleEmit(); + + boolean result = sampleEmitter.endSampleEmit(); + assertTrue(result); + verify(mockSpan).addEvent(eq("sw.profile"), any(Attributes.class)); + } + + @Test + @DisplayName("multiple emit cycles should work independently") + void multipleEmitCyclesShouldWorkIndependently() { + sampleEmitter.beginEntryEmit(); + sampleEmitter.addAttribute("entry", "value1"); + sampleEmitter.endEntryEmit(); + + sampleEmitter.beginSampleEmit(); + sampleEmitter.addAttribute("sample", "value2"); + sampleEmitter.endSampleEmit(); + + verify(mockSpan, times(2)).addEvent(eq("sw.profile"), any(Attributes.class)); + } + + @Test + @DisplayName("addAttribute should handle empty list") + void addAttributeShouldHandleEmptyList() { + sampleEmitter.beginEntryEmit(); + List emptyList = Collections.emptyList(); + + assertDoesNotThrow( + () -> { + sampleEmitter.addAttribute("emptyList", emptyList); + sampleEmitter.endEntryEmit(); + }); + } + + @Test + @DisplayName("addAttribute with Integer should not add attribute") + void addAttributeWithIntegerShouldNotAdd() { + sampleEmitter.beginEntryEmit(); + + sampleEmitter.addAttribute("intKey", 123); + sampleEmitter.endEntryEmit(); + + verify(mockSpan).addEvent(eventNameCaptor.capture(), attributesCaptor.capture()); + Attributes attributes = attributesCaptor.getValue(); + + assertNull(attributes.get(AttributeKey.longKey("intKey"))); + assertNull(attributes.get(AttributeKey.stringKey("intKey"))); + } + + @Test + @DisplayName("concurrent builders should work independently") + void concurrentBuildersShouldWorkIndependently() { + sampleEmitter.beginEntryEmit(); + sampleEmitter.beginSampleEmit(); + sampleEmitter.beginExitEmit(); + + sampleEmitter.addAttribute("key", "allBuilders"); + sampleEmitter.endEntryEmit(); + sampleEmitter.endSampleEmit(); + + sampleEmitter.endExitEmit(); + verify(mockSpan, times(3)).addEvent(eq("sw.profile"), any(Attributes.class)); + } + + @Test + @DisplayName("endExitEmit should nullify span reference") + void endExitEmitShouldNullifySpan() { + sampleEmitter.beginExitEmit(); + sampleEmitter.endExitEmit(); + + sampleEmitter.beginEntryEmit(); + assertDoesNotThrow(() -> sampleEmitter.addAttribute("test", "value")); + } + + @Test + @DisplayName("addAttribute should convert list of maps to JSON strings") + void addAttributeShouldConvertMapsToJson() { + sampleEmitter.beginEntryEmit(); + Map map1 = new HashMap(); + map1.put("name", "John"); + map1.put("age", 30); + Map map2 = new HashMap(); + map2.put("name", "Jane"); + map2.put("age", 25); + List> mapList = Arrays.asList(map1, map2); + + sampleEmitter.addAttribute("users", mapList); + sampleEmitter.endEntryEmit(); + + verify(mockSpan).addEvent(eventNameCaptor.capture(), attributesCaptor.capture()); + Attributes attributes = attributesCaptor.getValue(); + List jsonStrings = attributes.get(AttributeKey.stringArrayKey("users")); + + assertNotNull(jsonStrings); + assertEquals(2, jsonStrings.size()); + assertTrue(jsonStrings.get(0).contains("John")); + assertTrue(jsonStrings.get(0).contains("30")); + assertTrue(jsonStrings.get(1).contains("Jane")); + assertTrue(jsonStrings.get(1).contains("25")); + } + + @Test + @DisplayName("endEntryEmit should add event with empty attributes when no attributes added") + void endEntryEmitShouldAddEventWithEmptyAttributes() { + sampleEmitter.beginEntryEmit(); + sampleEmitter.endEntryEmit(); + + verify(mockSpan).addEvent(eventNameCaptor.capture(), attributesCaptor.capture()); + Attributes attributes = attributesCaptor.getValue(); + + assertEquals("sw.profile", eventNameCaptor.getValue()); + assertEquals(0, attributes.size()); + } + + @Test + @DisplayName("endExitEmit should add event with empty attributes when no attributes added") + void endExitEmitShouldAddEventWithEmptyAttributes() { + sampleEmitter.beginExitEmit(); + sampleEmitter.endExitEmit(); + + verify(mockSpan).addEvent(eventNameCaptor.capture(), attributesCaptor.capture()); + Attributes attributes = attributesCaptor.getValue(); + + assertEquals("sw.profile", eventNameCaptor.getValue()); + assertEquals(0, attributes.size()); + } + + @Test + @DisplayName("endSampleEmit should add event with empty attributes when no attributes added") + void endSampleEmitShouldAddEventWithEmptyAttributes() { + sampleEmitter.beginSampleEmit(); + sampleEmitter.endSampleEmit(); + + verify(mockSpan).addEvent(eventNameCaptor.capture(), attributesCaptor.capture()); + Attributes attributes = attributesCaptor.getValue(); + + assertEquals("sw.profile", eventNameCaptor.getValue()); + assertEquals(0, attributes.size()); + } +} diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts index e39abca0..51e3656b 100644 --- a/dependencyManagement/build.gradle.kts +++ b/dependencyManagement/build.gradle.kts @@ -7,7 +7,7 @@ val otelSdkVersion = "1.54.1" val mockitoVersion = "4.11.0" val byteBuddyVersion = "1.15.10" -val joboeVersion = "10.0.23" +val joboeVersion = "10.0.24-cc.nh-119445-a0ec705-SNAPSHOT" val opentelemetryJavaagentAlpha = "$otelAgentVersion-alpha" val opentelemetryAlpha = "$otelSdkVersion-alpha"