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) -->
210 lines
7.3 KiB
Go
210 lines
7.3 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 rpc
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"time"
|
|
|
|
"go.temporal.io/api/serviceerror"
|
|
"go.temporal.io/server/common/headers"
|
|
"go.temporal.io/server/common/log"
|
|
"go.temporal.io/server/common/log/tag"
|
|
"go.temporal.io/server/common/metrics"
|
|
"go.temporal.io/server/common/persistence/serialization"
|
|
"go.temporal.io/server/common/rpc/interceptor"
|
|
serviceerrors "go.temporal.io/server/common/serviceerror"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/backoff"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"google.golang.org/grpc/metadata"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
const (
|
|
// DefaultServiceConfig is a default gRPC connection service config which enables DNS round robin between IPs.
|
|
// To use DNS resolver, a "dns:///" prefix should be applied to the hostPort.
|
|
// https://github.com/grpc/grpc/blob/master/doc/naming.md
|
|
DefaultServiceConfig = `{"loadBalancingConfig": [{"round_robin":{}}]}`
|
|
|
|
// MaxBackoffDelay is a maximum interval between reconnect attempts.
|
|
MaxBackoffDelay = 10 * time.Second
|
|
|
|
// MaxHTTPAPIRequestBytes is the maximum number of bytes an HTTP API request
|
|
// can have. This is currently set to the max gRPC request size.
|
|
MaxHTTPAPIRequestBytes = 4 * 1024 * 1024
|
|
|
|
// MaxNexusAPIRequestBodyBytes is the maximum number of bytes a Nexus HTTP API request can have. Because the body is
|
|
// read into a Payload object, this is currently set to the max Payload size. Content headers are transformed to
|
|
// Payload metadata and contribute to the Payload size as well. A separate limit is enforced on top of this.
|
|
MaxNexusAPIRequestBodyBytes = 2 * 1024 * 1024
|
|
|
|
// minConnectTimeout is the minimum amount of time we are willing to give a connection to complete.
|
|
minConnectTimeout = 20 * time.Second
|
|
|
|
// maxInternodeRecvPayloadSize indicates the internode max receive payload size.
|
|
maxInternodeRecvPayloadSize = 128 * 1024 * 1024 // 128 Mb
|
|
|
|
// ResourceExhaustedCauseHeader will be added to rpc response if request returns ResourceExhausted error.
|
|
// Value of this header will be ResourceExhaustedCause.
|
|
ResourceExhaustedCauseHeader = "X-Resource-Exhausted-Cause"
|
|
|
|
// ResourceExhaustedScopeHeader will be added to rpc response if request returns ResourceExhausted error.
|
|
// Value of this header will be the scope of exhausted resource.
|
|
ResourceExhaustedScopeHeader = "X-Resource-Exhausted-Scope"
|
|
)
|
|
|
|
// Dial creates a client connection to the given target with default options.
|
|
// The hostName syntax is defined in
|
|
// https://github.com/grpc/grpc/blob/master/doc/naming.md.
|
|
// dns resolver is used by default
|
|
func Dial(hostName string, tlsConfig *tls.Config, logger log.Logger, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
|
|
var grpcSecureOpt grpc.DialOption
|
|
if tlsConfig == nil {
|
|
grpcSecureOpt = grpc.WithTransportCredentials(insecure.NewCredentials())
|
|
} else {
|
|
grpcSecureOpt = grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))
|
|
}
|
|
|
|
// gRPC maintains connection pool inside grpc.ClientConn.
|
|
// This connection pool has auto reconnect feature.
|
|
// If connection goes down, gRPC will try to reconnect using exponential backoff strategy:
|
|
// https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md.
|
|
// Default MaxDelay is 120 seconds which is too high.
|
|
var cp = grpc.ConnectParams{
|
|
Backoff: backoff.DefaultConfig,
|
|
MinConnectTimeout: minConnectTimeout,
|
|
}
|
|
cp.Backoff.MaxDelay = MaxBackoffDelay
|
|
|
|
dialOptions := []grpc.DialOption{
|
|
grpcSecureOpt,
|
|
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxInternodeRecvPayloadSize)),
|
|
grpc.WithChainUnaryInterceptor(
|
|
headersInterceptor,
|
|
metrics.NewClientMetricsTrailerPropagatorInterceptor(logger),
|
|
errorInterceptor,
|
|
),
|
|
grpc.WithChainStreamInterceptor(
|
|
interceptor.StreamErrorInterceptor,
|
|
),
|
|
grpc.WithDefaultServiceConfig(DefaultServiceConfig),
|
|
grpc.WithDisableServiceConfig(),
|
|
grpc.WithConnectParams(cp),
|
|
}
|
|
dialOptions = append(dialOptions, opts...)
|
|
|
|
return grpc.NewClient(hostName, dialOptions...)
|
|
}
|
|
|
|
func errorInterceptor(
|
|
ctx context.Context,
|
|
method string,
|
|
req, reply interface{},
|
|
cc *grpc.ClientConn,
|
|
invoker grpc.UnaryInvoker,
|
|
opts ...grpc.CallOption,
|
|
) error {
|
|
err := invoker(ctx, method, req, reply, cc, opts...)
|
|
err = serviceerrors.FromStatus(status.Convert(err))
|
|
return err
|
|
}
|
|
|
|
func headersInterceptor(
|
|
ctx context.Context,
|
|
method string,
|
|
req, reply interface{},
|
|
cc *grpc.ClientConn,
|
|
invoker grpc.UnaryInvoker,
|
|
opts ...grpc.CallOption,
|
|
) error {
|
|
ctx = headers.Propagate(ctx)
|
|
return invoker(ctx, method, req, reply, cc, opts...)
|
|
}
|
|
|
|
func ServiceErrorInterceptor(
|
|
ctx context.Context,
|
|
req interface{},
|
|
_ *grpc.UnaryServerInfo,
|
|
handler grpc.UnaryHandler,
|
|
) (interface{}, error) {
|
|
|
|
resp, err := handler(ctx, req)
|
|
|
|
var deserializationError *serialization.DeserializationError
|
|
var serializationError *serialization.SerializationError
|
|
// convert serialization errors to be captured as serviceerrors across gRPC calls
|
|
if errors.As(err, &deserializationError) || errors.As(err, &serializationError) {
|
|
err = serviceerror.NewDataLoss(err.Error())
|
|
}
|
|
return resp, serviceerror.ToStatus(err).Err()
|
|
}
|
|
|
|
func NewFrontendServiceErrorInterceptor(
|
|
logger log.Logger,
|
|
) grpc.UnaryServerInterceptor {
|
|
return func(
|
|
ctx context.Context,
|
|
req interface{},
|
|
_ *grpc.UnaryServerInfo,
|
|
handler grpc.UnaryHandler,
|
|
) (interface{}, error) {
|
|
|
|
resp, err := handler(ctx, req)
|
|
|
|
if err == nil {
|
|
return resp, err
|
|
}
|
|
|
|
// mask some internal service errors at frontend
|
|
switch err.(type) {
|
|
case *serviceerrors.ShardOwnershipLost:
|
|
err = serviceerror.NewUnavailable("shard unavailable, please backoff and retry")
|
|
case *serviceerror.DataLoss:
|
|
err = serviceerror.NewUnavailable("internal history service error")
|
|
}
|
|
|
|
addHeadersForResourceExhausted(ctx, logger, err)
|
|
|
|
return resp, err
|
|
}
|
|
}
|
|
|
|
func addHeadersForResourceExhausted(ctx context.Context, logger log.Logger, err error) {
|
|
var reErr *serviceerror.ResourceExhausted
|
|
if errors.As(err, &reErr) {
|
|
headerErr := grpc.SetHeader(ctx, metadata.Pairs(
|
|
ResourceExhaustedCauseHeader, reErr.Cause.String(),
|
|
ResourceExhaustedScopeHeader, reErr.Scope.String(),
|
|
))
|
|
if headerErr != nil {
|
|
logger.Error("Failed to add Resource-Exhausted headers to response", tag.Error(headerErr))
|
|
}
|
|
}
|
|
}
|