commit f3eb2cef6875512e68efa079083fc47feb308515 Author: martin Date: Fri Feb 10 22:47:58 2023 +0100 initial commit Signed-off-by: martin diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d22e30d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +img/ +grafana/ +.github/ +Dockerfile \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..755743f --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +IMMICH_USERNAME= +IMMICH_PASSWORD= +IMMICH_BASE_URL= \ No newline at end of file diff --git a/.github/workflows/push_docker.yml b/.github/workflows/push_docker.yml new file mode 100644 index 0000000..7f29356 --- /dev/null +++ b/.github/workflows/push_docker.yml @@ -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 + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67d6924 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..87c63fb --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ff64fd --- /dev/null +++ b/README.md @@ -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) + +

+   +

+ +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='' \ + -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='' + - 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) | diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b024225 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module immich-exporter + +go 1.20 + +require github.com/joho/godotenv v1.5.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d61b19e --- /dev/null +++ b/go.sum @@ -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= diff --git a/grafana/dashboard.json b/grafana/dashboard.json new file mode 100644 index 0000000..6f2cb62 --- /dev/null +++ b/grafana/dashboard.json @@ -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": "" + } \ No newline at end of file diff --git a/img/golang.png b/img/golang.png new file mode 100644 index 0000000..f335009 Binary files /dev/null and b/img/golang.png differ diff --git a/img/immich.png b/img/immich.png new file mode 100644 index 0000000..d4e7085 Binary files /dev/null and b/img/immich.png differ diff --git a/img/prometheus.png b/img/prometheus.png new file mode 100644 index 0000000..72cb7b6 Binary files /dev/null and b/img/prometheus.png differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..1de3af6 --- /dev/null +++ b/package.json @@ -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" +} diff --git a/src/immich/auth.go b/src/immich/auth.go new file mode 100644 index 0000000..cb11915 --- /dev/null +++ b/src/immich/auth.go @@ -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) + } + + } + + } + +} diff --git a/src/immich/data.go b/src/immich/data.go new file mode 100644 index 0000000..0a50f7b --- /dev/null +++ b/src/immich/data.go @@ -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 + + } + + } + +} diff --git a/src/immich/sendbackmessage.go b/src/immich/sendbackmessage.go new file mode 100644 index 0000000..2868829 --- /dev/null +++ b/src/immich/sendbackmessage.go @@ -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 +} diff --git a/src/init.go b/src/init.go new file mode 100644 index 0000000..7b38288 --- /dev/null +++ b/src/init.go @@ -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) + +} diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..7d5db30 --- /dev/null +++ b/src/main.go @@ -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) +} diff --git a/src/models/api.go b/src/models/api.go new file mode 100644 index 0000000..bb7f468 --- /dev/null +++ b/src/models/api.go @@ -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 +} diff --git a/src/models/error.go b/src/models/error.go new file mode 100644 index 0000000..f60f316 --- /dev/null +++ b/src/models/error.go @@ -0,0 +1,11 @@ +package models + +var myerr bool + +func SetPromptError(prompt bool) { + myerr = prompt +} + +func GetPromptError() bool { + return myerr +} diff --git a/src/models/models.go b/src/models/models.go new file mode 100644 index 0000000..ffc0020 --- /dev/null +++ b/src/models/models.go @@ -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 +}