238 lines
6.8 KiB
Go
238 lines
6.8 KiB
Go
|
package appinsights
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"compress/gzip"
|
||
|
"encoding/json"
|
||
|
"io/ioutil"
|
||
|
"net/http"
|
||
|
"sort"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
type transmitter interface {
|
||
|
Transmit(payload []byte, items TelemetryBufferItems) (*transmissionResult, error)
|
||
|
}
|
||
|
|
||
|
type httpTransmitter struct {
|
||
|
endpoint string
|
||
|
}
|
||
|
|
||
|
type transmissionResult struct {
|
||
|
statusCode int
|
||
|
retryAfter *time.Time
|
||
|
response *backendResponse
|
||
|
}
|
||
|
|
||
|
// Structures returned by data collector
|
||
|
type backendResponse struct {
|
||
|
ItemsReceived int `json:"itemsReceived"`
|
||
|
ItemsAccepted int `json:"itemsAccepted"`
|
||
|
Errors itemTransmissionResults `json:"errors"`
|
||
|
}
|
||
|
|
||
|
// This needs to be its own type because it implements sort.Interface
|
||
|
type itemTransmissionResults []*itemTransmissionResult
|
||
|
|
||
|
type itemTransmissionResult struct {
|
||
|
Index int `json:"index"`
|
||
|
StatusCode int `json:"statusCode"`
|
||
|
Message string `json:"message"`
|
||
|
}
|
||
|
|
||
|
const (
|
||
|
successResponse = 200
|
||
|
partialSuccessResponse = 206
|
||
|
requestTimeoutResponse = 408
|
||
|
tooManyRequestsResponse = 429
|
||
|
tooManyRequestsOverExtendedTimeResponse = 439
|
||
|
errorResponse = 500
|
||
|
serviceUnavailableResponse = 503
|
||
|
)
|
||
|
|
||
|
func newTransmitter(endpointAddress string) transmitter {
|
||
|
return &httpTransmitter{endpointAddress}
|
||
|
}
|
||
|
|
||
|
func (transmitter *httpTransmitter) Transmit(payload []byte, items TelemetryBufferItems) (*transmissionResult, error) {
|
||
|
diagnosticsWriter.Printf("----------- Transmitting %d items ---------", len(items))
|
||
|
startTime := time.Now()
|
||
|
|
||
|
// Compress the payload
|
||
|
var postBody bytes.Buffer
|
||
|
gzipWriter := gzip.NewWriter(&postBody)
|
||
|
if _, err := gzipWriter.Write(payload); err != nil {
|
||
|
diagnosticsWriter.Printf("Failed to compress the payload: %s", err.Error())
|
||
|
gzipWriter.Close()
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
gzipWriter.Close()
|
||
|
|
||
|
req, err := http.NewRequest("POST", transmitter.endpoint, &postBody)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
req.Header.Set("Content-Encoding", "gzip")
|
||
|
req.Header.Set("Content-Type", "application/x-json-stream")
|
||
|
req.Header.Set("Accept-Encoding", "gzip, deflate")
|
||
|
|
||
|
client := http.DefaultClient
|
||
|
resp, err := client.Do(req)
|
||
|
if err != nil {
|
||
|
diagnosticsWriter.Printf("Failed to transmit telemetry: %s", err.Error())
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
body, err := ioutil.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
diagnosticsWriter.Printf("Failed to read response from server: %s", err.Error())
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
duration := time.Since(startTime)
|
||
|
|
||
|
result := &transmissionResult{statusCode: resp.StatusCode}
|
||
|
|
||
|
// Grab Retry-After header
|
||
|
if retryAfterValue, ok := resp.Header[http.CanonicalHeaderKey("Retry-After")]; ok && len(retryAfterValue) == 1 {
|
||
|
if retryAfterTime, err := time.Parse(time.RFC1123, retryAfterValue[0]); err == nil {
|
||
|
result.retryAfter = &retryAfterTime
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Parse body, if possible
|
||
|
response := &backendResponse{}
|
||
|
if err := json.Unmarshal(body, &response); err == nil {
|
||
|
result.response = response
|
||
|
}
|
||
|
|
||
|
// Write diagnostics
|
||
|
if diagnosticsWriter.hasListeners() {
|
||
|
diagnosticsWriter.Printf("Telemetry transmitted in %s", duration)
|
||
|
diagnosticsWriter.Printf("Response: %d", result.statusCode)
|
||
|
if result.response != nil {
|
||
|
diagnosticsWriter.Printf("Items accepted/received: %d/%d", result.response.ItemsAccepted, result.response.ItemsReceived)
|
||
|
if len(result.response.Errors) > 0 {
|
||
|
diagnosticsWriter.Printf("Errors:")
|
||
|
for _, err := range result.response.Errors {
|
||
|
if err.Index < len(items) {
|
||
|
diagnosticsWriter.Printf("#%d - %d %s", err.Index, err.StatusCode, err.Message)
|
||
|
diagnosticsWriter.Printf("Telemetry item:\n\t%s", err.Index, string(items[err.Index:err.Index+1].serialize()))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result, nil
|
||
|
}
|
||
|
|
||
|
func (result *transmissionResult) IsSuccess() bool {
|
||
|
return result.statusCode == successResponse ||
|
||
|
// Partial response but all items accepted
|
||
|
(result.statusCode == partialSuccessResponse &&
|
||
|
result.response != nil &&
|
||
|
result.response.ItemsReceived == result.response.ItemsAccepted)
|
||
|
}
|
||
|
|
||
|
func (result *transmissionResult) IsFailure() bool {
|
||
|
return result.statusCode != successResponse && result.statusCode != partialSuccessResponse
|
||
|
}
|
||
|
|
||
|
func (result *transmissionResult) CanRetry() bool {
|
||
|
if result.IsSuccess() {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
return result.statusCode == partialSuccessResponse ||
|
||
|
result.retryAfter != nil ||
|
||
|
(result.statusCode == requestTimeoutResponse ||
|
||
|
result.statusCode == serviceUnavailableResponse ||
|
||
|
result.statusCode == errorResponse ||
|
||
|
result.statusCode == tooManyRequestsResponse ||
|
||
|
result.statusCode == tooManyRequestsOverExtendedTimeResponse)
|
||
|
}
|
||
|
|
||
|
func (result *transmissionResult) IsPartialSuccess() bool {
|
||
|
return result.statusCode == partialSuccessResponse &&
|
||
|
result.response != nil &&
|
||
|
result.response.ItemsReceived != result.response.ItemsAccepted
|
||
|
}
|
||
|
|
||
|
func (result *transmissionResult) IsThrottled() bool {
|
||
|
return result.statusCode == tooManyRequestsResponse ||
|
||
|
result.statusCode == tooManyRequestsOverExtendedTimeResponse ||
|
||
|
result.retryAfter != nil
|
||
|
}
|
||
|
|
||
|
func (result *itemTransmissionResult) CanRetry() bool {
|
||
|
return result.StatusCode == requestTimeoutResponse ||
|
||
|
result.StatusCode == serviceUnavailableResponse ||
|
||
|
result.StatusCode == errorResponse ||
|
||
|
result.StatusCode == tooManyRequestsResponse ||
|
||
|
result.StatusCode == tooManyRequestsOverExtendedTimeResponse
|
||
|
}
|
||
|
|
||
|
func (result *transmissionResult) GetRetryItems(payload []byte, items TelemetryBufferItems) ([]byte, TelemetryBufferItems) {
|
||
|
if result.statusCode == partialSuccessResponse && result.response != nil {
|
||
|
// Make sure errors are ordered by index
|
||
|
sort.Sort(result.response.Errors)
|
||
|
|
||
|
var resultPayload bytes.Buffer
|
||
|
resultItems := make(TelemetryBufferItems, 0)
|
||
|
ptr := 0
|
||
|
idx := 0
|
||
|
|
||
|
// Find each retryable error
|
||
|
for _, responseResult := range result.response.Errors {
|
||
|
if responseResult.CanRetry() {
|
||
|
// Advance ptr to start of desired line
|
||
|
for ; idx < responseResult.Index && ptr < len(payload); ptr++ {
|
||
|
if payload[ptr] == '\n' {
|
||
|
idx++
|
||
|
}
|
||
|
}
|
||
|
|
||
|
startPtr := ptr
|
||
|
|
||
|
// Read to end of line
|
||
|
for ; idx == responseResult.Index && ptr < len(payload); ptr++ {
|
||
|
if payload[ptr] == '\n' {
|
||
|
idx++
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Copy item into output buffer
|
||
|
resultPayload.Write(payload[startPtr:ptr])
|
||
|
resultItems = append(resultItems, items[responseResult.Index])
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return resultPayload.Bytes(), resultItems
|
||
|
} else if result.CanRetry() {
|
||
|
return payload, items
|
||
|
} else {
|
||
|
return payload[:0], items[:0]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// sort.Interface implementation for Errors[] list
|
||
|
|
||
|
func (results itemTransmissionResults) Len() int {
|
||
|
return len(results)
|
||
|
}
|
||
|
|
||
|
func (results itemTransmissionResults) Less(i, j int) bool {
|
||
|
return results[i].Index < results[j].Index
|
||
|
}
|
||
|
|
||
|
func (results itemTransmissionResults) Swap(i, j int) {
|
||
|
tmp := results[i]
|
||
|
results[i] = results[j]
|
||
|
results[j] = tmp
|
||
|
}
|