Skip to content
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

pkg/{sdk,generator} Expose Prometheus metrics port #323

Merged
merged 1 commit into from
Jul 6, 2018
Merged
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
50 changes: 49 additions & 1 deletion Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 12 additions & 4 deletions pkg/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"path/filepath"
"strings"
"text/template"

k8sutil "github.com/operator-framework/operator-sdk/pkg/util/k8sutil"
)

const (
Expand Down Expand Up @@ -237,8 +239,11 @@ func renderDeployFiles(deployDir, projectName, apiVersion, kind string) error {
// RenderOperatorYaml generates "deploy/operator.yaml"
func RenderOperatorYaml(c *Config, image string) error {
td := tmplData{
ProjectName: c.ProjectName,
Image: image,
ProjectName: c.ProjectName,
Image: image,
MetricsPort: k8sutil.PrometheusMetricsPort,
MetricsPortName: k8sutil.PrometheusMetricsPortName,
OperatorNameEnv: k8sutil.OperatorNameEnvVar,
}
return renderWriteFile(operatorYaml, operatorTmplName, operatorYamlTmpl, td)
}
Expand Down Expand Up @@ -443,8 +448,11 @@ type tmplData struct {
// plural name to be used in the URL: /apis/<group>/<version>/<plural>
KindPlural string

Image string
Name string
Image string
Name string
MetricsPort int
MetricsPortName string
OperatorNameEnv string

PackageName string
ChannelName string
Expand Down
17 changes: 16 additions & 1 deletion pkg/generator/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"strings"
"testing"

k8sutil "github.com/operator-framework/operator-sdk/pkg/util/k8sutil"
"github.com/sergi/go-diff/diffmatchpatch"
)

Expand Down Expand Up @@ -199,6 +200,9 @@ spec:
containers:
- name: app-operator
image: quay.io/example-inc/app-operator:0.0.1
ports:
- containerPort: 60000
name: metrics
command:
- app-operator
imagePullPolicy: Always
Expand All @@ -207,6 +211,8 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: OPERATOR_NAME
value: "app-operator"
`

const rbacYamlExp = `kind: Role
Expand Down Expand Up @@ -276,7 +282,14 @@ func TestGenDeploy(t *testing.T) {
}

buf = &bytes.Buffer{}
if err := renderFile(buf, operatorTmplName, operatorYamlTmpl, tmplData{ProjectName: appProjectName, Image: appImage}); err != nil {
td := tmplData{
ProjectName: appProjectName,
Image: appImage,
MetricsPort: k8sutil.PrometheusMetricsPort,
MetricsPortName: k8sutil.PrometheusMetricsPortName,
OperatorNameEnv: k8sutil.OperatorNameEnvVar,
}
if err := renderFile(buf, operatorTmplName, operatorYamlTmpl, td); err != nil {
t.Error(err)
}
if operatorYamlExp != buf.String() {
Expand Down Expand Up @@ -423,6 +436,8 @@ func printVersion() {
func main() {
printVersion()

sdk.ExposeMetricsPort()

resource := "app.example.com/v1alpha1"
kind := "AppService"
namespace, err := k8sutil.GetWatchNamespace()
Expand Down
8 changes: 8 additions & 0 deletions pkg/generator/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ func printVersion() {
func main() {
printVersion()

sdk.ExposeMetricsPort()

resource := "{{.APIVersion}}"
kind := "{{.Kind}}"
namespace, err := k8sutil.GetWatchNamespace()
Expand Down Expand Up @@ -275,6 +277,7 @@ required = [
branch = "master"
# version = "=v0.0.5"
`

const projectGitignoreTmpl = `
# Temporary Build Files
tmp/_output
Expand Down Expand Up @@ -417,6 +420,9 @@ spec:
containers:
- name: {{.ProjectName}}
image: {{.Image}}
ports:
- containerPort: {{.MetricsPort}}
name: {{.MetricsPortName}}
command:
- {{.ProjectName}}
imagePullPolicy: Always
Expand All @@ -425,6 +431,8 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: {{.OperatorNameEnv}}
value: "{{.ProjectName}}"
`

const rbacYamlTmpl = `kind: Role
Expand Down
28 changes: 28 additions & 0 deletions pkg/sdk/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package sdk

import (
"net/http"
"strconv"

k8sutil "github.com/operator-framework/operator-sdk/pkg/util/k8sutil"
"github.com/prometheus/client_golang/prometheus/promhttp"
Copy link
Contributor

@secat secat Jul 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a suggestion, the prometheus OpenCensus exporter library could be used.

Here is a code snippet to illustrate the usage:

import (
        "github.com/sirupsen/logrus"
        "github.com/prometheus/client_golang/prometheus"
        ocprometheus "go.opencensus.io/exporter/prometheus"
	ocview "go.opencensus.io/stats/view"
)

metricsRegistry := prometheus.NewRegistry()
metricsRegistry.MustRegister(prometheus.NewGoCollector())

prometheusExporter, err := ocprometheus.NewExporter(ocprometheus.Options{
	Namespace: "operator_sdk",
	Registry:  metricsRegistry,
})
if err != nil {
        logrus.Fatalf("Failed to create prometheus exporter: %v", err)
	return
}

ocview.RegisterExporter(prometheusExporter)

http.Handle("/"+k8sutil.PrometheusMetricsPortName, prometheusExporter)

IMHO, this project should integrate and use the OpenCensus library.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@secat I'm not too familiar with the OpenCensus library so I can't comment on the need for it yet. However this PR is only meant to be the starting point for exposing the Prometheus metrics so I would prefer to keep it simple right now and iterate on more changes after this.
It'll be helpful if you can create an issue so we can discuss the use cases for the OpenCensus library in the SDK.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hasbro17 I have created the issue #343

"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/api/errors"
)

// ExposeMetricsPort generate a Kubernetes Service to expose metrics port
func ExposeMetricsPort() {
http.Handle("/"+k8sutil.PrometheusMetricsPortName, promhttp.Handler())
go http.ListenAndServe(":"+strconv.Itoa(k8sutil.PrometheusMetricsPort), nil)

service, err := k8sutil.InitOperatorService()
if err != nil {
logrus.Fatalf("Failed to init operator service: %v", err)
}
err = Create(service)
if err != nil && !errors.IsAlreadyExists(err) {
logrus.Infof("Failed to create operator service: %v", err)
return
}
logrus.Infof("Metrics service %s created", service.Name)
}
10 changes: 10 additions & 0 deletions pkg/util/k8sutil/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,14 @@ const (
// WatchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE
// which is the namespace that the pod is currently running in.
WatchNamespaceEnvVar = "WATCH_NAMESPACE"

// OperatorNameEnvVar is the constant for env variable OPERATOR_NAME
// wich is the name of the current operator
OperatorNameEnvVar = "OPERATOR_NAME"

// PrometheusMetricsPort defines the port which expose prometheus metrics
PrometheusMetricsPort = 60000

// PrometheusMetricsPortName define the port name used in kubernetes deployment and service
PrometheusMetricsPortName = "metrics"
)
52 changes: 52 additions & 0 deletions pkg/util/k8sutil/k8sutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ import (
"fmt"
"os"

v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
intstr "k8s.io/apimachinery/pkg/util/intstr"
cgoscheme "k8s.io/client-go/kubernetes/scheme"
)

Expand Down Expand Up @@ -147,3 +149,53 @@ func GetWatchNamespace() (string, error) {
}
return ns, nil
}

// GetOperatorName return the operator name
func GetOperatorName() (string, error) {
operatorName, found := os.LookupEnv(OperatorNameEnvVar)
if !found {
return "", fmt.Errorf("%s must be set", OperatorNameEnvVar)
}
if len(operatorName) == 0 {
return "", fmt.Errorf("%s must not be empty", OperatorNameEnvVar)
}
return operatorName, nil
}

// InitOperatorService return the static service which expose operator metrics
func InitOperatorService() (*v1.Service, error) {
operatorName, err := GetOperatorName()
if err != nil {
return nil, err
}
namespace, err := GetWatchNamespace()
if err != nil {
return nil, err
}
service := &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: operatorName,
Namespace: namespace,
Labels: map[string]string{"name": operatorName},
},
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "v1",
},
Spec: v1.ServiceSpec{
Ports: []v1.ServicePort{
{
Port: PrometheusMetricsPort,
Protocol: v1.ProtocolTCP,
TargetPort: intstr.IntOrString{
Type: intstr.String,
StrVal: PrometheusMetricsPortName,
},
Name: PrometheusMetricsPortName,
},
},
Selector: map[string]string{"name": operatorName},
},
}
return service, nil
}
61 changes: 61 additions & 0 deletions pkg/util/k8sutil/k8sutil_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package k8sutil

import (
"fmt"
"os"
"reflect"
"testing"
)

func TestGetOperatorName(t *testing.T) {
type Output struct {
operatorName string
err error
}

type Scenario struct {
name string
envVarKey string
envVarValue string
expectedOutput Output
}

tests := []Scenario{
Scenario{
name: "Simple case",
envVarKey: OperatorNameEnvVar,
envVarValue: "myoperator",
expectedOutput: Output{
operatorName: "myoperator",
err: nil,
},
},
Scenario{
name: "Unset env var",
envVarKey: "",
envVarValue: "",
expectedOutput: Output{
operatorName: "",
err: fmt.Errorf("%s must be set", OperatorNameEnvVar),
},
},
Scenario{
name: "Empty env var",
envVarKey: OperatorNameEnvVar,
envVarValue: "",
expectedOutput: Output{
operatorName: "",
err: fmt.Errorf("%s must not be empty", OperatorNameEnvVar),
},
},
}

for _, test := range tests {
_ = os.Setenv(test.envVarKey, test.envVarValue)
operatorName, err := GetOperatorName()
if !(operatorName == test.expectedOutput.operatorName && reflect.DeepEqual(err, test.expectedOutput.err)) {
t.Errorf("test %s failed, expected ouput: %s,%v; got: %s,%v", test.name, test.expectedOutput.operatorName, test.expectedOutput.err, operatorName, err)
}
_ = os.Unsetenv(test.envVarKey)
}
}