文章目录

  • CAT源码分析 - 客户端(中 - 消息API)
  • 1. 官方Demo
  • 2. newTransaction创建
  • 2.1 获取Context上下文
  • 2.2 创建Transaction消息
  • 2.3 开启Transaction
  • 3. Complete结束
  • 3.1 检查Problem报表
  • 3.2 end方法
  • 4. logEvent添加Event事件
  • 5. logMetric添加业务埋点
  • 5.1 logMetricForCount
  • 5.2 logMerticForDuration
  • 6. logError记录异常
  • 7. errorHandler
  • 8. 远程调用串联
  • 8.1 logRemoteClient
  • 8.2 Cat.Context
  • 8.3 logRemoteServer


CAT源码分析 - 客户端(中 - 消息API)

由于cat-client目录下的代码官方已经不再维护更新,所以分析lib目录下的cat客户端源码

1. 官方Demo

CAT客户端的源码分析以官方的使用QuickStart入手

如上图代码所示:

  • 通过Cat客户端开启了一个Transaction,记录整个方法的运行时间。
  • 在过程中“记录”(插入)了成功的Event事件,和两个Metric业务指标。
  • 最后设置Transaction成功 SUCCESS ,标记结束 complete() 。

下面以上述代码,分析客户端源码

android 美团源代码 美团源码app_android 美团源代码

2. newTransaction创建

com.dianping.cat.Cat#newTransaction方法传入type和name作为参数,二者是CAT报表展示时的目录层级分类,type下包含name。
isEnabled检查是否启用CAT客户端后,获取 DefaultMessageProducer ,这部分逻辑上一章已经描述过。
通过 DefaultMessageProducer 执行newTransaction方法

public static Transaction newTransaction(String type, String name) {
    if (isEnabled()) {
        try {
            return Cat.getProducer().newTransaction(type, name);
        } catch (Exception e) {
            errorHandler(e);
            return NullMessage.TRANSACTION;
        }
    } else {
        return NullMessage.TRANSACTION;
    }
}

com.dianping.cat.message.internal.DefaultMessageProducer#newTransaction方法中,
首先通过DefaultMessageManager检查ThreadLocal中是否已有Context上下文。如果没有创建Context上下文,则执行setUp方法执行创建。
由于Cat客户端是单实例的,因此要使用ThreadLocal保证采集数据的线程安全,避免使用锁保证性能
完成Context上下文的读取/创建后,生成一个DefaultTransaction的Message。
开启Transaction,设置其中相关信息

public Transaction newTransaction(String type, String name) {
    // 1.获取或者创建ThreadLocal的上下文
    if (!manager.hasContext()) {
        manager.setup();
    }
	// 2.创建消息
    DefaultTransaction transaction = new DefaultTransaction(type, name, manager);
	// 3.开启Transaction
    manager.start(transaction, false);
    return transaction;
}

2.1 获取Context上下文

获取上下文的方法主要包含在com.dianping.cat.message.internal.DefaultMessageManager类中,其主要逻辑如下
setUp时会读取采样率,默认配置为1.0即全部采样。如果设置采样率小于1,会设置标记位,在发送时再进行处理。(采样逻辑以后文章介绍)

@Override
public boolean hasContext() {
    // 从ThreadLocal中读上下文
    Context context = this.context.get();
    return context != null;
}

@Override
public void setup() {
    // 没读到时创建一个Context
    Context ctx = new Context(domain, hostName, ip);
    double samplingRate = configService.getSamplingRate();
	// 判断是否命中采样
    if (samplingRate < 1.0 && hitSample(samplingRate)) {
        ctx.tree.setHitSample(true);
    }
    // 保存到ThreadLocal中
    context.set(ctx);
}

private boolean hitSample(double sampleRatio) {
    // sampleCount是单例Manager的一个单实例,因此不同线程调用setup时,一定会使sampleCount增加,触发采样判断
    int count = sampleCount.incrementAndGet();
    return count % ((int) (1.0 / sampleRatio)) == 0;
}

2.2 创建Transaction消息

创建Transaction的逻辑较为简单,引用MessageManager,保存Transaction开始时间等,设置基础的消息内容

public DefaultTransaction(String type, String name, MessageManager manager) {
    super(type, name);
    this.manager = manager;
    durationStart = System.nanoTime();
}

// com.dianping.cat.message.internal.AbstractMessage#AbstractMessage
AbstractMessage(String type, String name) {
    this.type = String.valueOf(type);
    this.name = String.valueOf(name);
    timestampInMillis = System.currentTimeMillis();
}

2.3 开启Transaction

通过Manager开启Transaction是核心逻辑 com.dianping.cat.message.internal.DefaultMessageManager#start,通过上下文,开启Transaction

public void start(Transaction transaction, boolean forked) {
    Context ctx = getContext();

    if (ctx != null) {
        ctx.start(transaction, forked);
    } else if (firstMessage) {
        // Cat客户端正常调用时,这部分逻辑不会进入,因为已经setUp
        // 如果直接调用,调用不规范,则会进入这部分
        firstMessage = false;
        LOGGER.error("CAT client is not enabled because it's not initialized yet");
    }
}

com.dianping.cat.message.internal.DefaultMessageManager.Context#start中:
stack变量是Context保存Transaction的集合private Stack<Transaction> stack 当栈为空时,说明当前线程上下文中没有父Transaction,messageTree中直接保存当前message
当栈不为空时,从栈顶读取消息,将当前transaction设置为其子transaction。保存transaction之间的父子关系,即transaction“树”。
最后,将当前消息压入栈。对于异步Transaction暂时不解释
注:如果Transaction是子消息,那么只是消息只是入栈,并没有修改Context上下文MessageTree所引用的Message对象。Tree中的Transaction类型Message,只保存根节点

public void start(Transaction transaction, boolean forked) {
    if (!stack.isEmpty()) {
        // Do NOT make strong reference from parent transaction to forked transaction.
        // Instead, we create a "soft" reference to forked transaction later, via linkAsRunAway()
        // By doing so, there is no need for synchronization between parent and child threads.
        // Both threads can complete() anytime despite the other thread.

        if (!(transaction instanceof ForkedTransaction)) {
            // 读取栈顶消息,但是不取出
            Transaction parent = stack.peek();
            // 添加子消息
            addTransactionChild(transaction, parent);
        }
    } else {
        tree.setMessage(transaction);
    }
    if (!forked) {
        // 消息入栈
        stack.push(transaction);
    }
}

添加子消息时,对消息的触发时间是否是当前小时、子消息是否超过2000条进行判断。
如果不满组响应条件,则会触发截断操作,发送一次消息。之后,将消息添加到Transaction的List中

关于长度Length
length保存在线程上下文中,每当添加一个子transaction时加一,累加到root transaction发送清空上下文为止;
同级transaction有多个兄弟时,也一定是前一个兄弟先end,再压入新的弟transaction。
length = 深度 + 个数

// 传入当前消息和parent
private void addTransactionChild(Message message, Transaction transaction) {
    long treePeriod = tree.getMessage().getTimestamp() / HOUR;
    long messagePeriod = (message.getTimestamp() - 10 * 1000L) / HOUR; // 10 seconds extra time allowed
    // 判断时间周期是否还是当前小时
    // 上下文中父子消息数量超过2000
    if (treePeriod < messagePeriod || length >= SIZE) {
        // 发送
        validator.truncateAndFlush(this, message.getTimestamp());
    }
	// 添加子消息到父消息的集合中
    transaction.addChild(message);
    // length在上下文中保存直到发送后清理,因此一个root,length = root下子节点总数
    length++;
}

// Transaction中子消息的集合添加操作com.dianping.cat.message.internal.DefaultTransaction#addChild
public DefaultTransaction addChild(Message message) {
    if (children == null) {
        children = new ArrayList<Message>();
    }
    children.add(message);
    return this;
}

validator使用TransactionHelper实现,truncateAndFlush方法的目的是
将现有的消息复制并发送,同时记录消息ID等,通过消息ID串联两个Transaction。将过大、或者过期的Transaction消息按长度、时间分割为两个Transaction。
其逻辑如下(删除了ForkedTransaction相关逻辑)

  • 获取当前的MessageTree,并读取/生成相应的MessageId,ParentId,RootId,用于链接消息树。
  • 将当消息Transaction拷贝到一个临时target中,并且递归source和其childMessage,将信息转移到target对象上
  • 创建一个DefaultEvent,记录串联事件
  • 拷贝当前MessageTree,将traget设置到其中的Message,将新tree的Id设置为childId,将新tree的parentId设置为当前tree的Id。完成串联
public void truncateAndFlush(Context ctx, long timestamp) {
    // 获取当前的Message和stack,Message为父
    MessageTree tree = ctx.tree;
    Stack<Transaction> stack = ctx.stack;
    Message message = tree.getMessage();

    if (message instanceof DefaultTransaction) {
        // 检查ID,rootId,ChildId,为发送后关联消息链做准备
        String id = tree.getMessageId();
        if (id == null) {
            id = nextMessageId();
            tree.setMessageId(id);
        }
        String rootId = tree.getRootMessageId();
        String childId = nextMessageId();
        
        Transaction source = (Transaction) message;
        // 新建一个Transaction消息,拷贝父数据
        DefaultTransaction target = new DefaultTransaction(source.getType(), source.getName(),
                DefaultMessageManager.this);
        target.setTimestamp(source.getTimestamp());
        target.setDurationInMicros(source.getDurationInMicros());
        target.setStatus(Message.SUCCESS);

        // 递归迁移原有的父信息到traget中
        migrateMessage(stack, source, target, 1);

        if (message instanceof DefaultTransaction) {
            // 增加到子logview的下一个链接,向下关联
            // Event报表的RemoteCall.Next可以看到该链路的整合信息
            DefaultEvent next = new DefaultEvent("RemoteCall", "Next");
            next.addData(childId);
            next.setStatus(Message.SUCCESS);
            target.addChild(next);
        } else {
            // 在forked情况下,最后一个messageTree需要和父亲线程上下文保持关联,在堆栈里面加入已经发送的logview的链接,这个地方是向上关联
			// forked先不看
        }

        // 拷贝当前MessageTree到一个临时tree t
        MessageTree t = tree.copy();
		// 设置message相关信息
        t.setMessage(target);
        ctx.tree.setMessageId(childId);
        ctx.tree.setParentMessageId(id);
        ctx.tree.setRootMessageId(rootId != null ? rootId : id);

        if (ctx.knownExceptions != null) {
            ctx.knownExceptions.clear();
        }
        ctx.length = stack.size();
        ctx.totalDurationInMicros = ctx.totalDurationInMicros + target.getDurationInMicros();
		// 将t发送,但不清理当前Context上下文。
        flush(t, false);
    }
}

递归算法解释如下

首先需要说明,更具Context的start方法逻辑已知,stack中只要栈顶有transaction,新建的transaction一定属于其子transaction。

那么栈内的元素父子关系可能如下图,T1是root位于栈底,T1.1.1.1是T1的第3层子节点。上下文tree中的Message始终引用根节点

android 美团源代码 美团源码app_java_02

递归时传入4个参数:

  • stack是transaction保存的堆栈,用于在递归中读取transaction
  • source可以理解为当前待迁移数据的节点
  • traget是存储新的节点数据的引用树
  • level是栈的索引,从1开始,即第一层子节点
private void migrateMessage(Stack<Transaction> stack, Transaction source, Transaction target, int level) {
    Transaction current = level < stack.size() ? stack.get(level) : null;
    boolean shouldKeep = false;
    final List<Message> childs = source.getChildren();

    for (Message child : childs) {
        if (child != current) {
            target.addChild(child);
        } else {
            DefaultTransaction cloned = new DefaultTransaction(current.getType(), current.getName(),
                    DefaultMessageManager.this);
            cloned.setTimestamp(current.getTimestamp());
            cloned.setDurationInMicros(current.getDurationInMicros());
            cloned.setStatus(Message.SUCCESS);

            target.addChild(cloned);
            migrateMessage(stack, current, cloned, level + 1);
            shouldKeep = true;
        }
    }
	// 清理节点,保留干净的父子关系
    source.getChildren().clear();
    if (shouldKeep) { // add it back
        source.addChild(current);
    }
}

通过递归,调整level+1,读取下一个子节点,如果子节点存在且与自身不同,继续递归。直到不符合条件弹出。

target完成了整个子树的链路引用,示意图如下

android 美团源代码 美团源码app_java_03

综上,完成了Transaction的开启过程。

3. Complete结束

先跳过为Transaction树添加消息的方法,研究transaction如何完成/关闭,执行发送。
DefaultTransaction的complete实现如下:

  • 校验completed标记位,避免重复执行
  • 计算Transaction的运行时间,注意是纳秒,除以1000L,支持ms级的精确度(纳秒统计内存级别的时间)
  • 设置标记位
  • 检查是否属于Problem报表?是,则标记该消息不可丢弃,不可丢弃则保证problem不受采样控制
  • 执行end结束方法
public void complete() {
    try {
        if (isCompleted()) {
			// 校验标记位,是否已经标记过
        } else {
            // 计算Transaction的运行时间
            if (durationInMicro == -1) {
                durationInMicro = (System.nanoTime() - durationStart) / 1000L;
            }
            setCompleted(true);
			// 检查problem报表
            if (manager != null && isProblem(this, manager, type, durationInMicro / 1000L)) {
                manager.getThreadLocalMessageTree().setDiscardPrivate(false);
            }
			// 执行结束方法
            if (manager != null) {
                manager.end(this);
            }
        }
    } catch (Exception e) {
        // ignore
    }
}

3.1 检查Problem报表

校验Problem主要用于当transaction是type是long-sql等数据CAT默认检查项的类型,判断是否超限的方法。

  • 取type,从缓存的threshold map中读取阈值
  • 未读到缓存时,从config中读取配置,并进行缓存
  • 超限时,记录每分钟type-name对应的告警次数,小于60次为true
private boolean isProblem(Transaction t, MessageManager manager, String type, long duration) {
    boolean problem;
    // transaction成功状态下记录异常Problem
    if (t.isSuccess()) {
        // map,缓存每个type对应的threshold
        Integer value = map.get(type);
        if (value != null) {
            // 已经读取过配置,直接从内存取
            problem = duration > value;
        } else {
            // 没读到,读config配置
            int threshold = Integer.MAX_VALUE;
            ClientConfigService config = manager.getConfigService();
			// 获取枚举,读取相关配置
            ProblemLongType longType = ProblemLongType.findByMessageType(type);
            if (longType != null) {
                switch (longType) {
                    // 通过配置文件读取配置
                    case LONG_CACHE:
                        threshold = config.getLongConfigThreshold(ProblemLongType.LONG_CACHE.getName());
                        break;
                    case LONG_CALL:
                        threshold = config.getLongConfigThreshold(ProblemLongType.LONG_CALL.getName());
                        break;
                    case LONG_SERVICE:
                        threshold = config.getLongConfigThreshold(ProblemLongType.LONG_SERVICE.getName());
                        break;
                    case LONG_SQL:
                        threshold = config.getLongConfigThreshold(ProblemLongType.LONG_SQL.getName());
                        break;
                    case LONG_URL:
                        threshold = config.getLongConfigThreshold(ProblemLongType.LONG_URL.getName());
                        break;
                    case LONG_MQ:
                        threshold = config.getLongConfigThreshold(ProblemLongType.LONG_MQ.getName());
                        break;
                }
            }
			// 缓存配置
            map.put(type, threshold);
			// 满足Threshold,记录problem数量,最大60条,超过时丢弃
            problem = duration > threshold && recordProblem(t.getTimestamp(), t.getType() + t.getName());
        }
    } else {
        problem = recordProblem(t.getTimestamp(), t.getType() + t.getName());
    }

    return problem;
}

// 记录type和name对应的problem数量
private boolean recordProblem(long time, String id) {
    try {
        long minute = time / 1000 / 60;
        ConcurrentHashMap<String, AtomicInteger> count = DefaultTransaction.count.get(minute);
        if (count == null) {
            count = new ConcurrentHashMap<String, AtomicInteger>();
            ConcurrentHashMap<String, AtomicInteger> oldCount = DefaultTransaction.count.putIfAbsent(minute, count);
            if (oldCount != null) {
                count = oldCount;
            }
        }
        AtomicInteger value = count.get(id);
        if (value == null) {
            value = new AtomicInteger(0);

            AtomicInteger oldValue = count.putIfAbsent(id, value);

            if (oldValue != null) {
                value = oldValue;
            }
        }
        return value.incrementAndGet() <= 60;
    } catch (Exception e) {
        return false;
    }
}

枚举中定义了会被Problem报表采集的type
我们可以自己埋点时设置type对应枚举,实现统计problem

public enum ProblemLongType {
    LONG_CACHE("long-cache", 25) {
        @Override
        protected boolean checkLongType(String type) {
            return type.startsWith("Squirrel.") || type.startsWith("Cellar.") || type.startsWith("Cache.");
        }
    },
    LONG_CALL("long-call", 100) {
        @Override
        protected boolean checkLongType(String type) {
            return "PigeonCall".equals(type) || "OctoCall".equals(type) || "Call".equals(type);
        }
    },
    LONG_SERVICE("long-service", 100) {
        @Override
        protected boolean checkLongType(String type) {
            return "PigeonService".equals(type) || "OctoService".equals(type) || "Service".equals(type);
        }
    },
    LONG_SQL("long-sql", 100) {
        @Override
        protected boolean checkLongType(String type) {
            return "SQL".equals(type);
        }
    },
    LONG_URL("long-url", 1000) {
        @Override
        protected boolean checkLongType(String type) {
            return "URL".equals(type);
        }
    },
    LONG_MQ("long-mq", 100) {
        @Override
        protected boolean checkLongType(String type) {
            return "MtmqRecvMessage".equals(type) || "MafkaRecvMessage".equals(type);
        }
    }
    // 其他代码
}

3.2 end方法

complete方法最后调用com.dianping.cat.message.internal.DefaultMessageManager#end,结束指定transaction消息。它通过获取当先线程上下文ctx实现end

public void end(Transaction transaction) {
    Context ctx = getContext();
    if (ctx != null) {
        ctx.end(this, transaction);
    }
}

com.dianping.cat.message.internal.DefaultMessageManager.Context#end方法的实现逻辑如下:

  • 校验transaction的stack是否有数据需要发送
  • 判断元素是否是栈顶的transaction,若不是则循环判断弹出,直到找到相等的transaction
  • 判断Stack已经完全清空,说明消息树中的根Transaction已经弹出,可以将整个tree发送。保证只有当根transaction可以结束时,才发送消息
  • 拷贝当前tree,并清空,将tree发送
public boolean end(DefaultMessageManager manager, Transaction transaction) {
    if (!stack.isEmpty()) {
        Transaction current = stack.pop();
		// 判断栈顶元素是否是要结束的transaction
        if (transaction == current) {
        } else {
            // 将不相同的transaction全部弹出,直到相等或空栈
            while (transaction != current && !stack.empty()) {
                current = stack.pop();
            }
        }
        
		// 当栈为空时继续
        if (stack.isEmpty()) {
            // 先不考虑forktransaction
            final List<ForkableTransaction> forkableTransactions = tree.getForkableTransactions();
            if (forkableTransactions != null && !forkableTransactions.isEmpty()) {
                for (ForkableTransaction forkableTransaction : forkableTransactions) {
                    forkableTransaction.complete();
                }
            }
			// 拷贝树,将上下文清空
            MessageTree tree = this.tree.copy();
            this.tree.setMessageId(null);
            this.tree.setMessage(null);
			// totalDurationInMicros大于0,说明是transant的消息,不能采样处理
            if (totalDurationInMicros > 0) {
                tree.setDiscardPrivate(false);
            }
			// 发送消息
            manager.flush(tree, true);
            return true;
        }
    }

    return false;
}

com.dianping.cat.message.internal.DefaultMessageManager#flush的发送逻辑相对简单:

  • 调用TCP客户端发送消息
  • 重置上下文数据
public void flush(MessageTree tree, boolean clearContext) {
    if (sender != null) {
        // TCPSender发送
        sender.send(tree);
        if (clearContext) {
            // 重置上下文
            reset();
        }
    } else {
        throttleTimes++;
        if (throttleTimes % 10000 == 0 || throttleTimes == 1) {
            LOGGER.info("Cat Message is throttled! Times:" + throttleTimes);
        }
    }
}

4. logEvent添加Event事件

LogEvent方法可以单独发送一个Event事件,或者向一个父Transaction插入一个Event事件。Cat的接口如下,主要逻辑与创建Transaction相近,最终通过Producer创建Event

/**
 * Log an event in one shot.
 *
 * @param type           event type
 * @param name           event name
 * @param status         "0" means success, otherwise means error code
 * @param nameValuePairs name value pairs in the format of "a=1&b=2&..."
 */
public static void logEvent(String type, String name, String status, String nameValuePairs) {
    if (isEnabled()) {
        try {
            Cat.getProducer().logEvent(type, name, status, nameValuePairs);
        } catch (Exception e) {
            errorHandler(e);
        }
    }
}

com.dianping.cat.message.internal.DefaultMessageProducer#logEvent方法的实现相对很简单。
实例化event对象,记录type、name,data即完成了创建。由于Event是孤立的事件,没有相关关系,可以直接complete,标记完成执行发送

@Override
public void logEvent(String type, String name, String status, String nameValuePairs) {
    // 实例化Event,设置数据
    Event event = newEvent(type, name);
    if (nameValuePairs != null && nameValuePairs.length() > 0) {
        event.addData(nameValuePairs);
    }
    event.setStatus(status);
    // 标记完成,准备发送
    event.complete();
}

com.dianping.cat.message.internal.DefaultEvent#complete方法中,设置标记位,通过DefaultMessageManager中在当前线程上下文中添加当前Event消息

public void complete() {
    setCompleted(true);
	// 添加消息
    if (manager != null) {
        manager.add(this);
    }
}

// com.dianping.cat.message.internal.DefaultMessageManager#add
public void add(Message message) {
    Context ctx = getContext();
    if (ctx != null) {
        ctx.add(message);
    }
}

com.dianping.cat.message.internal.DefaultMessageManager.Context#add方法中:

  • 通过stack判断是否有父Transaction,如果没有transaction,复制tree通过flush发送,与transaction发送类似
  • 如果有父Transaction,则给该Transaciton添加一个child,但是不向stack中保存
public void add(Message message) {
    if (stack.isEmpty()) {
        MessageTree tree = this.tree.copy();

        tree.setMessage(message);
        flush(tree, true);
    } else {
        Transaction parent = stack.peek();

        addTransactionChild(message, parent);
    }
}        public void add(Message message) {
    if (stack.isEmpty()) {
        MessageTree tree = this.tree.copy();

        tree.setMessage(message);
        flush(tree, true);
    } else {
        Transaction parent = stack.peek();

        addTransactionChild(message, parent);
    }
}

5. logMetric添加业务埋点

5.1 logMetricForCount

Cat接口中logMetricForCount方法如下,与其他接口类似,先检查初始化,然后获取实例添加测量埋点。

public static void logMetricForCount(String name, int quantity, Map<String, String> tags) {
    if (isEnabled()) {
        // 初始化
        checkAndInitialize();
        try {
            // 获取Metric聚合实例,添加测量点
            MetricTagAggregator.getInstance().addCountMetric(name, quantity, tags);
        } catch (Exception e) {
            // 记录异常
            errorHandler(e);
        }
    }
}

com.dianping.cat.analyzer.MetricTagAggregator#addCountMetric()方法的实现如下

  • 校验并获取MetricTagItem:MetricTagItem是针对业务指标的计数类,原始计数存储在其内部
  • metrics是Aggregator中的一个线程安全Map,用来保存用户指定的name与计数tag直接的关系
  • 对MetricTagItem执行Count加一操作
public void addCountMetric(String name, int quantity, Map<String, String> tags) {
    MetricTagItem metricTagItem = makeSureMetricExist(name, tags, metrics);
    addCountMetric(quantity, metricTagItem);
}

private void addCountMetric(int quantity, MetricTagItem metricTagItem) {
    // 添加方法很简单,原子类直接自增
    metricTagItem.count.addAndGet(quantity);
}

private MetricTagItem makeSureMetricExist(String key, Map<String, String> tags, Map<String, Map<String, MetricTagItem>> metrics) {
    // 从<name,<tagkey, Item>>的Map中,取<tagkey, Item>
    Map<String, MetricTagItem> items = metrics.get(key);
    // itemsmap不存在,双重检查并创建,但是个人认为这个双重检查有点多余
    if (null == items) {
        synchronized (this) {
            items = metrics.get(key);
            if (null == items) {
                items = new ConcurrentHashMap<String, MetricTagItem>();
                metrics.put(key, items);
            }
        }
    }

    // 根据插入的tagsMap拼装key,格式如name=zhangsan&age=20,空EMPTY
    String tagKey = buildTagKey(tags);
    // 获取计数类
    MetricTagItem item = items.get(tagKey);

    if (null == item) {
        // 一个name下的metric最多不超过1000
        if (items.size() >= MAX_KEY_SIZE) {
            Cat.logEvent("cat.TooManyTagValuesForMetric", key);
            tagKey = OTHERS;
        }
		// 双重检查,创建item类
        item = items.get(tagKey);
        if (null == item) {
            synchronized (this) {
                item = items.get(tagKey);
                if (null == item) {
                    item = createMetricItem(tagKey);
                    items.put(tagKey, item);
                }
            }
        }
    }

    return item;
}

private MetricTagItem createMetricItem(String key) {
    // 创建一个item
    MetricTagItem item = new MetricTagItem();
    item.setKey(key);
    // metric的门限是用户自定义设置的
    // com.dianping.cat.analyzer.MetricTagAggregator#setMetricSlowThreshold
    Integer threshold = metricThresholds.get(key);
    if (threshold != null) {
        item.setSlowThreshold(threshold);
    }
    return item;
}

5.2 logMerticForDuration

该方法用于记录时段内的平均值,与5.1的主要区别在于计数方式不同

public void addTimerMetric(String name, long durationInMillis, Map<String, String> tags) {
    MetricTagItem item = makeSureMetricExist(name, tags, metrics);

    addTimerMetric(durationInMillis, item);
}
private void addTimerMetric(long durationInMillis, MetricTagItem item) {
    // 记录数量和时间
    item.count.incrementAndGet();
    item.sum.addAndGet(durationInMillis);

    if (item.slowThreshold > 0 && durationInMillis > item.slowThreshold) {
        item.slowCount.incrementAndGet();
    }
}

注: Metric以及下面的Error发送并不是通过当前线程上下文的消息树发送。而是通过统一的线程采集记录在Aggregator中的数据,拼装发送。下一节为详细介绍

6. logError记录异常

Cat中的logError接口不再赘述,与其他Message接口类似

public static void logError(String message, Throwable cause) {
    if (isEnabled()) {
        try {
            Cat.getProducer().logError(message, cause);
        } catch (Exception e) {
            errorHandler(e);
        }
    }
}

直接进入producer逻辑:

  • 判断同一个异常是否已经存在与ctx上下文中,上下文会保存一个异常的集合,记录产生的异常。该异常集合在ctx reset时完成清空重置。因为buildStackInfo需要IO操作,所以要避免重复,减少IO。
  • 根据异常构建detailMessage的内容
  • 通过logEvent记录异常事件
public void logError(String message, Throwable cause) {
    if (notExsitCause(cause)) {
        String detailMessage = buildStackInfo(message, cause);
        final String name = cause.getClass().getName();

        if (cause instanceof Error) {
            logEvent("Error", name, ERROR, detailMessage);
        } else if (cause instanceof RuntimeException) {
            logEvent("RuntimeException", name, ERROR, detailMessage);
        } else {
            logEvent("Exception", name, ERROR, detailMessage);
        }
    }
}

buildStackInfo方法中,通过流写入数据。并记录异常的次数,以分钟为单位,最多记录80条。超过80条,则无法记录。
这个缓存记录会通过StatusTask管理

private String buildStackInfo(String message, Throwable cause) {
    StringWriter writer = new StringWriter(2048);
    if (message != null) {
        // 把message写到writer中,message是异常描述
        writer.write(message);
        writer.write(' ');
    }

    if (recordStackTrace(cause.getClass().getName())) {
        // when build stack, cat will report the message tree.
        Cat.getManager().getThreadLocalMessageTree().setDiscardPrivate(false);
        cause.printStackTrace(new PrintWriter(writer));
    } else {
        writer.write("The exception with same name will print stack eighty times in one minute, discard exception stack to avoid abnormal performance bottleneck");
    }
    return writer.toString();
}

// 这个逻辑类似complete标记transaction时的recordProblem逻辑
private boolean recordStackTrace(String exception) {
    try {
        long minute = System.currentTimeMillis() / 1000 / 60;
        ConcurrentHashMap<String, AtomicInteger> stack = DefaultMessageProducer.stack.get(minute);
        if (stack == null) {
            stack = new ConcurrentHashMap<String, AtomicInteger>();
            ConcurrentHashMap<String, AtomicInteger> oldStack = DefaultMessageProducer.stack.putIfAbsent(minute, stack);
            if (oldStack != null) {
                stack = oldStack;
            }
        }
        AtomicInteger value = stack.get(exception);
        if (value == null) {
            value = new AtomicInteger(0);
            AtomicInteger oldValue = stack.putIfAbsent(exception, value);
            if (oldValue != null) {
                value = oldValue;
            }
        }
        // 大于当前分钟80不记录
        return value.incrementAndGet() <= 80;
    } catch (Exception e) {
        return true;
    }
}

7. errorHandler

errorHandler方法在Cat客户端中用于记录客户端自身的报错。方法中会判断报错次数,限制次数小于3时记录。
且,errorCount是Cat中的静态变量,没有重置方法,因此一个客户端最多记录3次自身异常。

private static void errorHandler(Exception e) {
    if (isEnabled() && errorCount < 3) {
        // errorCount是
        errorCount++;
        CatLogger.getInstance().error(e.getMessage(), e);
    }
}

CatLogger的error方法,最终会调用out方法。
该方法会落盘,修改/data/applogs/cat 目录下的cat_client_{0,date,yyyyMMdd}.log 文件。
因此,考虑要控制次数,并判断是否是开发模式

private void out(String severity, String message, Throwable throwable) {
    lock.lock();
    try {
        String timedMessage = formatMessage(severity, message);
        if (devMode) {
            console(timedMessage);
            if (throwable != null) {
                throwable.printStackTrace(System.out);
            }
        } else {
            try {
                BufferedWriter writer = getWriter();
                if (writer != null) {
                    writer.write(timedMessage);
                    writer.newLine();

                    if (throwable != null) {
                        throwable.printStackTrace(new PrintWriter(writer));
                    }
                    writer.flush();
                }
            } catch (Exception e) {
                console(formatMessage("ERROR", timedMessage + e.toString()));
            }
        }
    } finally {
        lock.unlock();
    }
}

8. 远程调用串联

目前,互联网服务大多是分布式的,因此跨服务的远程调用,Cat也提供了响应的监控方式。对于分布式追踪的原理,可以参考Google Dapper论文,此处不再赘述。
只需要了解,Cat的MessageTree支持设置

  • RootMessageId 消息入口服务产生的Id,整个链路透传,可以标记一整个链路
  • ParentMessageId 上游服务/调用者产生的Id
  • MessageId 下游服务/服务提供者产生的Id,当前消息Id

通过三种MessageId即可串联整个链路
例如,
一个调用方(client)通过Rest方式调用一个服务提供者(Server)的方法式,可能需要设置一个拦截器,记录相关Id,进行埋点

/**
 * 调用方伪代码
 */
// 创建Transaction,准备记录调用时间
Transaction t = Cat.newTransaction(CatConstants.TYPE_CALL, request.getURI().toString());
try {
	HttpHeaders headers = request.getHeaders();

	// 通过HttpHeader,保存和传递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 {
    // 标记Transaction完成
	t.complete();
}

服务端对消息的记录伪代码如下:

// 从header中读数据
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));
// 记录server端信息
Cat.logRemoteCallServer(catContext);

Transaction t = Cat.newTransaction(CatConstants.TYPE_SERVICE, url);
try {
    // 执行servletFilter,完成调用链
    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();
}

8.1 logRemoteClient

com.dianping.cat.Cat#logRemoteCallClient()用于在客户端侧记录埋点数据,主要逻辑为

  • 从线程上下文读取对应的MessageTree
  • 获取/创建rootId,MessageId
  • 创建RpcServerId时,要传入服务端的domain,不传默认default。但是,若同一ip的client机器上存在多个应用,都调用了一个远程调用服务。那么此时id可能重复
  • 设置Cat.Context上下文,保存Id
public static void logRemoteCallClient(Context ctx, String domain) {
    if (isEnabled()) {
        try {
            MessageTree tree = Cat.getManager().getThreadLocalMessageTree();
            String messageId = tree.getMessageId();
            if (messageId == null) {
                messageId = Cat.getProducer().createMessageId();
                tree.setMessageId(messageId);
            }
            // 根据server的domain创建MessageId,MessageId的创建之前介绍过了,默认default
            String childId = Cat.getProducer().createRpcServerId(domain);
            Cat.logEvent(CatConstants.TYPE_REMOTE_CALL, "", Event.SUCCESS, childId);
            String root = tree.getRootMessageId();
            if (root == null) {
                root = messageId;
            }
            ctx.addProperty(Context.ROOT, root);
            ctx.addProperty(Context.PARENT, messageId);
            ctx.addProperty(Context.CHILD, childId);
        } catch (Exception e) {
            errorHandler(e);
        }
    }
}

8.2 Cat.Context

Cat中包含一个内部接口Context,用来跨进程时保存MessageId,为tree串联远程调用的消息。
Context接口的两个方法可以自定义实现来决定如何保存三种Id。,例如最简单的用Map来存储

public interface Context {
    String ROOT = "_catRootMessageId";
    String PARENT = "_catParentMessageId";
    String CHILD = "_catChildMessageId";
    String DISCARD = "_catDiscard";

    void addProperty(String key, String value);
    String getProperty(String key);
}

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);
    }
}

8.3 logRemoteServer

在服务端记录埋点信息,将ctx中的三种Id拿出并设置到MessageTree中

public static void logRemoteCallServer(Context ctx) {
    if (isEnabled()) {
        try {
            MessageTree tree = Cat.getManager().getThreadLocalMessageTree();
            String childId = ctx.getProperty(Context.CHILD);
            String rootId = ctx.getProperty(Context.ROOT);
            String parentId = ctx.getProperty(Context.PARENT);

            if (parentId != null) {
                tree.setParentMessageId(parentId);
            }
            if (rootId != null) {
                tree.setRootMessageId(rootId);
            }
            if (childId != null) {
                tree.setMessageId(childId);
            }
        } catch (Exception e) {
            errorHandler(e);
        }
    }
}

以上是CAT客户端API的源码介绍,下一篇将介绍客户端与服务端的通信、消息上报逻辑