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 }