From b549f06db4ff440b2fa73c4d67d15152f7898b6a Mon Sep 17 00:00:00 2001 From: DTPKGOUJI2020 Date: Tue, 9 Mar 2021 16:21:38 +0900 Subject: [PATCH] add source --- Dockerfile | 12 +++ main.go | 237 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 Dockerfile create mode 100644 main.go diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..46e9e15 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:alpine as builder +RUN apk update && apk add git && apk add ca-certificates +COPY *.go $GOPATH/src/mypackage/myapp/ +WORKDIR $GOPATH/src/mypackage/myapp/ +RUN go mod init && go mod tidy +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o /go/bin/docker_state_exporter + +FROM alpine:3 +COPY --from=builder /go/bin/docker_state_exporter /go/bin/docker_state_exporter +EXPOSE 8080 +ENTRYPOINT ["/go/bin/docker_state_exporter"] +CMD ["-listen-address=:8080"] \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..ab31346 --- /dev/null +++ b/main.go @@ -0,0 +1,237 @@ +package main + +import ( + "context" + "flag" + "fmt" + "net/http" + "os" + "os/signal" + "regexp" + "strings" + "sync" + "syscall" + "time" + + "github.com/docker/docker/api/types" + tcontainer "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" + "github.com/go-kit/kit/log" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +const ( + // cachePeriod indicates the period of time the collector will reuse the results of docker inspect. + cachePeriod = 1 * time.Second +) + +type dockerHealthCollector struct { + mu sync.Mutex + containerClient *client.Client + containerInfoCache []types.ContainerJSON + lastseen time.Time +} + +type descSource struct { + name string + help string +} + +func (desc *descSource) Desc(labels prometheus.Labels) *prometheus.Desc { + return prometheus.NewDesc(desc.name, desc.help, nil, labels) +} + +var ( + namespace = "container_state_" + healthStatusDesc = descSource{ + namespace + "health_status", + "Container health status."} + statusDesc = descSource{ + namespace + "status", + "Container status."} + oomkilledDesc = descSource{ + namespace + "oomkilled", + "Container was killed by OOMKiller."} + startedatDesc = descSource{ + namespace + "startedat", + "Time when the Container started."} + finishedatDesc = descSource{ + namespace + "finishedat", + "Time when the Container finished."} + restartcountDesc = descSource{ + "container_restartcount", + "Number of times the container has been restarted"} +) + +func (c *dockerHealthCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- healthStatusDesc.Desc(nil) + ch <- statusDesc.Desc(nil) + ch <- oomkilledDesc.Desc(nil) + ch <- startedatDesc.Desc(nil) + ch <- finishedatDesc.Desc(nil) + ch <- restartcountDesc.Desc(nil) +} + +func (c *dockerHealthCollector) Collect(ch chan<- prometheus.Metric) { + c.mu.Lock() + defer c.mu.Unlock() + now := time.Now() + if now.Sub(c.lastseen) >= cachePeriod { + c.collectContainer() + c.lastseen = now + } + c.collectMetrics(ch) +} + +func (c *dockerHealthCollector) collectMetrics(ch chan<- prometheus.Metric) { + for _, info := range c.containerInfoCache { + var labels = map[string]string{} + + rep := regexp.MustCompile("[^a-zA-Z0-9_]") + + for k, v := range info.Config.Labels { + label := strings.ToLower("container_label_" + k) + labels[rep.ReplaceAllLiteralString(label, "_")] = v + } + labels["id"] = "/docker/" + info.ID + labels["image"] = info.Config.Image + labels["name"] = strings.TrimPrefix(info.Name, "/") + + b2f := func(b bool) float64 { + if b { + return 1 + } + return 0 + } + mapcopy := func(src map[string]string) prometheus.Labels { + dst := map[string]string{} + for k, v := range labels { + dst[k] = v + } + return dst + } + + for _, lv := range []string{"none", "starting", "healthy", "unhealthy"} { + tmpLabels := mapcopy(labels) + tmpLabels["status"] = lv + ch <- prometheus.MustNewConstMetric(healthStatusDesc.Desc(tmpLabels), prometheus.GaugeValue, b2f(info.State.Health.Status == lv)) + } + for _, lv := range []string{"paused", "restarting", "running", "removing", "dead", "created", "exited"} { + tmpLabels := mapcopy(labels) + tmpLabels["status"] = lv + ch <- prometheus.MustNewConstMetric(statusDesc.Desc(tmpLabels), prometheus.GaugeValue, b2f(info.State.Status == lv)) + } + ch <- prometheus.MustNewConstMetric(oomkilledDesc.Desc(labels), prometheus.GaugeValue, b2f(info.State.OOMKilled)) + startedat, err := time.Parse(time.RFC3339Nano, info.State.StartedAt) + errCheck(err) + finishedat, err := time.Parse(time.RFC3339Nano, info.State.FinishedAt) + errCheck(err) + ch <- prometheus.MustNewConstMetric(startedatDesc.Desc(labels), prometheus.GaugeValue, float64(startedat.Unix())) + ch <- prometheus.MustNewConstMetric(finishedatDesc.Desc(labels), prometheus.GaugeValue, float64(finishedat.Unix())) + ch <- prometheus.MustNewConstMetric(restartcountDesc.Desc(labels), prometheus.GaugeValue, float64(info.RestartCount)) + } +} +func (c *dockerHealthCollector) collectContainer() { + containers, err := c.containerClient.ContainerList(context.Background(), types.ContainerListOptions{}) + errCheck(err) + c.containerInfoCache = []types.ContainerJSON{} + + for _, container := range containers { + info, err := c.containerClient.ContainerInspect(context.Background(), container.ID) + errCheck(err) + c.containerInfoCache = append(c.containerInfoCache, info) + + if info.Config == nil { + info.Config = &tcontainer.Config{Labels: map[string]string{}} + } + + if info.State.Health == nil { + info.State.Health = &types.Health{Status: "none"} + } + } +} + +type loggerWrapper struct { + Logger *log.Logger +} + +func (l *loggerWrapper) Println(v ...interface{}) { + (*l.Logger).Log("messages", v) +} + +// Define loggers. +var ( + normalLogger = log.NewJSONLogger(log.NewSyncWriter(os.Stdout)) + errorLogger = log.NewJSONLogger(log.NewSyncWriter(os.Stderr)) +) + +func errCheck(err error) { + if err != nil { + errorLogger.Log("message", err) + os.Exit(1) + } +} + +// Define flags. +var ( + address = flag.String("listen-address", ":8080", "The address to listen on for HTTP requests.") +) + +func init() { + normalLogger = log.With(normalLogger, "timestamp", log.DefaultTimestampUTC) + normalLogger = log.With(normalLogger, "severity", "info") + errorLogger = log.With(errorLogger, "timestamp", log.DefaultTimestampUTC) + errorLogger = log.With(errorLogger, "severity", "error") + prometheus.MustRegister(prometheus.NewBuildInfoCollector()) +} + +func main() { + flag.Parse() + + client, err := client.NewEnvClient() + errCheck(err) + defer client.Close() + + _, err = client.Ping(context.Background()) + errCheck(err) + + prometheus.MustRegister(&dockerHealthCollector{ + containerClient: client, + }) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "

docker state exporter

") + }) + + http.HandleFunc("/-/healthy", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "up") + }) + + http.Handle("/metrics", promhttp.HandlerFor( + prometheus.DefaultGatherer, + promhttp.HandlerOpts{ErrorLog: &loggerWrapper{Logger: &errorLogger}, EnableOpenMetrics: true})) + + normalLogger.Log("message", "Server listening...", "address", address) + + server := &http.Server{Addr: *address, Handler: nil} + + go func() { + err = server.ListenAndServe() + if err != http.ErrServerClosed { + errCheck(err) + } + }() + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGTERM, os.Interrupt) + <-quit + normalLogger.Log("message", "Server shutting down...") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := server.Shutdown(ctx); err != nil { + errorLogger.Log("message", fmt.Sprintf("Failed to gracefully shutdown: %v", err)) + } + normalLogger.Log("message", "Server shutdown") +}