Skip to content

Commit 1f5a383

Browse files
author
Kiran Kumar Veeravelly
committed
feat(osgi): add opt-in Apache Felix plugin framework
Add an opt-in plugin-loading mode backed by an embedded Apache Felix OSGi framework, enabled with the system property -Dplugin.framework=osgi. The default classpath loader remains the default when the flag is not set. When the flag is not set, plugin discovery is functionally identical to today: the same ServiceLoader-based providers are returned in the same order, the OSGi framework never starts, and no OSGi code path executes. The only added work in legacy mode is the construction of a couple of inert Spring beans. The new plugin-framework-osgi module embeds the Felix lifecycle (FelixPluginManager), bootstraps it in OSGi mode through a Spring-managed runner (OsgiFrameworkRunner), and exposes a PluginProvider backed by the OSGi service registry (OsgiPluginRegistry). At startup in OSGi mode, StaticBundleLoader installs each legacy plugin JAR through BundleAdapter, which installs already-OSGi JARs directly and wraps legacy (non-OSGi) JARs by generating a versioned OSGi manifest. Wrapped plugins' @DataPrepperPlugin classes are registered as OSGi services (LegacyPluginBundleActivator). Data Prepper API packages are exported to bundles with semver version attributes read from data-prepper-api build metadata (DataPrepperOsgiPackages, DataPrepperApiVersion), consistent with the data-prepper-api backward-compatibility contract from #6607, so a plugin built on 2.15 resolves on a 2.16 host. DataPrepperOsgiPackages also exports the framework package org.opensearch.dataprepper.plugin.osgi so adapted bundles can load the LegacyPluginBundleActivator from the system bundle. StaticBundleLoader installs, resolves, and starts bundles with fail-fast behavior, a startup summary log, and Micrometer metrics. BundleResolutionErrorTranslator turns Felix resolution failures into readable diagnostics, and BundleHealthCheck verifies framework, bundle, and classloader isolation state. BundleClassLoaderScope manages the thread context classloader at the plugin invocation boundary so ServiceLoader and SPI resolve against the bundle classloader. PluginHealthProbe is a seam for future functional health checks. The OsgiFrameworkRunner bean is always instantiated by Spring but does nothing unless -Dplugin.framework=osgi is set, because it returns early in its @PostConstruct method. The Felix framework JAR ships on the runtime classpath as a small (<1MB) library with zero transitive dependencies, but it is not exercised in legacy mode. Scope is limited to classloader isolation and deploy-time validation. Bundles install once at startup and stop once at shutdown, the same lifecycle as classpath plugins today. No production hot-reload is included. The PluginHotLoader helper is test-scoped only and not on the production classpath. Wiring changes: settings.gradle adds the module, a Felix version-catalog entry, and the bnd plugin; build-resources.gradle adds the module to coreProjects; data-prepper-core declares a runtimeOnly dependency on the new module; PluginProviderLoader is made public with a registerProvider() hook and caches the framework-mode flag at construction; a test in data-prepper-plugin-framework's PluginProviderLoaderTest verifies the mode-caching behavior. Legacy behavior is unchanged. A new, permanent, minimal example plugin data-prepper-plugins/echo-processor is added to the build to demonstrate a minimal @DataPrepperPlugin and the SPI path. Resolves #6760 Signed-off-by: Kiran Kumar Veeravelly <veeravkk@amazon.com>
1 parent 4818896 commit 1f5a383

48 files changed

Lines changed: 5765 additions & 27 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build-resources.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ ext.coreProjects = [
1919
project(':data-prepper-test'),
2020
project(':data-prepper-plugin-framework'),
2121
project(':data-prepper-plugin-schema'),
22-
project(':data-prepper-plugin-schema-cli')
22+
project(':data-prepper-plugin-schema-cli'),
23+
project(':plugin-framework-osgi')
2324
]

data-prepper-core/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ dependencies {
3737
implementation project(':data-prepper-logstash-configuration')
3838
implementation project(':data-prepper-pipeline-parser')
3939
implementation project(':data-prepper-plugin-framework')
40+
runtimeOnly project(':plugin-framework-osgi')
4041
testImplementation project(':data-prepper-plugin-framework').sourceSets.test.output
4142
testImplementation project(':data-prepper-plugins:common').sourceSets.test.output
4243
testImplementation project(':data-prepper-plugins:file-source')
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Data Prepper OSGi Gradle Plugin
2+
3+
A standalone, publishable Gradle plugin that prepares a Data Prepper plugin JAR
4+
for OSGi consumption by baking an OSGi-compliant manifest into it at build time.
5+
6+
This replaces the runtime JAR-to-bundle adaptation previously performed by
7+
`BundleAdapter` in `plugin-framework-osgi`. By generating the manifest at build
8+
time, plugins are valid OSGi bundles from the moment they are built — no runtime
9+
repackaging is required.
10+
11+
## Plugin ID
12+
13+
```
14+
org.opensearch.dataprepper.osgi
15+
```
16+
17+
## Usage
18+
19+
### Internal Data Prepper plugin projects
20+
21+
In your plugin's `build.gradle`:
22+
23+
```groovy
24+
plugins {
25+
id 'org.opensearch.dataprepper.osgi'
26+
}
27+
```
28+
29+
### External plugin authors
30+
31+
Add the plugin to your `buildscript` dependencies or plugin management:
32+
33+
```groovy
34+
// settings.gradle
35+
pluginManagement {
36+
repositories {
37+
mavenCentral()
38+
// or wherever Data Prepper publishes its artifacts
39+
}
40+
plugins {
41+
id 'org.opensearch.dataprepper.osgi' version '<data-prepper-version>'
42+
}
43+
}
44+
```
45+
46+
Then in your plugin project's `build.gradle`:
47+
48+
```groovy
49+
plugins {
50+
id 'org.opensearch.dataprepper.osgi'
51+
}
52+
```
53+
54+
## Requirements
55+
56+
Your project must include a resource file at:
57+
58+
```
59+
src/main/resources/META-INF/data-prepper.plugins.properties
60+
```
61+
62+
With the following property:
63+
64+
```properties
65+
org.opensearch.dataprepper.plugin.packages=com.example.myplugin
66+
```
67+
68+
The value is a comma-separated list of Java package names that contain your
69+
`@DataPrepperPlugin`-annotated classes. The plugin scans these packages at
70+
bundle activation time to register your plugin classes with the OSGi service
71+
registry.
72+
73+
If this file is absent, the build will fail with a clear error message.
74+
75+
## Generated Manifest Headers
76+
77+
When applied, this plugin produces the following OSGi manifest headers in the
78+
output JAR:
79+
80+
| Header | Value | Derivation |
81+
|--------|-------|------------|
82+
| `Bundle-SymbolicName` | `org.opensearch.dataprepper.plugin.<sanitized-name>` | Project name with non-alphanumeric chars replaced by dots |
83+
| `Bundle-Version` | OSGi-normalized project version | `2.16.0-SNAPSHOT` becomes `2.16.0`; ensures 3-part numeric |
84+
| `Export-Package` | All packages except `*.internal.*` | Computed by bnd from compiled bytecode |
85+
| `Import-Package` | `*` (bnd default) | Computed by bnd from bytecode dependency analysis |
86+
| `Bundle-Activator` | `org.opensearch.dataprepper.plugin.osgi.LegacyPluginBundleActivator` | Fixed; provided by `plugin-framework-osgi` at runtime |
87+
| `DataPrepper-Plugin-Classes` | Comma-separated package names | Read from `data-prepper.plugins.properties` |
88+
89+
## How it works
90+
91+
1. The plugin applies `biz.aQute.bnd.builder` to the consuming project.
92+
2. After project evaluation, it reads `data-prepper.plugins.properties` from the
93+
main resource directories.
94+
3. It configures the jar task's bnd instructions with the headers listed above.
95+
4. At jar time, bnd analyzes the compiled bytecode to compute accurate
96+
`Import-Package` and `Export-Package` values — this is more reliable than the
97+
runtime heuristic approach that `BundleAdapter` used.
98+
99+
## Comparison with BundleAdapter
100+
101+
| Aspect | BundleAdapter (runtime) | This plugin (build time) |
102+
|--------|------------------------|--------------------------|
103+
| When it runs | At application startup | At build time |
104+
| Import-Package | Static list of shared API packages | bnd computes from actual bytecode |
105+
| Export-Package | Discovered by scanning JAR entries | bnd computes from compiled classes |
106+
| Bundle-Version | Always `1.0.0` | Actual project version (OSGi-normalized) |
107+
| Performance | Rewrites JARs at startup | Zero startup cost |
108+
| External plugins | Must be adapted at runtime | Already valid bundles |
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*
5+
* The OpenSearch Contributors require contributions made to
6+
* this file be licensed under the Apache-2.0 license or a
7+
* compatible open source license.
8+
*
9+
*/
10+
11+
plugins {
12+
id 'java-gradle-plugin'
13+
id 'maven-publish'
14+
}
15+
16+
group = 'org.opensearch.dataprepper'
17+
18+
repositories {
19+
mavenCentral()
20+
gradlePluginPortal()
21+
}
22+
23+
dependencies {
24+
implementation gradleApi()
25+
implementation 'biz.aQute.bnd:biz.aQute.bnd.gradle:7.0.0'
26+
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2'
27+
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.2'
28+
testImplementation 'org.hamcrest:hamcrest:2.2'
29+
}
30+
31+
gradlePlugin {
32+
plugins {
33+
dataPrepperOsgi {
34+
id = 'org.opensearch.dataprepper.osgi'
35+
implementationClass = 'org.opensearch.dataprepper.gradle.plugin.osgi.DataPrepperOsgiPlugin'
36+
}
37+
}
38+
}
39+
40+
test {
41+
useJUnitPlatform()
42+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*
5+
* The OpenSearch Contributors require contributions made to
6+
* this file be licensed under the Apache-2.0 license or a
7+
* compatible open source license.
8+
*/
9+
10+
pluginManagement {
11+
repositories {
12+
mavenCentral()
13+
gradlePluginPortal()
14+
}
15+
}
16+
17+
rootProject.name = 'osgi-plugin'

0 commit comments

Comments
 (0)