Skip to content

Commit f72f3f6

Browse files
[dynamic control] Add file reader with line-by-line policy provider (#2622)
Co-authored-by: Jay DeLuca <jaydeluca4@gmail.com>
1 parent e552c18 commit f72f3f6

File tree

3 files changed

+274
-0
lines changed

3 files changed

+274
-0
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.dynamic.policy;
7+
8+
import java.io.IOException;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
import java.util.Objects;
14+
import java.util.logging.Logger;
15+
import java.util.stream.Stream;
16+
17+
/**
18+
* A {@link PolicyProvider} that reads policies from a local file, where each line represents a
19+
* separate policy configuration.
20+
*
21+
* <p>The file format supports two types of lines:
22+
*
23+
* <ul>
24+
* <li><b>JSON Objects:</b> Lines starting with <code>{</code> are treated as JSON objects and
25+
* validated against the registered {@link PolicyValidator}s.
26+
* <li><b>Key-Value Pairs:</b> Lines in the format <code>key=value</code> are treated as aliases,
27+
* where the key matches a validator's {@link PolicyValidator#getAlias()} and the value is
28+
* parsed accordingly.
29+
* </ul>
30+
*
31+
* <p>Empty lines and lines starting with <code>#</code> are ignored.
32+
*/
33+
final class LinePerPolicyFileProvider implements PolicyProvider {
34+
private static final Logger logger = Logger.getLogger(LinePerPolicyFileProvider.class.getName());
35+
private final Path file;
36+
private final List<PolicyValidator> validators;
37+
38+
public LinePerPolicyFileProvider(Path file, List<PolicyValidator> validators) {
39+
Objects.requireNonNull(file, "file cannot be null");
40+
this.file = file;
41+
this.validators = new ArrayList<>(validators);
42+
}
43+
44+
@Override
45+
public List<TelemetryPolicy> fetchPolicies() throws IOException {
46+
List<TelemetryPolicy> policies = new ArrayList<>();
47+
if (!Files.exists(file)) {
48+
logger.info("Policy file does not exist: " + file);
49+
return policies;
50+
}
51+
52+
try (Stream<String> lines = Files.lines(file)) {
53+
lines.forEach(
54+
line -> {
55+
String trimmedLine = line.trim();
56+
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) {
57+
return;
58+
}
59+
60+
TelemetryPolicy policy = null;
61+
62+
if (trimmedLine.startsWith("{")) {
63+
for (PolicyValidator validator : validators) {
64+
if (trimmedLine.contains("\"" + validator.getPolicyType() + "\"")) {
65+
policy = validator.validate(trimmedLine);
66+
if (policy != null) {
67+
break;
68+
}
69+
}
70+
}
71+
} else {
72+
int idx = trimmedLine.indexOf('=');
73+
if (idx > 0) {
74+
String key = trimmedLine.substring(0, idx).trim();
75+
String valueStr = trimmedLine.substring(idx + 1).trim();
76+
77+
for (PolicyValidator validator : validators) {
78+
String alias = validator.getAlias();
79+
if (alias != null && alias.equals(key)) {
80+
try {
81+
policy = validator.validateAlias(key, valueStr);
82+
} catch (UnsupportedOperationException e) {
83+
logger.info(
84+
"Validator does not support alias validation: "
85+
+ validator.getClass().getName());
86+
continue;
87+
}
88+
if (policy != null) {
89+
break;
90+
}
91+
}
92+
}
93+
}
94+
}
95+
96+
if (policy == null) {
97+
logger.info("Validator not found or rejected for line: " + trimmedLine);
98+
return;
99+
}
100+
101+
policies.add(policy);
102+
});
103+
}
104+
return policies;
105+
}
106+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.dynamic.policy;
7+
8+
import java.io.Closeable;
9+
import java.util.List;
10+
import java.util.function.Consumer;
11+
12+
public interface PolicyProvider {
13+
/**
14+
* Retrieves the current list of telemetry policies provided by this source.
15+
*
16+
* @return A list of {@link TelemetryPolicy} objects.
17+
* @throws Exception if an error occurs while fetching the policies.
18+
*/
19+
List<TelemetryPolicy> fetchPolicies() throws Exception;
20+
21+
/**
22+
* Starts a mechanism to push policy updates to the provided consumer.
23+
*
24+
* <p>This method is optional and may be a no-op for providers that only support polling via
25+
* {@link #fetchPolicies()}.
26+
*
27+
* @param onUpdate A consumer that accepts the new list of policies when an update occurs.
28+
* @return A {@link Closeable} that stops watching when closed.
29+
*/
30+
default Closeable startWatching(Consumer<List<TelemetryPolicy>> onUpdate) {
31+
return () -> {};
32+
}
33+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.dynamic.policy;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
import java.io.IOException;
11+
import java.nio.file.Files;
12+
import java.nio.file.Path;
13+
import java.util.Arrays;
14+
import java.util.Collections;
15+
import java.util.List;
16+
import org.junit.jupiter.api.Test;
17+
import org.junit.jupiter.api.io.TempDir;
18+
19+
class LinePerPolicyFileProviderTest {
20+
21+
private static final String TRACE_SAMPLING_TYPE = "trace-sampling";
22+
private static final String TRACE_SAMPLING_ALIAS = "trace-sampling.probability";
23+
24+
@TempDir Path tempDir;
25+
26+
@Test
27+
void fetchPoliciesReturnsEmptyWhenFileMissing() throws Exception {
28+
Path missingFile = tempDir.resolve("missing-policies.txt");
29+
LinePerPolicyFileProvider provider =
30+
new LinePerPolicyFileProvider(missingFile, Collections.singletonList(acceptingValidator()));
31+
32+
List<TelemetryPolicy> policies = provider.fetchPolicies();
33+
34+
assertThat(policies).isEmpty();
35+
}
36+
37+
@Test
38+
void fetchPoliciesParsesJsonLines() throws Exception {
39+
Path file = writeLines("{\"trace-sampling\": {\"probability\": 0.5}}");
40+
LinePerPolicyFileProvider provider =
41+
new LinePerPolicyFileProvider(file, Collections.singletonList(acceptingValidator()));
42+
43+
List<TelemetryPolicy> policies = provider.fetchPolicies();
44+
45+
assertThat(policies).hasSize(1);
46+
assertThat(policies.get(0).getType()).isEqualTo(TRACE_SAMPLING_TYPE);
47+
}
48+
49+
@Test
50+
void fetchPoliciesParsesAliasLines() throws Exception {
51+
Path file = writeLines("trace-sampling.probability=0.5");
52+
LinePerPolicyFileProvider provider =
53+
new LinePerPolicyFileProvider(file, Collections.singletonList(acceptingValidator()));
54+
55+
List<TelemetryPolicy> policies = provider.fetchPolicies();
56+
57+
assertThat(policies).hasSize(1);
58+
assertThat(policies.get(0).getType()).isEqualTo(TRACE_SAMPLING_TYPE);
59+
}
60+
61+
@Test
62+
void fetchPoliciesSkipsBlankLinesAndComments() throws Exception {
63+
Path file = writeLines("", " ", "# comment line", "trace-sampling.probability=0.25");
64+
LinePerPolicyFileProvider provider =
65+
new LinePerPolicyFileProvider(file, Collections.singletonList(acceptingValidator()));
66+
67+
List<TelemetryPolicy> policies = provider.fetchPolicies();
68+
69+
assertThat(policies).hasSize(1);
70+
assertThat(policies.get(0).getType()).isEqualTo(TRACE_SAMPLING_TYPE);
71+
}
72+
73+
@Test
74+
void fetchPoliciesSkipsUnknownOrRejectedPolicies() throws Exception {
75+
PolicyValidator rejectingValidator =
76+
new TestPolicyValidator(/* acceptJson= */ false, /* acceptAlias= */ false);
77+
Path file =
78+
writeLines(
79+
"{\"trace-sampling\": {\"probability\": 0.5}}",
80+
"{\"other-policy\": {\"probability\": 0.5}}",
81+
"other.key=1");
82+
LinePerPolicyFileProvider provider =
83+
new LinePerPolicyFileProvider(file, Collections.singletonList(rejectingValidator));
84+
85+
List<TelemetryPolicy> policies = provider.fetchPolicies();
86+
87+
assertThat(policies).isEmpty();
88+
}
89+
90+
private Path writeLines(String... lines) throws IOException {
91+
Path file = tempDir.resolve("policies.txt");
92+
Files.write(file, Arrays.asList(lines));
93+
return file;
94+
}
95+
96+
private static PolicyValidator acceptingValidator() {
97+
return new TestPolicyValidator(/* acceptJson= */ true, /* acceptAlias= */ true);
98+
}
99+
100+
private static class TestPolicyValidator implements PolicyValidator {
101+
private final boolean acceptJson;
102+
private final boolean acceptAlias;
103+
104+
private TestPolicyValidator(boolean acceptJson, boolean acceptAlias) {
105+
this.acceptJson = acceptJson;
106+
this.acceptAlias = acceptAlias;
107+
}
108+
109+
@Override
110+
public TelemetryPolicy validate(String json) {
111+
if (!acceptJson) {
112+
return null;
113+
}
114+
return new TelemetryPolicy(TRACE_SAMPLING_TYPE, null);
115+
}
116+
117+
@Override
118+
public String getPolicyType() {
119+
return TRACE_SAMPLING_TYPE;
120+
}
121+
122+
@Override
123+
public TelemetryPolicy validateAlias(String key, String value) {
124+
if (!acceptAlias) {
125+
return null;
126+
}
127+
return new TelemetryPolicy(TRACE_SAMPLING_TYPE, null);
128+
}
129+
130+
@Override
131+
public String getAlias() {
132+
return TRACE_SAMPLING_ALIAS;
133+
}
134+
}
135+
}

0 commit comments

Comments
 (0)