docker安装elk

一、安装部署

1.1 挂载目录准备

 创建es、logstash和kibana的三个挂载目录,然后将目录权限修改为1000

chown -R 1000:1000 xxxxx

1.2 es挂载目录

下一般需要挂载数据目录和配置文件。初次启动时,先不挂在es的目录,等es启动成功后,进入es容器,将es的配置目录和数据目录拷贝出来即可。

docker exec -it elasticsearch sh

tar -cvf config.tar config/

tar -cvf data.tar data/

exit

docker cp elasticsearch:/usr/share/elasticsearch/config.tar es挂载目录

docker cp elasticsearch:/usr/share/elasticsearch/data.tar es挂载目录

tar -vxf data.tar

tar -vxf config.tar

1.3 logstash挂载目录

config:logstash的服务配置logstash.yml

http.host: "0.0.0.0"

xpack.monitoring.elasticsearch.hosts: [ "http://elk-es:9200" ]

pipeline:日志收集器的配置,可以有多个配置文件,配置方式也有很多种,这里以tcp方式为例(本例子的日志发送端已将日志全部格式化成json了)

input {

  tcp {

    port => 5044

    codec => json_lines

  }

}

output {

  elasticsearch {

    hosts => ["http://elk-es:9200"]

    index => "log-%{+YYYY.MM.dd}"

    #user => "elastic"

    #password => "changeme"

  }

}

1.4 kibana挂载目录

config:kibana的服务配置目录,kibana.yml

server.name: kibana

server.host: "0"

elasticsearch.hosts: [ "http://elk-es:9200" ]

monitoring.ui.container.elasticsearch.enabled: true


1.5 启动脚本

注意初次启动时删除es的挂载目录,待拷贝完es的配置和数据目录后再修改。

docker-compose.yml

version: '3'

services:

  elk-es:

    image: docker.elastic.co/elasticsearch/elasticsearch:7.10.0

    environment:

      - discovery.type=single-node

    volumes:

      - /data/docker-space/elk/bind/es/data:/usr/share/elasticsearch/data

      - /data/docker-space/elk/bind/es/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml

    networks:

      - elk

  elk-logstash:

    image: docker.elastic.co/logstash/logstash:7.10.0

    volumes:

      - /data/docker-space/elk/bind/logstash/pipeline:/usr/share/logstash/pipeline

      - /data/docker-space/elk/bind/logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml

    command: -f /usr/share/logstash/pipeline/logstash.conf

    depends_on:

      - elk-es

    ports:

      - "5044:5044"

    networks:

      - elk

  elk-kibana:

    image: docker.elastic.co/kibana/kibana:7.10.0

    environment:

      - ELASTICSEARCH_URL=http://elk-es:9200

    volumes:

      - /data/docker-space/elk/bind/kibana/config/kibana.yml:/usr/share/kibana/config/kibana.yml

    depends_on:

      - elk-es

    ports:

      - "5601:5601"

    networks:

      - elk

networks:

  elk:

    driver: bridge

1.6 启动

docker-compose up -d

二、spring-boot适配

2.1 添加依赖包

<dependency>

    <groupId>net.logstash.logback</groupId>

    <artifactId>logstash-logback-encoder</artifactId>

</dependency>

2.2 修改logback.xml

太过简单,此处省略,自行搜索

自此,技术上的探索已经完毕,服务启动后日志会通过logstash保存到es,然后可通过kibana查看。

但是,实际使用中,通常是需要更精确的使用,比如将日志按照需要结构化、添加接口拦截器,从而实现接口日志的采集功能等。

三、spring-boot日志结构化

3.1 自定义日志对象

@Data

public class LocalLogEntity {

@JsonIgnore

private final static Format dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS").toFormat();

// 应用环境

private String appEnv;

// 应用名称

private String appName;

// 应用IP

private String appIp;

// 线程名称

private String threadName;

// 日志ID,可用于日志追踪

private String trackId;

// 日志名称

private ELogName logName = ELogName.NORMAL_LOG;;

// 日志时间

private String logTime;

// 日志级别

private String logLevel;

// 输出日志的类

private String logger;

// 输出日志的方法

private String loggerMethod;

// 输出日志的代码行数

private int loggerLine;

// 日志信息

private String message;

// 接口地址

private String apiUrl;

// 接口状态

private String apiStatus;

// 请求IP

private String requestIp;

// 路径参数

private String pathParams;

// 请求体参数

private String bodyParams;

// 用户ID

private String userId;

// 用户部门ID

private String deptId;

// 用户部门上级ID

private String parentId;

@JsonIgnore

public void setContext(Context context) {

if(context == null) {

return;

}

this.appEnv = context.getProperty("appEnv");

this.appName = context.getProperty("appName");

this.appIp = context.getProperty("appIp");

}

@JsonIgnore

public void setEvent(ILoggingEvent event) {

if(event == null) {

return;

}

this.logLevel = event.getLevel().levelStr;

this.threadName = event.getThreadName();

this.logTime = dateTimeFormatter.format(LocalDateTime.now());

this.trackId = event.getMDCPropertyMap().get(SecurityConstants.LOG_TRACK_ID);

this.message = event.getMessage();

// 有异常信息时,获取异常抛出的信息

if (event.getThrowableProxy() != null) {

StackTraceElementProxy[] traces = event.getThrowableProxy().getStackTraceElementProxyArray();

StackTraceElement element = traces[0].getStackTraceElement();

if ("某自定义exception".equals(element.getClassName())) {

element = event.getThrowableProxy().getStackTraceElementProxyArray()[1].getStackTraceElement();

this.logName = ELogName.BUSSINESS_ERROR;

} else {

this.logName = ELogName.SYSTEM_ERROR;

StringBuffer sb = new StringBuffer();

sb.append(String.format("%s: %s\r\n",event.getThrowableProxy().getClassName(),event.getThrowableProxy().getMessage()));

for(int i=0;i<traces.length;i++) {

sb.append(String.format("\t%s\r\n",traces[i].getSTEAsString()));

}

this.message = sb.toString();

}

this.logger = element.getClassName();

this.loggerMethod = element.getMethodName();

this.loggerLine = element.getLineNumber();

} else {

this.logger = event.getLoggerName();

this.loggerMethod = event.getCallerData()[0].getMethodName();

this.loggerLine = event.getCallerData()[0].getLineNumber();

try {

LocalBeanUtils.copyJsonToEntity(JSON.parseObject(event.getMessage()), this);

} catch (Exception e) {

}

}

}

/**

* 设置会话信息

*/

public void setSessionInfo() {

SessionInfo sessionInfo = SecurityUtils.getSessionInfo();

if(sessionInfo==null) {

return;

}

SessionUser user = sessionInfo.getUser();

if(user != null) {

this.userId = user.getUserId();

}

SessionDept dept = sessionInfo.getCurrentDept();

if(dept !=null) {

this.deptId = dept.getDeptId();

this.parentId = dept.getParentId();

}

}

}

3.2 自定义日志采集器

public class LocalLogJsonEncoder implements Encoder<ILoggingEvent> {

private Charset charset = Charset.forName("UTF-8");

private Context context;

private String appIp;

@Getter

@Setter

private Level level = Level.WARN;

@Override

public byte[] headerBytes() {

// System.out.println("headerBytes");

return new byte[0];

}

@Override

public byte[] footerBytes() {

// System.out.println("footerBytes");

return new byte[0];

}

@Override

public void setContext(Context context) {

this.context = context;

}

@Override

public Context getContext() {

return context;

}

@Override

public void addStatus(Status status) {

// System.out.println("addStatus");

// System.out.println(JSON.toJSONString(status));

}

@Override

public void addInfo(String msg) {

// System.out.println("addInfo2");

}

@Override

public void addInfo(String msg, Throwable ex) {

// System.out.println("addInfo2");

}

@Override

public void addWarn(String msg) {

// System.out.println("addWarn");

}

@Override

public void addWarn(String msg, Throwable ex) {

// System.out.println("addWarn2");

}

@Override

public void addError(String msg) {

// System.out.println("addError");

// System.out.println(msg);

}

@Override

public void addError(String msg, Throwable ex) {

// System.out.println("addError2");

}

@Override

public void start() {

this.appIp = IpUtils.getHostIp();

}

@Override

public void stop() {

// System.out.println("stop");

}

@Override

public boolean isStarted() {

// System.out.println("isStarted");

return false;

}

@Override

public byte[] encode(ILoggingEvent event) {

if (!event.getLevel().isGreaterOrEqual(this.level)) {

// 高于配置的日志级别才记录日志

return null;

}

LocalLogEntity entity;

try {

entity = JSON.parseObject(event.getMessage(), LocalLogEntity.class);

if (entity == null) {

entity = new LocalLogEntity();

}

} catch (Exception e) {

entity = new LocalLogEntity();

}

entity.setContext(this.context);

entity.setEvent(event);

entity.setAppIp(appIp);

return String.format("%s\r\n", JSON.toJSONString(entity)).getBytes(charset);

}

}

3.3 修改logback.xml

<?xmlversion="1.0"encoding="UTF-8"?>

<configurationscan="true"debug="false">

<springPropertyscope="context"name="log_level"source="logging.level"defaultValue="error"/>

<springPropertyscope="context"name="appEnv"source="spring.profiles.active"defaultValue="unknow"/>

<springPropertyscope="context"name="appName"source="spring.application.name"defaultValue="unknow"/>

<springPropertyscope="context"name="logstash_address"source="logging.logstash.address" defaultValue="unknow_logstash_address"/>

<!-- ConsoleAppender 控制台输出日志 -->

<appendername="STDOUT"class="ch.qos.logback.core.ConsoleAppender">

<!-- 对日志进行格式化 -->

......

</appender>

<!--输出到 logstash的 appender -->

<appendername="logstash"class="net.logstash.logback.appender.LogstashTcpSocketAppender">

<destination>${logstash_address}</destination>

<encoderclass="com.xxxxx.LocalLogJsonEncoder">

<level>WARN</level>

</encoder>

</appender>

<!-- 全部环境的日志输出配置 -->

<rootlevel="INFO">

<appender-refref="STDOUT"/>

</root>

<!-- elk日志输出配置 -->

<rootlevel="WARN">

<appender-refref="logstash"/>

</root>

</configuration>

这样es中存储的日志mapping就是自定义的日志对象,后续可用于自定义的业务统计需求和分析。

3.4 接口拦截

@Slf4j

@Configuration

@ConditionalOnProperty(value = "logging.local.enabled", havingValue = "true", matchIfMissing = true)

public class LogInterceptor implements HandlerInterceptor, WebMvcConfigurer {

@Override

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)

throws Exception {

String traceId = request.getHeader(SecurityConstants.LOG_TRACK_ID);

if(LocalStringUtils.isEmpty(traceId)) {

traceId = IdUtils.getSnowId();

}

MDC.put(SecurityConstants.LOG_TRACK_ID, traceId);

String requestBody = "{}";

if (request instanceof LocalHttpServletRequestWrapper) {

LocalHttpServletRequestWrapper requestWrapper = (LocalHttpServletRequestWrapper) request;

byte[] bytes = requestWrapper.getBody();

requestBody = new String(bytes);

}

log.warn(LocalLogFormater.apiStart(IpUtils.getIpAddr(request), request.getRequestURI(),

request.getParameterMap(), requestBody));

return HandlerInterceptor.super.preHandle(request, response, handler);

}

@Override

public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,

ModelAndView modelAndView) throws Exception {

HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);

}

@Override

public void afterCompletion(HttpServletRequest httpRequest, HttpServletResponse response, Object handler,

Exception ex) throws Exception {

String url = httpRequest.getRequestURI().substring(httpRequest.getContextPath().length());// 请求的URL

String bussinessCode = response.getHeader(SecurityConstants.BUSSINESS_CODE);

if (LocalStringUtils.isEmpty(bussinessCode)) {

log.warn(LocalLogFormater.apiEnd(String.valueOf(response.getStatus()), url));

} else {

log.warn(LocalLogFormater.apiEnd(bussinessCode, url));

}

HandlerInterceptor.super.afterCompletion(httpRequest, response, handler, ex);

}

@Override

public void addInterceptors(InterceptorRegistry registry) {

registry.addInterceptor(new LogInterceptor()).addPathPatterns("/**");

}

}

参数过滤器

@Slf4j

@Configuration

@ConditionalOnProperty(value = "log.local.enabled", havingValue = "true", matchIfMissing = true)

public class LogRequestBodyReadFilter implements Filter {

@Override

public void init(FilterConfig filterConfig) throws ServletException {

}

@Override

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)

throws IOException, ServletException {

if (servletRequest instanceof HttpServletRequest) {

HttpServletRequest request = (HttpServletRequest) servletRequest;

String contentType = servletRequest.getContentType();

if (contentType==null || !contentType.contains("multipart")) {

/*

* 只要不是文件上传接口,都转换成可以反复读取输入流的Wrapper

* 后续的日志拦截器就可以读取body,并且不影响接口读取参数了

*/

filterChain.doFilter(new LocalHttpServletRequestWrapper(request), servletResponse);

return;

}

}

filterChain.doFilter(servletRequest, servletResponse);

}

@Override

public void destroy() {

}

}

public class LocalHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] body;

    public LocalHttpServletRequestWrapper(HttpServletRequest request) throws IOException {

        super(request);

        request.setCharacterEncoding("UTF-8");

        ServletInputStream inputStream = request.getInputStream();

        DataInputStream dataInputStream = new DataInputStream(inputStream);

        body = dataInputStream.readAllBytes();

    }

    public byte[] getBody(){

        return this.body;

    }

    @Override

    public BufferedReader getReader() throws IOException {

        return new BufferedReader(new InputStreamReader(getInputStream()));

    }

    @Override

    public ServletInputStream getInputStream() {

        final ByteArrayInputStream basis = new ByteArrayInputStream(body);

        return new ServletInputStream() {

            @Override

            public int read() {

                return basis.read();

            }

            @Override

            public boolean isFinished() {

                return false;

            }

            @Override

            public boolean isReady() {

                return false;

            }

            @Override

            public void setReadListener(ReadListener readListener) {

            }

        };

    }

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容

  • 利用elk进行日志提取分析。 安装elasticsearch 创建一个内网组,方便其他组件之间直接通过内网访问,比...
    little多米阅读 378评论 0 0
  • 文章目录结构如下: 巨坑提醒:ES和kibana的版本尽可能的保证一致,否则要去修改很多配置信息,而且不一定能安装...
    小明哥206阅读 18,490评论 3 9
  • 一、Docker 安装 Elasticsearch 官网镜像地址:https://hub.docker.com/_...
    麦田的香阅读 1,010评论 0 0
  • 很久没有发文了,今天水一篇操作文章分享一下经验吧,后续陆续推出深度好文 springboot+logStash+e...
    醉鱼java阅读 606评论 0 0
  • docker-compose 搭建ELK 本文主要是参考别人博客,并迭代记录一些原文没有遇到或者说明的问题,供自己...
    dsjaikdnsajdnua阅读 2,045评论 0 1