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  }