Skip to content

Commit 5e77597

Browse files
andselyaauie
andauthored
Load a plugin by alias name. (#12796)
Introcuce the concept of alias for a plugin. Creates an AliasRegistry to map plugin aliases to original plugins. If a real plugin with same name of the an alias is present in the system, then the real plugin take precedence during the instantiation of the pipeline. Simplified the error handling in class lookup Co-authored-by: Ry Biesemeyer <[email protected]>
1 parent 08f758c commit 5e77597

File tree

6 files changed

+256
-54
lines changed

6 files changed

+256
-54
lines changed

logstash-core/lib/logstash/plugins/registry.rb

Lines changed: 50 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def register(hooks, settings)
113113

114114
attr_reader :hooks
115115

116-
def initialize
116+
def initialize(alias_registry = nil)
117117
@mutex = Mutex.new
118118
# We need a threadsafe class here because we may perform
119119
# get/set operations concurrently despite the fact we don't use
@@ -123,6 +123,7 @@ def initialize
123123
@registry = java.util.concurrent.ConcurrentHashMap.new
124124
@java_plugins = java.util.concurrent.ConcurrentHashMap.new
125125
@hooks = HooksRegistry.new
126+
@alias_registry = alias_registry || Java::org.logstash.plugins.AliasRegistry.new
126127
end
127128

128129
def setup!
@@ -196,41 +197,47 @@ def lookup(type, plugin_name, &block)
196197
# a plugin and will do a lookup on the namespace of the required class to find a matching
197198
# plugin with the appropriate type.
198199
def legacy_lookup(type, plugin_name)
199-
begin
200-
path = "logstash/#{type}s/#{plugin_name}"
200+
klass = load_plugin_class(type, plugin_name)
201201

202-
klass = begin
203-
namespace_lookup(type, plugin_name)
204-
rescue UnknownPlugin => e
205-
# Plugin not registered. Try to load it.
206-
begin
207-
require path
208-
namespace_lookup(type, plugin_name)
209-
rescue LoadError => e
210-
logger.error("Tried to load a plugin's code, but failed.", :exception => e, :path => path, :type => type, :name => plugin_name)
211-
raise
212-
end
213-
end
202+
if !klass && @alias_registry.alias?(type.to_java, plugin_name)
203+
resolved_plugin_name = @alias_registry.original_from_alias(type.to_java, plugin_name)
204+
logger.debug("Loading #{type} plugin #{resolved_plugin_name} via its alias #{plugin_name}...")
205+
klass = load_plugin_class(type, resolved_plugin_name)
206+
lazy_add(type, resolved_plugin_name, klass) if klass
207+
end
214208

215-
plugin = lazy_add(type, plugin_name, klass)
216-
rescue => e
217-
logger.error("Problems loading a plugin with",
218-
:type => type,
219-
:name => plugin_name,
220-
:path => path,
221-
:error_message => e.message,
222-
:error_class => e.class,
223-
:error_backtrace => e.backtrace)
224-
225-
raise LoadError, "Problems loading the requested plugin named #{plugin_name} of type #{type}. Error: #{e.class} #{e.message}"
209+
unless klass
210+
logger.error("Unable to load plugin.", :type => type, :name => plugin_name)
211+
raise LoadError, "Unable to load the requested plugin named #{plugin_name} of type #{type}. The plugin is not installed."
226212
end
227213

228-
plugin
214+
lazy_add(type, plugin_name, klass)
215+
end
216+
217+
# load a plugin's class, or return nil if the plugin cannot be loaded.
218+
# attempts to load the class purely through namespace lookup,
219+
# and falls back to requiring the path of the expected plugin.
220+
# @param type [String]: plugin type, such as "input", "output", "filter", "codec"
221+
# @param plugin_name [String]: plugin name, such as "grok", "elasticsearch"
222+
# @return [Class,nil] the plugin class, or nil
223+
private
224+
def load_plugin_class(type, plugin_name)
225+
klass = namespace_lookup(type, plugin_name)
226+
227+
unless klass
228+
require("logstash/#{type}s/#{plugin_name}")
229+
klass = namespace_lookup(type, plugin_name)
230+
end
231+
klass
232+
rescue LoadError => e
233+
logger.debug("Tried to load a plugin's code, but failed.", :exception => e, :path => e.path, :type => type, :name => plugin_name)
234+
nil
229235
end
230236

237+
public
231238
def lookup_pipeline_plugin(type, name)
232239
LogStash::PLUGIN_REGISTRY.lookup(type, name) do |plugin_klass, plugin_name|
233-
is_a_plugin?(plugin_klass, plugin_name)
240+
is_a_plugin_or_alias?(plugin_klass, type.to_java, plugin_name)
234241
end
235242
rescue LoadError, NameError => e
236243
logger.debug("Problems loading the plugin with", :type => type, :name => name)
@@ -268,19 +275,15 @@ def size
268275
# ex.: namespace_lookup("filter", "grok") looks for LogStash::Filters::Grok
269276
# @param type [String] plugin type, "input", "output", "filter"
270277
# @param name [String] plugin name, ex.: "grok"
271-
# @return [Class] the plugin class or raises NameError
272-
# @raise NameError if plugin class does not exist or is invalid
278+
# @return [Class,nil] the plugin class or nil
273279
def namespace_lookup(type, name)
274280
type_const = "#{type.capitalize}s"
275281
namespace = LogStash.const_get(type_const)
276282
# the namespace can contain constants which are not for plugins classes (do not respond to :config_name)
277283
# namespace.constants is the shallow collection of all constants symbols in namespace
278284
# note that below namespace.const_get(c) should never result in a NameError since c is from the constants collection
279285
klass_sym = namespace.constants.find { |c| is_a_plugin?(namespace.const_get(c), name) }
280-
klass = klass_sym && namespace.const_get(klass_sym)
281-
282-
raise(UnknownPlugin) unless klass
283-
klass
286+
klass_sym && namespace.const_get(klass_sym)
284287
end
285288

286289
# check if klass is a valid plugin for name
@@ -290,7 +293,19 @@ def namespace_lookup(type, name)
290293
def is_a_plugin?(klass, name)
291294
(klass.class == Java::JavaLang::Class && klass.simple_name.downcase == name.gsub('_','')) ||
292295
(klass.class == Java::JavaClass && klass.simple_name.downcase == name.gsub('_','')) ||
293-
(klass.ancestors.include?(LogStash::Plugin) && klass.respond_to?(:config_name) && klass.config_name == name)
296+
(klass.ancestors.include?(LogStash::Plugin) && klass.respond_to?(:config_name) &&
297+
klass.config_name == name)
298+
end
299+
300+
# check if klass is a valid plugin for name,
301+
# including alias resolution
302+
def is_a_plugin_or_alias?(klass, type, plugin_name)
303+
return true if is_a_plugin?(klass, plugin_name)
304+
305+
resolved_plugin_name = @alias_registry.resolve_alias(type, plugin_name)
306+
return true if is_a_plugin?(klass, resolved_plugin_name)
307+
308+
false
294309
end
295310

296311
def add_plugin(type, name, klass)

logstash-core/spec/logstash/java_pipeline_spec.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,15 @@ def flush(options)
199199
end
200200
end
201201

202+
describe "aliased plugin instantiation" do
203+
it "should create the pipeline as if it's using the original plugin" do
204+
alias_registry = Java::org.logstash.plugins.AliasRegistry.new({["input", "alias"] => "generator"})
205+
LogStash::PLUGIN_REGISTRY = LogStash::Plugins::Registry.new alias_registry
206+
pipeline = mock_java_pipeline_from_string("input { alias { count => 1 } } output { null {} }")
207+
expect(pipeline.ephemeral_id).to_not be_nil
208+
pipeline.close
209+
end
210+
end
202211

203212
describe "event cancellation" do
204213
# test harness for https://github.com/elastic/logstash/issues/6055

logstash-core/spec/logstash/plugins/registry_spec.rb

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,23 @@ def register; end
3434
end
3535

3636
describe LogStash::Plugins::Registry do
37-
let(:registry) { described_class.new }
37+
let(:alias_registry) { nil }
38+
let(:registry) { described_class.new alias_registry }
3839

3940
context "when loading installed plugins" do
41+
let(:alias_registry) { Java::org.logstash.plugins.AliasRegistry.new({["input", "alias_std_input"] => "stdin"}) }
4042
let(:plugin) { double("plugin") }
4143

4244
it "should return the expected class" do
4345
klass = registry.lookup("input", "stdin")
4446
expect(klass).to eq(LogStash::Inputs::Stdin)
4547
end
4648

49+
it "should load an aliased ruby plugin" do
50+
klass = registry.lookup("input", "alias_std_input")
51+
expect(klass).to eq(LogStash::Inputs::Stdin)
52+
end
53+
4754
it "should raise an error if can not find the plugin class" do
4855
expect { registry.lookup("input", "do-not-exist-elastic") }.to raise_error(LoadError)
4956
end
@@ -53,13 +60,34 @@ def register; end
5360
expect { registry.lookup("input", "new_plugin") }.to change { registry.size }.by(1)
5461
expect { registry.lookup("input", "new_plugin") }.not_to change { registry.size }
5562
end
63+
64+
context "when loading installed plugin that overrides an alias" do
65+
let(:alias_registry) { Java::org.logstash.plugins.AliasRegistry.new({["input", "dummy"] => "new_plugin"}) }
66+
67+
it 'should load the concrete implementation instead of resolving the alias' do
68+
klass = registry.lookup("input", "dummy")
69+
expect(klass).to eq(LogStash::Inputs::Dummy)
70+
end
71+
end
5672
end
5773

5874
context "when loading code defined plugins" do
75+
let(:alias_registry) { Java::org.logstash.plugins.AliasRegistry.new({["input", "alias_input"] => "new_plugin"}) }
76+
5977
it "should return the expected class" do
6078
klass = registry.lookup("input", "dummy")
6179
expect(klass).to eq(LogStash::Inputs::Dummy)
6280
end
81+
82+
it "should return the expected class also for aliased plugins" do
83+
klass = registry.lookup("input", "alias_input")
84+
expect(klass).to eq(LogStash::Inputs::NewPlugin)
85+
end
86+
87+
it "should return the expected class also for alias-targeted plugins" do
88+
klass = registry.lookup("input", "new_plugin")
89+
expect(klass).to eq(LogStash::Inputs::NewPlugin)
90+
end
6391
end
6492

6593
context "when plugin is not installed and not defined" do
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package org.logstash.plugins;
2+
3+
import org.logstash.plugins.PluginLookup.PluginType;
4+
5+
import java.util.HashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
import java.util.Objects;
9+
import java.util.Optional;
10+
11+
public class AliasRegistry {
12+
13+
private final static class PluginCoordinate {
14+
private final PluginType type;
15+
private final String name;
16+
17+
public PluginCoordinate(PluginType type, String name) {
18+
this.type = type;
19+
this.name = name;
20+
}
21+
22+
@Override
23+
public boolean equals(Object o) {
24+
if (this == o) return true;
25+
if (o == null || getClass() != o.getClass()) return false;
26+
PluginCoordinate that = (PluginCoordinate) o;
27+
return type == that.type && Objects.equals(name, that.name);
28+
}
29+
30+
@Override
31+
public int hashCode() {
32+
return Objects.hash(type, name);
33+
}
34+
35+
PluginCoordinate withName(String name) {
36+
return new PluginCoordinate(this.type, name);
37+
}
38+
}
39+
40+
41+
private final Map<PluginCoordinate, String> aliases = new HashMap<>();
42+
private final Map<PluginCoordinate, String> reversedAliases = new HashMap<>();
43+
44+
public AliasRegistry() {
45+
Map<PluginCoordinate, String> defaultDefinitions = new HashMap<>();
46+
defaultDefinitions.put(new PluginCoordinate(PluginType.INPUT, "elastic_agent"), "beats");
47+
configurePluginAliases(defaultDefinitions);
48+
}
49+
50+
/**
51+
* Constructor used in tests to customize the plugins renames.
52+
* The input map's key are tuples of (type, name)
53+
* */
54+
public AliasRegistry(Map<List<String>, String> aliasDefinitions) {
55+
Map<PluginCoordinate, String> aliases = new HashMap<>();
56+
57+
// transform the (tye, name) into PluginCoordinate
58+
for (Map.Entry<List<String>, String> e : aliasDefinitions.entrySet()) {
59+
final List<String> tuple = e.getKey();
60+
final PluginCoordinate key = mapTupleToCoordinate(tuple);
61+
aliases.put(key, e.getValue());
62+
}
63+
64+
configurePluginAliases(aliases);
65+
}
66+
67+
private PluginCoordinate mapTupleToCoordinate(List<String> tuple) {
68+
if (tuple.size() != 2) {
69+
throw new IllegalArgumentException("Expected a tuple of 2 elements, but found: " + tuple);
70+
}
71+
final PluginType type = PluginType.valueOf(tuple.get(0).toUpperCase());
72+
final String name = tuple.get(1);
73+
final PluginCoordinate key = new PluginCoordinate(type, name);
74+
return key;
75+
}
76+
77+
private void configurePluginAliases(Map<PluginCoordinate, String> aliases) {
78+
this.aliases.putAll(aliases);
79+
for (Map.Entry<PluginCoordinate, String> e : this.aliases.entrySet()) {
80+
final PluginCoordinate reversedAlias = e.getKey().withName(e.getValue());
81+
if (reversedAliases.containsKey(reversedAlias)) {
82+
throw new IllegalStateException("Found plugin " + e.getValue() + " aliased more than one time");
83+
}
84+
reversedAliases.put(reversedAlias, e.getKey().name);
85+
}
86+
}
87+
88+
public boolean isAlias(String type, String pluginName) {
89+
final PluginType pluginType = PluginType.valueOf(type.toUpperCase());
90+
91+
return isAlias(pluginType, pluginName);
92+
}
93+
94+
public boolean isAlias(PluginType type, String pluginName) {
95+
return aliases.containsKey(new PluginCoordinate(type, pluginName));
96+
}
97+
98+
public String originalFromAlias(PluginType type, String alias) {
99+
return aliases.get(new PluginCoordinate(type, alias));
100+
}
101+
102+
public String originalFromAlias(String type, String alias) {
103+
return originalFromAlias(PluginType.valueOf(type.toUpperCase()), alias);
104+
}
105+
106+
public Optional<String> aliasFromOriginal(PluginType type, String realPluginName) {
107+
return Optional.ofNullable(reversedAliases.get(new PluginCoordinate(type, realPluginName)));
108+
}
109+
110+
/**
111+
* if pluginName is an alias then return the real plugin name else return it unchanged
112+
*/
113+
public String resolveAlias(String type, String pluginName) {
114+
final PluginCoordinate pluginCoord = new PluginCoordinate(PluginType.valueOf(type.toUpperCase()), pluginName);
115+
return aliases.getOrDefault(pluginCoord, pluginName);
116+
}
117+
}

0 commit comments

Comments
 (0)