Skip to content

[receiver/prometheus] feat: implement Prometheus API #32646

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions extension/prometheusapiserverextension/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../../Makefile.Common
13 changes: 13 additions & 0 deletions extension/prometheusapiserverextension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Prometheus API Server Extension

This extension runs as a Web server that loads the remote observers that are registered against it.

No configuration settings are within the extension itself, but settings to enable in a Prometheus receiver component are necessary in order to have data for the API server.

Example:
```yaml
extensions:
prometheus_api_server:
```

The full list of settings exposed for this exporter are documented [here](./config.go).
6 changes: 6 additions & 0 deletions extension/prometheusapiserverextension/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package prometheusapiserverextension // import "github.com/open-telemetry/opentelemetry-collector-contrib/extension/prometheusapiserverextension"

type Config struct{}
7 changes: 7 additions & 0 deletions extension/prometheusapiserverextension/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

//go:generate mdatagen metadata.yaml

// Package prometheusuiextension implements an extension that exposes the Prometheus UI for the prometheus receiver.
package prometheusapiserverextension // import "github.com/open-telemetry/opentelemetry-collector-contrib/extension/prometheusapiserverextension"
209 changes: 209 additions & 0 deletions extension/prometheusapiserverextension/extension.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package prometheusapiserverextension // import "github.com/open-telemetry/opentelemetry-collector-contrib/extension/prometheusapiserverextension"

import (
"context"
"fmt"
"net/http"
"net/url"
"os"
"runtime"
"runtime/debug"
"time"

"github.com/go-kit/kit/log"
"github.com/go-kit/log/level"
"github.com/mwitkow/go-conntrack"
"golang.org/x/net/netutil"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/route"
toolkit_web "github.com/prometheus/exporter-toolkit/web"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/scrape"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/util/httputil"
"github.com/prometheus/prometheus/web"
api_v1 "github.com/prometheus/prometheus/web/api/v1"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config/confighttp"
"go.opentelemetry.io/collector/extension"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

type prometheusAPIServerExtension struct {
config *Config
host component.Host
settings extension.CreateSettings
ctx context.Context
httpServer *http.Server
prometheusReceivers map[string]*prometheusReceiver
}

type prometheusReceiver struct {
name string
serverConfig confighttp.ServerConfig
prometheusConfig *config.Config
scrapeManager *scrape.Manager
registerer prometheus.Registerer
}

// Use same settings as Prometheus web server
const (
maxConnections = 512
readTimeoutMinutes = 10
)

func (e *prometheusAPIServerExtension) Start(_ context.Context, host component.Host) error {
e.ctx = context.Background()
e.host = host

return nil
}

func (e *prometheusAPIServerExtension) RegisterPrometheusReceiverComponents(receiverName string, serverConfig confighttp.ServerConfig,
prometheusConfig *config.Config, scrapeManager *scrape.Manager, registerer prometheus.Registerer) error {

prometheusReceiver := &prometheusReceiver{
name: receiverName,
serverConfig: serverConfig,
prometheusConfig: prometheusConfig,
scrapeManager: scrapeManager,
registerer: registerer,
}
e.prometheusReceivers[receiverName] = prometheusReceiver

o := &web.Options{
ScrapeManager: prometheusReceiver.scrapeManager,
Context: e.ctx,
ListenAddress: serverConfig.Endpoint,
ExternalURL: &url.URL{
Scheme: "http",
Host: serverConfig.Endpoint,
Path: "",
},
RoutePrefix: "/",
ReadTimeout: time.Minute * readTimeoutMinutes,
PageTitle: "Prometheus Receiver",
Flags: make(map[string]string),
MaxConnections: maxConnections,
IsAgent: true,
Gatherer: prometheus.DefaultGatherer,
Registerer: prometheusReceiver.registerer,
}

// Creates the API object in the same way as the Prometheus web package: https://github.com/prometheus/prometheus/blob/6150e1ca0ede508e56414363cc9062ef522db518/web/web.go#L314-L354
// Anything not defined by the options above will be nil, such as o.QueryEngine, o.Storage, etc. IsAgent=true, so these being nil is expected by Prometheus.
factorySPr := func(_ context.Context) api_v1.ScrapePoolsRetriever { return prometheusReceiver.scrapeManager }
factoryTr := func(_ context.Context) api_v1.TargetRetriever { return prometheusReceiver.scrapeManager }
factoryAr := func(_ context.Context) api_v1.AlertmanagerRetriever { return nil }
FactoryRr := func(_ context.Context) api_v1.RulesRetriever { return nil }
var app storage.Appendable
logger := log.NewNopLogger()

apiV1 := api_v1.NewAPI(o.QueryEngine, o.Storage, app, o.ExemplarStorage, factorySPr, factoryTr, factoryAr,
func() config.Config {
return *prometheusReceiver.prometheusConfig
},
o.Flags,
api_v1.GlobalURLOptions{
ListenAddress: o.ListenAddress,
Host: o.ExternalURL.Host,
Scheme: o.ExternalURL.Scheme,
},
func(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
f(w, r)
}
},
o.LocalStorage,
o.TSDBDir,
o.EnableAdminAPI,
logger,
FactoryRr,
o.RemoteReadSampleLimit,
o.RemoteReadConcurrencyLimit,
o.RemoteReadBytesInFrame,
o.IsAgent,
o.CORSOrigin,
func() (api_v1.RuntimeInfo, error) {
status := api_v1.RuntimeInfo{
GoroutineCount: runtime.NumGoroutine(),
GOMAXPROCS: runtime.GOMAXPROCS(0),
GOMEMLIMIT: debug.SetMemoryLimit(-1),
GOGC: os.Getenv("GOGC"),
GODEBUG: os.Getenv("GODEBUG"),
}

return status, nil
},
nil,
o.Gatherer,
o.Registerer,
nil,
o.EnableRemoteWriteReceiver,
o.EnableOTLPWriteReceiver,
)

// Create listener and monitor with conntrack in the same way as the Prometheus web package: https://github.com/prometheus/prometheus/blob/6150e1ca0ede508e56414363cc9062ef522db518/web/web.go#L564-L579
listener, err := serverConfig.ToListener(e.ctx)
if err != nil {
return fmt.Errorf("failed to create listener: %s", err.Error())
}
listener = netutil.LimitListener(listener, o.MaxConnections)
listener = conntrack.NewListener(listener,
conntrack.TrackWithName("http"),
conntrack.TrackWithTracing())

// Run the API server in the same way as the Prometheus web package: https://github.com/prometheus/prometheus/blob/6150e1ca0ede508e56414363cc9062ef522db518/web/web.go#L582-L630
mux := http.NewServeMux()
promHandler := promhttp.HandlerFor(o.Gatherer, promhttp.HandlerOpts{Registry: o.Registerer})
mux.Handle("/metrics", promHandler)

// This is the path the web package uses, but the router above with no prefix can also be Registered by apiV1 instead.
apiPath := "/api"
if o.RoutePrefix != "/" {
apiPath = o.RoutePrefix + apiPath
level.Info(logger).Log("msg", "Router prefix", "prefix", o.RoutePrefix)
}
av1 := route.New().
WithInstrumentation(setPathWithPrefix(apiPath + "/v1"))
apiV1.Register(av1)
mux.Handle(apiPath+"/v1/", http.StripPrefix(apiPath+"/v1", av1))

spanNameFormatter := otelhttp.WithSpanNameFormatter(func(_ string, r *http.Request) string {
return fmt.Sprintf("%s %s", r.Method, r.URL.Path)
})
e.httpServer, err = serverConfig.ToServer(e.ctx, e.host, e.settings.TelemetrySettings, otelhttp.NewHandler(mux, "", spanNameFormatter))
if err != nil {
return err
}
webconfig := ""

go func() {
toolkit_web.Serve(listener, e.httpServer, &toolkit_web.FlagConfig{WebConfigFile: &webconfig}, logger)
}()

return nil
}

func (e *prometheusAPIServerExtension) UpdatePrometheusConfig(receiverName string, prometheusConfig *config.Config) {
e.prometheusReceivers[receiverName].prometheusConfig = prometheusConfig
}

func (e *prometheusAPIServerExtension) Shutdown(ctx context.Context) error {
e.httpServer.Shutdown(ctx)

return nil
}

func setPathWithPrefix(prefix string) func(handlerName string, handler http.HandlerFunc) http.HandlerFunc {
return func(handlerName string, handler http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
handler(w, r.WithContext(httputil.ContextWithPath(r.Context(), prefix+r.URL.Path)))
}
}
}
Loading