1:elasticsearch插件分类简述
2:Java插件开发要点
3:如何针对不同版本elasticsearch提供多版本的插件
4:插件具有外部依赖时遇到的一些问题(2016-09-07更新)
elasticsearch插件分类简述
elasticsearch插件分为Site插件及Java插件,前者比如使用最广泛的head插件,而后者比如elastic官方提供的商业插件shield。
需要注意的是Site插件从elasticsearch2.3.0版本开始已被标记为Deprecated,并将从5.0.0版本开始被正式移除,相关的Site插件将被整合到kibana中,或者单独部署web server。具体如何整合我还不太清楚。
下文主要记录Java插件的开发要点,以maven管理为例。
Java插件开发要点
以下一一个简单功能为例来说明开发流程,该插件记录所有对部署elasticsearch节点的请求,并且根据指定的配置参数,过滤掉敏感操作(DELETE)
1:Java插件入口
继承org.elasticsearch.plugins.Plugin类的入口类,并实现onModule方法。该类中可以通过settings访问elasticsearch配置文件,获取配置信息,并加载下一步的handler,具体实现代码如下所示:
1 package org.elasticsearch.es.plugin; 2 3 import org.elasticsearch.common.settings.Settings; 4 import org.elasticsearch.rest.RestModule; 5 import org.elasticsearch.plugins.Plugin; 6 7 @SuppressWarnings("static-method") 8 public class MyRestPlugin extends Plugin { 9 10 private final Settings settings; 11 12 public MyRestPlugin(Settings settings){ 13 this.settings = settings; 14 } 15 16 @Override 17 public String name(){ 18 return "MyRest"; 19 } 20 21 @Override 22 public String description(){ 23 return "ElasticSearch Plugin"; 24 } 25 26 public void onModule(RestModule module){ 27 String isPluginEnabled = settings.get("ld.enabled"); 28 MethodAuthenticator.setEnabledStr(isPluginEnabled); 29 if(isPluginEnabled != null && isPluginEnabled.toLowerCase().equals("true")){ 30 MethodAuthenticator.setIsPluginEnabled(true); 31 String[] denyMethods = settings.getAsArray("ld.deny", new String[]{}); 32 if(denyMethods != null){ 33 MethodAuthenticator.setDenyMethods(denyMethods); 34 } 35 } else { 36 MethodAuthenticator.setIsPluginEnabled(false); 37 } 38 39 module.addRestAction(MyRestHandler.class); 40 } 41 42 }
配置文件信息放入authenticator静态类,代码如下:
1 package org.elasticsearch.es.plugin; 2 3 import java.util.Arrays; 4 5 public class MethodAuthenticator{ 6 7 private static String enabledStr; 8 private static boolean isPluginEnabled; 9 private static String[] denyMethods; 10 11 // public MethodAuthenticator(){ 12 // 13 // } 14 15 public static boolean isMethodEnabled(String method){ 16 if(denyMethods == null) { 17 MyLogger.debug("The deny methods is null"); 18 return true; 19 } 20 if(Arrays.asList(denyMethods).contains(method)){ 21 return false; 22 } else { 23 return true; 24 } 25 } 26 27 public static String getEnabledStr(){ 28 return enabledStr; 29 } 30 31 public static void setEnabledStr(String enabledStr){ 32 MethodAuthenticator.enabledStr = enabledStr; 33 } 34 35 public static boolean getIsPluginEnabled() { 36 return isPluginEnabled; 37 } 38 39 public static String[] getDenyMethods() { 40 return denyMethods; 41 } 42 43 public static void setIsPluginEnabled(boolean isPluginEnabled) { 44 MethodAuthenticator.isPluginEnabled = isPluginEnabled; 45 } 46 47 public static void setDenyMethods(String[] denyMethods) { 48 MethodAuthenticator.denyMethods = denyMethods; 49 } 50 }
2:相关的Handler
继承org.elasticsearch.rest.BaseRestHandler类的handler,将filter注入controller,实现代码如下:
1 package org.elasticsearch.es.plugin; 2 3 import org.elasticsearch.rest.*; 4 5 import org.elasticsearch.client.Client; 6 import org.elasticsearch.common.inject.Inject; 7 import org.elasticsearch.common.settings.Settings; 8 9 public class MyRestHandler extends BaseRestHandler { 10 @Inject 11 public MyRestHandler(Settings settings, RestController restController, Client client){ 12 super(settings, restController, client); 13 RestFilter filter = new MyRestFilter(client); 14 restController.registerFilter(filter); 15 } 16 17 @Override 18 protected void handleRequest(RestRequest restRequest, RestChannel restChannel, Client client) 19 throws Exception { 20 // TODO Auto-generated method stub 21 22 } 23 }
3:注入Filter
继承org.elasticsearch.rest.RestFilter类的filter,在过滤器中实现对请求的记录及过滤,实现代码如下:
1 package org.elasticsearch.es.plugin; 2 3 import org.elasticsearch.client.Client; 4 import org.elasticsearch.rest.RestFilter; 5 import org.elasticsearch.rest.RestRequest; 6 import org.elasticsearch.rest.RestChannel; 7 import org.elasticsearch.rest.RestFilterChain; 8 import org.elasticsearch.rest.RestStatus; 9 import org.elasticsearch.rest.BytesRestResponse; 10 11 import java.net.InetSocketAddress; 12 13 14 public class MyRestFilter extends RestFilter{ 15 Client client; 16 public MyRestFilter(Client client){ 17 this.client = client; 18 } 19 20 @Override 21 public void process(RestRequest request, RestChannel channel, RestFilterChain filterChain) throws Exception{ 22 try{ 23 if(MethodAuthenticator.getIsPluginEnabled()){ 24 MyLogger.info(getLogInfo(request)); 25 String methodStr = request.method().name().toLowerCase(); 26 if(MethodAuthenticator.isMethodEnabled(methodStr)){ 27 //MyLogger.info("The request method " + methodStr + " is allowed"); 28 filterChain.continueProcessing(request, channel); 29 } else { 30 MyLogger.info("The request method " + methodStr + " is denyed"); 31 BytesRestResponse res = new BytesRestResponse(RestStatus.FORBIDDEN, "Forbidden Method Request by MyRest plugin"); 32 channel.sendResponse(res); 33 } 34 } else { 35 //MyLogger.info("MyRest is disabled"); 36 filterChain.continueProcessing(request, channel); 37 } 38 } catch (Exception exp) { 39 channel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, "My Rest Filter internal exception")); 40 } 41 return ; 42 } 43 44 private String getLogInfo(RestRequest request) throws Exception { 45 try{ 46 String pathStr = request.path(); 47 String ipaddr = ((InetSocketAddress)request.getRemoteAddress()).getAddress().getHostAddress(); 48 return "The [" + request.method().name() + "] request [" + pathStr + "] is from [" + ipaddr + "]"; 49 } catch (Exception exp) { 50 MyLogger.error("Request resolve failed: ", exp); 51 } 52 return "The request resolve failed"; 53 } 54 }
日志记录使用elasticsearch的logger,入口static类代码如下所示:
1 package org.elasticsearch.es.plugin; 2 3 import org.elasticsearch.common.logging.ESLogger; 4 import org.elasticsearch.common.logging.ESLoggerFactory; 5 6 public class MyLogger { 7 8 private static ESLogger esLogger; 9 static { 10 esLogger = ESLoggerFactory.getLogger("myRest"); 11 } 12 13 public static void debug(String msg){ 14 esLogger.debug(msg); 15 } 16 17 public static void info(String msg){ 18 esLogger.info(msg); 19 } 20 21 public static void warn(String msg){ 22 esLogger.warn(msg); 23 } 24 25 public static void error(String msg){ 26 esLogger.error(msg); 27 } 28 29 public static void error(String msg, Exception exp){ 30 esLogger.error(msg, exp); 31 } 32 }
4:插件配置信息
由于插件使用elasticsearch配置文件保存配置信息,因此在配置文件中加入如下信息:
默认elasticsearch配置文件路径为:/etc/elasticsearch/elasticsearch.yml
ld.enabled: false ld.deny: ["delete"]
需要注意的是配置文件中配置项前面加入2个空格字符,否则可能导致elasticsearch不能启动;
此外,插件正常工作还依赖于plugin-descriptor.properties文件(elasticsearch-1.x版本中是es-plugin.properties文件),该文件配置信息如下例:
name=myRest description=GridsumLawDissectorElasticSearchPlugin version=0.0.1 jvm=true site=false classname=org.elasticsearch.es.plugin.MyRestPlugin java.version=1.8 elasticsearch.version=2.3.3
我这里以elasticsearch-2.3.3版本为例,可按需求改为2.X的任意版本。
插件描述文件如下所示(其中format指定插件以zip形式安装):
<?xml version="1.0" encoding="UTF-8"?> <assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd"> <id>plugin</id> <formats> <format>zip</format> </formats> <includeBaseDirectory>false</includeBaseDirectory> <dependencySets> <dependencySet> <outputDirectory>/</outputDirectory> <useProjectArtifact>true</useProjectArtifact> <useTransitiveFiltering>true</useTransitiveFiltering> </dependencySet> </dependencySets> </assembly>
5:编译部署
插件编译使用maven即可:
mvn clean package
需要注意的是如果首次编译可能需要较长时间,maven会下载目标版本的org.elasticsearch.jar。
版本信息在maven对应的pom文件中指定。
编译完成后,将打包的jar文件与plugin-descriptor.properties文件打包为zip安装包,拷贝至elasticsearch的部署节点。
使用如下方式安装插件:
/usr/share/elasticsearch/bin/plugin install file:///opt/myrest.zip
指令中需要说明的是:
1)elasticsearch安装目录下的bin目录下的plugin脚本;
2)由于直接通过打包安装文件安装,因此指定为file://;
3)紧跟打包zip文件的路径即可。
部署完毕后,确保elasticsearch.yml中的配置信息正确,即可重启elasticsearch节点:
systemctl restart elasticsearch
6:验证插件
通过监控elasticsearch日志可看到插件记录的所有请求目标及来源:
tail -f /var/log/elasticsearch/xx.xx.log
上述命令中为log的默认路径,查看以集群clustername命名的日志文件即可。
如果配置参数中过滤方法设置了DELETE,则可通过发送DELETE请求来验证,插件将过滤该请求,并返回403。
如何针对不同版本elasticsearch提供多版本的插件
如前文所述,elasticsearch从2.X版本开始JAVA插件相关的Plugin,BaseRestHandler,RestFilter等抽象类都相同,区别只在于依赖的org.elasticsearch,因此,JAVA插件对elasticsearch2.X以上各版本的支持,只需要修改对应的依赖版本信息并重新打包部署即可。
具体的两个需要修改依赖版本信息的文件分别是:
plugin-descriptor.properties
pom.xml
当然这是以maven为例,如果不是以maven,则只需修改第一个文件的版本信息,并手动引用对应版本的org.elasticsearch,重新编译打包即可。
插件具有外部依赖时遇到的一些问题(2016-09-07更新)
最近由于插件功能扩展,需要用到json序列化,使用了fastJson的1.2.16版本,如下是dependency:
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.16</version> </dependency>
遇到的问题主要是在插件部署环节,主要有2个问题:
1:插件发布并未包含依赖的jar包;
2:fastJson.jar需要许可权(不知道表述是否正确,本人不是javaer),以下为问题2的异常信息:
java.security.AccessControlException: access denied (java.lang.RuntimePermission getClassLoader)
...
...
...
略
解决方法:
1:在插件发布后打包zip过程中加入fastjson-1.2.16.jar即可,如果打包的package中未包含该jar包,可以从maven的repository路径中获取;
2:更改java.policy文件许可权(默认路径/usr/java/jdk1.8.0_73/jre/lib/security/java.policy),jdk版本为目标主机的jdk版本,在grant中加入以下许可信息:
permission java.lang.RuntimePermission "createClassLoader"; permission java.lang.RuntimePermission "getClassLoader";
重启elasticsearch进程即可。
java.policy如找不到,可以在/usr/lib/jvm/java-1.8.0/jre/lib/security路径下找到。