Initial commit
Signed-off-by: Michael Hanselmann <public@hansmi.ch>
This commit is contained in:
commit
f2cc188833
29 changed files with 1776 additions and 0 deletions
14
.github/dependabot.yml
vendored
Normal file
14
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
|
||||
# vim: set sw=2 sts=2 et :
|
19
.github/workflows/ci.yaml
vendored
Normal file
19
.github/workflows/ci.yaml
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
name: Run tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
schedule:
|
||||
- cron: '13 4 */9 * *'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: hansmi/ghactions-go-test-workflow/.github/workflows/test.yaml@stable
|
||||
with:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# vim: set sw=2 sts=2 et :
|
17
.github/workflows/release.yaml
vendored
Normal file
17
.github/workflows/release.yaml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: hansmi/ghactions-goreleaser-workflow/.github/workflows/release.yaml@stable
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
# vim: set sw=2 sts=2 et :
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/dist/
|
||||
/prometheus-paperless-exporter
|
73
.goreleaser.yml
Normal file
73
.goreleaser.yml
Normal file
|
@ -0,0 +1,73 @@
|
|||
# Configuration for GoReleaser
|
||||
# https://goreleaser.com/
|
||||
#
|
||||
# Local test: contrib/build-all
|
||||
#
|
||||
|
||||
project_name: prometheus-paperless-exporter
|
||||
|
||||
builds:
|
||||
- main: .
|
||||
binary: prometheus-paperless-exporter
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
targets:
|
||||
- go_first_class
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags: |
|
||||
-s -w
|
||||
-X github.com/prometheus/common/version.Version={{.Version}}
|
||||
-X github.com/prometheus/common/version.Revision={{.FullCommit}}
|
||||
-X github.com/prometheus/common/version.Branch={{.Branch}}
|
||||
-X github.com/prometheus/common/version.BuildDate={{.Date}}
|
||||
|
||||
nfpms:
|
||||
- description: Prometheus metrics for Paperless-ngx
|
||||
maintainer: M. Hanselmann
|
||||
bindir: /usr/bin
|
||||
license: BSD-3-Clause
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
contents:
|
||||
- src: ./README.md
|
||||
dst: /usr/share/doc/prometheus-paperless-exporter/README.md
|
||||
- src: ./LICENSE
|
||||
dst: /usr/share/doc/prometheus-paperless-exporter/LICENSE
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
wrap_in_directory: true
|
||||
files:
|
||||
- LICENSE
|
||||
- README.md
|
||||
|
||||
dockers:
|
||||
- ids:
|
||||
- prometheus-paperless-exporter
|
||||
use: buildx
|
||||
dockerfile: contrib/Dockerfile.goreleaser
|
||||
extra_files:
|
||||
- LICENSE
|
||||
- README.md
|
||||
image_templates:
|
||||
- ghcr.io/hansmi/prometheus-paperless-exporter:{{.Tag}}
|
||||
- ghcr.io/hansmi/prometheus-paperless-exporter:v{{.Major}}
|
||||
- ghcr.io/hansmi/prometheus-paperless-exporter:latest
|
||||
build_flag_templates:
|
||||
- --pull
|
||||
- --label=org.opencontainers.image.created={{.Date}}
|
||||
- --label=org.opencontainers.image.name={{.ProjectName}}
|
||||
- --label=org.opencontainers.image.revision={{.FullCommit}}
|
||||
- --label=org.opencontainers.image.version={{.Version}}
|
||||
- --label=org.opencontainers.image.source={{.GitURL}}
|
||||
|
||||
release:
|
||||
draft: true
|
||||
prerelease: auto
|
||||
|
||||
snapshot:
|
||||
name_template: '{{ incpatch .Version }}-snapshot{{ replace (replace .Date ":" "") "-" "" }}+g{{ .ShortCommit }}'
|
||||
|
||||
# vim: set sw=2 sts=2 et :
|
27
LICENSE
Normal file
27
LICENSE
Normal file
|
@ -0,0 +1,27 @@
|
|||
Copyright (c) 2023 Michael Hanselmann. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
57
README.md
Normal file
57
README.md
Normal file
|
@ -0,0 +1,57 @@
|
|||
# Paperless-ngx metrics for Prometheus
|
||||
|
||||
[![Latest release](https://img.shields.io/github/v/release/hansmi/prometheus-paperless-exporter)][releases]
|
||||
[![Release workflow](https://github.com/hansmi/prometheus-paperless-exporter/actions/workflows/release.yaml/badge.svg)](https://github.com/hansmi/prometheus-paperless-exporter/actions/workflows/release.yaml)
|
||||
[![CI workflow](https://github.com/hansmi/prometheus-paperless-exporter/actions/workflows/ci.yaml/badge.svg)](https://github.com/hansmi/prometheus-paperless-exporter/actions/workflows/ci.yaml)
|
||||
[![Go reference](https://pkg.go.dev/badge/github.com/hansmi/prometheus-paperless-exporter.svg)](https://pkg.go.dev/github.com/hansmi/prometheus-paperless-exporter)
|
||||
|
||||
This repository hosts a Prometheus metrics exporter for
|
||||
[Paperless-ngx][paperless], a document management system transforming physical
|
||||
documents into a searchable online archive. The exporter relies on [Paperless'
|
||||
REST API][paperless-api].
|
||||
|
||||
An implementation using the API was chosen to provide the same perspective as
|
||||
web browsers.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
`prometheus-paperless-exporter` listens on TCP port 8081 by default. To listen on
|
||||
another address use the `-web.listen-address` flag (e.g.
|
||||
`-web.listen-address=127.0.0.1:3000`).
|
||||
|
||||
TLS and HTTP basic authentication is supported through the [Prometheus exporter
|
||||
toolkit][toolkit]. A configuration file can be passed to the `-web.config` flag
|
||||
([documentation][toolkitconfig]).
|
||||
|
||||
See the `--help` output for more flags.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Pre-built binaries are provided for [all releases][releases]:
|
||||
|
||||
* Binary archives (`.tar.gz`)
|
||||
* Debian/Ubuntu (`.deb`)
|
||||
* RHEL/Fedora (`.rpm`)
|
||||
* Microsoft Windows (`.zip`)
|
||||
|
||||
Docker images via GitHub's container registry:
|
||||
|
||||
```shell
|
||||
docker pull ghcr.io/hansmi/prometheus-paperless-exporter
|
||||
```
|
||||
|
||||
With the source being available it's also possible to produce custom builds
|
||||
directly using [Go][golang] or [GoReleaser][goreleaser].
|
||||
|
||||
|
||||
[golang]: https://golang.org/
|
||||
[goreleaser]: https://goreleaser.com/
|
||||
[paperless-api]: https://docs.paperless-ngx.com/api/
|
||||
[paperless]: https://docs.paperless-ngx.com/
|
||||
[releases]: https://github.com/hansmi/prometheus-paperless-exporter/releases/latest
|
||||
[toolkit]: https://github.com/prometheus/exporter-toolkit
|
||||
[toolkitconfig]: https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md
|
||||
|
||||
<!-- vim: set sw=2 sts=2 et : -->
|
22
collector.go
Normal file
22
collector.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/hansmi/paperhooks/pkg/client"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func newCollector(cl *client.Client, timeout time.Duration) prometheus.Collector {
|
||||
return &multiCollector{
|
||||
timeout: timeout,
|
||||
members: []multiCollectorMember{
|
||||
newTagCollector(cl),
|
||||
newCorrespondentCollector(cl),
|
||||
newDocumentTypeCollector(cl),
|
||||
newStoragePathCollector(cl),
|
||||
newTaskCollector(cl),
|
||||
newLogCollector(cl),
|
||||
},
|
||||
}
|
||||
}
|
31
collector_test.go
Normal file
31
collector_test.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hansmi/paperhooks/pkg/client"
|
||||
"github.com/hansmi/prometheus-paperless-exporter/internal/testutil"
|
||||
)
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
io.WriteString(w, "{}")
|
||||
}))
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
cl := client.New(client.Options{
|
||||
BaseURL: ts.URL,
|
||||
})
|
||||
|
||||
c := newCollector(cl, time.Minute)
|
||||
|
||||
testutil.CollectAndCompare(t, c, `
|
||||
# HELP paperless_task_status_info Task status names.
|
||||
# TYPE paperless_task_status_info gauge
|
||||
paperless_task_status_info{status="success"} 1
|
||||
`)
|
||||
}
|
15
contrib/Dockerfile.goreleaser
Normal file
15
contrib/Dockerfile.goreleaser
Normal file
|
@ -0,0 +1,15 @@
|
|||
FROM docker.io/library/alpine:latest
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
|
||||
LABEL org.opencontainers.image.licenses=BSD-3-Clause
|
||||
LABEL org.opencontainers.image.description="Prometheus metrics for Paperless-ngx"
|
||||
|
||||
WORKDIR /
|
||||
|
||||
COPY LICENSE README.md /
|
||||
COPY prometheus-paperless-exporter /
|
||||
|
||||
ENTRYPOINT ["/prometheus-paperless-exporter"]
|
||||
|
||||
# vim: set ft=dockerfile :
|
16
contrib/build-all
Executable file
16
contrib/build-all
Executable file
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e -u -o pipefail
|
||||
|
||||
package=github.com/hansmi/prometheus-paperless-exporter
|
||||
docker_gid=$(getent group docker | cut -d: -f3)
|
||||
|
||||
docker run --rm \
|
||||
--user "$(id -u):$(id -g)" --group-add="$docker_gid" \
|
||||
--env HOME=/tmp \
|
||||
-v "${PWD}:/go/src/${package}" \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-w "/go/src/${package}" \
|
||||
goreleaser/goreleaser:latest release --snapshot --clean --skip-publish
|
||||
|
||||
# vim: set sw=2 sts=2 et :
|
66
correspondent.go
Normal file
66
correspondent.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/hansmi/paperhooks/pkg/client"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type correspondentClient interface {
|
||||
ListAllCorrespondents(context.Context, *client.ListCorrespondentsOptions, func(context.Context, client.Correspondent) error) error
|
||||
}
|
||||
|
||||
type correspondentCollector struct {
|
||||
cl correspondentClient
|
||||
|
||||
infoDesc *prometheus.Desc
|
||||
docCountDesc *prometheus.Desc
|
||||
lastCorrespondenceDesc *prometheus.Desc
|
||||
}
|
||||
|
||||
func newCorrespondentCollector(cl correspondentClient) *correspondentCollector {
|
||||
return &correspondentCollector{
|
||||
cl: cl,
|
||||
|
||||
infoDesc: prometheus.NewDesc("paperless_correspondent_info",
|
||||
"Static information about a correspondent.",
|
||||
[]string{"id", "name", "slug"}, nil),
|
||||
docCountDesc: prometheus.NewDesc("paperless_correspondent_document_count",
|
||||
"Number of documents associated with a correspondent.",
|
||||
[]string{"id"}, nil),
|
||||
lastCorrespondenceDesc: prometheus.NewDesc("paperless_correspondent_last_correspondence_timestamp_seconds",
|
||||
"Number of seconds since 1970 of the most recent correspondence.",
|
||||
[]string{"id"}, nil),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *correspondentCollector) describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- c.infoDesc
|
||||
ch <- c.docCountDesc
|
||||
ch <- c.lastCorrespondenceDesc
|
||||
}
|
||||
|
||||
func (c *correspondentCollector) collect(ctx context.Context, ch chan<- prometheus.Metric) error {
|
||||
opts := &client.ListCorrespondentsOptions{}
|
||||
opts.Ordering.Field = "name"
|
||||
|
||||
return c.cl.ListAllCorrespondents(ctx, opts, func(_ context.Context, correspondent client.Correspondent) error {
|
||||
id := strconv.FormatInt(correspondent.ID, 10)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(c.infoDesc, prometheus.GaugeValue, 1,
|
||||
id,
|
||||
correspondent.Name,
|
||||
correspondent.Slug,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(c.docCountDesc, prometheus.GaugeValue,
|
||||
float64(correspondent.DocumentCount), id)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(c.lastCorrespondenceDesc, prometheus.GaugeValue,
|
||||
optionalTimestamp(correspondent.LastCorrespondence), id)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
107
correspondent_test.go
Normal file
107
correspondent_test.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/hansmi/paperhooks/pkg/client"
|
||||
"github.com/hansmi/prometheus-paperless-exporter/internal/ref"
|
||||
"github.com/hansmi/prometheus-paperless-exporter/internal/testutil"
|
||||
)
|
||||
|
||||
type fakeCorrespondentClient struct {
|
||||
items []client.Correspondent
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *fakeCorrespondentClient) ListAllCorrespondents(ctx context.Context, opts *client.ListCorrespondentsOptions, handler func(context.Context, client.Correspondent) error) error {
|
||||
for _, i := range c.items {
|
||||
if err := handler(ctx, i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.err
|
||||
}
|
||||
|
||||
func TestCorrespondent(t *testing.T) {
|
||||
errTest := errors.New("test error")
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
cl fakeCorrespondentClient
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
name: "listing fails",
|
||||
cl: fakeCorrespondentClient{
|
||||
err: errTest,
|
||||
},
|
||||
wantErr: errTest,
|
||||
},
|
||||
{
|
||||
name: "correspondents",
|
||||
cl: fakeCorrespondentClient{
|
||||
items: []client.Correspondent{
|
||||
{ID: 21383},
|
||||
{ID: 3096},
|
||||
{ID: 22044},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := newCorrespondentCollector(&tc.cl)
|
||||
|
||||
err := c.collect(context.Background(), testutil.DiscardMetrics(t))
|
||||
|
||||
if diff := cmp.Diff(tc.wantErr, err, cmpopts.EquateErrors()); diff != "" {
|
||||
t.Errorf("Error diff (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorrespondentCollect(t *testing.T) {
|
||||
cl := fakeCorrespondentClient{}
|
||||
|
||||
c := newMultiCollector(newCorrespondentCollector(&cl))
|
||||
|
||||
testutil.CollectAndCompare(t, c, ``)
|
||||
|
||||
cl.items = append(cl.items, []client.Correspondent{
|
||||
{ID: 15818, Name: "bank", Slug: "aslug"},
|
||||
{
|
||||
ID: 167,
|
||||
Name: "insurance",
|
||||
DocumentCount: 2,
|
||||
LastCorrespondence: ref.Ref(time.Date(2019, time.July, 1, 0, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
{ID: 24467, Name: "employer", DocumentCount: 121},
|
||||
}...)
|
||||
|
||||
testutil.CollectAndCompare(t, c, `
|
||||
# HELP paperless_correspondent_document_count Number of documents associated with a correspondent.
|
||||
# TYPE paperless_correspondent_document_count gauge
|
||||
paperless_correspondent_document_count{id="15818"} 0
|
||||
paperless_correspondent_document_count{id="167"} 2
|
||||
paperless_correspondent_document_count{id="24467"} 121
|
||||
# HELP paperless_correspondent_info Static information about a correspondent.
|
||||
# TYPE paperless_correspondent_info gauge
|
||||
paperless_correspondent_info{id="15818",name="bank",slug="aslug"} 1
|
||||
paperless_correspondent_info{id="167",name="insurance",slug=""} 1
|
||||
paperless_correspondent_info{id="24467",name="employer",slug=""} 1
|
||||
# HELP paperless_correspondent_last_correspondence_timestamp_seconds Number of seconds since 1970 of the most recent correspondence.
|
||||
# TYPE paperless_correspondent_last_correspondence_timestamp_seconds gauge
|
||||
paperless_correspondent_last_correspondence_timestamp_seconds{id="15818"} 0
|
||||
paperless_correspondent_last_correspondence_timestamp_seconds{id="167"} 1.5619392e+09
|
||||
paperless_correspondent_last_correspondence_timestamp_seconds{id="24467"} 0
|
||||
`)
|
||||
}
|
58
documenttype.go
Normal file
58
documenttype.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/hansmi/paperhooks/pkg/client"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type documentTypeClient interface {
|
||||
ListAllDocumentTypes(context.Context, *client.ListDocumentTypesOptions, func(context.Context, client.DocumentType) error) error
|
||||
}
|
||||
|
||||
type documentTypeCollector struct {
|
||||
cl documentTypeClient
|
||||
|
||||
infoDesc *prometheus.Desc
|
||||
docCountDesc *prometheus.Desc
|
||||
}
|
||||
|
||||
func newDocumentTypeCollector(cl documentTypeClient) *documentTypeCollector {
|
||||
return &documentTypeCollector{
|
||||
cl: cl,
|
||||
|
||||
infoDesc: prometheus.NewDesc("paperless_document_type_info",
|
||||
"Static information about a document type.",
|
||||
[]string{"id", "name", "slug"}, nil),
|
||||
docCountDesc: prometheus.NewDesc("paperless_document_type_document_count",
|
||||
"Number of documents associated with a document type.",
|
||||
[]string{"id"}, nil),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *documentTypeCollector) describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- c.infoDesc
|
||||
ch <- c.docCountDesc
|
||||
}
|
||||
|
||||
func (c *documentTypeCollector) collect(ctx context.Context, ch chan<- prometheus.Metric) error {
|
||||
opts := &client.ListDocumentTypesOptions{}
|
||||
opts.Ordering.Field = "name"
|
||||
|
||||
return c.cl.ListAllDocumentTypes(ctx, opts, func(_ context.Context, doctype client.DocumentType) error {
|
||||
id := strconv.FormatInt(doctype.ID, 10)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(c.infoDesc, prometheus.GaugeValue, 1,
|
||||
id,
|
||||
doctype.Name,
|
||||
doctype.Slug,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(c.docCountDesc, prometheus.GaugeValue,
|
||||
float64(doctype.DocumentCount), id)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
92
documenttype_test.go
Normal file
92
documenttype_test.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/hansmi/paperhooks/pkg/client"
|
||||
"github.com/hansmi/prometheus-paperless-exporter/internal/testutil"
|
||||
)
|
||||
|
||||
type fakeDocumentTypeClient struct {
|
||||
items []client.DocumentType
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *fakeDocumentTypeClient) ListAllDocumentTypes(ctx context.Context, opts *client.ListDocumentTypesOptions, handler func(context.Context, client.DocumentType) error) error {
|
||||
for _, i := range c.items {
|
||||
if err := handler(ctx, i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.err
|
||||
}
|
||||
|
||||
func TestDocumentType(t *testing.T) {
|
||||
errTest := errors.New("test error")
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
cl fakeDocumentTypeClient
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
name: "listing fails",
|
||||
cl: fakeDocumentTypeClient{
|
||||
err: errTest,
|
||||
},
|
||||
wantErr: errTest,
|
||||
},
|
||||
{
|
||||
name: "documentTypes",
|
||||
cl: fakeDocumentTypeClient{
|
||||
items: []client.DocumentType{
|
||||
{ID: 23758},
|
||||
{ID: 22848},
|
||||
{ID: 10504},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := newDocumentTypeCollector(&tc.cl)
|
||||
|
||||
err := c.collect(context.Background(), testutil.DiscardMetrics(t))
|
||||
|
||||
if diff := cmp.Diff(tc.wantErr, err, cmpopts.EquateErrors()); diff != "" {
|
||||
t.Errorf("Error diff (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentTypeCollect(t *testing.T) {
|
||||
cl := fakeDocumentTypeClient{}
|
||||
|
||||
c := newMultiCollector(newDocumentTypeCollector(&cl))
|
||||
|
||||
testutil.CollectAndCompare(t, c, ``)
|
||||
|
||||
cl.items = append(cl.items, []client.DocumentType{
|
||||
{ID: 3760, Name: "Contract", Slug: "contract"},
|
||||
{ID: 5558, Name: "Purchase order", Slug: "po", DocumentCount: 20},
|
||||
}...)
|
||||
|
||||
testutil.CollectAndCompare(t, c, `
|
||||
# HELP paperless_document_type_document_count Number of documents associated with a document type.
|
||||
# TYPE paperless_document_type_document_count gauge
|
||||
paperless_document_type_document_count{id="3760"} 0
|
||||
paperless_document_type_document_count{id="5558"} 20
|
||||
# HELP paperless_document_type_info Static information about a document type.
|
||||
# TYPE paperless_document_type_info gauge
|
||||
paperless_document_type_info{id="3760",name="Contract",slug="contract"} 1
|
||||
paperless_document_type_info{id="5558",name="Purchase order",slug="po"} 1
|
||||
`)
|
||||
}
|
42
go.mod
Normal file
42
go.mod
Normal file
|
@ -0,0 +1,42 @@
|
|||
module github.com/hansmi/prometheus-paperless-exporter
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/alecthomas/kingpin/v2 v2.3.2
|
||||
github.com/go-kit/log v0.2.1
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/hansmi/paperhooks v0.0.3
|
||||
github.com/prometheus/client_golang v1.16.0
|
||||
github.com/prometheus/common v0.44.0
|
||||
github.com/prometheus/exporter-toolkit v0.10.0
|
||||
golang.org/x/sync v0.3.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/go-resty/resty/v2 v2.7.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/jpillora/backoff v1.0.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
||||
github.com/prometheus/client_model v0.4.0 // indirect
|
||||
github.com/prometheus/procfs v0.11.0 // indirect
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.10.0 // indirect
|
||||
golang.org/x/net v0.11.0 // indirect
|
||||
golang.org/x/oauth2 v0.9.0 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
golang.org/x/text v0.10.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
101
go.sum
Normal file
101
go.sum
Normal file
|
@ -0,0 +1,101 @@
|
|||
github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU=
|
||||
github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
|
||||
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
|
||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/hansmi/paperhooks v0.0.3 h1:0RgPHZaEG790kjtA9iYks+4pxLOkq4UHf2oNquA8aXk=
|
||||
github.com/hansmi/paperhooks v0.0.3/go.mod h1:OQVL9560CVYa3lLVYPQ0WrRzGMs9ibyY3HSPrLRXEz4=
|
||||
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
|
||||
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
|
||||
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
||||
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
|
||||
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/exporter-toolkit v0.10.0 h1:yOAzZTi4M22ZzVxD+fhy1URTuNRj/36uQJJ5S8IPza8=
|
||||
github.com/prometheus/exporter-toolkit v0.10.0/go.mod h1:+sVFzuvV5JDyw+Ih6p3zFxZNVnKQa3x5qPmDSiPu4ZY=
|
||||
github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk=
|
||||
github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs=
|
||||
golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
8
internal/ref/ref.go
Normal file
8
internal/ref/ref.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package ref
|
||||
|
||||
// Return a pointer to a value.
|
||||
//
|
||||
// https://github.com/golang/go/issues/45624
|
||||
func Ref[T any](x T) *T {
|
||||
return &x
|
||||
}
|
34
internal/testutil/metrics.go
Normal file
34
internal/testutil/metrics.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package testutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
)
|
||||
|
||||
func CollectAndCompare(t *testing.T, c prometheus.Collector, want string, metricNames ...string) {
|
||||
t.Helper()
|
||||
|
||||
if err := testutil.CollectAndCompare(c, strings.NewReader(want), metricNames...); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func DiscardMetrics(t *testing.T) chan<- prometheus.Metric {
|
||||
t.Helper()
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
|
||||
t.Cleanup(func() {
|
||||
close(ch)
|
||||
})
|
||||
|
||||
go func() {
|
||||
for range ch {
|
||||
}
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
147
log.go
Normal file
147
log.go
Normal file
|
@ -0,0 +1,147 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hansmi/paperhooks/pkg/client"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type logClient interface {
|
||||
ListLogs(context.Context) ([]string, *client.Response, error)
|
||||
GetLog(context.Context, string) ([]client.LogEntry, *client.Response, error)
|
||||
}
|
||||
|
||||
type logPosition struct {
|
||||
valid bool
|
||||
time time.Time
|
||||
module string
|
||||
level string
|
||||
}
|
||||
|
||||
func newLogPosition(e client.LogEntry) logPosition {
|
||||
return logPosition{
|
||||
valid: true,
|
||||
time: e.Time,
|
||||
module: e.Module,
|
||||
level: e.Level,
|
||||
}
|
||||
}
|
||||
|
||||
func (p logPosition) equal(e client.LogEntry) bool {
|
||||
return p.valid && e.Time.Equal(p.time) && e.Module == p.module && e.Level == p.level
|
||||
}
|
||||
|
||||
type logCollector struct {
|
||||
cl logClient
|
||||
|
||||
mu sync.Mutex
|
||||
|
||||
seen map[string]logPosition
|
||||
totalVec *prometheus.CounterVec
|
||||
}
|
||||
|
||||
func newLogCollector(cl logClient) *logCollector {
|
||||
return &logCollector{
|
||||
cl: cl,
|
||||
|
||||
seen: map[string]logPosition{},
|
||||
totalVec: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "paperless_log_entries_total",
|
||||
Help: `Best-effort count of log entries.`,
|
||||
}, []string{"name", "module", "level"}),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *logCollector) describe(ch chan<- *prometheus.Desc) {
|
||||
c.totalVec.Describe(ch)
|
||||
}
|
||||
|
||||
func (c *logCollector) collectOne(ctx context.Context, name string) error {
|
||||
entries, _, err := c.cl.GetLog(ctx, name)
|
||||
if err != nil {
|
||||
var reqErr *client.RequestError
|
||||
|
||||
if errors.As(err, &reqErr) && reqErr.StatusCode == http.StatusNotFound {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
entryLabels := func(e client.LogEntry) prometheus.Labels {
|
||||
return prometheus.Labels{
|
||||
"name": name,
|
||||
"module": e.Module,
|
||||
"level": strings.ToLower(e.Level),
|
||||
}
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
start := 0
|
||||
|
||||
if seen := c.seen[name]; seen.valid {
|
||||
// Find the first log entry which hasn't been seen previously.
|
||||
for idx, entry := range entries {
|
||||
if seen.equal(entry) {
|
||||
start = idx + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, entry := range entries[start:] {
|
||||
c.totalVec.With(entryLabels(entry)).Inc()
|
||||
}
|
||||
|
||||
newest := entries[len(entries)-1]
|
||||
newest.Message = ""
|
||||
|
||||
c.seen[name] = newLogPosition(newest)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *logCollector) collect(ctx context.Context, ch chan<- prometheus.Metric) error {
|
||||
names, _, err := c.cl.ListLogs(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing log names: %w", err)
|
||||
}
|
||||
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
g.SetLimit(runtime.GOMAXPROCS(0))
|
||||
|
||||
for _, name := range names {
|
||||
name := name
|
||||
|
||||
g.Go(func() error {
|
||||
if err := c.collectOne(ctx, name); err != nil {
|
||||
return fmt.Errorf("log %s: %w", name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.totalVec.Collect(ch)
|
||||
|
||||
return nil
|
||||
}
|
171
log_test.go
Normal file
171
log_test.go
Normal file
|
@ -0,0 +1,171 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/hansmi/paperhooks/pkg/client"
|
||||
"github.com/hansmi/prometheus-paperless-exporter/internal/testutil"
|
||||
)
|
||||
|
||||
type fakeLogClient struct {
|
||||
names []string
|
||||
entries map[string][]client.LogEntry
|
||||
|
||||
listErr error
|
||||
getErr error
|
||||
}
|
||||
|
||||
func (c *fakeLogClient) addEntries(name string, e []client.LogEntry) {
|
||||
if c.entries == nil {
|
||||
c.entries = map[string][]client.LogEntry{}
|
||||
}
|
||||
|
||||
c.entries[name] = append(c.entries[name], e...)
|
||||
}
|
||||
|
||||
func (c *fakeLogClient) ListLogs(context.Context) ([]string, *client.Response, error) {
|
||||
return c.names, nil, c.listErr
|
||||
}
|
||||
|
||||
func (c *fakeLogClient) GetLog(_ context.Context, name string) ([]client.LogEntry, *client.Response, error) {
|
||||
if c.getErr != nil {
|
||||
return nil, nil, c.getErr
|
||||
}
|
||||
|
||||
entries, ok := c.entries[name]
|
||||
if !ok {
|
||||
return nil, nil, &client.RequestError{StatusCode: http.StatusNotFound}
|
||||
}
|
||||
|
||||
return entries, nil, nil
|
||||
}
|
||||
|
||||
func TestLog(t *testing.T) {
|
||||
errTest := errors.New("test error")
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
cl fakeLogClient
|
||||
wantErr error
|
||||
}{
|
||||
{name: "empty"},
|
||||
{
|
||||
name: "listing fails",
|
||||
cl: fakeLogClient{
|
||||
listErr: errTest,
|
||||
},
|
||||
wantErr: errTest,
|
||||
},
|
||||
{
|
||||
name: "get fails",
|
||||
cl: fakeLogClient{
|
||||
names: []string{"foo", "bar"},
|
||||
getErr: errTest,
|
||||
},
|
||||
wantErr: errTest,
|
||||
},
|
||||
{
|
||||
name: "empty logs",
|
||||
cl: fakeLogClient{
|
||||
names: []string{"first", "second", "error404"},
|
||||
entries: map[string][]client.LogEntry{
|
||||
"first": nil,
|
||||
"second": nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "entries",
|
||||
cl: fakeLogClient{
|
||||
names: []string{"first", "second"},
|
||||
entries: map[string][]client.LogEntry{
|
||||
"first": []client.LogEntry{
|
||||
{
|
||||
Time: time.Date(2020, time.March, 1, 0, 0, 0, 0, time.UTC),
|
||||
Message: "aaa",
|
||||
},
|
||||
{
|
||||
Time: time.Date(2020, time.March, 2, 0, 0, 0, 0, time.UTC),
|
||||
Message: "bbb",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := newLogCollector(&tc.cl)
|
||||
|
||||
err := c.collect(context.Background(), testutil.DiscardMetrics(t))
|
||||
|
||||
if diff := cmp.Diff(tc.wantErr, err, cmpopts.EquateErrors()); diff != "" {
|
||||
t.Errorf("Error diff (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogCollect(t *testing.T) {
|
||||
cl := fakeLogClient{
|
||||
entries: map[string][]client.LogEntry{},
|
||||
}
|
||||
|
||||
c := newMultiCollector(newLogCollector(&cl))
|
||||
|
||||
testutil.CollectAndCompare(t, c, "")
|
||||
|
||||
cl.names = append(cl.names, "server", "db", "not found")
|
||||
|
||||
testutil.CollectAndCompare(t, c, "")
|
||||
|
||||
cl.addEntries("server", []client.LogEntry{
|
||||
{
|
||||
Time: time.Date(2020, time.March, 2, 0, 0, 0, 0, time.UTC),
|
||||
Module: "storage",
|
||||
},
|
||||
{
|
||||
Time: time.Date(2020, time.April, 1, 0, 0, 0, 0, time.UTC),
|
||||
Module: "storage",
|
||||
Level: "another",
|
||||
},
|
||||
})
|
||||
|
||||
testutil.CollectAndCompare(t, c, `
|
||||
# HELP paperless_log_entries_total Best-effort count of log entries.
|
||||
# TYPE paperless_log_entries_total counter
|
||||
paperless_log_entries_total{level="",module="storage",name="server"} 1
|
||||
paperless_log_entries_total{level="another",module="storage",name="server"} 1
|
||||
`)
|
||||
|
||||
cl.addEntries("server", []client.LogEntry{
|
||||
{
|
||||
Time: time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||
Module: "storage",
|
||||
},
|
||||
})
|
||||
|
||||
for range [3]int{} {
|
||||
cl.addEntries("db", []client.LogEntry{
|
||||
{
|
||||
Time: time.Date(2021, time.January, 1, 2, 3, 0, 0, time.UTC),
|
||||
},
|
||||
})
|
||||
|
||||
testutil.CollectAndCompare(t, c, `
|
||||
# HELP paperless_log_entries_total Best-effort count of log entries.
|
||||
# TYPE paperless_log_entries_total counter
|
||||
paperless_log_entries_total{level="",module="",name="db"} 1
|
||||
paperless_log_entries_total{level="",module="storage",name="server"} 2
|
||||
paperless_log_entries_total{level="another",module="storage",name="server"} 1
|
||||
`)
|
||||
|
||||
// Reset logs
|
||||
cl.entries = nil
|
||||
}
|
||||
}
|
65
main.go
Normal file
65
main.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
kitlog "github.com/go-kit/log"
|
||||
"github.com/hansmi/paperhooks/pkg/client"
|
||||
"github.com/hansmi/paperhooks/pkg/kpflag"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/prometheus/common/version"
|
||||
"github.com/prometheus/exporter-toolkit/web"
|
||||
webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag"
|
||||
)
|
||||
|
||||
var webConfig = webflag.AddFlags(kingpin.CommandLine, ":8081")
|
||||
var metricsPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics").Default("/metrics").String()
|
||||
var disableExporterMetrics = kingpin.Flag("web.disable-exporter-metrics", "Exclude metrics about the exporter itself").Bool()
|
||||
var timeout = kingpin.Flag("scrape-timeout", "Maximum duration for a scrape").Default("1m").Duration()
|
||||
|
||||
func main() {
|
||||
var clientFlags client.Flags
|
||||
|
||||
kpflag.RegisterClient(kingpin.CommandLine, &clientFlags)
|
||||
kingpin.Parse()
|
||||
|
||||
client, err := clientFlags.Build()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
reg := prometheus.NewPedanticRegistry()
|
||||
reg.MustRegister(newCollector(client, *timeout))
|
||||
|
||||
if !*disableExporterMetrics {
|
||||
reg.MustRegister(
|
||||
collectors.NewBuildInfoCollector(),
|
||||
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
|
||||
collectors.NewGoCollector(),
|
||||
version.NewCollector("prometheus_paperless_exporter"),
|
||||
)
|
||||
}
|
||||
|
||||
http.Handle(*metricsPath, promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
io.WriteString(w, `<html>
|
||||
<head><title>Paperless Exporter</title></head>
|
||||
<body>
|
||||
<h1>Paperless Exporter</h1>
|
||||
<p><a href="`+*metricsPath+`">Metrics</a></p>
|
||||
</body>
|
||||
</html>`)
|
||||
})
|
||||
|
||||
logger := kitlog.NewLogfmtLogger(kitlog.StdlibWriter{})
|
||||
server := &http.Server{}
|
||||
|
||||
if err := web.ListenAndServe(server, webConfig, logger); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
60
multicollector.go
Normal file
60
multicollector.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type multiCollectorMember interface {
|
||||
describe(chan<- *prometheus.Desc)
|
||||
collect(context.Context, chan<- prometheus.Metric) error
|
||||
}
|
||||
|
||||
type multiCollector struct {
|
||||
// Impose a timeout on collection if non-zero.
|
||||
timeout time.Duration
|
||||
|
||||
members []multiCollectorMember
|
||||
}
|
||||
|
||||
var _ prometheus.Collector = (*multiCollector)(nil)
|
||||
|
||||
func newMultiCollector(m ...multiCollectorMember) *multiCollector {
|
||||
return &multiCollector{
|
||||
members: m,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *multiCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
for _, i := range c.members {
|
||||
i.describe(ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *multiCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
ctx := context.Background()
|
||||
|
||||
if c.timeout != 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
g.SetLimit(runtime.GOMAXPROCS(0))
|
||||
|
||||
for _, i := range c.members {
|
||||
collect := i.collect
|
||||
g.Go(func() error { return collect(ctx, ch) })
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
ch <- prometheus.NewInvalidMetric(
|
||||
prometheus.NewDesc("paperless_error", "Metrics collection failed", nil, nil),
|
||||
err)
|
||||
}
|
||||
}
|
58
storagepath.go
Normal file
58
storagepath.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/hansmi/paperhooks/pkg/client"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type storagePathClient interface {
|
||||
ListAllStoragePaths(context.Context, *client.ListStoragePathsOptions, func(context.Context, client.StoragePath) error) error
|
||||
}
|
||||
|
||||
type storagePathCollector struct {
|
||||
cl storagePathClient
|
||||
|
||||
infoDesc *prometheus.Desc
|
||||
docCountDesc *prometheus.Desc
|
||||
}
|
||||
|
||||
func newStoragePathCollector(cl storagePathClient) *storagePathCollector {
|
||||
return &storagePathCollector{
|
||||
cl: cl,
|
||||
|
||||
infoDesc: prometheus.NewDesc("paperless_storage_path_info",
|
||||
"Static information about a storage path.",
|
||||
[]string{"id", "name", "slug"}, nil),
|
||||
docCountDesc: prometheus.NewDesc("paperless_storage_path_document_count",
|
||||
"Number of documents associated with a storage path.",
|
||||
[]string{"id"}, nil),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *storagePathCollector) describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- c.infoDesc
|
||||
ch <- c.docCountDesc
|
||||
}
|
||||
|
||||
func (c *storagePathCollector) collect(ctx context.Context, ch chan<- prometheus.Metric) error {
|
||||
opts := &client.ListStoragePathsOptions{}
|
||||
opts.Ordering.Field = "name"
|
||||
|
||||
return c.cl.ListAllStoragePaths(ctx, opts, func(_ context.Context, sp client.StoragePath) error {
|
||||
id := strconv.FormatInt(sp.ID, 10)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(c.infoDesc, prometheus.GaugeValue, 1,
|
||||
id,
|
||||
sp.Name,
|
||||
sp.Slug,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(c.docCountDesc, prometheus.GaugeValue,
|
||||
float64(sp.DocumentCount), id)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
92
storagepath_test.go
Normal file
92
storagepath_test.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/hansmi/paperhooks/pkg/client"
|
||||
"github.com/hansmi/prometheus-paperless-exporter/internal/testutil"
|
||||
)
|
||||
|
||||
type fakeStoragePathClient struct {
|
||||
items []client.StoragePath
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *fakeStoragePathClient) ListAllStoragePaths(ctx context.Context, opts *client.ListStoragePathsOptions, handler func(context.Context, client.StoragePath) error) error {
|
||||
for _, i := range c.items {
|
||||
if err := handler(ctx, i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.err
|
||||
}
|
||||
|
||||
func TestStoragePath(t *testing.T) {
|
||||
errTest := errors.New("test error")
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
cl fakeStoragePathClient
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
name: "listing fails",
|
||||
cl: fakeStoragePathClient{
|
||||
err: errTest,
|
||||
},
|
||||
wantErr: errTest,
|
||||
},
|
||||
{
|
||||
name: "storagePaths",
|
||||
cl: fakeStoragePathClient{
|
||||
items: []client.StoragePath{
|
||||
{ID: 70},
|
||||
{ID: 20667},
|
||||
{ID: 12805},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := newStoragePathCollector(&tc.cl)
|
||||
|
||||
err := c.collect(context.Background(), testutil.DiscardMetrics(t))
|
||||
|
||||
if diff := cmp.Diff(tc.wantErr, err, cmpopts.EquateErrors()); diff != "" {
|
||||
t.Errorf("Error diff (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoragePathCollect(t *testing.T) {
|
||||
cl := fakeStoragePathClient{}
|
||||
|
||||
c := newMultiCollector(newStoragePathCollector(&cl))
|
||||
|
||||
testutil.CollectAndCompare(t, c, ``)
|
||||
|
||||
cl.items = append(cl.items, []client.StoragePath{
|
||||
{ID: 23547, Name: "personal", Slug: "personal"},
|
||||
{ID: 704, Name: "work", DocumentCount: 13},
|
||||
}...)
|
||||
|
||||
testutil.CollectAndCompare(t, c, `
|
||||
# HELP paperless_storage_path_document_count Number of documents associated with a storage path.
|
||||
# TYPE paperless_storage_path_document_count gauge
|
||||
paperless_storage_path_document_count{id="23547"} 0
|
||||
paperless_storage_path_document_count{id="704"} 13
|
||||
# HELP paperless_storage_path_info Static information about a storage path.
|
||||
# TYPE paperless_storage_path_info gauge
|
||||
paperless_storage_path_info{id="23547",name="personal",slug="personal"} 1
|
||||
paperless_storage_path_info{id="704",name="work",slug=""} 1
|
||||
`)
|
||||
}
|
58
tag.go
Normal file
58
tag.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/hansmi/paperhooks/pkg/client"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type tagClient interface {
|
||||
ListAllTags(context.Context, *client.ListTagsOptions, func(context.Context, client.Tag) error) error
|
||||
}
|
||||
|
||||
type tagCollector struct {
|
||||
cl tagClient
|
||||
|
||||
infoDesc *prometheus.Desc
|
||||
docCountDesc *prometheus.Desc
|
||||
}
|
||||
|
||||
func newTagCollector(cl tagClient) *tagCollector {
|
||||
return &tagCollector{
|
||||
cl: cl,
|
||||
|
||||
infoDesc: prometheus.NewDesc("paperless_tag_info",
|
||||
"Static information about a tag.",
|
||||
[]string{"id", "name", "slug"}, nil),
|
||||
docCountDesc: prometheus.NewDesc("paperless_tag_document_count",
|
||||
"Number of documents associated with a tag.",
|
||||
[]string{"id"}, nil),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *tagCollector) describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- c.infoDesc
|
||||
ch <- c.docCountDesc
|
||||
}
|
||||
|
||||
func (c *tagCollector) collect(ctx context.Context, ch chan<- prometheus.Metric) error {
|
||||
opts := &client.ListTagsOptions{}
|
||||
opts.Ordering.Field = "name"
|
||||
|
||||
return c.cl.ListAllTags(ctx, opts, func(_ context.Context, tag client.Tag) error {
|
||||
id := strconv.FormatInt(tag.ID, 10)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(c.infoDesc, prometheus.GaugeValue, 1,
|
||||
id,
|
||||
tag.Name,
|
||||
tag.Slug,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(c.docCountDesc, prometheus.GaugeValue,
|
||||
float64(tag.DocumentCount), id)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
98
tag_test.go
Normal file
98
tag_test.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/hansmi/paperhooks/pkg/client"
|
||||
"github.com/hansmi/prometheus-paperless-exporter/internal/testutil"
|
||||
)
|
||||
|
||||
type fakeTagClient struct {
|
||||
items []client.Tag
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *fakeTagClient) ListAllTags(ctx context.Context, opts *client.ListTagsOptions, handler func(context.Context, client.Tag) error) error {
|
||||
for _, i := range c.items {
|
||||
if err := handler(ctx, i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.err
|
||||
}
|
||||
|
||||
func TestTag(t *testing.T) {
|
||||
errTest := errors.New("test error")
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
cl fakeTagClient
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
name: "listing fails",
|
||||
cl: fakeTagClient{
|
||||
err: errTest,
|
||||
},
|
||||
wantErr: errTest,
|
||||
},
|
||||
{
|
||||
name: "tags",
|
||||
cl: fakeTagClient{
|
||||
items: []client.Tag{
|
||||
{ID: 8463},
|
||||
{ID: 8463},
|
||||
{ID: 338},
|
||||
{ID: 11768},
|
||||
{ID: 30619},
|
||||
{ID: 27086},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := newTagCollector(&tc.cl)
|
||||
|
||||
err := c.collect(context.Background(), testutil.DiscardMetrics(t))
|
||||
|
||||
if diff := cmp.Diff(tc.wantErr, err, cmpopts.EquateErrors()); diff != "" {
|
||||
t.Errorf("Error diff (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagCollect(t *testing.T) {
|
||||
cl := fakeTagClient{}
|
||||
|
||||
c := newMultiCollector(newTagCollector(&cl))
|
||||
|
||||
testutil.CollectAndCompare(t, c, ``)
|
||||
|
||||
cl.items = append(cl.items, []client.Tag{
|
||||
{ID: 8463, Name: "aaa", Slug: "aslug"},
|
||||
{ID: 338, Name: "three-three-eight", DocumentCount: 13},
|
||||
{ID: 26429, Name: "last"},
|
||||
}...)
|
||||
|
||||
testutil.CollectAndCompare(t, c, `
|
||||
# HELP paperless_tag_document_count Number of documents associated with a tag.
|
||||
# TYPE paperless_tag_document_count gauge
|
||||
paperless_tag_document_count{id="26429"} 0
|
||||
paperless_tag_document_count{id="338"} 13
|
||||
paperless_tag_document_count{id="8463"} 0
|
||||
# HELP paperless_tag_info Static information about a tag.
|
||||
# TYPE paperless_tag_info gauge
|
||||
paperless_tag_info{id="26429",name="last",slug=""} 1
|
||||
paperless_tag_info{id="338",name="three-three-eight",slug=""} 1
|
||||
paperless_tag_info{id="8463",name="aaa",slug="aslug"} 1
|
||||
`)
|
||||
}
|
126
task.go
Normal file
126
task.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hansmi/paperhooks/pkg/client"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func optionalTimestamp(t *time.Time) float64 {
|
||||
if t == nil || t.IsZero() {
|
||||
return 0
|
||||
}
|
||||
|
||||
return float64(t.UnixMilli()) / 1000
|
||||
}
|
||||
|
||||
type taskClient interface {
|
||||
ListTasks(context.Context) ([]client.Task, *client.Response, error)
|
||||
}
|
||||
|
||||
type taskCollector struct {
|
||||
cl taskClient
|
||||
|
||||
infoDesc *prometheus.Desc
|
||||
createdDesc *prometheus.Desc
|
||||
doneDesc *prometheus.Desc
|
||||
statusDesc *prometheus.Desc
|
||||
filenameDesc *prometheus.Desc
|
||||
|
||||
statusInfoVec *prometheus.GaugeVec
|
||||
}
|
||||
|
||||
func newTaskCollector(cl taskClient) *taskCollector {
|
||||
c := &taskCollector{
|
||||
cl: cl,
|
||||
|
||||
infoDesc: prometheus.NewDesc("paperless_task_info",
|
||||
"Static information about a task.",
|
||||
[]string{"id", "task_id", "type"}, nil),
|
||||
createdDesc: prometheus.NewDesc("paperless_task_created_timestamp_seconds",
|
||||
"Number of seconds since 1970 of the task creation.",
|
||||
[]string{"id"}, nil),
|
||||
doneDesc: prometheus.NewDesc("paperless_task_done_timestamp_seconds",
|
||||
"Number of seconds since 1970 of when the task finished.",
|
||||
[]string{"id"}, nil),
|
||||
statusDesc: prometheus.NewDesc("paperless_task_status",
|
||||
"Task status.",
|
||||
[]string{"id", "status"}, nil),
|
||||
filenameDesc: prometheus.NewDesc("paperless_task_filename",
|
||||
"Filename associated with the task (if any).",
|
||||
[]string{"id", "filename"}, nil),
|
||||
|
||||
statusInfoVec: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "paperless_task_status_info",
|
||||
Help: "Task status names.",
|
||||
}, []string{"status"}),
|
||||
}
|
||||
|
||||
c.ensureStatusInfo(client.TaskSuccess)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Returns a canonicalized status string for labels.
|
||||
func (c *taskCollector) ensureStatusInfo(s client.TaskStatus) string {
|
||||
status := strings.ToLower(s.String())
|
||||
|
||||
c.statusInfoVec.With(prometheus.Labels{
|
||||
"status": status,
|
||||
}).Set(1)
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
func (c *taskCollector) describe(ch chan<- *prometheus.Desc) {
|
||||
c.statusInfoVec.Describe(ch)
|
||||
|
||||
ch <- c.infoDesc
|
||||
ch <- c.createdDesc
|
||||
ch <- c.doneDesc
|
||||
ch <- c.statusDesc
|
||||
ch <- c.filenameDesc
|
||||
}
|
||||
|
||||
func (c *taskCollector) collect(ctx context.Context, ch chan<- prometheus.Metric) error {
|
||||
tasks, _, err := c.cl.ListTasks(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
var filename string
|
||||
|
||||
if task.TaskFileName != nil {
|
||||
filename = *task.TaskFileName
|
||||
}
|
||||
|
||||
id := strconv.FormatInt(task.ID, 10)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(c.infoDesc, prometheus.GaugeValue, 1,
|
||||
id,
|
||||
task.TaskID,
|
||||
task.Type,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(c.createdDesc, prometheus.GaugeValue,
|
||||
optionalTimestamp(task.Created), id)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(c.doneDesc, prometheus.GaugeValue,
|
||||
optionalTimestamp(task.Done), id)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(c.statusDesc, prometheus.GaugeValue,
|
||||
1, id, c.ensureStatusInfo(task.Status))
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(c.filenameDesc, prometheus.GaugeValue,
|
||||
1, id, filename)
|
||||
}
|
||||
|
||||
c.statusInfoVec.Collect(ch)
|
||||
|
||||
return nil
|
||||
}
|
100
task_test.go
Normal file
100
task_test.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/hansmi/paperhooks/pkg/client"
|
||||
"github.com/hansmi/prometheus-paperless-exporter/internal/ref"
|
||||
"github.com/hansmi/prometheus-paperless-exporter/internal/testutil"
|
||||
)
|
||||
|
||||
type fakeTaskClient struct {
|
||||
tasks []client.Task
|
||||
listErr error
|
||||
}
|
||||
|
||||
func (c *fakeTaskClient) ListTasks(context.Context) ([]client.Task, *client.Response, error) {
|
||||
return c.tasks, nil, c.listErr
|
||||
}
|
||||
|
||||
func TestTask(t *testing.T) {
|
||||
errTest := errors.New("test error")
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
cl fakeTaskClient
|
||||
wantErr error
|
||||
}{
|
||||
{name: "empty"},
|
||||
{
|
||||
name: "listing fails",
|
||||
cl: fakeTaskClient{
|
||||
listErr: errTest,
|
||||
},
|
||||
wantErr: errTest,
|
||||
},
|
||||
{
|
||||
name: "tasks",
|
||||
cl: fakeTaskClient{
|
||||
tasks: []client.Task{
|
||||
{ID: 2942},
|
||||
{ID: 27064},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := newTaskCollector(&tc.cl)
|
||||
|
||||
err := c.collect(context.Background(), testutil.DiscardMetrics(t))
|
||||
|
||||
if diff := cmp.Diff(tc.wantErr, err, cmpopts.EquateErrors()); diff != "" {
|
||||
t.Errorf("Error diff (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskCollect(t *testing.T) {
|
||||
cl := fakeTaskClient{}
|
||||
|
||||
c := newMultiCollector(newTaskCollector(&cl))
|
||||
|
||||
testutil.CollectAndCompare(t, c, `
|
||||
# HELP paperless_task_status_info Task status names.
|
||||
# TYPE paperless_task_status_info gauge
|
||||
paperless_task_status_info{status="success"} 1
|
||||
`)
|
||||
|
||||
cl.tasks = append(cl.tasks, client.Task{
|
||||
ID: 31563,
|
||||
Created: ref.Ref(time.Date(1980, time.January, 1, 0, 0, 0, 0, time.UTC)),
|
||||
})
|
||||
|
||||
testutil.CollectAndCompare(t, c, `
|
||||
# HELP paperless_task_created_timestamp_seconds Number of seconds since 1970 of the task creation.
|
||||
# TYPE paperless_task_created_timestamp_seconds gauge
|
||||
paperless_task_created_timestamp_seconds{id="31563"} 3.155328e+08
|
||||
# HELP paperless_task_done_timestamp_seconds Number of seconds since 1970 of when the task finished.
|
||||
# TYPE paperless_task_done_timestamp_seconds gauge
|
||||
paperless_task_done_timestamp_seconds{id="31563"} 0
|
||||
# HELP paperless_task_filename Filename associated with the task (if any).
|
||||
# TYPE paperless_task_filename gauge
|
||||
paperless_task_filename{filename="",id="31563"} 1
|
||||
# HELP paperless_task_info Static information about a task.
|
||||
# TYPE paperless_task_info gauge
|
||||
paperless_task_info{id="31563",task_id="",type=""} 1
|
||||
# HELP paperless_task_status Task status.
|
||||
# TYPE paperless_task_status gauge
|
||||
paperless_task_status{id="31563",status="statusunspecified"} 1
|
||||
# HELP paperless_task_status_info Task status names.
|
||||
# TYPE paperless_task_status_info gauge
|
||||
paperless_task_status_info{status="statusunspecified"} 1
|
||||
paperless_task_status_info{status="success"} 1
|
||||
`)
|
||||
}
|
Loading…
Reference in a new issue