github.com/aavshr/aws-sdk-go@v1.41.3/aws/csm/reporter.go (about) 1 package csm 2 3 import ( 4 "encoding/json" 5 "net" 6 "time" 7 8 "github.com/aavshr/aws-sdk-go/aws" 9 "github.com/aavshr/aws-sdk-go/aws/awserr" 10 "github.com/aavshr/aws-sdk-go/aws/request" 11 ) 12 13 // Reporter will gather metrics of API requests made and 14 // send those metrics to the CSM endpoint. 15 type Reporter struct { 16 clientID string 17 url string 18 conn net.Conn 19 metricsCh metricChan 20 done chan struct{} 21 } 22 23 var ( 24 sender *Reporter 25 ) 26 27 func connect(url string) error { 28 const network = "udp" 29 if err := sender.connect(network, url); err != nil { 30 return err 31 } 32 33 if sender.done == nil { 34 sender.done = make(chan struct{}) 35 go sender.start() 36 } 37 38 return nil 39 } 40 41 func newReporter(clientID, url string) *Reporter { 42 return &Reporter{ 43 clientID: clientID, 44 url: url, 45 metricsCh: newMetricChan(MetricsChannelSize), 46 } 47 } 48 49 func (rep *Reporter) sendAPICallAttemptMetric(r *request.Request) { 50 if rep == nil { 51 return 52 } 53 54 now := time.Now() 55 creds, _ := r.Config.Credentials.Get() 56 57 m := metric{ 58 ClientID: aws.String(rep.clientID), 59 API: aws.String(r.Operation.Name), 60 Service: aws.String(r.ClientInfo.ServiceID), 61 Timestamp: (*metricTime)(&now), 62 UserAgent: aws.String(r.HTTPRequest.Header.Get("User-Agent")), 63 Region: r.Config.Region, 64 Type: aws.String("ApiCallAttempt"), 65 Version: aws.Int(1), 66 67 XAmzRequestID: aws.String(r.RequestID), 68 69 AttemptLatency: aws.Int(int(now.Sub(r.AttemptTime).Nanoseconds() / int64(time.Millisecond))), 70 AccessKey: aws.String(creds.AccessKeyID), 71 } 72 73 if r.HTTPResponse != nil { 74 m.HTTPStatusCode = aws.Int(r.HTTPResponse.StatusCode) 75 } 76 77 if r.Error != nil { 78 if awserr, ok := r.Error.(awserr.Error); ok { 79 m.SetException(getMetricException(awserr)) 80 } 81 } 82 83 m.TruncateFields() 84 rep.metricsCh.Push(m) 85 } 86 87 func getMetricException(err awserr.Error) metricException { 88 msg := err.Error() 89 code := err.Code() 90 91 switch code { 92 case request.ErrCodeRequestError, 93 request.ErrCodeSerialization, 94 request.CanceledErrorCode: 95 return sdkException{ 96 requestException{exception: code, message: msg}, 97 } 98 default: 99 return awsException{ 100 requestException{exception: code, message: msg}, 101 } 102 } 103 } 104 105 func (rep *Reporter) sendAPICallMetric(r *request.Request) { 106 if rep == nil { 107 return 108 } 109 110 now := time.Now() 111 m := metric{ 112 ClientID: aws.String(rep.clientID), 113 API: aws.String(r.Operation.Name), 114 Service: aws.String(r.ClientInfo.ServiceID), 115 Timestamp: (*metricTime)(&now), 116 UserAgent: aws.String(r.HTTPRequest.Header.Get("User-Agent")), 117 Type: aws.String("ApiCall"), 118 AttemptCount: aws.Int(r.RetryCount + 1), 119 Region: r.Config.Region, 120 Latency: aws.Int(int(time.Since(r.Time) / time.Millisecond)), 121 XAmzRequestID: aws.String(r.RequestID), 122 MaxRetriesExceeded: aws.Int(boolIntValue(r.RetryCount >= r.MaxRetries())), 123 } 124 125 if r.HTTPResponse != nil { 126 m.FinalHTTPStatusCode = aws.Int(r.HTTPResponse.StatusCode) 127 } 128 129 if r.Error != nil { 130 if awserr, ok := r.Error.(awserr.Error); ok { 131 m.SetFinalException(getMetricException(awserr)) 132 } 133 } 134 135 m.TruncateFields() 136 137 // TODO: Probably want to figure something out for logging dropped 138 // metrics 139 rep.metricsCh.Push(m) 140 } 141 142 func (rep *Reporter) connect(network, url string) error { 143 if rep.conn != nil { 144 rep.conn.Close() 145 } 146 147 conn, err := net.Dial(network, url) 148 if err != nil { 149 return awserr.New("UDPError", "Could not connect", err) 150 } 151 152 rep.conn = conn 153 154 return nil 155 } 156 157 func (rep *Reporter) close() { 158 if rep.done != nil { 159 close(rep.done) 160 } 161 162 rep.metricsCh.Pause() 163 } 164 165 func (rep *Reporter) start() { 166 defer func() { 167 rep.metricsCh.Pause() 168 }() 169 170 for { 171 select { 172 case <-rep.done: 173 rep.done = nil 174 return 175 case m := <-rep.metricsCh.ch: 176 // TODO: What to do with this error? Probably should just log 177 b, err := json.Marshal(m) 178 if err != nil { 179 continue 180 } 181 182 rep.conn.Write(b) 183 } 184 } 185 } 186 187 // Pause will pause the metric channel preventing any new metrics from being 188 // added. It is safe to call concurrently with other calls to Pause, but if 189 // called concurently with Continue can lead to unexpected state. 190 func (rep *Reporter) Pause() { 191 lock.Lock() 192 defer lock.Unlock() 193 194 if rep == nil { 195 return 196 } 197 198 rep.close() 199 } 200 201 // Continue will reopen the metric channel and allow for monitoring to be 202 // resumed. It is safe to call concurrently with other calls to Continue, but 203 // if called concurently with Pause can lead to unexpected state. 204 func (rep *Reporter) Continue() { 205 lock.Lock() 206 defer lock.Unlock() 207 if rep == nil { 208 return 209 } 210 211 if !rep.metricsCh.IsPaused() { 212 return 213 } 214 215 rep.metricsCh.Continue() 216 } 217 218 // Client side metric handler names 219 const ( 220 APICallMetricHandlerName = "awscsm.SendAPICallMetric" 221 APICallAttemptMetricHandlerName = "awscsm.SendAPICallAttemptMetric" 222 ) 223 224 // InjectHandlers will will enable client side metrics and inject the proper 225 // handlers to handle how metrics are sent. 226 // 227 // InjectHandlers is NOT safe to call concurrently. Calling InjectHandlers 228 // multiple times may lead to unexpected behavior, (e.g. duplicate metrics). 229 // 230 // // Start must be called in order to inject the correct handlers 231 // r, err := csm.Start("clientID", "127.0.0.1:8094") 232 // if err != nil { 233 // panic(fmt.Errorf("expected no error, but received %v", err)) 234 // } 235 // 236 // sess := session.NewSession() 237 // r.InjectHandlers(&sess.Handlers) 238 // 239 // // create a new service client with our client side metric session 240 // svc := s3.New(sess) 241 func (rep *Reporter) InjectHandlers(handlers *request.Handlers) { 242 if rep == nil { 243 return 244 } 245 246 handlers.Complete.PushFrontNamed(request.NamedHandler{ 247 Name: APICallMetricHandlerName, 248 Fn: rep.sendAPICallMetric, 249 }) 250 251 handlers.CompleteAttempt.PushFrontNamed(request.NamedHandler{ 252 Name: APICallAttemptMetricHandlerName, 253 Fn: rep.sendAPICallAttemptMetric, 254 }) 255 } 256 257 // boolIntValue return 1 for true and 0 for false. 258 func boolIntValue(b bool) int { 259 if b { 260 return 1 261 } 262 263 return 0 264 }