github.com/waldiirawan/apm-agent-go/v2@v2.2.2/transport/transporttest/recorder.go (about)

     1  // Licensed to Elasticsearch B.V. under one or more contributor
     2  // license agreements. See the NOTICE file distributed with
     3  // this work for additional information regarding copyright
     4  // ownership. Elasticsearch B.V. licenses this file to you under
     5  // the Apache License, Version 2.0 (the "License"); you may
     6  // not use this file except in compliance with the License.
     7  // You may obtain a copy of the License at
     8  //
     9  //     http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing,
    12  // software distributed under the License is distributed on an
    13  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    14  // KIND, either express or implied.  See the License for the
    15  // specific language governing permissions and limitations
    16  // under the License.
    17  
    18  package transporttest // import "github.com/waldiirawan/apm-agent-go/v2/transport/transporttest"
    19  
    20  import (
    21  	"compress/zlib"
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"io"
    26  	"io/ioutil"
    27  	"sync"
    28  
    29  	"github.com/google/go-cmp/cmp"
    30  
    31  	"github.com/waldiirawan/apm-agent-go/v2"
    32  	"github.com/waldiirawan/apm-agent-go/v2/model"
    33  )
    34  
    35  // NewRecorderTracer returns a new apm.Tracer and
    36  // RecorderTransport, which is set as the tracer's transport.
    37  //
    38  // DEPRECATED. Use apmtest.NewRecordingTracer instead.
    39  func NewRecorderTracer() (*apm.Tracer, *RecorderTransport) {
    40  	var transport RecorderTransport
    41  	tracer, err := apm.NewTracerOptions(apm.TracerOptions{
    42  		ServiceName: "transporttest",
    43  		Transport:   &transport,
    44  	})
    45  	if err != nil {
    46  		panic(err)
    47  	}
    48  	return tracer, &transport
    49  }
    50  
    51  // RecorderTransport implements transport.Transport, recording the
    52  // streams sent. The streams can be retrieved using the Payloads
    53  // method.
    54  type RecorderTransport struct {
    55  	mu       sync.Mutex
    56  	metadata *metadata
    57  	payloads Payloads
    58  }
    59  
    60  // ResetPayloads clears out any recorded payloads.
    61  func (r *RecorderTransport) ResetPayloads() {
    62  	r.mu.Lock()
    63  	defer r.mu.Unlock()
    64  	r.payloads = Payloads{}
    65  }
    66  
    67  // SendStream records the stream such that it can later be obtained via Payloads.
    68  func (r *RecorderTransport) SendStream(ctx context.Context, stream io.Reader) error {
    69  	return r.record(ctx, stream)
    70  }
    71  
    72  // SendProfile records the stream such that it can later be obtained via Payloads.
    73  func (r *RecorderTransport) SendProfile(ctx context.Context, metadata io.Reader, profiles ...io.Reader) error {
    74  	return r.recordProto(ctx, metadata, profiles)
    75  }
    76  
    77  // Metadata returns the metadata recorded by the transport. If metadata is yet to
    78  // be received, this method will panic.
    79  //
    80  // TODO(axw) introduce an exported type which contains all metadata, and return
    81  // that. Although we don't guarantee stability for this package this has a high
    82  // probability of breaking existing external tests, so let's do that in v2.
    83  func (r *RecorderTransport) Metadata() (_ model.System, _ model.Process, _ model.Service, labels model.IfaceMap) {
    84  	r.mu.Lock()
    85  	defer r.mu.Unlock()
    86  	return r.metadata.System, r.metadata.Process, r.metadata.Service, r.metadata.Labels
    87  }
    88  
    89  // CloudMetadata returns the cloud metadata recorded by the transport. If metadata
    90  // is yet to be received, this method will panic.
    91  //
    92  // TODO(axw) remove when Metadata returns an exported type containing all metadata.
    93  func (r *RecorderTransport) CloudMetadata() model.Cloud {
    94  	r.mu.Lock()
    95  	defer r.mu.Unlock()
    96  	return r.metadata.Cloud
    97  }
    98  
    99  // Payloads returns the payloads recorded by SendStream.
   100  func (r *RecorderTransport) Payloads() Payloads {
   101  	r.mu.Lock()
   102  	defer r.mu.Unlock()
   103  	return r.payloads
   104  }
   105  
   106  func (r *RecorderTransport) record(ctx context.Context, stream io.Reader) error {
   107  	reader, err := zlib.NewReader(stream)
   108  	if err != nil {
   109  		if err == io.ErrUnexpectedEOF {
   110  			if contextDone(ctx) {
   111  				return ctx.Err()
   112  			}
   113  			// truly unexpected
   114  		}
   115  		panic(err)
   116  	}
   117  	decoder := json.NewDecoder(reader)
   118  
   119  	// The first object of any request must be a metadata struct.
   120  	var metadataPayload struct {
   121  		Metadata metadata `json:"metadata"`
   122  	}
   123  	if err := decoder.Decode(&metadataPayload); err != nil {
   124  		panic(err)
   125  	}
   126  	r.recordMetadata(&metadataPayload.Metadata)
   127  
   128  	for {
   129  		var payload struct {
   130  			Error       *model.Error       `json:"error"`
   131  			Metrics     *model.Metrics     `json:"metricset"`
   132  			Span        *model.Span        `json:"span"`
   133  			Transaction *model.Transaction `json:"transaction"`
   134  		}
   135  		err := decoder.Decode(&payload)
   136  		if err == io.EOF || (err == io.ErrUnexpectedEOF && contextDone(ctx)) {
   137  			break
   138  		} else if err != nil {
   139  			panic(err)
   140  		}
   141  		r.mu.Lock()
   142  		switch {
   143  		case payload.Error != nil:
   144  			r.payloads.Errors = append(r.payloads.Errors, *payload.Error)
   145  		case payload.Metrics != nil:
   146  			r.payloads.Metrics = append(r.payloads.Metrics, *payload.Metrics)
   147  		case payload.Span != nil:
   148  			r.payloads.Spans = append(r.payloads.Spans, *payload.Span)
   149  		case payload.Transaction != nil:
   150  			r.payloads.Transactions = append(r.payloads.Transactions, *payload.Transaction)
   151  		}
   152  		r.mu.Unlock()
   153  	}
   154  	return nil
   155  }
   156  
   157  func (r *RecorderTransport) recordProto(ctx context.Context, metadataReader io.Reader, profileReaders []io.Reader) error {
   158  	var metadata metadata
   159  	if err := json.NewDecoder(metadataReader).Decode(&metadata); err != nil {
   160  		panic(err)
   161  	}
   162  	r.recordMetadata(&metadata)
   163  
   164  	r.mu.Lock()
   165  	defer r.mu.Unlock()
   166  	for _, profileReader := range profileReaders {
   167  		data, err := ioutil.ReadAll(profileReader)
   168  		if err != nil {
   169  			panic(err)
   170  		}
   171  		r.payloads.Profiles = append(r.payloads.Profiles, data)
   172  	}
   173  	return nil
   174  }
   175  
   176  func (r *RecorderTransport) recordMetadata(m *metadata) {
   177  	r.mu.Lock()
   178  	defer r.mu.Unlock()
   179  	if r.metadata == nil {
   180  		r.metadata = m
   181  	} else {
   182  		// Make sure the metadata doesn't change between requests.
   183  		if diff := cmp.Diff(r.metadata, m); diff != "" {
   184  			panic(fmt.Errorf("metadata changed\n%s", diff))
   185  		}
   186  	}
   187  }
   188  
   189  func contextDone(ctx context.Context) bool {
   190  	select {
   191  	case <-ctx.Done():
   192  		return true
   193  	default:
   194  		return false
   195  	}
   196  }
   197  
   198  // Payloads holds the recorded payloads.
   199  type Payloads struct {
   200  	Errors       []model.Error
   201  	Metrics      []model.Metrics
   202  	Spans        []model.Span
   203  	Transactions []model.Transaction
   204  	Profiles     [][]byte
   205  }
   206  
   207  // Len returns the number of recorded payloads.
   208  func (p *Payloads) Len() int {
   209  	return len(p.Transactions) + len(p.Errors) + len(p.Metrics)
   210  }
   211  
   212  type metadata struct {
   213  	System  model.System   `json:"system"`
   214  	Process model.Process  `json:"process"`
   215  	Service model.Service  `json:"service"`
   216  	Cloud   model.Cloud    `json:"cloud"`
   217  	Labels  model.IfaceMap `json:"labels,omitempty"`
   218  }