Pull to refresh

Notes about OpenTracing and Logs

Reading time3 min
Views1.4K

1) OpenTracing (OT) != Logs but they are very similar.

2) Every application has 2 types of scopes: ApplicationScope (AScope) and RequestScope (RScope).

ApplicationScope is everything about configs and DI.

Think about it as a singleton. This is the thing that starts, does some work, and stops.

RequestScope is everything about user request / amqp request / etc..

It starts when the application receives a request from a client. It encapsulates an execution context.

3) What is the difference between OpenTracing and Logs?

Logs are about AScope and OpenTracing is about RScope.

4) Your real application can have more then one AScope at the same time (for example if you are using graceful reload - create new/destroy old), but usually, only one AScope exists.

5) Logs and OT use the same approach for the messages. This approach called structured logging.

This approach is very easy. There is only one important thing. Your log message must not contain variable params. 

6) Real application has different layers. 

PrimaryAdapter → UseCase → DomainService → DAO → Client (Secondary Adapter) → ExternalResource

The central part of this chain usually builds by DI.

For example:

type CService struct {}

func NewCService(..., logger logger.ILogger) *CService {}

And as you can see above Logger is an AScope object, because it should be created when we build our DI-container.

7) Logger is a global object. We should not create a new logger on every request and store it into ctx.

8) We should pass context everywhere.

Context is the thing that contains our RScope. (function arguments contain request scope too - because they change between requests)

OpenTracing widely uses context. (https://github.com/opentracing/opentracing-go#creating-a-span-given-an-existing-go-contextcontext)

So if you do not pass context - you do not have OpenTracing.

9) Small piece of code.

/pkg/logger/interface.go

// ==== FIELD ====
type Field struct {
   Key   string
   Value interface{}
}

func F(key string, value interface{}) Field {
   return Field{Key: key, Value: value}
}

// func FError - returns field based on error

// ==== LOGGER ====
type Logger interface {
   WithFields(with ...Field) Logger

   Debug(ctx context.Context, msg string, with ...Field)
   Info(ctx context.Context, msg string, with ...Field)
   Warn(ctx context.Context, msg string, with ...Field)
   Error(ctx context.Context, msg string, with ...Field)
}

/pkg/logger/context.go

type key int

var loggerKey key

func NewContextWithFields(ctx context.Context, with ...Field) context.Context {
   
   fields := make([]Field, 0)
   fields = append(fields, fetchFieldsFromContext(ctx)...)
   fields = append(fields, with...)

   return context.WithValue(ctx, loggerKey, fields)
}

func fetchFieldsFromContext(ctx context.Context) []Field {
   if fields, ok := ctx.Value(loggerKey).([]Field); ok {
      return fields
   }

   return nil
}

/example.go

func (uc *UseCase) SomeOperation(ctx context.Context, param uint64) {
   span, ctx := opentracing.StartSpanFromContext(ctx, "operation_name")
   defer span.Finish()
 
   uc.logger.Info(ctx, "Starting domain operation", logger.F("param", param))
   if err := uc.domainSvc.SomeOperation(ctx, param); err != nil {
      uc.logger.Error(ctx, "Unable to perform domain operation", logger.FError(err))
      return
   }
   uc.logger.Info(ctx, "Domain operation finished")
}

As you can see above Logger receives the context as the first argument.

Why? - Because inside of Logger implementation we can define either we need to send this message to OpenTracing collector (if ctx contains span) or just to stdout. (backward compatibility)

10) One more thing. We need to process our errors only one time. SO if we return an error we need only return it. We should not log it inside the same function.

Tags:
Hubs:
Rating0
Comments1

Articles