天天看點

調用鍊監控Cat實戰

1、Docker快速部署Cat

下載下傳Cat源碼:

git clone https://github.com/dianping/cat.git
           

容器建構:

cd docker
docker-compose up
           

使用官方的腳本啟動報錯:

Creating cat-mysql ... done
Creating cat       ... error

ERROR: for cat  Cannot create container for service cat: conflicting options: host type networking can't be used with links. This would result in undefined behavior

ERROR: for cat  Cannot create container for service cat: conflicting options: host type networking can't be used with links. This would result in undefined behavior
ERROR: Encountered errors while bringing up the project.
           

修改

docker-compose.yml

檔案,掉

network_mode: "host"

即可,修改後的

docker-compose.yml

檔案如下:

# [email protected]

version: '2.2'

services:
  cat:
    image: rolesle/cat:0.0.1
    container_name: cat

    ######## build from Dockerfile ###########
    # build:
    #  context: ../
    #  dockerfile: ./docker/Dockerfile
    ######## End -> build from Dockerfile ###########

    environment:
      # if you have your own mysql, config it here, and disable the 'mysql' config blow
      - MYSQL_URL=cat-mysql # links will maintain /etc/hosts, just use 'container_name'
      - MYSQL_PORT=3306
      - MYSQL_USERNAME=root
      - MYSQL_PASSWD=
      - MYSQL_SCHEMA=cat
      # 必須設定成你的機器IP位址
      # - SERVER_IP=YOUR IP
    working_dir: /app
    volumes:
      # 預設127.0.0.1,可以修改為自己真實的伺服器叢集位址
      - "./client.xml:/data/appdatas/cat/client.xml"
      # 預設使用環境變量設定。可以啟用本注解,并修改為自己的配置
#      - "./datasources.xml:/data/appdatas/cat/datasources.xml"
    command: /bin/sh -c 'chmod +x /datasources.sh && /datasources.sh && catalina.sh run'
    links:
      - mysql
    depends_on:
      - mysql
    ports:
      - "8080:8080"
      - "2280:2280"
#    network_mode: "host"

  # disable this if you have your own mysql
  mysql:
    container_name: cat-mysql
    image: mysql:5.7.22
    # expose 33306 to client (navicat)
    ports:
       - 33306:3306
    volumes:
      # change './docker/mysql/volume' to your own path
      # WARNING: without this line, your data will be lost.
      - "./mysql/volume:/var/lib/mysql"
      # 第一次啟動,可以通過指令建立資料庫表 :
      # docker exec 容器id bash -c "mysql -uroot -Dcat < /init.sql"
      - "../script/CatApplication.sql:/init.sql"
    command: mysqld -uroot --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --init-connect='SET NAMES utf8mb4;' --innodb-flush-log-at-trx-commit=0
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: "true"
      MYSQL_DATABASE: "cat"
      MYSQL_USER: "root"
      MYSQL_PASSWORD: ""
           

第一次運作以後,資料庫中沒有表結構,需要通過下面的指令建立表:

docker exec <container_id> bash -c "mysql -uroot -Dcat < /init.sql"
           

<container_id>

需要替換為容器的真實id。通過docker ps可以檢視到mysql容器id

mysql占用的端口為33306,使用者名為root,密碼為空

通路http://localhost:8080/cat即可進行Cat的首頁面

調用鍊監控Cat實戰

官方部署文檔:https://github.com/dianping/cat/wiki/readme_server

2、SpringBoot項目內建Cat監控

1)、啟動Cat用戶端前的準備工作

建立

/data/appdatas/cat

目錄,并授權

mkdir -p /data/appdatas/cat
chmod -R 777 /data/
           

建立

/data/appdatas/cat/client.xml

,内容如下

<?xml version="1.0" encoding="utf-8"?>
<config mode="client">
    <servers>
        <!-- ip:部署cat應用的伺服器ip port:cat服務端接收用戶端資料的端口 http-port:cat應用部署到的tomcat端口-->
        <server ip="127.0.0.1" port="2280" http-port="8080"/>
    </servers>
</config>
           

2)、pom.xml

引入cat-client的依賴

<dependency>
            <groupId>com.dianping.cat</groupId>
            <artifactId>cat-client</artifactId>
            <version>3.0.0</version>
        </dependency>
           

指定cat-client私有倉庫位址

<repositories>
        <repository>
            <id>unidal-nexus-repo</id>
            <url>http://unidal.org/nexus/content/repositories/releases</url>
        </repository>
    </repositories>
           

3)、app.properties檔案

在你的項目中建立

src/main/resources/META-INF/app.properties

檔案, 并添加如下内容:

app.name={appkey}
           

4)、Cat消息鍊建構思路

cat鍊路樹是通過消息編号串聯起來的,編号模型:

public static interface Context {

    // 根的編号
		public final String ROOT = "_catRootMessageId";

    // 上級編号
		public final String PARENT = "_catParentMessageId";

    // 子級編号
		public final String CHILD = "_catChildMessageId";

		public void addProperty(String key, String value);

		public String getProperty(String key);
	}
           

消息樹就是上下級編号關聯起來的,是以如果是跨服務通過HTTP調用,要把編号模型放到HTTP頭中,進而使得下遊服務能夠擷取到

以A服務調用B服務(

A->B

)為例:

A用戶端生成編号模型,然後通過HTTP請求調用(底層使用HTTP請求,上層可能是Feign或者RestTemplate等等)的時候帶過去

B用戶端接收到編号模型,在本地生成消息樹的時候,将編号模型植入進去完成綁定

5)、代碼實作

對一個服務的埋點包含三個部分:服務的入口點埋點、調用下遊服務時埋點和資料庫埋點

1)服務的入口點埋點

服務的入口點埋點是通過Filter來實作的,過濾器中先從HTTP請求頭中擷取編号模型來恢複調用鍊

public class CatServletFilter implements Filter {

    private String[] urlPatterns = new String[0];

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        String patterns = filterConfig.getInitParameter("CatHttpModuleUrlPatterns");
        if (patterns != null) {
            patterns = patterns.trim();
            urlPatterns = patterns.split(",");
            for (int i = 0; i < urlPatterns.length; i++) {
                urlPatterns[i] = urlPatterns[i].trim();
            }
        }
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) servletRequest;

        String url = request.getRequestURL().toString();
        for (String urlPattern : urlPatterns) {
            if (url.startsWith(urlPattern)) {
                url = urlPattern;
            }
        }

        // 恢複調用鍊
        CatContext catContext = new CatContext();
        catContext.addProperty(Cat.Context.ROOT, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID));
        catContext.addProperty(Cat.Context.PARENT, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID));
        catContext.addProperty(Cat.Context.CHILD, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID));
        Cat.logRemoteCallServer(catContext);

        Transaction t = Cat.newTransaction(CatConstants.TYPE_URL, url);

        try {

            Cat.logEvent("Service.method", request.getMethod(), Message.SUCCESS, request.getRequestURL().toString());
            Cat.logEvent("Service.client", request.getRemoteHost());

            filterChain.doFilter(servletRequest, servletResponse);

            t.setStatus(Transaction.SUCCESS);
        } catch (Exception ex) {
            t.setStatus(ex);
            Cat.logError(ex);
            throw ex;
        } finally {
            t.complete();
        }
    }

    @Override
    public void destroy() {

    }
}
           
public class CatContext implements Cat.Context {

    private Map<String, String> properties = new HashMap<>();

    @Override
    public void addProperty(String key, String value) {
        properties.put(key, value);
    }

    @Override
    public String getProperty(String key) {
        return properties.get(key);
    }
}
           
public class CatHttpConstants {
    public static final String CAT_HTTP_HEADER_CHILD_MESSAGE_ID = "X-CAT-CHILD-ID";
    public static final String CAT_HTTP_HEADER_PARENT_MESSAGE_ID = "X-CAT-PARENT-ID";
    public static final String CAT_HTTP_HEADER_ROOT_MESSAGE_ID = "X-CAT-ROOT-ID";
}
           
@Configuration
public class CatFilterConfig {

    @Bean
    public FilterRegistrationBean catFilter() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        CatServletFilter filter = new CatServletFilter();
        registration.setFilter(filter);
        registration.addUrlPatterns("/*");
        registration.setName("cat-filter");
        registration.setOrder(1);
        return registration;
    }
}
           
2)調用下遊服務時埋點

由于調用下遊服務時使用的是RestTemplate,是以這裡用到了RestTemplate攔截器,這裡會把編号模型放到HTTP請求頭中以便下遊服務能夠擷取到

public class CatRestInterceptor implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
            throws IOException {

        Transaction t = Cat.newTransaction(CatConstants.TYPE_CALL, request.getURI().toString());

        try {
            HttpHeaders headers = request.getHeaders();

            // 儲存和傳遞CAT調用鍊上下文
            Context ctx = new CatContext();
            Cat.logRemoteCallClient(ctx);
            headers.add(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID, ctx.getProperty(Cat.Context.ROOT));
            headers.add(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID, ctx.getProperty(Cat.Context.PARENT));
            headers.add(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID, ctx.getProperty(Cat.Context.CHILD));

            // 保證請求繼續被執行
            ClientHttpResponse response =  execution.execute(request, body);
            t.setStatus(Transaction.SUCCESS);
            return response;
        } catch (Exception e) {
            Cat.getProducer().logError(e);
            t.setStatus(e);
            throw e;
        } finally {
            t.complete();
        }
    }
}
           
@Configuration
public class RestTemplateConfig {

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        // 儲存和傳遞調用鍊上下文
        restTemplate.setInterceptors(Collections.singletonList(new CatRestInterceptor()));
        return restTemplate;
    }
}
           

如果使用Feign進行調用,可以實作RequestInterceptor:

@Component
public class FeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        Cat.Context ctx = new CatContext();
        Cat.logRemoteCallClient(ctx);
        requestTemplate.header(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID, ctx.getProperty(Cat.Context.ROOT));
        requestTemplate.header(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID, ctx.getProperty(Cat.Context.PARENT));
        requestTemplate.header(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID, ctx.getProperty(Cat.Context.CHILD));
    }
}
           
3)資料庫埋點

內建mybatis攔截器,這裡資料源用的是HikariDataSource,如果是其他資料源修改

getSqlURL()

方法中的判斷即可

@Intercepts({
        @Signature(method = "query", type = Executor.class, args = {
                MappedStatement.class, Object.class, RowBounds.class,
                ResultHandler.class}),
        @Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class})
})
@Component
public class CatMybatisInterceptor implements Interceptor {

    private static Log logger = LogFactory.getLog(CatMybatisInterceptor.class);

    //緩存,提高性能
    private static final Map<String, String> sqlURLCache = new ConcurrentHashMap<String, String>(256);

    private static final String EMPTY_CONNECTION = "jdbc:mysql://unknown:3306/%s?useUnicode=true";

    private Executor target;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        //得到類名,方法
        String[] strArr = mappedStatement.getId().split("\\.");
        String methodName = strArr[strArr.length - 2] + "." + strArr[strArr.length - 1];

        Transaction t = Cat.newTransaction("SQL", methodName);

        //得到sql語句
        Object parameter = null;
        if (invocation.getArgs().length > 1) {
            parameter = invocation.getArgs()[1];
        }
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        Configuration configuration = mappedStatement.getConfiguration();
        String sql = showSql(configuration, boundSql);

        //擷取SQL類型
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
        Cat.logEvent("SQL.Method", sqlCommandType.name().toLowerCase(), Message.SUCCESS, sql);

        String s = this.getSQLDatabase();
        Cat.logEvent("SQL.Database", s);

        Object returnObj = null;
        try {
            returnObj = invocation.proceed();
            t.setStatus(Transaction.SUCCESS);
        } catch (Exception e) {
            Cat.logError(e);
        } finally {
            t.complete();
        }

        return returnObj;
    }

    private javax.sql.DataSource getDataSource() {
        org.apache.ibatis.transaction.Transaction transaction = this.target.getTransaction();
        if (transaction == null) {
            logger.error(String.format("Could not find transaction on target [%s]", this.target));
            return null;
        }
        if (transaction instanceof SpringManagedTransaction) {
            String fieldName = "dataSource";
            Field field = ReflectionUtils.findField(transaction.getClass(), fieldName, javax.sql.DataSource.class);

            if (field == null) {
                logger.error(String.format("Could not find field [%s] of type [%s] on target [%s]",
                        fieldName, javax.sql.DataSource.class, this.target));
                return null;
            }

            ReflectionUtils.makeAccessible(field);
            javax.sql.DataSource dataSource = (javax.sql.DataSource) ReflectionUtils.getField(field, transaction);
            return dataSource;
        }

        logger.error(String.format("---the transaction is not SpringManagedTransaction:%s", transaction.getClass().toString()));

        return null;
    }

    private String getSqlURL() {
        javax.sql.DataSource dataSource = this.getDataSource();

        if (dataSource == null) {
            return null;
        }
        if (dataSource instanceof HikariDataSource) {
            return ((HikariDataSource) dataSource).getJdbcUrl();
        }
        return null;
    }

    private String getSQLDatabase() {
//        String dbName = RouteDataSourceContext.getRouteKey();
        //根據設定的多資料源修改此處,擷取dbname
        String dbName = null;
        if (dbName == null) {
            dbName = "DEFAULT";
        }
        String url = CatMybatisInterceptor.sqlURLCache.get(dbName);
        if (url != null) {
            return url;
        }

        //目前監控隻支援mysql ,其餘資料庫需要各自修改監控服務端
        url = this.getSqlURL();
        if (url == null) {
            url = String.format(EMPTY_CONNECTION, dbName);
        }
        CatMybatisInterceptor.sqlURLCache.put(dbName, url);
        return url;
    }

    /**
     * 解析sql語句
     *
     * @param configuration
     * @param boundSql
     * @return
     */
    public String showSql(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (parameterMappings.size() > 0 && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(parameterObject)));

            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
                    }
                }
            }
        }
        return sql;
    }

    /**
     * 參數解析
     *
     * @param obj
     * @return
     */
    private String getParameterValue(Object obj) {
        String value = null;
        if (obj instanceof String) {
            value = "'" + obj.toString() + "'";
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format(new Date()) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }

        }
        return value;
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            this.target = (Executor) target;
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
    }

}
           
@Configuration
public class CatMyBatisConfig {
    @Resource
    private CatMybatisInterceptor catMybatisInterceptor;

    @Bean
    public Interceptor[] plugins() {
        return new Interceptor[]{catMybatisInterceptor};
    }
}
           

請求接口,調用鍊資訊如下:

調用鍊監控Cat實戰

參考:

Cat提供的架構內建方案:https://github.com/dianping/cat/tree/v2.0.0/%E6%A1%86%E6%9E%B6%E5%9F%8B%E7%82%B9%E6%96%B9%E6%A1%88%E9%9B%86%E6%88%90

Cat Client for Java:https://github.com/dianping/cat/blob/master/lib/java/README.zh-CN.md

https://blog.csdn.net/lkx444368875/article/details/80887496