Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions NOTICE.TXT
Original file line number Diff line number Diff line change
Expand Up @@ -11379,26 +11379,6 @@ 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.
==========
Notice for: org.reflections:reflections-0.10.2
----------

DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004

Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>

Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.

DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

0. You just DO WHAT THE FUCK YOU WANT TO.

Also licensed under BSD-2-Clause according to https://github.com/ronmamo/reflections/blob/0.9.11/pom.xml#L20-L22

==========
Notice for: org.slf4j:slf4j-api-1.7.30
----------
Expand Down
3 changes: 0 additions & 3 deletions logstash-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,6 @@ dependencies {
runtimeOnly 'commons-logging:commons-logging:1.3.1'
// also handle libraries relying on log4j 1.x to redirect their logs
runtimeOnly "org.apache.logging.log4j:log4j-1.2-api:${log4jVersion}"
implementation('org.reflections:reflections:0.10.2') {
exclude group: 'com.google.guava', module: 'guava'
}
implementation 'commons-codec:commons-codec:1.17.0' // transitively required by httpclient
// Jackson version moved to versions.yml in the project root (the JrJackson version is there too)
implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package org.logstash.plugins.discovery;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.annotation.Annotation;
import java.net.JarURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
* Minimal package scanner to locate classes within Logstash's own packages.
*/
final class PackageScanner {

private PackageScanner() {
}

static Set<Class<?>> scanForAnnotation(String basePackage, Class<? extends Annotation> annotation, ClassLoader loader) {
Set<String> classNames = collectClassNames(basePackage, loader);
Set<Class<?>> result = new HashSet<>();
for (String className : classNames) {
if (className.contains("$")) {
continue;
}
try {
Class<?> candidate = Class.forName(className, false, loader);
if (candidate.isAnnotationPresent(annotation)) {
result.add(candidate);
}
} catch (ClassNotFoundException | LinkageError e) {
throw new IllegalStateException("Unable to load class discovered during scanning: " + className, e);
}
}
return result;
}

private static Set<String> collectClassNames(String basePackage, ClassLoader loader) {
String resourcePath = basePackage.replace('.', '/');
Set<String> classNames = new HashSet<>();
try {
Enumeration<URL> resources = loader.getResources(resourcePath);
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
String protocol = resource.getProtocol();
if ("file".equals(protocol)) {
scanDirectory(resource, basePackage, classNames);
} else if ("jar".equals(protocol) || resource.toString().contains("!")) {
scanJar(resource, resourcePath, classNames);
Comment on lines +55 to +56
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check resource.toString().contains(\"!\") is fragile and could match false positives. Consider using a more robust approach to detect JAR URLs, such as checking if the URL can be cast to JarURLConnection.

Suggested change
} else if ("jar".equals(protocol) || resource.toString().contains("!")) {
scanJar(resource, resourcePath, classNames);
} else if ("jar".equals(protocol)) {
scanJar(resource, resourcePath, classNames);
} else {
// Attempt to detect JAR resources more robustly
try {
if (resource.openConnection() instanceof JarURLConnection) {
scanJar(resource, resourcePath, classNames);
}
} catch (IOException e) {
throw new UncheckedIOException("Failed to open connection for resource: " + resource, e);
}

Copilot uses AI. Check for mistakes.
}
}
} catch (IOException e) {
throw new UncheckedIOException("Failed to scan package: " + basePackage, e);
}
return classNames;
}

private static void scanDirectory(URL resource, String basePackage, Set<String> classNames) {
try {
Path directory = Paths.get(resource.toURI());
if (!Files.exists(directory)) {
return;
}
Files.walk(directory)
.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().endsWith(".class"))
.forEach(path -> {
Path relative = directory.relativize(path);
String className = basePackage + '.' + relative.toString()
.replace('/', '.').replace('\\', '.');
className = className.substring(0, className.length() - ".class".length());
classNames.add(className);
});
} catch (IOException | URISyntaxException e) {
throw new IllegalStateException("Failed to scan directory for classes: " + resource, e);
}
}

private static void scanJar(URL resource, String resourcePath, Set<String> classNames) {
try {
JarURLConnection connection = (JarURLConnection) resource.openConnection();
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider adding a comment explaining why caching is disabled for JAR connections, as this may have performance implications during plugin discovery.

Suggested change
JarURLConnection connection = (JarURLConnection) resource.openConnection();
JarURLConnection connection = (JarURLConnection) resource.openConnection();
// Disable caching to prevent file locking issues (especially on Windows) and to ensure
// that the JAR file can be closed and deleted after use.

Copilot uses AI. Check for mistakes.
connection.setUseCaches(false);
try (JarFile jarFile = connection.getJarFile()) {
String entryPrefix = connection.getEntryName();
if (entryPrefix == null || entryPrefix.isEmpty()) {
entryPrefix = resourcePath;
}
if (!entryPrefix.endsWith("/")) {
entryPrefix += "/";
}
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.isDirectory()) {
continue;
}
String name = entry.getName();
if (!name.endsWith(".class") || !name.startsWith(entryPrefix)) {
continue;
}
String className = name.substring(0, name.length() - ".class".length())
.replace('/', '.');
classNames.add(className);
}
}
} catch (IOException e) {
throw new UncheckedIOException("Failed to scan jar for classes: " + resource, e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,9 @@
import co.elastic.logstash.api.LogstashPlugin;
import co.elastic.logstash.api.Output;
import org.logstash.plugins.PluginLookup.PluginType;
import org.reflections.Reflections;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
Expand All @@ -47,7 +44,7 @@
* This is singleton ofr two reasons:
* <ul>
* <li>it's a registry so no need for multiple instances</li>
* <li>the Reflections library used need to run in single thread during discovery phase</li>
* <li>plugin discovery touches the classpath, so we keep it single-threaded</li>
* </ul>
* */
public final class PluginRegistry {
Expand Down Expand Up @@ -79,34 +76,30 @@ public static PluginRegistry getInstance() {

@SuppressWarnings("unchecked")
private void discoverPlugins() {
// the constructor of Reflection must be called only by one thread, else there is a
// risk that the first thread that completes close the Zip files for the others.
// scan all .class present in package classpath
final ConfigurationBuilder configurationBuilder = new ConfigurationBuilder()
.setUrls(ClasspathHelper.forPackage("org.logstash.plugins"))
.filterInputsBy(input -> input.endsWith(".class"));
Reflections reflections = new Reflections(configurationBuilder);

Set<Class<?>> annotated = reflections.getTypesAnnotatedWith(LogstashPlugin.class);
// the discovery runs single-threaded to avoid concurrent access issues while scanning
Set<Class<?>> annotated = PackageScanner.scanForAnnotation("org.logstash.plugins", LogstashPlugin.class, PluginRegistry.class.getClassLoader());
for (final Class<?> cls : annotated) {
for (final Annotation annotation : cls.getAnnotations()) {
if (annotation instanceof LogstashPlugin) {
String name = ((LogstashPlugin) annotation).name();
if (Filter.class.isAssignableFrom(cls)) {
filters.put(name, (Class<Filter>) cls);
}
if (Output.class.isAssignableFrom(cls)) {
outputs.put(name, (Class<Output>) cls);
}
if (Input.class.isAssignableFrom(cls)) {
inputs.put(name, (Class<Input>) cls);
}
if (Codec.class.isAssignableFrom(cls)) {
codecs.put(name, (Class<Codec>) cls);
}

break;
}
if (Modifier.isAbstract(cls.getModifiers())) {
continue;
}

LogstashPlugin annotation = cls.getAnnotation(LogstashPlugin.class);
if (annotation == null) {
continue;
}
Comment on lines +87 to +89
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The null check for annotation at line 87 is redundant since scanForAnnotation already filters for classes with the LogstashPlugin annotation. This check can be removed to simplify the code.

Suggested change
if (annotation == null) {
continue;
}

Copilot uses AI. Check for mistakes.

String name = annotation.name();
if (Filter.class.isAssignableFrom(cls)) {
filters.put(name, (Class<Filter>) cls);
}
if (Output.class.isAssignableFrom(cls)) {
outputs.put(name, (Class<Output>) cls);
}
if (Input.class.isAssignableFrom(cls)) {
inputs.put(name, (Class<Input>) cls);
}
if (Codec.class.isAssignableFrom(cls)) {
codecs.put(name, (Class<Codec>) cls);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.logstash.plugins.discovery;

import co.elastic.logstash.api.Codec;
import co.elastic.logstash.api.Filter;
import co.elastic.logstash.api.Input;
import co.elastic.logstash.api.Output;
import org.junit.Test;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

public class PluginRegistryTest {

private final PluginRegistry registry = PluginRegistry.getInstance();

@Test
public void discoversBuiltInInputPlugin() {
Class<?> input = registry.getInputClass("java_stdin");
assertNotNull(input);
assertTrue(Input.class.isAssignableFrom(input));
}

@Test
public void discoversBuiltInOutputPlugin() {
Class<?> output = registry.getOutputClass("java_stdout");
assertNotNull(output);
assertTrue(Output.class.isAssignableFrom(output));
}

@Test
public void discoversBuiltInFilterPlugin() {
Class<?> filter = registry.getFilterClass("java_uuid");
assertNotNull(filter);
assertTrue(Filter.class.isAssignableFrom(filter));
}

@Test
public void discoversBuiltInCodecPlugin() {
Class<?> codec = registry.getCodecClass("java_line");
assertNotNull(codec);
assertTrue(Codec.class.isAssignableFrom(codec));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ dependency,dependencyUrl,licenseOverride,copyright,sourceURL
"org.javassist:javassist:",https://github.com/jboss-javassist/javassist,Apache-2.0
"org.jruby:jruby-core:",http://jruby.org/,EPL-2.0
"org.logstash:jvm-options-parser:",http://github.com/elastic/logstash,Apache-2.0
"org.reflections:reflections:",https://github.com/ronmamo/reflections,BSD-2-Clause
"org.slf4j:slf4j-api:",http://www.slf4j.org/,MIT
"org.yaml:snakeyaml:",https://bitbucket.org/snakeyaml/snakeyaml/src/master/,Apache-2.0
"ostruct:",https://github.com/ruby/ostruct,BSD-2-Clause
Expand Down

This file was deleted.