Skip to content

Commit 5a76ebb

Browse files
authored
[dynamic control] add config parsing for both json and yaml (#2738)
1 parent c4102e5 commit 5a76ebb

File tree

8 files changed

+357
-0
lines changed

8 files changed

+357
-0
lines changed

dynamic-control/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ dependencies {
1717
compileOnly("com.google.auto.service:auto-service-annotations")
1818

1919
implementation("com.fasterxml.jackson.core:jackson-databind")
20+
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
2021

2122
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
2223
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.dynamic.policy.registry.json;
7+
8+
import com.fasterxml.jackson.databind.JsonNode;
9+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
10+
import io.opentelemetry.contrib.dynamic.policy.registry.PolicyInitConfig;
11+
import io.opentelemetry.contrib.dynamic.policy.registry.PolicySourceConfig;
12+
import io.opentelemetry.contrib.dynamic.policy.registry.PolicySourceMappingConfig;
13+
import io.opentelemetry.contrib.dynamic.policy.source.SourceFormat;
14+
import io.opentelemetry.contrib.dynamic.policy.source.SourceKind;
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
18+
/** Shared JsonNode-to-model parser for registry init configuration. */
19+
public final class JsonNodePolicyInitConfigParser {
20+
private JsonNodePolicyInitConfigParser() {}
21+
22+
public static PolicyInitConfig parse(JsonNode root) {
23+
if (root == null) {
24+
throw new IllegalArgumentException("Config payload cannot be empty.");
25+
}
26+
JsonNode sourcesNode =
27+
requireArray(root.get("sources"), "Config must contain a 'sources' array.");
28+
List<PolicySourceConfig> sources = new ArrayList<>();
29+
for (JsonNode sourceNode : sourcesNode) {
30+
sources.add(parseSource(sourceNode));
31+
}
32+
return new PolicyInitConfig(sources);
33+
}
34+
35+
private static PolicySourceConfig parseSource(JsonNode node) {
36+
JsonNode objectNode = requireObject(node, "Each source entry must be an object.");
37+
38+
String kindValue =
39+
requireText(objectNode.get("kind"), "Each source must define string 'kind'.");
40+
String formatValue =
41+
requireText(objectNode.get("format"), "Each source must define string 'format'.");
42+
43+
SourceKind kind = SourceKind.fromConfigValue(kindValue);
44+
SourceFormat format = SourceFormat.fromConfigValue(formatValue);
45+
46+
JsonNode locationNode = objectNode.get("location");
47+
String location =
48+
locationNode != null && locationNode.isTextual() ? locationNode.asText() : null;
49+
50+
JsonNode mappingsNode =
51+
requireArray(objectNode.get("mappings"), "Each source must define a 'mappings' array.");
52+
List<PolicySourceMappingConfig> mappings = new ArrayList<>();
53+
for (JsonNode mappingNode : mappingsNode) {
54+
mappings.add(parseMapping(mappingNode));
55+
}
56+
return new PolicySourceConfig(kind, format, location, mappings);
57+
}
58+
59+
private static PolicySourceMappingConfig parseMapping(JsonNode node) {
60+
JsonNode objectNode = requireObject(node, "Each mapping entry must be an object.");
61+
String sourceKey =
62+
requireText(objectNode.get("sourceKey"), "Each mapping must define string 'sourceKey'.");
63+
String policyType =
64+
requireText(objectNode.get("policyType"), "Each mapping must define string 'policyType'.");
65+
return new PolicySourceMappingConfig(sourceKey, policyType);
66+
}
67+
68+
@CanIgnoreReturnValue
69+
private static JsonNode requireArray(JsonNode node, String message) {
70+
if (node == null || !node.isArray()) {
71+
throw new IllegalArgumentException(message);
72+
}
73+
return node;
74+
}
75+
76+
@CanIgnoreReturnValue
77+
private static JsonNode requireObject(JsonNode node, String message) {
78+
if (node == null || !node.isObject()) {
79+
throw new IllegalArgumentException(message);
80+
}
81+
return node;
82+
}
83+
84+
private static String requireText(JsonNode node, String message) {
85+
if (node == null || !node.isTextual()) {
86+
throw new IllegalArgumentException(message);
87+
}
88+
String value = node.asText();
89+
if (value.isEmpty()) {
90+
throw new IllegalArgumentException(message);
91+
}
92+
return value;
93+
}
94+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.dynamic.policy.registry.json;
7+
8+
import com.fasterxml.jackson.databind.JsonNode;
9+
import com.fasterxml.jackson.databind.ObjectMapper;
10+
import io.opentelemetry.contrib.dynamic.policy.registry.PolicyInitConfig;
11+
import java.io.ByteArrayInputStream;
12+
import java.io.IOException;
13+
import java.io.InputStream;
14+
import java.nio.charset.StandardCharsets;
15+
import java.util.Objects;
16+
17+
/** Reads {@link PolicyInitConfig} from JSON (registry initialization file or payload). */
18+
public final class JsonPolicyInitConfigReader {
19+
20+
private static final ObjectMapper MAPPER = new ObjectMapper();
21+
22+
private JsonPolicyInitConfigReader() {}
23+
24+
public static PolicyInitConfig read(String json) throws IOException {
25+
Objects.requireNonNull(json, "json cannot be null");
26+
return read(new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)));
27+
}
28+
29+
public static PolicyInitConfig read(InputStream in) throws IOException {
30+
Objects.requireNonNull(in, "in cannot be null");
31+
JsonNode root = MAPPER.readTree(in);
32+
return JsonNodePolicyInitConfigParser.parse(root);
33+
}
34+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.dynamic.policy.registry.yaml;
7+
8+
import com.fasterxml.jackson.databind.JsonNode;
9+
import com.fasterxml.jackson.databind.ObjectMapper;
10+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
11+
import io.opentelemetry.contrib.dynamic.policy.registry.PolicyInitConfig;
12+
import io.opentelemetry.contrib.dynamic.policy.registry.json.JsonNodePolicyInitConfigParser;
13+
import java.io.ByteArrayInputStream;
14+
import java.io.IOException;
15+
import java.io.InputStream;
16+
import java.nio.charset.StandardCharsets;
17+
import java.util.Objects;
18+
19+
/** Reads {@link PolicyInitConfig} from YAML (registry initialization file or payload). */
20+
public final class YamlPolicyInitConfigReader {
21+
22+
private static final ObjectMapper MAPPER = new ObjectMapper(new YAMLFactory());
23+
24+
private YamlPolicyInitConfigReader() {}
25+
26+
public static PolicyInitConfig read(InputStream in) throws IOException {
27+
Objects.requireNonNull(in, "in cannot be null");
28+
JsonNode root = MAPPER.readTree(in);
29+
return JsonNodePolicyInitConfigParser.parse(root);
30+
}
31+
32+
public static PolicyInitConfig read(String yaml) throws IOException {
33+
Objects.requireNonNull(yaml, "yaml cannot be null");
34+
return read(new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8)));
35+
}
36+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.dynamic.policy.registry.json;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
10+
11+
import io.opentelemetry.contrib.dynamic.policy.registry.PolicyInitConfig;
12+
import io.opentelemetry.contrib.dynamic.policy.registry.PolicySourceConfig;
13+
import io.opentelemetry.contrib.dynamic.policy.source.SourceFormat;
14+
import io.opentelemetry.contrib.dynamic.policy.source.SourceKind;
15+
import java.io.InputStream;
16+
import org.junit.jupiter.api.Test;
17+
18+
class JsonPolicyInitConfigReaderTest {
19+
20+
private static final String EXAMPLE_FIXTURE =
21+
"/io/opentelemetry/contrib/dynamic/policy/registry/json/policy-init-example.json";
22+
23+
@Test
24+
void readsSourceCentricFixture() throws Exception {
25+
try (InputStream in = getClass().getResourceAsStream(EXAMPLE_FIXTURE)) {
26+
assertThat(in).isNotNull();
27+
28+
PolicyInitConfig config = JsonPolicyInitConfigReader.read(in);
29+
assertThat(config.getSources()).hasSize(1);
30+
31+
PolicySourceConfig source = config.getSources().get(0);
32+
assertThat(source.getKind()).isEqualTo(SourceKind.OPAMP);
33+
assertThat(source.getFormat()).isEqualTo(SourceFormat.JSONKEYVALUE);
34+
assertThat(source.getLocation()).isEqualTo("vendor-specific");
35+
assertThat(source.getMappings()).hasSize(4);
36+
assertThat(source.getMappings().get(0).getSourceKey()).isEqualTo("sampling_rate");
37+
assertThat(source.getMappings().get(0).getPolicyType())
38+
.isEqualTo("trace_sampling_rate_policy");
39+
}
40+
}
41+
42+
@Test
43+
void missingSourcesThrows() {
44+
assertThatThrownBy(() -> JsonPolicyInitConfigReader.read("{}"))
45+
.isInstanceOf(IllegalArgumentException.class)
46+
.hasMessageContaining("sources");
47+
}
48+
49+
@Test
50+
void emptyPayloadThrows() {
51+
assertThatThrownBy(() -> JsonPolicyInitConfigReader.read(""))
52+
.isInstanceOf(IllegalArgumentException.class)
53+
.hasMessageMatching("(?s).*(empty|sources).*");
54+
}
55+
56+
@Test
57+
void missingFormatThrows() {
58+
String json = "{\"sources\":[{\"kind\":\"opamp\",\"mappings\":[]}]}";
59+
assertThatThrownBy(() -> JsonPolicyInitConfigReader.read(json))
60+
.isInstanceOf(IllegalArgumentException.class)
61+
.hasMessageContaining("format");
62+
}
63+
64+
@Test
65+
void missingMappingsThrows() {
66+
String json = "{\"sources\":[{\"kind\":\"opamp\",\"format\":\"jsonkeyvalue\"}]}";
67+
assertThatThrownBy(() -> JsonPolicyInitConfigReader.read(json))
68+
.isInstanceOf(IllegalArgumentException.class)
69+
.hasMessageContaining("mappings");
70+
}
71+
72+
@Test
73+
void mappingMissingSourceKeyThrows() {
74+
String json =
75+
"{\"sources\":[{\"kind\":\"opamp\",\"format\":\"jsonkeyvalue\",\"mappings\":[{\"policyType\":\"x\"}]}]}";
76+
assertThatThrownBy(() -> JsonPolicyInitConfigReader.read(json))
77+
.isInstanceOf(IllegalArgumentException.class)
78+
.hasMessageContaining("sourceKey");
79+
}
80+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.dynamic.policy.registry.yaml;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
10+
11+
import io.opentelemetry.contrib.dynamic.policy.registry.PolicyInitConfig;
12+
import io.opentelemetry.contrib.dynamic.policy.registry.PolicySourceConfig;
13+
import io.opentelemetry.contrib.dynamic.policy.source.SourceFormat;
14+
import io.opentelemetry.contrib.dynamic.policy.source.SourceKind;
15+
import java.io.InputStream;
16+
import org.junit.jupiter.api.Test;
17+
18+
class YamlPolicyInitConfigReaderTest {
19+
20+
private static final String EXAMPLE_FIXTURE =
21+
"/io/opentelemetry/contrib/dynamic/policy/registry/yaml/policy-init-example.yaml";
22+
23+
@Test
24+
void readsSourceCentricFixture() throws Exception {
25+
try (InputStream in = getClass().getResourceAsStream(EXAMPLE_FIXTURE)) {
26+
assertThat(in).isNotNull();
27+
28+
PolicyInitConfig config = YamlPolicyInitConfigReader.read(in);
29+
assertThat(config.getSources()).hasSize(1);
30+
31+
PolicySourceConfig source = config.getSources().get(0);
32+
assertThat(source.getKind()).isEqualTo(SourceKind.OPAMP);
33+
assertThat(source.getFormat()).isEqualTo(SourceFormat.JSONKEYVALUE);
34+
assertThat(source.getLocation()).isEqualTo("vendor-specific");
35+
assertThat(source.getMappings()).hasSize(4);
36+
assertThat(source.getMappings().get(0).getSourceKey()).isEqualTo("sampling_rate");
37+
assertThat(source.getMappings().get(0).getPolicyType())
38+
.isEqualTo("trace_sampling_rate_policy");
39+
}
40+
}
41+
42+
@Test
43+
void missingSourcesThrows() {
44+
assertThatThrownBy(() -> YamlPolicyInitConfigReader.read("{}"))
45+
.isInstanceOf(IllegalArgumentException.class)
46+
.hasMessageContaining("sources");
47+
}
48+
49+
@Test
50+
void emptyPayloadThrows() {
51+
assertThatThrownBy(() -> YamlPolicyInitConfigReader.read(""))
52+
.isInstanceOf(IllegalArgumentException.class)
53+
.hasMessageMatching("(?s).*(empty|sources).*");
54+
}
55+
56+
@Test
57+
void missingFormatThrows() {
58+
String yaml = "sources:\n - kind: opamp\n mappings: []\n";
59+
assertThatThrownBy(() -> YamlPolicyInitConfigReader.read(yaml))
60+
.isInstanceOf(IllegalArgumentException.class)
61+
.hasMessageContaining("format");
62+
}
63+
64+
@Test
65+
void missingMappingsThrows() {
66+
String yaml = "sources:\n - kind: opamp\n format: jsonkeyvalue\n";
67+
assertThatThrownBy(() -> YamlPolicyInitConfigReader.read(yaml))
68+
.isInstanceOf(IllegalArgumentException.class)
69+
.hasMessageContaining("mappings");
70+
}
71+
72+
@Test
73+
void mappingMissingSourceKeyThrows() {
74+
String yaml =
75+
"sources:\n"
76+
+ " - kind: opamp\n"
77+
+ " format: jsonkeyvalue\n"
78+
+ " mappings:\n"
79+
+ " - policyType: x\n";
80+
assertThatThrownBy(() -> YamlPolicyInitConfigReader.read(yaml))
81+
.isInstanceOf(IllegalArgumentException.class)
82+
.hasMessageContaining("sourceKey");
83+
}
84+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"sources": [
3+
{
4+
"kind": "opamp",
5+
"format": "jsonkeyvalue",
6+
"location": "vendor-specific",
7+
"mappings": [
8+
{ "sourceKey": "sampling_rate", "policyType": "trace_sampling_rate_policy" },
9+
{ "sourceKey": "send_logs", "policyType": "log_export_enabled_policy" },
10+
{ "sourceKey": "send_traces", "policyType": "trace_export_enabled_policy" },
11+
{ "sourceKey": "send_metrics", "policyType": "metric_export_enabled_policy" }
12+
]
13+
}
14+
]
15+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
sources:
2+
- kind: opamp
3+
format: jsonkeyvalue
4+
location: vendor-specific
5+
mappings:
6+
- sourceKey: sampling_rate
7+
policyType: trace_sampling_rate_policy
8+
- sourceKey: send_logs
9+
policyType: log_export_enabled_policy
10+
- sourceKey: send_traces
11+
policyType: trace_export_enabled_policy
12+
- sourceKey: send_metrics
13+
policyType: metric_export_enabled_policy

0 commit comments

Comments
 (0)