[GODRIVER-739] Support OpenTracing Created: 31/Dec/18  Updated: 27/Oct/23  Resolved: 22/Oct/20

Status: Closed
Project: Go Driver
Component/s: API
Affects Version/s: No Release
Fix Version/s: None

Type: New Feature Priority: Major - P3
Reporter: wuyuxiang Assignee: Divjot Arora (Inactive)
Resolution: Works as Designed Votes: 0
Labels: opentracing
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Issue Links:
Related

 Description   

Opentracing

Please look at https://opentracing.io first.

We are building distributed services, and we use opentracing stack to help us tracing what our requests really route to, and what our requests really do.

Work with mongodb

Many business has to work with mongodb. But at this time, what we can do if we want to monitor every mongodb command do and time costs. Yes, check the mongodb profiling log may help. But in real industry mode, we don't have this permission to check this log, or mongodb didn't set high profiling level.

That's what opentracing can do. We trace mongodb command at the client side. We trace these three info for every mongodb command: command type, namespace, err. Even more, we can add more infomation if we need.

How does this work

span, ctx := opentracing.StartSpanFromContext(ctx, "mongodb command")
defer span.Finish()
span.LogFields(
    log.String("command", "insert"),
    log.String("namespace", "db.collection"))

That's easy as above.

We start a child span, naming "mongodb command", with two fields, command:insert, namespace: db.collection. Of course, we can add mongo result when mongodb command done.

Proposal

1. Wrapper

func (c *client) WrapCommand(fn func(oldProcess func(context.Context, *command.Command) error) func(context.Context, *command.Command) {
    return fn(c.mongoRawProcess)
}
c.WrapCommand(func(oldProcess func(context.Context, *command.Command) error {
    return func(ctx context.Context, cmd *command.Command) {
        span, ctx := opentracing.StartSpanFromContext(ctx, "mongodb command")
        defer span.Finish()
        span.LogFields(
            log.String("command", cmd.Type()),
            log.String("namespace", cmd.Namespace())
        err := oldProcess(ctx, cmd)
        if err != nil {
            span.LogFields("fail", 1)
        }
        return err
    }
})

This may keep mongo-go-driver repo clean. Leave choice to user.

2. WithTracer()

NewClient().WithTracer(tracer)

add tracer option after client init, then driver do the job of start child span for every command.



 Comments   
Comment by Divjot Arora (Inactive) [ 22/Oct/20 ]

Hi robert_e_dawkins@homedepot.com,

Thank you for the proposed API. We're leaning towards not adding this API because the use of interface{} in the finished event handler makes the API more confusing and the type switch required can also have performance constraints.

– Divjot

Comment by ROBERT DAWKINS [ 02/Oct/20 ]

Is it possible to have this revisited. The structure provided in the example is not very idiomatic to the prescribed way of handling spans in open tracing. In addition, the prescribed way is not inherently concurrency safe, and ultimately defeats the purpose of context.Context

here is what it would look like using the prescribed method (untested):

type tracer struct {
	spans sync.Map
}
 
func NewTracer() *tracer {
	return &tracer{}
}
 
const prefix = "mongodb."
 
func (t *tracer) HandleStartedEvent(ctx context.Context, evt *event.CommandStartedEvent) {
	if evt == nil {
		return
	}
	span, ctx := opentracing.StartSpanFromContext(ctx, prefix+evt.CommandName)
	ext.DBType.Set(span, "mongo")
	ext.DBInstance.Set(span, evt.DatabaseName)
	ext.DBStatement.Set(span, string(evt.Command))
	span.SetTag("db.host", evt.ConnectionID)
	ext.SpanKind.Set(span, ext.SpanKindRPCClientEnum)
	ext.Component.Set(span, "golang-mongo")
	t.spans.Store(evt.RequestID, span)
}
 
func (t *tracer) HandleSucceededEvent(ctx context.Context, evt *event.CommandSucceededEvent) {
	if evt == nil {
		return
	}
	if rawSpan, ok := t.spans.Load(evt.RequestID); ok {
		defer t.spans.Delete(evt.RequestID)
		if span, ok := rawSpan.(opentracing.Span); ok {
			defer span.Finish()
			span.SetTag(prefix+"reply", string(evt.Reply))
			span.SetTag(prefix+"duration", evt.DurationNanos)
		}
	}
}
 
func (t *tracer) HandleFailedEvent(ctx context.Context, evt *event.CommandFailedEvent) {
	if evt == nil {
		return
	}
	if rawSpan, ok := t.spans.Load(evt.RequestID); ok {
		defer t.spans.Delete(evt.RequestID)
		if span, ok := rawSpan.(opentracing.Span); ok {
			defer span.Finish()
			ext.Error.Set(span, true)
			span.LogFields(log.Error(errors.New(evt.Failure)))
		}
	}
}

   heres what it would look like idiomatic:

 

func (t *tracer) HandleEvent(ctx context.Context, evt *event.CommandStartedEvent) (endEvent func(evt interface{})) {
	span, ctx := opentracing.StartSpanFromContext(ctx, prefix+evt.CommandName)
 
	ext.DBType.Set(span, "mongo")
	ext.DBInstance.Set(span, evt.DatabaseName)
	ext.DBStatement.Set(span, string(evt.Command))
	span.SetTag("db.host", evt.ConnectionID)
	ext.SpanKind.Set(span, ext.SpanKindRPCClientEnum)
	ext.Component.Set(span, "golang-mongo")
 
	return func(evt interface{}) {
		defer span.Finish()
		if command, ok := evt.(*event.CommandFailedEvent); ok{
			ext.Error.Set(span, true)
			span.LogFields(log.Error(errors.New(command.Failure)))
			return
		}
		if command, ok := evt.(*event.CommandSucceededEvent); ok {
			span.SetTag(prefix+"reply", string(command.Reply))
			span.SetTag(prefix+"duration", command.DurationNanos)
		}
	}
 
}

 

 

Comment by Divjot Arora (Inactive) [ 09/Jun/20 ]

I'm moving this back to "closed" as there hasn't been a response on the ticket. 

Comment by Divjot Arora (Inactive) [ 19/May/20 ]

Hi tomilchik@gmail.com,

Is it necessary to open the span in the CommandStartedEvent? Can it be opened before calling the command instead?

The monitoring API does support contexts propagation. The context passed to the top-level CRUD function is propagated to the event listeners. However, it does not support modifying that context, which might be necessary if you needed to store a new value in it during the listener itself. This is because Go's context package doesn't support modifying contexts in-place, as functions like context.WithValue return a copy rather than modifying the parent.

However, because the API is callback based, you should be able to use functions with receivers as the callback. I'm not very familiar with OpenTracing, but I've written up some code to show what I had in mind at https://gist.github.com/divjotarora/6ee1925b28ccc5f4b5187351f54851cf.

Please take a look at the code I linked and let me know if that'll work for this use case, or if I'm missing something important here.

– Divjot

Comment by Leonid Tomilchik [ 15/May/20 ]

I’d like to ask to re-open this issue.

despite Kris’s statement that tracing should be possible to hook up using monitor - it doesn’t appear to be doable.

the monitor api provides hooks to command start and finish, but no visible mechanism to allow finding a span that was opened in start-command, and finishing it in the finish-command hook. The monitor api doesn’t support context propagation, which makes it close to impossible to use decorator-type approach to implement tracing.

in general: the design of the driver relies very heavily on structs, as opposed to interfaces; this makes it virtually impossible to extend. It may have been a conscious design choice - but I think not the most fortunate one.

Comment by Kristofer Brandow (Inactive) [ 14/Jan/19 ]

Hi wyx.loading,

You should be able to build this on top of the command monitoring API which can be found in the event package.

--Kris

Generated at Thu Feb 08 08:34:51 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.