-
Notifications
You must be signed in to change notification settings - Fork 24.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add unstable plugin API telemetry (#87061)
This commit adds some new information to the node info and cluster stats responses regarding plugins. For each plugin returned, in addition to the plugin descriptor, a boolean indicating whether the plugin is an official plugin, as well as info on the particular interfaces and methods that the plugin implements are returned. This will allow measuring the effectiveness over time of the stable plugin API. Co-authored-by: ChrisHegarty <christopher.hegarty@elastic.co>
- Loading branch information
1 parent
d52e1dc
commit 8d502e6
Showing
17 changed files
with
749 additions
and
136 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
44 changes: 44 additions & 0 deletions
44
server/src/main/java/org/elasticsearch/plugins/PluginApiInfo.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
package org.elasticsearch.plugins; | ||
|
||
import org.elasticsearch.common.io.stream.StreamInput; | ||
import org.elasticsearch.common.io.stream.StreamOutput; | ||
import org.elasticsearch.common.io.stream.Writeable; | ||
import org.elasticsearch.xcontent.ToXContentFragment; | ||
import org.elasticsearch.xcontent.XContentBuilder; | ||
|
||
import java.io.IOException; | ||
import java.util.List; | ||
|
||
/** | ||
* Information about APIs extended by a custom plugin. | ||
* | ||
* @param legacyInterfaces Plugin API interfaces that the plugin implemented, introspected at runtime. | ||
* @param legacyMethods Method names overriden from the {@link Plugin} class and Plugin API interfaces | ||
*/ | ||
public record PluginApiInfo(List<String> legacyInterfaces, List<String> legacyMethods) implements Writeable, ToXContentFragment { | ||
|
||
public PluginApiInfo(StreamInput in) throws IOException { | ||
this(in.readImmutableList(StreamInput::readString), in.readImmutableList(StreamInput::readString)); | ||
} | ||
|
||
@Override | ||
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { | ||
builder.field("legacy_interfaces", legacyInterfaces); | ||
builder.field("legacy_methods", legacyMethods); | ||
return builder; | ||
} | ||
|
||
@Override | ||
public void writeTo(StreamOutput out) throws IOException { | ||
out.writeStringCollection(legacyInterfaces); | ||
out.writeStringCollection(legacyMethods); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
133 changes: 133 additions & 0 deletions
133
server/src/main/java/org/elasticsearch/plugins/PluginIntrospector.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
package org.elasticsearch.plugins; | ||
|
||
import org.elasticsearch.core.SuppressForbidden; | ||
|
||
import java.lang.reflect.Method; | ||
import java.lang.reflect.Modifier; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.function.Function; | ||
import java.util.stream.Stream; | ||
|
||
import static java.util.stream.Collectors.toMap; | ||
|
||
final class PluginIntrospector { | ||
|
||
private final Set<Class<?>> pluginClasses = Set.of( | ||
Plugin.class, | ||
ActionPlugin.class, | ||
AnalysisPlugin.class, | ||
CircuitBreakerPlugin.class, | ||
ClusterPlugin.class, | ||
DiscoveryPlugin.class, | ||
EnginePlugin.class, | ||
ExtensiblePlugin.class, | ||
HealthPlugin.class, | ||
IndexStorePlugin.class, | ||
IngestPlugin.class, | ||
MapperPlugin.class, | ||
NetworkPlugin.class, | ||
PersistentTaskPlugin.class, | ||
RecoveryPlannerPlugin.class, | ||
ReloadablePlugin.class, | ||
RepositoryPlugin.class, | ||
ScriptPlugin.class, | ||
SearchPlugin.class, | ||
ShutdownAwarePlugin.class, | ||
SystemIndexPlugin.class | ||
); | ||
|
||
private record MethodType(String name, Class<?>[] parameterTypes) {} | ||
|
||
private final Map<Class<?>, List<MethodType>> pluginMethodsMap; | ||
|
||
private PluginIntrospector() { | ||
pluginMethodsMap = pluginClasses.stream().collect(toMap(Function.identity(), PluginIntrospector::findMethods)); | ||
} | ||
|
||
static PluginIntrospector getInstance() { | ||
return new PluginIntrospector(); | ||
} | ||
|
||
/** | ||
* Returns the list of Elasticsearch plugin interfaces implemented by the given plugin | ||
* implementation class. The list contains the simple names of the interfaces. | ||
*/ | ||
List<String> interfaces(final Class<?> pluginClass) { | ||
assert Plugin.class.isAssignableFrom(pluginClass); | ||
return interfaceClasses(pluginClass).map(Class::getSimpleName).sorted().toList(); | ||
} | ||
|
||
/** | ||
* Returns the list of methods overridden by the given plugin implementation class. The list | ||
* contains the simple names of the methods. | ||
*/ | ||
List<String> overriddenMethods(final Class<?> pluginClass) { | ||
assert Plugin.class.isAssignableFrom(pluginClass); | ||
List<Class<?>> esPluginClasses = Stream.concat(Stream.of(Plugin.class), interfaceClasses(pluginClass)).toList(); | ||
|
||
List<String> overriddenMethods = new ArrayList<>(); | ||
for (var esPluginClass : esPluginClasses) { | ||
List<MethodType> esPluginMethods = pluginMethodsMap.get(esPluginClass); | ||
assert esPluginMethods != null : "no plugin methods for " + esPluginClass; | ||
for (var mt : esPluginMethods) { | ||
try { | ||
Method m = pluginClass.getMethod(mt.name(), mt.parameterTypes()); | ||
if (m.getDeclaringClass() == esPluginClass) { | ||
// it's not overridden | ||
} else { | ||
assert esPluginClass.isAssignableFrom(m.getDeclaringClass()); | ||
overriddenMethods.add(mt.name()); | ||
} | ||
} catch (NoSuchMethodException unexpected) { | ||
throw new AssertionError(unexpected); | ||
} | ||
} | ||
} | ||
return overriddenMethods.stream().sorted().toList(); | ||
} | ||
|
||
// Returns the non-static methods declared in the given class. | ||
@SuppressForbidden(reason = "Need declared methods") | ||
private static List<MethodType> findMethods(Class<?> cls) { | ||
assert cls.getName().startsWith("org.elasticsearch.plugins"); | ||
assert cls.isInterface() || cls == Plugin.class : cls; | ||
return Arrays.stream(cls.getDeclaredMethods()) | ||
.filter(m -> Modifier.isStatic(m.getModifiers()) == false) | ||
.map(m -> new MethodType(m.getName(), m.getParameterTypes())) | ||
.toList(); | ||
} | ||
|
||
// Returns a stream of o.e.XXXPlugin interfaces, that the given plugin class implements. | ||
private Stream<Class<?>> interfaceClasses(Class<?> pluginClass) { | ||
assert Plugin.class.isAssignableFrom(pluginClass); | ||
Set<Class<?>> pluginInterfaces = new HashSet<>(); | ||
do { | ||
Arrays.stream(pluginClass.getInterfaces()).forEach(inf -> superInterfaces(inf, pluginInterfaces)); | ||
} while ((pluginClass = pluginClass.getSuperclass()) != java.lang.Object.class); | ||
return pluginInterfaces.stream(); | ||
} | ||
|
||
private void superInterfaces(Class<?> c, Set<Class<?>> interfaces) { | ||
if (isESPlugin(c)) { | ||
interfaces.add(c); | ||
} | ||
Arrays.stream(c.getInterfaces()).forEach(inf -> superInterfaces(inf, interfaces)); | ||
} | ||
|
||
private boolean isESPlugin(Class<?> c) { | ||
return pluginClasses.contains(c); | ||
} | ||
} |
Oops, something went wrong.