节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
6.11 KiB
Stardust
"""W3C Trace Context(traceparent)工具

跨服务、跨语言链路统一使用 W3C Trace Context 标准头:

    traceparent: 00-<trace-id>-<span-id>-<flags>

- trace-id:32 位小写十六进制;为兼容 .NET 历史实现,解析时允许更短的非空十六进制串
- span-id:16 位小写十六进制
- flags:01 表示采样。SDK 不做头部采样,出站统一写 01

本模块只负责解析与格式化,不依赖 SDK 的其他模块。
"""

import re
import uuid
from typing import Any, Mapping, NamedTuple, Optional

TRACE_FLAG_SAMPLED = 0x01

_TRACE_ID_PATTERN = re.compile(r"^[0-9a-f]{1,32}$")
_SPAN_ID_PATTERN = re.compile(r"^[0-9a-f]{16}$")
_VERSION_PATTERN = re.compile(r"^[0-9a-f]{2}$")
_FLAGS_PATTERN = re.compile(r"^[0-9a-f]{2}$")

_ZERO_TRACE_ID = "0" * 32
_ZERO_SPAN_ID = "0" * 16


class TraceParent(NamedTuple):
    """解析后的 traceparent 信息"""

    trace_id: str
    span_id: str
    flags: int = TRACE_FLAG_SAMPLED
    version: str = "00"

    def __str__(self) -> str:
        return format_traceparent(self.trace_id, self.span_id, self.flags, self.version)


def new_trace_id() -> str:
    """生成 32 位小写十六进制 TraceId"""
    while True:
        value = uuid.uuid4().hex
        if value != _ZERO_TRACE_ID:
            return value


def new_span_id() -> str:
    """生成 16 位小写十六进制 SpanId"""
    while True:
        value = uuid.uuid4().hex[:16]
        if value != _ZERO_SPAN_ID:
            return value


def _normalize(value: Any) -> Optional[str]:
    """把 bytes/str 统一为去除首尾空白的字符串"""
    if value is None:
        return None
    if isinstance(value, bytes):
        try:
            value = value.decode("ascii")
        except Exception:
            return None
    if not isinstance(value, str):
        return None
    return value.strip()


def is_valid_trace_id(value: Any) -> bool:
    """判断是否为可接受的 TraceId(32 位以内非空十六进制)"""
    text = _normalize(value)
    if not text:
        return False
    text = text.lower()
    return bool(_TRACE_ID_PATTERN.match(text)) and text.strip("0") != ""


def is_valid_span_id(value: Any) -> bool:
    """判断是否为可接受的 SpanId(16 位非零十六进制)"""
    text = _normalize(value)
    if not text:
        return False
    text = text.lower()
    return bool(_SPAN_ID_PATTERN.match(text)) and text.strip("0") != ""


def parse_traceparent(value: Any) -> Optional[TraceParent]:
    """解析 traceparent 头

    Args:
        value: traceparent 头字符串或字节串

    Returns:
        解析成功时返回 TraceParent,缺失或非法时返回 None
    """
    text = _normalize(value)
    if not text:
        return None

    parts = text.split("-")
    if len(parts) < 4:
        return None

    version, trace_id, span_id, flags = parts[:4]
    version = version.lower()
    if version == "00" and len(parts) != 4:
        return None
    trace_id = trace_id.lower()
    span_id = span_id.lower()
    flags = flags.lower()

    if not _VERSION_PATTERN.match(version) or version == "ff":
        return None
    if not _TRACE_ID_PATTERN.match(trace_id) or trace_id.strip("0") == "":
        return None
    if not _SPAN_ID_PATTERN.match(span_id) or span_id.strip("0") == "":
        return None
    if not _FLAGS_PATTERN.match(flags):
        return None

    return TraceParent(trace_id, span_id, int(flags, 16), version)


def format_traceparent(
    trace_id: Any,
    span_id: Any,
    flags: Any = TRACE_FLAG_SAMPLED,
    version: str = "00",
) -> str:
    """格式化 traceparent 头

    较短的 TraceId 会原样保留,以兼容 .NET 历史实现并保持链路关联;SpanId 必须为 16 位。

    Args:
        trace_id: TraceId,非空十六进制串
        span_id: SpanId,非空十六进制串
        flags: 采样标志,int(如 1)或两位十六进制字符串
        version: 版本号,默认 00

    Returns:
        标准的 traceparent 字符串

    Raises:
        ValueError: 参数不是合法的十六进制串
    """
    text_trace = _normalize(trace_id)
    text_span = _normalize(span_id)
    if not text_trace or not is_valid_trace_id(text_trace):
        raise ValueError("invalid trace id: %r" % (trace_id,))
    if not text_span or not is_valid_span_id(text_span):
        raise ValueError("invalid span id: %r" % (span_id,))

    if isinstance(flags, str):
        text_flags = flags.strip().lower() or "01"
        flag_value = int(text_flags, 16)
    else:
        flag_value = int(flags) & 0xFF

    text_version = _normalize(version) or "00"
    if not _VERSION_PATTERN.match(text_version.lower()):
        raise ValueError("invalid version: %r" % (version,))

    return "%s-%s-%s-%02x" % (
        text_version.lower(),
        text_trace.lower(),
        text_span.lower(),
        flag_value,
    )


def find_traceparent(headers: Any) -> Optional[TraceParent]:
    """从请求头中查找并解析 traceparent

    支持三种输入:
    - 大小写不敏感的映射(dict 等),同时识别 HTTP_TRACEPARENT(WSGI/Django)
    - 二元组序列(ASGI [(b"traceparent", b"00-...")])
    - None / 空

    Args:
        headers: 请求头集合

    Returns:
        解析成功时返回 TraceParent,否则返回 None
    """
    if not headers:
        return None

    try:
        if isinstance(headers, Mapping):
            for key in headers:
                if not isinstance(key, str):
                    continue
                name = key.lower()
                if name in ("traceparent", "http_traceparent"):
                    parsed = parse_traceparent(headers[key])
                    if parsed is not None:
                        return parsed
            return None

        for item in headers:  # type: ignore[union-attr]
            if not item:
                continue
            key, value = item[0], item[1]
            name = _normalize(key)
            if name is not None and name.lower() in ("traceparent", "http_traceparent"):
                parsed = parse_traceparent(value)
                if parsed is not None:
                    return parsed
    except Exception:
        return None

    return None