Skip to content

Commit e18edf9

Browse files
[dynamic control] Add pipeline config initialization from declarative config or failover to file (#2766)
Co-authored-by: Jay DeLuca <jaydeluca4@gmail.com>
1 parent aa2dbfb commit e18edf9

3 files changed

Lines changed: 710 additions & 1 deletion

File tree

dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/registry/PolicyInitConfig.java

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,44 @@
55

66
package io.opentelemetry.contrib.dynamic.policy.registry;
77

8+
import io.opentelemetry.api.OpenTelemetry;
9+
import io.opentelemetry.api.incubator.ExtendedOpenTelemetry;
10+
import io.opentelemetry.api.incubator.config.ConfigProvider;
11+
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
12+
import io.opentelemetry.contrib.dynamic.policy.registry.json.JsonPolicyInitConfigReader;
13+
import io.opentelemetry.contrib.dynamic.policy.registry.yaml.YamlPolicyInitConfigReader;
14+
import io.opentelemetry.contrib.dynamic.policy.source.SourceFormat;
15+
import io.opentelemetry.contrib.dynamic.policy.source.SourceKind;
16+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
17+
import java.io.IOException;
18+
import java.io.InputStream;
19+
import java.lang.reflect.InvocationTargetException;
20+
import java.lang.reflect.Method;
21+
import java.nio.file.Files;
22+
import java.nio.file.Paths;
823
import java.util.ArrayList;
924
import java.util.Collections;
1025
import java.util.List;
1126
import java.util.Objects;
27+
import java.util.logging.Level;
28+
import java.util.logging.Logger;
29+
import javax.annotation.Nullable;
1230

1331
/** Top-level registry initialization model containing per-source mapping config. */
1432
public final class PolicyInitConfig {
33+
static final String TELEMETRY_POLICY_DECLARATIVE_KEY = "telemetry_policy";
34+
static final String SOURCES_DECLARATIVE_KEY = "sources";
35+
static final String KIND_DECLARATIVE_KEY = "kind";
36+
static final String FORMAT_DECLARATIVE_KEY = "format";
37+
static final String LOCATION_DECLARATIVE_KEY = "location";
38+
static final String MAPPINGS_DECLARATIVE_KEY = "mappings";
39+
static final String SOURCE_KEY_DECLARATIVE_KEY = "sourceKey";
40+
static final String POLICY_TYPE_DECLARATIVE_KEY = "policyType";
41+
static final String POLICY_INIT_CONFIG_PROPERTY_JSON =
42+
"otel.java.experimental.telemetry.policy.init.json";
43+
static final String POLICY_INIT_CONFIG_PROPERTY_YAML =
44+
"otel.java.experimental.telemetry.policy.init.yaml";
45+
private static final Logger logger = Logger.getLogger(PolicyInitConfig.class.getName());
1546

1647
private final List<PolicySourceConfig> sources;
1748

@@ -28,6 +59,225 @@ public List<PolicySourceConfig> getSources() {
2859
return sources;
2960
}
3061

62+
/**
63+
* Reads policy-init configuration from declarative config properties.
64+
*
65+
* <p>Expected shape is:
66+
*
67+
* <pre>{@code
68+
* telemetry_policy:
69+
* sources:
70+
* - kind: ...
71+
* format: ...
72+
* location: ...
73+
* mappings:
74+
* - sourceKey: ...
75+
* policyType: ...
76+
* }</pre>
77+
*
78+
* @param declarativeConfig declarative config root
79+
* @return parsed init config, or null when telemetry_policy is not configured
80+
* @throws NullPointerException if declarativeConfig is null
81+
* @throws IllegalArgumentException if telemetry_policy is present but invalid (for example,
82+
* missing or empty sources)
83+
*/
84+
@Nullable
85+
public static PolicyInitConfig readFromDeclarativeConfigProperties(
86+
DeclarativeConfigProperties declarativeConfig) {
87+
Objects.requireNonNull(declarativeConfig, "declarativeConfig cannot be null");
88+
DeclarativeConfigProperties telemetryPolicyConfig =
89+
declarativeConfig.getStructured(TELEMETRY_POLICY_DECLARATIVE_KEY);
90+
if (telemetryPolicyConfig == null) {
91+
return null;
92+
}
93+
List<DeclarativeConfigProperties> sourceConfigs =
94+
telemetryPolicyConfig.getStructuredList(SOURCES_DECLARATIVE_KEY);
95+
if (sourceConfigs == null || sourceConfigs.isEmpty()) {
96+
throw new IllegalArgumentException("Config must contain a non-empty 'sources' array.");
97+
}
98+
99+
List<PolicySourceConfig> sources = new ArrayList<>();
100+
for (DeclarativeConfigProperties sourceConfig : sourceConfigs) {
101+
sources.add(parseDeclarativeSource(sourceConfig));
102+
}
103+
return new PolicyInitConfig(sources);
104+
}
105+
106+
/**
107+
* Reads policy-init configuration with declarative-first fallback.
108+
*
109+
* <p>When declarative config contains {@code telemetry_policy.sources}, that configuration is
110+
* used. Otherwise, this falls back to file-path based loading via {@link
111+
* #readFromConfigProperties(ConfigProperties)}.
112+
*/
113+
@Nullable
114+
public static PolicyInitConfig readFromDeclarativeOrConfigProperties(
115+
ConfigProperties config, DeclarativeConfigProperties declarativeConfig) {
116+
Objects.requireNonNull(config, "config cannot be null");
117+
Objects.requireNonNull(declarativeConfig, "declarativeConfig cannot be null");
118+
PolicyInitConfig fromDeclarative = readFromDeclarativeConfigProperties(declarativeConfig);
119+
if (fromDeclarative != null) {
120+
return fromDeclarative;
121+
}
122+
return readFromConfigProperties(config);
123+
}
124+
125+
/**
126+
* Reads policy-init configuration with declarative-first fallback via {@link OpenTelemetry}.
127+
*
128+
* <p>If {@code openTelemetry} is an {@link ExtendedOpenTelemetry}, this method reads {@link
129+
* ConfigProvider} general declarative config and tries to parse {@code telemetry_policy} from
130+
* there. If unavailable, this falls back to file-path loading via {@link
131+
* #readFromConfigProperties(ConfigProperties)}.
132+
*/
133+
@Nullable
134+
public static PolicyInitConfig readFromOpenTelemetryOrConfigProperties(
135+
ConfigProperties config, @Nullable OpenTelemetry openTelemetry) {
136+
Objects.requireNonNull(config, "config cannot be null");
137+
ConfigProvider configProvider = getConfigProvider(openTelemetry);
138+
if (configProvider != null) {
139+
PolicyInitConfig fromDeclarative =
140+
readFromDeclarativeConfigProperties(getGeneralDeclarativeConfig(configProvider));
141+
if (fromDeclarative != null) {
142+
return fromDeclarative;
143+
}
144+
}
145+
return readFromConfigProperties(config);
146+
}
147+
148+
/**
149+
* Reads policy-init configuration based on config properties.
150+
*
151+
* <p>YAML takes precedence over JSON when both are present. If both are present, and the YAML
152+
* file is invalid, the JSON file is still ignored. If the file parsed is invalid, a warning is
153+
* logged and null is returned.
154+
*
155+
* @param config OpenTelemetry config properties
156+
* @return parsed init config, or null when no init-config path is configured or the file is
157+
* invalid
158+
* @throws NullPointerException if config is null
159+
*/
160+
@Nullable
161+
public static PolicyInitConfig readFromConfigProperties(ConfigProperties config) {
162+
Objects.requireNonNull(config, "config cannot be null");
163+
String mappingPathYaml = config.getString(POLICY_INIT_CONFIG_PROPERTY_YAML);
164+
if (mappingPathYaml == null || mappingPathYaml.trim().isEmpty()) {
165+
String mappingPathJson = config.getString(POLICY_INIT_CONFIG_PROPERTY_JSON);
166+
if (mappingPathJson == null || mappingPathJson.trim().isEmpty()) {
167+
return null;
168+
} else {
169+
try (InputStream in = Files.newInputStream(Paths.get(mappingPathJson.trim()))) {
170+
return JsonPolicyInitConfigReader.read(in);
171+
} catch (IOException | RuntimeException e) {
172+
logReadFailure(mappingPathJson.trim(), e);
173+
return null;
174+
}
175+
}
176+
} else {
177+
try (InputStream in = Files.newInputStream(Paths.get(mappingPathYaml.trim()))) {
178+
return YamlPolicyInitConfigReader.read(in);
179+
} catch (IOException | RuntimeException e) {
180+
logReadFailure(mappingPathYaml.trim(), e);
181+
return null;
182+
}
183+
}
184+
}
185+
186+
private static PolicySourceConfig parseDeclarativeSource(
187+
DeclarativeConfigProperties sourceConfig) {
188+
Objects.requireNonNull(sourceConfig, "source config cannot be null");
189+
String kindValue =
190+
requireDeclarativeText(
191+
sourceConfig.getString(KIND_DECLARATIVE_KEY), "Each source must define string 'kind'.");
192+
String formatValue =
193+
requireDeclarativeText(
194+
sourceConfig.getString(FORMAT_DECLARATIVE_KEY),
195+
"Each source must define string 'format'.");
196+
String location =
197+
normalizeOptionalDeclarativeText(sourceConfig.getString(LOCATION_DECLARATIVE_KEY));
198+
199+
List<DeclarativeConfigProperties> mappingConfigs =
200+
sourceConfig.getStructuredList(MAPPINGS_DECLARATIVE_KEY);
201+
if (mappingConfigs == null || mappingConfigs.isEmpty()) {
202+
throw new IllegalArgumentException("Each source must define a 'mappings' array.");
203+
}
204+
List<PolicySourceMappingConfig> mappings = new ArrayList<>();
205+
for (DeclarativeConfigProperties mappingConfig : mappingConfigs) {
206+
mappings.add(parseDeclarativeMapping(mappingConfig));
207+
}
208+
209+
return new PolicySourceConfig(
210+
SourceKind.fromConfigValue(kindValue),
211+
SourceFormat.fromConfigValue(formatValue),
212+
location,
213+
mappings);
214+
}
215+
216+
private static PolicySourceMappingConfig parseDeclarativeMapping(
217+
DeclarativeConfigProperties mappingConfig) {
218+
Objects.requireNonNull(mappingConfig, "mapping config cannot be null");
219+
String sourceKey =
220+
requireDeclarativeText(
221+
mappingConfig.getString(SOURCE_KEY_DECLARATIVE_KEY),
222+
"Each mapping must define string 'sourceKey'.");
223+
String policyType =
224+
requireDeclarativeText(
225+
mappingConfig.getString(POLICY_TYPE_DECLARATIVE_KEY),
226+
"Each mapping must define string 'policyType'.");
227+
return new PolicySourceMappingConfig(sourceKey, policyType);
228+
}
229+
230+
private static String requireDeclarativeText(@Nullable String value, String message) {
231+
if (value == null) {
232+
throw new IllegalArgumentException(message);
233+
}
234+
String trimmed = value.trim();
235+
if (trimmed.isEmpty()) {
236+
throw new IllegalArgumentException(message);
237+
}
238+
return trimmed;
239+
}
240+
241+
@Nullable
242+
private static String normalizeOptionalDeclarativeText(@Nullable String value) {
243+
if (value == null) {
244+
return null;
245+
}
246+
String trimmed = value.trim();
247+
return trimmed.isEmpty() ? null : trimmed;
248+
}
249+
250+
@Nullable
251+
private static ConfigProvider getConfigProvider(@Nullable OpenTelemetry openTelemetry) {
252+
return openTelemetry instanceof ExtendedOpenTelemetry
253+
? ((ExtendedOpenTelemetry) openTelemetry).getConfigProvider()
254+
: null;
255+
}
256+
257+
private static void logReadFailure(String configuredPath, Throwable throwable) {
258+
logger.log(
259+
Level.WARNING,
260+
"Failed to load telemetry policy init config from " + configuredPath,
261+
throwable);
262+
}
263+
264+
private static DeclarativeConfigProperties getGeneralDeclarativeConfig(
265+
ConfigProvider configProvider) {
266+
// Prefer the general config accessor when available in the API/runtime.
267+
try {
268+
Method method = configProvider.getClass().getMethod("getGeneralConfig");
269+
Object maybeConfig = method.invoke(configProvider);
270+
if (maybeConfig instanceof DeclarativeConfigProperties) {
271+
return (DeclarativeConfigProperties) maybeConfig;
272+
}
273+
} catch (NoSuchMethodException ignored) {
274+
// Method doesn't exist in this runtime version — fall through.
275+
} catch (IllegalAccessException | InvocationTargetException e) {
276+
logger.log(Level.WARNING, "Failed to invoke getGeneralConfig via reflection", e);
277+
}
278+
return configProvider.getGeneralInstrumentationConfig();
279+
}
280+
31281
@Override
32282
public boolean equals(Object obj) {
33283
if (this == obj) {

dynamic-control/src/main/java/io/opentelemetry/contrib/dynamic/policy/registry/json/JsonNodePolicyInitConfigParser.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import io.opentelemetry.contrib.dynamic.policy.source.SourceKind;
1515
import java.util.ArrayList;
1616
import java.util.List;
17+
import javax.annotation.Nullable;
1718

1819
/** Shared JsonNode-to-model parser for registry init configuration. */
1920
public final class JsonNodePolicyInitConfigParser {
@@ -45,7 +46,9 @@ private static PolicySourceConfig parseSource(JsonNode node) {
4546

4647
JsonNode locationNode = objectNode.get("location");
4748
String location =
48-
locationNode != null && locationNode.isTextual() ? locationNode.asText() : null;
49+
locationNode != null && locationNode.isTextual()
50+
? normalizeOptionalText(locationNode.asText())
51+
: null;
4952

5053
JsonNode mappingsNode =
5154
requireArray(objectNode.get("mappings"), "Each source must define a 'mappings' array.");
@@ -91,4 +94,10 @@ private static String requireText(JsonNode node, String message) {
9194
}
9295
return value;
9396
}
97+
98+
@Nullable
99+
private static String normalizeOptionalText(String value) {
100+
String trimmed = value.trim();
101+
return trimmed.isEmpty() ? null : trimmed;
102+
}
94103
}

0 commit comments

Comments
 (0)