xChar

JMeter

下载地址

其中Binaries是编译好的版本,Source是源码包

tgz是Linux版本,zip是windows版本

  • 变更JMeter语言

    找到JMeter配置文件 D:\tools\apache-jmeter-5.5\bin\jmeter.properties

    修改语言(默认英文):

    language=zh_CN

添加日志切面

需要开启配置:test.function.cost=false

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Configuration;

/**
 * 打印方法调用耗时和方法入参
 * @author caimeng
 * @date 2023/06/13 10:58
 */
@Slf4j
@Aspect
@Configuration
@ConditionalOnExpression("${test.function.cost:false}")
public class ServiceDigestAop {
    public ServiceDigestAop() {
    }

    /**
     * 切面:方法调用时间和方法入参
     * @param joinPoint 切点
     * @return 方法调用返回数据
     * @throws Throwable 调用失败返回的异常
     */
    @Around("execution (* com.gomain..*.service.impl.*.*(..))" +
            "||execution(* com.gomain.api.sign.controller.ShZhStandardController.*(..))" +
            "||execution(* com.gomain.sign.feign.SignFeign.*(..))" +
            "||execution(* com.gomain.seal.feign.SealQueryFeign.*(..))" +
            "||execution(* com.gomain.api.mq.producer.Producer.*(..))" +
            "||execution(* com.gomain.calculate.feign.CalculateServiceFeign.*(..))")
    public Object timeAround(ProceedingJoinPoint joinPoint) throws Throwable{
        Object obj = null;
        Object[] args = joinPoint.getArgs();
        long startTime = System.currentTimeMillis();
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        String methodName = signature.getDeclaringTypeName() + "_" + signature.getName();
        this.printBeginLog(methodName, args);
        try {
            obj = joinPoint.proceed(args);
        } catch (Throwable var10) {
            this.printErrorLog(methodName, args, var10);
            throw var10;
        }
        long endTime = System.currentTimeMillis();
        this.printExecTime(methodName, args, startTime, endTime);
        return obj;
    }

    /**
     * 是否基本数据类型,或字符串类型
     * @param clazz 待检测类
     * @return 是否基本类型或字符串类型
     */
    private boolean isPrimitive(Class<?> clazz) {
        return clazz.isPrimitive() || clazz == String.class;
    }

    /**
     * 是否短字符串
     * @param str 待检测字符串
     * @return 是否短字符串
     */
    private boolean isShortString(String str) {
        return str.length() <= 100;
    }

    /**
     * 打印方法开始执行时间点
     * @param methodName 方法名
     * @param args 方法参数
     */
    private void printBeginLog(String methodName, Object[] args) {
        String param = this.generateParamDigest(args);
        log.info("[{}] begin param [{}]", methodName, param);
    }

    /**
     * 打印方法成功执行后时间点
     * @param methodName 方法名
     * @param args 方法参数
     */
    private void printExecTime(String methodName, Object[] args, long startTime, long endTime) {
        String param = this.generateParamDigest(args);
        log.info("[{}] success param [{}] cost {} ms", methodName, param, endTime - startTime);
    }

    /**
     * 方法执行失败,打印参数和错误消息
     * @param methodName 方法名
     * @param args 方法参数
     */
    private void printErrorLog(String methodName, Object[] args, Throwable e) {
        String param = this.generateParamDigest(args);
        String errormsg = e.getMessage().replaceAll("\\s*", "_");
        log.error("[{}] error param [{}] errmsg [{}]", methodName, param, errormsg);
    }

    /**
     * 简单方法参数
     * @param args 方法参数
     * @return 方法参数的简易拼接
     */
    private String generateParamDigest(Object[] args) {
        if (args == null) {
            return "args is null !";
        } else {
            StringBuffer argString = new StringBuffer();
            Object[] var3 = args;
            int var4 = args.length;

            for(int var5 = 0; var5 < var4; ++var5) {
                Object arg = var3[var5];
                if (arg != null) {
                    if (this.isPrimitive(arg.getClass())) {
                        if (this.isShortString(arg.toString())) {
                            argString.append(arg).append(",");
                        } else {
                            argString.append(arg.getClass().getName()).append(",");
                        }
                    } else {
                        argString.append(arg.getClass().getName()).append(",");
                    }
                }
            }

            return argString.toString();
        }
    }
}

配置日志打印文件

<appender name="digestLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!--encoder 默认配置为PatternLayoutEncoder-->
    <encoder>
        <pattern>${LOG_PATTERN}</pattern>
    </encoder>
    <!--滚动策略-->
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <!--路径-->
        <fileNamePattern>${LOG_PATH}/digest-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
        <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
            <maxFileSize>500MB</maxFileSize>
        </timeBasedFileNamingAndTriggeringPolicy>
        <!--MaxHistory指的是文件数量,超过MaxHistory数量才会删除日志文件 -->
        <maxHistory>10</maxHistory>
        <!--TimeBasedRollingPolicy下logback 启动项目时候 默认不删除多余的文件-->
        <cleanHistoryOnStart>true</cleanHistoryOnStart>
    </rollingPolicy>
</appender>
...

<logger name="com.gomain.api.configuration.ServiceDigestAop" level="info">
    <appender-ref ref="digestLog"/>
</logger>

日志分析

cat digest-2023-06-13.0.log | grep 'StampServiceImpl_stampApi' | sed -E 's/^.*StampServiceImpl_stampApi.*(cost * ms)$/\1/' | awk '{print $14}'

第8位是traceId,第14位是耗时

取平均值

cat data | awk 'BEGIN {sum = 0;line = 0}  {sum += $1;line+=1} END {printf "NR=%d,Average=%3.3fms\n",line,sum*2/line}'

以上不合适,以下面的为准

接口调用耗时

cat digest.log | grep 'ShZhStandardController_genCentralizedSignatureSTD' | awk '{if($14!=""){print $14}}'| awk 'BEGIN {sum = 0;line = 0}  {sum += $1;line+=1} END {printf "line=%d,Average=%3.3fms\n",line,sum/line}'
Loading comments...