initial commit
Signed-off-by: martin <martin.labat92@gmail.com>
This commit is contained in:
commit
f3eb2cef68
21 changed files with 1385 additions and 0 deletions
4
.dockerignore
Normal file
4
.dockerignore
Normal file
|
@ -0,0 +1,4 @@
|
|||
img/
|
||||
grafana/
|
||||
.github/
|
||||
Dockerfile
|
3
.env.example
Normal file
3
.env.example
Normal file
|
@ -0,0 +1,3 @@
|
|||
IMMICH_USERNAME=
|
||||
IMMICH_PASSWORD=
|
||||
IMMICH_BASE_URL=
|
47
.github/workflows/push_docker.yml
vendored
Normal file
47
.github/workflows/push_docker.yml
vendored
Normal file
|
@ -0,0 +1,47 @@
|
|||
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
|
||||
|
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
.env
|
14
Dockerfile
Normal file
14
Dockerfile
Normal file
|
@ -0,0 +1,14 @@
|
|||
FROM golang:1.19-alpine3.17 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go get -d -v ./src/ && \
|
||||
go build -o /go/bin/immich-exporter ./src
|
||||
|
||||
FROM alpine:3.17
|
||||
|
||||
COPY --from=builder /go/bin/immich-exporter /go/bin/immich-exporter
|
||||
|
||||
CMD ["/go/bin/immich-exporter"]
|
74
README.md
Normal file
74
README.md
Normal file
|
@ -0,0 +1,74 @@
|
|||
# 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)
|
||||
|
||||
<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)
|
||||
|
||||
## Run it
|
||||
|
||||
### 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 \
|
||||
-p 8090:8090 \
|
||||
martabal/immich-exporter
|
||||
```
|
||||
|
||||
### Docker-compose
|
||||
|
||||
```yaml
|
||||
version: "2.1"
|
||||
services:
|
||||
immich:
|
||||
image: martabal/immich-exporter:latest
|
||||
container_name: immich-exporter
|
||||
environment:
|
||||
- IMMICH_URL=http://192.168.1.10:8080
|
||||
- IMMICH_PASSWORD='<your_password>'
|
||||
- IMMICH_USERNAME=admin
|
||||
ports:
|
||||
- 8090:8090
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
### Without docker
|
||||
|
||||
```sh
|
||||
git clone https://github.com/martabal/immich-exporter.git
|
||||
cd immich-exporter/
|
||||
go get -d -v
|
||||
cd src
|
||||
go build -o ./immich-exporter
|
||||
./immich-exporter
|
||||
```
|
||||
|
||||
If you want to use an .env file, edit `.env.example` to match your setup, rename it `.env` then run it with :
|
||||
|
||||
```sh
|
||||
./immich-exporter -e
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Parameters | Function |
|
||||
| :-----: | ----- |
|
||||
| `-p 8090` | Webservice port |
|
||||
| `-e IMMICH_USERNAME` | Immich username |
|
||||
| `-e IMMICH_PASSWORD` | Immich password |
|
||||
| `-e IMMICH_BASE_URL` | Immich base URL |
|
||||
|
||||
### Arguments
|
||||
|
||||
| Arguments | Function |
|
||||
| :-----: | ----- |
|
||||
| -e | Use a .env file containing environment variables (.env file must be placed in the same directory) |
|
5
go.mod
Normal file
5
go.mod
Normal file
|
@ -0,0 +1,5 @@
|
|||
module immich-exporter
|
||||
|
||||
go 1.20
|
||||
|
||||
require github.com/joho/godotenv v1.5.1
|
2
go.sum
Normal file
2
go.sum
Normal file
|
@ -0,0 +1,2 @@
|
|||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
698
grafana/dashboard.json
Normal file
698
grafana/dashboard.json
Normal file
|
@ -0,0 +1,698 @@
|
|||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 65,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 10,
|
||||
"title": "Row title",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "zBtc3iPnk"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "string"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 6,
|
||||
"w": 9,
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"first"
|
||||
],
|
||||
"fields": "/^version$/",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "9.3.6",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "zBtc3iPnk"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": false,
|
||||
"expr": "immich_app_version",
|
||||
"format": "table",
|
||||
"instant": true,
|
||||
"legendFormat": "{{version}}",
|
||||
"range": false,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Immich version",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "zBtc3iPnk"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "string"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 6,
|
||||
"w": 5,
|
||||
"x": 9,
|
||||
"y": 1
|
||||
},
|
||||
"id": 13,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"first"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "9.3.6",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "zBtc3iPnk"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": false,
|
||||
"expr": "immich_app_nb_users",
|
||||
"format": "table",
|
||||
"instant": true,
|
||||
"legendFormat": "__auto",
|
||||
"range": false,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Number of users",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "zBtc3iPnk"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"displayMode": "auto",
|
||||
"filterable": false,
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Time"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hidden",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "__name__"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hidden",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "instance"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hidden",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Value"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hidden",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "firstname"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "First name"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "job"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hidden",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "lastname"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Last name"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "usage"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "unit",
|
||||
"value": "bytes"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 6,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 7
|
||||
},
|
||||
"id": 15,
|
||||
"options": {
|
||||
"footer": {
|
||||
"fields": "",
|
||||
"reducer": [
|
||||
"sum"
|
||||
],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true,
|
||||
"sortBy": [
|
||||
{
|
||||
"desc": false,
|
||||
"displayName": "Time"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pluginVersion": "9.3.6",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "zBtc3iPnk"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": false,
|
||||
"expr": "immich_user_info",
|
||||
"format": "table",
|
||||
"instant": true,
|
||||
"legendFormat": "",
|
||||
"range": false,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Users details",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "zBtc3iPnk"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "bytes"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 13
|
||||
},
|
||||
"id": 12,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "zBtc3iPnk"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": false,
|
||||
"expr": "immich_user_usage",
|
||||
"instant": true,
|
||||
"legendFormat": "{{firstname}} {{lastname}}",
|
||||
"range": false,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "User usage",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "zBtc3iPnk"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 13
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": false
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "zBtc3iPnk"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "immich_app_total_photos + immich_app_total_videos",
|
||||
"legendFormat": "__auto",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Total assets",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "zBtc3iPnk"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
}
|
||||
},
|
||||
"mappings": []
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 22
|
||||
},
|
||||
"id": 8,
|
||||
"options": {
|
||||
"legend": {
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"pieType": "pie",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "zBtc3iPnk"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "immich_app_total_photos",
|
||||
"legendFormat": "Photos",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "zBtc3iPnk"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "immich_app_total_videos",
|
||||
"hide": false,
|
||||
"legendFormat": "Videos",
|
||||
"range": true,
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Proportion Photos / Videos",
|
||||
"type": "piechart"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "zBtc3iPnk"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
}
|
||||
},
|
||||
"mappings": []
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 22
|
||||
},
|
||||
"id": 6,
|
||||
"options": {
|
||||
"legend": {
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"pieType": "pie",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "zBtc3iPnk"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": false,
|
||||
"expr": "immich_user_usage",
|
||||
"instant": true,
|
||||
"legendFormat": "{{firstname}} {{lastname}}",
|
||||
"range": false,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Usage by user",
|
||||
"type": "piechart"
|
||||
}
|
||||
],
|
||||
"refresh": false,
|
||||
"schemaVersion": 37,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Immich",
|
||||
"uid": "9QXCv3AVk",
|
||||
"version": 7,
|
||||
"weekStart": ""
|
||||
}
|
BIN
img/golang.png
Normal file
BIN
img/golang.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
BIN
img/immich.png
Normal file
BIN
img/immich.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 144 KiB |
BIN
img/prometheus.png
Normal file
BIN
img/prometheus.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 104 KiB |
19
package.json
Normal file
19
package.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"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"
|
||||
}
|
60
src/immich/auth.go
Normal file
60
src/immich/auth.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
166
src/immich/data.go
Normal file
166
src/immich/data.go
Normal file
|
@ -0,0 +1,166 @@
|
|||
package immich
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"immich-exporter/src/models"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Allrequests() string {
|
||||
|
||||
return serverversion() + Analyze()
|
||||
}
|
||||
|
||||
func Analyze() string {
|
||||
allusers, err := GetAllUsers()
|
||||
users, err2 := users()
|
||||
if err != nil && err2 != nil {
|
||||
return ""
|
||||
} else {
|
||||
return Sendbackmessagepreference(users, allusers)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
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")
|
||||
}
|
||||
|
||||
return &result, err
|
||||
|
||||
}
|
||||
}
|
||||
return &models.AllUsers{}, 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)
|
||||
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")
|
||||
}
|
||||
|
||||
return Sendbackmessageserverversion(&result)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if models.GetPromptError() == true {
|
||||
models.SetPromptError(false)
|
||||
}
|
||||
body, err := ioutil.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")
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
|
||||
}
|
||||
}
|
||||
return &models.Users{}, err
|
||||
}
|
||||
|
||||
func Apirequest(uri string, method string) (*http.Response, error) {
|
||||
|
||||
req, err := http.NewRequest(method, models.GetURL()+uri, nil)
|
||||
if err != nil {
|
||||
log.Fatalln("Error with url")
|
||||
}
|
||||
|
||||
req.AddCookie(&http.Cookie{Name: "immich_access_token", Value: models.GetAccessToken()})
|
||||
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())
|
||||
models.SetPromptError(true)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
46
src/immich/sendbackmessage.go
Normal file
46
src/immich/sendbackmessage.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
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
|
||||
}
|
70
src/init.go
Normal file
70
src/init.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"immich-exporter/src/immich"
|
||||
"immich-exporter/src/models"
|
||||
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func startup() {
|
||||
var envfile bool
|
||||
|
||||
flag.BoolVar(&envfile, "e", false, "Use .env file")
|
||||
flag.Parse()
|
||||
log.Println("Loading all parameters")
|
||||
if envfile {
|
||||
useenvfile()
|
||||
} else {
|
||||
initenv()
|
||||
}
|
||||
|
||||
immich.Auth()
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Error loading .env file")
|
||||
}
|
||||
log.Println("Using .env file")
|
||||
}
|
||||
|
||||
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.")
|
||||
|
||||
}
|
||||
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)
|
||||
|
||||
}
|
28
src/main.go
Normal file
28
src/main.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
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)
|
||||
}
|
64
src/models/api.go
Normal file
64
src/models/api.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Login struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
UserID string `json:"userId"`
|
||||
UserEmail string `json:"userEmail"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
ShouldChangePassword bool `json:"shouldChangePassword"`
|
||||
}
|
||||
|
||||
type Users struct {
|
||||
Photos int `json:"photos"`
|
||||
Videos int `json:"videos"`
|
||||
UsageByUser []struct {
|
||||
UserID string `json:"userId"`
|
||||
Videos int `json:"videos"`
|
||||
Photos int `json:"photos"`
|
||||
UsageRaw int64 `json:"usageRaw"`
|
||||
Usage string `json:"usage"`
|
||||
} `json:"usageByUser"`
|
||||
UsageRaw int64 `json:"usageRaw"`
|
||||
Usage string `json:"usage"`
|
||||
}
|
||||
|
||||
type ServerInfo struct {
|
||||
DiskAvailable string `json:"diskAvailable"`
|
||||
DiskSize string `json:"diskSize"`
|
||||
DiskUse string `json:"diskUse"`
|
||||
DiskAvailableRaw int64 `json:"diskAvailableRaw"`
|
||||
DiskSizeRaw int64 `json:"diskSizeRaw"`
|
||||
DiskUseRaw int64 `json:"diskUseRaw"`
|
||||
DiskUsagePercentage float64 `json:"diskUsagePercentage"`
|
||||
}
|
||||
|
||||
type ServerVersion struct {
|
||||
Major int `json:"major"`
|
||||
Minor int `json:"minor"`
|
||||
Patch int `json:"patch"`
|
||||
}
|
||||
|
||||
type AllUsers []struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ProfileImagePath string `json:"profileImagePath"`
|
||||
ShouldChangePassword bool `json:"shouldChangePassword"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
DeletedAt time.Time `json:"deletedAt"`
|
||||
OauthID string `json:"oauthId"`
|
||||
}
|
||||
|
||||
type CustomUser struct {
|
||||
Email string
|
||||
ID string
|
||||
FirstName string
|
||||
LastName string
|
||||
IsAdmin bool
|
||||
}
|
11
src/models/error.go
Normal file
11
src/models/error.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package models
|
||||
|
||||
var myerr bool
|
||||
|
||||
func SetPromptError(prompt bool) {
|
||||
myerr = prompt
|
||||
}
|
||||
|
||||
func GetPromptError() bool {
|
||||
return myerr
|
||||
}
|
51
src/models/models.go
Normal file
51
src/models/models.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
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
|
||||
}
|
Loading…
Reference in a new issue