说一说分布式系统的链路追踪(下)

2,218 阅读18分钟

1.说明

opentracing-go软件包是OpenTracing的golang平台的API

jaeger-client-go软件包是为Jaeger实现OpenTracing Go Tracer的工具库

特别注意: 这个库的导入路径是基于github.com/uber.不要尝试导入使用 github.com/jaegertracing,这将不可编译。官方在下个版本将会重新考虑这个问题

正确导入方式:

import "github.com/uber/jaeger-client-go"

错误导入方式:

import "github.com/jaegertracing/jaeger-client-go"

本文为什么提到这两个包,因为一个是golang平台的OpenTracing API,一个是实现了OpenTracing的,结合两者学习,更能明白如何在项目中使用他们。

2.Tracer

opentracing-go中的Tracer是一个简单的接口类型,用于Span的创建和SpanContext的传播。

type Tracer interface {
	StartSpan(operationName string, opts ...StartSpanOption) Span
	Inject(sm SpanContext, format interface{}, carrier interface{}) error
	Extract(format interface{}, carrier interface{}) (SpanContext, error)
}

本部分,特别特别介绍一下StartSpanStartSpan方法使用给定的operationName和提供的一系列StartSpanOption选项来创建,开启并返回一个新的Span。

type StartSpanOptions struct {
	// 零个或者多个对其他Span的因果引用(通过他们的SpanContext)
	// 如果为空,开启一个根`Span`(例如:启动一个新的trace)
	References []SpanReference

	// 该字段会覆盖Span的开始时间,如果StartTime.IsZero(),会设置为当前时间
	StartTime time.Time

	// 标签可能有零个或多个条目; 该map值的限制与Span.SetTag()相同. 可以为nil.
	// 如果指定了,则调用方在StartSpan()调用时移交标签的所有权。
	Tags map[string]interface{}
}

type StartSpanOption interface {
	Apply(*StartSpanOptions)
}

StartSpanOption是一个接口类型,为什么使用接口类型,因为任何只要实现了Apply(*StartSpanOptions)方法的都可以作为StartSpan方法的参数。这也是opentracing-go中普遍意义上的模式,就像是一个通用意义上的标准,任何实现了这个标准的人,都可以在这里使用。这种模式再后续中还会经常见到。

重点就是这个StartSpanOptionsStartSpan方法提供的所有的StartSpanOption参数都将Apply到这个结构体上。其实也没有什么特别的地方,主要涵盖三个方面内容的东西,一个是Span的开始时间,一个是Span的对其他Span的引用关系列表,一个是多个标签的映射关系。

StartSpanOptions结构体允许Tracer.StartSpan()调用者和实现者采用一种机制来覆盖开始时间戳,指定Span References以及在Span开始时间提供单个或多个Tag。

StartSpan()调用者应该查找此包中可用的StartSpanOption接口和实现者。 第一个StartSpanOption接口实现者:SpanReference,该实现者用于设置StartSpanOptions结构体中的References

type SpanReferenceType int

const (
	ChildOfRef SpanReferenceType = iota
	FollowsFromRef
)

type SpanReference struct {
	Type              SpanReferenceType
	ReferencedContext SpanContext
}
func (r SpanReference) Apply(o *StartSpanOptions) {
	if r.ReferencedContext != nil {
		o.References = append(o.References, r)
	}
}
func ChildOf(sc SpanContext) SpanReference {
	return SpanReference{
		Type:              ChildOfRef,
		ReferencedContext: sc,
	}
}
func FollowsFrom(sc SpanContext) SpanReference {
	return SpanReference{
		Type:              FollowsFromRef,
		ReferencedContext: sc,
	}
}

从上面的方法可以看到,如果SpanReference在创建的时候提供的ReferencedContext为nil也没关系,不会有什么影响。 例如,在下面的代码中

sc, _ := tracer.Extract(someFormat, someCarrier)
span := tracer.StartSpan("operation", opentracing.ChildOf(sc))

如果提供的sc== nil opentracing.ChildOf方法也不会panic

第二个StartSpanOption接口实现者:StartTime,该实现者用于设置StartSpanOptions结构体中的StartTime

type StartTime time.Time

// Apply satisfies the StartSpanOption interface.
func (t StartTime) Apply(o *StartSpanOptions) {
	o.StartTime = time.Time(t)
}

StartTime用于显式的设置新Span的开始时间。

第三个StartSpanOption接口实现者:Tags,该实现者用于设置StartSpanOptions结构体中的Tags

type Tags map[string]interface{}

// Apply satisfies the StartSpanOption interface.
func (t Tags) Apply(o *StartSpanOptions) {
	if o.Tags == nil {
		o.Tags = make(map[string]interface{})
	}
	for k, v := range t {
		o.Tags[k] = v
	}
}

第四个StartSpanOption接口实现者:Tag,也用于设置StartSpanOptions结构体中的Tags

type Tag struct {
	Key   string
	Value interface{}
}

// Apply satisfies the StartSpanOption interface.
func (t Tag) Apply(o *StartSpanOptions) {
	if o.Tags == nil {
		o.Tags = make(map[string]interface{})
	}
	o.Tags[t.Key] = t.Value
}

// Set applies the tag to an existing Span.
func (t Tag) Set(s Span) {
	s.SetTag(t.Key, t.Value)
}

Tag不仅可以作为StartSpanOption用于添加一个tag到新的Span,也可以使用Set方法在一个已经存在的Span上设置tag

Tracer实现者可以将StartSpanOption接口实例的切片转换为一个StartSpanOptions结构体像下面一样

func StartSpan(opName string, opts ...opentracing.StartSpanOption) {
	sso := opentracing.StartSpanOptions{}
	for _, o := range opts {
		o.Apply(&sso)
	}
	...
}

如果Span没有提供一个SpanReference选项就将变成自己trace跟踪的根。例如:

var tracer opentracing.Tracer = ...
// The root-span case:
sp := tracer.StartSpan("GetFeed")
// The vanilla child span case:
sp := tracer.StartSpan(
	"GetFeed",
	opentracing.ChildOf(parentSpan.Context()))

// All the bells and whistles:
sp := tracer.StartSpan(
	"GetFeed",
	opentracing.ChildOf(parentSpan.Context()),
	opentracing.Tag{"user_agent", loggedReq.UserAgent},
	opentracing.StartTime(loggedReq.Timestamp),
	)

有关StartSpan的内容,你可能到现在为止还是一知半解的。没关系,我们来学学jaeger-client-go,看看它是如何实现我们的Tracer接口的中的StartSpan方法的,并如何记录StartTime等这些启动时的信息的。

func (t *Tracer) StartSpan(
	operationName string,
	options ...opentracing.StartSpanOption,
) opentracing.Span {
	sso := opentracing.StartSpanOptions{}
	for _, o := range options {
		o.Apply(&sso)
	}
	return t.startSpanWithOptions(operationName, sso)
}

看看嘛,整个代码思路都是上面提及的,所有的options都应用到sso上。然后调用接下来的startSpanWithOptions方法,该方法返回opentracing.Span,至于这块内容,我们后面看完SpanContextSpan之后再看,会更加有感觉。

Inject/Extract方法先大致了解一些,后续再详细了解,因为刚开始,我们的关注点在Tracer是如何开启一个Span的,而注入和提取的内容在后续的时候才会用到。

Inject() 方法接受一个sm SpanContext类型的实例,然后将它注入到carrier载体中传播, 载体carrier的实际类型依赖于format的值。 OpenTracing 定义了一组通用的format值(可以查看BuiltinFormat),并且每一个都有一个预期的载体类型。 其他包可以声明他们自己的format值,就像在golang context.Context中使用的key

func WithValue(parent Context, key, val interface{}) Context

注意:所有opentracing.Tracer的实现都必须支持内建的format.如果对于实现者format不支持或者未知,那么将返回opentracing.ErrUnsupportedFormat 如果format支持,但注入还是失败了,实现者将返回opentracing.ErrInvalidCarrier或者其他实现者指定的错误。

Extract()方法根据提供的format和carrier载体返回一个SpanContext实例

返回值:

  • 一个成功提取方法的执行返回一个SpanContext实例,和一个nil错误
  • 如果载体carrier中没有SpanContext提取,Extract()方法返回nil和opentracing.ErrSpanContextNotFound
  • 如果format不支持或者无法识别,Extract()方法返回nil或者opentracing.ErrUnsupportedFormat
  • 如果carrier对象还有更多基本问题,Extract()方法将返回opentracing.ErrInvalidCarrier,opentracing.ErrSpanContextCorrupted或者 其他实现者特定的错误。

3.Span和SpanContext

opentracing-go中的SpanSpanContext都是接口类型

type SpanContext interface {
	ForeachBaggageItem(handler func(k, v string) bool)
}

SpanContext表示必须传播到后继Span和跨过程边界的Span状态 (例如, 一个<trace_id, span_id, sampled>元祖).

其所需方法ForeachBaggageItem授权访问存储在SpanContext上的所有的baggage items,其中方法的参数handler是一个函数类型,将应用在每个baggage键值对上。items的顺序不保证,结果返回值的bool类型表示handler是否要继续迭代后续的baggage items。例如如果handler想要通过模式匹配名称找到一些baggage item,一旦发现该item,它可以返回false,停止进一步的迭代。

这个时候,就可以拿出jaeger-client-go来看看他的实现方案

// TraceID 表示跟踪的唯一128位标识符
type TraceID struct {
	High, Low uint64
}

// SpanID 表示一个span的唯一64位标识符
type SpanID uint64

// SpanContext 代表传播的span的身份和状态
type SpanContext struct {
    // traceID 代表trace全局唯一的ID
    // 通常作为一个随机数生产
	traceID TraceID

	// spanID表示在其trace中必须唯一的spanID
	// 但不必是全局唯一的。
	spanID SpanID

	// parentID是指父span的ID。
    // 如果当前span是一个根span,该值需为0
	parentID SpanID

    // 分布式上下文包。 是即时的快照。
	baggage map[string]string

	// 从TextMap载体中提取上下文时,可以将debugID设置为某些相关ID。
	debugID string

	// samplingState 跨所有span分享
	samplingState *samplingState

    // 标识span context代表一个远程的parent
	remote bool
}

func (c SpanContext) ForeachBaggageItem(handler func(k, v string) bool) {
	for k, v := range c.baggage {
		if !handler(k, v) {
			break
		}
	}
}

再来看看Span

type Span interface {
	Finish()
	FinishWithOptions(opts FinishOptions)
	Context() SpanContext
	SetOperationName(operationName string) Span
	SetTag(key string, value interface{}) Span
	LogFields(fields ...log.Field)
	LogKV(alternatingKeyValues ...interface{})
	SetBaggageItem(restrictedKey, value string) Span
	BaggageItem(restrictedKey string) string
	Tracer() Tracer

	// Deprecated: use LogFields or LogKV
	LogEvent(event string)
	// Deprecated: use LogFields or LogKV
	LogEventWithPayload(event string, payload interface{})
	// Deprecated: use LogFields or LogKV
	Log(data LogData)
}

Span在OpenTracing系统中代表一个活跃的,未完成的span。由Tracer接口来创建。 我们来看一下Span中需要实现的方法

第一个为Finish()方法,该方法用于设置Span的结束时间戳,并设置Span的结束状态。除了对Context()的调用(始终允许)之外,Finish()必须是对任何span实例的最后一次调用,否则将导致未定义的行为。

第二个方法为FinishWithOptions(opts FinishOptions)该方法就像Finish()但是可以明确控制时间戳和日志数据。参数一个FinishOptions类型的数据.

// LogRecord是Span log关联的数据。每个LogRecord实例必须指定至少一个Field
type LogRecord struct {
	Timestamp time.Time
	Fields    []log.Field
}

// FinishOptions 允许Span.FinishWithOptions调用者覆盖结束时间戳和提供日志数据
type FinishOptions struct {
	// FinishTime overrides the Span's finish time, or implicitly becomes
	// time.Now() if FinishTime.IsZero().
	// FinishTime覆盖Span的结束时间,如果FinishTime.IsZero()将会把Span结束时间隐式的设置为当前时间。
    // FinishTime必须解析为一个大于等于Span.StartTime的时间戳。
	FinishTime time.Time

	// LogRecords允许调用者使用单个切片指定许多LogFields()调用的内容。 可能为零。
	// LogRecord.Timestamp调用.IsZero()不能返回true,这就意味着必须明确设置其时间戳。
    // 并且,他们的值必须大于等于Span的开始时间,且小于等于Span的结束时间。
    // 否则FinishWithOptions() 的行为是未知的.
	//
	// 如果指定,则调用者在FinishWithOptions()调用时移交LogRecords的所有权。
	//
	// 如果指定,已经弃用的字段必须设置为nil或者为空。
	LogRecords []LogRecord

	// BulkLogData 已经弃用.
	BulkLogData []LogData
}

第三个方法是Context() SpanContext该方法返回的是针对该SpanSpanContext.注意在Span.Finish()方法调用完成后,Context()的返回值依然是有效的。 第四个方法是SetOperationName(operationName string) Span用于设置或者修改操作名称。返回对此Span的引用以进行链接。 第五个方法是SetTag(key string, value interface{}) Span用于将一个标签添加到Span上。如果key已经存在,该操作将覆盖之前的值。value可以是数字类型,字符串,或者布尔类型。其他类型的数据作为value时,在OpenTracing层面上是未知的。如果跟踪系统不知道如何处理一个特别value类型,那么他将忽略掉该Tag但是不会panic,同样的该方法也会返回Span的引用可以进行链式处理。

第六个方法为LogFields(fields ...log.Field)该方法是一个有效的且类型检测的方法来记录Span的key:value日志数据,尽管编程接口比LogKV()更为冗长。下面是一个示例:

span.LogFields(
	log.String("event", "soft error"),
	log.String("type", "cache timeout"),
    log.Int("waited.millis", 1500))

第七个方法为LogKV(alternatingKeyValues ...interface{}),该方法是一种简洁,易读的方式来记录有关Span的key:value记录数据,尽管遗憾的是,这也使LogKV的效率和类型安全性低于LogFields()。示例:

span.LogKV(
	"event", "soft error",
    "type", "cache timeout",
    "waited.millis", 1500)

对于LogKV(与LogFields()相对),参数必须显示为键值对,就像这样span.LogKV(key1, val1, key2, val2, key3, val3, ...)所有key必须是string类型,value可以是字符串,数字类型,布尔类型,Go error实例,或者任意结构体类型。

第八个方法为SetBaggageItem(restrictedKey, value string) Span,用于在Span上设置一个键值对并且其SpanContext也会将其传播到此Span的后代。SetBaggageItem()实现了强大的功能,并提供了全栈opentracing集成(例如,来自移动端app的任意应用程序数据都可以透明地使其完全进入存储系).并带来一些强大的成本:使用此功能 功能要小心。需要特别注意的由两点:1.SetBaggageItem()只会将行baggage items传播到将来的因果关系的关联的Span。2。需要特别谨慎的使用。 因为每个键和值都会复制到关联的Span的每个本地和远程子节点中,这可能会增加大量的网络和cpu开销。

同样,该方法也会返回Span的引用,可用于链式操作

第九个方法为BaggageItem(restrictedKey string) string,该方法用于获取baggage中指定key的值,如果在Span中未找到给定的值将放回空字符串。

第十个方法为Tracer() Tracer,该方法用于访问创建该Span的跟踪器Tracer

后续已经过时的方法就不必关注了。

看看jaeger-client-go中的具体Span实现者

type Span struct {
    // referenceCounter用于增加对象的生命周期记录在将其放入到池中之前
	referenceCounter int32

	sync.RWMutex

	tracer *Tracer

	// TODO: (breaking change) change to use a pointer
	context SpanContext
    
	operationName string

    // 如果为true,标识span是当前进程中span树的根span.
	firstInProcess bool

	// span开始的时间戳,微妙精度。
	startTime time.Time

	// duration以微秒级的精度返回跨度的持续时间。
	// 零值标识持续时间未知
	duration time.Duration

    // 附加到当前span到标签列表
	tags []Tag

	// 当前span到微日记
	logs []opentracing.LogRecord

    // 因为MaxLogsPerSpan删除到log数量
	numDroppedLogs int

	// 当前span的引用列表references
	references []Reference

	observer ContribSpanObserver
}

这个时候再回头看看jaeger-client-go中Tracer在创建Span的StartSpan函数中调用的startSpanWithOptions

func (t *Tracer) startSpanWithOptions(
	operationName string,
	options opentracing.StartSpanOptions,
) opentracing.Span {
	if options.StartTime.IsZero() {
		options.StartTime = t.timeNow()
	}

	// Predicate whether the given span context is an empty reference
	// or may be used as parent / debug ID / baggage items source
	isEmptyReference := func(ctx SpanContext) bool {
		return !ctx.IsValid() && !ctx.isDebugIDContainerOnly() && len(ctx.baggage) == 0
	}

	var references []Reference
	var parent SpanContext
	var hasParent bool // need this because `parent` is a value, not reference
	var ctx SpanContext
	var isSelfRef bool
	for _, ref := range options.References {
		ctxRef, ok := ref.ReferencedContext.(SpanContext)
		if !ok {
			t.logger.Error(fmt.Sprintf(
				"Reference contains invalid type of SpanReference: %s",
				reflect.ValueOf(ref.ReferencedContext)))
			continue
		}
		if isEmptyReference(ctxRef) {
			continue
		}

		if ref.Type == selfRefType {
			isSelfRef = true
			ctx = ctxRef
			continue
		}

		if ctxRef.IsValid() {
			// we don't want empty context that contains only debug-id or baggage
			references = append(references, Reference{Type: ref.Type, Context: ctxRef})
		}

		if !hasParent {
			parent = ctxRef
			hasParent = ref.Type == opentracing.ChildOfRef
		}
	}
	if !hasParent && !isEmptyReference(parent) {
		// If ChildOfRef wasn't found but a FollowFromRef exists, use the context from
		// the FollowFromRef as the parent
		hasParent = true
	}

	rpcServer := false
	if v, ok := options.Tags[ext.SpanKindRPCServer.Key]; ok {
		rpcServer = (v == ext.SpanKindRPCServerEnum || v == string(ext.SpanKindRPCServerEnum))
	}

	var internalTags []Tag
	newTrace := false
	if !isSelfRef {
		if !hasParent || !parent.IsValid() {
			newTrace = true
			ctx.traceID.Low = t.randomID()
			if t.options.gen128Bit {
				ctx.traceID.High = t.options.highTraceIDGenerator()
			}
			ctx.spanID = SpanID(ctx.traceID.Low)
			ctx.parentID = 0
			ctx.samplingState = &samplingState{
				localRootSpan: ctx.spanID,
			}
			if hasParent && parent.isDebugIDContainerOnly() && t.isDebugAllowed(operationName) {
				ctx.samplingState.setDebugAndSampled()
				internalTags = append(internalTags, Tag{key: JaegerDebugHeader, value: parent.debugID})
			}
		} else {
			ctx.traceID = parent.traceID
			if rpcServer && t.options.zipkinSharedRPCSpan {
				// Support Zipkin's one-span-per-RPC model
				ctx.spanID = parent.spanID
				ctx.parentID = parent.parentID
			} else {
				ctx.spanID = SpanID(t.randomID())
				ctx.parentID = parent.spanID
			}
			ctx.samplingState = parent.samplingState
			if parent.remote {
				ctx.samplingState.setFinal()
				ctx.samplingState.localRootSpan = ctx.spanID
			}
		}
		if hasParent {
			// copy baggage items
			if l := len(parent.baggage); l > 0 {
				ctx.baggage = make(map[string]string, len(parent.baggage))
				for k, v := range parent.baggage {
					ctx.baggage[k] = v
				}
			}
		}
	}

	sp := t.newSpan()
	sp.context = ctx
	sp.tracer = t
	sp.operationName = operationName
	sp.startTime = options.StartTime
	sp.duration = 0
	sp.references = references
	sp.firstInProcess = rpcServer || sp.context.parentID == 0

	if !sp.isSamplingFinalized() {
		decision := t.sampler.OnCreateSpan(sp)
		sp.applySamplingDecision(decision, false)
	}
	sp.observer = t.observer.OnStartSpan(sp, operationName, options)

	if tagsTotalLength := len(options.Tags) + len(internalTags); tagsTotalLength > 0 {
		if sp.tags == nil || cap(sp.tags) < tagsTotalLength {
			sp.tags = make([]Tag, 0, tagsTotalLength)
		}
		sp.tags = append(sp.tags, internalTags...)
		for k, v := range options.Tags {
			sp.setTagInternal(k, v, false)
		}
	}
	t.emitNewSpanMetrics(sp, newTrace)
	return sp
}

现在再看这个方法的实现,是不是耳目一新。除了最后sp.observer部分,我不相信到现在,你还有看不懂的地方。

4.传播中的carrier载体格式

我们在上一篇文章中提到所有平台都要求OpenTracing实现支持两种载体格式:一种为text map样式,一种为binary样式,这其实是OpenTracing中默认的格式

// BuiltinFormat用于在opentracing包中划定旨在与Tracer.Inject()和Tracer.Extract()方法一起使用的值。
type BuiltinFormat byte

const (
	// Binary 将SpanContexts表示为不透明的二进制数据。
	//
	// 对于 Tracer.Inject(): the carrier 必须是一个 `io.Writer`.
	//
	// 对于 Tracer.Extract(): the carrier 必须是一个 `io.Reader`.
	Binary BuiltinFormat = iota

	// TextMap将SpanContexts表示为键:值字符串对。
	//
	// 与HTTPHeaders不同,TextMap格式不以任何方式限制键或值字符集。
	//
	// 对于 Tracer.Inject(): the carrier 必须是一个 `TextMapWriter`.
	//
	// 对于 Tracer.Extract(): the carrier 必须是一个 `TextMapReader`.
	TextMap

	// HTTPHeaders将SpanContexts表示为HTTP标头字符串对。
	//
	// 与TextMap不同,HTTPHeaders格式要求键和值按原样作为HTTP标头有效
    //(即,字符大小写可能不稳定,键中不允许使用特殊字符,值应转义URL,等等)
	//
	// 对于 Tracer.Inject(): the carrier 必须是一个 `TextMapWriter`.
	//
	// 对于 Tracer.Extract(): the carrier 必须是一个 `TextMapReader`.
	HTTPHeaders
)

// TextMapWriter 是内建TextMap格式的Inject()载体。 With
// 通过它,调用者可以对SpanContext进行编码,以作为unicode字符串映射中的条目传播。
type TextMapWriter interface {
    // 将一个键值对设置进载体carrier中.对同一个key多次调用Set()方法将导致未定义的行为。
    // 注意:TextMapWriter的后备存储区可能包含与SpanContext不相关的数据。
    // 这样,调用TextMapWriter和TextMapReader接口的Inject()和Extract()实现
    // 必须在前缀或其他约定上达成共识,以区分其自己的key:value对。
	Set(key, val string)
}

// TextMapReader是TextMap内置格式的Extract()载体。 
// 使用它,调用者可以将传播的SpanContext解码为Unicode字符串映射中的条目。
type TextMapReader interface {
	// ForeachKey通过重复调用`handler`函数来返回TextMap内容。
	//  如果任何对`handler`的调用返回非nil错误,则ForeachKey终止并返回该错误。
	// 注意:  TextMapReader的后备存储区可能包含与SpanContext不相关的数据。
    // 因此,调用TextMapWriter和TextMapReader接口的Inject()和Extract()实现
    // 必须在前缀或其他约定上达成共识,以区分自己的key:value对。
	//
	// “ foreach”回调模式在某些情况下减少了不必要的复制,并且还允许实现者在读取映射时持有锁。
	ForeachKey(handler func(key, val string) error) error
}

比如,HTTPHeadersCarrier类型为http.Header.同时实现了TextMapWriterTextMapReader,就可以作为Inject()和Extract()方法的carrier载体。

type HTTPHeadersCarrier http.Header

// Set conforms to the TextMapWriter interface.
func (c HTTPHeadersCarrier) Set(key, val string) {
	h := http.Header(c)
	h.Set(key, val)
}

// ForeachKey conforms to the TextMapReader interface.
func (c HTTPHeadersCarrier) ForeachKey(handler func(key, val string) error) error {
	for k, vals := range c {
		for _, v := range vals {
			if err := handler(k, v); err != nil {
				return err
			}
		}
	}
	return nil
}

客户端示例:

carrier := opentracing.HTTPHeadersCarrier(httpReq.Header)
err := tracer.Inject(
	span.Context(),
	opentracing.HTTPHeaders,
	carrier)

服务端示例:

carrier := opentracing.HTTPHeadersCarrier(httpReq.Header)
clientContext, err := tracer.Extract(opentracing.HTTPHeaders, carrier)
// 假设此处的最终目标是使用服务器端Span跟踪
var serverSpan opentracing.Span
if err == nil {
	span = tracer.StartSpan(
		rpcMethodName, ext.RPCServerOption(clientContext))
} else {
	span = tracer.StartSpan(rpcMethodName)
}

5.NoopTracer

以下是NoopTracer的全部内容,实现了Tracer接口,noopSpan实现了Span接口,noopSpanContext实现了SpanContext接口,只不过这些实现里面,不做什么操作罢了。

type NoopTracer struct{}

type noopSpan struct{}
type noopSpanContext struct{}

var (
	defaultNoopSpanContext SpanContext = noopSpanContext{}
	defaultNoopSpan        Span        = noopSpan{}
	defaultNoopTracer      Tracer      = NoopTracer{}
)

const (
	emptyString = ""
)

// noopSpanContext:
func (n noopSpanContext) ForeachBaggageItem(handler func(k, v string) bool) {}

// noopSpan:
func (n noopSpan) Context() SpanContext                                  { return defaultNoopSpanContext }
func (n noopSpan) SetBaggageItem(key, val string) Span                   { return n }
func (n noopSpan) BaggageItem(key string) string                         { return emptyString }
func (n noopSpan) SetTag(key string, value interface{}) Span             { return n }
func (n noopSpan) LogFields(fields ...log.Field)                         {}
func (n noopSpan) LogKV(keyVals ...interface{})                          {}
func (n noopSpan) Finish()                                               {}
func (n noopSpan) FinishWithOptions(opts FinishOptions)                  {}
func (n noopSpan) SetOperationName(operationName string) Span            { return n }
func (n noopSpan) Tracer() Tracer                                        { return defaultNoopTracer }
func (n noopSpan) LogEvent(event string)                                 {}
func (n noopSpan) LogEventWithPayload(event string, payload interface{}) {}
func (n noopSpan) Log(data LogData)                                      {}

// StartSpan belongs to the Tracer interface.
func (n NoopTracer) StartSpan(operationName string, opts ...StartSpanOption) Span {
	return defaultNoopSpan
}

// Inject belongs to the Tracer interface.
func (n NoopTracer) Inject(sp SpanContext, format interface{}, carrier interface{}) error {
	return nil
}

// Extract belongs to the Tracer interface.
func (n NoopTracer) Extract(format interface{}, carrier interface{}) (SpanContext, error) {
	return nil, ErrSpanContextNotFound
}

6.globalTracer

全局的跟踪器默认就是一个NoopTracer.

type registeredTracer struct {
	tracer       Tracer
	isRegistered bool
}

var (
	globalTracer = registeredTracer{NoopTracer{}, false}
)

func SetGlobalTracer(tracer Tracer) {
	globalTracer = registeredTracer{tracer, true}
}

// GlobalTracer 返回全局单例`Tracer`实现
// 在`SetGlobalTracer()`调用之前,`GlobalTracer()`是一个noop实现,它将丢弃所有传递给它的数据。
func GlobalTracer() Tracer {
	return globalTracer.tracer
}

当然了,可以通过调用SetGlobalTracer进行设置,而不是使用默认的NoopTracer,特别注意SetGlobalTracer设置由GlobalTracer()返回的单例opentracing.Tracer。 那些使用GlobalTracer(而不是直接管理opentracing.Tracer实例)的用户应在main()之前尽早调用SetGlobalTracer,然后再调用下面的StartSpan全局函数。 在调用SetGlobalTracer之前,通过StartSpan等全局变量启动的所有Span都是Noop

所以,我们在实际应用中,在实例化好Tracer之后,都要先SetGlobalTracer,然后再GlobalTracer.

func main() {
	t, io, err := tracer.NewTracer(name, "")
	if err != nil {
		log.Fatal(err)
	}
	defer io.Close()
	opentracing.SetGlobalTracer(t)
    ....
}

7. tag

上一篇文章中,在讲述Span时,提到了常见的span tag,比如 db.instance指明数据库的主机, http.status_code 标识http返回状态码, 或error,如果由跨度表示的操作失败,则可以将其设置为true。 在opentracing-goext目录下,都有定义

var (
	//////////////////////////////////////////////////////////////////////
	// SpanKind (client/server or producer/consumer)
	//////////////////////////////////////////////////////////////////////

	// SpanKind hints at relationship between spans, e.g. client/server
	SpanKind = spanKindTagName("span.kind")

	// SpanKindRPCClient marks a span representing the client-side of an RPC
	// or other remote call
	SpanKindRPCClientEnum = SpanKindEnum("client")
	SpanKindRPCClient     = opentracing.Tag{Key: string(SpanKind), Value: SpanKindRPCClientEnum}

	// SpanKindRPCServer marks a span representing the server-side of an RPC
	// or other remote call
	SpanKindRPCServerEnum = SpanKindEnum("server")
	SpanKindRPCServer     = opentracing.Tag{Key: string(SpanKind), Value: SpanKindRPCServerEnum}

	// SpanKindProducer marks a span representing the producer-side of a
	// message bus
	SpanKindProducerEnum = SpanKindEnum("producer")
	SpanKindProducer     = opentracing.Tag{Key: string(SpanKind), Value: SpanKindProducerEnum}

	// SpanKindConsumer marks a span representing the consumer-side of a
	// message bus
	SpanKindConsumerEnum = SpanKindEnum("consumer")
	SpanKindConsumer     = opentracing.Tag{Key: string(SpanKind), Value: SpanKindConsumerEnum}

	//////////////////////////////////////////////////////////////////////
	// Component name
	//////////////////////////////////////////////////////////////////////

	// Component is a low-cardinality identifier of the module, library,
	// or package that is generating a span.
	Component = StringTagName("component")

	//////////////////////////////////////////////////////////////////////
	// Sampling hint
	//////////////////////////////////////////////////////////////////////

	// SamplingPriority determines the priority of sampling this Span.
	SamplingPriority = Uint16TagName("sampling.priority")

	//////////////////////////////////////////////////////////////////////
	// Peer tags. These tags can be emitted by either client-side or
	// server-side to describe the other side/service in a peer-to-peer
	// communications, like an RPC call.
	//////////////////////////////////////////////////////////////////////

	// PeerService records the service name of the peer.
	PeerService = StringTagName("peer.service")

	// PeerAddress records the address name of the peer. This may be a "ip:port",
	// a bare "hostname", a FQDN or even a database DSN substring
	// like "mysql://username@127.0.0.1:3306/dbname"
	PeerAddress = StringTagName("peer.address")

	// PeerHostname records the host name of the peer
	PeerHostname = StringTagName("peer.hostname")

	// PeerHostIPv4 records IP v4 host address of the peer
	PeerHostIPv4 = IPv4TagName("peer.ipv4")

	// PeerHostIPv6 records IP v6 host address of the peer
	PeerHostIPv6 = StringTagName("peer.ipv6")

	// PeerPort records port number of the peer
	PeerPort = Uint16TagName("peer.port")

	//////////////////////////////////////////////////////////////////////
	// HTTP Tags
	//////////////////////////////////////////////////////////////////////

	// HTTPUrl should be the URL of the request being handled in this segment
	// of the trace, in standard URI format. The protocol is optional.
	HTTPUrl = StringTagName("http.url")

	// HTTPMethod is the HTTP method of the request, and is case-insensitive.
	HTTPMethod = StringTagName("http.method")

	// HTTPStatusCode is the numeric HTTP status code (200, 404, etc) of the
	// HTTP response.
	HTTPStatusCode = Uint16TagName("http.status_code")

	//////////////////////////////////////////////////////////////////////
	// DB Tags
	//////////////////////////////////////////////////////////////////////

	// DBInstance is database instance name.
	DBInstance = StringTagName("db.instance")

	// DBStatement is a database statement for the given database type.
	// It can be a query or a prepared statement (i.e., before substitution).
	DBStatement = StringTagName("db.statement")

	// DBType is a database type. For any SQL database, "sql".
	// For others, the lower-case database category, e.g. "redis"
	DBType = StringTagName("db.type")

	// DBUser is a username for accessing database.
	DBUser = StringTagName("db.user")

	//////////////////////////////////////////////////////////////////////
	// Message Bus Tag
	//////////////////////////////////////////////////////////////////////

	// MessageBusDestination is an address at which messages can be exchanged
	MessageBusDestination = StringTagName("message_bus.destination")

	//////////////////////////////////////////////////////////////////////
	// Error Tag
	//////////////////////////////////////////////////////////////////////

	// Error indicates that operation represented by the span resulted in an error.
	Error = BoolTagName("error")
)

这些常量定义了推荐的通用标签名称,以更好地在跟踪系统和语言/平台之间进行移植。注释中有说明不同的应用场景。

对于标签值的不同数据类型,都有特定的包装类型,其设置方法都是接收Span和需要设置的value,在Set方法中都是调用span.SetTag()方法。


// StringTagName is a common tag name to be set to a string value
type StringTagName string

// Set adds a string tag to the `span`
func (tag StringTagName) Set(span opentracing.Span, value string) {
	span.SetTag(string(tag), value)
}