diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ef9a210..0116d3e 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,7 @@ -From golang:1.21-alpine3.18 +ARG alpine_version=3.19 +ARG golang_version=1.21 + +FROM golang:${golang_version}-alpine${alpine_version} RUN apk add --no-cache \ git \ nano\ @@ -15,3 +18,7 @@ RUN adduser $USERNAME -s /bin/sh -D -u $USER_UID $USER_GID && \ chmod 0440 /etc/sudoers.d/$USERNAME USER $USERNAME + +RUN go install -v golang.org/x/tools/gopls@latest +RUN go install -v github.com/go-delve/delve/cmd/dlv@latest +RUN go install -v honnef.co/go/tools/cmd/staticcheck@latest diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b5adbd0..ae96fbb 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,10 +8,10 @@ "vscode": { "settings": {}, "extensions": [ - "ms-vscode.go", "eamodio.gitlens", "EditorConfig.EditorConfig", - "golang.Go" + "golang.Go", + "github.vscode-github-actions" ] } }, diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fd5ecc8..4a91552 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,7 +9,7 @@ updates: open-pull-requests-limit: 10 target-branch: development - package-ecosystem: gomod - directory: "/" + directory: "/src/" schedule: interval: weekly day: sunday diff --git a/.github/workflows/delete_PR_images.yml b/.github/workflows/delete_PR_images.yml new file mode 100644 index 0000000..8d79d7f --- /dev/null +++ b/.github/workflows/delete_PR_images.yml @@ -0,0 +1,35 @@ +name: Remove obsolet PR images from registry + +on: + pull_request: + types: [closed] + +env: + PACKAGE_NAME: docker-event-monitor + +jobs: + Delete_PR_image: + if: | + github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + permissions: + packages: write + steps: + - name: Get image ID of PR + id: version + run: | + curl -sSL \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/users/${{ github.repository_owner }}/packages/container/${{ env.PACKAGE_NAME }}/versions >> containerMeta.json ; + echo "VERSION_ID=$(jq -r '.[] | select(.metadata.container.tags[] == "pr-${{ github.event.pull_request.number }}").id' containerMeta.json)" >> "$GITHUB_ENV" ; + + - name: Delete PR image + uses: actions/delete-package-versions@v5.0.0 + if: ${{ env.VERSION_ID != '' }} + with: + package-version-ids: ${{ env.VERSION_ID }} + package-type: container + package-name: ${{ env.PACKAGE_NAME }} diff --git a/.github/workflows/delete_untagged_images.yml b/.github/workflows/delete_untagged_images.yml new file mode 100644 index 0000000..9071419 --- /dev/null +++ b/.github/workflows/delete_untagged_images.yml @@ -0,0 +1,31 @@ +name: Remove untagged images from registry + +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +env: + PACKAGE_NAME: docker-event-monitor + +jobs: + Delete_untagged_images: + runs-on: ubuntu-latest + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@v3.0.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.PAT_TOKEN }} + + - name: Delete all images from repository without tags + uses: Chizkiyahu/delete-untagged-ghcr-action@v3.2.0 + with: + token: ${{ secrets.PAT_TOKEN }} + repository_owner: ${{ github.repository_owner }} + repository: ${{ github.repository }} + package_name: ${{ env.PACKAGE_NAME }} + untagged_only: true + owner_type: user + except_untagged_multiplatform: true diff --git a/.github/workflows/ghcr.yml b/.github/workflows/ghcr.yml index 870505f..3172f67 100644 --- a/.github/workflows/ghcr.yml +++ b/.github/workflows/ghcr.yml @@ -5,54 +5,128 @@ on: release: types: [published] pull_request: + push: + branches: + - development + +env: + REGISTRY_IMAGE: ghcr.io/${{ github.repository }} jobs: - build-and-push: + build: runs-on: ubuntu-latest permissions: contents: read packages: write + strategy: + fail-fast: true + matrix: + include: + - platform: linux/amd64 + - platform: linux/arm64 + - platform: linux/arm/v6 + - platform: linux/arm/v7 + - platform: linux/386 steps: + - name: Prepare name for digest up/download + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + - name: Checkout Code - uses: actions/checkout@v3.6.0 + uses: actions/checkout@v4.1.1 - name: Set up QEMU - uses: docker/setup-qemu-action@v2.2.0 + uses: docker/setup-qemu-action@v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2.10.0 + uses: docker/setup-buildx-action@v3.0.0 + with: + buildkitd-flags: --debug - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: | - ghcr.io/${{ github.repository_owner }}/${{ github.repository }} - flavor: latest=${{ startsWith(github.ref, 'refs/tags/') }} - tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=sha, enable=${{ !startsWith(github.ref, 'refs/tags/') }} - type=edge - + ${{ env.REGISTRY_IMAGE }} - name: Login to GitHub Container Registry - if: github.event_name != 'pull_request' - uses: docker/login-action@v2.2.0 + uses: docker/login-action@v3.0.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Docker image - uses: docker/build-push-action@v4.1.1 + uses: docker/build-push-action@v5.1.0 + id: build with: context: . file: ./Dockerfile - platforms: linux/amd64, linux/arm64, linux/386, linux/arm/v7, linux/arm/v6 - push: ${{ github.event_name != 'pull_request' }} - cache-from: type=gha - cache-to: type=gha - provenance: false - tags: ${{ steps.meta.outputs.tags }} + platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} + provenance: false + outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' || github.event_name == 'release' }} + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + - name: Upload digest + uses: actions/upload-artifact@v4.3.0 + with: + name: digests-${{ env.PLATFORM_PAIR }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + needs: + - build + if: | + github.actor != 'dependabot[bot]' + && ( github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' || github.event_name == 'release' ) + permissions: + contents: read + packages: write + steps: + - name: Download digests + uses: actions/download-artifact@v4.1.1 + with: + path: /tmp/digests + pattern: digests-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.0.0 + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.REGISTRY_IMAGE }} + flavor: latest=${{ startsWith(github.ref, 'refs/tags/') }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha,enable=${{ github.event_name == 'workflow_dispatch' }} + type=ref,event=pr + type=ref,event=branch + - name: Login to GitHub Container Registry + uses: docker/login-action@v3.0.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) + - name: Inspect image + run: | + docker buildx imagetools inspect --raw ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }} + + diff --git a/.github/workflows/sync-back-to-dev.yml b/.github/workflows/sync-back-to-dev.yml index c8cb3b5..8d36bb7 100644 --- a/.github/workflows/sync-back-to-dev.yml +++ b/.github/workflows/sync-back-to-dev.yml @@ -18,7 +18,7 @@ jobs: name: Syncing branches steps: - name: Checkout - uses: actions/checkout@v3.6.0 + uses: actions/checkout@v4.1.1 - name: Opening pull request run: gh pr create -B development -H main --title 'Sync main back into development' --body 'Created by Github action' --label 'internal' env: diff --git a/Dockerfile b/Dockerfile index cfec690..95ebb29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,14 @@ -From golang:1.21-alpine3.18 as builder +ARG alpine_version=3.19 +ARG golang_version=1.21 + +FROM golang:${golang_version}-alpine${alpine_version} as builder COPY /src /src WORKDIR /src RUN go mod download RUN CGO_ENABLED=0 go build -ldflags "-s -w" docker-event-monitor.go -From scratch as deploy +FROM scratch as deploy COPY --from=builder /src/docker-event-monitor docker-event-monitor # the tls certificates: # this pulls directly from the upstream image, which already has ca-certificates: diff --git a/Readme.md b/Readme.md index db7d3bc..f5d7234 100644 --- a/Readme.md +++ b/Readme.md @@ -7,13 +7,14 @@ Monitor Docker events and send push notifications for each event. ## Features - Small memory and CPU footprint -- Pushover integration -- Gotify integration +- Pushover notification +- Gotify notification +- E-Mail notification (SMTP) - Filter events ## Background -I've been using [Monocker](https://github.com/petersem/monocker) to monitor my Docker containers and get push notifications on status changes. However, it's using polling (with hard lower limit of 10 sec) to poll for status changes. This is too long to catch all status changes (e.g. watchtower updating an container). While I did remove the limit in my own Monocker fork, I noticed that the CPU usage goes up quite a bit for polling times < 1sec. +I've been using [Monocker](https://github.com/petersem/monocker) to monitor my Docker containers and get push notifications on status changes. However, it's using polling (with hard lower limit of 10 sec) to poll for status changes. This is too long to catch all status changes (e.g. watchtower updating a container). While I did remove the limit in my own Monocker fork, I noticed that the CPU usage goes up quite a bit for polling times < 1sec. I needed another soultion, and found [docker-events-notifier](https://github.com/hasnat/docker-events-notifier), but Pushover integration was missing. So I started to develop my own solution which ended up being a `bash` script doing exactly what I wanted it to do (you can still find it in `/legacy/`). However, the used `jq` caused CPU spikes for each processed event. As I could not find a good solution, I decied to write my own application and to learn something new - [Go](https://go.dev/). @@ -29,7 +30,7 @@ The application uses Docker's API to connect to the [event stream](https://docs. The simplest way to use the docker event monitor is to run the docker container. It'a very small ( < 10MB) image. You can download it via ```shell -docker pull ghcr.io/yubiuser/yubiuser/docker-event-monitor:latest +docker pull ghcr.io/yubiuser/docker-event-monitor:latest ``` ### Docker compose @@ -40,7 +41,7 @@ version: '2.4' services: docker-event-monitor: container_name: docker-event-monitor - image: ghcr.io/yubiuser/yubiuser/docker-event-monitor:latest + image: ghcr.io/yubiuser/docker-event-monitor:latest volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - /etc/localtime:/etc/localtime:ro @@ -52,6 +53,13 @@ services: GOTIFY: false GOTIFY_URL: 'URL' GOTIFY_TOKEN: 'TOKEN' + MAIL: false + MAIL_FROM: 'your.username@provider.com' + MAIL_TO: 'recipient@provider.com' + MAIL_USER: 'SMTP USER' + MAIL_PASSWORD: 'PASSWORD' + MAIL_PORT: 587 + MAIL_HOST: 'smtp@provider.com' FILTER: 'event=start,event=stop,type=container' DELAY: '500ms' LOG_LEVEL: 'info' @@ -74,14 +82,22 @@ If you have a suitable `Go` environment set up, you can build the binary from `/ Configurations can use the CLI flags or environment variables. The table below outlines all supported options and their respective env vars. -| Flag | Env Variable | Default | Details | -| ---------------- | ---------------------- | ------- |-------- | -| `--pushover` | `PUSHOVER` | `false` |Enable/Disable Pushover notification| -| `--pushoverapitoken` | `PUSHOVER_APITOKEN` | `""` | | -| `--pushoveruserkey` | `PUSHOVER_USER` | `""` | | -| `--delay` | `DELAY` | `500ms` |Delay befor processing next event. Can be useful if messages arrive in wrong order | -| `--gotify` | `GOTIFY` | `false` |Enable/Disable Gotify notification| -| `--gotifyurl` | `GOTIFY_URL` | `""` | | -| `--gotifytoken` | `GOTIFY_TOKEN` | `""` | | -| `--filter` | `FILTER` | `""` | Filter events. Uses the same filters as `docker events` (see [here](https://docs.docker.com/engine/reference/commandline/events/#filter)) | -| `--loglevel` | `LOG_LEVEL` | `"info"`| Use `debug` for more verbose logging` | +| Flag | Env Variable | Default | Details | +| ---------------- | ---------------------- | ------- |-------- | +| `--pushover` | `PUSHOVER` | `false` | Enable/Disable Pushover notification| +| `--pushoverapitoken` | `PUSHOVER_APITOKEN` | `""` | | +| `--pushoveruserkey` | `PUSHOVER_USER` | `""` | | +| `--delay` | `DELAY` | `500ms` | Delay befor processing next event. Can be useful if messages arrive in wrong order | +| `--gotify` | `GOTIFY` | `false` | Enable/Disable Gotify notification| +| `--gotifyurl` | `GOTIFY_URL` | `""` | | +| `--gotifytoken` | `GOTIFY_TOKEN` | `""` | | +| `--mail` | `MAIL` | `false` | Enable/Disable E-Mail (SMTP) notification| +| `--mailfrom` | `MAIL_FROM` | `""` | optional: `your.username@provider.com`, set to MAIL_USER if empty/unset | +| `--mailto` | `MAIL_TO` | `""` | `recipient@provider.com` | +| `--mailuser` | `MAIL_USER` | `""` | SMTP username | +| `--mailpassword` | `MAIL_PASSWORD` | `""` | | +| `--mailport` | `MAIL_PORT` | `587` | | +| `--mailhost` | `MAIL_HOST` | `""` | `smtp@provider.com` | +| `--filter` | `FILTER` | `""` | Filter events. Uses the same filters as `docker events` (see [here](https://docs.docker.com/engine/reference/commandline/events/#filter)) | +| `--loglevel` | `LOG_LEVEL` | `"info"`| Use `debug` for more verbose logging | +| `--servertag` | `SERVER_TAG` | `""` | Prefix to include in the title of notifications. Useful when running docker-event-monitors on multiple machines | diff --git a/docker-compose.yml b/docker-compose.yml index 6cd5dec..5233e27 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: '2.4' services: docker-event-monitor: container_name: docker-event-monitor - image: ghcr.io/yubiuser/yubiuser/docker-event-monitor:latest + image: ghcr.io/yubiuser/docker-event-monitor:latest volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - /etc/localtime:/etc/localtime:ro @@ -15,6 +15,13 @@ services: GOTIFY: false GOTIFY_URL: 'URL' GOTIFY_TOKEN: 'TOKEN' + MAIL: false + MAIL_FROM: 'your.username@provider.com' + MAIL_TO: 'recipient@provider.com' + MAIL_USER: 'SMTP USER' + MAIL_PASSWORD: 'PASSWORD' + MAIL_PORT: 587 + MAIL_HOST: 'smtp@provider.com' FILTER: 'event=start,event=stop,type=container' DELAY: '500ms' LOG_LEVEL: 'info' diff --git a/src/docker-event-monitor.go b/src/docker-event-monitor.go index f2299c1..7fafc67 100644 --- a/src/docker-event-monitor.go +++ b/src/docker-event-monitor.go @@ -2,11 +2,12 @@ package main import ( "context" - "fmt" "io" "net/http" + "net/smtp" "net/url" "os" + "strconv" "strings" "sync" "time" @@ -18,7 +19,8 @@ import ( "github.com/docker/docker/client" "github.com/gregdel/pushover" - log "github.com/sirupsen/logrus" + "github.com/rs/zerolog" + "golang.org/x/text/cases" "golang.org/x/text/language" ) @@ -30,49 +32,113 @@ type args struct { Gotify bool `arg:"env:GOTIFY" default:"false" help:"Enable/Disable Gotify Notification (True/False)"` GotifyURL string `arg:"env:GOTIFY_URL" help:"URL of your Gotify server"` GotifyToken string `arg:"env:GOTIFY_TOKEN" help:"Gotify's App Token"` + Mail bool `arg:"env:MAIL" default:"false" help:"Enable/Disable Mail (SMTP) Notification (True/False)"` + MailFrom string `arg:"env:MAIL_FROM" help:"your.username@provider.com"` + MailTo string `arg:"env:MAIL_TO" help:"recipient@provider.com"` + MailUser string `arg:"env:MAIL_USER" help:"SMTP Username"` + MailPassword string `arg:"env:MAIL_PASSWORD" help:"SMTP Password"` + MailPort int `arg:"env:MAIL_PORT" default:"587" help:"SMTP Port"` + MailHost string `arg:"env:MAIL_HOST" help:"SMTP Host"` Delay time.Duration `arg:"env:DELAY" default:"500ms" help:"Delay before next message is send"` FilterStrings []string `arg:"env:FILTER,--filter,separate" help:"Filter docker events using Docker syntax."` Filter map[string][]string `arg:"-"` LogLevel string `arg:"env:LOG_LEVEL" default:"info" help:"Set log level. Use debug for more logging."` + ServerTag string `arg:"env:SERVER_TAG" help:"Prefix to include in the title of notifications. Useful when running docker-event-monitors on multiple machines."` } -func main() { - args := parseArgs() - var wg sync.WaitGroup +// Creating a global logger +var logger zerolog.Logger - log.Infof("Starting docker event monitor") +// hold the supplied run-time arguments globally +var glb_arguments args - if args.Pushover { - log.Infof("Using Pushover API Token %s", args.PushoverAPIToken) - log.Infof("Using Pushover User Key %s", args.PushoverUserKey) - } else { - log.Info("Pushover notification disabled") - } +func init() { + parseArgs() - if args.Gotify { - log.Infof("Using Gotify APP Token %s", args.GotifyToken) - log.Infof("Using Gotify URL %s", args.GotifyURL) - } else { - log.Info("Gotify notification disabled") + configureLogger(glb_arguments.LogLevel) + + if glb_arguments.Pushover { + if len(glb_arguments.PushoverAPIToken) == 0 { + logger.Fatal().Msg("Pushover enabled. Pushover API token required!") + } + if len(glb_arguments.PushoverUserKey) == 0 { + logger.Fatal().Msg("Pushover enabled. Pushover user key required!") + } } - if args.Delay > 0 { - log.Infof("Using delay of %v", args.Delay) + if glb_arguments.Gotify { + if len(glb_arguments.GotifyURL) == 0 { + logger.Fatal().Msg("Gotify enabled. Gotify URL required!") + } + if len(glb_arguments.GotifyToken) == 0 { + logger.Fatal().Msg("Gotify enabled. Gotify APP token required!") + } } + if glb_arguments.Mail { + if len(glb_arguments.MailUser) == 0 { + logger.Fatal().Msg("E-Mail notification enabled. SMTP username required!") + } + if len(glb_arguments.MailTo) == 0 { + logger.Fatal().Msg("E-Mail notification enabled. Recipient address required!") + } + if len(glb_arguments.MailFrom) == 0 { + glb_arguments.MailFrom = glb_arguments.MailUser + } + if len(glb_arguments.MailPassword) == 0 { + logger.Fatal().Msg("E-Mail notification enabled. SMTP Password required!") + } + if len(glb_arguments.MailHost) == 0 { + logger.Fatal().Msg("E-Mail notification enabled. SMTP host address required!") + } + } +} + +func main() { + + logger.Info(). + Dict("options", zerolog.Dict(). + Dict("reporter", zerolog.Dict(). + Dict("Pushover", zerolog.Dict(). + Bool("enabled", glb_arguments.Pushover). + Str("PushoverAPIToken", glb_arguments.PushoverAPIToken). + Str("PushoverUserKey", glb_arguments.PushoverUserKey), + ). + Dict("Gotify", zerolog.Dict(). + Bool("enabled", glb_arguments.Gotify). + Str("GotifyURL", glb_arguments.GotifyURL). + Str("GotifyToken", glb_arguments.GotifyToken), + ). + Dict("Mail", zerolog.Dict(). + Bool("enabled", glb_arguments.Mail). + Str("MailFrom", glb_arguments.MailFrom). + Str("MailTo", glb_arguments.MailTo). + Str("MailHost", glb_arguments.MailHost). + Str("MailUser", glb_arguments.MailUser). + Int("Port", glb_arguments.MailPort), + ), + ). + Str("Delay", glb_arguments.Delay.String()). + Str("Loglevel", glb_arguments.LogLevel). + Str("ServerTag", glb_arguments.ServerTag). + Str("Filter", strings.Join(glb_arguments.FilterStrings, " ")), + ). + Msg("Docker event monitor started") + + timestamp := time.Now() + startup_message := buildStartupMessage(timestamp) + sendNotifications(timestamp, startup_message, "Starting docker event monitor") filterArgs := filters.NewArgs() - for key, values := range args.Filter { + for key, values := range glb_arguments.Filter { for _, value := range values { filterArgs.Add(key, value) } } - log.Debugf("filterArgs = %v", filterArgs) - sendNotifications(&args, time.Now().Format("02-01-2006 15:04:05"), "Starting docker event monitor", &wg) - - cli, err := client.NewClientWithOpts(client.FromEnv) + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { - log.Fatal(err) + logger.Fatal().Err(err).Msg("") } + defer cli.Close() // receives events from the channel event_chan, errs := cli.Events(context.Background(), types.EventsOptions{Filters: filterArgs}) @@ -80,46 +146,134 @@ func main() { for { select { case err := <-errs: - log.Fatal(err) + logger.Fatal().Err(err).Msg("") case event := <-event_chan: - processEvent(&args, &event, &wg) - // Adding a small configurable delay here - // Sometimes events are pushed through the channel really quickly, but - // they arrive on the clients in wrong order (probably due to message delivery time) - // This affects mostly Pushover - // Consuming the events with a small delay solves the issue - if args.Delay > 0 { - time.Sleep(args.Delay) - } + processEvent(&event) } } } -func sendNotifications(args *args, message, title string, wg *sync.WaitGroup) { +func buildStartupMessage(timestamp time.Time) string { + var startup_message_builder strings.Builder + + startup_message_builder.WriteString("Docker event monitor started at " + timestamp.Format(time.RFC1123Z) + "\n") + + if glb_arguments.Pushover { + startup_message_builder.WriteString("Notify via Pushover, using API Token " + glb_arguments.PushoverAPIToken + " and user key " + glb_arguments.PushoverUserKey) + } else { + startup_message_builder.WriteString("Pushover notification disabled") + } + + if glb_arguments.Gotify { + startup_message_builder.WriteString("\nNotify via Gotify, using URL " + glb_arguments.GotifyURL + " and APP Token " + glb_arguments.GotifyToken) + } else { + startup_message_builder.WriteString("\nGotify notification disabled") + } + if glb_arguments.Mail { + startup_message_builder.WriteString("\nNotify via E-Mail from " + glb_arguments.MailFrom + " to " + glb_arguments.MailTo + " using host " + glb_arguments.MailHost + " and port " + strconv.Itoa(glb_arguments.MailPort)) + } else { + startup_message_builder.WriteString("\nE-Mail notification disabled") + } + + if glb_arguments.Delay > 0 { + startup_message_builder.WriteString("\nUsing delay of " + glb_arguments.Delay.String()) + } else { + startup_message_builder.WriteString("\nDelay disabled") + } + + startup_message_builder.WriteString("\nLog level: " + glb_arguments.LogLevel) + + if glb_arguments.ServerTag != "" { + startup_message_builder.WriteString("\nServerTag: " + glb_arguments.ServerTag) + } else { + startup_message_builder.WriteString("\nServerTag: none") + } + + return startup_message_builder.String() +} + +func sendNotifications(timestamp time.Time, message string, title string) { // Sending messages to different services as goroutines concurrently // Adding a wait group here to delay execution until all functions return, - // otherwise the delay in main() would not use its full time + // otherwise delaying in processEvent() would not make any sense - if args.Pushover { + var wg sync.WaitGroup + + // If there is a server tag, add it to the title + if len(glb_arguments.ServerTag) > 0 { + title = "[" + glb_arguments.ServerTag + "] " + title + } + + if glb_arguments.Pushover { + wg.Add(1) + go func() { + defer wg.Done() + sendPushover(message, title) + }() + } + + if glb_arguments.Gotify { wg.Add(1) - go sendPushover(args, message, title, wg) + go func() { + defer wg.Done() + sendGotify(message, title) + }() } - if args.Gotify { + if glb_arguments.Mail { wg.Add(1) - go sendGotify(args, message, title, wg) + go func() { + defer wg.Done() + sendMail(timestamp, message, title) + }() } wg.Wait() } -func sendGotify(args *args, message, title string, wg *sync.WaitGroup) { - defer wg.Done() +func buildEMail(timestamp time.Time, from string, to []string, subject string, body string) string { + var msg strings.Builder + msg.WriteString("From: " + from + "\r\n") + msg.WriteString("To: " + strings.Join(to, ";") + "\r\n") + msg.WriteString("Date: " + timestamp.Format(time.RFC1123Z) + "\r\n") + msg.WriteString("Content-Type: text/plain; charset=UTF-8\r\n") + msg.WriteString("Subject: " + subject + "\r\n") + msg.WriteString("\r\n" + body + "\r\n") + + return msg.String() +} + +func sendMail(timestamp time.Time, message string, title string) { + + from := glb_arguments.MailFrom + to := []string{glb_arguments.MailTo} + username := glb_arguments.MailUser + password := glb_arguments.MailPassword + + host := glb_arguments.MailHost + port := strconv.Itoa(glb_arguments.MailPort) + address := host + ":" + port - response, err := http.PostForm(args.GotifyURL+"/message?token="+args.GotifyToken, + subject := title + body := message + + mail := buildEMail(timestamp, from, to, subject, body) + + auth := smtp.PlainAuth("", username, password, host) + + err := smtp.SendMail(address, auth, from, to, []byte(mail)) + if err != nil { + logger.Error().Err(err).Str("reporter", "Mail").Msg("") + return + } +} + +func sendGotify(message string, title string) { + + response, err := http.PostForm(glb_arguments.GotifyURL+"/message?token="+glb_arguments.GotifyToken, url.Values{"message": {message}, "title": {title}}) if err != nil { - log.Error(err) + logger.Error().Err(err).Str("reporter", "Gotify").Msg("") return } @@ -129,30 +283,29 @@ func sendGotify(args *args, message, title string, wg *sync.WaitGroup) { body, err := io.ReadAll(response.Body) if err != nil { - log.Error(err) + logger.Error().Err(err).Str("reporter", "Gotify").Msg("") return } - log.Debugf("Gotify response statusCode: %d", statusCode) - log.Debugf("Gotify response body: %s", string(body)) + logger.Debug().Str("reporter", "Gotify").Msgf("Gotify response statusCode: %d", statusCode) + logger.Debug().Str("reporter", "Gotify").Msgf("Gotify response body: %s", string(body)) // Log non successfull status codes if statusCode == 200 { - log.Debugf("Gotify message delivered") + logger.Debug().Str("reporter", "Gotify").Msgf("Gotify message delivered") } else { - log.Errorf("Pushing gotify message failed.") - log.Errorf("Gotify response body: %s", string(body)) + logger.Error().Str("reporter", "Gotify").Msgf("Pushing gotify message failed.") + logger.Error().Str("reporter", "Gotify").Msgf("Gotify response body: %s", string(body)) } } -func sendPushover(args *args, message, title string, wg *sync.WaitGroup) { - defer wg.Done() +func sendPushover(message string, title string) { // Create a new pushover app with an API token - app := pushover.New(args.PushoverAPIToken) + app := pushover.New(glb_arguments.PushoverAPIToken) // Create a new recipient (user key) - recipient := pushover.NewRecipient(args.PushoverUserKey) + recipient := pushover.NewRecipient(glb_arguments.PushoverUserKey) // Create the message to send pushmessage := pushover.NewMessageWithTitle(message, title) @@ -160,102 +313,158 @@ func sendPushover(args *args, message, title string, wg *sync.WaitGroup) { // Send the message to the recipient response, err := app.SendMessage(pushmessage, recipient) if err != nil { - log.Error(err) + logger.Error().Err(err).Str("reporter", "Pushover").Msg("") return } if response != nil { - log.Debugf("%s", response) + logger.Debug().Str("reporter", "Pushover").Msgf("%s", response) } if (*response).Status == 1 { // Pushover returns 1 if the message request to the API was valid // https://pushover.net/api#response - log.Debugf("Pushover message delivered") + logger.Debug().Str("reporter", "Pushover").Msgf("Pushover message delivered") return } // if response Status !=1 - log.Errorf("Pushover message not delivered") + logger.Error().Str("reporter", "Pushover").Msg("Pushover message not delivered") } -func processEvent(args *args, event *events.Message, wg *sync.WaitGroup) { +func processEvent(event *events.Message) { // the Docker Events endpoint will return a struct events.Message // https://pkg.go.dev/github.com/docker/docker/api/types/events#Message - var message string + var msg_builder, title_builder strings.Builder + var ActorID, ActorImage, ActorName, TitleID string - // if logging level is Debug, log the event - log.Debugf("%#v", event) + // Adding a small configurable delay here + // Sometimes events are pushed through the event channel really quickly, but they arrive on the notification clients in + // wrong order (probably due to message delivery time), e.g. Pushover is susceptible for this. + // Finishing this function not before a certain time before draining the next event from the event channel in main() solves the issue + timer := time.NewTimer(glb_arguments.Delay) - //event_timestamp := time.Unix(event.Time, 0).Format("02-01-2006 15:04:05") + // if logging level is Debug, log the event + logger.Debug().Msgf("%#v", event) - //some events don't return Actor.ID or Actor.Attributes["image"] - var ID, image string - if len(event.Actor.ID) > 0 { - ID = strings.TrimPrefix(event.Actor.ID, "sha256:")[:8] //remove prefix + limit ID legth + //some events don't return Actor.ID, Actor.Attributes["image"] or Actor.Attributes["name"] + if len(event.Actor.ID) > 0 && strings.HasPrefix(event.Actor.ID, "sha256:") { + ActorID = strings.TrimPrefix(event.Actor.ID, "sha256:")[:8] //remove prefix + limit ActorID legth } if len(event.Actor.Attributes["image"]) > 0 { - image = event.Actor.Attributes["image"] - } - - // Prepare message - if len(ID) == 0 { - if len(image) == 0 { - message = fmt.Sprintf("Object '%s' reported: %s", cases.Title(language.English, cases.Compact).String(event.Type), event.Action) - } else { - message = fmt.Sprintf("Object '%s' from image %s reported: %s", cases.Title(language.English, cases.Compact).String(event.Type), image, event.Action) - } + ActorImage = event.Actor.Attributes["image"] } else { - if len(image) == 0 { - message = fmt.Sprintf("Object '%s' with ID %s reported: %s", cases.Title(language.English, cases.Compact).String(event.Type), ID, event.Action) + // try to recover image name from org.opencontainers.image info + if len(event.Actor.Attributes["org.opencontainers.image.title"]) > 0 && len(event.Actor.Attributes["org.opencontainers.image.version"]) > 0 { + ActorImage = event.Actor.Attributes["org.opencontainers.image.title"] + ":" + event.Actor.Attributes["org.opencontainers.image.version"] + } + } + if len(event.Actor.Attributes["name"]) > 0 { + // in case the ActorName is only an hash + if strings.HasPrefix(event.Actor.Attributes["name"], "sha256:") { + ActorName = strings.TrimPrefix(event.Actor.Attributes["name"], "sha256:")[:8] //remove prefix + limit ActorName legth } else { - message = fmt.Sprintf("Object '%s' with ID %s from image %s reported: %s", cases.Title(language.English, cases.Compact).String(event.Type), ID, image, event.Action) + ActorName = event.Actor.Attributes["name"] } } - log.Info(message) + // Check possible image and container name + // The order of the checks is important, because we want name rather than ActorID + // as identifier in the title + if len(ActorID) > 0 { + msg_builder.WriteString("ID: " + ActorID + "\n") + TitleID = ActorID + } + if len(ActorImage) > 0 { + msg_builder.WriteString("Image: " + ActorImage + "\n") + // Not using ActorImage as possible title, because it's too long + } + if len(ActorName) > 0 { + msg_builder.WriteString("Name: " + ActorName + "\n") + TitleID = ActorName + } + + // Build title + title_builder.WriteString(cases.Title(language.English, cases.Compact).String(string(event.Type))) + if len(TitleID) > 0 { + title_builder.WriteString(" " + TitleID) + } + title_builder.WriteString(": " + string(event.Action)) - sendNotifications(args, message, "New Docker Event", wg) + // Get event timestamp + timestamp := time.Unix(event.Time, 0) + msg_builder.WriteString("Time: " + timestamp.Format(time.RFC1123Z) + "\n") -} + // Append possible docker compose context + if len(event.Actor.Attributes["com.docker.compose.project.working_dir"]) > 0 { + msg_builder.WriteString("Docker compose context: " + event.Actor.Attributes["com.docker.compose.project.working_dir"] + "\n") + } + if len(event.Actor.Attributes["com.docker.compose.service"]) > 0 { + msg_builder.WriteString("Docker compose service: " + event.Actor.Attributes["com.docker.compose.service"] + "\n") + } -func parseArgs() args { - var args args - parser := arg.MustParse(&args) + // Build message and title + title := title_builder.String() + message := strings.TrimRight(msg_builder.String(), "\n") + + // Log message + logger.Info(). + Str("eventType", string(event.Type)). + Str("ActorID", ActorID). + Str("eventAction", string(event.Action)). + Str("ActorImage", ActorImage). + Str("ActorName", ActorName). + Str("DockerComposeContext", event.Actor.Attributes["com.docker.compose.project.working_dir"]). + Str("DockerComposeService", event.Actor.Attributes["com.docker.compose.service"]). + Msg(title) + + // send notifications to various reporters + // function will finish when all reporters finished + sendNotifications(timestamp, message, title) + + // block function until time (delay) triggers + // if sendNotifications is faster than the delay, function blocks here until delay is over + // if sendNotifications takes longer than the delay, trigger already fired and no delay is added + <-timer.C - configureLogger(args.LogLevel) +} + +func parseArgs() { + parser := arg.MustParse(&glb_arguments) - args.Filter = make(map[string][]string) + glb_arguments.Filter = make(map[string][]string) - for _, filter := range args.FilterStrings { + for _, filter := range glb_arguments.FilterStrings { pos := strings.Index(filter, "=") if pos == -1 { parser.Fail("each filter should be of the form key=value") } key := filter[:pos] val := filter[pos+1:] - args.Filter[key] = append(args.Filter[key], val) + glb_arguments.Filter[key] = append(glb_arguments.Filter[key], val) } - return args } func configureLogger(LogLevel string) { - // set log level - if l, err := log.ParseLevel(LogLevel); err == nil { - log.SetLevel(l) + // UNIX Time is faster and smaller than most timestamps + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + + // Change logging level when debug flag is set + if LogLevel == "debug" { + logger = zerolog.New(os.Stderr). + Level(zerolog.DebugLevel). + With(). + Timestamp(). + Str("service", "docker event monitor"). + Logger() } else { - log.Fatal(err) + logger = zerolog.New(os.Stderr). + Level(zerolog.InfoLevel). + With(). + Str("service", "docker event monitor"). + Timestamp(). + Logger() } - - // Output to stdout instead of the default stderr - log.SetOutput(os.Stdout) - - // set log formatting - log.SetFormatter(&log.TextFormatter{ - DisableTimestamp: true, - DisableLevelTruncation: true, - }) - } diff --git a/src/go.mod b/src/go.mod index 1531b5c..8f39c99 100644 --- a/src/go.mod +++ b/src/go.mod @@ -1,32 +1,42 @@ module docker-event-monitor -go 1.21.0 +go 1.21 require ( github.com/alexflint/go-arg v1.4.3 - github.com/docker/docker v24.0.6+incompatible - github.com/gregdel/pushover v1.2.1 - github.com/sirupsen/logrus v1.9.3 - golang.org/x/text v0.13.0 + github.com/docker/docker v25.0.1+incompatible + github.com/gregdel/pushover v1.3.0 + github.com/rs/zerolog v1.31.0 + golang.org/x/text v0.14.0 ) require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/alexflint/go-scalar v1.2.0 // indirect - github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/docker/go-connections v0.4.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/distribution/reference v0.5.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/stretchr/testify v1.8.4 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.15.0 // indirect - golang.org/x/sys v0.12.0 // indirect - golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.13.0 // indirect - gotest.tools/v3 v3.5.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect + go.opentelemetry.io/otel v1.22.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect + go.opentelemetry.io/otel/metric v1.22.0 // indirect + go.opentelemetry.io/otel/sdk v1.22.0 // indirect + go.opentelemetry.io/otel/trace v1.22.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.17.0 // indirect + gotest.tools/v3 v3.5.1 // indirect ) diff --git a/src/go.sum b/src/go.sum index 9bd24b2..dfdeaa7 100644 --- a/src/go.sum +++ b/src/go.sum @@ -7,25 +7,48 @@ github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258m github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE= -github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v25.0.1+incompatible h1:k5TYd5rIVQRSqcTwCID+cyVA0yRg86+Pcrz1ls0/frA= +github.com/docker/docker v25.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -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/gregdel/pushover v1.2.1 h1:IPPJCdzXz60gMqnlzS0ZAW5z5aS1gI4nU+YM0Pe+ssA= -github.com/gregdel/pushover v1.2.1/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gregdel/pushover v1.3.0 h1:CewbxqsThoN/1imgwkDKFkRkltaQMoyBV0K9IquQLtw= +github.com/gregdel/pushover v1.3.0/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= @@ -38,6 +61,9 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 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= @@ -47,49 +73,75 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= +go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= +go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY= +go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= +go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= +go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= +go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= +go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= +go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/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= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 h1:W18sezcAYs+3tDZX4F80yctqa12jcP1PUS2gQu1zTPU= +google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97/go.mod h1:iargEX0SFPm3xcfMI0d1domjg0ZF4Aa0p2awqyxhvF0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= +google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= +google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= -gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=