github.com/m3db/m3@v1.5.0/src/integration/resources/docker/prometheus.go (about)

     1  // Copyright (c) 2021  Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package docker
    22  
    23  import (
    24  	"context"
    25  	"encoding/json"
    26  	"errors"
    27  	"fmt"
    28  	"io/ioutil"
    29  	"net/http"
    30  	"time"
    31  
    32  	"github.com/ory/dockertest/v3"
    33  	"github.com/prometheus/common/model"
    34  
    35  	"github.com/m3db/m3/src/integration/resources"
    36  	"github.com/m3db/m3/src/x/instrument"
    37  )
    38  
    39  // Prometheus is a docker-backed instantiation of Prometheus.
    40  type Prometheus struct {
    41  	pool      *dockertest.Pool
    42  	pathToCfg string
    43  	iOpts     instrument.Options
    44  
    45  	resource *Resource
    46  }
    47  
    48  // PrometheusOptions contains the options for
    49  // spinning up docker container running Prometheus
    50  type PrometheusOptions struct {
    51  	// Pool is the connection to the docker API
    52  	Pool *dockertest.Pool
    53  	// PathToCfg contains the path to the prometheus.yml configuration
    54  	// file to be used on startup.
    55  	PathToCfg string
    56  	// InstrumentOptions are the instrument.Options to use when
    57  	// creating the resource.
    58  	InstrumentOptions instrument.Options
    59  }
    60  
    61  // NewPrometheus creates a new docker-backed Prometheus
    62  // that implements the resources.ExternalResources interface.
    63  func NewPrometheus(opts PrometheusOptions) resources.ExternalResources {
    64  	if opts.InstrumentOptions == nil {
    65  		opts.InstrumentOptions = instrument.NewOptions()
    66  	}
    67  	return &Prometheus{
    68  		pool:      opts.Pool,
    69  		pathToCfg: opts.PathToCfg,
    70  		iOpts:     opts.InstrumentOptions,
    71  	}
    72  }
    73  
    74  // Setup is a method that setups up the prometheus instance.
    75  func (p *Prometheus) Setup() error {
    76  	if p.resource != nil {
    77  		return errors.New("prometheus already setup. must close resource " +
    78  			"before attempting to setup again")
    79  	}
    80  
    81  	if err := SetupNetwork(p.pool); err != nil {
    82  		return err
    83  	}
    84  
    85  	res, err := NewDockerResource(p.pool, ResourceOptions{
    86  		ContainerName: "prometheus",
    87  		Image: Image{
    88  			Name: "prom/prometheus",
    89  			Tag:  "latest",
    90  		},
    91  		PortList: []int{9090},
    92  		Mounts: []string{
    93  			fmt.Sprintf("%s:/etc/prometheus/prometheus.yml", p.pathToCfg),
    94  		},
    95  		InstrumentOpts: p.iOpts,
    96  	})
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	p.resource = res
   102  
   103  	return p.waitForHealthy()
   104  }
   105  
   106  func (p *Prometheus) waitForHealthy() error {
   107  	return resources.Retry(func() error {
   108  		req, err := http.NewRequestWithContext(
   109  			context.Background(),
   110  			http.MethodGet,
   111  			"http://0.0.0.0:9090/-/ready",
   112  			nil,
   113  		)
   114  		if err != nil {
   115  			return err
   116  		}
   117  
   118  		client := http.Client{}
   119  		res, _ := client.Do(req)
   120  		if res != nil {
   121  			_ = res.Body.Close()
   122  
   123  			if res.StatusCode == http.StatusOK {
   124  				return nil
   125  			}
   126  		}
   127  
   128  		return errors.New("prometheus not ready")
   129  	})
   130  }
   131  
   132  // PrometheusQueryRequest contains the parameters for making a query request.
   133  type PrometheusQueryRequest struct {
   134  	// Query is the prometheus query to execute
   135  	Query string
   136  	// Time is the time to execute the query at
   137  	Time time.Time
   138  }
   139  
   140  // String converts the query request into a string suitable for use
   141  // in the url params or request body
   142  func (p *PrometheusQueryRequest) String() string {
   143  	str := fmt.Sprintf("query=%v", p.Query)
   144  
   145  	if !p.Time.IsZero() {
   146  		str += fmt.Sprintf("&time=%v", p.Time.Unix())
   147  	}
   148  
   149  	return str
   150  }
   151  
   152  // Query executes a query request against the prometheus resource.
   153  func (p *Prometheus) Query(req PrometheusQueryRequest) (model.Vector, error) {
   154  	if p.resource.Closed() {
   155  		return nil, errClosed
   156  	}
   157  
   158  	r, err := http.NewRequestWithContext(
   159  		context.Background(),
   160  		http.MethodGet,
   161  		fmt.Sprintf("http://0.0.0.0:9090/api/v1/query?%s", req.String()),
   162  		nil,
   163  	)
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  
   168  	client := http.Client{}
   169  	res, err := client.Do(r)
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  	defer res.Body.Close()
   174  
   175  	body, err := ioutil.ReadAll(res.Body)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	if res.StatusCode != http.StatusOK {
   181  		return nil, fmt.Errorf("non-200 status code received. "+
   182  			"status=%v responseBody=%v", res.StatusCode, string(body))
   183  	}
   184  
   185  	var parsedResp jsonInstantQueryResponse
   186  	if err := json.Unmarshal(body, &parsedResp); err != nil {
   187  		return nil, err
   188  	}
   189  
   190  	return parsedResp.Data.Result, nil
   191  }
   192  
   193  type jsonInstantQueryResponse struct {
   194  	Status string
   195  	Data   vectorResult
   196  }
   197  
   198  type vectorResult struct {
   199  	ResultType model.ValueType
   200  	Result     model.Vector
   201  }
   202  
   203  // Close cleans up the prometheus instance.
   204  func (p *Prometheus) Close() error {
   205  	if p.resource.Closed() {
   206  		return errClosed
   207  	}
   208  
   209  	if err := p.resource.Close(); err != nil {
   210  		return err
   211  	}
   212  
   213  	p.resource = nil
   214  
   215  	return nil
   216  }