github.com/mongodb/grip@v0.0.0-20240213223901-f906268d82b9/send/buildlogger.go (about) 1 package send 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "net/http" 12 "os" 13 "strconv" 14 "strings" 15 "time" 16 17 "github.com/mongodb/grip/level" 18 "github.com/mongodb/grip/message" 19 ) 20 21 type buildlogger struct { 22 conf *BuildloggerConfig 23 name string 24 cache chan []interface{} 25 client *http.Client 26 *Base 27 } 28 29 // BuildloggerConfig describes the configuration needed for a Sender 30 // instance that posts log messages to a buildlogger service 31 // (e.g. logkeeper.) 32 type BuildloggerConfig struct { 33 // CreateTest controls 34 CreateTest bool `json:"create_test" bson:"create_test"` 35 URL string `json:"url" bson:"url"` 36 37 // The following values are used by the buildlogger service to 38 // attach metadata to the logs. The GetBuildloggerConfig 39 // method populates Number, Phase, Builder, and Test from 40 // environment variables, though you can set them directly in 41 // your application. You must set the Command value directly. 42 Number int `json:"number" bson:"number"` 43 Phase string `json:"phase" bson:"phase"` 44 Builder string `json:"builder" bson:"builder"` 45 Test string `json:"test" bson:"test"` 46 Command string `json:"command" bson:"command"` 47 48 // Configure a local sender for "fallback" operations and to 49 // collect the location (URLS) of the buildlogger output 50 Local Sender `json:"-" bson:"-"` 51 52 buildID string 53 testID string 54 username string 55 password string 56 } 57 58 // ReadCredentialsFromFile parses a JSON file for buildlogger 59 // credentials and updates the config. 60 func (c *BuildloggerConfig) ReadCredentialsFromFile(fn string) error { 61 if _, err := os.Stat(fn); os.IsNotExist(err) { 62 return errors.New("credentials file does not exist") 63 } 64 65 contents, err := ioutil.ReadFile(fn) 66 if err != nil { 67 return err 68 } 69 70 out := struct { 71 Username string `json:"username"` 72 Password string `json:"password"` 73 }{} 74 if err := json.Unmarshal(contents, &out); err != nil { 75 return err 76 } 77 78 c.username = out.Username 79 c.password = out.Password 80 81 return nil 82 } 83 84 // SetCredentials configures the username and password of the 85 // BuildLoggerConfig object. Use to programatically update the 86 // credentials in a configuration object instead of reading from the 87 // credentials file with ReadCredentialsFromFile(), 88 func (c *BuildloggerConfig) SetCredentials(username, password string) { 89 c.username = username 90 c.password = password 91 } 92 93 // GetBuildloggerConfig produces a BuildloggerConfig object, reading 94 // default values from environment variables, although you can set 95 // these options yourself. 96 // 97 // You must also populate the credentials seperatly using either the 98 // ReadCredentialsFromFile or SetCredentials methods. If the 99 // BUILDLOGGER_CREDENTIALS environment variable is set, 100 // GetBuildloggerConfig will read credentials from this file. 101 // 102 // Buildlogger has a concept of a build, with a global log, as well as 103 // subsidiary "test" logs. To exercise this functionality, you will 104 // use a single Buildlogger config instance to create individual 105 // Sender instances that target each of these output formats. 106 // 107 // The Buildlogger config has a Local attribute which is used by the 108 // logger config when: the Buildlogger instance cannot contact the 109 // messages sent to the buildlogger. Additional the Local Sender also 110 // logs the remote location of the build logs. This Sender 111 // implementation is used in the default ErrorHandler for this 112 // implementation. 113 // 114 // Create a BuildloggerConfig instance, set up the crednetials if 115 // needed, and create a sender. This will be the "global" log, in 116 // buildlogger terminology. Then, set set the CreateTest attribute, 117 // and generate additional per-test senders. For example: 118 // 119 // conf := GetBuildloggerConfig() 120 // global := MakeBuildlogger("<name>-global", conf) 121 // // ... use global 122 // conf.CreateTest = true 123 // testOne := MakeBuildlogger("<name>-testOne", conf) 124 func GetBuildloggerConfig() (*BuildloggerConfig, error) { 125 conf := &BuildloggerConfig{ 126 URL: os.Getenv("BULDLOGGER_URL"), 127 Phase: os.Getenv("MONGO_PHASE"), 128 Builder: os.Getenv("MONGO_BUILDER_NAME"), 129 Test: os.Getenv("MONGO_TEST_FILENAME"), 130 } 131 132 if creds := os.Getenv("BUILDLOGGER_CREDENTIALS"); creds != "" { 133 if err := conf.ReadCredentialsFromFile(creds); err != nil { 134 return nil, err 135 } 136 } 137 138 buildNum, err := strconv.Atoi(os.Getenv("MONGO_BUILD_NUMBER")) 139 if err != nil { 140 return nil, err 141 } 142 conf.Number = buildNum 143 144 if conf.Test == "" { 145 conf.Test = "unknown" 146 } 147 148 if conf.Phase == "" { 149 conf.Phase = "unknown" 150 } 151 152 return conf, nil 153 } 154 155 // GetGlobalLogURL returns the URL for the current global log in use. 156 // Must use after constructing the buildlogger instance. 157 func (c *BuildloggerConfig) GetGlobalLogURL() string { 158 return fmt.Sprintf("%s/build/%s", c.URL, c.buildID) 159 } 160 161 // GetTestLogURL returns the current URL for the test log currently in 162 // use. Must use after constructing the buildlogger instance. 163 func (c *BuildloggerConfig) GetTestLogURL() string { 164 return fmt.Sprintf("%s/build/%s/test/%s", c.URL, c.buildID, c.testID) 165 } 166 167 // GetBuildID returns the build ID for the log currently in use. 168 func (c *BuildloggerConfig) GetBuildID() string { 169 return c.buildID 170 } 171 172 // GetTestID returns the test ID for the log currently in use. 173 func (c *BuildloggerConfig) GetTestID() string { 174 return c.testID 175 } 176 177 // NewBuildlogger constructs a Buildlogger-targeted Sender, with level 178 // information set. See MakeBuildlogger and GetBuildloggerConfig for 179 // more information. 180 func NewBuildlogger(name string, conf *BuildloggerConfig, l LevelInfo) (Sender, error) { 181 s, err := MakeBuildlogger(name, conf) 182 if err != nil { 183 return nil, err 184 } 185 186 return setup(s, name, l) 187 } 188 189 // MakeBuildlogger constructs a buildlogger targeting sender using the 190 // BuildloggerConfig object for configuration. Generally you will 191 // create a "global" instance, and then several subsidiary Senders 192 // that target specific tests. See the documentation of 193 // GetBuildloggerConfig for more information. 194 // 195 // Upon creating a logger, this method will write, to standard out, 196 // the URL that you can use to view the logs produced by this Sender. 197 func MakeBuildlogger(name string, conf *BuildloggerConfig) (Sender, error) { 198 b := &buildlogger{ 199 name: name, 200 conf: conf, 201 cache: make(chan []interface{}), 202 client: &http.Client{Timeout: 10 * time.Second}, 203 Base: NewBase(name), 204 } 205 206 if b.conf.Local == nil { 207 b.conf.Local = MakeNative() 208 } 209 210 if err := b.SetErrorHandler(ErrorHandlerFromSender(b.conf.Local)); err != nil { 211 return nil, err 212 } 213 214 if b.conf.buildID == "" { 215 data := struct { 216 Builder string `json:"builder"` 217 Number int `json:"buildnum"` 218 }{ 219 Builder: name, 220 Number: conf.Number, 221 } 222 223 out, err := b.doPost(data) 224 if err != nil { 225 b.conf.Local.Send(message.NewErrorMessage(level.Error, err)) 226 return nil, err 227 } 228 229 b.conf.buildID = out.ID 230 231 b.conf.Local.Send(message.NewLineMessage(level.Notice, 232 "Writing logs to buildlogger global log at:", 233 b.conf.GetGlobalLogURL())) 234 } 235 236 if b.conf.CreateTest { 237 data := struct { 238 Filename string `json:"test_filename"` 239 Command string `json:"command"` 240 Phase string `json:"phase"` 241 }{ 242 Filename: conf.Test, 243 Command: conf.Command, 244 Phase: conf.Phase, 245 } 246 247 out, err := b.doPost(data) 248 if err != nil { 249 b.conf.Local.Send(message.NewErrorMessage(level.Error, err)) 250 return nil, err 251 } 252 253 b.conf.testID = out.ID 254 255 b.conf.Local.Send(message.NewLineMessage(level.Notice, 256 "Writing logs to buildlogger test log at:", 257 b.conf.GetTestLogURL())) 258 } 259 260 return b, nil 261 } 262 263 func (b *buildlogger) Send(m message.Composer) { 264 if b.Level().ShouldLog(m) { 265 req := [][]interface{}{ 266 {float64(time.Now().Unix()), m.String()}, 267 } 268 269 out, err := json.Marshal(req) 270 if err != nil { 271 b.conf.Local.Send(message.NewErrorMessage(level.Error, err)) 272 return 273 } 274 275 if err := b.postLines(bytes.NewBuffer(out)); err != nil { 276 b.ErrorHandler()(err, message.NewBytesMessage(b.level.Default, out)) 277 } 278 } 279 } 280 281 func (b *buildlogger) Flush(_ context.Context) error { return nil } 282 283 func (b *buildlogger) SetName(n string) { 284 b.conf.Local.SetName(n) 285 b.Base.SetName(n) 286 } 287 288 func (b *buildlogger) SetLevel(l LevelInfo) error { 289 if err := b.Base.SetLevel(l); err != nil { 290 return err 291 } 292 293 if err := b.conf.Local.SetLevel(l); err != nil { 294 return err 295 } 296 297 return nil 298 } 299 300 /////////////////////////////////////////////////////////////////////////// 301 // 302 // internal methods and helpers 303 // 304 /////////////////////////////////////////////////////////////////////////// 305 306 type buildLoggerIDResponse struct { 307 ID string `json:"id"` 308 } 309 310 func (b *buildlogger) doPost(data interface{}) (*buildLoggerIDResponse, error) { 311 body, err := json.Marshal(data) 312 if err != nil { 313 return nil, err 314 } 315 316 req, err := http.NewRequest("POST", b.getURL(), bytes.NewBuffer(body)) 317 if err != nil { 318 return nil, err 319 } 320 req.Header.Set("Content-Type", "application/json; charset=utf-8") 321 req.SetBasicAuth(b.conf.username, b.conf.password) 322 323 resp, err := b.client.Do(req) 324 if err != nil { 325 return nil, err 326 } 327 defer resp.Body.Close() 328 decoder := json.NewDecoder(resp.Body) 329 330 out := &buildLoggerIDResponse{} 331 if err := decoder.Decode(out); err != nil { 332 return nil, err 333 } 334 335 return out, nil 336 } 337 338 func (b *buildlogger) getURL() string { 339 parts := []string{b.conf.URL, "build"} 340 341 if b.conf.buildID != "" { 342 parts = append(parts, b.conf.buildID) 343 } 344 345 // if we want to create a test id, (e.g. the CreateTest flag 346 // is set and we don't have a testID), then the following URL 347 // will generate a testID. 348 if b.conf.CreateTest && b.conf.testID == "" && b.conf.buildID != "" { 349 // this will create the testID. 350 parts = append(parts, "test") 351 } 352 353 // if a test id is present, then we want to append to the test logs. 354 if b.conf.testID != "" { 355 parts = append(parts, "test", b.conf.testID) 356 } 357 358 return strings.Join(parts, "/") 359 } 360 361 func (b *buildlogger) postLines(body io.Reader) error { 362 req, err := http.NewRequest("POST", b.getURL(), body) 363 364 if err != nil { 365 return err 366 } 367 req.SetBasicAuth(b.conf.username, b.conf.password) 368 369 _, err = b.client.Do(req) 370 return err 371 }