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  }