Compare commits

...

48 commits
v0.1.0 ... main

Author SHA1 Message Date
Simon Rieger
0c832c4dec
Merge branch 'martabal:main' into main 2024-01-25 13:15:54 +01:00
martin
e176ed4db9
refactor: project structure (#16) 2024-01-15 15:14:54 +01:00
martabal
560a8c093c
ci: publish to docker hub 2024-01-10 13:49:12 +01:00
Tim
bd5c47c7fc
chore: switch from Docker Hub examples to Github Container Registry (#15)
The docker hub images haven't been updated for several months, I ran into errors following the examples in the readme.
2024-01-10 13:48:37 +01:00
martabal
c59a3d23d6
chore: bump dependencies 2024-01-01 01:43:35 +01:00
martabal
d6a4b3cc6c
fix: docker tag 2023-12-23 22:28:19 +01:00
martabal
907ba3a708
chore: pump version 2023-12-23 21:59:44 +01:00
martin
7e3d56c3e0
ci: linter (#14)
* ci: add linter

* chore: update dependencies

* remove package.json

* feat: use enums

* ci: rename workflow
2023-12-23 21:58:57 +01:00
martabal
b52e18aca1
ci: more docker tags 2023-12-19 03:14:05 +01:00
martin
00a1ed912b
fix: reconnect automatically after disconnect (#13) 2023-12-19 01:55:45 +01:00
dependabot[bot]
0ea6cd556c
chore(deps): bump github/codeql-action from 2 to 3 (#11) 2023-12-14 12:30:37 +01:00
Simon Rieger
6fb59dd7be
Merge branch 'martabal:main' into main 2023-11-23 15:18:12 +01:00
martabal
035752eca9
ci: add automatic release 2023-11-22 01:25:06 +01:00
martabal
299192121a
chore: pump version 2023-11-21 19:52:57 +01:00
Pedro Pombeiro
0bf2b5a6ee
feat: export job status counts (#10) 2023-11-21 19:51:57 +01:00
martabal
5784e15fa0
chore: pump version 2023-11-15 00:55:08 +01:00
martin
e9d70573a7
fix: v1.86 (#9) 2023-11-14 13:41:21 +01:00
Simon Rieger
9ea3bac4af
Merge branch 'martabal:main' into main 2023-11-09 14:46:38 +01:00
martabal
e78aad9501
fix: api breaking change 2023-11-08 15:50:40 +01:00
dependabot[bot]
657181a728
chore(deps): bump docker/login-action from 2 to 3 (#7)
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 20:23:26 +02:00
dependabot[bot]
2d17c2f066
chore(deps): bump docker/setup-qemu-action from 2 to 3 (#5)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 20:23:21 +02:00
dependabot[bot]
8fb9f9d7f6
chore(deps): bump docker/build-push-action from 4 to 5 (#4)
Dependabot couldn't find the original pull request head commit, b46e7d77a5c9f6dcc1b146dc72caa32c26421cfe.

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 20:23:18 +02:00
dependabot[bot]
edd88f3883
chore(deps): bump docker/setup-buildx-action from 2 to 3 (#3)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 20:23:08 +02:00
dependabot[bot]
17ecce3cc6
chore(deps): bump actions/checkout from 2 to 4 (#6)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 20:23:03 +02:00
martabal
809e084698
refactor: simplify data registration 2023-10-19 20:19:59 +02:00
Simon Rieger
4cbc006e84
refactor: update grafana dashboard (#2)
* grafana/dashboard.json aktualisiert

rename to "number of videos"
add refresh and extend timestamp

* README.md aktualisiert

Rename IMMICH_URL to IMMICH_BASE_URL

---------

Co-authored-by: Simon Rieger <simono41@noreply.localhost>
2023-10-18 23:29:56 +02:00
Simon Rieger
2865d787d7 README.md aktualisiert
Rename IMMICH_URL to IMMICH_BASE_URL
2023-10-18 22:48:23 +02:00
Simon Rieger
fa224e66e4 grafana/dashboard.json aktualisiert
rename to "number of videos"
add refresh and extend timestamp
2023-10-18 22:47:15 +02:00
martin
c880ba2e36
chore: update dependencies
Signed-off-by: martin <martin.labat92@gmail.com>
2023-07-06 12:05:57 +02:00
martabal
8a7ebfefa1
feat: up version
Signed-off-by: martabal <74269598+martabal@users.noreply.github.com>
2023-06-21 23:29:41 +02:00
martin
cf29943ebe
fix(dashboard): show total photos & videos (#1) 2023-02-22 21:48:50 +01:00
martin
3b4e9143be
up version release
Signed-off-by: martin <martin.labat92@gmail.com>
2023-02-19 12:17:31 +01:00
martin
05a3dd2501
fix: correct metric name for immich version
Signed-off-by: martin <martin.labat92@gmail.com>
2023-02-19 12:16:32 +01:00
martin
e6870170aa
chore: add license
Signed-off-by: martin <martin.labat92@gmail.com>
2023-02-18 19:29:07 +01:00
martin
81b4b9be55
refactor: remove main.go
Signed-off-by: martin <martin.labat92@gmail.com>
2023-02-17 20:22:56 +01:00
martin
abce8035f3
refactor: remove main.go
Signed-off-by: martin <martin.labat92@gmail.com>
2023-02-17 20:20:36 +01:00
martin
b5ced9df2c
refactor
Signed-off-by: martin <martin.labat92@gmail.com>
2023-02-15 20:46:01 +01:00
martin
5b426932cf
Merge branch 'main' of github.com:martabal/immich-exporter 2023-02-15 20:45:33 +01:00
martin
be9d6fa0dc
refactor
Signed-off-by: martin <martin.labat92@gmail.com>
2023-02-15 20:45:26 +01:00
martin
ad306c90e3
fix: scripts
Signed-off-by: martin <martin.labat92@gmail.com>
2023-02-14 00:45:27 +01:00
martin
4022be77ae
feat: use package.json to show project infos
Signed-off-by: martin <martin.labat92@gmail.com>
2023-02-13 18:18:09 +01:00
martin
bf3ea6ad35
pump version
Signed-off-by: martin <martin.labat92@gmail.com>
2023-02-13 18:01:48 +01:00
martin
107112b139
feat: use goroutines
Signed-off-by: martin <martin.labat92@gmail.com>
2023-02-13 13:46:10 +01:00
martin
3749b1eef9
feat: use prometheus
Signed-off-by: martin <martin.labat92@gmail.com>
2023-02-13 11:33:45 +01:00
martin
5c230986d0
wip
Signed-off-by: martin <martin.labat92@gmail.com>
2023-02-13 00:59:12 +01:00
martin
a3f089ed86
wip
Signed-off-by: martin <martin.labat92@gmail.com>
2023-02-12 23:36:01 +01:00
martin
a29621a3b3
fix(workflowss): correct repo
Signed-off-by: martin <martin.labat92@gmail.com>
2023-02-11 01:02:16 +01:00
martin
e1e068dd5f
fix: README
Signed-off-by: martin <martin.labat92@gmail.com>
2023-02-10 23:55:21 +01:00
30 changed files with 1705 additions and 1157 deletions

View file

@ -1,4 +1,10 @@
img/
# folders
grafana/
.github/
Dockerfile
img/
# files
.env*
.gitignore
Dockerfile
README.md

View file

@ -1,3 +1,2 @@
IMMICH_USERNAME=
IMMICH_PASSWORD=
IMMICH_API_KEY=
IMMICH_BASE_URL=

6
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

16
.github/workflows/build.yml vendored Normal file
View 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
View 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
View 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 }}

View file

@ -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
View 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

View file

@ -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
View 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
View 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

View file

@ -1,25 +1,28 @@
# immich-exporter
[![manual push](https://github.com/martabal/immich-exporter/actions/workflows/push_docker.yml/badge.svg)](https://github.com/martabal/immich-exporter/actions/workflows/push_docker.yml)
[![Publish Release](https://github.com/martabal/immich-exporter/actions/workflows/docker.yml/badge.svg)](https://github.com/martabal/immich-exporter/actions/workflows/docker.yml)
[![Build](https://github.com/martabal/immich-exporter/actions/workflows/build.yml/badge.svg)](https://github.com/martabal/immich-exporter/actions/workflows/build.yml)
[![Test](https://github.com/martabal/immich-exporter/actions/workflows/test.yml/badge.svg)](https://github.com/martabal/immich-exporter/actions/workflows/test.yml)
<p align="center">
<img src="img/immich.png" width=100> &nbsp; <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
View file

@ -1,5 +0,0 @@
module immich-exporter
go 1.20
require github.com/joho/godotenv v1.5.1

2
go.sum
View file

@ -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

View file

@ -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
View 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
View 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=

View file

@ -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)
}
}
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
View 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
}

View file

@ -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
View 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
}

View file

@ -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
}

View 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)
}
}

View 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)
}
}