Skip to content

Commit

Permalink
Add unstable plugin API telemetry (#87061)
Browse files Browse the repository at this point in the history
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
rjernst and ChrisHegarty committed May 25, 2022
1 parent d52e1dc commit 8d502e6
Show file tree
Hide file tree
Showing 17 changed files with 749 additions and 136 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@

package org.elasticsearch.action.admin.cluster.node.info;

import org.elasticsearch.Version;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.node.ReportingService;
import org.elasticsearch.plugins.PluginDescriptor;
import org.elasticsearch.plugins.PluginRuntimeInfo;
import org.elasticsearch.xcontent.XContentBuilder;

import java.io.IOException;
Expand All @@ -24,32 +26,34 @@
* Information about plugins and modules
*/
public class PluginsAndModules implements ReportingService.Info {
private final List<PluginDescriptor> plugins;
private final List<PluginRuntimeInfo> plugins;
private final List<PluginDescriptor> modules;

public PluginsAndModules(List<PluginDescriptor> plugins, List<PluginDescriptor> modules) {
public PluginsAndModules(List<PluginRuntimeInfo> plugins, List<PluginDescriptor> modules) {
this.plugins = Collections.unmodifiableList(plugins);
this.modules = Collections.unmodifiableList(modules);
}

public PluginsAndModules(StreamInput in) throws IOException {
this.plugins = Collections.unmodifiableList(in.readList(PluginDescriptor::new));
this.plugins = in.readImmutableList(PluginRuntimeInfo::new);
this.modules = Collections.unmodifiableList(in.readList(PluginDescriptor::new));
}

@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeList(plugins);
if (out.getVersion().onOrAfter(Version.V_8_3_0)) {
out.writeList(plugins);
} else {
out.writeList(plugins.stream().map(PluginRuntimeInfo::descriptor).toList());
}
out.writeList(modules);
}

/**
* Returns an ordered list based on plugins name
*/
public List<PluginDescriptor> getPluginInfos() {
List<PluginDescriptor> plugins = new ArrayList<>(this.plugins);
Collections.sort(plugins, Comparator.comparing(PluginDescriptor::getName));
return plugins;
public List<PluginRuntimeInfo> getPluginInfos() {
return plugins.stream().sorted(Comparator.comparing(p -> p.descriptor().getName())).toList();
}

/**
Expand All @@ -61,19 +65,11 @@ public List<PluginDescriptor> getModuleInfos() {
return modules;
}

public void addPlugin(PluginDescriptor info) {
plugins.add(info);
}

public void addModule(PluginDescriptor info) {
modules.add(info);
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startArray("plugins");
for (PluginDescriptor pluginDescriptor : getPluginInfos()) {
pluginDescriptor.toXContent(builder, params);
for (PluginRuntimeInfo pluginInfo : plugins) {
pluginInfo.toXContent(builder, params);
}
builder.endArray();
// TODO: not ideal, make a better api for this (e.g. with jar metadata, and so on)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import org.elasticsearch.monitor.fs.FsInfo;
import org.elasticsearch.monitor.jvm.JvmInfo;
import org.elasticsearch.monitor.os.OsInfo;
import org.elasticsearch.plugins.PluginDescriptor;
import org.elasticsearch.plugins.PluginRuntimeInfo;
import org.elasticsearch.transport.TransportInfo;
import org.elasticsearch.xcontent.ToXContentFragment;
import org.elasticsearch.xcontent.XContentBuilder;
Expand All @@ -52,7 +52,7 @@ public class ClusterStatsNodes implements ToXContentFragment {
private final ProcessStats process;
private final JvmStats jvm;
private final FsInfo.Path fs;
private final Set<PluginDescriptor> plugins;
private final Set<PluginRuntimeInfo> plugins;
private final NetworkTypes networkTypes;
private final DiscoveryTypes discoveryTypes;
private final PackagingTypes packagingTypes;
Expand Down Expand Up @@ -118,7 +118,7 @@ public FsInfo.Path getFs() {
return fs;
}

public Set<PluginDescriptor> getPlugins() {
public Set<PluginRuntimeInfo> getPlugins() {
return plugins;
}

Expand Down Expand Up @@ -161,8 +161,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
fs.toXContent(builder, params);

builder.startArray(Fields.PLUGINS);
for (PluginDescriptor pluginDescriptor : plugins) {
pluginDescriptor.toXContent(builder, params);
for (PluginRuntimeInfo pluginInfo : plugins) {
pluginInfo.toXContent(builder, params);
}
builder.endArray();

Expand Down
44 changes: 44 additions & 0 deletions server/src/main/java/org/elasticsearch/plugins/PluginApiInfo.java
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -403,22 +403,25 @@ public boolean isLicensed() {
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
{
builder.field("name", name);
builder.field("version", version);
builder.field("elasticsearch_version", elasticsearchVersion);
builder.field("java_version", javaVersion);
builder.field("description", description);
builder.field("classname", classname);
builder.field("extended_plugins", extendedPlugins);
builder.field("has_native_controller", hasNativeController);
builder.field("licensed", isLicensed);
builder.field("type", type);
if (type == PluginType.BOOTSTRAP) {
builder.field("java_opts", javaOpts);
}
}
toXContentFragment(builder, params);
builder.endObject();
return builder;
}

public XContentBuilder toXContentFragment(XContentBuilder builder, Params params) throws IOException {
builder.field("name", name);
builder.field("version", version);
builder.field("elasticsearch_version", elasticsearchVersion);
builder.field("java_version", javaVersion);
builder.field("description", description);
builder.field("classname", classname);
builder.field("extended_plugins", extendedPlugins);
builder.field("has_native_controller", hasNativeController);
builder.field("licensed", isLicensed);
builder.field("type", type);
if (type == PluginType.BOOTSTRAP) {
builder.field("java_opts", javaOpts);
}

return builder;
}
Expand Down
133 changes: 133 additions & 0 deletions server/src/main/java/org/elasticsearch/plugins/PluginIntrospector.java
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);
}
}

0 comments on commit 8d502e6

Please sign in to comment.