Back to our main package

The first line of our main block calls our InitLog function:

func main() {
InitLog("trace-log.txt",
ioutil.Discard, os.Stdout, os.Stderr)

We use the INFO level to indicate which our server is listening.

We launch our server using a Goroutine, and since this is the main() function, we use the log.Fatal method, which is equivalent to println with a panic. This is because if we fail to start our server at this point, there are no buffers to flush, no outstanding defer statements, and no temporary files to process. We also wait for a second in order to give our server time to start:

Info.Printf("Metrics server listening on %s", serverUrl)
go func() {
log.Fatal(easy_metrics.Serve(serverUrl))
}()
time.Sleep(1 * time.Second)

Next, we declare our request using req, which we'll later execute NumRequests times:

req, err := http.NewRequest(http.MethodGet, protocol + serverUrl, nil)
if err != nil {
log.Fatalln(err)
}

In our example, we use a proxy server to pass all our requests through. This gives us the flexibility to handle proxy-level processing on a per-call basis. Our simple example does no such processing, but we do specify a proxy timeout of 1 second:

Info.Printf("Proxy listening on %s", proxyUrl)
proxyURL, _ := url.Parse(proxyUrl)
tr := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}

Our client uses the decorator pattern to wrap our proxyTimeoutClient client with the Authorization, LoadBalancing, Logging, and FaultTolerance functionality:

tr.TLSNextProto = make(map[string]func(string, *tls.Conn) http.RoundTripper)
proxyTimeoutClient := &http.Client{Transport: tr, Timeout: 1 * time.Second}

We do not modify our client implementation, rather extend its functionality (remember the open/close principle?):

client := Decorate(proxyTimeoutClient,
Authorization("mysecretpassword"),
LoadBalancing(RoundRobin(0, "web01:3000", "web02:3000", "web03:3000")),
Logging(log.New(InfoHandler, "client: ", log.Ltime)),
FaultTolerance(2, time.Second),
)

This is a declarative form of programming. There is no code ceremony. We chain our function calls, passing only the minimally required information to configure its behavior.

To get the load balancing working locally, you can add the following line to your /etc/hosts file:

127.0.0.1 localhost web01 web02 web03

Next, we define our job. We pass our client, request, the number of requests to process, and the time to wait before processing each request:

job := &Job{
Client: client,
Request: req,
NumRequests: 10,
IntervalSecs: 10,
}

In order to better comprehend the statistics, later in the easy-metrics web app, we'll set the IntervalSecs value to 10. There will 10 seconds between each of our 10 request-processing attempts.

We set our start time and kick off our job processing with job.Run(). The Run function uses the sync package to wait until all the running jobs have completed before returning the control, at which time we print out how long the request-processing bit took:

start := time.Now()
job.Run()
Info.Printf("\n>> It took %s", time.Since(start))

Once our processing is complete, we call DisplayResults from the easy_metrics package, which displays a message like the following:

INFO  : 12:48:30 Go to http://127.0.0.1:3000/easy-metrics?show=Stats
Info.Printf("metrics")
err = easy_metrics.DisplayResults(serverUrl)
if err != nil {
log.Fatalln(err)
}

Our server needs to keep running so that we can visit the easy-metrics URL to view our statistics with the user-friendly easy-metrics web app.

We create a channel to capture the Ctrl + C key sequence, which will signal our program to stop:

        Info.Printf("CTRL+C to exit")
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
}