-
Notifications
You must be signed in to change notification settings - Fork 175
[dynamic control] Add json and keyvalue parsing and mapping #2655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
a9da56a
json and keyvalue parsing
jackshirazi 0d05098
Update dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic…
jackshirazi 3eecaba
Update dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic…
jackshirazi 509fc69
spotless
jackshirazi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
92 changes: 92 additions & 0 deletions
92
...ntrol/src/main/java/io/opentelemetry/contrib/dynamic/policy/source/JsonSourceWrapper.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| /* | ||
| * Copyright The OpenTelemetry Authors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package io.opentelemetry.contrib.dynamic.policy.source; | ||
|
|
||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Objects; | ||
| import javax.annotation.Nullable; | ||
|
|
||
| /** JSON-backed source wrapper for a single-policy object. */ | ||
| public final class JsonSourceWrapper implements SourceWrapper { | ||
| private static final ObjectMapper MAPPER = new ObjectMapper(); | ||
| private final JsonNode source; | ||
|
|
||
| public JsonSourceWrapper(JsonNode source) { | ||
| this.source = Objects.requireNonNull(source, "source cannot be null"); | ||
| } | ||
|
|
||
| @Override | ||
| public SourceFormat getFormat() { | ||
| return SourceFormat.JSON; | ||
| } | ||
|
|
||
| @Override | ||
| @Nullable | ||
| public String getPolicyType() { | ||
| JsonNode node = asJsonNode(); | ||
| if (!node.isObject() || node.size() != 1) { | ||
| return null; | ||
| } | ||
| return node.fieldNames().next(); | ||
| } | ||
|
|
||
| public JsonNode asJsonNode() { | ||
| return source; | ||
| } | ||
|
|
||
| /** | ||
| * Parses JSON source into one wrapper per top-level policy object. | ||
| * | ||
| * @return an empty list if the source is an empty JSON array; a non-empty list of wrappers if the | ||
| * source is a valid single-policy object or array thereof; or {@code null} if the shape is | ||
| * unsupported or the source is not valid JSON. | ||
| * @throws NullPointerException if source is null | ||
| */ | ||
| @Nullable | ||
| public static List<SourceWrapper> parse(String source) { | ||
| Objects.requireNonNull(source, "source cannot be null"); | ||
| try { | ||
| JsonNode parsed = MAPPER.readTree(source); | ||
| if (!isSupportedJsonShape(parsed)) { | ||
| return null; | ||
| } | ||
| if (parsed.isObject()) { | ||
| return Collections.singletonList(new JsonSourceWrapper(parsed)); | ||
| } | ||
| List<SourceWrapper> wrappers = new ArrayList<>(); | ||
| for (JsonNode element : parsed) { | ||
| wrappers.add(new JsonSourceWrapper(element)); | ||
| } | ||
| return Collections.unmodifiableList(wrappers); | ||
| } catch (JsonProcessingException e) { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| private static boolean isSupportedJsonShape(JsonNode node) { | ||
| if (node.isObject()) { | ||
| return isSinglePolicyObject(node); | ||
| } | ||
| if (!node.isArray()) { | ||
| return false; | ||
| } | ||
| for (JsonNode element : node) { | ||
| if (!isSinglePolicyObject(element)) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| private static boolean isSinglePolicyObject(JsonNode node) { | ||
| return node.isObject() && node.size() == 1; | ||
| } | ||
| } |
88 changes: 88 additions & 0 deletions
88
...l/src/main/java/io/opentelemetry/contrib/dynamic/policy/source/KeyValueSourceWrapper.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| /* | ||
| * Copyright The OpenTelemetry Authors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package io.opentelemetry.contrib.dynamic.policy.source; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Objects; | ||
| import java.util.regex.Pattern; | ||
| import javax.annotation.Nullable; | ||
|
|
||
| /** KEYVALUE-backed source wrapper for a single key/value policy entry. */ | ||
| public final class KeyValueSourceWrapper implements SourceWrapper { | ||
| private final String key; | ||
| private final String value; | ||
|
|
||
| private static final Pattern LINE_SEPARATOR = Pattern.compile("\\R"); | ||
|
|
||
| public KeyValueSourceWrapper(String key, String value) { | ||
| this.key = Objects.requireNonNull(key, "key cannot be null"); | ||
| this.value = Objects.requireNonNull(value, "value cannot be null"); | ||
| } | ||
|
|
||
| @Override | ||
| public SourceFormat getFormat() { | ||
| return SourceFormat.KEYVALUE; | ||
| } | ||
|
|
||
| @Override | ||
| @Nullable | ||
| public String getPolicyType() { | ||
| return key; | ||
| } | ||
|
|
||
| public String getKey() { | ||
| return key; | ||
| } | ||
|
|
||
| public String getValue() { | ||
| return value; | ||
| } | ||
|
|
||
| /** | ||
| * Parses KEYVALUE source into one wrapper per non-empty line. | ||
| * | ||
| * @return an empty list if the source contains no non-blank lines; a non-empty list of wrappers | ||
| * if all non-blank lines are valid key=value pairs; or {@code null} if any line is malformed. | ||
| * @throws NullPointerException if source is null | ||
| */ | ||
| @Nullable | ||
| public static List<SourceWrapper> parse(String source) { | ||
| Objects.requireNonNull(source, "source cannot be null"); | ||
| String[] lines = LINE_SEPARATOR.split(source, -1); | ||
| List<SourceWrapper> wrappers = new ArrayList<>(); | ||
| for (String rawLine : lines) { | ||
| String trimmedLine = rawLine.trim(); | ||
| if (trimmedLine.isEmpty()) { | ||
| continue; | ||
| } | ||
| KeyValueSourceWrapper wrapper = parseSingleKeyValue(trimmedLine); | ||
| if (wrapper == null) { | ||
| return null; | ||
| } | ||
| wrappers.add(wrapper); | ||
| } | ||
jackshirazi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (wrappers.isEmpty()) { | ||
| return Collections.emptyList(); | ||
| } | ||
| return Collections.unmodifiableList(wrappers); | ||
| } | ||
|
|
||
| @Nullable | ||
| private static KeyValueSourceWrapper parseSingleKeyValue(String line) { | ||
| int separatorIndex = line.indexOf('='); | ||
| if (separatorIndex <= 0) { | ||
| return null; | ||
| } | ||
| String key = line.substring(0, separatorIndex).trim(); | ||
| String value = line.substring(separatorIndex + 1).trim(); | ||
| if (key.isEmpty()) { | ||
| return null; | ||
| } | ||
| return new KeyValueSourceWrapper(key, value); | ||
| } | ||
| } | ||
50 changes: 50 additions & 0 deletions
50
...ic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/source/SourceFormat.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| /* | ||
| * Copyright The OpenTelemetry Authors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package io.opentelemetry.contrib.dynamic.policy.source; | ||
|
|
||
| import com.google.errorprone.annotations.Immutable; | ||
| import java.util.List; | ||
| import java.util.Objects; | ||
| import javax.annotation.Nullable; | ||
|
|
||
| /** Supported source formats and their parser dispatch. */ | ||
| public enum SourceFormat { | ||
| KEYVALUE("keyvalue", KeyValueSourceWrapper::parse), | ||
| JSON("json", JsonSourceWrapper::parse); | ||
|
|
||
| private final String configValue; | ||
| private final SourceParser parser; | ||
|
|
||
| SourceFormat(String configValue, SourceParser parser) { | ||
| this.configValue = configValue; | ||
| this.parser = parser; | ||
| } | ||
|
|
||
| public String configValue() { | ||
| return configValue; | ||
| } | ||
|
|
||
| /** | ||
| * Parses source text into normalized wrappers for this format. | ||
| * | ||
| * @return an empty list if the source is valid but contains no policies; a non-empty list of | ||
| * wrappers if one or more policies were parsed successfully; or {@code null} if the source is | ||
| * malformed or does not conform to the expected shape for this format. | ||
| * @throws NullPointerException if source is null | ||
| */ | ||
| @Nullable | ||
| public List<SourceWrapper> parse(String source) { | ||
| Objects.requireNonNull(source, "source cannot be null"); | ||
| return parser.parse(source); | ||
| } | ||
|
|
||
| @Immutable | ||
| @FunctionalInterface | ||
| private interface SourceParser { | ||
| @Nullable | ||
| List<SourceWrapper> parse(String source); | ||
| } | ||
| } |
16 changes: 16 additions & 0 deletions
16
...c-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/source/SourceWrapper.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| /* | ||
| * Copyright The OpenTelemetry Authors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package io.opentelemetry.contrib.dynamic.policy.source; | ||
|
|
||
| import javax.annotation.Nullable; | ||
|
|
||
| /** Parsed source payload paired with its source format. */ | ||
| public interface SourceWrapper { | ||
| SourceFormat getFormat(); | ||
|
|
||
| @Nullable | ||
| String getPolicyType(); | ||
| } |
74 changes: 74 additions & 0 deletions
74
...l/src/test/java/io/opentelemetry/contrib/dynamic/policy/source/JsonSourceWrapperTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| /* | ||
| * Copyright The OpenTelemetry Authors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package io.opentelemetry.contrib.dynamic.policy.source; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
|
|
||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import java.util.List; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| class JsonSourceWrapperTest { | ||
| private static final ObjectMapper MAPPER = new ObjectMapper(); | ||
|
|
||
| @Test | ||
| void parseSupportsSingleObject() { | ||
| List<SourceWrapper> parsed = JsonSourceWrapper.parse("{\"trace-sampling\": 0.5}"); | ||
|
|
||
| assertThat(parsed).hasSize(1); | ||
| assertThat(parsed.get(0)).isInstanceOf(JsonSourceWrapper.class); | ||
| assertThat(parsed.get(0).getPolicyType()).isEqualTo("trace-sampling"); | ||
| } | ||
|
|
||
| @Test | ||
| void parseSupportsArrayOfObjects() { | ||
| List<SourceWrapper> parsed = | ||
| JsonSourceWrapper.parse("[{\"other-policy\": 1}, {\"trace-sampling\": 0.5}]"); | ||
|
|
||
| assertThat(parsed).hasSize(2); | ||
| assertThat(parsed.get(0).getPolicyType()).isEqualTo("other-policy"); | ||
| assertThat(parsed.get(1).getPolicyType()).isEqualTo("trace-sampling"); | ||
| } | ||
|
|
||
| @Test | ||
| void parseSupportsEmptyArray() { | ||
| assertThat(JsonSourceWrapper.parse("[]")).isEmpty(); | ||
| } | ||
|
|
||
| @Test | ||
| void parseArrayResultIsImmutable() { | ||
| List<SourceWrapper> parsed = JsonSourceWrapper.parse("[{\"trace-sampling\": 0.5}]"); | ||
|
|
||
| assertThatThrownBy(() -> parsed.add(new JsonSourceWrapper(MAPPER.readTree("{\"x\":1}")))) | ||
| .isInstanceOf(UnsupportedOperationException.class); | ||
| } | ||
|
|
||
| @Test | ||
| void getPolicyTypeReturnsNullWhenObjectHasMultipleFields() throws Exception { | ||
| JsonSourceWrapper wrapper = new JsonSourceWrapper(MAPPER.readTree("{\"a\": 1, \"b\": 2}")); | ||
|
|
||
| assertThat(wrapper.getPolicyType()).isNull(); | ||
| } | ||
|
|
||
| @Test | ||
| void parseRejectsUnsupportedJsonShapes() { | ||
| assertThat(JsonSourceWrapper.parse("{}")).isNull(); | ||
| assertThat(JsonSourceWrapper.parse("{\"a\": 1, \"b\": 2}")).isNull(); | ||
| assertThat(JsonSourceWrapper.parse("[1, 2, 3]")).isNull(); | ||
| assertThat(JsonSourceWrapper.parse("[{\"trace-sampling\": 0.5}, {}]")).isNull(); | ||
| assertThat(JsonSourceWrapper.parse("[{\"a\": 1, \"b\": 2}]")).isNull(); | ||
| assertThat(JsonSourceWrapper.parse("\"text\"")).isNull(); | ||
| assertThat(JsonSourceWrapper.parse("{invalid-json")).isNull(); | ||
| } | ||
|
|
||
| @Test | ||
| void parseRejectsNullInput() { | ||
| assertThatThrownBy(() -> JsonSourceWrapper.parse(null)) | ||
| .isInstanceOf(NullPointerException.class) | ||
| .hasMessage("source cannot be null"); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.