diff --git a/.circleci/config.yml b/.circleci/config.yml index 6cb53048..5e3aee54 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,7 @@ version: 2.1 orbs: - lumigo-orb: &lumigo_orb_version lumigo/lumigo-orb@volatile + lumigo-orb: &lumigo_orb_version lumigo/lumigo-orb@dev:c7858b2 defaults: &defaults working_directory: ~/java-tracer diff --git a/README.md b/README.md index 68d54890..943bcf04 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,15 @@ There are 2 way to pass configuration properties Adding `LUMIGO_TRACER_TOKEN` environment variables +#### Execution Tags + +Execution tags can be added by utilizing `ExecutionTags` Module + +```java +ExecutionTags.addTag("key", "value", true); +``` + + #### Static code initiation ```java diff --git a/scripts/checks.sh b/scripts/checks.sh index 01a91bbe..149d29e0 100755 --- a/scripts/checks.sh +++ b/scripts/checks.sh @@ -1,6 +1,11 @@ #!/usr/bin/env bash set -eo pipefail -java -jar libs/google-java-format-1.7-all-deps.jar --set-exit-if-changed -i -a $(find . -type f -name "*.java" | grep ".*/src/.*java") +echo "Checking for formatting issues..." +if ! java -jar libs/google-java-format-1.7-all-deps.jar --set-exit-if-changed -i -a $(find . -type f -name "*.java" | grep ".*/src/.*java"); then + echo "some files were formatted, exiting" + exit 1 +fi + mvn -Djava.security.manager=allow -f agent/pom.xml clean package mvn -Djava.security.manager=allow clean package \ No newline at end of file diff --git a/src/main/java/io/lumigo/core/ExecutionTags.java b/src/main/java/io/lumigo/core/ExecutionTags.java new file mode 100644 index 00000000..9ecb28a6 --- /dev/null +++ b/src/main/java/io/lumigo/core/ExecutionTags.java @@ -0,0 +1,84 @@ +package io.lumigo.core; + +import io.lumigo.models.Span.ExecutionTag; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.pmw.tinylog.Logger; + +public final class ExecutionTags { + private static final int MAX_TAG_KEY_LEN = 100; + private static final int MAX_TAG_VALUE_LEN = 100; + private static final int MAX_TAGS = 50; + private static final String ADD_TAG_ERROR_MSG_PREFIX = "Error adding tag"; + private final List tags = new ArrayList(); + + private static final ExecutionTags ourInstance = new ExecutionTags(); + + private ExecutionTags() {} + + public static void add(String key, String value) { + getInstance().addTagInternal(key, value); + } + + // Package-private methods for internal consumption + static ExecutionTags getInstance() { + return ourInstance; + } + + static List getTags() { + return Collections.unmodifiableList(new ArrayList<>(getInstance().tags)); + } + + static void clear() { + getInstance().tags.clear(); + } + + private boolean validateTag(String key, String value) { + key = String.valueOf(key); + value = String.valueOf(value); + if (key.isEmpty() || key.length() > MAX_TAG_KEY_LEN) { + Logger.debug( + String.format( + "%s: key length should be between 1 and %d: %s - %s", + ADD_TAG_ERROR_MSG_PREFIX, MAX_TAG_KEY_LEN, key, value)); + return false; + } + if (value.isEmpty() || value.length() > MAX_TAG_VALUE_LEN) { + Logger.debug( + String.format( + "%s: value length should be between 1 and %d: %s - %s", + ADD_TAG_ERROR_MSG_PREFIX, MAX_TAG_VALUE_LEN, key, value)); + return false; + } + if (tags.size() >= MAX_TAGS) { + Logger.debug( + String.format( + "%s: maximum number of tags is %d: %s - %s", + ADD_TAG_ERROR_MSG_PREFIX, MAX_TAGS, key, value)); + return false; + } + return true; + } + + private String normalizeTag(Object val) { + return (val == null) ? null : String.valueOf(val); + } + + private void addTagInternal(String key, String value) { + try { + Logger.debug(String.format("Adding tag: %s - %s", key, value)); + if (!validateTag(key, value)) { + Logger.debug(String.format("Invalid tag not added: %s - %s", key, value)); + return; + } + tags.add( + ExecutionTag.builder() + .key(normalizeTag(key)) + .value(normalizeTag(value)) + .build()); + } catch (Exception err) { + Logger.error(String.format("%s - %s", ADD_TAG_ERROR_MSG_PREFIX, err.getMessage())); + } + } +} diff --git a/src/main/java/io/lumigo/core/SpansContainer.java b/src/main/java/io/lumigo/core/SpansContainer.java index 07813efb..46ec7a8f 100644 --- a/src/main/java/io/lumigo/core/SpansContainer.java +++ b/src/main/java/io/lumigo/core/SpansContainer.java @@ -74,6 +74,7 @@ public void clear() { endFunctionSpan = null; reporter = null; spans = new LinkedList<>(); + ExecutionTags.clear(); } private SpansContainer() {} @@ -219,6 +220,12 @@ private void end(Span endFunctionSpan) throws IOException { .reporter_rtt(rttDuration) .ended(System.currentTimeMillis()) .id(this.baseSpan.getId()) + .info( + endFunctionSpan + .getInfo() + .toBuilder() + .tags(ExecutionTags.getTags()) + .build()) .build(); reporter.reportSpans( prepareToSend(getAllCollectedSpans(), endFunctionSpan.getError() != null), @@ -430,7 +437,6 @@ public void addHttpSpan( .statusCode(context.httpResponse().statusCode()) .build()) .build()); - Logger.debug( "Trying to extract aws custom properties for service: " + executionAttributes.getAttribute(SdkExecutionAttribute.SERVICE_NAME)); diff --git a/src/main/java/io/lumigo/models/Span.java b/src/main/java/io/lumigo/models/Span.java index f1511840..1ce60486 100644 --- a/src/main/java/io/lumigo/models/Span.java +++ b/src/main/java/io/lumigo/models/Span.java @@ -53,6 +53,7 @@ public static class Info { private String stage; private String messageId; private List messageIds; + private List tags; private long approxEventCreationTime; } @@ -80,6 +81,14 @@ public static class Error { private String stacktrace; } + @AllArgsConstructor + @Builder(toBuilder = true) + @Data(staticConstructor = "of") + public static class ExecutionTag { + private String key; + private String value; + } + public enum READINESS { WARM, COLD; diff --git a/src/test/java/io/lumigo/handlers/LumigoRequestHandlerTest.java b/src/test/java/io/lumigo/handlers/LumigoRequestHandlerTest.java index 8fdf5863..5cedbad9 100644 --- a/src/test/java/io/lumigo/handlers/LumigoRequestHandlerTest.java +++ b/src/test/java/io/lumigo/handlers/LumigoRequestHandlerTest.java @@ -10,6 +10,7 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.KinesisEvent; import com.fasterxml.jackson.core.JsonProcessingException; +import io.lumigo.core.ExecutionTags; import io.lumigo.core.SpansContainer; import io.lumigo.core.configuration.Configuration; import io.lumigo.core.network.Reporter; @@ -246,6 +247,7 @@ public void LumigoRequestHandler_with_response_happy_flow() throws Exception { new Customization("info.tracer.version", (o1, o2) -> o2 != null), new Customization("info.messageId", (o1, o2) -> o2 != null), new Customization("info.messageIds", (o1, o2) -> o2 != null), + new Customization("info.tags", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), new Customization("ended", (o1, o2) -> o2 != null), @@ -262,6 +264,7 @@ public void LumigoRequestHandler_with_response_happy_flow() throws Exception { new Customization("info.tracer.version", (o1, o2) -> o2 != null), new Customization("info.messageId", (o1, o2) -> o2 != null), new Customization("info.messageIds", (o1, o2) -> o2 != null), + new Customization("info.tags", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), new Customization("ended", (o1, o2) -> o2 != null), @@ -295,6 +298,7 @@ public void LumigoRequestHandler_with_exception_happy_flow() throws Exception { new Customization("info.tracer.version", (o1, o2) -> o2 != null), new Customization("info.messageId", (o1, o2) -> o2 != null), new Customization("info.messageIds", (o1, o2) -> o2 != null), + new Customization("info.tags", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), new Customization("ended", (o1, o2) -> o2 != null), @@ -310,6 +314,7 @@ public void LumigoRequestHandler_with_exception_happy_flow() throws Exception { new Customization("info.tracer.version", (o1, o2) -> o2 != null), new Customization("info.messageId", (o1, o2) -> o2 != null), new Customization("info.messageIds", (o1, o2) -> o2 != null), + new Customization("info.tags", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("error.stacktrace", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), @@ -349,6 +354,7 @@ public void LumigoRequestHandler_with_inline_configuration_return_response_happy new Customization("info.tracer.version", (o1, o2) -> o2 != null), new Customization("info.messageId", (o1, o2) -> o2 != null), new Customization("info.messageIds", (o1, o2) -> o2 != null), + new Customization("info.tags", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), new Customization("ended", (o1, o2) -> o2 != null))); @@ -368,6 +374,7 @@ public void LumigoRequestHandler_with_inline_configuration_return_response_happy new Customization("info.tracer.version", (o1, o2) -> o2 != null), new Customization("info.messageId", (o1, o2) -> o2 != null), new Customization("info.messageIds", (o1, o2) -> o2 != null), + new Customization("info.tags", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), new Customization("error.stacktrace", (o1, o2) -> o2 != null), @@ -465,6 +472,7 @@ public void LumigoRequestStreamHandler_happy_flow_response() throws Exception { new CustomComparator( JSONCompareMode.LENIENT, new Customization("info.tracer.version", (o1, o2) -> o2 != null), + new Customization("info.tags", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), new Customization("ended", (o1, o2) -> o2 != null), @@ -482,6 +490,7 @@ public void LumigoRequestStreamHandler_happy_flow_response() throws Exception { new CustomComparator( JSONCompareMode.LENIENT, new Customization("info.tracer.version", (o1, o2) -> o2 != null), + new Customization("info.tags", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), new Customization("ended", (o1, o2) -> o2 != null), @@ -528,6 +537,7 @@ public void LumigoRequestStreamHandler_happy_flow_error() throws Exception { new CustomComparator( JSONCompareMode.LENIENT, new Customization("info.tracer.version", (o1, o2) -> o2 != null), + new Customization("info.tags", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("error.stacktrace", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), @@ -563,6 +573,7 @@ public void LumigoRequestStreamHandler_happy_with_inline_configuration() throws new CustomComparator( JSONCompareMode.LENIENT, new Customization("info.tracer.version", (o1, o2) -> o2 != null), + new Customization("info.tags", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), new Customization("ended", (o1, o2) -> o2 != null))); @@ -581,6 +592,7 @@ public void LumigoRequestStreamHandler_happy_with_inline_configuration() throws new CustomComparator( JSONCompareMode.LENIENT, new Customization("info.tracer.version", (o1, o2) -> o2 != null), + new Customization("info.tags", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("error.stacktrace", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), @@ -777,6 +789,7 @@ public void LumigoRequestExecutor_with_response_happy_flow() throws Exception { new Customization("info.tracer.version", (o1, o2) -> o2 != null), new Customization("info.messageId", (o1, o2) -> o2 != null), new Customization("info.messageIds", (o1, o2) -> o2 != null), + new Customization("info.tags", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), new Customization("ended", (o1, o2) -> o2 != null), @@ -810,6 +823,7 @@ public void LumigoRequestExecutor_with_exception_happy_flow() throws Exception { new Customization("info.tracer.version", (o1, o2) -> o2 != null), new Customization("info.messageId", (o1, o2) -> o2 != null), new Customization("info.messageIds", (o1, o2) -> o2 != null), + new Customization("info.tags", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("ended", (o1, o2) -> o2 != null), @@ -825,12 +839,14 @@ public void LumigoRequestExecutor_with_exception_happy_flow() throws Exception { new Customization("info.tracer.version", (o1, o2) -> o2 != null), new Customization("info.messageId", (o1, o2) -> o2 != null), new Customization("info.messageIds", (o1, o2) -> o2 != null), + new Customization("info.tags", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("error.stacktrace", (o1, o2) -> o2 != null), new Customization("ended", (o1, o2) -> o2 != null), new Customization("event", JsonTestUtils::compareJsonStrings), - new Customization("envs", JsonTestUtils::compareJsonStrings))); + new Customization("envs", JsonTestUtils::compareJsonStrings), + new Customization("info.tags", JsonTestUtils::compareJsonStrings))); } @DisplayName( @@ -883,6 +899,7 @@ public void LumigoRequestExecutor_with_inline_configuration_return_reponse_happy new Customization("info.tracer.version", (o1, o2) -> o2 != null), new Customization("info.messageId", (o1, o2) -> o2 != null), new Customization("info.messageIds", (o1, o2) -> o2 != null), + new Customization("info.tags", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("error.stacktrace", (o1, o2) -> o2 != null), @@ -963,4 +980,79 @@ public void LumigoRequestExecutor_with_return_value_void() { verify(spansContainerMock, Mockito.times(0)).start(); } + + static class HandlerWithExecutionTags implements RequestHandler { + @Override + public String handleRequest(KinesisEvent kinesisEvent, Context context) { + // Add execution tags during the request handling + ExecutionTags.add("user_id", "12345"); + ExecutionTags.add("user_type", "admin"); + return "Response with tags"; + } + } + + @DisplayName( + "Create a handler that adds execution tags and verifies they are present in the end span") + @Test + public void LumigoRequestHandler_with_execution_tags() throws Exception { + HandlerWithExecutionTags handler = new HandlerWithExecutionTags(); + LumigoRequestExecutor.getInstance().setEnvUtil(envUtil); + LumigoRequestExecutor.getInstance().setReporter(reporter); + Configuration.getInstance().setEnvUtil(envUtil); + when(reporter.reportSpans((Span) any(), anyInt())).thenReturn(999L); + + String response = + LumigoRequestExecutor.execute( + kinesisEvent, context, () -> handler.handleRequest(kinesisEvent, context)); + + ArgumentCaptor argumentCaptorAllSpans = ArgumentCaptor.forClass(List.class); + ArgumentCaptor argumentCaptorStartSpan = ArgumentCaptor.forClass(Span.class); + verify(reporter, Mockito.times(1)).reportSpans(argumentCaptorAllSpans.capture(), anyInt()); + verify(reporter, Mockito.times(1)).reportSpans(argumentCaptorStartSpan.capture(), anyInt()); + + assertEquals("Response with tags", response); + + // Get the end span from the captured arguments + List allSpans = argumentCaptorAllSpans.getAllValues().get(0); + Span endSpan = (Span) allSpans.get(0); + + // Verify that execution tags are present in the end span + assertNotNull(endSpan.getInfo().getTags(), "Execution tags should not be null"); + assertEquals(2, endSpan.getInfo().getTags().size(), "Should have 2 execution tags"); + + // Verify specific tags are present + boolean hasUserId = + endSpan.getInfo().getTags().stream() + .anyMatch( + tag -> + "user_id".equals(tag.getKey()) + && "12345".equals(tag.getValue())); + assertTrue(hasUserId, "Should have user_id tag with value 12345"); + + boolean hasUserType = + endSpan.getInfo().getTags().stream() + .anyMatch( + tag -> + "user_type".equals(tag.getKey()) + && "admin".equals(tag.getValue())); + assertTrue(hasUserType, "Should have user_type tag with value admin"); + + // Verify the span structure matches expected format + Span expectedEndSpan = getEndSpan("Response with tags", null); + expectedEndSpan.setReporter_rtt(999L); // Set the expected reporter_rtt value + JSONAssert.assertEquals( + JsonUtils.getObjectAsJsonString(expectedEndSpan), + JsonUtils.getObjectAsJsonString(endSpan), + new CustomComparator( + JSONCompareMode.LENIENT, + new Customization("info.tracer.version", (o1, o2) -> o2 != null), + new Customization("info.messageId", (o1, o2) -> o2 != null), + new Customization("info.messageIds", (o1, o2) -> o2 != null), + new Customization("info.tags", (o1, o2) -> o2 != null), + new Customization("started", (o1, o2) -> o2 != null), + new Customization("maxFinishTime", (o1, o2) -> o2 != null), + new Customization("ended", (o1, o2) -> o2 != null), + new Customization("event", JsonTestUtils::compareJsonStrings), + new Customization("envs", JsonTestUtils::compareJsonStrings))); + } }