mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
## What changed? <!-- Describe what has changed in this PR --> **(1)** Adds the following OpenTelemetry (OTEL) span attributes: - `temporalWorkflowID` - `temporalRunID` These are the same OTEL attribute keys the SDKs emit. It makes sense to me to make them the same for consistency. **(2)** Replaces the deprecated approach of OTEL interceptors with `stats.Handler`s **(3)** If there are no exporters, disables the `stats.Handlers` completely. Right now we run through the OTEL processing even if it's disabled. ## Why? <!-- Tell your future self why have you made these changes --> Right now there is no way to query all spans for a workflow together. The newly added span attribute allows to correlate spans that belong to the same workflow execution. ## How did you test it? <!-- How have you verified this change? Tested locally? Added a unit test? Checked in staging env? --> (1) Added unit tests (2) Manually Using ``` docker run -p 127.0.0.1:4317:4317 -p 127.0.0.1:55679:55679 otel/opentelemetry-collector:0.106.1 2>&1 | tee collector-output.txt ``` it outputs <img width="591" alt="image" src="https://github.com/user-attachments/assets/0a1dfcc3-b3ae-49b7-bd0b-9667e8ef3945"> ## Potential risks <!-- Assuming the worst case, what can be broken when deploying this change to production? --> It appears to be **no** breaking change. (ie same OTEL attributes as before) <img width="2011" alt="Screenshot 2024-10-26 at 10 00 41 AM" src="https://github.com/user-attachments/assets/c871eeac-ed7b-428e-9c0a-425c796d8761"> ## Documentation <!-- Have you made sure this change doesn't falsify anything currently stated in `docs/`? If significant new behavior is added, have you described that in `docs/`? --> ## Is hotfix candidate? <!-- Is this PR a hotfix candidate or does it require a notification to be sent to the broader community? (Yes/No) -->
195 lines
6.1 KiB
Go
195 lines
6.1 KiB
Go
// The MIT License
|
|
//
|
|
// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved.
|
|
//
|
|
// Copyright (c) 2020 Uber Technologies, Inc.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
// THE SOFTWARE.
|
|
|
|
package telemetry
|
|
|
|
import (
|
|
"context"
|
|
|
|
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/propagation"
|
|
"go.opentelemetry.io/otel/trace"
|
|
"go.opentelemetry.io/otel/trace/noop"
|
|
"go.temporal.io/server/common"
|
|
"go.temporal.io/server/common/log"
|
|
"go.temporal.io/server/common/log/tag"
|
|
"go.temporal.io/server/common/rpc/interceptor/logtags"
|
|
"google.golang.org/grpc/stats"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
type methodNameKey struct{}
|
|
|
|
type (
|
|
// ServerStatsHandler gives a named type to the stats.Handler implementation provided by otelgrpc.
|
|
ServerStatsHandler stats.Handler
|
|
|
|
// ClientStatsHandler gives a named type to the grpc.UnaryClientInterceptor implementation provided by otelgrpc.
|
|
ClientStatsHandler stats.Handler
|
|
|
|
customServerStatsHandler struct {
|
|
isDebug bool
|
|
wrapped stats.Handler
|
|
tags *logtags.WorkflowTags
|
|
}
|
|
)
|
|
|
|
// NewServerStatsHandler creates a new gRPC stats handler that tracks each request with an encapsulating span
|
|
// using the provided TracerProvider and TextMapPropagator.
|
|
//
|
|
// NOTE: If the TracerProvider is `noop.TracerProvider`, it returns `nil`.
|
|
func NewServerStatsHandler(
|
|
tp trace.TracerProvider,
|
|
tmp propagation.TextMapPropagator,
|
|
logger log.Logger,
|
|
) ServerStatsHandler {
|
|
if !isEnabled(tp) {
|
|
return nil
|
|
}
|
|
|
|
return newCustomServerStatsHandler(
|
|
otelgrpc.NewServerHandler(
|
|
otelgrpc.WithPropagators(tmp),
|
|
otelgrpc.WithTracerProvider(tp),
|
|
),
|
|
logger)
|
|
}
|
|
|
|
// NewClientStatsHandler creates a new gRPC stats handler that tracks each request with an encapsulating span
|
|
// using the provided TracerProvider and TextMapPropagator.
|
|
//
|
|
// NOTE: If the TracerProvider is `noop.TracerProvider`, it returns `nil`.
|
|
func NewClientStatsHandler(
|
|
tp trace.TracerProvider,
|
|
tmp propagation.TextMapPropagator,
|
|
) ClientStatsHandler {
|
|
if !isEnabled(tp) {
|
|
return nil
|
|
}
|
|
|
|
return otelgrpc.NewClientHandler(
|
|
otelgrpc.WithPropagators(tmp),
|
|
otelgrpc.WithTracerProvider(tp),
|
|
)
|
|
}
|
|
|
|
func isEnabled(tp trace.TracerProvider) bool {
|
|
_, isNoop := tp.(noop.TracerProvider)
|
|
return !isNoop
|
|
}
|
|
|
|
func newCustomServerStatsHandler(
|
|
handler stats.Handler,
|
|
logger log.Logger,
|
|
) *customServerStatsHandler {
|
|
return &customServerStatsHandler{
|
|
wrapped: handler,
|
|
isDebug: debugMode(),
|
|
tags: logtags.NewWorkflowTags(common.NewProtoTaskTokenSerializer(), logger),
|
|
}
|
|
}
|
|
|
|
func (c *customServerStatsHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context {
|
|
return c.wrapped.TagRPC(
|
|
context.WithValue(ctx, methodNameKey{}, info.FullMethodName),
|
|
info)
|
|
}
|
|
|
|
func (c *customServerStatsHandler) HandleRPC(ctx context.Context, stat stats.RPCStats) {
|
|
// handling `End` before wrapped stats.Handler since it closes the span
|
|
switch s := stat.(type) {
|
|
case *stats.End:
|
|
// annotate with gRPC error payload
|
|
if c.isDebug {
|
|
span := trace.SpanFromContext(ctx)
|
|
|
|
//revive:disable-next-line:unchecked-type-assertion
|
|
statusErr, ok := status.FromError(s.Error)
|
|
if ok && statusErr != nil {
|
|
payload, _ := protojson.Marshal(statusErr.Proto())
|
|
span.SetAttributes(attribute.Key("rpc.response.error").String(string(payload)))
|
|
}
|
|
}
|
|
}
|
|
|
|
c.wrapped.HandleRPC(ctx, stat)
|
|
|
|
switch s := stat.(type) {
|
|
case *stats.InPayload:
|
|
span := trace.SpanFromContext(ctx)
|
|
|
|
methodName, ok := ctx.Value(methodNameKey{}).(string)
|
|
if !ok {
|
|
methodName = "unknown"
|
|
}
|
|
|
|
// annotate span with workflow tags (same ones the Temporal SDKs use)
|
|
for _, logTag := range c.tags.Extract(s.Payload, methodName) {
|
|
var k string
|
|
switch logTag.Key() {
|
|
case tag.WorkflowIDKey:
|
|
k = "temporalWorkflowID"
|
|
case tag.WorkflowRunIDKey:
|
|
k = "temporalRunID"
|
|
default:
|
|
continue
|
|
}
|
|
span.SetAttributes(attribute.Key(k).String(logTag.Value().(string)))
|
|
}
|
|
|
|
// annotate with gRPC request payload
|
|
if c.isDebug {
|
|
//revive:disable-next-line:unchecked-type-assertion
|
|
reqMsg := s.Payload.(proto.Message)
|
|
payload, _ := protojson.Marshal(reqMsg)
|
|
msgType := string(proto.MessageName(reqMsg).Name())
|
|
span.SetAttributes(attribute.Key("rpc.request.payload").String(string(payload)))
|
|
span.SetAttributes(attribute.Key("rpc.request.type").String(msgType))
|
|
}
|
|
case *stats.OutPayload:
|
|
// annotate with gRPC response payload
|
|
if c.isDebug {
|
|
span := trace.SpanFromContext(ctx)
|
|
|
|
//revive:disable-next-line:unchecked-type-assertion
|
|
respMsg := s.Payload.(proto.Message)
|
|
payload, _ := protojson.Marshal(respMsg)
|
|
msgType := string(proto.MessageName(respMsg).Name())
|
|
span.SetAttributes(attribute.Key("rpc.response.payload").String(string(payload)))
|
|
span.SetAttributes(attribute.Key("rpc.response.type").String(msgType))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *customServerStatsHandler) TagConn(ctx context.Context, info *stats.ConnTagInfo) context.Context {
|
|
return c.wrapped.TagConn(ctx, info)
|
|
}
|
|
|
|
func (c *customServerStatsHandler) HandleConn(ctx context.Context, stat stats.ConnStats) {
|
|
c.wrapped.HandleConn(ctx, stat)
|
|
}
|