synapsecns/sanguine

View on GitHub
core/metrics/localmetrics/local.go

Summary

Maintainability
A
0 mins
Test Coverage
package localmetrics

import (
    "context"
    "fmt"
    "github.com/Flaque/filet"
    "github.com/brianvoe/gofakeit/v6"
    "github.com/ory/dockertest/v3"
    "github.com/ory/dockertest/v3/docker"
    "github.com/stretchr/testify/assert"
    "github.com/synapsecns/sanguine/core/metrics/internal"
    "github.com/synapsecns/sanguine/core/retry"
    "net/http"
    "os"
    "strings"
    "sync"
    "testing"
    "time"
)

var keepAliveOnFailure = time.Minute * 10

var testMux sync.Mutex

const appLabel = "app"
const runIDLabel = "runID"

type testJaeger struct {
    tb                        testing.TB
    pool                      *dockertest.Pool
    logDir                    string
    runID                     string
    pyroscopeResource         *uiResource
    jaegerResource            *uiResource
    jaegerPyroscopeUIResource *uiResource
    networkMux                sync.Mutex
    // this should not be used directly, use getNetwork
    // this is not guaranteed to be set and only required for pyroscope-jaeger
    network *dockertest.Network
    cfg     *config
}

// startServer starts a local jaeger server for testing.
// this is a separate function so we can export testJaeger for testing.
func startServer(parentCtx context.Context, tb testing.TB, options ...Option) *testJaeger {
    tb.Helper()

    // create the test jaegar instance
    tj := testJaeger{
        tb:    tb,
        runID: gofakeit.UUID(),
        cfg:   makeConfig(options),
    }

    tb.Helper()
    // make sure we don't setup two
    testMux.Lock()
    defer testMux.Unlock()

    // if we have a global jaegerResource env var, don't setup a local one
    ctx, cancel := context.WithCancel(parentCtx)

    // create the pool
    var err error
    tj.pool, err = dockertest.NewPool("")
    assert.Nil(tb, err)

    tj.logDir = filet.TmpDir(tb, "")

    var wg sync.WaitGroup
    wg.Add(3)
    go func() {
        defer wg.Done()
        tj.jaegerResource = tj.StartJaegerServer(ctx)
    }()

    go func() {
        defer wg.Done()
        tj.pyroscopeResource = tj.StartPyroscopeServer(ctx)
    }()
    go func() {
        defer wg.Done()
        tj.jaegerPyroscopeUIResource = tj.StartJaegerPyroscopeUI(ctx)
    }()
    wg.Wait()

    logger.Warnf(tj.buildLogMessage(true))

    tb.Cleanup(func() {
        defer cancel()
        // Do not keep containers on ci.
        if tb.Failed() && os.Getenv("CI") == "" {
            logger.Warn("Test failed, will temporarily continue serving \n" + tj.buildLogMessage(false))
        } else if !tj.cfg.keepContainers {
            tj.purgeResources()
            if tj.network != nil {
                _ = tj.network.Close()
            }
        }
    })

    return &tj
}

// getNetworks gets the networks to be associaed with each container.
func (j *testJaeger) getNetworks() []*dockertest.Network {
    // no need to hit the mutex if no network is required
    if !j.cfg.requiresNetwork {
        return []*dockertest.Network{}
    }

    j.networkMux.Lock()
    defer j.networkMux.Unlock()

    if j.network != nil {
        return []*dockertest.Network{j.network}
    }

    var err error
    j.network, err = j.pool.CreateNetwork(j.runID, func(config *docker.CreateNetworkOptions) {
        config.Driver = "bridge"
        config.Labels = map[string]string{
            runIDLabel: j.runID,
        }
    })

    if err != nil {
        j.tb.Fatal(err)
    }

    return []*dockertest.Network{j.network}
}

// buildLogMessage builds a log message for the test jaeger instance.
func (j *testJaeger) buildLogMessage(includeAuxiliary bool) string {
    var messages []string
    messages = append(messages, fmt.Sprintf("jaeger ui: %s", os.Getenv(internal.JaegerUIEndpoint)))
    if j.cfg.enablePyroscope {
        messages = append(messages, fmt.Sprintf("pyroscope ui: %s", os.Getenv(internal.PyroscopeEndpoint)))
    }

    var bootMessages []string
    if len(j.getDockerizedResources()) > 0 {
        bootMessages = append(bootMessages, fmt.Sprintf("Container logs will be saved to %s", j.logDir))
    }

    if len(bootMessages) > 0 && includeAuxiliary {
        messages = append(messages, strings.Join(bootMessages, " "))
    }

    return strings.Join(messages, "\n")
}

// getDockerizedResources gets all resources that have been dockerized by this process.
func (j *testJaeger) getDockerizedResources() (dockerizedResources []*dockertest.Resource) {
    allResources := []*uiResource{j.jaegerResource, j.pyroscopeResource}
    for _, resource := range allResources {
        if resource == nil || resource.Resource == nil {
            return nil
        }
        dockerizedResources = append(dockerizedResources, resource.Resource)
    }
    return dockerizedResources
}

// purgeResources purges the resources from the pool.
func (j *testJaeger) purgeResources() {
    var wg sync.WaitGroup
    resources := j.getDockerizedResources()

    wg.Add(len(resources))
    for _, resource := range resources {
        go func(resource *dockertest.Resource) {
            _ = j.pool.Purge(resource)
            wg.Done()
        }(resource)
    }
    wg.Wait()
}

// uiResource is a wrapper around dockertest.Resource that logs the container logs to a file.
type uiResource struct {
    // Resource is the underlying dockertest resource.
    // this is not guaranteed to be set.
    *dockertest.Resource
    uiURL string
}

// checkURL is a helper function that checks if a url is alive.
// it does not check the status code.
func checkURL(url string) retry.RetryableFunc {
    return func(ctx context.Context) error {
        client := http.DefaultClient
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
        if err != nil {
            return fmt.Errorf("could not create request: %w", err)
        }

        resp, err := client.Do(req)
        if err != nil {
            return fmt.Errorf("could not get response: %w", err)
        }

        if resp != nil {
            _ = resp.Body.Close()
        }

        return nil
    }
}