55
66package 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 ;
823import java .util .ArrayList ;
924import java .util .Collections ;
1025import java .util .List ;
1126import 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. */
1432public 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 ) {
0 commit comments