最近接了一个埋点的需求,与我们平常接触的埋点不同, 平常接触的埋点或是对用户的点击率或登录情况等用户侧进行埋点 , 而这一次接触的埋点是: 对某一个功能的业务流程进行埋点统计, 主要是提供 分析在业务流程操作中, 一般出错率最高是在哪一个流程之中。

基于这一需求结合现有的成熟产品, 我们应该怎么设计才能达到: 高效、高质量、易扩展三结合为一体的功能?
实现的方式有很多,但需与当前项目的结构相匹配才是最好的实现方式。

分析问题的方向:什么人在什么地方操作了什么?

针对这一问题分析如下:
1.业务功能的操作,操作的是接口,那么功能的实现是针对接口进行埋点(因项目已实现了登录权限,则可获取当前操作人,一般的项目都是必须得有)

2.同一个操作可能针对不同的业务端,比如,上传用户数据,而在操作端这一块, 可能有分B端,管理端或C端等, 那么需对操作端进行汇总记录。(因项目的接口有对业务端进行区分模块,则我是知道哪一端)

3.不同的操作可能操作不同的业务功能,那么需对这些操作进行业务功能汇总: 比如,在不同的业务功能中, 都有导入execl数据, 那么可以把这些业务功能归类为:上传数据(自定义)。

好,分析完, 上设计图:
image.png
image.png

主要使用的技术: 自定义注解,切面,多线程异步处理
设计模式:策略模式,模板模式
代码如下:

/**
 * 事件埋点注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EventTrackingAnnotation {


    /**
     * 事件埋点策略枚举
     * @return
     */
    EventTrackingEnum eventTrackingEnum();

    /**
     * 模块标识
     * @return
     */
    BuriedPointModule buriedPointModule();

    /**
     * 平台标识
     * @return
     */
    BuriedPointPlatform buriedPointPlatform();

    /**
     * 事件埋点节点
     */
    BuriedPointNode buriedPointNode();
/**
 * 事件埋点切面
 */
@Slf4j
@Component
@Aspect
@Order(1)
public class EventTrackingAspect {

    @Resource
    private ApplicationEventPublisher publisher;


    @Pointcut("@annotation(com.test.test.strategy.event.tracking.annotation.EventTrackingAnnotation)")
    public void eventTracking() {
    }

    /**
     * 环绕
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("eventTracking()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        //获取注解的入参参数
        EventTrackingAnnotation annotation = method.getAnnotation(EventTrackingAnnotation.class);
        //事件埋点策略枚举
        EventTrackingEnum eventTrackingEnum = annotation.eventTrackingEnum();
        //模块标识
        BuriedPointModule buriedPointModule = annotation.buriedPointModule();
        //平台标识
        BuriedPointPlatform buriedPointPlatform = annotation.buriedPointPlatform();
        //事件埋点节点
        BuriedPointNode buriedPointNode = annotation.buriedPointNode();

        //获取业务参数
        Object[] args = joinPoint.getArgs();
        Map<String, Object> fieldsName = getFieldsName(method,args);
        boolean flag = true;
        if (fieldsName != null) {
            for (Map.Entry<String, Object> obj : fieldsName.entrySet()) {
                Object value = obj.getValue();
                //这里需要过滤导入导出等参数接口 如果不过滤则打印json时会报错 踩坑后补上的逻辑
                //测试只需要将接口中的参数加入 一下三个参数中的一个即可(注释到下面的判断)测试出结果
                //当然这个目前项目中只有这三种不能打印,其他的没有一一测试,以实际为准
                if (value instanceof MultipartFile || value instanceof HttpServletRequest || value instanceof HttpServletResponse) {
                    flag = false;
                }
            }
        }
        if (flag) {
            log.info("请求参数为:{}", JSON.toJSONString(fieldsName));
        }
        boolean isSuccess = true ;
        String message = null;
        try {
            // result的值就是被拦截方法的返回值
            Object result = joinPoint.proceed();
            log.info("请求结束,controller的返回值是:{}", JSON.toJSONString(result));
            return result;
        } catch (Throwable throwable){
            isSuccess = false;
            message = throwable.getMessage();
            throw throwable;
        } finally {
            // 异步处理事件埋点记录
            EventTrackingEvent event = new EventTrackingEvent();
            event.setEventTrackingEnum(eventTrackingEnum);
            event.setBuriedPointModule(buriedPointModule);
            event.setBuriedPointPlatform(buriedPointPlatform);
            event.setBuriedPointNode(buriedPointNode);
            event.setState(isSuccess);
            event.setFailMsg(message);
            event.setFieldsName(fieldsName);
            publisher.publishEvent(event);

        }

    }


    /**
     * 根据切面获取请求参数列表
     */
    private static Map<String, Object> getFieldsName(Method method,Object[] args) {
        ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();

        String[] parameterNames = pnd.getParameterNames(method);
        Map<String, Object> paramMap = new HashMap<>(32);
        if (parameterNames != null) {
            for (int i = 0; i < parameterNames.length; i++) {
                paramMap.put(parameterNames[i], args[i]);
            }
        }
        return paramMap;
    }
}
/**
 * 事件埋点异步处理器
 */
@Slf4j
@Component
public class EventTrackingListener {

    @Autowired
    private EventTrackingFactory eventTrackingFactory;


    @Async
    @TransactionalEventListener(fallbackExecution = true, phase = TransactionPhase.AFTER_COMMIT)
    public void touchTrackingFactory(EventTrackingEvent event) {
        log.info("事件埋点异步处理器,event={}",event);
        EventTrackingEnum eventTrackingEnum = event.getEventTrackingEnum();
        BuriedPointModule buriedPointModule = event.getBuriedPointModule();
        BuriedPointPlatform buriedPointPlatform = event.getBuriedPointPlatform();
        BuriedPointNode buriedPointNode = event.getBuriedPointNode();
        boolean state = event.isState();
        String failMsg = event.getFailMsg();
        Map<String, Object> fieldsName = event.getFieldsName();

        //初始化具体的策略
        EventTrackingStrategy strategy = eventTrackingFactory.getStrategy(eventTrackingEnum);

        //各自的实现策略根据接口入参, 各自封装自己需要的业务参数
        Map<String, Object> stringObjectMap = strategy.initParam(fieldsName);

        //触发埋点
        TrackingDTO trackingDTO = new TrackingDTO();
        trackingDTO.setBuriedPointModule(buriedPointModule);
        trackingDTO.setBuriedPointPlatform(buriedPointPlatform);
        trackingDTO.setBuriedPointNode(buriedPointNode);
        trackingDTO.setState(state);
        trackingDTO.setFailMsg(failMsg);
        trackingDTO.setData(stringObjectMap);
        strategy.tracking(trackingDTO);
    }

}
/**
 * 事件埋点工厂类
 */
@Slf4j
@Component
public class EventTrackingFactory {
    Map<EventTrackingEnum,EventTrackingStrategy> handlerMap = new HashMap<>(4);


    @PostConstruct
    private void init(){
        Map<String, EventTrackingStrategy> beansMap = SpringHelper.getApplicationContext().getBeansOfType(EventTrackingStrategy.class);
        beansMap.forEach((k,v)->{
            this.handlerMap.put(v.getStrategy(),v);
        });

    }



    public EventTrackingStrategy getStrategy(EventTrackingEnum strategyEnum){
        return handlerMap.get(strategyEnum);
    }

}
/**
 * 事件埋点策略抽象类
 */
public interface EventTrackingStrategy {

    /**
     * 获取指定策略
     * @return
     */
    EventTrackingEnum getStrategy();

    /**
     * 封装参数
     * @return 用Map的原因是针对不同的接口,有对应的业务参数, 可对业务参数处理完后返回自身需要的参数
     */
    Map<String,Object> initParam(Map<String,Object> paramMap);

    /**
     * 埋点记录
     * @return
     */
    void tracking(TrackingDTO trackingDTO);


}
/**
 * 事件埋点基类
 */
public class EventTrackingBase {

  


    /**
     * 埋点记录
     * @param signBuriedPointLog
     */
    public void sendSls(SignBuriedPointLog signBuriedPointLog) {
         //根据需求对埋点数据进行入库操作

    }


    /**
     * Object转成指定的类型
     * @param obj
     * @param type
     * @param <T>
     * @return
     */
    public static<T> T convert(Object obj, Class<T> type) {
        if (obj != null && StringUtils.isNotBlank(obj.toString())) {
            if (type.equals(Integer.class)||type.equals(int.class)) {
                return (T)Integer.valueOf(StringUtils.trim(obj.toString()));
            } else if (type.equals(Long.class)||type.equals(long.class)) {
                return (T)Long.valueOf(StringUtils.trim(obj.toString()));
            } else if (type.equals(Boolean.class)||type.equals(boolean.class)) {
                return (T)Boolean.valueOf(StringUtils.trim(obj.toString()));
            } else if (type.equals(Short.class)||type.equals(short.class)) {
                return (T)Short.valueOf(StringUtils.trim(obj.toString()));
            } else if (type.equals(Float.class)||type.equals(float.class)) {
                return (T)Float.valueOf(StringUtils.trim(obj.toString()));
            } else if (type.equals(Double.class)||type.equals(double.class)) {
                return (T)Double.valueOf(StringUtils.trim(obj.toString()));
            } else if (type.equals(Byte.class)||type.equals(byte.class)) {
                return (T)Byte.valueOf(StringUtils.trim(obj.toString()));
            } else if (type.equals(Character.class)||type.equals(char.class)) {
                return (T)Character.valueOf(obj.toString().charAt(0));
            } else if (type.equals(String.class)) {
                return (T) obj;
            } else if (type.equals(BigDecimal.class)) {
                return (T) new BigDecimal(StringUtils.trim(obj.toString()));
            } else if (type.equals(LocalDateTime.class)) {
                return (T) LocalDateTime.parse(obj.toString());
            } else if (type.equals(Date.class)) {
                try {
                    SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
                    return (T) formatter.parse(obj.toString());
                } catch (ParseException e) {
                    throw new RuntimeException(e.getMessage());
                }
            }else{
                return null;
            }
        } else {
            return null;

        }
    }


}

以下为策略具体的实现类,我就放一个了, 其他可自行实现

/**
 * 数据上传埋点策略实现类
 */
@Slf4j
@Component
public class DataUploadEventTrackingStrategy extends EventTrackingBase implements EventTrackingStrategy {


    @Override
    public EventTrackingEnum getStrategy() {
        return EventTrackingEnum.DATA_UPLOAD;
    }

    
    @Override
    public Map<String,Object> initParam(Map<String,Object> paramMap) {
        return null;
    }

    @Override
    public void tracking(TrackingDTO trackingDTO) {
        BuriedPointModule buriedPointModule = trackingDTO.getBuriedPointModule();
        BuriedPointPlatform buriedPointPlatform = trackingDTO.getBuriedPointPlatform();
        BuriedPointNode buriedPointNode = trackingDTO.getBuriedPointNode();
        boolean state = trackingDTO.isState();
        String failMsg = trackingDTO.getFailMsg();
        Map<String, Object> data = trackingDTO.getData();

        Date date = new Date();
        String traceId = TraceContext.traceId();

        //触发埋点记录
        SignBuriedPointLog signBuriedPointLog = SignBuriedPointLog.builder()
                .bizTime(date)
                .traceId(traceId)
                .module(buriedPointModule.name())
                .platform(buriedPointPlatform.name())
                .node(buriedPointNode.name())
                .resultFlag(state)
                .failReason(failMsg)
                .build();
        sendSls(signBuriedPointLog);


    }
}

好,总体思路是这样, 部分实体类对象就不提供了, 分享的是一种解决该类似需求的方案。

个人能力有限,各位大佬可一起探讨更多更好的实现

不喜勿喷哈


指尖上的Ken
1 声望1 粉丝