作者应用场景:

根据公司的业务需求,需要对用户调用本司的API做次数统计和计费统计,需要对网关日志进行解析,并将统计到的数据入库保存。公司的网关日志采用ELK架构,正好适用于ES来做解析。日志索引当天日期命名的,所以每天按日期调用一次解析服务,当天统计前一天的调用量,实现按日统计存储入库。后续将根据源数据做相应的业务快发,实现API调用的统计数据展示,便于客服查看用户的调用量和计费数据,也便于客户查看自己的API调用情况。
package com.maijia.cms.business.user.job;

import com.maijia.cms.business.user.domain.ApiRequestRecord;
import com.maijia.ucenter.utils.CalendarUtils;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @Date 2022/3/04
 * @Author Dylan
 */
@Component
public class SearchHelp {

    @Resource
    private ElasticQueryDSLTemplates elasticQueryDslTemplates;

    //索引前缀,索引格式为gw-invoking-log-yyyy-MM-dd
    private static final String ES_INDEX_NAME_PREFIX = "gw-invoking-log-";

    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

    /**
     * ES检索日志数据
     * @param appKey   appKey
     * @param dateStr  日期 yyyy-MM-dd
     * @param failureFlag  不为空时查询调用失败的记录
     */
    public List<ApiRequestRecord> logSearch(String appKey, String dateStr, String failureFlag) {
        //获取日期
        dateStr = getRequestDate(dateStr);
        SearchRequest searchRequest = new SearchRequest(ES_INDEX_NAME_PREFIX + dateStr);
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //构造ES查询条件
        buildItemQuery(searchSourceBuilder, boolQueryBuilder, appKey, failureFlag);
        QueryResult<?> qr = new QueryResult<>();
        elasticQueryDslTemplates.excuteQuery(searchRequest, searchSourceBuilder, qr);
        //解析分组统计数据
        List<ApiRequestRecord> resultList = new ArrayList<>();
        Map<String, Aggregation> aggMap = qr.getAggMap();
        if(null != aggMap && !aggMap.isEmpty()){
            ParsedStringTerms serviceTerms = (ParsedStringTerms) aggMap.get("group_by_service");
            Iterator<Terms.Bucket> serviceBucketIt = (Iterator<Terms.Bucket>)serviceTerms.getBuckets().iterator();
            //返回结果
            Date createDate = CalendarUtils.getDate(dateStr,"yyyy-MM-dd");
            while (serviceBucketIt.hasNext()){
                Terms.Bucket bucket = serviceBucketIt.next();
                ApiRequestRecord record = new ApiRequestRecord();
                record.setAppKey(appKey);
                record.setCreateDate(createDate);
                record.setApiMethod(bucket.getKeyAsString());
                //请求失败次数
                if(StringUtils.isNotBlank(failureFlag)){
                    record.setFailureTotal(bucket.getDocCount());
                }else{
                    //请求总次数
                    record.setRequestTotal(bucket.getDocCount());
                }
                resultList.add(record);
            }
        }

        return resultList;
    }

    private void buildItemQuery(SearchSourceBuilder searchSourceBuilder, BoolQueryBuilder boolQueryBuilder, String appKey, String failureFlag) {
        //匹配某个appKey
        if (StringUtils.isNotBlank(appKey)) {
            boolQueryBuilder.must(QueryBuilders.termQuery("appKey", appKey));
        }
        //匹配调用失败的
        if(StringUtils.isNotBlank(failureFlag)){
            boolQueryBuilder.must(QueryBuilders.termQuery("rpcStatus", "error"));
        }
        //根据method分组统计
        TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms("group_by_service").field("service");
        searchSourceBuilder.aggregation(termsAggregationBuilder);
        searchSourceBuilder.query(boolQueryBuilder);
    }

    /**
     * dateStr为空时,默认获取前一天日期
     * @param dateStr
     * @return
     */
    private String getRequestDate(String dateStr){
        if(StringUtils.isNotBlank(dateStr)){
            return dateStr;
        }
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        //当前时间往前推一天
        calendar.add(Calendar.DAY_OF_MONTH, -1);
        return sdf.format(calendar.getTime());
    }

}
ES分组查询类

查询结果与ES脚本执行结果一致:

GET gw-invoking-log-2022-03-02/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {
          "appKey": "5711393"
        }}
      ] 
        
    }
    },
  "aggs":{
    "group_by_service":{
      "terms": {
        "field": "service"
      }
    }
  }
}

//返回结果
"aggregations" : {
    "group_by_service" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "tsj.shopee.item.skuChart",
          "doc_count" : 446
        },
        {
          "key" : "tsj.shopee.shop.detail",
          "doc_count" : 296
        },
        {
          "key" : "tsj.shopee.item.detail",
          "doc_count" : 65
        },
        {
          "key" : "tsj.shopee.item.milestoneList",
          "doc_count" : 6
        }
      ]
    }
  }
ES脚本
package com.maijia.cms.business.user.job;

import com.maijia.cms.business.user.common.util.ExecuteTimeUtils;
import org.apache.log4j.Logger;
import org.elasticsearch.action.DocWriteRequest;
import org.elasticsearch.action.admin.indices.analyze.AnalyzeRequest;
import org.elasticsearch.action.admin.indices.analyze.AnalyzeResponse;
import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
import org.elasticsearch.index.reindex.UpdateByQueryRequest;
import org.elasticsearch.script.Script;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Component
public class ElasticQueryDSLTemplates {

    private static Logger logger = Logger.getLogger(ElasticQueryDSLTemplates.class);

    @Resource
    private RestHighLevelClient restHighLevelClient;

    public Set<String> esAnalyze(String content, String indexName) {
        AnalyzeRequest analyzeRequest = new AnalyzeRequest(indexName).text(content);

        AnalyzeResponse analyze = null;
        try {
            analyze = restHighLevelClient.indices().analyze(analyzeRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            logger.error("es分词出错,content="+content);
        }

        if (analyze != null) {
            return analyze.getTokens().stream().map(AnalyzeResponse.AnalyzeToken::getTerm).collect(Collectors.toSet());
        }

        return new HashSet<>();
    }

    public boolean esForceMerge(String... indexNames) {
        ForceMergeRequest forceMergeRequest = new ForceMergeRequest(indexNames);
        boolean flag = Boolean.TRUE;
        try {
            restHighLevelClient.indices().forcemerge(forceMergeRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            logger.error("es ForceMerge出错,indexNames="+indexNames+","+ e.getStackTrace());
            flag = Boolean.FALSE;
        }

        return flag;
    }

    public boolean bulkAddDocument(String indexName, List<Map<String, ?>> docList) {
        long l = System.currentTimeMillis();
        boolean flag = Boolean.TRUE;
        if (docList.size() < 1) {
            return true;
        }
        try {
            BulkRequest bulkRequest = new BulkRequest();
            docList.forEach(map -> bulkRequest.add(new IndexRequest(indexName).id(map.get("id") + "")
                    .source(map).opType(DocWriteRequest.OpType.INDEX)));
            restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
        } catch (Exception e) {
            logger.error("创建索引=【"+indexName+"】异常,索引内容="+e);
            flag = Boolean.FALSE;
        }
        //logger.info("批量创建{}{}条索引时:{}", indexName, docList.size(), (System.currentTimeMillis() - l));
        return flag;
    }

    public boolean bulkDeleteDocument(String indexName, QueryBuilder queryBuilder) {
        long l = System.currentTimeMillis();
        boolean flag = Boolean.TRUE;
        try {
            DeleteByQueryRequest deleteByQueryRequest = new DeleteByQueryRequest(indexName);
            deleteByQueryRequest.setQuery(queryBuilder);
            deleteByQueryRequest.setBatchSize(10000);
            deleteByQueryRequest.setConflicts("proceed");
            restHighLevelClient.deleteByQuery(deleteByQueryRequest, RequestOptions.DEFAULT);
        } catch (Exception e) {
            //logger.error("批量删除索=【{}】异常,索引内容={}", indexName, JsonNodeUtils.object2String(queryBuilder), e);
            flag = Boolean.FALSE;
        }
        //logger.info("批量删除{}索耗时:{}", indexName, (System.currentTimeMillis() - l));
        return flag;
    }

    public boolean bulkDeleteDocument(String indexName, List<Map<String, ?>> docList) {
        long l = System.currentTimeMillis();
        boolean flag = Boolean.TRUE;
        if (docList.size() < 1) {
            return true;
        }
        try {
            BulkRequest bulkRequest = new BulkRequest();
            docList.forEach(map -> bulkRequest.add(new DeleteRequest(indexName, map.get("id") + "")));
            restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
        } catch (Exception e) {
            //logger.error("批量删除索=【{}】异常,索引内容={}", indexName, JsonNodeUtils.object2String(docList), e);
            flag = Boolean.FALSE;
        }
        //logger.biz("批量删除{}索耗时:{}", indexName, (System.currentTimeMillis() - l));
        return flag;
    }

    public boolean bulkUpdateByQuery(String indexName, Map<String, Object> docDataMap, QueryBuilder queryBuilder) {
        boolean flag = Boolean.TRUE;
        if (docDataMap.size() < 1) {
            return Boolean.TRUE;
        }
        long l = System.currentTimeMillis();
        try {
            UpdateByQueryRequest updateByQueryRequest = new UpdateByQueryRequest(indexName);
            updateByQueryRequest.setConflicts("proceed");
            updateByQueryRequest.setQuery(queryBuilder);
            StringBuilder stringBuilder = new StringBuilder();
            docDataMap.forEach((key, value) -> stringBuilder.append("ctx._source.").append(key).append("=").append("params.").append(key).append(";"));

            Script script = new Script(Script.DEFAULT_SCRIPT_TYPE, Script.DEFAULT_SCRIPT_LANG, stringBuilder.toString(), docDataMap);

            updateByQueryRequest.setScript(script);
            restHighLevelClient.updateByQuery(updateByQueryRequest, RequestOptions.DEFAULT);
        } catch (Exception e) {
            //logger.error("批量修改索引【{}】异常,索引内容={}", indexName, "", e);
            flag = Boolean.FALSE;
        }

        return flag;
    }

    public boolean bulkUpdateListByQuery(String indexName, Map<String, Object> docDataMap, QueryBuilder queryBuilder) {
        boolean flag = Boolean.TRUE;
        if (docDataMap.size() < 1) {
            return Boolean.TRUE;
        }
        long l = System.currentTimeMillis();
        try {
            UpdateByQueryRequest updateByQueryRequest = new UpdateByQueryRequest(indexName);
            updateByQueryRequest.setConflicts("proceed");
            updateByQueryRequest.setQuery(queryBuilder);

            Script script = new Script(Script.DEFAULT_SCRIPT_TYPE,
                    Script.DEFAULT_SCRIPT_LANG, "for(int i=0; i<params.lives.size(); i++){int y=1; for(int x=0;x<ctx._source.lives.size();x++){if (params.lives[i].liveId==ctx._source.lives[x].liveId){y=0; break;}}if (y==1){ctx._source.lives.add(params.lives[i])    }}", docDataMap);

            updateByQueryRequest.setScript(script);
            restHighLevelClient.updateByQuery(updateByQueryRequest, RequestOptions.DEFAULT);
        } catch (Exception e) {
            //logger.error("批量修改索引【{}】异常,索引内容={}", indexName, JsonNodeUtils.object2String(docDataMap), e);
            flag = Boolean.FALSE;
        }
        return flag;
    }

    private void append(StringBuilder stringBuilder, Object value) {
        if (value instanceof String) {
            stringBuilder.append("'").append(value).append("'");
            return;
        }
        if (value instanceof List) {
            stringBuilder.append("[");
            List value1 = (List) value;
            for (int i = 0; i < value1.size(); i++) {
                if (i == value1.size() - 1) {
                    append(stringBuilder, value1.get(i));
                } else {
                    append(stringBuilder, value1.get(i));
                    stringBuilder.append(",");
                }
            }

            stringBuilder.append("]");
            return;
        }
        stringBuilder.append(value);
    }

    /**
     * 根据id获取文档
     *
     * @param indexName 索引名称
     * @param id        文档id
     * @param includes  需要返回的字段 空数组为所有字段返回
     * @param excludes  需要排除的字段
     * @return
     */
    public QueryResult<?> getDocumentById(String indexName, String id, String[] includes, String[] excludes) {
        SearchRequest searchRequest = new SearchRequest(indexName);
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

        searchSourceBuilder.query(QueryBuilders.termQuery("id", id));
        searchSourceBuilder.fetchSource(includes, excludes);
        QueryResult<?> qr = new QueryResult<>();
        excuteQuery(searchRequest, searchSourceBuilder, qr);
        return qr;
    }

    public SearchResponse excuteQuery(SearchRequest searchRequest, SearchSourceBuilder sourceBuilder, QueryResult<?> qr) {
        String searchId = UUID.randomUUID().toString().replaceAll("-", "");
        sourceBuilder.trackTotalHits(true);
        sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
        searchRequest.source(sourceBuilder);

        //logger.biz("searchId={},提交es搜索参数,={}", searchId, JsonNodeUtils.object2String(sourceBuilder));
        SearchResponse searchResponse = new SearchResponse();
        ExecuteTimeUtils instance = ExecuteTimeUtils.getInstance();
        instance.init();
        try {
            searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            logger.info("searchId="+searchId+",es搜索的耗时 "+instance.spentLastExecuteTime());
        } catch (IOException e) {
            logger.error("searchId="+searchId+",es搜索出错"+e);
            qr.setSuccessful(false);
            return searchResponse;
        }
        doQueryResult(searchResponse, qr);
        return searchResponse;
    }


    private void doQueryResult(SearchResponse searchResponse, QueryResult<?> qr) {
        // 查询耗时
        TimeValue took = searchResponse.getTook();
        qr.setTook(took.getMillis());
        // 是否超时
        qr.setTimeout(searchResponse.isTimedOut());
        // 查询总数
        qr.setHitsTotal(searchResponse.getHits().getTotalHits().value);
        // 最高评分
        qr.setMaxScore(searchResponse.getHits().getMaxScore());
        List<Map<String, Object>> hitsBody = new ArrayList<>();
        SearchHits searchHits = searchResponse.getHits();
        searchHits.forEach(searchHit -> {
            Map<String, Object> hitMap = searchHit.getSourceAsMap();
            if (hitMap != null) {
                hitsBody.add(hitMap);
            }
        });
        Aggregations aggregations = searchResponse.getAggregations();
        if (aggregations != null) {
            aggregations.forEach(aggregation -> qr.getAggregation().add(aggregation));
            qr.setAggMap(aggregations.asMap());
        }

        qr.setHitsBody(hitsBody);
    }

    public QueryResult<?> getDocumentByIds(String indexName, Collection<String> list, String[] includes, String[] excludes) {

        SearchRequest searchRequest = new SearchRequest(indexName);
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

        searchSourceBuilder.query(QueryBuilders.termsQuery("id", list));
        searchSourceBuilder.fetchSource(includes, excludes);
        QueryResult<?> qr = new QueryResult<>();
        excuteQuery(searchRequest, searchSourceBuilder, qr);
        return qr;
    }
}
ES查询操作执行类
package com.maijia.cms.business.user.job;

import org.elasticsearch.search.aggregations.Aggregation;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @author qiguoqiang
 * create 2019/7/3
 */
public class QueryResult<T> implements Serializable {

    /**
     * 序列化id
     */
    private static final long serialVersionUID = 7246688743186445516L;

    /**
     * 查询是否成功true=成功,false=失败
     */
    private boolean successful = true;

    /**
     * 查询耗时
     */
    private long took;

    /**
     * 是否超时
     */
    private boolean timeout;

    /**
     * 查询总数
     */
    private long hitsTotal;

    /**
     * 最高评分
     */
    private float maxScore;

    /**
     * 查询结果
     */
    private List<Map<String, Object>> hitsBody = new ArrayList<>();

    /**
     * 结果
     */
    private List<?> list = new ArrayList<>();

    /**
     * 聚合结果
     */
    private List<Aggregation> aggregation = new ArrayList<>();

    /**
     * 聚合结果Map
     */
    Map<String, Aggregation> aggMap;

    public List<?> getList() {
        return list;
    }

    public void setList(List<?> list) {
        this.list = list;
    }

    public List<Aggregation> getAggregation() {
        return aggregation;
    }

    public void setAggregation(List<Aggregation> aggregation) {
        this.aggregation = aggregation;
    }

    public QueryResult() {

    }

    public boolean isSuccessful() {
        return successful;
    }

    public void setSuccessful(boolean successful) {
        this.successful = successful;
    }

    public long getTook() {
        return took;
    }

    public void setTook(long took) {
        this.took = took;
    }

    public boolean isTimeout() {
        return timeout;
    }

    public void setTimeout(boolean timeout) {
        this.timeout = timeout;
    }

    public long getHitsTotal() {
        return hitsTotal;
    }

    public void setHitsTotal(long hitsTotal) {
        this.hitsTotal = hitsTotal;
    }

    public float getMaxScore() {
        return maxScore;
    }

    public void setMaxScore(float maxScore) {
        this.maxScore = maxScore;
    }

    public List<Map<String, Object>> getHitsBody() {
        return hitsBody;
    }

    public void setHitsBody(List<Map<String, Object>> hitsBody) {
        this.hitsBody = hitsBody;
    }

    public Map<String, Aggregation> getAggMap() {
        return aggMap;
    }

    public void setAggMap(Map<String, Aggregation> aggMap) {
        this.aggMap = aggMap;
    }
}
结果封装类
package com.maijia.cms.business.user.domain;

import lombok.Data;

import java.util.Date;

@Data
public class ApiRequestRecord {
    private Long id;
    /**
     * 开放平台appKey
     */
    private String appKey;
    /**
     * API服务名
     */
    private String apiMethod;
    /**
     * API服务名描述
     */
    private String apiDesc;
    /**
     * 调用成功次数
     */
    private Long successTotal;
    /**
     * 调用失败次数
     */
    private Long failureTotal;
    /**
     * 总调用次数
     */
    private Long requestTotal;
    /**
     * 调用成功计费(分)
     */
    private Long amount;
    /**
     * api类型:A、B、C、D
     */
    private String apiType;
    /**
     * 创建时间
     */
    private Date createDate;
}
入库结果集

 

posted on 2022-03-04 11:53  苟活之刍  阅读(260)  评论(0)    收藏  举报