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 }