github.com/Axway/agent-sdk@v1.1.101/pkg/traceability/httpclient.go (about) 1 package traceability 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "net/url" 10 "strconv" 11 "strings" 12 "sync" 13 "time" 14 15 "github.com/Axway/agent-sdk/pkg/agent" 16 "github.com/Axway/agent-sdk/pkg/util" 17 "github.com/Axway/agent-sdk/pkg/util/log" 18 "github.com/elastic/beats/v7/libbeat/beat" 19 "github.com/elastic/beats/v7/libbeat/outputs" 20 "github.com/elastic/beats/v7/libbeat/outputs/outil" 21 "github.com/elastic/beats/v7/libbeat/outputs/transport" 22 "github.com/elastic/beats/v7/libbeat/publisher" 23 "github.com/google/uuid" 24 ) 25 26 const ( 27 // TransactionFlow - the transaction flow used for events 28 TransactionFlow = "api-central-v8" 29 // FlowHeader - the header key for the flow value 30 FlowHeader = "axway-target-flow" 31 ) 32 33 // HTTPClient struct 34 type HTTPClient struct { 35 Connection 36 tlsConfig *transport.TLSConfig 37 compressionLevel int 38 proxyURL *url.URL 39 headers map[string]string 40 beatInfo beat.Info 41 logger log.FieldLogger 42 } 43 44 // HTTPClientSettings struct 45 type HTTPClientSettings struct { 46 BeatInfo beat.Info 47 URL string 48 Proxy *url.URL 49 TLS *transport.TLSConfig 50 Index outil.Selector 51 Pipeline *outil.Selector 52 Timeout time.Duration 53 CompressionLevel int 54 Observer outputs.Observer 55 Headers map[string]string 56 UserAgent string 57 } 58 59 // Connection struct 60 type Connection struct { 61 sync.Mutex 62 URL string 63 http *http.Client 64 connected bool 65 encoder bodyEncoder 66 userAgent string 67 } 68 69 // NewHTTPClient instantiate a client. 70 func NewHTTPClient(s HTTPClientSettings) (*HTTPClient, error) { 71 var encoder bodyEncoder 72 var err error 73 compression := s.CompressionLevel 74 if compression == 0 { 75 encoder = newJSONEncoder(nil) 76 } else { 77 encoder, err = newGzipEncoder(compression, nil) 78 if err != nil { 79 return nil, err 80 } 81 } 82 83 logger := log.NewFieldLogger(). 84 WithPackage("sdk.traceability"). 85 WithComponent("HTTPClient") 86 87 client := &HTTPClient{ 88 Connection: Connection{ 89 URL: s.URL, 90 http: &http.Client{ 91 Transport: &http.Transport{ 92 TLSClientConfig: s.TLS.ToConfig(), 93 Proxy: util.GetProxyURL(s.Proxy), 94 }, 95 Timeout: s.Timeout, 96 }, 97 encoder: encoder, 98 userAgent: s.UserAgent, 99 }, 100 compressionLevel: compression, 101 proxyURL: s.Proxy, 102 headers: s.Headers, 103 beatInfo: s.BeatInfo, 104 logger: logger, 105 } 106 107 return client, nil 108 } 109 110 // Connect establishes a connection to the clients sink. 111 func (client *HTTPClient) Connect() error { 112 client.Connection.updateConnected(true) 113 return nil 114 } 115 116 // Close publish a single event to output. 117 func (client *HTTPClient) Close() error { 118 client.Connection.updateConnected(false) 119 return nil 120 } 121 122 // Publish sends events to the clients sink. 123 func (client *HTTPClient) Publish(_ context.Context, batch publisher.Batch) error { 124 events := batch.Events() 125 err := client.publishEvents(events) 126 if err == nil { 127 batch.ACK() 128 } else { 129 batch.RetryEvents(events) 130 } 131 return err 132 } 133 134 func (client *HTTPClient) String() string { 135 return client.URL 136 } 137 138 // Clone clones a client. 139 func (client *HTTPClient) Clone() *HTTPClient { 140 c, _ := NewHTTPClient( 141 HTTPClientSettings{ 142 BeatInfo: client.beatInfo, 143 URL: client.URL, 144 Proxy: client.proxyURL, 145 TLS: client.tlsConfig, 146 Timeout: client.http.Timeout, 147 CompressionLevel: client.compressionLevel, 148 Headers: client.headers, 149 }, 150 ) 151 return c 152 } 153 154 // publishEvents - posts all events to the http endpoint. 155 func (client *HTTPClient) publishEvents(data []publisher.Event) error { 156 if len(data) == 0 { 157 return nil 158 } 159 160 if !client.isConnected() { 161 return ErrHTTPNotConnected 162 } 163 164 if client.headers == nil { 165 client.headers = make(map[string]string) 166 } 167 168 var events = make([]json.RawMessage, len(data)) 169 timeStamp := time.Now() 170 for i, event := range data { 171 events[i] = client.makeHTTPEvent(&event.Content) 172 if i == 0 { 173 timeStamp = event.Content.Timestamp 174 allFields, err := event.Content.Fields.GetValue("fields") 175 if err != nil { 176 client.headers[FlowHeader] = TransactionFlow 177 continue 178 } 179 if flow, ok := allFields.(map[string]interface{})[FlowHeader]; !ok { 180 client.headers[FlowHeader] = TransactionFlow 181 } else { 182 client.headers[FlowHeader] = flow.(string) 183 } 184 } 185 } 186 status, _, err := client.request(events, client.headers, timeStamp) 187 if err != nil { 188 client.logger.WithError(err).Error("transport error") 189 return err 190 } 191 192 if status != http.StatusOK && status != http.StatusCreated { // server error or bad input 193 client.logger.WithField("status", status).Error("failed to publish event") 194 return fmt.Errorf("failed to publish event, status: %d", status) 195 } 196 197 return nil 198 } 199 200 func (conn *Connection) isConnected() bool { 201 conn.Lock() 202 defer conn.Unlock() 203 return conn.connected 204 } 205 206 func (conn *Connection) updateConnected(update bool) { 207 conn.Lock() 208 defer conn.Unlock() 209 conn.connected = update 210 } 211 212 func (conn *Connection) request(body interface{}, headers map[string]string, eventTime time.Time) (int, []byte, error) { 213 urlStr := strings.TrimSuffix(conn.URL, "/") 214 215 if err := conn.encoder.Marshal(body); err != nil { 216 return 0, nil, ErrJSONEncodeFailed 217 } 218 return conn.execRequest(urlStr, conn.encoder.Reader(), headers, eventTime) 219 } 220 221 func (conn *Connection) execRequest(url string, body io.Reader, headers map[string]string, eventTime time.Time) (int, []byte, error) { 222 req, err := http.NewRequest("POST", url, body) 223 if log.IsHTTPLogTraceEnabled() { 224 req = log.NewRequestWithTraceContext(uuid.New().String(), req) 225 } 226 227 if err != nil { 228 return 0, nil, err 229 } 230 231 err = conn.addHeaders(&req.Header, body, eventTime) 232 if err != nil { 233 return 0, nil, err 234 } 235 236 return conn.execHTTPRequest(req, headers) 237 } 238 239 func (conn *Connection) addHeaders(header *http.Header, body io.Reader, eventTime time.Time) error { 240 token, err := agent.GetCentralAuthToken() 241 if err != nil { 242 return err 243 } 244 245 header.Add("Authorization", "Bearer "+token) 246 header.Add("Capture-Org-ID", agent.GetCentralConfig().GetTenantID()) 247 header.Add("User-Agent", conn.userAgent) 248 header.Add("Timestamp", strconv.FormatInt(eventTime.UTC().Unix(), 10)) 249 250 if body != nil { 251 conn.encoder.AddHeader(header) 252 } 253 return nil 254 } 255 256 func (conn *Connection) execHTTPRequest(req *http.Request, headers map[string]string) (int, []byte, error) { 257 for key, value := range headers { 258 req.Header.Add(key, value) 259 } 260 261 resp, err := conn.http.Do(req) 262 if err != nil { 263 conn.updateConnected(false) 264 return 0, nil, err 265 } 266 defer closing(resp.Body) 267 268 status := resp.StatusCode 269 if status >= 300 { 270 conn.updateConnected(false) 271 return status, nil, fmt.Errorf("%v", resp.Status) 272 } 273 obj, err := io.ReadAll(resp.Body) 274 if err != nil { 275 conn.updateConnected(false) 276 return status, nil, err 277 } 278 return status, obj, nil 279 } 280 281 func closing(c io.Closer) { 282 c.Close() 283 } 284 285 func (client *HTTPClient) makeHTTPEvent(v *beat.Event) json.RawMessage { 286 var eventData json.RawMessage 287 msg := v.Fields["message"].(string) 288 json.Unmarshal([]byte(msg), &eventData) 289 290 return eventData 291 }