Compare commits
48 commits
Author | SHA1 | Date | |
---|---|---|---|
|
0c832c4dec | ||
|
e176ed4db9 | ||
|
560a8c093c | ||
|
bd5c47c7fc | ||
|
c59a3d23d6 | ||
|
d6a4b3cc6c | ||
|
907ba3a708 | ||
|
7e3d56c3e0 | ||
|
b52e18aca1 | ||
|
00a1ed912b | ||
|
0ea6cd556c | ||
|
6fb59dd7be | ||
|
035752eca9 | ||
|
299192121a | ||
|
0bf2b5a6ee | ||
|
5784e15fa0 | ||
|
e9d70573a7 | ||
|
9ea3bac4af | ||
|
e78aad9501 | ||
|
657181a728 | ||
|
2d17c2f066 | ||
|
8fb9f9d7f6 | ||
|
edd88f3883 | ||
|
17ecce3cc6 | ||
|
809e084698 | ||
|
4cbc006e84 | ||
|
2865d787d7 | ||
|
fa224e66e4 | ||
|
c880ba2e36 | ||
|
8a7ebfefa1 | ||
|
cf29943ebe | ||
|
3b4e9143be | ||
|
05a3dd2501 | ||
|
e6870170aa | ||
|
81b4b9be55 | ||
|
abce8035f3 | ||
|
b5ced9df2c | ||
|
5b426932cf | ||
|
be9d6fa0dc | ||
|
ad306c90e3 | ||
|
4022be77ae | ||
|
bf3ea6ad35 | ||
|
107112b139 | ||
|
3749b1eef9 | ||
|
5c230986d0 | ||
|
a3f089ed86 | ||
|
a29621a3b3 | ||
|
e1e068dd5f |
30 changed files with 1705 additions and 1157 deletions
|
@ -1,4 +1,10 @@
|
|||
img/
|
||||
# folders
|
||||
grafana/
|
||||
.github/
|
||||
Dockerfile
|
||||
img/
|
||||
|
||||
# files
|
||||
.env*
|
||||
.gitignore
|
||||
Dockerfile
|
||||
README.md
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
IMMICH_USERNAME=
|
||||
IMMICH_PASSWORD=
|
||||
IMMICH_API_KEY=
|
||||
IMMICH_BASE_URL=
|
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
16
.github/workflows/build.yml
vendored
Normal file
16
.github/workflows/build.yml
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
name: Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
jobs:
|
||||
build:
|
||||
name: Run build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Build
|
||||
run: make build
|
39
.github/workflows/codeql-analysis.yml
vendored
Normal file
39
.github/workflows/codeql-analysis.yml
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
name: CodeQL
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ["go"]
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
69
.github/workflows/docker.yml
vendored
Normal file
69
.github/workflows/docker.yml
vendored
Normal file
|
@ -0,0 +1,69 @@
|
|||
name: Docker Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tags:
|
||||
description: "version"
|
||||
required: false
|
||||
type: string
|
||||
release:
|
||||
types:
|
||||
- created
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
jobs:
|
||||
build_and_push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: "main"
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=ref,event=tag
|
||||
type=raw,value=${{ inputs.tags }},enable=${{ github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./
|
||||
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||
push: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: BUILD_VERSION=${{ github.event.release.name }}
|
47
.github/workflows/push_docker.yml
vendored
47
.github/workflows/push_docker.yml
vendored
|
@ -1,47 +0,0 @@
|
|||
name: manual push
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tags:
|
||||
description: 'version'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
|
||||
|
||||
build_push_monolith:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'main'
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2.2.1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: martabal
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push immich
|
||||
uses: docker/build-push-action@v3.2.0
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: |
|
||||
martabal/qbitorrent-exporter:${{ inputs.tags }}
|
||||
martabal/qbitorrent-exporter:latest
|
||||
|
27
.github/workflows/test.yml
vendored
Normal file
27
.github/workflows/test.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
name: Tests
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run unit tests
|
||||
run: make test
|
||||
|
||||
- name: Run formatter
|
||||
run: cd src && test -z $(gofmt -l .)
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v1.55.2
|
||||
working-directory: src
|
18
Dockerfile
18
Dockerfile
|
@ -1,14 +1,22 @@
|
|||
FROM golang:1.19-alpine3.17 AS builder
|
||||
FROM golang:1.21-alpine3.19 AS builder
|
||||
|
||||
ARG BUILD_VERSION
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
COPY src src
|
||||
|
||||
RUN go get -d -v ./src/ && \
|
||||
go build -o /go/bin/immich-exporter ./src
|
||||
RUN cd src && \
|
||||
if [ -n "${BUILD_VERSION}" ]; then \
|
||||
go build -o /go/bin/immich-exporter -ldflags="-X 'main.Version=${BUILD_VERSION}'" . ; \
|
||||
else \
|
||||
go build -o /go/bin/immich-exporter . ; \
|
||||
fi
|
||||
|
||||
FROM alpine:3.17
|
||||
FROM alpine:3.19
|
||||
|
||||
COPY --from=builder /go/bin/immich-exporter /go/bin/immich-exporter
|
||||
|
||||
WORKDIR /go/bin
|
||||
|
||||
CMD ["/go/bin/immich-exporter"]
|
||||
|
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 martabal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
20
Makefile
Normal file
20
Makefile
Normal file
|
@ -0,0 +1,20 @@
|
|||
build:
|
||||
cd src && go build -o ../immich-exporter.out .
|
||||
|
||||
dev :
|
||||
cd src && go run .
|
||||
|
||||
dev-env :
|
||||
cd src && go run . -e
|
||||
|
||||
format :
|
||||
cd src && test -z $(gofmt -l .)
|
||||
|
||||
lint:
|
||||
docker run --rm -v ./src:/app -w /app golangci/golangci-lint:latest golangci-lint run -v
|
||||
|
||||
test:
|
||||
cd src && go test -v ./tests
|
||||
|
||||
update:
|
||||
cd src && go get -u . && go mod tidy
|
40
README.md
40
README.md
|
@ -1,25 +1,28 @@
|
|||
# immich-exporter
|
||||
|
||||
[](https://github.com/martabal/immich-exporter/actions/workflows/push_docker.yml)
|
||||
[](https://github.com/martabal/immich-exporter/actions/workflows/docker.yml)
|
||||
[](https://github.com/martabal/immich-exporter/actions/workflows/build.yml)
|
||||
[](https://github.com/martabal/immich-exporter/actions/workflows/test.yml)
|
||||
|
||||
<p align="center">
|
||||
<img src="img/immich.png" width=100> <img src="img/prometheus.png" width=100><img src="img/golang.png" width=100>
|
||||
</p>
|
||||
|
||||
This app is a Prometheus exporter for immich.
|
||||
This app is made to be integrated with the [immich-grafana-dashboard](https://github.com/martabal/immich-exporter/grafana/dashboard.json)
|
||||
This app is made to be integrated with the [immich-grafana-dashboard](https://github.com/martabal/immich-exporter/blob/main/grafana/dashboard.json)
|
||||
|
||||
## Run it
|
||||
|
||||
Create an API key in your Immich settings and set `IMMICH_API_KEY` to is value.
|
||||
|
||||
### Docker-cli ([click here for more info](https://docs.docker.com/engine/reference/commandline/cli/))
|
||||
|
||||
```sh
|
||||
docker run --name=immich-exporter \
|
||||
-e IMMICH_URL=http://192.168.1.10:8080 \
|
||||
-e IMMICH_PASSWORD='<your_password>' \
|
||||
-e IMMICH_USERNAME=admin \
|
||||
-e IMMICH_BASE_URL=http://192.168.1.10:8080 \
|
||||
-e IMMICH_API_KEY=<your_api_key> \
|
||||
-p 8090:8090 \
|
||||
martabal/immich-exporter
|
||||
ghcr.io/martabal/immich-exporter
|
||||
```
|
||||
|
||||
### Docker-compose
|
||||
|
@ -28,12 +31,11 @@ docker run --name=immich-exporter \
|
|||
version: "2.1"
|
||||
services:
|
||||
immich:
|
||||
image: martabal/immich-exporter:latest
|
||||
image: ghcr.io/martabal/immich-exporter:latest
|
||||
container_name: immich-exporter
|
||||
environment:
|
||||
- IMMICH_URL=http://192.168.1.10:8080
|
||||
- IMMICH_PASSWORD='<your_password>'
|
||||
- IMMICH_USERNAME=admin
|
||||
- IMMICH_BASE_URL=http://192.168.1.10:8080
|
||||
- IMMICH_API_KEY=<your_api_key>
|
||||
ports:
|
||||
- 8090:8090
|
||||
restart: unless-stopped
|
||||
|
@ -63,12 +65,24 @@ If you want to use an .env file, edit `.env.example` to match your setup, rename
|
|||
| Parameters | Function |
|
||||
| :-----: | ----- |
|
||||
| `-p 8090` | Webservice port |
|
||||
| `-e IMMICH_USERNAME` | Immich username |
|
||||
| `-e IMMICH_PASSWORD` | Immich password |
|
||||
| `-e IMMICH_BASE_URL` | Immich base URL |
|
||||
| `-e IMMICH_API_KEY` | Immich API key |
|
||||
| `-e EXPORTER_PORT` | qbittorrent export port (optional) | `8090` |
|
||||
| `-e LOG_LEVEL` | App log level (`TRACE`, `DEBUG`, `INFO`, `WARN` and `ERROR`) | `INFO` |
|
||||
|
||||
### Arguments
|
||||
|
||||
| Arguments | Function |
|
||||
| :-----: | ----- |
|
||||
| -e | Use a .env file containing environment variables (.env file must be placed in the same directory) |
|
||||
| -e | If qbittorrent-exporter detects a .env file in the same directory, the values in the .env will be used, `-e` forces the usage of environment variables |
|
||||
|
||||
### Setup
|
||||
|
||||
Add the target to your `scrape_configs` in your `prometheus.yml` file of your Prometheus instance.
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: 'immich'
|
||||
static_configs:
|
||||
- targets: [ '<your_ip_address>:8090' ]
|
||||
```
|
||||
|
|
5
go.mod
5
go.mod
|
@ -1,5 +0,0 @@
|
|||
module immich-exporter
|
||||
|
||||
go 1.20
|
||||
|
||||
require github.com/joho/godotenv v1.5.1
|
2
go.sum
2
go.sum
|
@ -1,2 +0,0 @@
|
|||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
File diff suppressed because it is too large
Load diff
19
package.json
19
package.json
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"name": "immich-exporter",
|
||||
"version": "0.1.0",
|
||||
"description": "exporter for immich",
|
||||
"main": "src/main.go",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"exporter",
|
||||
"immich",
|
||||
"grafana",
|
||||
"dashboard",
|
||||
"metrics",
|
||||
"prometheus"
|
||||
],
|
||||
"author": "martabal",
|
||||
"license": "ISC"
|
||||
}
|
19
src/go.mod
Normal file
19
src/go.mod
Normal file
|
@ -0,0 +1,19 @@
|
|||
module immich-exp
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.46.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
)
|
34
src/go.sum
Normal file
34
src/go.sum
Normal file
|
@ -0,0 +1,34 @@
|
|||
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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
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.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y=
|
||||
github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -1,60 +0,0 @@
|
|||
package immich
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"immich-exporter/src/models"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Auth() {
|
||||
|
||||
url := models.GetURL() + "/api/auth/login"
|
||||
method := "POST"
|
||||
|
||||
payload := strings.NewReader(`{
|
||||
"email": "` + models.GetUsername() + `",
|
||||
"password": "` + models.Getpassword() + `"}`)
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest(method, url, payload)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
} else {
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("Accept", "application/json")
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
} else {
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
if res.StatusCode == 400 {
|
||||
log.Fatalln("Incorrect login")
|
||||
} else {
|
||||
var result models.Login
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
log.Println("Can not unmarshal JSON")
|
||||
}
|
||||
|
||||
models.SetAccessToken(result.AccessToken)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -3,164 +3,218 @@ package immich
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"immich-exporter/src/models"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"immich-exp/models"
|
||||
"io"
|
||||
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
prom "immich-exp/prometheus"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func Allrequests() string {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
return serverversion() + Analyze()
|
||||
var (
|
||||
mutex sync.Mutex
|
||||
)
|
||||
|
||||
type Data struct {
|
||||
URL string
|
||||
HTTPMethod string
|
||||
}
|
||||
|
||||
func Analyze() string {
|
||||
allusers, err := GetAllUsers()
|
||||
users, err2 := users()
|
||||
if err != nil && err2 != nil {
|
||||
return ""
|
||||
} else {
|
||||
return Sendbackmessagepreference(users, allusers)
|
||||
var httpGetUsers = Data{
|
||||
URL: "/api/user?isAll=true",
|
||||
HTTPMethod: http.MethodGet,
|
||||
}
|
||||
var httpServerVersion = Data{
|
||||
URL: "/api/server-info/version",
|
||||
HTTPMethod: http.MethodGet,
|
||||
}
|
||||
var httpStatistics = Data{
|
||||
URL: "/api/server-info/statistics",
|
||||
HTTPMethod: http.MethodGet,
|
||||
}
|
||||
var httpGetJobs = Data{
|
||||
URL: "/api/jobs",
|
||||
HTTPMethod: http.MethodGet,
|
||||
}
|
||||
|
||||
var unmarshalError = "Can not unmarshal JSON"
|
||||
|
||||
func Allrequests(r *prometheus.Registry) {
|
||||
|
||||
wg.Add(1)
|
||||
go ServerVersion(r)
|
||||
wg.Add(1)
|
||||
go Analyze(r)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func Analyze(r *prometheus.Registry) {
|
||||
defer wg.Done()
|
||||
|
||||
alljobsstatus := make(chan func() (*models.StructAllJobsStatus, error))
|
||||
allusers := make(chan func() (*models.StructAllUsers, error))
|
||||
serverinfo := make(chan func() (*models.StructServerInfo, error))
|
||||
defer func() {
|
||||
close(serverinfo)
|
||||
close(allusers)
|
||||
close(alljobsstatus)
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go GetAllJobsStatus(alljobsstatus)
|
||||
res1, err := (<-alljobsstatus)()
|
||||
wg.Add(1)
|
||||
go GetAllUsers(allusers)
|
||||
res2, err2 := (<-allusers)()
|
||||
wg.Add(1)
|
||||
go ServerInfo(serverinfo)
|
||||
|
||||
res3, err3 := (<-serverinfo)()
|
||||
|
||||
if err == nil && err2 == nil && err3 == nil {
|
||||
prom.SendBackMessagePreference(res3, res2, res1, r)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func GetAllUsers() (*models.AllUsers, error) {
|
||||
resp, err := Apirequest("/api/user?isAll=true", "GET")
|
||||
if err != nil {
|
||||
if err.Error() == "403" {
|
||||
log.Println("Cookie changed, try to reconnect ...")
|
||||
Auth()
|
||||
} else {
|
||||
if models.GetPromptError() == false {
|
||||
log.Println("Error : ", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if models.GetPromptError() == true {
|
||||
models.SetPromptError(false)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
func GetAllUsers(c chan func() (*models.StructAllUsers, error)) {
|
||||
defer wg.Done()
|
||||
resp, err := Apirequest(httpGetUsers.URL, httpGetUsers.HTTPMethod)
|
||||
if err == nil {
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
} else {
|
||||
|
||||
var result models.AllUsers
|
||||
if err := json.Unmarshal(body, &result); err != nil { // Parse []byte to go struct pointer
|
||||
log.Println("Can not unmarshal JSON")
|
||||
result := new(models.StructAllUsers)
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
log.Error(unmarshalError)
|
||||
}
|
||||
|
||||
return &result, err
|
||||
|
||||
c <- (func() (*models.StructAllUsers, error) { return result, nil })
|
||||
return
|
||||
}
|
||||
}
|
||||
return &models.AllUsers{}, err
|
||||
c <- (func() (*models.StructAllUsers, error) { return new(models.StructAllUsers), err })
|
||||
}
|
||||
|
||||
func serverversion() string {
|
||||
resp, err := Apirequest("/api/server-info/version", "GET")
|
||||
if err != nil {
|
||||
if err.Error() == "403" {
|
||||
log.Println("Cookie changed, try to reconnect ...")
|
||||
Auth()
|
||||
} else {
|
||||
if models.GetPromptError() == false {
|
||||
log.Println("Error : ", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if models.GetPromptError() == true {
|
||||
models.SetPromptError(false)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
func ServerVersion(r *prometheus.Registry) {
|
||||
defer wg.Done()
|
||||
resp, err := Apirequest(httpServerVersion.URL, httpServerVersion.HTTPMethod)
|
||||
if err == nil {
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
} else {
|
||||
|
||||
var result models.ServerVersion
|
||||
if err := json.Unmarshal(body, &result); err != nil { // Parse []byte to go struct pointer
|
||||
log.Println("Can not unmarshal JSON")
|
||||
var result models.StructServerVersion
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
log.Error(unmarshalError)
|
||||
}
|
||||
|
||||
return Sendbackmessageserverversion(&result)
|
||||
|
||||
prom.SendBackMessageserverVersion(&result, r)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func users() (*models.Users, error) {
|
||||
resp, err := Apirequest("/api/server-info/stats", "GET")
|
||||
if err != nil {
|
||||
if err.Error() == "403" {
|
||||
log.Println("Cookie changed, try to reconnect ...")
|
||||
Auth()
|
||||
} else {
|
||||
if models.GetPromptError() == false {
|
||||
log.Println("Error : ", err)
|
||||
}
|
||||
}
|
||||
func ServerInfo(c chan func() (*models.StructServerInfo, error)) {
|
||||
defer wg.Done()
|
||||
resp, err := Apirequest(httpStatistics.URL, httpStatistics.HTTPMethod)
|
||||
if err == nil {
|
||||
|
||||
} else {
|
||||
if models.GetPromptError() == true {
|
||||
models.SetPromptError(false)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
} else {
|
||||
|
||||
var result models.Users
|
||||
if err := json.Unmarshal(body, &result); err != nil { // Parse []byte to go struct pointer
|
||||
log.Println("Can not unmarshal JSON")
|
||||
result := new(models.StructServerInfo)
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
log.Println(unmarshalError)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
|
||||
c <- (func() (*models.StructServerInfo, error) { return result, nil })
|
||||
return
|
||||
}
|
||||
}
|
||||
return &models.Users{}, err
|
||||
c <- (func() (*models.StructServerInfo, error) { return new(models.StructServerInfo), err })
|
||||
}
|
||||
|
||||
func GetAllJobsStatus(c chan func() (*models.StructAllJobsStatus, error)) {
|
||||
defer wg.Done()
|
||||
resp, err := Apirequest(httpGetJobs.URL, httpGetJobs.HTTPMethod)
|
||||
if err == nil {
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
} else {
|
||||
|
||||
result := new(models.StructAllJobsStatus)
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
log.Println(unmarshalError)
|
||||
}
|
||||
c <- (func() (*models.StructAllJobsStatus, error) { return result, nil })
|
||||
return
|
||||
}
|
||||
}
|
||||
c <- (func() (*models.StructAllJobsStatus, error) { return new(models.StructAllJobsStatus), err })
|
||||
}
|
||||
|
||||
func Apirequest(uri string, method string) (*http.Response, error) {
|
||||
|
||||
req, err := http.NewRequest(method, models.GetURL()+uri, nil)
|
||||
req, err := http.NewRequest(method, models.Getbaseurl()+uri, nil)
|
||||
if err != nil {
|
||||
log.Fatalln("Error with url")
|
||||
log.Fatal("Error with url")
|
||||
}
|
||||
|
||||
req.AddCookie(&http.Cookie{Name: "immich_access_token", Value: models.GetAccessToken()})
|
||||
req.Header.Add("Accept", "application/json")
|
||||
req.Header.Add("x-api-key", models.GetApiKey())
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
err := fmt.Errorf("Can't connect to server")
|
||||
if models.GetPromptError() == false {
|
||||
log.Println(err.Error())
|
||||
mutex.Lock()
|
||||
if !models.GetPromptError() {
|
||||
log.Error(err.Error())
|
||||
models.SetPromptError(true)
|
||||
}
|
||||
|
||||
mutex.Unlock()
|
||||
return resp, err
|
||||
|
||||
} else {
|
||||
models.SetPromptError(false)
|
||||
if resp.StatusCode == 200 {
|
||||
|
||||
return resp, nil
|
||||
|
||||
} else {
|
||||
err := fmt.Errorf("%d", resp.StatusCode)
|
||||
if models.GetPromptError() == false {
|
||||
models.SetPromptError(true)
|
||||
|
||||
log.Println("Error code", err.Error())
|
||||
|
||||
}
|
||||
return resp, err
|
||||
|
||||
}
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
mutex.Lock()
|
||||
if models.GetPromptError() {
|
||||
models.SetPromptError(false)
|
||||
}
|
||||
mutex.Unlock()
|
||||
return resp, nil
|
||||
case http.StatusNotFound:
|
||||
|
||||
log.Fatal("Error code ", resp.StatusCode, " for ", models.Getbaseurl()+uri)
|
||||
|
||||
return resp, fmt.Errorf("%d", resp.StatusCode)
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
|
||||
log.Fatal("Api key unauthorized")
|
||||
|
||||
return resp, fmt.Errorf("%d", resp.StatusCode)
|
||||
default:
|
||||
err := fmt.Errorf("%d", resp.StatusCode)
|
||||
mutex.Lock()
|
||||
if !models.GetPromptError() {
|
||||
models.SetPromptError(true)
|
||||
log.Debug("Error code ", resp.StatusCode)
|
||||
}
|
||||
mutex.Unlock()
|
||||
return resp, err
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
package immich
|
||||
|
||||
import (
|
||||
"immich-exporter/src/models"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func Sendbackmessagepreference(result *models.Users, result2 *models.AllUsers) string {
|
||||
total := "# HELP immich_app_number_users The number of immich users\n# TYPE immich_app_max_active_downloads gauge\nimmich_app_nb_users " + strconv.Itoa(len((*result).UsageByUser)) + "\n"
|
||||
total = total + "# HELP immich_app_total_photos The total number of photos\n# TYPE immich_app_total_photos gauge\nimmich_app_total_photos " + strconv.Itoa((*result).Photos) + "\n"
|
||||
total = total + "# HELP immich_app_total_videos The total number of videos\n# TYPE immich_app_total_videos gauge\nimmich_app_total_videos " + strconv.Itoa((*result).Videos) + "\n"
|
||||
total = total + "# HELP immich_app_total_usage The usage of the user\n# TYPE immich_app_total_usage gauge\nimmich_app_total_usage " + strconv.Itoa(int((*result).UsageRaw)) + "\n"
|
||||
immich_user_videos := "# HELP immich_app_user_videos The number of videos of the user\n# TYPE immich_app_user_videos gauge\n"
|
||||
immich_user_photos := "# HELP immich_app_user_photos The number of photo of the user\n# TYPE immich_app_user_photos gauge\n"
|
||||
immich_user_usageRaw := "# HELP immich_app_user_usage The usage of the user\n# TYPE immich_app_user_usage gauge\n"
|
||||
immich_user_info := "# HELP immich_user_info All info for torrents\n# TYPE immich_user_info gauge\n"
|
||||
for i := 0; i < len((*result).UsageByUser); i++ {
|
||||
var myuser = GetName((*result).UsageByUser[i].UserID, result2)
|
||||
immich_user_info = immich_user_info + `immich_user_info{videos="` + strconv.Itoa((*result).UsageByUser[i].Videos) + `",photos="` + strconv.Itoa((*result).UsageByUser[i].Photos) + `",uid="` + (*result).UsageByUser[i].UserID + `",usage="` + strconv.Itoa(int((*result).UsageByUser[i].UsageRaw)) + `",firstname="` + myuser.FirstName + `",lastname="` + myuser.LastName + `"} 1.0` + "\n"
|
||||
immich_user_usageRaw = immich_user_usageRaw + `immich_user_usage{uid="` + (*result).UsageByUser[i].UserID + `",firstname="` + myuser.FirstName + `",lastname="` + myuser.LastName + `",} ` + strconv.Itoa(int((*result).UsageByUser[i].UsageRaw)) + "\n"
|
||||
immich_user_photos = immich_user_photos + `immich_user_photos{uid="` + (*result).UsageByUser[i].UserID + `",firstname="` + myuser.FirstName + `",lastname="` + myuser.LastName + `",} ` + strconv.Itoa((*result).UsageByUser[i].Photos) + "\n"
|
||||
immich_user_videos = immich_user_videos + `immich_user_videos{uid="` + (*result).UsageByUser[i].UserID + `",firstname="` + myuser.FirstName + `",lastname="` + myuser.LastName + `",} ` + strconv.Itoa((*result).UsageByUser[i].Videos) + "\n"
|
||||
}
|
||||
return total + immich_user_info + immich_user_videos + immich_user_photos + immich_user_usageRaw
|
||||
}
|
||||
|
||||
func Sendbackmessageserverversion(result *models.ServerVersion) string {
|
||||
|
||||
return "# HELP immich_app_version The current immich version\n# TYPE immich_app_version gauge\nimmich_app_version" + `{version="` + strconv.Itoa((*result).Major) + "." + strconv.Itoa((*result).Minor) + "." + strconv.Itoa((*result).Patch) + `",} 1.0` + "\n"
|
||||
}
|
||||
|
||||
func GetName(result string, result2 *models.AllUsers) models.CustomUser {
|
||||
var myuser models.CustomUser
|
||||
for i := 0; i < len(*result2); i++ {
|
||||
if (*result2)[i].ID == result {
|
||||
|
||||
myuser.ID = (*result2)[i].ID
|
||||
myuser.FirstName = (*result2)[i].FirstName
|
||||
myuser.LastName = (*result2)[i].LastName
|
||||
myuser.Email = (*result2)[i].Email
|
||||
myuser.IsAdmin = (*result2)[i].IsAdmin
|
||||
}
|
||||
|
||||
}
|
||||
return myuser
|
||||
}
|
140
src/init.go
140
src/init.go
|
@ -2,69 +2,115 @@ package main
|
|||
|
||||
import (
|
||||
"flag"
|
||||
"immich-exporter/src/immich"
|
||||
"immich-exporter/src/models"
|
||||
"fmt"
|
||||
immich "immich-exp/immich"
|
||||
"immich-exp/models"
|
||||
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func startup() {
|
||||
var envfile bool
|
||||
const DEFAULTPORT = 8090
|
||||
|
||||
var (
|
||||
Version = "dev"
|
||||
Author = "martabal"
|
||||
ProjectName = "immich-exporter"
|
||||
)
|
||||
|
||||
func main() {
|
||||
loadenv()
|
||||
fmt.Printf("%s (version %s)\n", ProjectName, Version)
|
||||
fmt.Println("Author: ", Author)
|
||||
fmt.Println("Using log level: ", log.GetLevel())
|
||||
log.Info("Immich URL: ", models.Getbaseurl())
|
||||
log.Info("Started")
|
||||
http.HandleFunc("/metrics", metrics)
|
||||
addr := ":" + strconv.Itoa(models.GetPort())
|
||||
log.Info("Listening on port ", models.GetPort())
|
||||
err := http.ListenAndServe(addr, nil)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
|
||||
func metrics(w http.ResponseWriter, r *http.Request) {
|
||||
log.Trace("New request")
|
||||
registry := prometheus.NewRegistry()
|
||||
immich.Allrequests(registry)
|
||||
h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{})
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func loadenv() {
|
||||
var envfile bool
|
||||
flag.BoolVar(&envfile, "e", false, "Use .env file")
|
||||
flag.Parse()
|
||||
log.Println("Loading all parameters")
|
||||
if envfile {
|
||||
useenvfile()
|
||||
} else {
|
||||
initenv()
|
||||
_, err := os.Stat(".env")
|
||||
if !os.IsNotExist(err) && !envfile {
|
||||
err := godotenv.Load(".env")
|
||||
if err != nil {
|
||||
log.Panic("Error loading .env file:", err)
|
||||
}
|
||||
// fmt.Println("Using .env file")
|
||||
}
|
||||
|
||||
immich.Auth()
|
||||
immichapikey := getEnv("IMMICH_API_KEY", "", false, "Immich API Key is not set", true)
|
||||
immichURL := getEnv("IMMICH_BASE_URL", "http://localhost:8080", true, "Qbittorrent base_url is not set. Using default base_url", false)
|
||||
exporterPort := getEnv("EXPORTER_PORT", strconv.Itoa(DEFAULTPORT), false, "", false)
|
||||
|
||||
}
|
||||
|
||||
func useenvfile() {
|
||||
myEnv, err := godotenv.Read()
|
||||
username := myEnv["IMMICH_USERNAME"]
|
||||
password := myEnv["IMMICH_PASSWORD"]
|
||||
immich_url := myEnv["IMMICH_BASE_URL"]
|
||||
if myEnv["IMMICH_USERNAME"] == "" {
|
||||
log.Panic("Immich username is not set.")
|
||||
}
|
||||
if myEnv["IMMICH_PASSWORD"] == "" {
|
||||
log.Panic("Immich password is not set.")
|
||||
}
|
||||
if myEnv["IMMICH_BASE_URL"] == "" {
|
||||
log.Panic("IMMICH base_url is not set.")
|
||||
}
|
||||
models.Setuser(username, password, immich_url)
|
||||
num, err := strconv.Atoi(exporterPort)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Error loading .env file")
|
||||
log.Panic("EXPORTER_PORT must be an integer")
|
||||
}
|
||||
log.Println("Using .env file")
|
||||
if num < 0 || num > 65353 {
|
||||
log.Panic("EXPORTER_PORT must be > 0 and < 65353")
|
||||
}
|
||||
|
||||
setLogLevel(getEnv("LOG_LEVEL", "INFO", false, "", false))
|
||||
models.SetApp(num, false)
|
||||
models.Setuser(immichURL, immichapikey)
|
||||
}
|
||||
func setLogLevel(logLevel string) {
|
||||
logLevels := map[string]log.Level{
|
||||
"TRACE": log.TraceLevel,
|
||||
"DEBUG": log.DebugLevel,
|
||||
"INFO": log.InfoLevel,
|
||||
"WARN": log.WarnLevel,
|
||||
"ERROR": log.ErrorLevel,
|
||||
}
|
||||
|
||||
level, found := logLevels[strings.ToUpper(logLevel)]
|
||||
if !found {
|
||||
level = log.InfoLevel
|
||||
}
|
||||
|
||||
log.SetLevel(level)
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
DisableColors: false,
|
||||
FullTimestamp: true,
|
||||
})
|
||||
}
|
||||
|
||||
func initenv() {
|
||||
username := os.Getenv("IMMICH_USERNAME")
|
||||
password := os.Getenv("IMMICH_PASSWORD")
|
||||
immich_url := os.Getenv("IMMICH_BASE_URL")
|
||||
if os.Getenv("IMMICH_USERNAME") == "" {
|
||||
log.Panic("Immich username is not set.")
|
||||
|
||||
func getEnv(key string, fallback string, printLog bool, logPrinted string, needed bool) string {
|
||||
value, ok := os.LookupEnv(key)
|
||||
if !ok || value == "" {
|
||||
if needed {
|
||||
log.Panicln("Please set a value for", key)
|
||||
}
|
||||
if printLog {
|
||||
log.Warn(logPrinted)
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
if os.Getenv("IMMICH_PASSWORD") == "" {
|
||||
log.Panic("Immich password is not set.")
|
||||
|
||||
}
|
||||
if os.Getenv("IMMICH_BASE_URL") == "" {
|
||||
log.Panic("Immich base_url is not set.")
|
||||
|
||||
}
|
||||
models.Setuser(username, password, immich_url)
|
||||
|
||||
return value
|
||||
}
|
||||
|
|
28
src/main.go
28
src/main.go
|
@ -1,28 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"immich-exporter/src/immich"
|
||||
"immich-exporter/src/models"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
startup()
|
||||
log.Println("Immich URL :", models.GetURL())
|
||||
log.Println("username :", models.GetUsername())
|
||||
log.Println("password :", models.Getpasswordmasked())
|
||||
log.Println("Started")
|
||||
http.HandleFunc("/metrics", metrics)
|
||||
http.ListenAndServe(":8090", nil)
|
||||
}
|
||||
|
||||
func metrics(w http.ResponseWriter, req *http.Request) {
|
||||
value := immich.Allrequests()
|
||||
if value == "" {
|
||||
value = immich.Allrequests()
|
||||
}
|
||||
fmt.Fprintf(w, value)
|
||||
}
|
|
@ -2,31 +2,29 @@ package models
|
|||
|
||||
import "time"
|
||||
|
||||
type Login struct {
|
||||
type StructLogin struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
UserID string `json:"userId"`
|
||||
UserEmail string `json:"userEmail"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
Name string `json:"name"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
ShouldChangePassword bool `json:"shouldChangePassword"`
|
||||
}
|
||||
|
||||
type Users struct {
|
||||
Photos int `json:"photos"`
|
||||
Videos int `json:"videos"`
|
||||
type StructServerInfo struct {
|
||||
Photos int `json:"photos"`
|
||||
Videos int `json:"videos"`
|
||||
Usage int64 `json:"usage"`
|
||||
UsageByUser []struct {
|
||||
UserID string `json:"userId"`
|
||||
Videos int `json:"videos"`
|
||||
UserName string `json:"userName"`
|
||||
Photos int `json:"photos"`
|
||||
UsageRaw int64 `json:"usageRaw"`
|
||||
Usage string `json:"usage"`
|
||||
Videos int `json:"videos"`
|
||||
Usage int `json:"usage"`
|
||||
} `json:"usageByUser"`
|
||||
UsageRaw int64 `json:"usageRaw"`
|
||||
Usage string `json:"usage"`
|
||||
}
|
||||
|
||||
type ServerInfo struct {
|
||||
type StructDiskInfo struct {
|
||||
DiskAvailable string `json:"diskAvailable"`
|
||||
DiskSize string `json:"diskSize"`
|
||||
DiskUse string `json:"diskUse"`
|
||||
|
@ -36,17 +34,16 @@ type ServerInfo struct {
|
|||
DiskUsagePercentage float64 `json:"diskUsagePercentage"`
|
||||
}
|
||||
|
||||
type ServerVersion struct {
|
||||
type StructServerVersion struct {
|
||||
Major int `json:"major"`
|
||||
Minor int `json:"minor"`
|
||||
Patch int `json:"patch"`
|
||||
}
|
||||
|
||||
type AllUsers []struct {
|
||||
type StructAllUsers []struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ProfileImagePath string `json:"profileImagePath"`
|
||||
ShouldChangePassword bool `json:"shouldChangePassword"`
|
||||
|
@ -55,10 +52,39 @@ type AllUsers []struct {
|
|||
OauthID string `json:"oauthId"`
|
||||
}
|
||||
|
||||
type CustomUser struct {
|
||||
Email string
|
||||
ID string
|
||||
FirstName string
|
||||
LastName string
|
||||
IsAdmin bool
|
||||
type StructCustomUser struct {
|
||||
Email string
|
||||
ID string
|
||||
Name string
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
type StructJobStatus struct {
|
||||
JobCounts struct {
|
||||
Active int `json:"active"`
|
||||
Completed int `json:"completed"`
|
||||
Failed int `json:"failed"`
|
||||
Delayed int `json:"delayed"`
|
||||
Waiting int `json:"waiting"`
|
||||
Paused int `json:"paused"`
|
||||
} `json:"jobCounts"`
|
||||
QueueStatus struct {
|
||||
IsActive bool `json:"isActive"`
|
||||
IsPaused bool `json:"isPaused"`
|
||||
} `json:"queueStatus"`
|
||||
}
|
||||
|
||||
type StructAllJobsStatus struct {
|
||||
ThumbnailGeneration StructJobStatus `json:"thumbnailGeneration"`
|
||||
MetadataExtraction StructJobStatus `json:"metadataExtraction"`
|
||||
VideoConversion StructJobStatus `json:"videoConversion"`
|
||||
ObjectTagging StructJobStatus `json:"objectTagging"`
|
||||
RecognizeFaces StructJobStatus `json:"recognizeFaces"`
|
||||
ClipEncoding StructJobStatus `json:"clipEncoding"`
|
||||
BackgroundTask StructJobStatus `json:"backgroundTask"`
|
||||
StorageTemplateMigration StructJobStatus `json:"storageTemplateMigration"`
|
||||
Migration StructJobStatus `json:"migration"`
|
||||
Search StructJobStatus `json:"search"`
|
||||
Sidecar StructJobStatus `json:"sidecar"`
|
||||
Library StructJobStatus `json:"library"`
|
||||
}
|
||||
|
|
27
src/models/app.go
Normal file
27
src/models/app.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package models
|
||||
|
||||
type TypeAppConfig struct {
|
||||
Port int
|
||||
Error bool
|
||||
}
|
||||
|
||||
var AppConfig TypeAppConfig
|
||||
|
||||
func SetApp(setport int, seterror bool) {
|
||||
AppConfig = TypeAppConfig{
|
||||
Port: setport,
|
||||
Error: seterror,
|
||||
}
|
||||
}
|
||||
|
||||
func GetPort() int {
|
||||
return AppConfig.Port
|
||||
}
|
||||
|
||||
func SetPromptError(prompt bool) {
|
||||
AppConfig.Error = prompt
|
||||
}
|
||||
|
||||
func GetPromptError() bool {
|
||||
return AppConfig.Error
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package models
|
||||
|
||||
var myerr bool
|
||||
|
||||
func SetPromptError(prompt bool) {
|
||||
myerr = prompt
|
||||
}
|
||||
|
||||
func GetPromptError() bool {
|
||||
return myerr
|
||||
}
|
22
src/models/immich.go
Normal file
22
src/models/immich.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package models
|
||||
|
||||
type StructImmich struct {
|
||||
APIKey string
|
||||
URL string
|
||||
}
|
||||
|
||||
var myuser StructImmich
|
||||
|
||||
func Setuser(url string, apikey string) {
|
||||
myuser.URL = url
|
||||
myuser.APIKey = apikey
|
||||
|
||||
}
|
||||
|
||||
func Getbaseurl() string {
|
||||
return myuser.URL
|
||||
}
|
||||
|
||||
func GetApiKey() string {
|
||||
return myuser.APIKey
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package models
|
||||
|
||||
type User struct {
|
||||
Username string
|
||||
Password string
|
||||
URL string
|
||||
accessToken string
|
||||
}
|
||||
|
||||
var myuser User
|
||||
|
||||
func mask(input string) string {
|
||||
hide := ""
|
||||
for i := 0; i < len(input); i++ {
|
||||
hide += "*"
|
||||
}
|
||||
return hide
|
||||
}
|
||||
|
||||
func Getuser() (string, string) {
|
||||
return myuser.Username, myuser.Password
|
||||
}
|
||||
|
||||
func Setuser(username string, password string, url string) {
|
||||
myuser.Username = username
|
||||
myuser.Password = password
|
||||
myuser.URL = url
|
||||
}
|
||||
|
||||
func GetUsername() string {
|
||||
return myuser.Username
|
||||
}
|
||||
|
||||
func Getpassword() string {
|
||||
return myuser.Password
|
||||
}
|
||||
func Getpasswordmasked() string {
|
||||
return mask(myuser.Password)
|
||||
}
|
||||
|
||||
func SetAccessToken(accessToken string) {
|
||||
myuser.accessToken = accessToken
|
||||
}
|
||||
|
||||
func GetAccessToken() string {
|
||||
return myuser.accessToken
|
||||
}
|
||||
|
||||
func GetURL() string {
|
||||
return myuser.URL
|
||||
}
|
133
src/prometheus/prometheus.go
Normal file
133
src/prometheus/prometheus.go
Normal file
|
@ -0,0 +1,133 @@
|
|||
package prom
|
||||
|
||||
import (
|
||||
"immich-exp/models"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type Gauge []struct {
|
||||
name string
|
||||
help string
|
||||
value float64
|
||||
}
|
||||
|
||||
func SendBackMessagePreference(
|
||||
result *models.StructServerInfo,
|
||||
result2 *models.StructAllUsers,
|
||||
result3 *models.StructAllJobsStatus,
|
||||
r *prometheus.Registry,
|
||||
) {
|
||||
|
||||
gauges := Gauge{
|
||||
{"total photos", "The total number of photos", float64((*result).Photos)},
|
||||
{"total videos", "The total number of videos", float64((*result).Videos)},
|
||||
{"total usage", "The max number of active torrents allowed", float64((*result).Usage)},
|
||||
{"number users", "The total number of users", float64(len((*result).UsageByUser))},
|
||||
}
|
||||
|
||||
register(gauges, r)
|
||||
|
||||
user_info := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "immich_user_info",
|
||||
Help: "All infos about users",
|
||||
}, []string{"videos", "photos", "uid", "usage", "name"})
|
||||
|
||||
user_usage := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "immich_user_usage",
|
||||
Help: "The usage of the user",
|
||||
}, []string{"uid", "name"})
|
||||
user_photos := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "immich_user_photos",
|
||||
Help: "The number of photo of the user",
|
||||
}, []string{"uid", "name"})
|
||||
user_videos := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "immich_user_videos",
|
||||
Help: "The number of videos of the user",
|
||||
}, []string{"uid", "name"})
|
||||
|
||||
job_count := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "immich_job_count",
|
||||
Help: "The item count in the job",
|
||||
}, []string{"status", "job_name"})
|
||||
|
||||
r.MustRegister(user_info)
|
||||
r.MustRegister(user_usage)
|
||||
r.MustRegister(user_videos)
|
||||
r.MustRegister(user_photos)
|
||||
r.MustRegister(job_count)
|
||||
|
||||
for i := 0; i < len((*result).UsageByUser); i++ {
|
||||
var myuser = GetName((*result).UsageByUser[i].UserID, result2)
|
||||
user_info.With(prometheus.Labels{"videos": strconv.Itoa((*result).UsageByUser[i].Videos), "photos": strconv.Itoa((*result).UsageByUser[i].Photos), "uid": (*result).UsageByUser[i].UserID, "usage": strconv.Itoa(int((*result).UsageByUser[i].Usage)), "name": myuser.Name}).Inc()
|
||||
user_photos.With(prometheus.Labels{"uid": (*result).UsageByUser[i].UserID, "name": myuser.Name}).Set(float64((*result).UsageByUser[i].Photos))
|
||||
user_usage.With(prometheus.Labels{"uid": (*result).UsageByUser[i].UserID, "name": myuser.Name}).Set(float64((*result).UsageByUser[i].Usage))
|
||||
user_videos.With(prometheus.Labels{"uid": (*result).UsageByUser[i].UserID, "name": myuser.Name}).Set(float64((*result).UsageByUser[i].Videos))
|
||||
}
|
||||
|
||||
setJobStatusCounts(job_count, "background_task", &result3.BackgroundTask)
|
||||
setJobStatusCounts(job_count, "clip_encoding", &result3.ClipEncoding)
|
||||
setJobStatusCounts(job_count, "library", &result3.Library)
|
||||
setJobStatusCounts(job_count, "metadata_extraction", &result3.MetadataExtraction)
|
||||
setJobStatusCounts(job_count, "migration", &result3.Migration)
|
||||
setJobStatusCounts(job_count, "object_tagging", &result3.ObjectTagging)
|
||||
setJobStatusCounts(job_count, "recognize_faces", &result3.RecognizeFaces)
|
||||
setJobStatusCounts(job_count, "search", &result3.Search)
|
||||
setJobStatusCounts(job_count, "sidecar", &result3.Sidecar)
|
||||
setJobStatusCounts(job_count, "storage_template_migration", &result3.StorageTemplateMigration)
|
||||
setJobStatusCounts(job_count, "thumbnail_generation", &result3.ThumbnailGeneration)
|
||||
setJobStatusCounts(job_count, "video_conversion", &result3.VideoConversion)
|
||||
}
|
||||
|
||||
func setJobStatusCounts(job_count *prometheus.GaugeVec, jobName string, result *models.StructJobStatus) {
|
||||
job_count.With(prometheus.Labels{"status": "active", "job_name": jobName}).Set(float64(result.JobCounts.Active))
|
||||
job_count.With(prometheus.Labels{"status": "completed", "job_name": jobName}).Set(float64(result.JobCounts.Completed))
|
||||
job_count.With(prometheus.Labels{"status": "failed", "job_name": jobName}).Set(float64(result.JobCounts.Failed))
|
||||
job_count.With(prometheus.Labels{"status": "delayed", "job_name": jobName}).Set(float64(result.JobCounts.Delayed))
|
||||
job_count.With(prometheus.Labels{"status": "waiting", "job_name": jobName}).Set(float64(result.JobCounts.Waiting))
|
||||
job_count.With(prometheus.Labels{"status": "paused", "job_name": jobName}).Set(float64(result.JobCounts.Paused))
|
||||
}
|
||||
|
||||
func SendBackMessageserverVersion(result *models.StructServerVersion, r *prometheus.Registry) {
|
||||
|
||||
version := prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "immich_app_version",
|
||||
Help: "Immich version",
|
||||
ConstLabels: map[string]string{
|
||||
"version": strconv.Itoa((*result).Major) + "." + strconv.Itoa((*result).Minor) + "." + strconv.Itoa((*result).Patch),
|
||||
},
|
||||
})
|
||||
version.Set(1)
|
||||
r.MustRegister(version)
|
||||
|
||||
}
|
||||
|
||||
func GetName(result string, result2 *models.StructAllUsers) models.StructCustomUser {
|
||||
var myuser models.StructCustomUser
|
||||
for i := 0; i < len(*result2); i++ {
|
||||
if (*result2)[i].ID == result {
|
||||
|
||||
myuser.ID = (*result2)[i].ID
|
||||
myuser.Name = (*result2)[i].Name
|
||||
myuser.Email = (*result2)[i].Email
|
||||
myuser.IsAdmin = (*result2)[i].IsAdmin
|
||||
}
|
||||
|
||||
}
|
||||
return myuser
|
||||
}
|
||||
|
||||
func register(gauges Gauge, r *prometheus.Registry) {
|
||||
for _, gauge := range gauges {
|
||||
name := "immich_app_" + strings.Replace(gauge.name, " ", "_", -1)
|
||||
help := gauge.help
|
||||
g := prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: name,
|
||||
Help: help,
|
||||
})
|
||||
r.MustRegister(g)
|
||||
g.Set(gauge.value)
|
||||
}
|
||||
}
|
34
src/tests/prometheus_test.go
Normal file
34
src/tests/prometheus_test.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package prom
|
||||
|
||||
import (
|
||||
"immich-exp/models"
|
||||
prom "immich-exp/prometheus"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetName(t *testing.T) {
|
||||
result2 := &models.StructAllUsers{
|
||||
{ID: "1", Name: "John", Email: "john@example.com", IsAdmin: true},
|
||||
{ID: "2", Name: "Jane", Email: "jane@example.com", IsAdmin: false},
|
||||
}
|
||||
|
||||
result := "1"
|
||||
expected := models.StructCustomUser{
|
||||
ID: "1",
|
||||
Name: "John",
|
||||
Email: "john@example.com",
|
||||
IsAdmin: true,
|
||||
}
|
||||
actual := prom.GetName(result, result2)
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Errorf("Expected: %v, but got: %v", expected, actual)
|
||||
}
|
||||
|
||||
result = "3"
|
||||
expected = models.StructCustomUser{}
|
||||
actual = prom.GetName(result, result2)
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Errorf("Expected: %v, but got: %v", expected, actual)
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue