Skip to content

Commit

Permalink
Improve connection state logging for Jaeger exporter
Browse files Browse the repository at this point in the history
Signed-off-by: Juraci Paixão Kröhling <juraci@kroehling.de>
  • Loading branch information
jpkrohling committed Dec 2, 2020
1 parent ecb27f4 commit de4901c
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 2 deletions.
90 changes: 88 additions & 2 deletions exporter/jaegerexporter/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@ package jaegerexporter
import (
"context"
"fmt"
"sync"
"time"

jaegerproto "github.com/jaegertracing/jaeger/proto-gen/api_v2"
"go.opencensus.io/stats"
"go.opencensus.io/stats/view"
"go.opencensus.io/tag"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/backoff"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/metadata"

"go.opentelemetry.io/collector/component"
Expand All @@ -40,21 +47,37 @@ func newTraceExporter(cfg *Config, logger *zap.Logger) (component.TracesExporter
return nil, err
}

client, err := grpc.Dial(cfg.GRPCClientSettings.Endpoint, opts...)
opts = append(opts, grpc.WithConnectParams(
grpc.ConnectParams{
Backoff: backoff.DefaultConfig,
MinConnectTimeout: 10 * time.Second,
},
))

conn, err := grpc.Dial(cfg.GRPCClientSettings.Endpoint, opts...)
if err != nil {
return nil, err
}

collectorServiceClient := jaegerproto.NewCollectorServiceClient(client)
collectorServiceClient := jaegerproto.NewCollectorServiceClient(conn)
s := &protoGRPCSender{
name: cfg.NameVal,
logger: logger,
client: collectorServiceClient,
metadata: metadata.New(cfg.GRPCClientSettings.Headers),
waitForReady: cfg.WaitForReady,

conn: conn,
connStateReporterInterval: time.Second,

stopCh: make(chan (struct{})),
}
s.AddStateChangeCallback(s.onStateChange)

exp, err := exporterhelper.NewTraceExporter(
cfg, logger, s.pushTraceData,
exporterhelper.WithStart(s.start),
exporterhelper.WithShutdown(s.shutdown),
exporterhelper.WithTimeout(cfg.TimeoutSettings),
exporterhelper.WithRetry(cfg.RetrySettings),
exporterhelper.WithQueue(cfg.QueueSettings),
Expand All @@ -66,10 +89,22 @@ func newTraceExporter(cfg *Config, logger *zap.Logger) (component.TracesExporter
// protoGRPCSender forwards spans encoded in the jaeger proto
// format, to a grpc server.
type protoGRPCSender struct {
name string
logger *zap.Logger
client jaegerproto.CollectorServiceClient
metadata metadata.MD
waitForReady bool

conn StateReporter
connStateReporterInterval time.Duration
stateChangeCallbacks []func(connectivity.State)

stopCh chan (struct{})
stopWg sync.WaitGroup
}

type StateReporter interface {
GetState() connectivity.State
}

func (s *protoGRPCSender) pushTraceData(
Expand Down Expand Up @@ -100,3 +135,54 @@ func (s *protoGRPCSender) pushTraceData(

return 0, nil
}

func (s *protoGRPCSender) shutdown(context.Context) error {
close(s.stopCh)
s.stopWg.Wait()
view.Unregister(MetricViews()...)
return nil
}

func (s *protoGRPCSender) start(context.Context, component.Host) error {
view.Register(MetricViews()...)
go s.startConnectionStatusReporter()
return nil
}

func (s *protoGRPCSender) startConnectionStatusReporter() {
connState := s.conn.GetState()
s.propagateStateChange(connState)

ticker := time.NewTicker(s.connStateReporterInterval)
for {
select {
case <-ticker.C:
s.stopWg.Add(1)
st := s.conn.GetState()
if connState != st {
// state has changed, report it
connState = st
s.propagateStateChange(st)
}
s.stopWg.Done()
case <-s.stopCh:
return
}
}
}

func (s *protoGRPCSender) propagateStateChange(st connectivity.State) {
for _, callback := range s.stateChangeCallbacks {
callback(st)
}
}

func (s *protoGRPCSender) onStateChange(st connectivity.State) {
mCtx, _ := tag.New(context.Background(), tag.Upsert(tag.MustNewKey("exporter_name"), s.name))
stats.Record(mCtx, mLastConnectionState.M(int64(st)))
s.logger.Info("State of the connection with the Jaeger Collector backend", zap.Stringer("state", st))
}

func (s *protoGRPCSender) AddStateChangeCallback(f func(connectivity.State)) {
s.stateChangeCallbacks = append(s.stateChangeCallbacks, f)
}
46 changes: 46 additions & 0 deletions exporter/jaegerexporter/exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,24 @@ package jaegerexporter

import (
"context"
"fmt"
"net"
"path"
"sync"
"testing"
"time"

"github.com/jaegertracing/jaeger/model"
"github.com/jaegertracing/jaeger/proto-gen/api_v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/credentials"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/config/configgrpc"
"go.opentelemetry.io/collector/config/configtls"
"go.opentelemetry.io/collector/consumer/pdata"
Expand Down Expand Up @@ -248,6 +252,48 @@ func TestMutualTLS(t *testing.T) {
assert.Equal(t, jTraceID, requestes[0].GetBatch().Spans[0].TraceID)
}

func TestConnectionStateChange(t *testing.T) {
var state connectivity.State

wg := sync.WaitGroup{}
sr := &mockStateReporter{
state: connectivity.Connecting,
}
sender := &protoGRPCSender{
logger: zap.NewNop(),
stopCh: make(chan (struct{})),
conn: sr,
connStateReporterInterval: 10 * time.Millisecond,
}

wg.Add(1)
sender.AddStateChangeCallback(func(c connectivity.State) {
state = c
wg.Done()
})

sender.start(context.Background(), componenttest.NewNopHost())
defer sender.shutdown(context.Background())
wg.Wait() // wait for the initial state to be propagated

// test
wg.Add(1)
sr.state = connectivity.Ready

// verify
wg.Wait() // wait until we get the state change
assert.Equal(t, connectivity.Ready, state)
}

type mockStateReporter struct {
state connectivity.State
}

func (m *mockStateReporter) GetState() connectivity.State {
fmt.Printf("returning state: %d\n", m.state)
return m.state
}

func initializeGRPCTestServer(t *testing.T, beforeServe func(server *grpc.Server), opts ...grpc.ServerOption) (*grpc.Server, net.Addr) {
server := grpc.NewServer(opts...)
lis, err := net.Listen("tcp", "localhost:0")
Expand Down
39 changes: 39 additions & 0 deletions exporter/jaegerexporter/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package jaegerexporter

import (
"go.opencensus.io/stats"
"go.opencensus.io/stats/view"
"go.opencensus.io/tag"
)

var (
mLastConnectionState = stats.Int64("jaegerexporter_conn_state", "Last connection state: 0 = Idle, 1 = Connecting, 2 = Ready, 3 = TransientFailure, 4 = Shutdown", stats.UnitDimensionless)
vLastConnectionState = &view.View{
Name: mLastConnectionState.Name(),
Measure: mLastConnectionState,
Description: mLastConnectionState.Description(),
Aggregation: view.LastValue(),
TagKeys: []tag.Key{
tag.MustNewKey("exporter_name"),
},
}
)

// MetricViews return the metrics views according to given telemetry level.
func MetricViews() []*view.View {
return []*view.View{vLastConnectionState}
}
32 changes: 32 additions & 0 deletions exporter/jaegerexporter/metrics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package jaegerexporter

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestProcessorMetrics(t *testing.T) {
expectedViewNames := []string{
"jaegerexporter_conn_state",
}

views := MetricViews()
for i, viewName := range expectedViewNames {
assert.Equal(t, viewName, views[i].Name)
}
}

0 comments on commit de4901c

Please sign in to comment.