diff --git a/ecosystem-explorer/eslint.config.js b/ecosystem-explorer/eslint.config.js index 00da6f2..61ceba5 100644 --- a/ecosystem-explorer/eslint.config.js +++ b/ecosystem-explorer/eslint.config.js @@ -36,5 +36,14 @@ export default defineConfig([ ecmaVersion: 2020, globals: globals.browser, }, + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + }, }, ]) diff --git a/ecosystem-explorer/public/data/sdk/sdk-options.json b/ecosystem-explorer/public/data/sdk/sdk-options.json new file mode 100644 index 0000000..35de07c --- /dev/null +++ b/ecosystem-explorer/public/data/sdk/sdk-options.json @@ -0,0 +1,171 @@ +{ + "schema_version": "1.0", + "defaults": { + "file_format": "1.0", + "propagators": ["tracecontext", "baggage"], + "tracer_provider": { + "exporter_type": "otlp_http", + "exporters": { + "otlp_http": { + "endpoint": "http://localhost:4318", + "protocol": "http/protobuf" + }, + "otlp_grpc": { + "endpoint": "http://localhost:4317" + }, + "console": {} + }, + "sampler": { + "type": "parent_based", + "root": "always_on", + "ratio": 1.0 + }, + "batch_processor": { + "schedule_delay": 5000, + "export_timeout": 30000, + "max_queue_size": 2048, + "max_export_batch_size": 512 + } + } + }, + "sections": { + "propagators": { + "name": "Propagators", + "description": "Configure context propagation formats for distributed tracing", + "options": [ + { + "id": "tracecontext", + "name": "W3C TraceContext", + "description": "W3C Trace Context propagator. Required for interoperability with W3C-compatible systems." + }, + { + "id": "baggage", + "name": "W3C Baggage", + "description": "W3C Baggage propagator for passing key-value pairs across service boundaries." + }, + { + "id": "b3", + "name": "B3 (single header)", + "description": "Zipkin B3 single-header propagator for compatibility with Zipkin and older systems." + }, + { + "id": "b3multi", + "name": "B3 Multi-header", + "description": "Zipkin B3 multi-header propagator." + } + ] + }, + "tracer_provider": { + "name": "Traces", + "description": "Configure the OpenTelemetry tracer provider", + "exporter": { + "name": "Span Exporter", + "description": "Where to export trace data", + "options": [ + { + "id": "otlp_http", + "name": "OTLP HTTP", + "description": "Export spans via OTLP over HTTP. Recommended for most setups.", + "settings": [ + { + "name": "endpoint", + "label": "Endpoint URL", + "description": "OTLP HTTP endpoint", + "type": "string" + }, + { + "name": "protocol", + "label": "Protocol", + "description": "OTLP transport protocol", + "type": "enum", + "options": ["http/protobuf", "http/json"] + } + ] + }, + { + "id": "otlp_grpc", + "name": "OTLP gRPC", + "description": "Export spans via OTLP over gRPC.", + "settings": [ + { + "name": "endpoint", + "label": "Endpoint URL", + "description": "OTLP gRPC endpoint", + "type": "string" + } + ] + }, + { + "id": "console", + "name": "Console", + "description": "Print spans to stdout. Useful for debugging.", + "settings": [] + } + ] + }, + "sampler": { + "name": "Sampler", + "description": "How to make sampling decisions for new traces", + "options": [ + { + "id": "parent_based", + "name": "Parent Based", + "description": "Use the parent span's sampling decision. Root spans use the configured root sampler.", + "has_root": true, + "root_options": ["always_on", "always_off", "trace_id_ratio_based"] + }, + { + "id": "always_on", + "name": "Always On", + "description": "Sample 100% of traces." + }, + { + "id": "always_off", + "name": "Always Off", + "description": "Sample 0% of traces. Useful for disabling tracing." + }, + { + "id": "trace_id_ratio_based", + "name": "Trace ID Ratio", + "description": "Sample a fixed percentage of traces based on trace ID.", + "has_ratio": true + } + ] + }, + "batch_processor": { + "name": "Batch Processor", + "description": "Configure the batch span processor that buffers and exports spans", + "settings": [ + { + "name": "schedule_delay", + "label": "Schedule Delay (ms)", + "description": "Delay between consecutive exports", + "type": "integer", + "min": 0 + }, + { + "name": "export_timeout", + "label": "Export Timeout (ms)", + "description": "Maximum time allowed to export data. 0 = no limit.", + "type": "integer", + "min": 0 + }, + { + "name": "max_queue_size", + "label": "Max Queue Size", + "description": "Maximum spans to queue before dropping", + "type": "integer", + "min": 1 + }, + { + "name": "max_export_batch_size", + "label": "Max Batch Size", + "description": "Maximum spans per export batch", + "type": "integer", + "min": 1 + } + ] + } + } + } +} diff --git a/ecosystem-explorer/src/App.tsx b/ecosystem-explorer/src/App.tsx index 83c3e45..3f8313d 100644 --- a/ecosystem-explorer/src/App.tsx +++ b/ecosystem-explorer/src/App.tsx @@ -21,7 +21,7 @@ import { JavaAgentPage } from "@/features/java-agent/java-agent-page"; import { CollectorPage } from "@/features/collector/collector-page"; import { NotFoundPage } from "@/features/not-found/not-found-page"; import { JavaInstrumentationListPage } from "@/features/java-agent/java-instrumentation-list-page"; -import { JavaConfigurationListPage } from "@/features/java-agent/java-configuration-list-page"; +import { JavaConfigurationBuilderPage } from "@/features/java-agent/configuration/java-configuration-builder-page"; import { InstrumentationDetailPage } from "@/features/java-agent/instrumentation-detail-page"; export default function App() { @@ -38,7 +38,7 @@ export default function App() { path="/java-agent/instrumentation/:version/:name" element={} /> - } /> + } /> } /> } /> diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/export-toolbar.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/export-toolbar.tsx new file mode 100644 index 0000000..3e40911 --- /dev/null +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/export-toolbar.tsx @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useState } from "react"; +import { useConfigurationBuilder } from "../hooks/use-configuration-builder"; +import { generateYamlFile } from "../utils/yaml-generator"; + +export function ExportToolbar() { + const { state } = useConfigurationBuilder(); + const [copyStatus, setCopyStatus] = useState<"idle" | "copied">("idle"); + + const output = generateYamlFile(state); + const fileExtension = ".yaml"; + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(output); + setCopyStatus("copied"); + setTimeout(() => setCopyStatus("idle"), 2000); + } catch (err) { + console.error("Failed to copy:", err); + } + }; + + const handleDownload = () => { + const blob = new Blob([output], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `otel-config${fileExtension}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + const disabled = state.selectedInstrumentations.size === 0; + + return ( +
+ + +
+ ); +} diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/instrumentation-browser.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/instrumentation-browser.tsx new file mode 100644 index 0000000..851629b --- /dev/null +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/instrumentation-browser.tsx @@ -0,0 +1,181 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useMemo, useState } from "react"; +import { useInstrumentations } from "@/hooks/use-javaagent-data"; +import { useConfigurationBuilder } from "../hooks/use-configuration-builder"; +import { getInstrumentationDisplayName } from "../../utils/format"; +import type { InstrumentationData } from "@/types/javaagent"; + +export function InstrumentationBrowser() { + const { state, dispatch } = useConfigurationBuilder(); + const { data: instrumentations, loading } = useInstrumentations(state.version); + const [searchQuery, setSearchQuery] = useState(""); + + const filteredInstrumentations = useMemo(() => { + if (!instrumentations) return []; + + return instrumentations.filter((instr) => { + if (searchQuery) { + const searchLower = searchQuery.toLowerCase(); + const name = getInstrumentationDisplayName(instr).toLowerCase(); + const description = (instr.description || "").toLowerCase(); + + return name.includes(searchLower) || description.includes(searchLower); + } + return true; + }); + }, [instrumentations, searchQuery]); + + const selectedInstrumentations = useMemo(() => { + return Array.from(state.selectedInstrumentations.values()); + }, [state.selectedInstrumentations]); + + const availableInstrumentations = useMemo(() => { + return filteredInstrumentations.filter( + (instr) => !state.selectedInstrumentations.has(instr.name) + ); + }, [filteredInstrumentations, state.selectedInstrumentations]); + + const handleAddAll = () => { + dispatch({ + type: "ADD_ALL_INSTRUMENTATIONS", + instrumentations: availableInstrumentations, + }); + }; + + const handleToggleInstrumentation = (instr: InstrumentationData) => { + if (state.selectedInstrumentations.has(instr.name)) { + dispatch({ type: "REMOVE_INSTRUMENTATION", name: instr.name }); + } else { + dispatch({ + type: "ADD_INSTRUMENTATION", + name: instr.name, + data: instr, + }); + } + }; + + if (loading) { + return ( +
+
Loading instrumentations...
+
+ ); + } + + return ( +
+
+ + setSearchQuery(e.target.value)} + className="w-full px-3 py-2 rounded-lg border border-border bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary" + aria-label="Search instrumentations" + /> +
+ + {selectedInstrumentations.length > 0 && ( +
+
+

+ Selected ({selectedInstrumentations.length}) +

+ +
+
+ {selectedInstrumentations.map((config) => ( +
+ + {getInstrumentationDisplayName(config.data)} + + +
+ ))} +
+
+ )} + +
+
+

+ Available ({availableInstrumentations.length}) +

+ +
+
+ {availableInstrumentations.length === 0 ? ( +

+ {searchQuery + ? "No instrumentations match your search" + : "All instrumentations selected"} +

+ ) : ( + availableInstrumentations.map((instr) => ( + + )) + )} +
+
+
+ ); +} diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/output-preview.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/output-preview.tsx new file mode 100644 index 0000000..7f3a607 --- /dev/null +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/output-preview.tsx @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useMemo } from "react"; +import { useConfigurationBuilder } from "../hooks/use-configuration-builder"; +import { generateYamlFile } from "../utils/yaml-generator"; +import { ExportToolbar } from "./export-toolbar"; + +export function OutputPreview() { + const { state } = useConfigurationBuilder(); + + const output = useMemo(() => { + return generateYamlFile(state); + }, [state]); + + return ( +
+
+ +
+
+
+          {output}
+        
+
+
+ ); +} diff --git a/ecosystem-explorer/src/features/java-agent/configuration/components/sdk-browser.tsx b/ecosystem-explorer/src/features/java-agent/configuration/components/sdk-browser.tsx new file mode 100644 index 0000000..c4457f9 --- /dev/null +++ b/ecosystem-explorer/src/features/java-agent/configuration/components/sdk-browser.tsx @@ -0,0 +1,307 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useEffect } from "react"; +import { useSdkOptions } from "@/hooks/use-sdk-options"; +import { useConfigurationBuilder } from "../hooks/use-configuration-builder"; +import type { SdkConfig } from "@/types/sdk"; + +export function SdkBrowser() { + const { state, dispatch } = useConfigurationBuilder(); + const { data: options, loading } = useSdkOptions(); + const sdk = state.sdkConfig; + + // Initialize state from sdk-options.json defaults on first load + useEffect(() => { + if (!options) return; + const d = options.defaults; + const sdkConfig: SdkConfig = { + fileFormat: d.file_format, + propagators: [...d.propagators], + tracerProvider: { + exporterType: d.tracer_provider.exporter_type, + exporterEndpoint: + d.tracer_provider.exporters[d.tracer_provider.exporter_type]?.endpoint ?? "", + exporterProtocol: + d.tracer_provider.exporters[d.tracer_provider.exporter_type]?.protocol ?? "", + samplerType: d.tracer_provider.sampler.type, + samplerRoot: d.tracer_provider.sampler.root, + samplerRatio: d.tracer_provider.sampler.ratio, + batchScheduleDelay: d.tracer_provider.batch_processor.schedule_delay, + batchExportTimeout: d.tracer_provider.batch_processor.export_timeout, + batchMaxQueueSize: d.tracer_provider.batch_processor.max_queue_size, + batchMaxExportBatchSize: d.tracer_provider.batch_processor.max_export_batch_size, + }, + }; + dispatch({ type: "LOAD_SDK_DEFAULTS", sdkConfig }); + }, [options, dispatch]); + + if (loading) { + return ( +
+
Loading SDK options...
+
+ ); + } + + if (!options) { + return
Failed to load SDK options.
; + } + + const { sections } = options; + const tracerSection = sections.tracer_provider; + + const handleExporterTypeChange = (exporterType: string) => { + dispatch({ type: "SET_SDK_EXPORTER_TYPE", exporterType }); + const exporterDefaults = options.defaults.tracer_provider.exporters[exporterType] ?? {}; + dispatch({ type: "SET_SDK_EXPORTER_ENDPOINT", endpoint: exporterDefaults.endpoint ?? "" }); + dispatch({ type: "SET_SDK_EXPORTER_PROTOCOL", protocol: exporterDefaults.protocol ?? "" }); + }; + + return ( +
+ {/* Propagators */} +
+
+

{sections.propagators.name}

+

{sections.propagators.description}

+
+
+ {sections.propagators.options.map((opt) => ( + + ))} +
+
+ +
+ + {/* Exporter */} +
+
+

{tracerSection.exporter.name}

+

+ {tracerSection.exporter.description} +

+
+
+ {tracerSection.exporter.options.map((opt) => ( + + ))} +
+ + {/* Exporter endpoint field */} + {sdk.tracerProvider.exporterType !== "console" && ( +
+
+ + + dispatch({ type: "SET_SDK_EXPORTER_ENDPOINT", endpoint: e.target.value }) + } + className="w-full px-3 py-1.5 rounded border border-border bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary" + placeholder="http://localhost:4318" + /> +
+ {sdk.tracerProvider.exporterType === "otlp_http" && ( +
+ + +
+ )} +
+ )} +
+ +
+ + {/* Sampler */} +
+
+

{tracerSection.sampler.name}

+

+ {tracerSection.sampler.description} +

+
+
+ {tracerSection.sampler.options.map((opt) => ( + + ))} +
+ + {/* Parent-based root sampler */} + {sdk.tracerProvider.samplerType === "parent_based" && ( +
+ + + {sdk.tracerProvider.samplerRoot === "trace_id_ratio_based" && ( +
+ + + dispatch({ type: "SET_SDK_SAMPLER_RATIO", ratio: parseFloat(e.target.value) }) + } + className="w-full px-3 py-1.5 rounded border border-border bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary" + /> +
+ )} +
+ )} + + {/* Standalone ratio sampler */} + {sdk.tracerProvider.samplerType === "trace_id_ratio_based" && ( +
+ + + dispatch({ type: "SET_SDK_SAMPLER_RATIO", ratio: parseFloat(e.target.value) }) + } + className="w-full px-3 py-1.5 rounded border border-border bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary" + /> +
+ )} +
+ +
+ + {/* Batch Processor */} +
+
+

+ {tracerSection.batch_processor.name} +

+

+ {tracerSection.batch_processor.description} +

+
+
+ {tracerSection.batch_processor.settings.map((setting) => { + const stateKey = ( + { + schedule_delay: "batchScheduleDelay", + export_timeout: "batchExportTimeout", + max_queue_size: "batchMaxQueueSize", + max_export_batch_size: "batchMaxExportBatchSize", + } as Record + )[setting.name]; + + if (!stateKey) return null; + + return ( +
+ + + dispatch({ + type: "UPDATE_SDK_BATCH_SETTING", + key: stateKey, + value: parseInt(e.target.value, 10), + }) + } + className="w-full px-3 py-1.5 rounded border border-border bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary" + /> +

{setting.description}

+
+ ); + })} +
+
+
+ ); +} diff --git a/ecosystem-explorer/src/features/java-agent/configuration/context/configuration-builder-context.ts b/ecosystem-explorer/src/features/java-agent/configuration/context/configuration-builder-context.ts new file mode 100644 index 0000000..08ba053 --- /dev/null +++ b/ecosystem-explorer/src/features/java-agent/configuration/context/configuration-builder-context.ts @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createContext, type Dispatch } from "react"; +import type { ConfigurationBuilderState, ConfigurationBuilderAction } from "@/types/javaagent"; + +export interface ConfigurationBuilderContextValue { + state: ConfigurationBuilderState; + dispatch: Dispatch; +} + +export const ConfigurationBuilderContext = createContext( + null +); diff --git a/ecosystem-explorer/src/features/java-agent/configuration/context/configuration-builder-context.tsx b/ecosystem-explorer/src/features/java-agent/configuration/context/configuration-builder-context.tsx new file mode 100644 index 0000000..8b4de0d --- /dev/null +++ b/ecosystem-explorer/src/features/java-agent/configuration/context/configuration-builder-context.tsx @@ -0,0 +1,282 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useReducer, type ReactNode } from "react"; +import type { ConfigurationBuilderState, ConfigurationBuilderAction } from "@/types/javaagent"; +import { ConfigurationBuilderContext } from "./configuration-builder-context"; + +const initialState: ConfigurationBuilderState = { + version: "", + activeArea: "instrumentation", + selectedInstrumentations: new Map(), + configOverrides: new Map(), + outputFormat: "properties", + isInitialized: false, + sdkConfig: { + fileFormat: "1.0", + propagators: ["tracecontext", "baggage"], + tracerProvider: { + exporterType: "otlp_http", + exporterEndpoint: "http://localhost:4318", + exporterProtocol: "http/protobuf", + samplerType: "parent_based", + samplerRoot: "always_on", + samplerRatio: 1.0, + batchScheduleDelay: 5000, + batchExportTimeout: 30000, + batchMaxQueueSize: 2048, + batchMaxExportBatchSize: 512, + }, + }, +}; + +function configurationBuilderReducer( + state: ConfigurationBuilderState, + action: ConfigurationBuilderAction +): ConfigurationBuilderState { + switch (action.type) { + case "SET_VERSION": + return { + ...state, + version: action.version, + }; + + case "SET_ACTIVE_AREA": + return { + ...state, + activeArea: action.area, + }; + + case "ADD_INSTRUMENTATION": { + const newInstrumentations = new Map(state.selectedInstrumentations); + const enabledConfigs = new Set(); + + action.data.configurations?.forEach((config) => { + enabledConfigs.add(config.name); + }); + + newInstrumentations.set(action.name, { + name: action.name, + data: action.data, + enabledConfigs, + }); + + return { + ...state, + selectedInstrumentations: newInstrumentations, + }; + } + + case "ADD_ALL_INSTRUMENTATIONS": { + const newInstrumentations = new Map(state.selectedInstrumentations); + for (const instr of action.instrumentations) { + if (!newInstrumentations.has(instr.name)) { + const enabledConfigs = new Set(); + instr.configurations?.forEach((config) => { + enabledConfigs.add(config.name); + }); + newInstrumentations.set(instr.name, { + name: instr.name, + data: instr, + enabledConfigs, + }); + } + } + return { + ...state, + selectedInstrumentations: newInstrumentations, + }; + } + + case "REMOVE_ALL_INSTRUMENTATIONS": + return { + ...state, + selectedInstrumentations: new Map(), + }; + + case "REMOVE_INSTRUMENTATION": { + const newInstrumentations = new Map(state.selectedInstrumentations); + newInstrumentations.delete(action.name); + + return { + ...state, + selectedInstrumentations: newInstrumentations, + }; + } + + case "UPDATE_CONFIG": { + const newConfigOverrides = new Map(state.configOverrides); + const existingConfig = state.configOverrides.get(action.configName); + + newConfigOverrides.set(action.configName, { + name: action.configName, + value: action.value, + isModified: true, + default: existingConfig?.default ?? action.value, + }); + + return { + ...state, + configOverrides: newConfigOverrides, + }; + } + + case "TOGGLE_CONFIG": { + const newInstrumentations = new Map(state.selectedInstrumentations); + const instrumentation = newInstrumentations.get(action.instrumentationName); + + if (instrumentation) { + const newEnabledConfigs = new Set(instrumentation.enabledConfigs); + if (newEnabledConfigs.has(action.configName)) { + newEnabledConfigs.delete(action.configName); + } else { + newEnabledConfigs.add(action.configName); + } + + newInstrumentations.set(action.instrumentationName, { + ...instrumentation, + enabledConfigs: newEnabledConfigs, + }); + } + + return { + ...state, + selectedInstrumentations: newInstrumentations, + }; + } + + case "SET_OUTPUT_FORMAT": + return { + ...state, + outputFormat: action.format, + }; + + case "LOAD_STATE": { + return { + ...state, + ...action.state, + }; + } + + case "LOAD_SDK_DEFAULTS": + return { + ...state, + sdkConfig: action.sdkConfig, + }; + + case "TOGGLE_SDK_PROPAGATOR": { + const current = state.sdkConfig.propagators; + const updated = current.includes(action.propagatorId) + ? current.filter((p) => p !== action.propagatorId) + : [...current, action.propagatorId]; + return { + ...state, + sdkConfig: { ...state.sdkConfig, propagators: updated }, + }; + } + + case "SET_SDK_EXPORTER_TYPE": + return { + ...state, + sdkConfig: { + ...state.sdkConfig, + tracerProvider: { ...state.sdkConfig.tracerProvider, exporterType: action.exporterType }, + }, + }; + + case "SET_SDK_EXPORTER_ENDPOINT": + return { + ...state, + sdkConfig: { + ...state.sdkConfig, + tracerProvider: { ...state.sdkConfig.tracerProvider, exporterEndpoint: action.endpoint }, + }, + }; + + case "SET_SDK_EXPORTER_PROTOCOL": + return { + ...state, + sdkConfig: { + ...state.sdkConfig, + tracerProvider: { ...state.sdkConfig.tracerProvider, exporterProtocol: action.protocol }, + }, + }; + + case "SET_SDK_SAMPLER_TYPE": + return { + ...state, + sdkConfig: { + ...state.sdkConfig, + tracerProvider: { ...state.sdkConfig.tracerProvider, samplerType: action.samplerType }, + }, + }; + + case "SET_SDK_SAMPLER_ROOT": + return { + ...state, + sdkConfig: { + ...state.sdkConfig, + tracerProvider: { ...state.sdkConfig.tracerProvider, samplerRoot: action.root }, + }, + }; + + case "SET_SDK_SAMPLER_RATIO": + return { + ...state, + sdkConfig: { + ...state.sdkConfig, + tracerProvider: { ...state.sdkConfig.tracerProvider, samplerRatio: action.ratio }, + }, + }; + + case "UPDATE_SDK_BATCH_SETTING": + return { + ...state, + sdkConfig: { + ...state.sdkConfig, + tracerProvider: { ...state.sdkConfig.tracerProvider, [action.key]: action.value }, + }, + }; + + case "RESET": + return { + ...initialState, + version: state.version, + }; + + case "MARK_INITIALIZED": + return { + ...state, + isInitialized: true, + }; + + default: + return state; + } +} + +interface ConfigurationBuilderProviderProps { + children: ReactNode; +} + +export function ConfigurationBuilderProvider({ children }: ConfigurationBuilderProviderProps) { + const [state, dispatch] = useReducer(configurationBuilderReducer, initialState); + + return ( + + {children} + + ); +} diff --git a/ecosystem-explorer/src/features/java-agent/configuration/hooks/use-configuration-builder.ts b/ecosystem-explorer/src/features/java-agent/configuration/hooks/use-configuration-builder.ts new file mode 100644 index 0000000..bbd1ba6 --- /dev/null +++ b/ecosystem-explorer/src/features/java-agent/configuration/hooks/use-configuration-builder.ts @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useContext } from "react"; +import { ConfigurationBuilderContext } from "../context/configuration-builder-context"; + +export function useConfigurationBuilder() { + const context = useContext(ConfigurationBuilderContext); + + if (!context) { + throw new Error("useConfigurationBuilder must be used within a ConfigurationBuilderProvider"); + } + + return context; +} diff --git a/ecosystem-explorer/src/features/java-agent/configuration/java-configuration-builder-page.tsx b/ecosystem-explorer/src/features/java-agent/configuration/java-configuration-builder-page.tsx new file mode 100644 index 0000000..6d9470d --- /dev/null +++ b/ecosystem-explorer/src/features/java-agent/configuration/java-configuration-builder-page.tsx @@ -0,0 +1,147 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useEffect } from "react"; +import { BackButton } from "@/components/ui/back-button"; +import { useVersions } from "@/hooks/use-javaagent-data"; +import { ConfigurationBuilderProvider } from "./context/configuration-builder-context.tsx"; +import { useConfigurationBuilder } from "./hooks/use-configuration-builder"; +import { InstrumentationBrowser } from "./components/instrumentation-browser"; +import { SdkBrowser } from "./components/sdk-browser"; +import { OutputPreview } from "./components/output-preview"; + +function ConfigurationBuilderContent() { + const { data: versionsData, loading: versionsLoading } = useVersions(); + const { state, dispatch } = useConfigurationBuilder(); + + const latestVersion = versionsData?.versions.find((v) => v.is_latest)?.version ?? ""; + + useEffect(() => { + if (latestVersion && !state.version) { + dispatch({ type: "SET_VERSION", version: latestVersion }); + } + }, [latestVersion, state.version, dispatch]); + + if (versionsLoading) { + return ( +
+
+
+
Loading...
+
+
+
+ ); + } + + return ( +
+
+
+ +
+ + +
+
+ +
+

Configuration Builder

+

+ Build and customize your OpenTelemetry Java Agent configuration +

+
+ +
+ + +
+ +
+
+ {state.activeArea === "instrumentation" && ( +
+

Instrumentation Browser

+ +
+ )} + {state.activeArea === "sdk" && ( +
+

SDK Configuration

+ +
+ )} +
+ +
+

Output Preview

+ +
+
+
+
+ ); +} + +export function JavaConfigurationBuilderPage() { + return ( + + + + ); +} diff --git a/ecosystem-explorer/src/features/java-agent/configuration/utils/common-config-detector.ts b/ecosystem-explorer/src/features/java-agent/configuration/utils/common-config-detector.ts new file mode 100644 index 0000000..61efb5e --- /dev/null +++ b/ecosystem-explorer/src/features/java-agent/configuration/utils/common-config-detector.ts @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { InstrumentationConfig, CommonConfig, Configuration } from "@/types/javaagent"; + +export function detectCommonConfigs( + instrumentations: Map +): CommonConfig[] { + const usage = new Map(); + + for (const [name, instr] of instrumentations) { + if (!instr.data.configurations) continue; + + for (const config of instr.data.configurations) { + if (!instr.enabledConfigs.has(config.name)) continue; + + const existing = usage.get(config.name); + if (existing) { + existing.usedBy.push(name); + } else { + usage.set(config.name, { config, usedBy: [name] }); + } + } + } + + return Array.from(usage.entries()) + .filter(([_, info]) => info.usedBy.length >= 2) + .map(([name, info]) => ({ + name, + config: info.config, + usedBy: info.usedBy, + })) + .sort((a, b) => b.usedBy.length - a.usedBy.length); +} diff --git a/ecosystem-explorer/src/features/java-agent/configuration/utils/config-name-converter.test.ts b/ecosystem-explorer/src/features/java-agent/configuration/utils/config-name-converter.test.ts new file mode 100644 index 0000000..2f5da56 --- /dev/null +++ b/ecosystem-explorer/src/features/java-agent/configuration/utils/config-name-converter.test.ts @@ -0,0 +1,264 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect } from "vitest"; +import { flatToDeclarative, declarativeToFlat, flatToShellVar } from "./config-name-converter"; + +describe("config-name-converter", () => { + describe("flatToDeclarative", () => { + describe("SPECIAL_MAPPINGS", () => { + it("converts general HTTP client request headers", () => { + expect(flatToDeclarative("otel.instrumentation.http.client.capture-request-headers")).toBe( + "general.http.client.request_captured_headers" + ); + }); + + it("converts general HTTP client response headers", () => { + expect(flatToDeclarative("otel.instrumentation.http.client.capture-response-headers")).toBe( + "general.http.client.response_captured_headers" + ); + }); + + it("converts general HTTP server request headers", () => { + expect(flatToDeclarative("otel.instrumentation.http.server.capture-request-headers")).toBe( + "general.http.server.request_captured_headers" + ); + }); + + it("converts general HTTP server response headers", () => { + expect(flatToDeclarative("otel.instrumentation.http.server.capture-response-headers")).toBe( + "general.http.server.response_captured_headers" + ); + }); + + it("converts HTTP known methods", () => { + expect(flatToDeclarative("otel.instrumentation.http.known-methods")).toBe( + "instrumentation.java.common.http.known_methods" + ); + }); + + it("converts HTTP client redact query parameters with /development", () => { + expect( + flatToDeclarative("otel.instrumentation.http.client.experimental.redact-query-parameters") + ).toBe("instrumentation.java.common.http.client.redact_query_parameters/development"); + }); + + it("converts HTTP client emit experimental telemetry with /development", () => { + expect( + flatToDeclarative("otel.instrumentation.http.client.emit-experimental-telemetry") + ).toBe("instrumentation.java.common.http.client.emit_experimental_telemetry/development"); + }); + + it("converts HTTP server emit experimental telemetry with /development", () => { + expect( + flatToDeclarative("otel.instrumentation.http.server.emit-experimental-telemetry") + ).toBe("instrumentation.java.common.http.server.emit_experimental_telemetry/development"); + }); + + it("converts common db statement sanitizer", () => { + expect( + flatToDeclarative("otel.instrumentation.common.db-statement-sanitizer.enabled") + ).toBe("instrumentation.java.common.database.statement_sanitizer.enabled"); + }); + + it("converts common db sqlcommenter with /development", () => { + expect( + flatToDeclarative("otel.instrumentation.common.experimental.db-sqlcommenter.enabled") + ).toBe("instrumentation.java.common.database.sqlcommenter/development.enabled"); + }); + + it("converts messaging receive telemetry with /development", () => { + expect( + flatToDeclarative("otel.instrumentation.messaging.experimental.receive-telemetry.enabled") + ).toBe("instrumentation.java.common.messaging.receive_telemetry/development.enabled"); + }); + + it("converts messaging capture headers with /development", () => { + expect( + flatToDeclarative("otel.instrumentation.messaging.experimental.capture-headers") + ).toBe("instrumentation.java.common.messaging.capture_headers/development"); + }); + + it("converts genai capture message content", () => { + expect(flatToDeclarative("otel.instrumentation.genai.capture-message-content")).toBe( + "instrumentation.java.common.gen_ai.capture_message_content" + ); + }); + + it("converts span suppression strategy with /development", () => { + expect( + flatToDeclarative("otel.instrumentation.experimental.span-suppression-strategy") + ).toBe("instrumentation.java.common.span_suppression_strategy/development"); + }); + + it("converts opentelemetry annotations exclude methods", () => { + expect( + flatToDeclarative("otel.instrumentation.opentelemetry-annotations.exclude-methods") + ).toBe("instrumentation.java.opentelemetry_extension_annotations.exclude_methods"); + }); + + it("converts semconv stability opt-in", () => { + expect(flatToDeclarative("otel.semconv-stability.opt-in")).toBe( + "general.semconv_stability.opt_in" + ); + }); + }); + + describe("mechanical conversion", () => { + it("converts basic instrumentation property", () => { + expect(flatToDeclarative("otel.instrumentation.jdbc.statement-sanitizer.enabled")).toBe( + "instrumentation.java.jdbc.statement_sanitizer.enabled" + ); + }); + + it("replaces hyphens with underscores", () => { + expect(flatToDeclarative("otel.instrumentation.my-custom-lib.some-property")).toBe( + "instrumentation.java.my_custom_lib.some_property" + ); + }); + + it("handles experimental prefix with /development suffix", () => { + expect( + flatToDeclarative("otel.instrumentation.jdbc.experimental.transaction.enabled") + ).toBe("instrumentation.java.jdbc.experimental/development.transaction.enabled"); + }); + + it("handles experimental- prefix in segment", () => { + expect( + flatToDeclarative("otel.instrumentation.spring-webmvc.experimental-span-attributes") + ).toBe("instrumentation.java.spring_webmvc.span_attributes/development"); + }); + + it("preserves non-instrumentation properties", () => { + expect(flatToDeclarative("otel.some.other.property")).toBe("otel.some.other.property"); + }); + + it("handles properties without otel.instrumentation prefix", () => { + expect(flatToDeclarative("custom.property.name")).toBe("custom.property.name"); + }); + }); + }); + + describe("declarativeToFlat", () => { + describe("reverse SPECIAL_MAPPINGS", () => { + it("converts general HTTP client request headers back", () => { + expect(declarativeToFlat("general.http.client.request_captured_headers")).toBe( + "otel.instrumentation.http.client.capture-request-headers" + ); + }); + + it("converts HTTP known methods back", () => { + expect(declarativeToFlat("instrumentation.java.common.http.known_methods")).toBe( + "otel.instrumentation.http.known-methods" + ); + }); + + it("converts HTTP client emit experimental telemetry back", () => { + expect( + declarativeToFlat( + "instrumentation.java.common.http.client.emit_experimental_telemetry/development" + ) + ).toBe("otel.instrumentation.http.client.emit-experimental-telemetry"); + }); + + it("converts common db statement sanitizer back", () => { + expect( + declarativeToFlat("instrumentation.java.common.database.statement_sanitizer.enabled") + ).toBe("otel.instrumentation.common.db-statement-sanitizer.enabled"); + }); + + it("converts semconv stability opt-in back", () => { + expect(declarativeToFlat("general.semconv_stability.opt_in")).toBe( + "otel.semconv-stability.opt-in" + ); + }); + }); + + describe("mechanical reverse conversion", () => { + it("converts basic instrumentation property back", () => { + expect(declarativeToFlat("instrumentation.java.jdbc.statement_sanitizer.enabled")).toBe( + "otel.instrumentation.jdbc.statement-sanitizer.enabled" + ); + }); + + it("replaces underscores with hyphens", () => { + expect(declarativeToFlat("instrumentation.java.my_custom_lib.some_property")).toBe( + "otel.instrumentation.my-custom-lib.some-property" + ); + }); + + it("strips /development suffix", () => { + expect( + declarativeToFlat( + "instrumentation.java.jdbc.experimental/development.transaction.enabled" + ) + ).toBe("otel.instrumentation.jdbc.experimental.transaction.enabled"); + }); + + it("preserves non-instrumentation properties", () => { + expect(declarativeToFlat("custom.property.name")).toBe("custom.property.name"); + }); + }); + }); + + describe("flatToShellVar", () => { + it("converts to uppercase with underscores", () => { + expect(flatToShellVar("otel.instrumentation.jdbc.statement-sanitizer.enabled")).toBe( + "OTEL_INSTRUMENTATION_JDBC_STATEMENT_SANITIZER_ENABLED" + ); + }); + + it("replaces dots with underscores", () => { + expect(flatToShellVar("otel.instrumentation.http.known-methods")).toBe( + "OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS" + ); + }); + + it("replaces hyphens with underscores", () => { + expect(flatToShellVar("otel.instrumentation.my-custom-lib.some-property")).toBe( + "OTEL_INSTRUMENTATION_MY_CUSTOM_LIB_SOME_PROPERTY" + ); + }); + + it("handles experimental properties", () => { + expect(flatToShellVar("otel.instrumentation.jdbc.experimental.transaction.enabled")).toBe( + "OTEL_INSTRUMENTATION_JDBC_EXPERIMENTAL_TRANSACTION_ENABLED" + ); + }); + }); + + describe("round-trip conversions", () => { + it("flat -> declarative -> flat for standard property", () => { + const original = "otel.instrumentation.jdbc.statement-sanitizer.enabled"; + const declarative = flatToDeclarative(original); + const backToFlat = declarativeToFlat(declarative); + expect(backToFlat).toBe(original); + }); + + it("flat -> declarative -> flat for experimental property", () => { + const original = "otel.instrumentation.jdbc.experimental.transaction.enabled"; + const declarative = flatToDeclarative(original); + const backToFlat = declarativeToFlat(declarative); + expect(backToFlat).toBe(original); + }); + + it("flat -> declarative -> flat for SPECIAL_MAPPINGS", () => { + const original = "otel.instrumentation.http.known-methods"; + const declarative = flatToDeclarative(original); + const backToFlat = declarativeToFlat(declarative); + expect(backToFlat).toBe(original); + }); + }); +}); diff --git a/ecosystem-explorer/src/features/java-agent/configuration/utils/config-name-converter.ts b/ecosystem-explorer/src/features/java-agent/configuration/utils/config-name-converter.ts new file mode 100644 index 0000000..c25e78e --- /dev/null +++ b/ecosystem-explorer/src/features/java-agent/configuration/utils/config-name-converter.ts @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const SPECIAL_MAPPINGS_REVERSE: Record = { + "otel.instrumentation.http.client.capture-request-headers": + "general.http.client.request_captured_headers", + "otel.instrumentation.http.client.capture-response-headers": + "general.http.client.response_captured_headers", + "otel.instrumentation.http.server.capture-request-headers": + "general.http.server.request_captured_headers", + "otel.instrumentation.http.server.capture-response-headers": + "general.http.server.response_captured_headers", + "otel.instrumentation.http.known-methods": "java.common.http.known_methods", + "otel.instrumentation.http.client.experimental.redact-query-parameters": + "java.common.http.client.redact_query_parameters/development", + "otel.instrumentation.http.client.emit-experimental-telemetry": + "java.common.http.client.emit_experimental_telemetry/development", + "otel.instrumentation.http.server.emit-experimental-telemetry": + "java.common.http.server.emit_experimental_telemetry/development", + "otel.instrumentation.common.db-statement-sanitizer.enabled": + "java.common.database.statement_sanitizer.enabled", + "otel.instrumentation.common.experimental.db-sqlcommenter.enabled": + "java.common.database.sqlcommenter/development.enabled", + "otel.instrumentation.messaging.experimental.receive-telemetry.enabled": + "java.common.messaging.receive_telemetry/development.enabled", + "otel.instrumentation.messaging.experimental.capture-headers": + "java.common.messaging.capture_headers/development", + "otel.instrumentation.genai.capture-message-content": + "java.common.gen_ai.capture_message_content", + "otel.instrumentation.experimental.span-suppression-strategy": + "java.common.span_suppression_strategy/development", + "otel.instrumentation.opentelemetry-annotations.exclude-methods": + "java.opentelemetry_extension_annotations.exclude_methods", + "otel.semconv-stability.opt-in": "general.semconv_stability.opt_in", +}; + +export function flatToDeclarative(flatProp: string): string { + const specialMapping = SPECIAL_MAPPINGS_REVERSE[flatProp]; + if (specialMapping) { + if (specialMapping.startsWith("java.")) { + return `instrumentation.${specialMapping}`; + } + return specialMapping; + } + + if (!flatProp.startsWith("otel.instrumentation.")) { + return flatProp; + } + + let path = flatProp.substring("otel.instrumentation.".length); + + const segments = path.split("."); + const convertedSegments = segments.map((segment) => { + if (segment.startsWith("experimental-")) { + const withoutExp = segment.replace(/^experimental-/, ""); + return withoutExp ? `${withoutExp}/development` : "experimental/development"; + } else if (segment === "experimental") { + return "experimental/development"; + } + return segment; + }); + + path = convertedSegments.join(".").replace(/-/g, "_"); + + return `instrumentation.java.${path}`; +} + +export function declarativeToFlat(declarativePath: string): string { + const reverseMapping = Object.entries(SPECIAL_MAPPINGS_REVERSE).find( + ([_, declarative]) => + declarative === declarativePath || `instrumentation.${declarative}` === declarativePath + ); + if (reverseMapping) { + return reverseMapping[0]; + } + + if (declarativePath.startsWith("instrumentation.java.")) { + let path = declarativePath.substring("instrumentation.java.".length); + + path = path.replace(/\/development/g, ""); + + const segments = path.split("."); + const convertedSegments = segments.map((segment) => { + return segment.replace(/_/g, "-"); + }); + + return `otel.instrumentation.${convertedSegments.join(".")}`; + } + + return declarativePath; +} + +export function flatToShellVar(flatProp: string): string { + return flatProp.replace(/\./g, "_").replace(/-/g, "_").toUpperCase(); +} diff --git a/ecosystem-explorer/src/features/java-agent/configuration/utils/sdk-yaml-generator.ts b/ecosystem-explorer/src/features/java-agent/configuration/utils/sdk-yaml-generator.ts new file mode 100644 index 0000000..0552a7a --- /dev/null +++ b/ecosystem-explorer/src/features/java-agent/configuration/utils/sdk-yaml-generator.ts @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { SdkConfig } from "@/types/sdk"; + +export function generateSdkYaml(sdk: SdkConfig): string { + const lines: string[] = []; + + lines.push(`file_format: "${sdk.fileFormat}"`); + + // tracer_provider + lines.push("tracer_provider:"); + lines.push(" processors:"); + lines.push(" - batch:"); + lines.push(" exporter:"); + + const { exporterType, exporterEndpoint, exporterProtocol } = sdk.tracerProvider; + + if (exporterType === "console") { + lines.push(" console:"); + } else { + lines.push(` ${exporterType}:`); + if (exporterEndpoint) { + lines.push(` endpoint: ${exporterEndpoint}`); + } + if (exporterType === "otlp_http" && exporterProtocol) { + lines.push(` protocol: ${exporterProtocol}`); + } + } + + // sampler + const { samplerType, samplerRoot, samplerRatio } = sdk.tracerProvider; + lines.push(" sampler:"); + + if (samplerType === "parent_based") { + lines.push(" parent_based:"); + lines.push(" root:"); + if (samplerRoot === "trace_id_ratio_based") { + lines.push(" trace_id_ratio_based:"); + lines.push(` ratio: ${samplerRatio}`); + } else { + lines.push(` ${samplerRoot}:`); + } + } else if (samplerType === "trace_id_ratio_based") { + lines.push(" trace_id_ratio_based:"); + lines.push(` ratio: ${samplerRatio}`); + } else { + lines.push(` ${samplerType}:`); + } + + // propagator + if (sdk.propagators.length > 0) { + lines.push("propagator:"); + lines.push(" composite:"); + for (const propagator of sdk.propagators) { + lines.push(` - ${propagator}:`); + } + } + + return lines.join("\n"); +} diff --git a/ecosystem-explorer/src/features/java-agent/configuration/utils/yaml-generator.ts b/ecosystem-explorer/src/features/java-agent/configuration/utils/yaml-generator.ts new file mode 100644 index 0000000..bac7be1 --- /dev/null +++ b/ecosystem-explorer/src/features/java-agent/configuration/utils/yaml-generator.ts @@ -0,0 +1,131 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { ConfigurationBuilderState } from "@/types/javaagent"; +import { flatToDeclarative } from "./config-name-converter"; +import { generateSdkYaml } from "./sdk-yaml-generator"; + +type NestedConfig = Record; + +function setNestedValue(obj: NestedConfig, path: string[], value: unknown): void { + let current = obj; + + for (let i = 0; i < path.length - 1; i++) { + const key = path[i]; + if (!(key in current)) { + current[key] = {}; + } + current = current[key] as NestedConfig; + } + + const lastKey = path[path.length - 1]; + current[lastKey] = value; +} + +function formatYamlValue(value: unknown): string { + if (typeof value === "boolean") { + return String(value); + } + if (typeof value === "number") { + return String(value); + } + if (typeof value === "string") { + if (value === "") { + return '""'; + } + if (value.includes(",") || value.includes(":") || value.includes("#")) { + return `"${value}"`; + } + return value; + } + if (Array.isArray(value)) { + if (value.length === 0) { + return "[]"; + } + return `[${value.map(formatYamlValue).join(", ")}]`; + } + return String(value); +} + +function serializeYaml(obj: NestedConfig, indent = 0): string { + const lines: string[] = []; + const indentStr = " ".repeat(indent); + + const sortedKeys = Object.keys(obj).sort((a, b) => a.localeCompare(b)); + + for (const key of sortedKeys) { + const value = obj[key]; + if (typeof value === "object" && value !== null && !Array.isArray(value)) { + if (Object.keys(value as NestedConfig).length === 0) continue; + lines.push(`${indentStr}${key}:`); + lines.push(serializeYaml(value as NestedConfig, indent + 1)); + } else { + lines.push(`${indentStr}${key}: ${formatYamlValue(value)}`); + } + } + + return lines.join("\n"); +} + +export function generateYamlFile(state: ConfigurationBuilderState): string { + const lines: string[] = []; + + lines.push("# OpenTelemetry Java Agent Configuration"); + lines.push("# Generated by Ecosystem Explorer"); + lines.push(`# Version: ${state.version}`); + lines.push(""); + + const instrumentations = Array.from(state.selectedInstrumentations.values()); + + // Build instrumentation config block + const seenKeys = new Set(); + const rootConfig: NestedConfig = {}; + + for (const instrumentation of instrumentations) { + const configs = + instrumentation.data.configurations?.filter((config) => + instrumentation.enabledConfigs.has(config.name) + ) || []; + + for (const config of configs) { + const declarativeName = flatToDeclarative(config.name); + if (!seenKeys.has(declarativeName)) { + seenKeys.add(declarativeName); + const pathSegments = declarativeName.split("."); + setNestedValue(rootConfig, pathSegments, config.default); + } + } + } + + const hasInstrumentation = Object.keys(rootConfig).length > 0; + const hasSdk = true; // SDK config is always included + + if (hasSdk) { + lines.push("# --- SDK Configuration ---"); + lines.push(generateSdkYaml(state.sdkConfig)); + } + + if (hasInstrumentation) { + if (hasSdk) lines.push(""); + lines.push("# --- Instrumentation Configuration ---"); + lines.push(serializeYaml(rootConfig)); + } + + if (!hasSdk && !hasInstrumentation) { + lines.push("# No configurations selected"); + } + + return lines.join("\n"); +} diff --git a/ecosystem-explorer/src/hooks/use-sdk-options.ts b/ecosystem-explorer/src/hooks/use-sdk-options.ts new file mode 100644 index 0000000..ca1e2bb --- /dev/null +++ b/ecosystem-explorer/src/hooks/use-sdk-options.ts @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useState, useEffect } from "react"; +import type { SdkOptions } from "@/types/sdk"; + +interface DataState { + data: T | null; + loading: boolean; + error: Error | null; +} + +export function useSdkOptions(): DataState { + const [state, setState] = useState>({ + data: null, + loading: true, + error: null, + }); + + useEffect(() => { + let cancelled = false; + + async function loadData() { + try { + const res = await fetch("/data/sdk/sdk-options.json"); + if (!res.ok) throw new Error(`Failed to load sdk-options.json: ${res.status}`); + const data: SdkOptions = await res.json(); + if (!cancelled) { + setState({ data, loading: false, error: null }); + } + } catch (error) { + if (!cancelled) { + setState({ + data: null, + loading: false, + error: error instanceof Error ? error : new Error(String(error)), + }); + } + } + } + + loadData(); + + return () => { + cancelled = true; + }; + }, []); + + return state; +} diff --git a/ecosystem-explorer/src/types/javaagent.ts b/ecosystem-explorer/src/types/javaagent.ts index fcc27e5..2d249e6 100644 --- a/ecosystem-explorer/src/types/javaagent.ts +++ b/ecosystem-explorer/src/types/javaagent.ts @@ -87,3 +87,87 @@ export interface Attribute { | "DOUBLE_ARRAY" | "BOOLEAN_ARRAY"; } + +export interface ConfigurationBuilderState { + version: string; + activeArea: "instrumentation" | "sdk"; + selectedInstrumentations: Map; + configOverrides: Map; + outputFormat: "properties" | "env"; + isInitialized: boolean; + sdkConfig: import("@/types/sdk").SdkConfig; +} + +export interface InstrumentationConfig { + name: string; + data: InstrumentationData; + enabledConfigs: Set; +} + +export interface ConfigValue { + name: string; + value: string | boolean | number; + isModified: boolean; + default: string | boolean | number; +} + +export interface CommonConfig { + name: string; + config: Configuration; + usedBy: string[]; +} + +export type ConfigurationBuilderAction = + | { type: "SET_VERSION"; version: string } + | { type: "SET_ACTIVE_AREA"; area: "instrumentation" | "sdk" } + | { + type: "ADD_INSTRUMENTATION"; + name: string; + data: InstrumentationData; + } + | { type: "REMOVE_INSTRUMENTATION"; name: string } + | { + type: "UPDATE_CONFIG"; + configName: string; + value: string | boolean | number; + } + | { type: "TOGGLE_CONFIG"; instrumentationName: string; configName: string } + | { type: "SET_OUTPUT_FORMAT"; format: "properties" | "env" } + | { + type: "LOAD_STATE"; + state: Partial; + } + | { type: "ADD_ALL_INSTRUMENTATIONS"; instrumentations: InstrumentationData[] } + | { type: "REMOVE_ALL_INSTRUMENTATIONS" } + | { type: "LOAD_SDK_DEFAULTS"; sdkConfig: import("@/types/sdk").SdkConfig } + | { type: "TOGGLE_SDK_PROPAGATOR"; propagatorId: string } + | { type: "SET_SDK_EXPORTER_TYPE"; exporterType: string } + | { type: "SET_SDK_EXPORTER_ENDPOINT"; endpoint: string } + | { type: "SET_SDK_EXPORTER_PROTOCOL"; protocol: string } + | { type: "SET_SDK_SAMPLER_TYPE"; samplerType: string } + | { type: "SET_SDK_SAMPLER_ROOT"; root: string } + | { type: "SET_SDK_SAMPLER_RATIO"; ratio: number } + | { type: "UPDATE_SDK_BATCH_SETTING"; key: string; value: number } + | { type: "RESET" } + | { type: "MARK_INITIALIZED" }; + +export interface Template { + id: string; + name: string; + description: string; + category: "framework" | "experimental" | "semconv" | "custom"; + instrumentations: string[]; + configOverrides?: Record; +} + +export interface ShareConfig { + v: string; + i: string[]; + c: Record; + f?: "properties" | "env"; +} + +export interface ImportConfig { + v: string; + i: string[]; +} diff --git a/ecosystem-explorer/src/types/sdk.ts b/ecosystem-explorer/src/types/sdk.ts new file mode 100644 index 0000000..3b4902f --- /dev/null +++ b/ecosystem-explorer/src/types/sdk.ts @@ -0,0 +1,128 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Runtime state stored in the builder context +export interface SdkConfig { + fileFormat: string; + propagators: string[]; + tracerProvider: { + exporterType: string; + exporterEndpoint: string; + exporterProtocol: string; + samplerType: string; + samplerRoot: string; + samplerRatio: number; + batchScheduleDelay: number; + batchExportTimeout: number; + batchMaxQueueSize: number; + batchMaxExportBatchSize: number; + }; +} + +// sdk-options.json shape — source of truth for defaults and UI metadata +export interface SdkOptions { + schema_version: string; + defaults: SdkDefaults; + sections: SdkSections; +} + +export interface SdkDefaults { + file_format: string; + propagators: string[]; + tracer_provider: { + exporter_type: string; + exporters: Record>; + sampler: { + type: string; + root: string; + ratio: number; + }; + batch_processor: { + schedule_delay: number; + export_timeout: number; + max_queue_size: number; + max_export_batch_size: number; + }; + }; +} + +export interface SdkSections { + propagators: SdkPropagatorSection; + tracer_provider: SdkTracerProviderSection; +} + +export interface SdkPropagatorSection { + name: string; + description: string; + options: SdkPropagatorOption[]; +} + +export interface SdkPropagatorOption { + id: string; + name: string; + description: string; +} + +export interface SdkTracerProviderSection { + name: string; + description: string; + exporter: SdkExporterConfig; + sampler: SdkSamplerConfig; + batch_processor: SdkBatchProcessorConfig; +} + +export interface SdkExporterConfig { + name: string; + description: string; + options: SdkExporterOption[]; +} + +export interface SdkExporterOption { + id: string; + name: string; + description: string; + settings: SdkSettingField[]; +} + +export interface SdkSamplerConfig { + name: string; + description: string; + options: SdkSamplerOption[]; +} + +export interface SdkSamplerOption { + id: string; + name: string; + description: string; + has_root?: boolean; + root_options?: string[]; + has_ratio?: boolean; +} + +export interface SdkBatchProcessorConfig { + name: string; + description: string; + settings: SdkSettingField[]; +} + +export interface SdkSettingField { + name: string; + label: string; + description: string; + type: "string" | "integer" | "enum"; + options?: string[]; + min?: number; +} diff --git a/ecosystem-registry/configuration/schema/opentelemetry_configuration.yaml b/ecosystem-registry/configuration/schema/opentelemetry_configuration.yaml new file mode 100644 index 0000000..1a9499b --- /dev/null +++ b/ecosystem-registry/configuration/schema/opentelemetry_configuration.yaml @@ -0,0 +1,104 @@ +type: object +additionalProperties: true +properties: + file_format: + type: string + description: | + The file format version. + Represented as a string including the semver major, minor version numbers (and optionally the meta tag). For example: "0.4", "1.0-rc.2", "1.0" (after stable release). + See https://github.com/open-telemetry/opentelemetry-configuration/blob/main/VERSIONING.md for more details. + The yaml format is documented at https://github.com/open-telemetry/opentelemetry-configuration/tree/main/schema + disabled: + type: + - boolean + - "null" + description: | + Configure if the SDK is disabled or not. + defaultBehavior: false is used + log_level: + $ref: common.yaml#/$defs/SeverityNumber + description: | + Configure the log level of the internal logger used by the SDK. + defaultBehavior: INFO is used + attribute_limits: + $ref: "#/$defs/AttributeLimits" + description: | + Configure general attribute limits. See also tracer_provider.limits, logger_provider.limits. + defaultBehavior: default values as described in AttributeLimits are used + logger_provider: + $ref: "#/$defs/LoggerProvider" + description: | + Configure logger provider. + defaultBehavior: a noop logger provider is used + meter_provider: + $ref: "#/$defs/MeterProvider" + description: | + Configure meter provider. + defaultBehavior: a noop meter provider is used + propagator: + $ref: "#/$defs/Propagator" + description: | + Configure text map context propagators. + defaultBehavior: a noop propagator is used + tracer_provider: + $ref: "#/$defs/TracerProvider" + description: | + Configure tracer provider. + defaultBehavior: a noop tracer provider is used + resource: + $ref: "#/$defs/Resource" + description: | + Configure resource for all signals. + defaultBehavior: the default resource is used + instrumentation/development: + $ref: "#/$defs/ExperimentalInstrumentation" + description: | + Configure instrumentation. + defaultBehavior: instrumentation defaults are used + distribution: + $ref: "#/$defs/Distribution" + description: | + Defines configuration parameters specific to a particular OpenTelemetry distribution or vendor. + defaultBehavior: distribution defaults are used +required: + - file_format +$defs: + AttributeLimits: + type: object + additionalProperties: false + properties: + attribute_value_length_limit: + type: + - integer + - "null" + minimum: 0 + description: | + Configure max attribute value size. + Value must be non-negative. + defaultBehavior: there is no limit + attribute_count_limit: + type: + - integer + - "null" + minimum: 0 + description: | + Configure max attribute count. + Value must be non-negative. + defaultBehavior: 128 is used + LoggerProvider: + $ref: logger_provider.yaml + MeterProvider: + $ref: meter_provider.yaml + TracerProvider: + $ref: tracer_provider.yaml + Propagator: + $ref: propagator.yaml + Resource: + $ref: resource.yaml + ExperimentalInstrumentation: + $ref: instrumentation.yaml + Distribution: + type: object + additionalProperties: + type: object + minProperties: 1 diff --git a/ecosystem-registry/configuration/schema/propagator.yaml b/ecosystem-registry/configuration/schema/propagator.yaml new file mode 100644 index 0000000..48e99cb --- /dev/null +++ b/ecosystem-registry/configuration/schema/propagator.yaml @@ -0,0 +1,62 @@ +type: object +additionalProperties: false +properties: + composite: + type: array + minItems: 1 + items: + $ref: "#/$defs/TextMapPropagator" + description: | + Configure the propagators in the composite text map propagator. + Built-in propagator keys include: tracecontext, baggage, b3, b3multi. + defaultBehavior: a noop propagator is used + composite_list: + type: + - string + - "null" + description: | + Configure the propagators as a comma-separated string (matches OTEL_PROPAGATORS format). + Entries are appended to .composite with duplicates filtered out. + defaultBehavior: a noop propagator is used +$defs: + TextMapPropagator: + type: object + additionalProperties: + type: + - object + - "null" + minProperties: 1 + maxProperties: 1 + properties: + tracecontext: + $ref: "#/$defs/TraceContextPropagator" + description: Include the W3C trace context propagator. + baggage: + $ref: "#/$defs/BaggagePropagator" + description: Include the W3C baggage propagator. + b3: + $ref: "#/$defs/B3Propagator" + description: Include the Zipkin B3 single-header propagator. + b3multi: + $ref: "#/$defs/B3MultiPropagator" + description: Include the Zipkin B3 multi-header propagator. + TraceContextPropagator: + type: + - object + - "null" + additionalProperties: false + BaggagePropagator: + type: + - object + - "null" + additionalProperties: false + B3Propagator: + type: + - object + - "null" + additionalProperties: false + B3MultiPropagator: + type: + - object + - "null" + additionalProperties: false diff --git a/ecosystem-registry/configuration/schema/tracer_provider.yaml b/ecosystem-registry/configuration/schema/tracer_provider.yaml new file mode 100644 index 0000000..c9a26a6 --- /dev/null +++ b/ecosystem-registry/configuration/schema/tracer_provider.yaml @@ -0,0 +1,237 @@ +type: object +additionalProperties: false +properties: + processors: + type: array + minItems: 1 + items: + $ref: "#/$defs/SpanProcessor" + description: Configure span processors. + limits: + $ref: "#/$defs/SpanLimits" + description: Configure span limits. See also attribute_limits. + defaultBehavior: default values as described in SpanLimits are used + sampler: + $ref: "#/$defs/Sampler" + description: | + Configure the sampler. + defaultBehavior: parent based sampler with a root of always_on is used +required: + - processors +$defs: + BatchSpanProcessor: + type: object + additionalProperties: false + properties: + schedule_delay: + type: + - integer + - "null" + minimum: 0 + description: | + Configure delay interval (in milliseconds) between two consecutive exports. + Value must be non-negative. + defaultBehavior: 5000 is used + export_timeout: + type: + - integer + - "null" + minimum: 0 + description: | + Configure maximum allowed time (in milliseconds) to export data. + Value must be non-negative. A value of 0 indicates no limit (infinity). + defaultBehavior: 30000 is used + max_queue_size: + type: + - integer + - "null" + exclusiveMinimum: 0 + description: | + Configure maximum queue size. Value must be positive. + defaultBehavior: 2048 is used + max_export_batch_size: + type: + - integer + - "null" + exclusiveMinimum: 0 + description: | + Configure maximum batch size. Value must be positive. + defaultBehavior: 512 is used + exporter: + $ref: "#/$defs/SpanExporter" + description: Configure exporter. + required: + - exporter + Sampler: + type: object + additionalProperties: + type: + - object + - "null" + minProperties: 1 + maxProperties: 1 + properties: + always_off: + $ref: "#/$defs/AlwaysOffSampler" + description: Configure sampler to be always_off. + always_on: + $ref: "#/$defs/AlwaysOnSampler" + description: Configure sampler to be always_on. + parent_based: + $ref: "#/$defs/ParentBasedSampler" + description: Configure sampler to be parent_based. + trace_id_ratio_based: + $ref: "#/$defs/TraceIdRatioBasedSampler" + description: Configure sampler to be trace_id_ratio_based. + AlwaysOffSampler: + type: + - object + - "null" + additionalProperties: false + AlwaysOnSampler: + type: + - object + - "null" + additionalProperties: false + ParentBasedSampler: + type: + - object + - "null" + additionalProperties: false + properties: + root: + $ref: "#/$defs/Sampler" + description: | + Configure root sampler. + defaultBehavior: always_on is used + remote_parent_sampled: + $ref: "#/$defs/Sampler" + description: Configure remote_parent_sampled sampler. + defaultBehavior: always_on is used + remote_parent_not_sampled: + $ref: "#/$defs/Sampler" + description: Configure remote_parent_not_sampled sampler. + defaultBehavior: always_off is used + local_parent_sampled: + $ref: "#/$defs/Sampler" + description: Configure local_parent_sampled sampler. + defaultBehavior: always_on is used + local_parent_not_sampled: + $ref: "#/$defs/Sampler" + description: Configure local_parent_not_sampled sampler. + defaultBehavior: always_off is used + TraceIdRatioBasedSampler: + type: + - object + - "null" + additionalProperties: false + properties: + ratio: + type: + - number + - "null" + minimum: 0 + maximum: 1 + description: | + Configure trace_id_ratio. + defaultBehavior: 1.0 is used + SimpleSpanProcessor: + type: object + additionalProperties: false + properties: + exporter: + $ref: "#/$defs/SpanExporter" + description: Configure exporter. + required: + - exporter + SpanExporter: + type: object + additionalProperties: + type: + - object + - "null" + minProperties: 1 + maxProperties: 1 + properties: + otlp_http: + $ref: common.yaml#/$defs/OtlpHttpExporter + description: Configure exporter to be OTLP with HTTP transport. + otlp_grpc: + $ref: common.yaml#/$defs/OtlpGrpcExporter + description: Configure exporter to be OTLP with gRPC transport. + console: + $ref: common.yaml#/$defs/ConsoleExporter + description: Configure exporter to be console. + SpanLimits: + type: object + additionalProperties: false + properties: + attribute_value_length_limit: + type: + - integer + - "null" + minimum: 0 + description: | + Configure max attribute value size. Overrides .attribute_limits.attribute_value_length_limit. + Value must be non-negative. + defaultBehavior: there is no limit + attribute_count_limit: + type: + - integer + - "null" + minimum: 0 + description: | + Configure max attribute count. Overrides .attribute_limits.attribute_count_limit. + Value must be non-negative. + defaultBehavior: 128 is used + event_count_limit: + type: + - integer + - "null" + minimum: 0 + description: | + Configure max span event count. + Value must be non-negative. + defaultBehavior: 128 is used + link_count_limit: + type: + - integer + - "null" + minimum: 0 + description: | + Configure max span link count. + Value must be non-negative. + defaultBehavior: 128 is used + event_attribute_count_limit: + type: + - integer + - "null" + minimum: 0 + description: | + Configure max attributes per span event. + Value must be non-negative. + defaultBehavior: 128 is used + link_attribute_count_limit: + type: + - integer + - "null" + minimum: 0 + description: | + Configure max attributes per span link. + Value must be non-negative. + defaultBehavior: 128 is used + SpanProcessor: + type: object + additionalProperties: + type: + - object + - "null" + minProperties: 1 + maxProperties: 1 + properties: + batch: + $ref: "#/$defs/BatchSpanProcessor" + description: Configure a batch span processor. + simple: + $ref: "#/$defs/SimpleSpanProcessor" + description: Configure a simple span processor.