github.com/minio/console@v1.4.1/pkg/logger/target/http/http.go (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2022 MinIO, Inc.
     3  //
     4  // This program is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Affero General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // This program is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  // GNU Affero General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Affero General Public License
    15  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package http
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"net/http"
    26  	"strings"
    27  	"sync"
    28  	"sync/atomic"
    29  	"time"
    30  
    31  	xhttp "github.com/minio/console/pkg/http"
    32  	"github.com/minio/console/pkg/logger/target/types"
    33  )
    34  
    35  // Timeout for the webhook http call
    36  const webhookCallTimeout = 5 * time.Second
    37  
    38  // Config http logger target
    39  type Config struct {
    40  	Enabled    bool              `json:"enabled"`
    41  	Name       string            `json:"name"`
    42  	UserAgent  string            `json:"userAgent"`
    43  	Endpoint   string            `json:"endpoint"`
    44  	AuthToken  string            `json:"authToken"`
    45  	ClientCert string            `json:"clientCert"`
    46  	ClientKey  string            `json:"clientKey"`
    47  	QueueSize  int               `json:"queueSize"`
    48  	Transport  http.RoundTripper `json:"-"`
    49  
    50  	// Custom logger
    51  	LogOnce func(ctx context.Context, err error, id interface{}, errKind ...interface{}) `json:"-"`
    52  }
    53  
    54  // Target implements logger.Target and sends the json
    55  // format of a log entry to the configured http endpoint.
    56  // An internal buffer of logs is maintained but when the
    57  // buffer is full, new logs are just ignored and an errors
    58  // is returned to the caller.
    59  type Target struct {
    60  	status int32
    61  	wg     sync.WaitGroup
    62  
    63  	// Channel of log entries
    64  	logCh chan interface{}
    65  
    66  	config Config
    67  }
    68  
    69  // Endpoint returns the backend endpoint
    70  func (h *Target) Endpoint() string {
    71  	return h.config.Endpoint
    72  }
    73  
    74  func (h *Target) String() string {
    75  	return h.config.Name
    76  }
    77  
    78  // Init validate and initialize the http target
    79  func (h *Target) Init() error {
    80  	ctx, cancel := context.WithTimeout(context.Background(), 2*webhookCallTimeout)
    81  	defer cancel()
    82  
    83  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.config.Endpoint, strings.NewReader(`{}`))
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	req.Header.Set(xhttp.ContentType, "application/json")
    89  
    90  	// Set user-agent to indicate MinIO release
    91  	// version to the configured log endpoint
    92  	req.Header.Set("User-Agent", h.config.UserAgent)
    93  
    94  	if h.config.AuthToken != "" {
    95  		req.Header.Set("Authorization", h.config.AuthToken)
    96  	}
    97  
    98  	client := http.Client{Transport: h.config.Transport}
    99  	resp, err := client.Do(req)
   100  	if err != nil {
   101  		return err
   102  	}
   103  
   104  	// Drain any response.
   105  	xhttp.DrainBody(resp.Body)
   106  
   107  	if !acceptedResponseStatusCode(resp.StatusCode) {
   108  		if resp.StatusCode == http.StatusForbidden {
   109  			return fmt.Errorf("%s returned '%s', please check if your auth token is correctly set",
   110  				h.config.Endpoint, resp.Status)
   111  		}
   112  		return fmt.Errorf("%s returned '%s', please check your endpoint configuration",
   113  			h.config.Endpoint, resp.Status)
   114  	}
   115  
   116  	h.status = 1
   117  	go h.startHTTPLogger()
   118  	return nil
   119  }
   120  
   121  // Accepted HTTP Status Codes
   122  var acceptedStatusCodeMap = map[int]bool{http.StatusOK: true, http.StatusCreated: true, http.StatusAccepted: true, http.StatusNoContent: true}
   123  
   124  func acceptedResponseStatusCode(code int) bool {
   125  	return acceptedStatusCodeMap[code]
   126  }
   127  
   128  func (h *Target) logEntry(entry interface{}) {
   129  	logJSON, err := json.Marshal(&entry)
   130  	if err != nil {
   131  		return
   132  	}
   133  
   134  	ctx, cancel := context.WithTimeout(context.Background(), webhookCallTimeout)
   135  	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
   136  		h.config.Endpoint, bytes.NewReader(logJSON))
   137  	if err != nil {
   138  		h.config.LogOnce(ctx, fmt.Errorf("%s returned '%w', please check your endpoint configuration", h.config.Endpoint, err), h.config.Endpoint)
   139  		cancel()
   140  		return
   141  	}
   142  	req.Header.Set(xhttp.ContentType, "application/json")
   143  
   144  	// Set user-agent to indicate MinIO release
   145  	// version to the configured log endpoint
   146  	req.Header.Set("User-Agent", h.config.UserAgent)
   147  
   148  	if h.config.AuthToken != "" {
   149  		req.Header.Set("Authorization", h.config.AuthToken)
   150  	}
   151  
   152  	client := http.Client{Transport: h.config.Transport}
   153  	resp, err := client.Do(req)
   154  	cancel()
   155  	if err != nil {
   156  		h.config.LogOnce(ctx, fmt.Errorf("%s returned '%w', please check your endpoint configuration", h.config.Endpoint, err), h.config.Endpoint)
   157  		return
   158  	}
   159  
   160  	// Drain any response.
   161  	xhttp.DrainBody(resp.Body)
   162  
   163  	if !acceptedResponseStatusCode(resp.StatusCode) {
   164  		switch resp.StatusCode {
   165  		case http.StatusForbidden:
   166  			h.config.LogOnce(ctx, fmt.Errorf("%s returned '%s', please check if your auth token is correctly set", h.config.Endpoint, resp.Status), h.config.Endpoint)
   167  		default:
   168  			h.config.LogOnce(ctx, fmt.Errorf("%s returned '%s', please check your endpoint configuration", h.config.Endpoint, resp.Status), h.config.Endpoint)
   169  		}
   170  	}
   171  }
   172  
   173  func (h *Target) startHTTPLogger() {
   174  	// Create a routine which sends json logs received
   175  	// from an internal channel.
   176  	h.wg.Add(1)
   177  	go func() {
   178  		defer h.wg.Done()
   179  		for entry := range h.logCh {
   180  			h.logEntry(entry)
   181  		}
   182  	}()
   183  }
   184  
   185  // New initializes a new logger target which
   186  // sends log over http to the specified endpoint
   187  func New(config Config) *Target {
   188  	h := &Target{
   189  		logCh:  make(chan interface{}, config.QueueSize),
   190  		config: config,
   191  	}
   192  
   193  	return h
   194  }
   195  
   196  // Send log message 'e' to http target.
   197  func (h *Target) Send(entry interface{}, _ string) error {
   198  	if atomic.LoadInt32(&h.status) == 0 {
   199  		// Channel was closed or used before init.
   200  		return nil
   201  	}
   202  
   203  	select {
   204  	case h.logCh <- entry:
   205  	default:
   206  		// log channel is full, do not wait and return
   207  		// an errors immediately to the caller
   208  		return errors.New("log buffer full")
   209  	}
   210  
   211  	return nil
   212  }
   213  
   214  // Cancel - cancels the target
   215  func (h *Target) Cancel() {
   216  	if atomic.CompareAndSwapInt32(&h.status, 1, 0) {
   217  		close(h.logCh)
   218  	}
   219  	h.wg.Wait()
   220  }
   221  
   222  // Type - returns type of the target
   223  func (h *Target) Type() types.TargetType {
   224  	return types.TargetHTTP
   225  }