[
  {
    "path": ".dockerignore",
    "content": ".build/\n.tarballs/\n\n!.build/linux-amd64/\n!.build/linux-armv7/\n!.build/linux-arm64/\n!.build/linux-ppc64le/\n!.build/linux-s390x/\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "---\nname: Bug report\ndescription: Create a report to help us improve.\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thank you for opening a bug report for Alertmanager.\n\n        Please do *NOT* ask support questions in Github issues.\n\n        If your issue is not a feature request or bug report use our [community support](https://prometheus.io/community/).\n\n        There is also [commercial support](https://prometheus.io/support-training/) available.\n  - type: textarea\n    attributes:\n      label: What did you do?\n      description: Please provide steps for us to reproduce this issue.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: What did you expect to see?\n  - type: textarea\n    attributes:\n      label: What did you see instead? Under which circumstances?\n    validations:\n      required: true\n  - type: markdown\n    attributes:\n      value: |\n        ## Environment\n  - type: input\n    attributes:\n      label: System information\n      description: Insert output of `uname -srm` here, or operating system version.\n      placeholder: e.g. Linux 5.16.15 x86_64\n  - type: textarea\n    attributes:\n      label: Alertmanager version\n      description: Insert output of `alertmanager --version` here.\n      render: text\n      placeholder: |\n        e.g. alertmanager, version 0.22.2 (branch: HEAD, revision: 44f8adc06af5101ad64bd8b9c8b18273f2922051)\n          build user:       root@b595c7f32520\n          build date:       20210602-07:50:37\n          go version:       go1.16.4\n          platform:         linux/amd64\n  - type: textarea\n    attributes:\n      label: Alertmanager configuration file\n      description: Insert relevant configuration here. Don't forget to remove secrets.\n      render: yaml\n  - type: textarea\n    attributes:\n      label: Prometheus version\n      description: Insert output of `prometheus --version` here (if relevant to the issue).\n      render: text\n      placeholder: |\n        e.g. prometheus, version 2.23.0 (branch: HEAD, revision: 26d89b4b0776fe4cd5a3656dfa520f119a375273)\n          build user:       root@37609b3a0a21\n          build date:       20201126-10:56:17\n          go version:       go1.15.5\n          platform:         linux/amd64\n  - type: textarea\n    attributes:\n      label: Prometheus configuration file\n      description: Insert relevant configuration here. Don't forget to remove secrets.\n      render: yaml\n  - type: textarea\n    attributes:\n      label: Logs\n      description: Insert Prometheus and Alertmanager logs relevant to the issue here.\n      render: text\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n  - name: Prometheus Community Support\n    url: https://prometheus.io/community/\n    about: If you need help or support, please request help here.\n  - name: Commercial Support & Training\n    url: https://prometheus.io/support-training/\n    about: If you want commercial support or training, vendors are listed here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "---\nname: Feature request\ndescription: Suggest an idea for this project.\nbody:\n  - type: markdown\n    attributes:\n      value: >-\n        Please do *NOT* ask support questions in Github issues.\n\n\n        If your issue is not a feature request or bug report use\n        our [community support](https://prometheus.io/community/).\n\n\n        There is also [commercial\n        support](https://prometheus.io/support-training/) available.\n  - type: textarea\n    attributes:\n      label: Proposal\n      description: Use case. Why is this important?\n      placeholder: “Nice to have” is not a good use case. :)\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!--\n    - Please give your PR a title in the form \"area: short description\".  For example \"dispatcher: improve performance\"\n\n    - Please sign-off your commits by adding the -s / --signoff flag to `git commit`. See https://github.com/apps/dco for more information.\n\n    - If the PR adds or changes a behaviour or fixes a bug of an exported API it would need a unit/e2e test.\n\n    - Where possible use only exported APIs for tests to simplify the review and make it as close as possible to an actual library usage.\n\n    - Performance improvements would need a benchmark test to prove it.\n\n    - All exposed objects should have a comment.\n\n    - All comments should start with a capital letter and end with a full stop.\n -->\n\n#### Pull Request Checklist\nPlease check all the applicable boxes.\n\n- Please list all open issue(s) discussed with maintainers related to this change\n    - Fixes #<issue number>\n    <!--\n    If it applies.\n    Automatically closes linked issue when PR is merged.\n    Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.\n    More at https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword\n    -->\n- Is this a new Receiver integration?\n    - [ ] I have already tried to use the [Webhook Receiver Integration](https://prometheus.io/docs/alerting/latest/configuration/#webhook_config) and [3rd party integrations](https://prometheus.io/docs/operating/integrations/#alertmanager-webhook-receiver) before adding this new Receiver Integration\n- Is this a bugfix?\n    - [ ] I have added tests that can reproduce the bug which pass with this bugfix applied\n- Is this a new feature?\n    - [ ] I have added tests that test the new feature's functionality\n- Does this change affect performance?\n    - [ ] I have provided benchmarks comparison that shows performance is improved or is not degraded\n        - You can use [`benchstat`](https://pkg.go.dev/golang.org/x/perf/cmd/benchstat) to compare benchmarks\n    - [ ] I have added new benchmarks if required or requested by maintainers\n- Is this a breaking change?\n    - [ ] My changes do not break the existing cluster messages\n    - [ ] My changes do not break the existing api\n- [ ] I have added/updated the required documentation\n- [ ] I have signed-off my commits\n- [ ] I will follow [best practices for contributing to this project](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-open-source)\n\n#### Which user-facing changes does this PR introduce?\n<!--\nIf no, just write \"NONE\" in the release-notes block below.\nOtherwise, please describe what should be mentioned in the CHANGELOG. Use the following prefixes:\n[FEATURE] [ENHANCEMENT] [PERF] [BUGFIX] [SECURITY] [CHANGE]\nRefer to the existing CHANGELOG for inspiration:  https://github.com/prometheus/alertmanager/blob/main/CHANGELOG.md\nA concrete example may look as follows (be sure to leave out the surrounding quotes): \"[FEATURE] API: Add /api/v1/features for clients to understand which features are supported\".\nIf you need help formulating your entries, consult the reviewer(s).\n-->\n```release-notes\n\n```\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "---\nname: CI\non:  # yamllint disable-line rule:truthy\n  pull_request:\n  workflow_call:\njobs:\n  test_frontend:\n    name: Test alertmanager frontend\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - run: make clean\n      - run: make all\n        working-directory: ./ui/app\n      - run: make assets\n      - run: make apiv2\n      - run: git diff --exit-code\n\n  build:\n    name: Build Alertmanager for common architectures\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        thread: [0, 1, 2]\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.0.0\n      - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4\n      - uses: ./.github/promci/actions/build\n        with:\n          promu_opts: \"-p linux/amd64 -p windows/amd64 -p linux/arm64 -p darwin/amd64 -p darwin/arm64 -p linux/386\"\n          parallelism: 3\n          thread: ${{ matrix.thread }}\n\n  test:\n    name: Test\n    runs-on: ubuntu-latest\n    # Whenever the Go version is updated here, .promu.yml\n    # should also be updated.\n    container:\n\n      image: quay.io/prometheus/golang-builder:1.26-base\n    services:\n      maildev-noauth:\n        image: maildev/maildev:2.2.1\n      maildev-auth:\n        image: maildev/maildev:2.2.1\n        env:\n          MAILDEV_INCOMING_USER: user\n          MAILDEV_INCOMING_PASS: pass\n    env:\n      EMAIL_NO_AUTH_CONFIG: testdata/noauth.yml\n      EMAIL_AUTH_CONFIG: testdata/auth.yml\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4\n      - uses: ./.github/promci/actions/setup_environment\n      - run: make\n      - run: git diff --exit-code\n"
  },
  {
    "path": ".github/workflows/container_description.yml",
    "content": "---\nname: Push README to Docker Hub\non:\n  push:\n    paths:\n      - \"README.md\"\n      - \"README-containers.md\"\n      - \".github/workflows/container_description.yml\"\n    branches: [ main, master ]\n\npermissions:\n  contents: read\n\njobs:\n  PushDockerHubReadme:\n    runs-on: ubuntu-latest\n    name: Push README to Docker Hub\n    if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks.\n    steps:\n      - name: git checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n      - name: Set docker hub repo name\n        run: echo \"DOCKER_REPO_NAME=$(make docker-repo-name)\" >> $GITHUB_ENV\n      - name: Push README to Dockerhub\n        uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1\n        env:\n          DOCKER_USER: ${{ secrets.DOCKER_HUB_LOGIN }}\n          DOCKER_PASS: ${{ secrets.DOCKER_HUB_PASSWORD }}\n        with:\n          destination_container_repo: ${{ env.DOCKER_REPO_NAME }}\n          provider: dockerhub\n          short_description: ${{ env.DOCKER_REPO_NAME }}\n          # Empty string results in README-containers.md being pushed if it\n          # exists. Otherwise, README.md is pushed.\n          readme_file: ''\n\n  PushQuayIoReadme:\n    runs-on: ubuntu-latest\n    name: Push README to quay.io\n    if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks.\n    steps:\n      - name: git checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n      - name: Set quay.io org name\n        run: echo \"DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')\" >> $GITHUB_ENV\n      - name: Set quay.io repo name\n        run: echo \"DOCKER_REPO_NAME=$(make docker-repo-name)\" >> $GITHUB_ENV\n      - name: Push README to quay.io\n        uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1\n        env:\n          DOCKER_APIKEY: ${{ secrets.QUAY_IO_API_TOKEN }}\n        with:\n          destination_container_repo: ${{ env.DOCKER_REPO_NAME }}\n          provider: quay\n          # Empty string results in README-containers.md being pushed if it\n          # exists. Otherwise, README.md is pushed.\n          readme_file: ''\n"
  },
  {
    "path": ".github/workflows/mixin.yml",
    "content": "name: mixin\non:\n  pull_request:\n    paths:\n      - \"doc/alertmanager-mixin/**\"\n\njobs:\n  mixin:\n    name: mixin-lint\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: install Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: 1.26.x\n      # pin the mixtool version until https://github.com/monitoring-mixins/mixtool/issues/135 is merged.\n      - run: go install github.com/monitoring-mixins/mixtool/cmd/mixtool@2282201396b69055bb0f92f187049027a16d2130\n      - run: go install github.com/google/go-jsonnet/cmd/jsonnetfmt@latest\n      - run: go install github.com/jsonnet-bundler/jsonnet-bundler/cmd/jb@latest\n      - run: make -C doc/alertmanager-mixin lint\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "---\nname: Publish\non:  # yamllint disable-line rule:truthy\n  push:\n    branches:\n      - main\njobs:\n  ci:\n    name: Run ci\n    uses: ./.github/workflows/ci.yml\n\n  build:\n    name: Build Alertmanager for all architectures\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        thread: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]\n    needs: ci\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4\n      - uses: ./.github/promci/actions/build\n        with:\n          parallelism: 12\n          thread: ${{ matrix.thread }}\n  publish_main:\n    name: Publish main branch artefacts\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4\n      - uses: ./.github/promci/actions/publish_main\n        with:\n          docker_hub_login: ${{ secrets.docker_hub_login }}\n          docker_hub_password: ${{ secrets.docker_hub_password }}\n          quay_io_login: ${{ secrets.quay_io_login }}\n          quay_io_password: ${{ secrets.quay_io_password }}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "---\nname: Release\non:  # yamllint disable-line rule:truthy\n  push:\n    tags:\n      - v*\njobs:\n  ci:\n    name: Run ci\n    uses: ./.github/workflows/ci.yml\n\n  build:\n    name: Build Alertmanager for all architectures\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        thread: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]\n    needs: ci\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4\n      - uses: ./.github/promci/actions/build\n        with:\n          parallelism: 12\n          thread: ${{ matrix.thread }}\n  publish_release:\n    name: Publish release artefacts\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4\n      - uses: ./.github/promci/actions/publish_release\n        with:\n          docker_hub_login: ${{ secrets.docker_hub_login }}\n          docker_hub_password: ${{ secrets.docker_hub_password }}\n          quay_io_login: ${{ secrets.quay_io_login }}\n          quay_io_password: ${{ secrets.quay_io_password }}\n          github_token: ${{ secrets.PROMBOT_GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "name: Stale Check\non:\n  workflow_dispatch: {}\n  schedule:\n    - cron: '16 22 * * *'\npermissions:\n  issues: write\n  pull-requests: write\njobs:\n  stale:\n    if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks.\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0\n        with:\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n          # opt out of defaults to avoid marking issues as stale and closing them\n          # https://github.com/actions/stale#days-before-close\n          # https://github.com/actions/stale#days-before-stale\n          days-before-stale: -1\n          days-before-close: -1\n          # Setting it to empty string to skip comments.\n          # https://github.com/actions/stale#stale-pr-message\n          # https://github.com/actions/stale#stale-issue-message\n          stale-pr-message: ''\n          stale-issue-message: ''\n          operations-per-run: 30\n          # override days-before-stale, for only marking the pull requests as stale\n          days-before-pr-stale: 60\n          stale-pr-label: stale\n          exempt-pr-labels: keepalive\n"
  },
  {
    "path": ".github/workflows/ui-ci.yml",
    "content": "name: UI CI\n\non:\n  pull_request:\n    branches:\n      - '**'\n    paths:\n      - 'ui/**'\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.number || github.sha }}\n  cancel-in-progress: true\n\njobs:\n  test_mantine_ui:\n    name: Test mantine-ui\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: ./ui/mantine-ui\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0\n        with:\n          node-version-file: './ui/mantine-ui/.nvmrc'\n          cache: 'npm'\n          cache-dependency-path: '**/package-lock.json'\n      - name: Install dependencies\n        run: npm install\n      - name: Run build\n        run: npm run build\n      - name: Run tests\n        run: npm test\n"
  },
  {
    "path": ".gitignore",
    "content": "/data/\n/alertmanager\n/amtool\n*.yml\n*.yaml\n/.build\n/.release\n/.tarballs\n/vendor\n\n!.golangci.yml\n!/cli/testdata/*.yml\n!/cli/config/testdata/*.yml\n!/cluster/testdata/*.yml\n!/config/testdata/*.yml\n!/examples/ha/tls/*.yml\n!/notify/email/testdata/*.yml\n!/doc/examples/simple.yml\n!/circle.yml\n!/.travis.yml\n!/.promu.yml\n!/api/v2/openapi.yaml\n!.github/workflows/*.yml\n!.github/ISSUE_TEMPLATE/*.yml\n!buf*.yaml\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\nlinters:\n  enable:\n    - depguard\n    - errorlint\n    - godot\n    - misspell\n    - modernize\n    - revive\n    - sloglint\n    - testifylint\n  settings:\n    depguard:\n      rules:\n        main:\n          deny:\n            - pkg: github.com/stretchr/testify/assert\n              desc: \"Use github.com/stretchr/testify/require instead of github.com/stretchr/testify/assert\"\n            - pkg: github.com/go-kit/kit/log\n              desc: \"Use github.com/go-kit/log instead of github.com/go-kit/kit/log\"\n            - pkg: github.com/pkg/errors\n              desc: \"Use errors or fmt instead of github.com/pkg/errors\"\n    errcheck:\n      exclude-functions:\n        # Don't flag lines such as \"io.Copy(io.Discard, resp.Body)\".\n        - io.Copy\n        # The next two are used in HTTP handlers, any error is handled by the server itself.\n        - io.WriteString\n        - (net/http.ResponseWriter).Write\n        # No need to check for errors on server's shutdown.\n        - (*net/http.Server).Shutdown\n        # Never check for rollback errors as Rollback() is called when a previous error was detected.\n        - (github.com/prometheus/prometheus/storage.Appender).Rollback\n    godot:\n      scope: toplevel\n      exclude:\n        - \"^ ?This file is safe to edit\"\n        - \"^ ?scheme value\"\n      period: true\n      capital: true\n    revive:\n      rules:\n        - name: blank-imports\n        - name: context-as-argument\n        - name: error-naming\n        - name: error-return\n        - name: error-strings\n        - name: errorf\n        - name: exported\n          arguments:\n            - disableStutteringCheck\n        - name: if-return\n        - name: increment-decrement\n        - name: indent-error-flow\n        - name: package-comments\n        - name: range\n        - name: receiver-naming\n        - name: time-naming\n        - name: unexported-return\n        - name: var-declaration\n        - name: var-naming\n          disabled: true\n    testifylint:\n      disable:\n        - float-compare\n        - go-require\n      enable-all: true\n  exclusions:\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    paths:\n      # Skip autogenerated files.\n      - ^.*\\.(pb|y)\\.go$\n    rules:\n      - linters:\n          - errcheck\n        path: _test.go\n      - linters:\n          - modernize\n        text: \"omitzero: Omitempty has no effect on nested struct fields\"\n      - linters:\n          - staticcheck\n        text: \"SA1019:.*types\\\\.Alert.*\"\n    warn-unused: true\nissues:\n  max-issues-per-linter: 0\n  max-same-issues: 0\nrun:\n  timeout: 5m\nformatters:\n  enable:\n    - gofumpt\n    - goimports\n  settings:\n    gofumpt:\n      extra-rules: true\n    goimports:\n      local-prefixes:\n        - github.com/prometheus/alertmanager\n"
  },
  {
    "path": ".promu.yml",
    "content": "go:\n    # Whenever the Go version is updated here,\n    # .circle/config.yml should also be updated.\n    version: 1.26\nrepository:\n    path: github.com/prometheus/alertmanager\nbuild:\n    binaries:\n        - name: alertmanager\n          path: ./cmd/alertmanager\n        - name: amtool\n          path: ./cmd/amtool\n    tags:\n        all:\n            - netgo\n        windows: []\n    ldflags: |\n        -X github.com/prometheus/common/version.Version={{.Version}}\n        -X github.com/prometheus/common/version.Revision={{.Revision}}\n        -X github.com/prometheus/common/version.Branch={{.Branch}}\n        -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}}\n        -X github.com/prometheus/common/version.BuildDate={{date \"20060102-15:04:05\"}}\ntarball:\n    files:\n        - examples/ha/alertmanager.yml\n        - LICENSE\n        - NOTICE\ncrossbuild:\n    platforms:\n        - darwin\n        - dragonfly\n        - freebsd\n        - illumos\n        - linux\n        - netbsd\n        - openbsd\n        - windows\n"
  },
  {
    "path": ".yamllint",
    "content": "---\nextends: default\nignore: |\n  **/node_modules\n  web/api/v1/testdata/openapi_*_golden.yaml\n\nrules:\n  braces:\n    max-spaces-inside: 1\n    level: error\n  brackets:\n    max-spaces-inside: 1\n    level: error\n  commas: disable\n  comments: disable\n  comments-indentation: disable\n  document-start: disable\n  indentation:\n    spaces: consistent\n    indent-sequences: consistent\n  key-duplicates:\n    ignore: |\n      config/testdata/section_key_dup.bad.yml\n  line-length: disable\n  truthy:\n    check-keys: false\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## main / (unreleased)\n\n* [CHANGE] ...\n* [FEATURE] ...\n* [ENHANCEMENT] ...\n* [BUGFIX] Use dispatcher tick time when evaluating repeat interval in dedup stage. #2461\n\n## 0.31.1 / 2026-02-11\n\n* [BUGFIX] docs: Fix email TLS configuration example. #4976\n* [BUGFIX] docs: Add telegram bot token options to global config docs. #4999\n\n## 0.31.0 / 2026-02-02\n\n* [ENHANCEMENT] docs(opsgenie): Fix description of `api_url` field. #4908\n* [ENHANCEMENT] docs(slack): Document missing app configs. #4871\n* [ENHANCEMENT] docs: Fix `max-silence-size-bytes`. #4805\n* [ENHANCEMENT] docs: Update expr for `AlertmanagerClusterFailedToSendAlerts` to exclude value 0. #4872\n* [ENHANCEMENT] docs: Use matchers for inhibit rules examples. #4131\n* [ENHANCEMENT] docs: add notification integrations. #4901\n* [ENHANCEMENT] docs: update `slack_config` attachments documentation links. #4802\n* [ENHANCEMENT] docs: update description of filter query params in openapi doc. #4810\n* [ENHANCEMENT] provider: Reduce lock contention. #4809\n* [FEATURE] slack: Add support for top-level text field in slack notification. #4867\n* [FEATURE] smtp: Add support for authsecret from file. #3087\n* [FEATURE] smtp: Customize the ssl/tls port support (#4757). #4818\n* [FEATURE] smtp: Enhance email notifier configuration validation. #4826\n* [FEATURE] telegram: Add `chat_id_file` configuration parameter. #4909\n* [FEATURE] telegram: Support global bot token. #4823\n* [FEATURE] webhook: Support templating in url fields. #4798\n* [FEATURE] wechat: Add config directive to pass api secret via file. #4734\n* [FEATURE] provider: Implement per alert limits. #4819\n* [BUGFIX] Allow empty `group_by` to override parent route. #4825\n* [BUGFIX] Set `spellcheck=false` attribute on silence filter input. #4811\n* [BUGFIX] jira: Fix for handling api v3 with ADF. #4756\n* [BUGFIX] jira: Prevent hostname corruption in cloud api url replacement. #4892\n\n## 0.30.1 / 2026-01-12\n\n* [BUGFIX] Fix memory leak in tracing client. #4828\n\n## 0.30.0 / 2025-12-15\n\n* [CHANGE] Don't allow calling qids with an empty ids list. #4707\n* [FEATURE] Add mattermost integration. #4090\n* [FEATURE] Add saturday to the first day of the week options. #4473\n* [FEATURE] Add templating functions for working with urls. #4625\n* [FEATURE] cluster: Allow persistent peer names. #4636\n* [FEATURE] dispatch: Add start delay. #4704\n* [FEATURE] provider: Add subscriber channel metrics. #4630\n* [FEATURE] template: Add tojson function. #4773\n* [FEATURE] Add api http metrics. #4162\n* [FEATURE] Add distributed tracing support. #4745\n* [FEATURE] Add names to inhibit rules. #4628\n* [FEATURE] Add timeout option for pagerduty notifier. #4354\n* [FEATURE] Add timeout option for slack notifier. #4355\n* [FEATURE] Allow nested details fields in pagerduty. #3944\n* [FEATURE] Implement `phantom_threading` to group email alerts into threads. #4623\n* [FEATURE] gc: Report errors, but remove erroneous silences and continue. #4724\n* [FEATURE] jira: Template customfields. #4029\n* [FEATURE] jira: Allow configuring issue update via parameter. #4621\n* [FEATURE] Slack app support. #4211\n* [ENHANCEMENT] Add comment about smtp plain authentication. #4741\n* [ENHANCEMENT] Add documentation about high availability. #4708\n* [ENHANCEMENT] Add documentation for `client_allowed_sans`. #4706\n* [ENHANCEMENT] Improve logging around webhook dispatch failure. #4511\n* [ENHANCEMENT] Compile silence matchers when the silence is added. #4695\n* [ENHANCEMENT] Fix '`s/client/alerts_api/g`' broken link in 0.29. #4718\n* [ENHANCEMENT] Fix `rocketchat_config` docs. #4767\n* [ENHANCEMENT] Fix: `<mute_time_interval>` was renamed. #4729\n* [ENHANCEMENT] Improve inhibition performance. #4607\n* [ENHANCEMENT] Loadsnapshot: update matcher index properly while not holding lock. #4714\n* [ENHANCEMENT] Logging improvements. #4113\n* [ENHANCEMENT] Move query locking back into private query function. #4694\n* [ENHANCEMENT] Optimize the new inhibitor implementation for ~2.5x performance improvement. #4668\n* [ENHANCEMENT] Reduce the time dispatch.group holds the mutex. #4670\n* [ENHANCEMENT] Use b.loop() to simplify the code and improve performance. #4642\n* [ENHANCEMENT] Remove duplicate slice during silences query. #4696\n* [ENHANCEMENT] Silences: optimize incremental mutes queries via a silence version index. #4723\n* [ENHANCEMENT] Update description for filter param in openapi. #4775\n* [BUGFIX] Add new behavior to avoid races on config reload. #4705\n* [BUGFIX] config: Fix duplicate header detection for all case variants. #2810\n* [BUGFIX] marker: Stop state leakage from aggregation groups. #4438\n* [BUGFIX] Fix pprof debug endpoints not working with --web.route-prefix. #4698\n* [BUGFIX] Set context timeout for resolvepeers. #4343\n\n## 0.29.0 / 2025-11-01\n\n* [FEATURE] Add incident.io notifier. #4372\n* [FEATURE] Add monospace message formatting. #4362\n* [FEATURE] Add ability to customize interval for maintenance to run. #4541\n* [ENHANCEMENT] Update Jira notifier to support both Jira cloud API v3 and Jira datacenter API v2. #4542\n* [ENHANCEMENT] Increase mixin rate intervals for alert `FailedToSendAlerts`. #4206\n* [ENHANCEMENT] Make /alertmanager group writable in docker image. #4469\n* [BUGFIX] Fix logged notification count on error in notify. #4323\n* [BUGFIX] Fix docker image permissions path. #4288\n* [BUGFIX] Fix error handling in template rendering for Telegram. #4353\n* [BUGFIX] Fix duplicate `other` in error messages for config. #4366\n* [BUGFIX] Fix logic that considers an alert reopened in Jira. #4478\n* [BUGFIX] Fix Jira issue count #4615\n\n## 0.28.1 / 2025-03-07\n\n* [ENHANCEMENT] Improved performance of inhibition rules when using Equal labels. #4119\n* [ENHANCEMENT] Improve the documentation on escaping in UTF-8 matchers. #4157\n* [ENHANCEMENT] Update alertmanager_config_hash metric help to document the hash is not cryptographically strong. #4210\n* [BUGFIX] Fix panic in amtool when using `--verbose`. #4218\n* [BUGFIX] Fix templating of channel field for Rocket.Chat. #4220\n* [BUGFIX] Fix `rocketchat_configs` written as `rocket_configs` in docs. #4217\n* [BUGFIX] Fix usage for `--enable-feature` flag. #4214\n* [BUGFIX] Trim whitespace from OpsGenie API Key. #4195\n* [BUGFIX] Fix Jira project template not rendered when searching for existing issues. #4291\n* [BUGFIX] Fix subtle bug in JSON/YAML encoding of inhibition rules that would cause Equal labels to be omitted. #4292\n* [BUGFIX] Fix header for `slack_configs` in docs. #4247\n* [BUGFIX] Fix weight and wrap of Microsoft Teams notifications. #4222\n* [BUGFIX] Fix format of YAML examples in configuration.md. #4207\n\n## 0.28.0 / 2025-01-15\n\n* [CHANGE] Templating errors in the SNS integration now return an error. #3531 #3879\n* [CHANGE] Adopt log/slog, drop go-kit/log #4089\n* [FEATURE] Add a new Microsoft Teams integration based on Flows #4024\n* [FEATURE] Add a new Rocket.Chat integration #3600\n* [FEATURE] Add a new Jira integration #3590 #3931\n* [FEATURE] Add support for `GOMEMLIMIT`, enable it via the feature flag `--enable-feature=auto-gomemlimit`. #3895\n* [FEATURE] Add support for `GOMAXPROCS`, enable it via the feature flag `--enable-feature=auto-gomaxprocs`. #3837\n* [FEATURE] Add support for limits of silences including the maximum number of active and pending silences, and the maximum size per silence (in bytes). You can use the flags `--silences.max-silences` and `--silences.max-silence-size-bytes` to set them accordingly #3852 #3862 #3866 #3885 #3886 #3877\n* [FEATURE] Muted alerts now show whether they are suppressed or not in both the `/api/v2/alerts` endpoint and the Alertmanager UI. #3793 #3797 #3792\n* [ENHANCEMENT] Add support for `content`, `username` and `avatar_url` in the Discord integration. `content` and `username` also support templating. #4007\n* [ENHANCEMENT] Only invalidate the silences cache if a new silence is created or an existing silence replaced - should improve latency on both `GET api/v2/alerts` and `POST api/v2/alerts` API endpoint. #3961\n* [ENHANCEMENT] Add image source label to Dockerfile. To get changelogs shown when using Renovate #4062\n* [ENHANCEMENT] Build using go 1.23 #4071\n* [ENHANCEMENT] Support setting a global SMTP TLS configuration. #3732\n* [ENHANCEMENT] The setting `room_id` in the WebEx integration can now be templated to allow for dynamic room IDs. #3801\n* [ENHANCEMENT] Enable setting `message_thread_id` for the Telegram integration. #3638\n* [ENHANCEMENT] Support the `since` and `humanizeDuration` functions to templates. This means users can now format time to more human-readable text. #3863\n* [ENHANCEMENT] Support the `date` and `tz` functions to templates. This means users can now format time in a specified format and also change the timezone to their specific locale. #3812\n* [ENHANCEMENT] Latency metrics now support native histograms. #3737\n* [ENHANCEMENT] Add full width to adaptive card for msteamsv2 #4135\n* [ENHANCEMENT] Add timeout option for webhook notifier. #4137\n* [ENHANCEMENT] Update config to allow showing secret values when marshaled #4158\n* [ENHANCEMENT] Enable templating for Jira project and issue_type #4159\n* [BUGFIX] Fix the SMTP integration not correctly closing an SMTP submission, which may lead to unsuccessful dispatches being marked as successful. #4006\n* [BUGFIX]  The `ParseMode` option is now set explicitly in the Telegram integration. If we don't HTML tags had not been parsed by default. #4027\n* [BUGFIX] Fix a memory leak that was caused by updates silences continuously. #3930\n* [BUGFIX] Fix hiding secret URLs when the URL is incorrect. #3887\n* [BUGFIX] Fix a race condition in the alerts - it was more of a hypothetical race condition that could have occurred in the alert reception pipeline. #3648\n* [BUGFIX] Fix a race condition in the alert delivery pipeline that would cause a firing alert that was delivered earlier to be deleted from the aggregation group when instead it should have been delivered again. #3826\n* [BUGFIX] Fix version in APIv1 deprecation notice. #3815\n* [BUGFIX] Fix crash errors when using `url_file` in the Webhook integration. #3800\n* [BUGFIX] fix `Route.ID()` returns conflicting IDs. #3803\n* [BUGFIX] Fix deadlock on the alerts memory store. #3715\n* [BUGFIX] Fix `amtool template render` when using the default values. #3725\n* [BUGFIX] Fix `webhook_url_file` for both the Discord and Microsoft Teams integrations. #3728 #3745\n* [BUGFIX] Fix wechat api link #4084\n* [BUGFIX] Fix build info metric #4166\n* [BUGFIX] Fix UTF-8 not allowed in Equal field for inhibition rules #4177\n\n## 0.27.0 / 2024-02-28\n\n* [CHANGE] Discord Integration: Enforce max length in `message`. #3597\n* [CHANGE] API: Removal of all `api/v1/` endpoints. These endpoints now log and return a deprecation message and respond with a status code of `410`. #2970\n* [FEATURE] UTF-8 Support: Introduction of support for any UTF-8 character as part of label names and matchers. Please read more below. #3453, #3483, #3567, #3570\n* [FEATURE] Metrics: Introduced the experimental feature flag `--enable-feature=receiver-name-in-metrics` to include the receiver name in the following metrics: #3045\n  * `alertmanager_notifications_total`\n  * `alertmanager_notifications_failed_totall`\n  * `alertmanager_notification_requests_total`\n  * `alertmanager_notification_requests_failed_total`\n  * `alertmanager_notification_latency_seconds`\n* [FEATURE] Metrics: Introduced a new gauge named `alertmanager_inhibition_rules` that counts the number of configured inhibition rules. #3681\n* [FEATURE] Metrics: Introduced a new counter named `alertmanager_alerts_supressed_total` that tracks muted alerts, it contains a `reason` label to indicate the source of the mute. #3565\n* [ENHANCEMENT] Discord Integration: Introduced support for `webhook_url_file`. #3555\n* [ENHANCEMENT] Microsoft Teams Integration: Introduced support for `webhook_url_file`. #3555\n* [ENHANCEMENT] Microsoft Teams Integration: Add support for `summary`. #3616\n* [ENHANCEMENT] Metrics: Notification metrics now support two new values for the label `reason`, `contextCanceled` and `contextDeadlineExceeded`. #3631\n* [ENHANCEMENT] Email Integration: Contents of `auth_password_file` are now trimmed of prefixed and suffixed whitespace. #3680\n* [BUGFIX] amtool: Fixes the error `scheme required for webhook url` when using amtool with `--alertmanager.url`. #3509\n* [BUGFIX] Mixin: Fix `AlertmanagerFailedToSendAlerts`, `AlertmanagerClusterFailedToSendAlerts`, and `AlertmanagerClusterFailedToSendAlerts` to make sure they ignore the `reason` label. #3599\n\n### Removal of API v1\n\nThe Alertmanager `v1` API has been deprecated since January 2019 with the release of Alertmanager `v0.16.0`. With the release of version `0.27.0` it is now removed.\nA successful HTTP request to any of the `v1` endpoints will log and return a deprecation message while responding with a status code of `410`.\nPlease ensure you switch to the `v2` equivalent endpoint in your integrations before upgrading.\n\n### Alertmanager support for all UTF-8 characters in matchers and label names\n\nStarting with Alertmanager `v0.27.0`, we have a new parser for matchers that has a number of backwards incompatible changes. While most matchers will be forward-compatible, some will not. Alertmanager is operating a transition period where it supports both UTF-8 and classic matchers, so **it's entirely safe to upgrade without any additional configuration**. With that said, we recommend the following:\n\n- If this is a new Alertmanager installation, we recommend enabling UTF-8 strict mode before creating an Alertmanager configuration file. You can enable strict mode with `alertmanager --config.file=config.yml --enable-feature=\"utf8-strict-mode\"`.\n\n- If this is an existing Alertmanager installation, we recommend running the Alertmanager in the default mode called fallback mode before enabling UTF-8 strict mode. In this mode, Alertmanager will log a warning if you need to make any changes to your configuration file before UTF-8 strict mode can be enabled. **Alertmanager will make UTF-8 strict mode the default in the next two versions**, so it's important to transition as soon as possible.\n\nIrrespective of whether an Alertmanager installation is a new or existing installation, you can also use `amtool` to validate that an Alertmanager configuration file is compatible with UTF-8 strict mode before enabling it in Alertmanager server by running `amtool check-config config.yml` and inspecting the log messages.\n\nShould you encounter any problems, you can run the Alertmanager with just the classic parser enabled by running `alertmanager --config.file=config.yml --enable-feature=\"classic-mode\"`. If so, please submit a bug report via GitHub issues.\n\n## 0.26.0 / 2023-08-23\n\n* [SECURITY] Fix stored XSS via the /api/v1/alerts endpoint in the Alertmanager UI. CVE-2023-40577\n* [CHANGE] Telegram Integration: `api_url` is now optional. #2981\n* [CHANGE] Telegram Integration: `ParseMode` default is now `HTML` instead of `MarkdownV2`. #2981\n* [CHANGE] Webhook Integration: `url` is now marked as a secret. It will no longer show up in the logs as clear-text. #3228\n* [CHANGE] Metrics: New label `reason` for `alertmanager_notifications_failed_total` metric to indicate the type of error of the alert delivery. #3094 #3307\n* [FEATURE] Clustering: New flag `--cluster.label`, to help to block any traffic that is not meant for the cluster. #3354\n* [FEATURE] Integrations: Add Microsoft Teams as a supported integration. #3324\n* [ENHANCEMENT] Telegram Integration: Support `bot_token_file` for loading this secret from a file. #3226\n* [ENHANCEMENT] Webhook Integration: Support `url_file` for loading this secret from a file. #3223\n* [ENHANCEMENT] Webhook Integration: Leading and trailing white space is now removed for the contents of `url_file`. #3363\n* [ENHANCEMENT] Pushover Integration: Support options `device` and `sound` (sound was previously supported but undocumented). #3318\n* [ENHANCEMENT] Pushover Integration: Support `user_key_file` and `token_file` for loading this secret from a file. #3200\n* [ENHANCEMENT] Slack Integration: Support errors wrapped in successful (HTTP status code 200) responses. #3121\n* [ENHANCEMENT] API: Add `CORS` and `Cache-Control` HTTP headers to all version 2 API routes. #3195\n* [ENHANCEMENT] UI: Receiver name is now visible as part of the alerts page. #3289\n* [ENHANCEMENT] Templating: Better default text when using `{{ .Annotations }}` and `{{ .Labels }}`. #3256\n* [ENHANCEMENT] Templating: Introduced a new function `trimSpace` which removes leading and trailing white spaces. #3223\n* [ENHANCEMENT] CLI: `amtool silence query` now supports the `--id` flag to query an individual silence. #3241\n* [ENHANCEMENT] Metrics: Introduced `alertmanager_nflog_maintenance_total` and `alertmanager_nflog_maintenance_errors_total` to monitor maintenance of the notification log. #3286\n* [ENHANCEMENT] Metrics: Introduced `alertmanager_silences_maintenance_total` and `alertmanager_silences_maintenance_errors_total` to monitor maintenance of silences. #3285\n* [ENHANCEMENT] Logging: Log GroupKey and alerts on alert delivery when using debug mode. #3438\n* [BUGFIX] Configuration: Empty list of `receivers` and `inhibit_rules` would cause the alertmanager to crash. #3209\n* [BUGFIX] Templating: Fixed a race condition when using the `title` function. It is now race-safe. #3278\n* [BUGFIX] API: Fixed duplicate receiver names in the `api/v2/receivers` API endpoint. #3338\n* [BUGFIX] API: Attempting to delete a silence now returns the correct status code, `404` instead of `500`. #3352\n* [BUGFIX] Clustering: Fixes a panic when `tls_client_config` is empty. #3443\n* [BUGFIX] Fix stored XSS via the /api/v1/alerts endpoint in the Alertmanager UI.\n\n## 0.25.0 / 2022-12-22\n\n* [CHANGE] Change the default `parse_mode` value from `MarkdownV2` to `HTML` for Telegram. #2981\n* [CHANGE] Make `api_url` field optional for Telegram. #2981\n* [CHANGE] Use CanonicalMIMEHeaderKey instead of TitleCasing for email headers. #3080\n* [CHANGE] Reduce the number of notification logs broadcasted between peers by expiring them after (2 * repeat interval). #2982\n* [FEATURE] Add `proxy_url` support for OAuth2 in HTTP client configuration. #3010\n* [FEATURE] Reload TLS certificate and key from disk when updated. #3168\n* [FEATURE] Add Discord integration. #2948\n* [FEATURE] Add Webex integration. #3132\n* [ENHANCEMENT] Add `--web.systemd-socket` flag to systemd socket activation listeners instead of port listeners (Linux only). #3140\n* [ENHANCEMENT] Add `enable_http2` support in HTTP client configuration. #3010\n* [ENHANCEMENT] Add `min_version` support to select the minimum TLS version in HTTP client configuration. #3010\n* [ENHANCEMENT] Add `max_version` support to select the maximum TLS version in HTTP client configuration. #3168\n* [ENHANCEMENT] Emit warning logs when truncating messages in notifications. #3145\n* [ENHANCEMENT] Add `--data.maintenance-interval` flag to define the interval between the garbage collection and snapshotting to disk of the silences and the notification logs. #2849\n* [ENHANCEMENT] Support HEAD method for the `/-/healty` and `/-/ready` endpoints. #3039\n* [ENHANCEMENT] Truncate messages with the `…` ellipsis character instead of the 3-dots string `...`. #3072\n* [ENHANCEMENT] Add support for reading global and local SMTP passwords from files. #3038\n* [ENHANCEMENT] Add Location support to time intervals. #2782\n* [ENHANCEMENT] UI: Add 'Link' button to alerts in list. #2880\n* [ENHANCEMENT] Add the `source` field to the PagerDuty configuration. #3106\n* [ENHANCEMENT] Add support for reading PagerDuty routing and service keys from files. #3107\n* [ENHANCEMENT] Log response details when notifications fail for Webhooks, Pushover and VictorOps. #3103\n* [ENHANCEMENT] UI: Allow to choose the first day of the week as Sunday or Monday. #3093\n* [ENHANCEMENT] Add support for reading VictorOps API key from file. #3111\n* [ENHANCEMENT] Support templating for Opsgenie's responder type. #3060\n* [BUGFIX] Fail configuration loading if `api_key` and `api_key_file` are defined at the same time. #2910\n* [BUGFIX] Fix the `alertmanager_alerts` metric to avoid counting resolved alerts as active. Also added a new `alertmanager_marked_alerts` metric that retain the old behavior. #2943\n* [BUGFIX] Trim contents of Slack API URLs when reading from files. #2929\n* [BUGFIX] amtool: Avoid panic when the label value matcher is empty. #2968\n* [BUGFIX] Fail configuration loading if `api_url` is empty for OpsGenie. #2910\n* [BUGFIX] Fix email template for resolved notifications. #3166\n* [BUGFIX] Use the HTML template engine when the parse mode is HTML for Telegram. #3183\n\n## 0.24.0 / 2022-03-24\n\n* [CHANGE] Add the `/api/v2` prefix to all endpoints in the OpenAPI specification and generated client code. #2696\n* [CHANGE] Remove the `github.com/prometheus/alertmanager/client` Go package. #2763\n* [FEATURE] Add `--cluster.tls-config` experimental flag to secure cluster traffic via mutual TLS. #2237\n* [FEATURE] Add support for active time intervals. Active and mute time intervals should be defined via `time_intervals` rather than `mute_time_intervals` (the latter is deprecated but it will be supported until v1.0). #2779\n* [FEATURE] Add Telegram integration. #2827\n* [ENHANCEMENT] Add `update_alerts` field to the OpsGenie configuration to update message and description when sending alerts. #2519\n* [ENHANCEMENT] Add `--cluster.allow-insecure-public-advertise-address-discovery` feature flag to enable discovery and use of public IP addresses for clustering. #2719\n* [ENHANCEMENT] Add `entity` and `actions` fields to the OpsGenie configuration. #2753\n* [ENHANCEMENT] Add `opsgenie_api_key_file` field to the global configuration. #2728\n* [ENHANCEMENT] Add support for `teams` responders to the OpsGenie configuration. #2685\n* [ENHANCEMENT] Add the User-Agent header to all notification requests. #2730\n* [ENHANCEMENT] Re-enable HTTP/2. #2720\n* [ENHANCEMENT] web: Add support for security-related HTTP headers. #2759\n* [ENHANCEMENT] amtool: Allow filtering of silences by `createdBy` author. #2718\n* [ENHANCEMENT] amtool: add `--http.config.file` flag to configure HTTP settings. #2764\n* [BUGFIX] Fix HTTP client configuration for the SNS receiver. #2706\n* [BUGFIX] Fix unclosed file descriptor after reading the silences snapshot file. #2710\n* [BUGFIX] Fix field names for `mute_time_intervals` in JSON marshaling. #2765\n* [BUGFIX] Ensure that the root route doesn't have any matchers. #2780\n* [BUGFIX] Truncate the message's title to 1024 chars to avoid hitting Slack limits. #2774\n* [BUGFIX] Fix the default HTML email template (`email.default.html`) to match with the canonical source. #2798\n* [BUGFIX] Detect SNS FIFO topic based on the rendered value. #2819\n* [BUGFIX] Avoid deleting and recreating a silence when an update is possible. #2816\n* [BUGFIX] api/v2: Return 200 OK when deleting an expired silence. #2817\n* [BUGFIX] amtool: Fix the silence's end date when adding a silence. The end date is (start date + duration) while it used to be (current time + duration). The new behavior is consistent with the update operation. #2741\n\n## 0.23.0 / 2021-08-25\n\n* [FEATURE] Add AWS SNS receiver. #2615\n* [FEATURE] amtool: add new template render command. #2538\n* [ENHANCEMENT] amtool: Add ability to skip TLS verification for amtool. #2663\n* [ENHANCEMENT] amtool: Detect version drift and warn users. #2672\n* [BUGFIX] Time-based muting: Ensure time interval comparisons are in UTC. #2648\n* [BUGFIX] amtool: Fix empty isEqual when talking to incompatible alertmanager. #2668\n\n## 0.22.2 / 2021-06-01\n\n* [BUGFIX] Include pending silences for future muting decisions. #2590\n\n## 0.22.1 / 2021-05-27\n\nThis release addresses a regression in the API v1 that was introduced in 0.22.0.\nMatchers in silences created with the API v1 could be considered negative\nmatchers. This affects users using amtool prior to v0.17.0.\n\n* [BUGFIX] API v1: Decode matchers without isEqual are positive matchers. #2603\n\n## 0.22.0 / 2021-05-21\n\n* [CHANGE] Amtool and Alertmanager binaries help now prints to stdout. #2505\n* [CHANGE] Use path relative to the configuration file for certificates and password files. #2502\n* [CHANGE] Display Silence and Alert dates in ISO8601 format. #2363\n* [FEATURE] Add date picker to silence form views. #2262\n* [FEATURE] Add support for negative matchers. #2434 #2460 and many more.\n* [FEATURE] Add time-based muting to routing tree. #2393\n* [FEATURE] Support TLS and basic authentication on the web server. #2446\n* [FEATURE] Add OAuth 2.0 client support in HTTP client. #2560\n* [ENHANCEMENT] Add composite durations in the configuration (e.g. 2h20m). #2353\n* [ENHANCEMENT] Add follow_redirect option to disable following redirects. #2551\n* [ENHANCEMENT] Add metric for permanently failed notifications. #2383\n* [ENHANCEMENT] Add support for custom authorization scheme. #2499\n* [ENHANCEMENT] Add support for not following HTTP redirects. #2499\n* [ENHANCEMENT] Add support to set the Slack URL from a file. #2534\n* [ENHANCEMENT] amtool: Add alert status to extended and simple output. #2324\n* [ENHANCEMENT] Do not omit false booleans in the configuration page. #2317\n* [ENHANCEMENT] OpsGenie: Propagate labels to Opsgenie details. #2276\n* [ENHANCEMENT] PagerDuty: Filter out empty images and links. #2379\n* [ENHANCEMENT] WeChat: add markdown support. #2309\n* [BUGFIX] Fix a possible deadlock on shutdown. #2558\n* [BUGFIX] UI: Fix extended printing of regex sign. #2445\n* [BUGFIX] UI: Fix the favicon when using a path prefix. #2392\n* [BUGFIX] Make filter labels consistent with Prometheus. #2403\n* [BUGFIX] alertmanager_config_last_reload_successful takes templating failures into account. #2373\n* [BUGFIX] amtool: avoid nil dereference in silence update. #2427\n* [BUGFIX] VictorOps: Catch routing_key templating errors. #2467\n\n## 0.21.0 / 2020-06-16\n\nThis release removes the HipChat integration as it is discontinued by Atlassian on June 30th 2020.\n\n* [CHANGE] [HipChat] Remove HipChat integration as it is end-of-life. #2282\n* [CHANGE] [amtool] Remove default assignment of environment variables. #2161\n* [CHANGE] [PagerDuty] Enforce 512KB event size limit. #2225\n* [ENHANCEMENT] [amtool] Add `cluster` command to show cluster and peer statuses. #2256\n* [ENHANCEMENT] Add redirection from `/` to the routes prefix when it isn't empty. #2235\n* [ENHANCEMENT] [Webhook] Add `max_alerts` option to limit the number of alerts included in the payload. #2274\n* [ENHANCEMENT] Improve logs for API v2, notifications and clustering. #2177 #2188 #2260 #2261 #2273\n* [BUGFIX] Fix child routes not inheriting their parent route's grouping when `group_by: [...]`. #2154\n* [BUGFIX] [UI] Fix the receiver selector in the Alerts page when the receiver name contains regular expression metacharacters such as `+`. #2090\n* [BUGFIX] Fix error message about start and end time validation. #2173\n* [BUGFIX] Fix a potential race condition in dispatcher. #2208\n* [BUGFIX] [API v2] Return an empty array of peers when the clustering is disabled. #2203\n* [BUGFIX] Fix the registration of `alertmanager_dispatcher_aggregation_groups` and `alertmanager_dispatcher_alert_processing_duration_seconds` metrics. #2200\n* [BUGFIX] Always retry notifications with back-off. #2290\n\n## 0.20.0 / 2019-12-11\n\n* [CHANGE] Check that at least one silence matcher matches a non-empty string. #2081\n* [ENHANCEMENT] [pagerduty] Check that PagerDuty keys aren't empty. #2085\n* [ENHANCEMENT] [template] Add the `stringSlice` function. #2101\n* [ENHANCEMENT] Add `alertmanager_dispatcher_aggregation_groups` and `alertmanager_dispatcher_alert_processing_duration_seconds` metrics. #2113\n* [ENHANCEMENT] Log unused receivers. #2114\n* [ENHANCEMENT] Add `alertmanager_receivers` metric. #2114\n* [ENHANCEMENT] Add `alertmanager_integrations` metric. #2117\n* [ENHANCEMENT] [email] Add Message-Id Header to outgoing emails. #2057\n* [BUGFIX] Don't garbage-collect alerts from the store. #2040\n* [BUGFIX] [ui] Disable the grammarly plugin on all textareas. #2061\n* [BUGFIX] [config] Forbid nil regexp matchers. #2083\n* [BUGFIX] [ui] Fix Silences UI when several filters are applied. #2075\n\nContributors:\n\n* @CharlesJUDITH\n* @NotAFile\n* @Pger-Y\n* @TheMeier\n* @johncming\n* @n33pm\n* @ntk148v\n* @oddlittlebird\n* @perlun\n* @qoops-1\n* @roidelapluie\n* @simonpasquier\n* @stephenreddek\n* @sylr\n* @vrischmann\n\n## 0.19.0 / 2019-09-03\n\n* [CHANGE] Reject invalid external URLs at startup. #1960\n* [CHANGE] Add Fingerprint to template data. #1945\n* [CHANGE] Check Smarthost validity at config loading. #1957\n* [ENHANCEMENT] Improve error messages for email receiver. #1953\n* [ENHANCEMENT] Log error messages from OpsGenie API. #1965\n* [ENHANCEMENT] Add the ability to configure Slack markdown field. #1967\n* [ENHANCEMENT] Log warning when repeat_interval > retention. #1993\n* [ENHANCEMENT] Add `alertmanager_cluster_enabled` metric. #1973\n* [ENHANCEMENT] [ui] Recreate silence with previous comment. #1927\n* [BUGFIX] [ui] Fix /api/v2/alerts/groups endpoint with similar alert groups. #1964\n* [BUGFIX] Allow slashes in receivers. #2011\n* [BUGFIX] [ui] Fix expand/collapse button with identical alert groups. #2012\n\n## 0.18.0 / 2019-07-08\n\n* [CHANGE] Remove quantile labels from Summary metrics. #1921\n* [CHANGE] [OpsGenie] Move from the deprecated `teams` field in the configuration to `responders`. #1863\n* [CHANGE] [ui] Collapse alert groups on the initial view. #1876\n* [CHANGE] [Wechat] Set the default API secret to blank. #1888\n* [CHANGE/BUGFIX] [PagerDuty] Fix embedding of images, the `text` field in the configuration has been renamed to `href`. #1931\n* [ENHANCEMENT] Use persistent HTTP clients. #1904\n* [ENHANCEMENT] Add `alertmanager_cluster_alive_messages_total`, `alertmanager_cluster_peer_info` and `alertmanager_cluster_pings_seconds` metrics. #1941\n* [ENHANCEMENT] [api] Add missing metrics for API v2. #1902\n* [ENHANCEMENT] [Slack] Log error message on retry errors. #1655\n* [ENHANCEMENT] [ui] Allow to create silences from the alerts filter bar. #1911\n* [ENHANCEMENT] [ui] Enable auto resize the textarea fields. #1893\n* [BUGFIX] [amtool] Use scheme, authentication and base path from the URL if present. #1892 #1940\n* [BUGFIX] [amtool] Support filtering alerts by receiver. #1915\n* [BUGFIX] [api] Fix /api/v2/alerts with multiple receivers. #1948\n* [BUGFIX] [PagerDuty] Truncate description to 1024 chars for PagerDuty v1. #1922\n* [BUGFIX] [ui] Add filtering based off of \"active\" query param. #1879\n\n\n## 0.17.0 / 2019-05-02\n\nThis release includes changes to amtool which are not fully backwards\ncompatible with the previous amtool version (#1798) related to backup and\nimport of silences. If a backup of silences is created using a previous\nversion of amtool (v0.16.1 or earlier), it is possible that not all silences\ncan be correctly imported using a later version of amtool.\n\nAdditionally, the groups endpoint that was dropped from api v1 has been added\nto api v2. The default for viewing alerts in the UI now consumes from this\nendpoint and displays alerts grouped according to the groups defined in the\nrunning configuration. Custom grouping is still supported.\n\nThis release has added two new flags that may need to be tweaked. For people\nrunning with a lot of concurrent requests, consider increasing the value of\n`--web.get-concurrency`. An increase in 503 errors indicates that the request\nrate is exceeding the number of currently available workers. The other new\nflag, --web.timeout, limits the time a request is allowed to run. The default\nbehavior is to not use a timeout.\n\n* [CHANGE] Modify the self-inhibition prevention semantics (#1873)\n* [CHANGE] Make api/v2/status.cluster.{name,peers} properties optional for Alertmanager with disabled clustering (#1728)\n* [FEATURE] Add groups endpoint to v2 api (#1791)\n* [FEATURE] Optional timeout for HTTP requests (#1743)\n* [ENHANCEMENT] Set HTTP headers to prevent asset caching (#1817)\n* [ENHANCEMENT] API returns current silenced/inhibited state of alerts (#1733)\n* [ENHANCEMENT] Configurable concurrency limit for GET requests (#1743)\n* [ENHANCEMENT] Pushover notifier: support HTML, URL title and custom sounds (#1634)\n* [ENHANCEMENT] Support adding custom fields to VictorOps notifications (#1420)\n* [ENHANCEMENT] Migrate amtool CLI to API v2 (#1798)\n* [ENHANCEMENT][ui] Default alert list view grouped by configured alert groups (#1864)\n* [ENHANCEMENT][ui] Remove superfluous inhibited/silenced text, show inhibited status (#1698, #1862)\n* [ENHANCEMENT][ui] Silence preview now shows already-muted alerts (#1776)\n* [ENHANCEMENT][ui] Sort silences from api/v2 similarly to api/v1 (#1786)\n* [BUGFIX] Trim PagerDuty message summary to 1024 chars (#1701)\n* [BUGFIX] Add fix for race causing alerts to be dropped (#1843)\n* [BUGFIX][ui] Correctly construct filter query string for api (#1869)\n* [BUGFIX][ui] Do not display GroupByAll and GroupBy in marshaled config (#1665)\n* [BUGFIX][ui] Respect regex setting when creating silences (#1697)\n\n## 0.16.2 / 2019-04-03\n\nUpdating to v0.16.2 is recommended for all users using the Slack, Pagerduty,\nHipchat, Wechat, VictorOps and Pushover notifier, as connection errors could\nleak secrets embedded in the notifier's URL to stdout.\n\n* [BUGFIX] Redact notifier URL from logs to not leak secrets embedded in the URL (#1822, #1825)\n* [BUGFIX] Allow sending of unauthenticated SMTP requests when `smtp_auth_username` is not supplied (#1739)\n\n## 0.16.1 / 2019-01-31\n\n* [BUGFIX] Do not populate cluster info if clustering is disabled in API v2 (#1726)\n\n## 0.16.0 / 2019-01-17\n\nThis release introduces a new API v2, fully generated via the OpenAPI project\n[1]. At the same time with this release the previous API v1 is being\ndeprecated. API v1 will be removed with Alertmanager release v0.18.0.\n\n* [CHANGE] Deprecate API v1\n* [CHANGE] Remove `api/v1/alerts/groups` GET endpoint (#1508 & #1525)\n* [CHANGE] Revert Alertmanager working directory changes in Docker image back to `/alertmanager` (#1435)\n* [CHANGE] Using the recommended label syntax for maintainer in Dockerfile (#1533)\n* [CHANGE] Change `alertmanager_notifications_total` to count attempted notifications, not only successful ones (#1578)\n* [CHANGE] Run as nobody inside container (#1586)\n* [CHANGE] Support `w` for weeks when creating silences, remove `y` for year (#1620)\n* [FEATURE] Introduce OpenAPI generated API v2 (#1352)\n* [FEATURE] Lookup parts in strings using regexp.MatchString in templates (#1452)\n* [FEATURE] Support image/thumb url in attachment in Slack notifier (#1506)\n* [FEATURE] Support custom TLS certificates for the email notifier (#1528)\n* [FEATURE] Add support for images and links in the PagerDuty notification config (#1559)\n* [FEATURE] Add support for grouping by all labels (#1588)\n* [FEATURE] [amtool] Add timeout support to amtool commands (#1471)\n* [FEATURE] [amtool] Added `config routes` tools for visualization and testing routes (#1511)\n* [FEATURE] [amtool] Support adding alerts using amtool (#1461)\n* [ENHANCEMENT] Add support for --log.format (#1658)\n* [ENHANCEMENT] Add CORS support to API v2 (#1667)\n* [ENHANCEMENT] Support HTML, URL title and custom sounds for Pushover (#1634)\n* [ENHANCEMENT] Update Alert compact view (#1698)\n* [ENHANCEMENT] Support adding custom fields to VictorOps notifications (#1420)\n* [ENHANCEMENT] Add help link in UI to Alertmanager documentation (#1522)\n* [ENHANCEMENT] Enforce HTTP or HTTPS URLs in Alertmanager config (#1567)\n* [ENHANCEMENT] Make OpsGenie API Key a templated string (#1594)\n* [ENHANCEMENT] Add name, value and SlackConfirmationField to action in Slack notifier (#1557)\n* [ENHANCEMENT] Show more alert information on silence form and silence view pages (#1601)\n* [ENHANCEMENT] Add cluster peers DNS refresh job (#1428)\n* [BUGFIX] Fix unmarshaling of secret URLs in config (#1663)\n* [BUGFIX] Do not write groupbyall and groupby when marshaling config (#1665)\n* [BUGFIX] Make a copy of firing alerts with EndsAt=0 when flushing (#1686)\n* [BUGFIX] Respect regex matchers when recreating silences in UI (#1697)\n* [BUGFIX] Change DefaultGlobalConfig to a function in Alertmanager configuration (#1656)\n* [BUGFIX] Fix email template typo in alert-warning style (#1421)\n* [BUGFIX] Fix silence redirect on silence creation UI page (#1548)\n* [BUGFIX] Add missing `callback_id` parameter in Slack notifier (#1592)\n* [BUGFIX] Throw error if no auth mechanism matches in email notifier (#1608)\n* [BUGFIX] Use quoted-printable transfer encoding for the email notifier (#1609)\n* [BUGFIX] Do not merge expired gossip messages (#1631)\n* [BUGFIX] Fix \"PLAIN\" auth during notification via smtp-over-tls on port 465 (#1591)\n* [BUGFIX] [amtool] Support for assuming first label is alertname in silence add and query (#1693)\n* [BUGFIX] [amtool] Support assuming first label is alertname in alert query with matchers (#1575)\n* [BUGFIX] [amtool] Fix config path check in amtool (#1538)\n* [BUGFIX] [amtool] Fix rfc3339 example texts (#1526)\n* [BUGFIX] [amtool] Fixed issue with loading path of a default configs (#1529)\n\n[1] https://github.com/prometheus/alertmanager#api\n\n## 0.15.3 / 2018-11-09\n\n* [BUGFIX] Fix alert merging supporting both empty and set EndsAt property for firing alerts send by Prometheus (#1611)\n\n## 0.15.2 / 2018-08-14\n\n* [ENHANCEMENT] [amtool] Add support for stdin to check-config (#1431)\n* [ENHANCEMENT] Log PagerDuty v1 response on BadRequest (#1481)\n* [BUGFIX] Correctly encode query strings in notifiers (#1516)\n* [BUGFIX] Add cache control headers to the API responses to avoid IE caching (#1500)\n* [BUGFIX] Avoid listener blocking on unsubscribe (#1482)\n* [BUGFIX] Fix a bunch of unhandled errors (#1501)\n* [BUGFIX] Update PagerDuty API V2 to send full details on resolve (#1483)\n* [BUGFIX] Validate URLs at config load time (#1468)\n* [BUGFIX] Fix Settle() interval (#1478)\n* [BUGFIX] Fix email to be green if only none firing (#1475)\n* [BUGFIX] Handle errors in notify (#1474)\n* [BUGFIX] Fix templating of hipchat room id (#1463)\n\n## 0.15.1 / 2018-07-10\n\n* [BUGFIX] Fix email template typo in alert-warning style (#1421)\n* [BUGFIX] Fix regression in Pager Duty config (#1455)\n* [BUGFIX] Catch templating errors in Wechat Notify (#1436)\n* [BUGFIX] Fail when no private address can be found for cluster (#1437)\n* [BUGFIX] Make sure we don't miss the first pushPull when joining cluster (#1456)\n* [BUGFIX] Fix concurrent read and write group error in dispatch (#1447)\n\n## 0.15.0 / 2018-06-22\n\n* [CHANGE] [amtool] Update silence add and update flags (#1298)\n* [CHANGE] Replace deprecated InstrumentHandler() (#1302)\n* [CHANGE] Validate Slack field config and only allow the necessary input (#1334)\n* [CHANGE] Remove legacy alert ingest endpoint (#1362)\n* [CHANGE] Move to memberlist as underlying gossip protocol including cluster flag changes from --mesh.xxx to --cluster.xxx (#1232)\n* [CHANGE] Move Alertmanager working directory in Docker image to /etc/alertmanager (#1313)\n* [BUGFIX/CHANGE] The default group by is no labels. (#1287)\n* [FEATURE] [amtool] Filter alerts by receiver (#1402)\n* [FEATURE] Wait for mesh to settle before sending alerts (#1209)\n* [FEATURE] [amtool] Support basic auth in alertmanager url (#1279)\n* [FEATURE] Make HTTP clients used for integrations configurable\n* [ENHANCEMENT] Support receiving alerts with end time and zero start time\n* [ENHANCEMENT] Sort dispatched alerts by job+instance (#1234)\n* [ENHANCEMENT] Support alert query filters `active` and `unprocessed` (#1366)\n* [ENHANCEMENT] [amtool] Expose alert query flags --active and --unprocessed (#1370)\n* [ENHANCEMENT] Add Slack actions to notifications (#1355)\n* [BUGFIX] Register nflog snapShotSize metric\n* [BUGFIX] Sort alerts in correct order before flushing to notifiers (#1349)\n* [BUGFIX] Don't reset initial wait timer if flush is in-progress (#1301)\n* [BUGFIX] Fix resolved alerts still inhibiting (#1331)\n* [BUGFIX] Template wechat config fields (#1356)\n* [BUGFIX] Notify resolved alerts properly (#1408)\n* [BUGFIX] Fix parsing for label values with commas (#1395)\n* [BUGFIX] Hide sensitive Wechat configuration (#1253)\n* [BUGFIX] Prepopulate matchers when recreating a silence (#1270)\n* [BUGFIX] Fix wechat panic (#1293)\n* [BUGFIX] Allow empty matchers in silences/filtering (#1289)\n* [BUGFIX] Properly configure HTTP client for Wechat integration\n\n## 0.14.0 / 2018-02-12\n\n* [ENHANCEMENT] [amtool] Silence update support dwy suffixes to expire flag (#1197)\n* [ENHANCEMENT] Allow templating PagerDuty receiver severity (#1214)\n* [ENHANCEMENT] Include receiver name in failed notifications log messages (#1207)\n* [ENHANCEMENT] Allow global opsgenie api key (#1208)\n* [ENHANCEMENT] Add mesh metrics (#1225)\n* [ENHANCEMENT] Add Class field to PagerDuty; add templating to PagerDuty-CEF fields (#1231)\n* [BUGFIX] Don't notify of resolved alerts if none were reported firing (#1198)\n* [BUGFIX] Notify only when new firing alerts are added (#1205)\n* [BUGFIX] [mesh] Fix pending connections never set to established (#1204)\n* [BUGFIX] Allow OpsGenie notifier to have empty team fields (#1224)\n* [BUGFIX] Don't count alerts with EndTime in the future as resolved (#1233)\n* [BUGFIX] Speed up re-rendering of Silence UI (#1235)\n* [BUGFIX] Forbid 0 value for group_interval and repeat_interval (#1230)\n* [BUGFIX] Fix WeChat agentid issue (#1229)\n\n## 0.13.0 / 2018-01-12\n\n* [CHANGE] Switch cmd/alertmanager to kingpin (#974)\n* [CHANGE] [amtool] Switch amtool to kingpin (#976)\n* [CHANGE] [amtool] silence query: --expired flag only shows expired silences (#1190)\n* [CHANGE] Return config reload result from reload endpoint (#1180)\n* [FEATURE] UI silence form is populated from location bar (#1148)\n* [FEATURE] Add /-/healthy endpoint (#1159)\n* [ENHANCEMENT] Instrument and log snapshot sizes on maintenance (#1155)\n* [ENHANCEMENT] Make alertGC interval configurable (#1151)\n* [ENHANCEMENT] Display mesh connections in the Status page (#1164)\n* [BUGFIX] Template service keys for pagerduty notifier (#1182)\n* [BUGFIX] Fix expire buttons on the silences page (#1171)\n* [BUGFIX] Fix JavaScript error in MSIE due to endswith() usage (#1172)\n* [BUGFIX] Correctly format UI error output (#1167)\n\n## 0.12.0 / 2017-12-15\n\n* [FEATURE] package amtool in docker container (#1127)\n* [FEATURE] Add notify support for Chinese User wechat (#1059)\n* [FEATURE] [amtool] Add a new `silence import` command (#1082)\n* [FEATURE] [amtool] Add new command to update silence (#1123)\n* [FEATURE] [amtool] Add ability to query for silences that will expire soon (#1120)\n* [ENHANCEMENT] Template source field in PagerDuty alert payload (#1117)\n* [ENHANCEMENT] Add footer field for slack messages (#1141)\n* [ENHANCEMENT] Add Slack additional \"fields\" to notifications (#1135)\n* [ENHANCEMENT] Adding check for webhook's URL formatting (#1129)\n* [ENHANCEMENT] Let the browser remember the creator of a silence (#1112)\n* [BUGFIX] Fix race in stopping inhibitor (#1118)\n* [BUGFIX] Fix browser UI when entering negative duration (#1132)\n\n## 0.11.0 / 2017-11-16\n\n* [CHANGE] Make silence negative filtering consistent with alert filtering (#1095)\n* [CHANGE] Change HipChat and OpsGenie api config names (#1087)\n* [ENHANCEMENT] amtool: Allow 'd', 'w', 'y' time suffixes when creating silence (#1091)\n* [ENHANCEMENT] Support OpsGenie Priority field (#1094)\n* [BUGFIX] Fix UI when no silences are present (#1090)\n* [BUGFIX] Fix OpsGenie Teams field (#1101)\n* [BUGFIX] Fix OpsGenie Tags field (#1108)\n\n## 0.10.0 / 2017-11-09\n\n* [CHANGE] Prevent inhibiting alerts in the source of the inhibition (#1017)\n* [ENHANCEMENT] Improve amtool check-config use and description text (#1016)\n* [ENHANCEMENT] Add metrics about current silences and alerts (#998)\n* [ENHANCEMENT] Sorted silences based on current status (#1015)\n* [ENHANCEMENT] Add metric of alertmanager position in mesh (#1024)\n* [ENHANCEMENT] Initialise notifications_total and notifications_failed_total (#1011)\n* [ENHANCEMENT] Allow selectable matchers on silence view (#1030)\n* [ENHANCEMENT] Allow template in victorops message_type field (#1038)\n* [ENHANCEMENT] Optionally hide inhibited alerts in API response (#1039)\n* [ENHANCEMENT] Toggle silenced and inhibited alerts in UI (#1049)\n* [ENHANCEMENT] Fix pushover limits (title, message, url) (#1055)\n* [ENHANCEMENT] Add limit to OpsGenie message (#1045)\n* [ENHANCEMENT] Upgrade OpsGenie notifier to v2 API. (#1061)\n* [ENHANCEMENT] Allow template in victorops routing_key field (#1083)\n* [ENHANCEMENT] Add support for PagerDuty API v2 (#1054)\n* [BUGFIX] Fix inhibit race (#1032)\n* [BUGFIX] Fix segfault on amtool (#1031)\n* [BUGFIX] Remove .WasInhibited and .WasSilenced fields of Alert type (#1026)\n* [BUGFIX] nflog: Fix Log() crash when gossip is nil (#1064)\n* [BUGFIX] Fix notifications for flapping alerts (#1071)\n* [BUGFIX] Fix shutdown crash with nil mesh router (#1077)\n* [BUGFIX] Fix negative matchers filtering (#1077)\n\n## 0.9.1 / 2017-09-29\n* [BUGFIX] Fix -web.external-url regression in ui (#1008)\n* [BUGFIX] Fix multipart email implementation (#1009)\n\n## 0.9.0 / 2017-09-28\n* [ENHANCEMENT] Add current time to webhook message (#909)\n* [ENHANCEMENT] Add link_names to slack notifier (#912)\n* [ENHANCEMENT] Make ui labels selectable/highlightable (#932)\n* [ENHANCEMENT] Make links in ui annotations selectable (#946)\n* [ENHANCEMENT] Expose the alert's \"fingerprint\" (unique identifier) through API (#786)\n* [ENHANCEMENT] Add README information for amtool (#939)\n* [ENHANCEMENT] Use user-set logging option consistently throughout alertmanager (#968)\n* [ENHANCEMENT] Sort alerts returned from API by their fingerprint (#969)\n* [ENHANCEMENT] Add edit/delete silence buttons on silence page view (#970)\n* [ENHANCEMENT] Add check-config subcommand to amtool (#978)\n* [ENHANCEMENT] Add email notification text content support (#934)\n* [ENHANCEMENT] Support passing binary name to make build target (#990)\n* [ENHANCEMENT] Show total no. of silenced alerts in preview (#994)\n* [ENHANCEMENT] Added confirmation dialog when expiring silences (#993)\n* [BUGFIX] Fix crash when no mesh router is configured (#919)\n* [BUGFIX] Render status page without mesh (#920)\n* [BUGFIX] Exit amtool subcommands with non-zero error code (#938)\n* [BUGFIX] Change mktemp invocation in makefile to work for macOS (#971)\n* [BUGFIX] Add a mutex to silences.go:gossipData (#984)\n* [BUGFIX] silences: avoid deadlock (#995)\n* [BUGFIX] Ignore expired silences OnGossip (#999)\n\n## 0.8.0 / 2017-07-20\n\n* [FEATURE] Add ability to filter alerts by receiver in the UI (#890)\n* [FEATURE] Add User-Agent for webhook requests (#893)\n* [ENHANCEMENT] Add possibility to have a global victorops api_key (#897)\n* [ENHANCEMENT] Add EntityDisplayName and improve StateMessage for Victorops\n  (#769)\n* [ENHANCEMENT] Omit empty config fields and show regex upon re-marshaling to\n  elide secrets (#864)\n* [ENHANCEMENT] Parse API error messages in UI (#866)\n* [ENHANCEMENT] Enable sending mail via smtp port 465 (#704)\n* [BUGFIX] Prevent duplicate notifications by sorting matchers (#882)\n* [BUGFIX] Remove timeout for UI requests (#890)\n* [BUGFIX] Update config file location of CLI in flag usage text (#895)\n\n## 0.7.1 / 2017-06-09\n\n* [BUGFIX] Fix filtering by label on Alert list and Silence list page\n\n## 0.7.0 / 2017-06-08\n\n* [CHANGE] Rewrite UI from scratch improving UX\n* [CHANGE] Rename `config` to `configYAML` on `api/v1/status`\n* [FEATURE] Add ability to update a silence on `api/v1/silences` POST endpoint (See #765)\n* [FEATURE] Return alert status on `api/v1/alerts` GET endpoint\n* [FEATURE] Serve silence state on `api/v1/silences` GET endpoint\n* [FEATURE] Add ability to specify a route prefix\n* [FEATURE] Add option to disable AM listening on mesh port\n* [ENHANCEMENT] Add ability to specify `filter` string and `silenced` flag on `api/v1/alerts` GET endpoint\n* [ENHANCEMENT] Update `cache-control` to prevent caching for web assets in general.\n* [ENHANCEMENT] Serve web assets by alertmanager instead of external CDN (See #846)\n* [ENHANCEMENT] Elide secrets in alertmanager config (See #840)\n* [ENHANCEMENT] AMTool: Move config file to a more consistent location (See #843)\n* [BUGFIX] Enable builds for Solaris/Illumos\n* [BUGFIX] Load web assets based on url path (See #323)\n\n## 0.6.2 / 2017-05-09\n\n* [BUGFIX] Correctly link to silences from alert again\n* [BUGFIX] Correctly hide silenced/show active alerts in UI again\n* [BUGFIX] Fix regression of alerts not being displayed until first processing\n* [BUGFIX] Fix internal usage of wrong lock for silence markers\n* [BUGFIX] Adapt amtool's API parsing to recent API changes\n* [BUGFIX] Correctly marshal regexes in config JSON response\n* [CHANGE] Anchor silence regex matchers to be consistent with Prometheus\n* [ENHANCEMENT] Error if root route is using `continue` keyword\n\n## 0.6.1 / 2017-04-28\n\n* [BUGFIX] Fix incorrectly serialized hash for notification providers.\n* [ENHANCEMENT] Add processing status field to alerts.\n* [FEATURE] Add config hash metric.\n\n## 0.6.0 / 2017-04-25\n\n* [BUGFIX] Add `groupKey` to `alerts/groups` endpoint https://github.com/prometheus/alertmanager/pull/576\n* [BUGFIX] Only notify on firing alerts https://github.com/prometheus/alertmanager/pull/595\n* [BUGFIX] Correctly marshal regex's in config for routing tree https://github.com/prometheus/alertmanager/pull/602\n* [BUGFIX] Prevent panic when failing to load config https://github.com/prometheus/alertmanager/pull/607\n* [BUGFIX] Prevent panic when alertmanager is started with an empty `-mesh.peer` https://github.com/prometheus/alertmanager/pull/726\n* [CHANGE] Rename VictorOps config variables https://github.com/prometheus/alertmanager/pull/667\n* [CHANGE] No longer generate releases for openbsd/arm https://github.com/prometheus/alertmanager/pull/732\n* [ENHANCEMENT] Add `DELETE` as accepted CORS method https://github.com/prometheus/alertmanager/commit/0ecc59076ca6b4cbb63252fa7720a3d89d1c81d3\n* [ENHANCEMENT] Switch to using `gogoproto` for protobuf https://github.com/prometheus/alertmanager/pull/715\n* [ENHANCEMENT] Include notifier type in logs and errors https://github.com/prometheus/alertmanager/pull/702\n* [FEATURE] Expose mesh peers on status page https://github.com/prometheus/alertmanager/pull/644\n* [FEATURE] Add `reReplaceAll` template function https://github.com/prometheus/alertmanager/pull/639\n* [FEATURE] Allow label-based filtering alerts/silences through API https://github.com/prometheus/alertmanager/pull/633\n* [FEATURE] Add commandline tool for interacting with alertmanager https://github.com/prometheus/alertmanager/pull/636\n\n## 0.5.1 / 2016-11-24\n\n* [BUGFIX] Fix crash caused by race condition in silencing\n* [ENHANCEMENT] Improve logging of API errors\n* [ENHANCEMENT] Add metrics for the notification log\n\n## 0.5.0 / 2016-11-01\n\nThis release requires a storage wipe. It contains fundamental internal\nchanges that came with implementing the high availability mode.\n\n* [FEATURE] Alertmanager clustering for high availability\n* [FEATURE] Garbage collection of old silences and notification logs\n* [CHANGE] New storage format\n* [CHANGE] Stricter silence semantics for consistent historical view\n\n## 0.4.2 / 2016-09-02\n\n* [BUGFIX] Fix broken regex checkbox in silence form\n* [BUGFIX] Simplify inconsistent silence update behavior\n\n## 0.4.1 / 2016-08-31\n\n* [BUGFIX] Wait for silence query to finish instead of showing error\n* [BUGFIX] Fix sorting of silences\n* [BUGFIX] Provide visual feedback after creating a silence\n* [BUGFIX] Fix styling of silences\n* [ENHANCEMENT] Provide cleaner API silence interface\n\n## 0.4.0 / 2016-08-23\n\n* [FEATURE] Silences are now paginated in the web ui\n* [CHANGE] Failure to start on unparsed flags\n\n## 0.3.0 / 2016-07-07\n\n* [CHANGE] Alerts are purely in memory and no longer persistent across restarts\n* [FEATURE] Add SMTP LOGIN authentication mechanism\n\n## 0.2.1 / 2016-06-23\n\n* [ENHANCEMENT] Allow inheritance of route receiver\n* [ENHANCEMENT] Add silence cache to silence provider\n* [BUGFIX] Fix HipChat room number in integration URL\n\n## 0.2.0 / 2016-06-17\n\nThis release uses a new storage backend based on BoltDB. You have to backup\nand wipe your former storage path to run it.\n\n* [CHANGE] Use BoltDB as data store.\n* [CHANGE] Move SMTP authentication to configuration file\n* [FEATURE] add /-/reload HTTP endpoint\n* [FEATURE] Filter silenced alerts in web UI\n* [ENHANCEMENT] reduce inhibition computation complexity\n* [ENHANCEMENT] Add support for teams and tags in OpsGenie integration\n* [BUGFIX] Handle OpsGenie responses correctly\n* [BUGFIX] Fix Pushover queue length issue\n* [BUGFIX] STARTTLS before querying auth mechanism in email integration\n\n## 0.1.1 / 2016-03-15\n* [BUGFIX] Fix global database lock issue\n* [ENHANCEMENT] Improve SQLite alerts index\n* [ENHANCEMENT] Enable debug endpoint\n\n## 0.1.0 / 2016-02-23\nThis version is a full rewrite of the Alertmanager with a very different\nfeature set. Thus, there is no meaningful changelog.\n\nChanges with respect to 0.1.0-beta2:\n* [CHANGE] Expose same data structure to templates and webhook\n* [ENHANCEMENT] Show generator URL in default templates and web UI\n* [ENHANCEMENT] Support for Slack icon_emoji field\n* [ENHANCEMENT] Expose incident key to templates and webhook data\n* [ENHANCEMENT] Allow markdown in Slack 'text' field\n* [BUGFIX] Fixed database locking issue\n\n## 0.1.0-beta2 / 2016-02-03\n* [BUGFIX] Properly set timeout for incoming alerts with fixed start time\n* [ENHANCEMENT] Send source field in OpsGenie integration\n* [ENHANCEMENT] Improved routing configuration validation\n* [FEATURE] Basic instrumentation added\n\n## 0.1.0-beta1 / 2016-01-08\n* [BUGFIX] Send full alert group state on each update. Fixes erroneous resolved notifications.\n* [FEATURE] HipChat integration\n* [CHANGE] Slack integration no longer sends resolved notifications by default\n\n## 0.1.0-beta0 / 2015-12-23\nThis version is a full rewrite of the Alertmanager with a very different\nfeature set. Thus, there is no meaningful changelog.\n\n## 0.0.4 / 2015-09-09\n* [BUGFIX] Fix version info string in startup message.\n* [BUGFIX] Fix Pushover notifications by setting the right priority level, as\n  well as required retry and expiry intervals.\n* [FEATURE] Make it possible to link to individual alerts in the UI.\n* [FEATURE] Rearrange alert columns in UI and allow expanding more alert details.\n* [FEATURE] Add Amazon SNS notifications.\n* [FEATURE] Add OpsGenie Webhook notifications.\n* [FEATURE] Add `-web.external-url` flag to control the externally visible\n  Alertmanager URL.\n* [FEATURE] Add runbook and alertmanager URLs to PagerDuty and email notifications.\n* [FEATURE] Add a GET API to /api/alerts which pulls JSON formatted\n  AlertAggregates.\n* [ENHANCEMENT] Sort alerts consistently in web UI.\n* [ENHANCEMENT] Suggest to use email address as silence creator.\n* [ENHANCEMENT] Make Slack timeout configurable.\n* [ENHANCEMENT] Add channel name to error logging about Slack notifications.\n* [ENHANCEMENT] Refactoring and tests for Flowdock notifications.\n* [ENHANCEMENT] New Dockerfile using alpine-golang-make-onbuild base image.\n* [CLEANUP] Add Docker instructions and other cleanups in README.md.\n* [CLEANUP] Update Makefile.COMMON from prometheus/utils.\n\n## 0.0.3 / 2015-06-10\n* [BUGFIX] Fix email template body writer being called with parameters in wrong order.\n\n## 0.0.2 / 2015-06-09\n\n* [BUGFIX] Fixed silences.json permissions in Docker image.\n* [CHANGE] Changed case of API JSON properties to initial lower letter.\n* [CHANGE] Migrated logging to use http://github.com/prometheus/log.\n* [FEATURE] Flowdock notification support.\n* [FEATURE] Slack notification support.\n* [FEATURE] Generic webhook notification support.\n* [FEATURE] Support for \"@\"-mentions in HipChat notifications.\n* [FEATURE] Path prefix option to support reverse proxies.\n* [ENHANCEMENT] Improved web redirection and 404 behavior.\n* [CLEANUP] Updated compiled web assets from source.\n* [CLEANUP] Updated fsnotify package to its new source location.\n* [CLEANUP] Updates to README.md and AUTHORS.md.\n* [CLEANUP] Various smaller cleanups and improvements.\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Prometheus Community Code of Conduct\n\nPrometheus follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md).\n"
  },
  {
    "path": "COPYRIGHT.txt",
    "content": "Copyright Prometheus Team\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "Dockerfile",
    "content": "ARG ARCH=\"amd64\"\nARG OS=\"linux\"\nFROM quay.io/prometheus/busybox-${OS}-${ARCH}:latest\nLABEL maintainer=\"The Prometheus Authors <prometheus-developers@googlegroups.com>\"\nLABEL org.opencontainers.image.source=\"https://github.com/prometheus/alertmanager\"\n\nARG ARCH=\"amd64\"\nARG OS=\"linux\"\nCOPY .build/${OS}-${ARCH}/amtool       /bin/amtool\nCOPY .build/${OS}-${ARCH}/alertmanager /bin/alertmanager\nCOPY examples/ha/alertmanager.yml      /etc/alertmanager/alertmanager.yml\n\nRUN mkdir -p /alertmanager && \\\n    chown -R nobody:nobody /etc/alertmanager /alertmanager && \\\n    chmod -R g+w /alertmanager\n\nUSER       nobody\nEXPOSE     9093\nVOLUME     [ \"/alertmanager\" ]\nWORKDIR    /alertmanager\nENTRYPOINT [ \"/bin/alertmanager\" ]\nCMD        [ \"--config.file=/etc/alertmanager/alertmanager.yml\", \\\n             \"--storage.path=/alertmanager\" ]\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "MAINTAINERS.md",
    "content": "* Simon Pasquier <pasquier.simon@gmail.com> @simonpasquier\n* Andrey Kuzmin <unsoundscapes@gmail.com> @w0rm\n* Josue Abreu <josue.abreu@gmail.com> @gotjosh\n* George Robinson <george.robinson@grafana.com> @grobinson-grafana\n* Solomon Jacobs <solomonjacobs@protonmail.com> @SoloJacobs\n* Ethan Hunter <fc.spaceman@gmail.com> @Spaceman1701\n* Guido Trotter <ultrotter@gmail.com> @ultrotter\n* Siavash Safi <siavash@cloudflare.com> @siavashs\n"
  },
  {
    "path": "Makefile",
    "content": "# Copyright 2015 The Prometheus Authors\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# Needs to be defined before including Makefile.common to auto-generate targets\nDOCKER_ARCHS ?= amd64 armv7 arm64 ppc64le s390x\n\ninclude Makefile.common\n\nFRONTEND_DIR             = $(BIN_DIR)/ui/app\nTEMPLATE_DIR             = $(BIN_DIR)/template\nDOCKER_IMAGE_NAME       ?= alertmanager\n\nSTATICCHECK_IGNORE =\n\n.PHONY: build-all\n# Will build both the front-end as well as the back-end\nbuild-all: assets apiv2 build\n\n.PHONY: build\nbuild: common-build\n\n.PHONY: lint\nlint: common-lint\n\n.PHONY: assets\nassets: ui/app/script.js template/email.tmpl\n\nui/app/script.js: $(shell find ui/app/src -iname *.elm) api/v2/openapi.yaml\n\tcd $(FRONTEND_DIR) && $(MAKE) script.js\n\ntemplate/email.tmpl: template/email.html\n\tcd $(TEMPLATE_DIR) && $(MAKE) email.tmpl\n\n.PHONY: apiv2\napiv2: api/v2/models api/v2/restapi api/v2/client\n\n\napi/v2/models api/v2/restapi api/v2/client:  api/v2/openapi.yaml\n\tscripts/swagger.sh\n\n.PHONY: fuzz-config\nfuzz-config:\n\tgo test -fuzz=^Fuzz -fuzztime=5s ./config\n\n.PHONY: clean\nclean:\n\t- @rm -rf template/email.tmpl \\\n                  api/v2/models api/v2/restapi api/v2/client\n\t- @cd $(FRONTEND_DIR) && $(MAKE) clean\n"
  },
  {
    "path": "Makefile.common",
    "content": "# Copyright The Prometheus Authors\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\n# A common Makefile that includes rules to be reused in different prometheus projects.\n# !!! Open PRs only against the prometheus/prometheus/Makefile.common repository!\n\n# Example usage :\n# Create the main Makefile in the root project directory.\n# include Makefile.common\n# customTarget:\n# \t@echo \">> Running customTarget\"\n#\n\n# Ensure GOBIN is not set during build so that promu is installed to the correct path\nunexport GOBIN\n\nGO           ?= go\nGOFMT        ?= $(GO)fmt\nFIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH)))\nGOOPTS       ?=\nGOHOSTOS     ?= $(shell $(GO) env GOHOSTOS)\nGOHOSTARCH   ?= $(shell $(GO) env GOHOSTARCH)\n\nGO_VERSION        ?= $(shell $(GO) version)\nGO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION))\nPRE_GO_111        ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\\.(10|[0-9])\\.')\n\nPROMU        := $(FIRST_GOPATH)/bin/promu\npkgs          = ./...\n\nifeq (arm, $(GOHOSTARCH))\n\tGOHOSTARM ?= $(shell GOARM= $(GO) env GOARM)\n\tGO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)v$(GOHOSTARM)\nelse\n\tGO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)\nendif\n\nGOTEST := $(GO) test\nGOTEST_DIR :=\nifneq ($(CIRCLE_JOB),)\nifneq ($(shell command -v gotestsum 2> /dev/null),)\n\tGOTEST_DIR := test-results\n\tGOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml --\nendif\nendif\n\nPROMU_VERSION ?= 0.18.0\nPROMU_URL     := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz\n\nSKIP_GOLANGCI_LINT :=\nGOLANGCI_LINT :=\nGOLANGCI_LINT_OPTS ?=\nGOLANGCI_LINT_VERSION ?= v2.10.1\nGOLANGCI_FMT_OPTS ?=\n# golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64.\n# windows isn't included here because of the path separator being different.\nifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))\n\tifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64))\n\t\t# If we're in CI and there is an Actions file, that means the linter\n\t\t# is being run in Actions, so we don't need to run it here.\n\t\tifneq (,$(SKIP_GOLANGCI_LINT))\n\t\t\tGOLANGCI_LINT :=\n\t\telse ifeq (,$(CIRCLE_JOB))\n\t\t\tGOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint\n\t\telse ifeq (,$(wildcard .github/workflows/golangci-lint.yml))\n\t\t\tGOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint\n\t\tendif\n\tendif\nendif\n\nPREFIX                  ?= $(shell pwd)\nBIN_DIR                 ?= $(shell pwd)\nDOCKER_IMAGE_TAG        ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD))\nDOCKERBUILD_CONTEXT     ?= ./\nDOCKER_REPO             ?= prom\n\n# Check if deprecated DOCKERFILE_PATH is set\nifdef DOCKERFILE_PATH\n$(error DOCKERFILE_PATH is deprecated. Use DOCKERFILE_VARIANTS ?= $(DOCKERFILE_PATH) in the Makefile)\nendif\n\nDOCKER_ARCHS            ?= amd64\nDOCKERFILE_VARIANTS     ?= Dockerfile $(wildcard Dockerfile.*)\n\n# Function to extract variant from Dockerfile label.\n# Returns the variant name from io.prometheus.image.variant label, or \"default\" if not found.\ndefine dockerfile_variant\n$(strip $(or $(shell sed -n 's/.*io\\.prometheus\\.image\\.variant=\"\\([^\"]*\\)\".*/\\1/p' $(1)),default))\nendef\n\n# Check for duplicate variant names (including default for Dockerfiles without labels).\nDOCKERFILE_VARIANT_NAMES := $(foreach df,$(DOCKERFILE_VARIANTS),$(call dockerfile_variant,$(df)))\nDOCKERFILE_VARIANT_NAMES_SORTED := $(sort $(DOCKERFILE_VARIANT_NAMES))\nifneq ($(words $(DOCKERFILE_VARIANT_NAMES)),$(words $(DOCKERFILE_VARIANT_NAMES_SORTED)))\n$(error Duplicate variant names found. Each Dockerfile must have a unique io.prometheus.image.variant label, and only one can be without a label (default))\nendif\n\n# Build variant:dockerfile pairs for shell iteration.\nDOCKERFILE_VARIANTS_WITH_NAMES := $(foreach df,$(DOCKERFILE_VARIANTS),$(call dockerfile_variant,$(df)):$(df))\n\n# Shell helper to check whether a dockerfile/arch pair is excluded.\ndefine dockerfile_arch_is_excluded\ncase \" $(DOCKERFILE_ARCH_EXCLUSIONS) \" in \\\n\t*\" $$dockerfile:$(1) \"*) true ;; \\\n\t*) false ;; \\\nesac\nendef\n\n# Shell helper to check whether a registry/arch pair is excluded.\n# Extracts registry from DOCKER_REPO (e.g., quay.io/prometheus -> quay.io)\ndefine registry_arch_is_excluded\nregistry=$$(echo \"$(DOCKER_REPO)\" | cut -d'/' -f1); \\\ncase \" $(DOCKER_REGISTRY_ARCH_EXCLUSIONS) \" in \\\n\t*\" $$registry:$(1) \"*) true ;; \\\n\t*) false ;; \\\nesac\nendef\n\nBUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS))\nPUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS))\nTAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS))\n\nSANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG))\n\nifeq ($(GOHOSTARCH),amd64)\n        ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows))\n                # Only supported on amd64\n                test-flags := -race\n        endif\nendif\n\n# This rule is used to forward a target like \"build\" to \"common-build\".  This\n# allows a new \"build\" target to be defined in a Makefile which includes this\n# one and override \"common-build\" without override warnings.\n%: common-% ;\n\n.PHONY: common-all\ncommon-all: precheck style check_license lint yamllint unused build test\n\n.PHONY: common-style\ncommon-style:\n\t@echo \">> checking code style\"\n\t@fmtRes=$$($(GOFMT) -d $$(git ls-files '*.go' ':!:vendor/*' || find . -path ./vendor -prune -o -name '*.go' -print)); \\\n\tif [ -n \"$${fmtRes}\" ]; then \\\n\t\techo \"gofmt checking failed!\"; echo \"$${fmtRes}\"; echo; \\\n\t\techo \"Please ensure you are using $$($(GO) version) for formatting code.\"; \\\n\t\texit 1; \\\n\tfi\n\n.PHONY: common-check_license\ncommon-check_license:\n\t@echo \">> checking license header\"\n\t@licRes=$$(for file in $$(git ls-files '*.go' ':!:vendor/*' || find . -path ./vendor -prune -o -type f -iname '*.go' -print) ; do \\\n               awk 'NR<=3' $$file | grep -Eq \"(Copyright|generated|GENERATED)\" || echo $$file; \\\n       done); \\\n       if [ -n \"$${licRes}\" ]; then \\\n               echo \"license header checking failed:\"; echo \"$${licRes}\"; \\\n               exit 1; \\\n       fi\n\t@echo \">> checking for copyright years 2026 or later\"\n\t@futureYearRes=$$(git grep -E 'Copyright (202[6-9]|20[3-9][0-9])' -- '*.go' ':!:vendor/*' || true); \\\n\tif [ -n \"$${futureYearRes}\" ]; then \\\n\t\techo \"Files with copyright year 2026 or later found (should use 'Copyright The Prometheus Authors'):\"; echo \"$${futureYearRes}\"; \\\n\t\texit 1; \\\n\tfi\n\n.PHONY: common-deps\ncommon-deps:\n\t@echo \">> getting dependencies\"\n\t$(GO) mod download\n\n.PHONY: update-go-deps\nupdate-go-deps:\n\t@echo \">> updating Go dependencies\"\n\t@for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \\\n\t\t$(GO) get $$m; \\\n\tdone\n\t$(GO) mod tidy\n\n.PHONY: common-test-short\ncommon-test-short: $(GOTEST_DIR)\n\t@echo \">> running short tests\"\n\t$(GOTEST) -short $(GOOPTS) $(pkgs)\n\n.PHONY: common-test\ncommon-test: $(GOTEST_DIR)\n\t@echo \">> running all tests\"\n\t$(GOTEST) $(test-flags) $(GOOPTS) $(pkgs)\n\n$(GOTEST_DIR):\n\t@mkdir -p $@\n\n.PHONY: common-format\ncommon-format: $(GOLANGCI_LINT)\n\t@echo \">> formatting code\"\n\t$(GO) fmt $(pkgs)\nifdef GOLANGCI_LINT\n\t@echo \">> formatting code with golangci-lint\"\n\t$(GOLANGCI_LINT) fmt $(GOLANGCI_FMT_OPTS)\nendif\n\n.PHONY: common-vet\ncommon-vet:\n\t@echo \">> vetting code\"\n\t$(GO) vet $(GOOPTS) $(pkgs)\n\n.PHONY: common-lint\ncommon-lint: $(GOLANGCI_LINT)\nifdef GOLANGCI_LINT\n\t@echo \">> running golangci-lint\"\n\t$(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs)\nendif\n\n.PHONY: common-lint-fix\ncommon-lint-fix: $(GOLANGCI_LINT)\nifdef GOLANGCI_LINT\n\t@echo \">> running golangci-lint fix\"\n\t$(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT_OPTS) $(pkgs)\nendif\n\n.PHONY: common-yamllint\ncommon-yamllint:\n\t@echo \">> running yamllint on all YAML files in the repository\"\nifeq (, $(shell command -v yamllint 2> /dev/null))\n\t@echo \"yamllint not installed so skipping\"\nelse\n\tyamllint .\nendif\n\n# For backward-compatibility.\n.PHONY: common-staticcheck\ncommon-staticcheck: lint\n\n.PHONY: common-unused\ncommon-unused:\n\t@echo \">> running check for unused/missing packages in go.mod\"\n\t$(GO) mod tidy\n\t@git diff --exit-code -- go.sum go.mod\n\n.PHONY: common-build\ncommon-build: promu\n\t@echo \">> building binaries\"\n\t$(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES)\n\n.PHONY: common-tarball\ncommon-tarball: promu\n\t@echo \">> building release tarball\"\n\t$(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR)\n\n.PHONY: common-docker-repo-name\ncommon-docker-repo-name:\n\t@echo \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)\"\n\n.PHONY: common-docker $(BUILD_DOCKER_ARCHS)\ncommon-docker: $(BUILD_DOCKER_ARCHS)\n$(BUILD_DOCKER_ARCHS): common-docker-%:\n\t@for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \\\n\t\tdockerfile=$${variant#*:}; \\\n\t\tvariant_name=$${variant%%:*}; \\\n\t\tif $(call dockerfile_arch_is_excluded,$*); then \\\n\t\t\techo \"Skipping $$variant_name variant for linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)\"; \\\n\t\t\tcontinue; \\\n\t\tfi; \\\n\t\tdistroless_arch=\"$*\"; \\\n\t\tif [ \"$*\" = \"armv7\" ]; then \\\n\t\t\tdistroless_arch=\"arm\"; \\\n\t\tfi; \\\n\t\tif [ \"$$dockerfile\" = \"Dockerfile\" ]; then \\\n\t\t\techo \"Building default variant ($$variant_name) for linux-$* using $$dockerfile\"; \\\n\t\t\tdocker build -t \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)\" \\\n\t\t\t\t-f $$dockerfile \\\n\t\t\t\t--build-arg ARCH=\"$*\" \\\n\t\t\t\t--build-arg OS=\"linux\" \\\n\t\t\t\t--build-arg DISTROLESS_ARCH=\"$$distroless_arch\" \\\n\t\t\t\t$(DOCKERBUILD_CONTEXT); \\\n\t\t\tif [ \"$$variant_name\" != \"default\" ]; then \\\n\t\t\t\techo \"Tagging default variant with $$variant_name suffix\"; \\\n\t\t\t\tdocker tag \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)\" \\\n\t\t\t\t\t\"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name\"; \\\n\t\t\tfi; \\\n\t\telse \\\n\t\t\techo \"Building $$variant_name variant for linux-$* using $$dockerfile\"; \\\n\t\t\tdocker build -t \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name\" \\\n\t\t\t\t-f $$dockerfile \\\n\t\t\t\t--build-arg ARCH=\"$*\" \\\n\t\t\t\t--build-arg OS=\"linux\" \\\n\t\t\t\t--build-arg DISTROLESS_ARCH=\"$$distroless_arch\" \\\n\t\t\t\t$(DOCKERBUILD_CONTEXT); \\\n\t\tfi; \\\n\tdone\n\n.PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS)\ncommon-docker-publish: $(PUBLISH_DOCKER_ARCHS)\n$(PUBLISH_DOCKER_ARCHS): common-docker-publish-%:\n\t@for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \\\n\t\tdockerfile=$${variant#*:}; \\\n\t\tvariant_name=$${variant%%:*}; \\\n\t\tif $(call dockerfile_arch_is_excluded,$*); then \\\n\t\t\techo \"Skipping push for $$variant_name variant on linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)\"; \\\n\t\t\tcontinue; \\\n\t\tfi; \\\n\t\tif $(call registry_arch_is_excluded,$*); then \\\n\t\t\techo \"Skipping push for $$variant_name variant on linux-$* to $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)\"; \\\n\t\t\tcontinue; \\\n\t\tfi; \\\n\t\tif [ \"$$dockerfile\" != \"Dockerfile\" ] || [ \"$$variant_name\" != \"default\" ]; then \\\n\t\t\techo \"Pushing $$variant_name variant for linux-$*\"; \\\n\t\t\tdocker push \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name\"; \\\n\t\tfi; \\\n\t\tif [ \"$$dockerfile\" = \"Dockerfile\" ]; then \\\n\t\t\techo \"Pushing default variant ($$variant_name) for linux-$*\"; \\\n\t\t\tdocker push \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)\"; \\\n\t\tfi; \\\n\t\tif [ \"$(DOCKER_IMAGE_TAG)\" = \"latest\" ]; then \\\n\t\t\tif [ \"$$dockerfile\" != \"Dockerfile\" ] || [ \"$$variant_name\" != \"default\" ]; then \\\n\t\t\t\techo \"Pushing $$variant_name variant version tags for linux-$*\"; \\\n\t\t\t\tdocker push \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name\"; \\\n\t\t\tfi; \\\n\t\t\tif [ \"$$dockerfile\" = \"Dockerfile\" ]; then \\\n\t\t\t\techo \"Pushing default variant version tag for linux-$*\"; \\\n\t\t\t\tdocker push \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)\"; \\\n\t\t\tfi; \\\n\t\tfi; \\\n\tdone\n\nDOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION)))\n.PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS)\ncommon-docker-tag-latest: $(TAG_DOCKER_ARCHS)\n$(TAG_DOCKER_ARCHS): common-docker-tag-latest-%:\n\t@for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \\\n\t\tdockerfile=$${variant#*:}; \\\n\t\tvariant_name=$${variant%%:*}; \\\n\t\tif $(call dockerfile_arch_is_excluded,$*); then \\\n\t\t\techo \"Skipping tag for $$variant_name variant on linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)\"; \\\n\t\t\tcontinue; \\\n\t\tfi; \\\n\t\tif $(call registry_arch_is_excluded,$*); then \\\n\t\t\techo \"Skipping tag for $$variant_name variant on linux-$* for $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)\"; \\\n\t\t\tcontinue; \\\n\t\tfi; \\\n\t\tif [ \"$$dockerfile\" != \"Dockerfile\" ] || [ \"$$variant_name\" != \"default\" ]; then \\\n\t\t\techo \"Tagging $$variant_name variant for linux-$* as latest\"; \\\n\t\t\tdocker tag \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name\" \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest-$$variant_name\"; \\\n\t\t\tdocker tag \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name\" \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name\"; \\\n\t\tfi; \\\n\t\tif [ \"$$dockerfile\" = \"Dockerfile\" ]; then \\\n\t\t\techo \"Tagging default variant ($$variant_name) for linux-$* as latest\"; \\\n\t\t\tdocker tag \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)\" \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest\"; \\\n\t\t\tdocker tag \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)\" \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)\"; \\\n\t\tfi; \\\n\tdone\n\n.PHONY: common-docker-manifest\ncommon-docker-manifest:\n\t@for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \\\n\t\tdockerfile=$${variant#*:}; \\\n\t\tvariant_name=$${variant%%:*}; \\\n\t\tif [ \"$$dockerfile\" != \"Dockerfile\" ] || [ \"$$variant_name\" != \"default\" ]; then \\\n\t\t\techo \"Creating manifest for $$variant_name variant\"; \\\n\t\t\trefs=\"\"; \\\n\t\t\tfor arch in $(DOCKER_ARCHS); do \\\n\t\t\t\tif $(call dockerfile_arch_is_excluded,$$arch); then \\\n\t\t\t\t\techo \"  Skipping $$arch for $$variant_name (excluded by DOCKERFILE_ARCH_EXCLUSIONS)\"; \\\n\t\t\t\t\tcontinue; \\\n\t\t\t\tfi; \\\n\t\t\t\tif $(call registry_arch_is_excluded,$$arch); then \\\n\t\t\t\t\techo \"  Skipping $$arch for $$variant_name on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)\"; \\\n\t\t\t\t\tcontinue; \\\n\t\t\t\tfi; \\\n\t\t\t\trefs=\"$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name\"; \\\n\t\t\tdone; \\\n\t\t\tif [ -z \"$$refs\" ]; then \\\n\t\t\t\techo \"Skipping manifest for $$variant_name variant (no supported architectures)\"; \\\n\t\t\t\tcontinue; \\\n\t\t\tfi; \\\n\t\t\tDOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name\" $$refs; \\\n\t\t\tDOCKER_CLI_EXPERIMENTAL=enabled docker manifest push \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name\"; \\\n\t\tfi; \\\n\t\tif [ \"$$dockerfile\" = \"Dockerfile\" ]; then \\\n\t\t\techo \"Creating default variant ($$variant_name) manifest\"; \\\n\t\t\trefs=\"\"; \\\n\t\t\tfor arch in $(DOCKER_ARCHS); do \\\n\t\t\t\tif $(call dockerfile_arch_is_excluded,$$arch); then \\\n\t\t\t\t\techo \"  Skipping $$arch for default variant (excluded by DOCKERFILE_ARCH_EXCLUSIONS)\"; \\\n\t\t\t\t\tcontinue; \\\n\t\t\t\tfi; \\\n\t\t\t\tif $(call registry_arch_is_excluded,$$arch); then \\\n\t\t\t\t\techo \"  Skipping $$arch for default variant on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)\"; \\\n\t\t\t\t\tcontinue; \\\n\t\t\t\tfi; \\\n\t\t\t\trefs=\"$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)\"; \\\n\t\t\tdone; \\\n\t\t\tif [ -z \"$$refs\" ]; then \\\n\t\t\t\techo \"Skipping default variant manifest (no supported architectures)\"; \\\n\t\t\t\tcontinue; \\\n\t\t\tfi; \\\n\t\t\tDOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)\" $$refs; \\\n\t\t\tDOCKER_CLI_EXPERIMENTAL=enabled docker manifest push \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)\"; \\\n\t\tfi; \\\n\t\tif [ \"$(DOCKER_IMAGE_TAG)\" = \"latest\" ]; then \\\n\t\t\tif [ \"$$dockerfile\" != \"Dockerfile\" ] || [ \"$$variant_name\" != \"default\" ]; then \\\n\t\t\t\techo \"Creating manifest for $$variant_name variant version tag\"; \\\n\t\t\t\trefs=\"\"; \\\n\t\t\t\tfor arch in $(DOCKER_ARCHS); do \\\n\t\t\t\t\tif $(call dockerfile_arch_is_excluded,$$arch); then \\\n\t\t\t\t\t\techo \"  Skipping $$arch for $$variant_name version tag (excluded by DOCKERFILE_ARCH_EXCLUSIONS)\"; \\\n\t\t\t\t\t\tcontinue; \\\n\t\t\t\t\tfi; \\\n\t\t\t\t\tif $(call registry_arch_is_excluded,$$arch); then \\\n\t\t\t\t\t\techo \"  Skipping $$arch for $$variant_name version tag on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)\"; \\\n\t\t\t\t\t\tcontinue; \\\n\t\t\t\t\tfi; \\\n\t\t\t\t\trefs=\"$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name\"; \\\n\t\t\t\tdone; \\\n\t\t\t\tif [ -z \"$$refs\" ]; then \\\n\t\t\t\t\techo \"Skipping version-tag manifest for $$variant_name variant (no supported architectures)\"; \\\n\t\t\t\t\tcontinue; \\\n\t\t\t\tfi; \\\n\t\t\t\tDOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name\" $$refs; \\\n\t\t\t\tDOCKER_CLI_EXPERIMENTAL=enabled docker manifest push \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name\"; \\\n\t\t\tfi; \\\n\t\t\tif [ \"$$dockerfile\" = \"Dockerfile\" ]; then \\\n\t\t\t\techo \"Creating default variant version tag manifest\"; \\\n\t\t\t\trefs=\"\"; \\\n\t\t\t\tfor arch in $(DOCKER_ARCHS); do \\\n\t\t\t\t\tif $(call dockerfile_arch_is_excluded,$$arch); then \\\n\t\t\t\t\t\techo \"  Skipping $$arch for default variant version tag (excluded by DOCKERFILE_ARCH_EXCLUSIONS)\"; \\\n\t\t\t\t\t\tcontinue; \\\n\t\t\t\t\tfi; \\\n\t\t\t\t\tif $(call registry_arch_is_excluded,$$arch); then \\\n\t\t\t\t\t\techo \"  Skipping $$arch for default variant version tag on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)\"; \\\n\t\t\t\t\t\tcontinue; \\\n\t\t\t\t\tfi; \\\n\t\t\t\t\trefs=\"$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)\"; \\\n\t\t\t\tdone; \\\n\t\t\t\tif [ -z \"$$refs\" ]; then \\\n\t\t\t\t\techo \"Skipping default variant version-tag manifest (no supported architectures)\"; \\\n\t\t\t\t\tcontinue; \\\n\t\t\t\tfi; \\\n\t\t\t\tDOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)\" $$refs; \\\n\t\t\t\tDOCKER_CLI_EXPERIMENTAL=enabled docker manifest push \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)\"; \\\n\t\t\tfi; \\\n\t\tfi; \\\n\tdone\n\n.PHONY: promu\npromu: $(PROMU)\n\n$(PROMU):\n\t$(eval PROMU_TMP := $(shell mktemp -d))\n\tcurl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP)\n\tmkdir -p $(FIRST_GOPATH)/bin\n\tcp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu\n\trm -r $(PROMU_TMP)\n\n.PHONY: common-proto\ncommon-proto:\n\t@echo \">> generating code from proto files\"\n\t@./scripts/genproto.sh\n\nifdef GOLANGCI_LINT\n$(GOLANGCI_LINT):\n\tmkdir -p $(FIRST_GOPATH)/bin\n\tcurl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \\\n\t\t| sed -e '/install -d/d' \\\n\t\t| sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION)\nendif\n\n.PHONY: common-print-golangci-lint-version\ncommon-print-golangci-lint-version:\n\t@echo $(GOLANGCI_LINT_VERSION)\n\n.PHONY: precheck\nprecheck::\n\ndefine PRECHECK_COMMAND_template =\nprecheck:: $(1)_precheck\n\nPRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1)))\n.PHONY: $(1)_precheck\n$(1)_precheck:\n\t@if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \\\n\t\techo \"Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?\"; \\\n\t\texit 1; \\\n\tfi\nendef\n\ngovulncheck: install-govulncheck\n\tgovulncheck ./...\n\ninstall-govulncheck:\n\tcommand -v govulncheck > /dev/null || go install golang.org/x/vuln/cmd/govulncheck@latest\n"
  },
  {
    "path": "NOTICE",
    "content": "Prometheus Alertmanager\nCopyright 2013-2015 The Prometheus Authors\n\nThis product includes software developed at\nSoundCloud Ltd. (http://soundcloud.com/).\n\n\nThe following components are included in this product:\n\nBootstrap\nhttp://getbootstrap.com\nCopyright 2011-2014 Twitter, Inc.\nLicensed under the MIT License\n"
  },
  {
    "path": "Procfile",
    "content": "a1: ./alertmanager --log.level=debug --storage.path=$TMPDIR/a1 --web.listen-address=:9093  --cluster.listen-address=127.0.0.1:8001 --config.file=examples/ha/alertmanager.yml\na2: ./alertmanager --log.level=debug --storage.path=$TMPDIR/a2 --web.listen-address=:9094  --cluster.listen-address=127.0.0.1:8002 --cluster.peer=127.0.0.1:8001 --config.file=examples/ha/alertmanager.yml\na3: ./alertmanager --log.level=debug --storage.path=$TMPDIR/a3 --web.listen-address=:9095  --cluster.listen-address=127.0.0.1:8003 --cluster.peer=127.0.0.1:8001 --config.file=examples/ha/alertmanager.yml\nwh: go run ./examples/webhook/echo.go\n\n"
  },
  {
    "path": "README.md",
    "content": "# Alertmanager [![CircleCI](https://circleci.com/gh/prometheus/alertmanager/tree/main.svg?style=shield)][circleci]\n\n[![Docker Repository on Quay](https://quay.io/repository/prometheus/alertmanager/status \"Docker Repository on Quay\")][quay]\n[![Docker Pulls](https://img.shields.io/docker/pulls/prom/alertmanager.svg?maxAge=604800)][hub]\n\nThe Alertmanager handles alerts sent by client applications such as the Prometheus server. It takes care of deduplicating, grouping, and routing them to the correct [receiver integrations](https://prometheus.io/docs/alerting/latest/configuration/#receiver) such as email, PagerDuty, OpsGenie, or many other [mechanisms](https://prometheus.io/docs/operating/integrations/#alertmanager-webhook-receiver) thanks to the webhook receiver. It also takes care of silencing and inhibition of alerts.\n\n* [Documentation](http://prometheus.io/docs/alerting/alertmanager/)\n\n## Install\n\nThere are various ways of installing Alertmanager.\n\n### Precompiled binaries\n\nPrecompiled binaries for released versions are available in the\n[*download* section](https://prometheus.io/download/)\non [prometheus.io](https://prometheus.io). Using the latest production release binary\nis the recommended way of installing Alertmanager.\n\n### Docker images\n\nDocker images are available on [Quay.io](https://quay.io/repository/prometheus/alertmanager) or [Docker Hub](https://hub.docker.com/r/prom/alertmanager/).\n\nYou can launch an Alertmanager container for trying it out with\n\n    $ docker run --name alertmanager -d -p 127.0.0.1:9093:9093 quay.io/prometheus/alertmanager\n\nAlertmanager will now be reachable at http://localhost:9093/.\n\n### Compiling the binary\n\nYou can either `go install` it:\n\n```\n$ go install github.com/prometheus/alertmanager/cmd/...@latest\n# cd $GOPATH/src/github.com/prometheus/alertmanager\n$ alertmanager --config.file=<your_file>\n```\n\nOr clone the repository and build manually:\n\n```\n$ mkdir -p $GOPATH/src/github.com/prometheus\n$ cd $GOPATH/src/github.com/prometheus\n$ git clone https://github.com/prometheus/alertmanager.git\n$ cd alertmanager\n$ make build\n$ ./alertmanager --config.file=<your_file>\n```\n\nYou can also build just one of the binaries in this repo by passing a name to the build function:\n```\n$ make build BINARIES=amtool\n```\n\n## Example\n\nThis is an example configuration that should cover most relevant aspects of the new YAML configuration format. The full documentation of the configuration can be found [here](https://prometheus.io/docs/alerting/configuration/).\n\n```yaml\nglobal:\n  # The smarthost and SMTP sender used for mail notifications.\n  smtp_smarthost: 'localhost:25'\n  smtp_from: 'alertmanager@example.org'\n\n# The root route on which each incoming alert enters.\nroute:\n  # The root route must not have any matchers as it is the entry point for\n  # all alerts. It needs to have a receiver configured so alerts that do not\n  # match any of the sub-routes are sent to someone.\n  receiver: 'team-X-mails'\n\n  # The labels by which incoming alerts are grouped together. For example,\n  # multiple alerts coming in for cluster=A and alertname=LatencyHigh would\n  # be batched into a single group.\n  #\n  # To aggregate by all possible labels use '...' as the sole label name.\n  # This effectively disables aggregation entirely, passing through all\n  # alerts as-is. This is unlikely to be what you want, unless you have\n  # a very low alert volume or your upstream notification system performs\n  # its own grouping. Example: group_by: [...]\n  group_by: ['alertname', 'cluster']\n\n  # When a new group of alerts is created by an incoming alert, wait at\n  # least 'group_wait' to send the initial notification.\n  # This way ensures that you get multiple alerts for the same group that start\n  # firing shortly after another are batched together on the first\n  # notification.\n  group_wait: 30s\n\n  # When the first notification was sent, wait 'group_interval' to send a batch\n  # of new alerts that started firing for that group.\n  group_interval: 5m\n\n  # If an alert has successfully been sent, wait 'repeat_interval' to\n  # resend them.\n  repeat_interval: 3h\n\n  # All the above attributes are inherited by all child routes and can\n  # overwritten on each.\n\n  # The child route trees.\n  routes:\n  # This route performs a regular expression match on alert labels to\n  # catch alerts that are related to a list of services.\n  - matchers:\n    - service=~\"^(foo1|foo2|baz)$\"\n    receiver: team-X-mails\n\n    # The service has a sub-route for critical alerts, any alerts\n    # that do not match, i.e. severity != critical, fall-back to the\n    # parent node and are sent to 'team-X-mails'\n    routes:\n    - matchers:\n      - severity=\"critical\"\n      receiver: team-X-pager\n\n  - matchers:\n    - service=\"files\"\n    receiver: team-Y-mails\n\n    routes:\n    - matchers:\n      - severity=\"critical\"\n      receiver: team-Y-pager\n\n  # This route handles all alerts coming from a database service. If there's\n  # no team to handle it, it defaults to the DB team.\n  - matchers:\n    - service=\"database\"\n\n    receiver: team-DB-pager\n    # Also group alerts by affected database.\n    group_by: [alertname, cluster, database]\n\n    routes:\n    - matchers:\n      - owner=\"team-X\"\n      receiver: team-X-pager\n\n    - matchers:\n      - owner=\"team-Y\"\n      receiver: team-Y-pager\n\n\n# Inhibition rules allow to mute a set of alerts given that another alert is\n# firing.\n# We use this to mute any warning-level notifications if the same alert is\n# already critical.\ninhibit_rules:\n- source_matchers:\n    - severity=\"critical\"\n  target_matchers:\n    - severity=\"warning\"\n  # Apply inhibition if the alertname is the same.\n  # CAUTION: \n  #   If all label names listed in `equal` are missing \n  #   from both the source and target alerts,\n  #   the inhibition rule will apply!\n  equal: ['alertname']\n\n\nreceivers:\n- name: 'team-X-mails'\n  email_configs:\n  - to: 'team-X+alerts@example.org, team-Y+alerts@example.org'\n\n- name: 'team-X-pager'\n  email_configs:\n  - to: 'team-X+alerts-critical@example.org'\n  pagerduty_configs:\n  - routing_key: <team-X-key>\n\n- name: 'team-Y-mails'\n  email_configs:\n  - to: 'team-Y+alerts@example.org'\n\n- name: 'team-Y-pager'\n  pagerduty_configs:\n  - routing_key: <team-Y-key>\n\n- name: 'team-DB-pager'\n  pagerduty_configs:\n  - routing_key: <team-DB-key>\n```\n\n## API\n\nThe current Alertmanager API is version 2. This API is fully generated via the\n[OpenAPI project](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md)\nand [Go Swagger](https://github.com/go-swagger/go-swagger/) with the exception\nof the HTTP handlers themselves. The API specification can be found in\n[api/v2/openapi.yaml](api/v2/openapi.yaml). A HTML rendered version can be\naccessed [here](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/prometheus/alertmanager/main/api/v2/openapi.yaml).\nClients can be easily generated via any OpenAPI generator for all major languages.\n\nAPIv2 is accessed via the `/api/v2` prefix. APIv1 was deprecated in `0.16.0` and is removed as of version `0.27.0`.\nThe v2 `/status` endpoint would be `/api/v2/status`. If `--web.route-prefix` is set then API routes are\nprefixed with that as well, so `--web.route-prefix=/alertmanager/` would\nrelate to `/alertmanager/api/v2/status`.\n\n## amtool\n\n`amtool` is a cli tool for interacting with the Alertmanager API. It is bundled with all releases of Alertmanager.\n\n### Install\n\nAlternatively you can install with:\n```\n$ go install github.com/prometheus/alertmanager/cmd/amtool@latest\n```\n\n### Examples\n\nView all currently firing alerts:\n```\n$ amtool alert\nAlertname        Starts At                Summary\nTest_Alert       2017-08-02 18:30:18 UTC  This is a testing alert!\nTest_Alert       2017-08-02 18:30:18 UTC  This is a testing alert!\nCheck_Foo_Fails  2017-08-02 18:30:18 UTC  This is a testing alert!\nCheck_Foo_Fails  2017-08-02 18:30:18 UTC  This is a testing alert!\n```\n\nView all currently firing alerts with extended output:\n```\n$ amtool -o extended alert\nLabels                                        Annotations                                                    Starts At                Ends At                  Generator URL\nalertname=\"Test_Alert\" instance=\"node0\"       link=\"https://example.com\" summary=\"This is a testing alert!\"  2017-08-02 18:31:24 UTC  0001-01-01 00:00:00 UTC  http://my.testing.script.local\nalertname=\"Test_Alert\" instance=\"node1\"       link=\"https://example.com\" summary=\"This is a testing alert!\"  2017-08-02 18:31:24 UTC  0001-01-01 00:00:00 UTC  http://my.testing.script.local\nalertname=\"Check_Foo_Fails\" instance=\"node0\"  link=\"https://example.com\" summary=\"This is a testing alert!\"  2017-08-02 18:31:24 UTC  0001-01-01 00:00:00 UTC  http://my.testing.script.local\nalertname=\"Check_Foo_Fails\" instance=\"node1\"  link=\"https://example.com\" summary=\"This is a testing alert!\"  2017-08-02 18:31:24 UTC  0001-01-01 00:00:00 UTC  http://my.testing.script.local\n```\n\nIn addition to viewing alerts, you can use the rich query syntax provided by Alertmanager:\n```\n$ amtool -o extended alert query alertname=\"Test_Alert\"\nLabels                                   Annotations                                                    Starts At                Ends At                  Generator URL\nalertname=\"Test_Alert\" instance=\"node0\"  link=\"https://example.com\" summary=\"This is a testing alert!\"  2017-08-02 18:31:24 UTC  0001-01-01 00:00:00 UTC  http://my.testing.script.local\nalertname=\"Test_Alert\" instance=\"node1\"  link=\"https://example.com\" summary=\"This is a testing alert!\"  2017-08-02 18:31:24 UTC  0001-01-01 00:00:00 UTC  http://my.testing.script.local\n\n$ amtool -o extended alert query instance=~\".+1\"\nLabels                                        Annotations                                                    Starts At                Ends At                  Generator URL\nalertname=\"Test_Alert\" instance=\"node1\"       link=\"https://example.com\" summary=\"This is a testing alert!\"  2017-08-02 18:31:24 UTC  0001-01-01 00:00:00 UTC  http://my.testing.script.local\nalertname=\"Check_Foo_Fails\" instance=\"node1\"  link=\"https://example.com\" summary=\"This is a testing alert!\"  2017-08-02 18:31:24 UTC  0001-01-01 00:00:00 UTC  http://my.testing.script.local\n\n$ amtool -o extended alert query alertname=~\"Test.*\" instance=~\".+1\"\nLabels                                   Annotations                                                    Starts At                Ends At                  Generator URL\nalertname=\"Test_Alert\" instance=\"node1\"  link=\"https://example.com\" summary=\"This is a testing alert!\"  2017-08-02 18:31:24 UTC  0001-01-01 00:00:00 UTC  http://my.testing.script.local\n```\n\nSilence an alert:\n```\n$ amtool silence add alertname=Test_Alert\nb3ede22e-ca14-4aa0-932c-ca2f3445f926\n\n$ amtool silence add alertname=\"Test_Alert\" instance=~\".+0\"\ne48cb58a-0b17-49ba-b734-3585139b1d25\n```\n\nView silences:\n```\n$ amtool silence query\nID                                    Matchers              Ends At                  Created By  Comment\nb3ede22e-ca14-4aa0-932c-ca2f3445f926  alertname=Test_Alert  2017-08-02 19:54:50 UTC  kellel\n\n$ amtool silence query instance=~\".+0\"\nID                                    Matchers                            Ends At                  Created By  Comment\ne48cb58a-0b17-49ba-b734-3585139b1d25  alertname=Test_Alert instance=~.+0  2017-08-02 22:41:39 UTC  kellel\n```\n\nExpire a silence:\n```\n$ amtool silence expire b3ede22e-ca14-4aa0-932c-ca2f3445f926\n```\n\nExpire all silences matching a query:\n```\n$ amtool silence query instance=~\".+0\"\nID                                    Matchers                            Ends At                  Created By  Comment\ne48cb58a-0b17-49ba-b734-3585139b1d25  alertname=Test_Alert instance=~.+0  2017-08-02 22:41:39 UTC  kellel\n\n$ amtool silence expire $(amtool silence query -q instance=~\".+0\")\n\n$ amtool silence query instance=~\".+0\"\n\n```\n\nExpire all silences:\n```\n$ amtool silence expire $(amtool silence query -q)\n```\n\nTry out how a template works. Let's say you have this in your configuration file:\n```\ntemplates:\n  - '/foo/bar/*.tmpl'\n```\n\nThen you can test out how a template would look like with example by using this command:\n```\namtool template render --template.glob='/foo/bar/*.tmpl' --template.text='{{ template \"slack.default.markdown.v1\" . }}'\n```\n\n### Configuration\n\n`amtool` allows a configuration file to specify some options for convenience. The default configuration file paths are `$HOME/.config/amtool/config.yml` or `/etc/amtool/config.yml`\n\nAn example configuration file might look like the following:\n\n```\n# Define the path that `amtool` can find your `alertmanager` instance\nalertmanager.url: \"http://localhost:9093\"\n\n# Override the default author. (unset defaults to your username)\nauthor: me@example.com\n\n# Force amtool to give you an error if you don't include a comment on a silence\ncomment_required: true\n\n# Set a default output format. (unset defaults to simple)\noutput: extended\n\n# Set a default receiver\nreceiver: team-X-pager\n```\n\n### Routes\n\n`amtool` allows you to visualize the routes of your configuration in form of text tree view.\nAlso you can use it to test the routing by passing it label set of an alert\nand it prints out all receivers the alert would match ordered and separated by `,`.\n(If you use `--verify.receivers` amtool returns error code 1 on mismatch)\n\nExample of usage:\n```\n# View routing tree of remote Alertmanager\n$ amtool config routes --alertmanager.url=http://localhost:9090\n\n# Test if alert matches expected receiver\n$ amtool config routes test --config.file=doc/examples/simple.yml --tree --verify.receivers=team-X-pager service=database owner=team-X\n```\n\n## High Availability\n\nAlertmanager's high availability is in production use at many companies and is enabled by default.\n\n> Important: Both UDP and TCP are needed in alertmanager 0.15 and higher for the cluster to work.\n>  - If you are using a firewall, make sure to whitelist the clustering port for both protocols.\n>  - If you are running in a container, make sure to expose the clustering port for both protocols.\n\nTo create a highly available cluster of the Alertmanager the instances need to\nbe configured to communicate with each other. This is configured using the\n`--cluster.*` flags.\n\n- `--cluster.listen-address` string: cluster listen address (default \"0.0.0.0:9094\"; empty string disables HA mode)\n- `--cluster.advertise-address` string: cluster advertise address\n- `--cluster.peer` value: initial peers (repeat flag for each additional peer)\n- `--cluster.peer-timeout` value: peer timeout period (default \"15s\")\n- `--cluster.peers-resolve-timeout` value: peers resolve timeout period (default \"15s\")\n- `--cluster.gossip-interval` value: cluster message propagation speed\n  (default \"200ms\")\n- `--cluster.pushpull-interval` value: lower values will increase\n  convergence speeds at expense of bandwidth (default \"1m0s\")\n- `--cluster.settle-timeout` value: maximum time to wait for cluster\n  connections to settle before evaluating notifications.\n- `--cluster.tcp-timeout` value: timeout value for tcp connections, reads and writes (default \"10s\")\n- `--cluster.probe-timeout` value: time to wait for ack before marking node unhealthy\n  (default \"500ms\")\n- `--cluster.probe-interval` value: interval between random node probes (default \"1s\")\n- `--cluster.reconnect-interval` value: interval between attempting to reconnect to lost peers (default \"10s\")\n- `--cluster.reconnect-timeout` value: length of time to attempt to reconnect to a lost peer (default: \"6h0m0s\")\n- `--cluster.label` value: the label is an optional string to include on each packet and stream. It uniquely identifies the cluster and prevents cross-communication issues when sending gossip messages (default:\"\")\n\nThe chosen port in the `cluster.listen-address` flag is the port that needs to be\nspecified in the `cluster.peer` flag of the other peers.\n\nThe `cluster.advertise-address` flag is required if the instance doesn't have\nan IP address that is part of [RFC 6890](https://tools.ietf.org/html/rfc6890)\nwith a default route.\n\nTo start a cluster of three peers on your local machine use [`goreman`](https://github.com/mattn/goreman) and the\nProcfile within this repository.\n\n\tgoreman start\n\nTo point your Prometheus 1.4, or later, instance to multiple Alertmanagers, configure them\nin your `prometheus.yml` configuration file, for example:\n\n```yaml\nalerting:\n  alertmanagers:\n  - static_configs:\n    - targets:\n      - alertmanager1:9093\n      - alertmanager2:9093\n      - alertmanager3:9093\n```\n\n> Important: Do not load balance traffic between Prometheus and its Alertmanagers, but instead point Prometheus to a list of all Alertmanagers. The Alertmanager implementation expects all alerts to be sent to all Alertmanagers to ensure high availability.\n\n### Turn off high availability\n\nIf running Alertmanager in high availability mode is not desired, setting `--cluster.listen-address=` prevents Alertmanager from listening to incoming peer requests.\n\n## Contributing\n\nCheck the [Prometheus contributing page](https://github.com/prometheus/prometheus/blob/main/CONTRIBUTING.md).\n\nTo contribute to the user interface, refer to [ui/app/CONTRIBUTING.md](ui/app/CONTRIBUTING.md).\n\n## Architecture\n\n![](doc/arch.svg)\n\n## License\n\nApache License 2.0, see [LICENSE](https://github.com/prometheus/alertmanager/blob/main/LICENSE).\n\n[hub]: https://hub.docker.com/r/prom/alertmanager/\n[circleci]: https://circleci.com/gh/prometheus/alertmanager\n[quay]: https://quay.io/repository/prometheus/alertmanager\n"
  },
  {
    "path": "RELEASE.md",
    "content": "# Releases\nThis page describes the release process and the currently planned schedule for upcoming releases as well as the respective release shepherd. Release shepherds are chosen on a voluntary basis.\n\n## Release Schedule\n\nRelease cadence of first pre-releases being cut is 12 weeks.\n\n| release series | date (year-month-day) | release shepherd                          |\n|----------------|-----------------------|-------------------------------------------|\n| v0.26          | 2023-08-23            | Josh Abreu (Github: @gotjosh)             |\n| v0.27          | 2024-02-28            | Josh Abreu (Github: @gotjosh)             |\n| v0.28          | 2024-05-28            | Josh Abreu (Github: @gotjosh)             |\n| v0.29          | 2025-11-01            | Joe Adams (Github: @sysadmind)            |\n| v0.30          | 2025-12-12            | Solomon Jacobs (Github: @SoloJacobs)      |\n| v0.31          | 2026-01-31            | Solomon Jacobs (Github: @SoloJacobs)      |\n| v0.32          | 2026-04-06            | Anand Rajagopal (Github: @rajagopalanand) |\n| v0.33          | 2026-06-06            | **volunteer welcome**                     |\n\nIf you are interested in volunteering please create a pull request against the [prometheus/alertmanager](https://github.com/prometheus/alertmanager) repository and propose yourself for the release of your choice.\n\nIf you'd like to know more about the shepherd responsibilities or the release instructions please [refer to the `RELEASE.MD`](https://github.com/prometheus/prometheus/blob/main/RELEASE.md) in [prometheus/prometheus](https://github.com/prometheus/prometheus).\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Reporting a security issue\n\nThe Prometheus security policy, including how to report vulnerabilities, can be\nfound here:\n\n<https://prometheus.io/docs/operating/security/>\n"
  },
  {
    "path": "VERSION",
    "content": "0.31.1\n"
  },
  {
    "path": "alert/alert.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage alert\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/prometheus/common/model\"\n)\n\n// Alert wraps a model.Alert with additional information relevant\n// to internal of the Alertmanager.\n// The type is never exposed to external communication and the\n// embedded alert has to be sanitized beforehand.\ntype Alert struct {\n\tmodel.Alert\n\n\t// The authoritative timestamp.\n\tUpdatedAt time.Time\n\tTimeout   bool\n}\n\n// Merge merges the timespan of two alerts based and overwrites annotations\n// based on the authoritative timestamp.  A new alert is returned, the labels\n// are assumed to be equal.\nfunc (a *Alert) Merge(o *Alert) *Alert {\n\t// Let o always be the younger alert.\n\tif o.UpdatedAt.Before(a.UpdatedAt) {\n\t\treturn o.Merge(a)\n\t}\n\n\tres := *o\n\n\t// Always pick the earliest starting time.\n\tif a.StartsAt.Before(o.StartsAt) {\n\t\tres.StartsAt = a.StartsAt\n\t}\n\n\tif o.Resolved() {\n\t\t// The latest explicit resolved timestamp wins if both alerts are effectively resolved.\n\t\tif a.Resolved() && a.EndsAt.After(o.EndsAt) {\n\t\t\tres.EndsAt = a.EndsAt\n\t\t}\n\t} else {\n\t\t// A non-timeout timestamp always rules if it is the latest.\n\t\tif a.EndsAt.After(o.EndsAt) && !a.Timeout {\n\t\t\tres.EndsAt = a.EndsAt\n\t\t}\n\t}\n\n\treturn &res\n}\n\n// Validate overrides the same method in model.Alert to allow UTF-8 labels.\n// This can be removed once prometheus/common has support for UTF-8.\nfunc (a *Alert) Validate() error {\n\tif a.StartsAt.IsZero() {\n\t\treturn fmt.Errorf(\"start time missing\")\n\t}\n\tif !a.EndsAt.IsZero() && a.EndsAt.Before(a.StartsAt) {\n\t\treturn fmt.Errorf(\"start time must be before end time\")\n\t}\n\tif len(a.Labels) == 0 {\n\t\treturn fmt.Errorf(\"at least one label pair required\")\n\t}\n\tif err := validateLs(a.Labels); err != nil {\n\t\treturn fmt.Errorf(\"invalid label set: %w\", err)\n\t}\n\tif err := validateLs(a.Annotations); err != nil {\n\t\treturn fmt.Errorf(\"invalid annotations: %w\", err)\n\t}\n\treturn nil\n}\n\n// AlertSlice is a sortable slice of Alerts.\ntype AlertSlice []*Alert\n\nfunc (as AlertSlice) Less(i, j int) bool {\n\t// Look at labels.job, then labels.instance.\n\tfor _, overrideKey := range [...]model.LabelName{\"job\", \"instance\"} {\n\t\tiVal, iOk := as[i].Labels[overrideKey]\n\t\tjVal, jOk := as[j].Labels[overrideKey]\n\t\tif !iOk && !jOk {\n\t\t\tcontinue\n\t\t}\n\t\tif !iOk {\n\t\t\treturn false\n\t\t}\n\t\tif !jOk {\n\t\t\treturn true\n\t\t}\n\t\tif iVal != jVal {\n\t\t\treturn iVal < jVal\n\t\t}\n\t}\n\treturn as[i].Labels.Before(as[j].Labels)\n}\nfunc (as AlertSlice) Swap(i, j int) { as[i], as[j] = as[j], as[i] }\nfunc (as AlertSlice) Len() int      { return len(as) }\n\n// Alerts turns a sequence of internal alerts into a list of\n// exposable model.Alert structures.\nfunc Alerts(alerts ...*Alert) model.Alerts {\n\tres := make(model.Alerts, 0, len(alerts))\n\tfor _, a := range alerts {\n\t\tv := a.Alert\n\t\t// If the end timestamp is not reached yet, do not expose it.\n\t\tif !a.Resolved() {\n\t\t\tv.EndsAt = time.Time{}\n\t\t}\n\t\tres = append(res, &v)\n\t}\n\treturn res\n}\n"
  },
  {
    "path": "alert/alert_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage alert\n\nimport (\n\t\"reflect\"\n\t\"sort\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/prometheus/common/model\"\n)\n\nfunc TestAlertMerge(t *testing.T) {\n\tnow := time.Now()\n\n\t// By convention, alert A is always older than alert B.\n\tpairs := []struct {\n\t\tA, B, Res *Alert\n\t}{\n\t\t{\n\t\t\t// Both alerts have the Timeout flag set.\n\t\t\t// StartsAt is defined by Alert A.\n\t\t\t// EndsAt is defined by Alert B.\n\t\t\tA: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-2 * time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(2 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now,\n\t\t\t\tTimeout:   true,\n\t\t\t},\n\t\t\tB: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(3 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(time.Minute),\n\t\t\t\tTimeout:   true,\n\t\t\t},\n\t\t\tRes: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-2 * time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(3 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(time.Minute),\n\t\t\t\tTimeout:   true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// Alert A has the Timeout flag set while Alert B has it unset.\n\t\t\t// StartsAt is defined by Alert A.\n\t\t\t// EndsAt is defined by Alert B.\n\t\t\tA: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(3 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now,\n\t\t\t\tTimeout:   true,\n\t\t\t},\n\t\t\tB: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now,\n\t\t\t\t\tEndsAt:   now.Add(2 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(time.Minute),\n\t\t\t},\n\t\t\tRes: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(2 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(time.Minute),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// Alert A has the Timeout flag unset while Alert B has it set.\n\t\t\t// StartsAt is defined by Alert A.\n\t\t\t// EndsAt is defined by Alert A.\n\t\t\tA: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(3 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now,\n\t\t\t},\n\t\t\tB: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now,\n\t\t\t\t\tEndsAt:   now.Add(2 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(time.Minute),\n\t\t\t\tTimeout:   true,\n\t\t\t},\n\t\t\tRes: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(3 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(time.Minute),\n\t\t\t\tTimeout:   true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// Both alerts have the Timeout flag unset and are not resolved.\n\t\t\t// StartsAt is defined by Alert A.\n\t\t\t// EndsAt is defined by Alert A.\n\t\t\tA: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(3 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now,\n\t\t\t},\n\t\t\tB: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now,\n\t\t\t\t\tEndsAt:   now.Add(2 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(time.Minute),\n\t\t\t},\n\t\t\tRes: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(3 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(time.Minute),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// Both alerts have the Timeout flag unset and are not resolved.\n\t\t\t// StartsAt is defined by Alert A.\n\t\t\t// EndsAt is defined by Alert B.\n\t\t\tA: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(3 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now,\n\t\t\t},\n\t\t\tB: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(4 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(time.Minute),\n\t\t\t},\n\t\t\tRes: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(4 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(time.Minute),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// Both alerts have the Timeout flag unset, A is resolved while B isn't.\n\t\t\t// StartsAt is defined by Alert A.\n\t\t\t// EndsAt is defined by Alert B.\n\t\t\tA: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-3 * time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(-time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now,\n\t\t\t},\n\t\t\tB: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-2 * time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(time.Minute),\n\t\t\t},\n\t\t\tRes: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-3 * time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(time.Minute),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// Both alerts have the Timeout flag unset, B is resolved while A isn't.\n\t\t\t// StartsAt is defined by Alert A.\n\t\t\t// EndsAt is defined by Alert B.\n\t\t\tA: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-2 * time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(3 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now,\n\t\t\t},\n\t\t\tB: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-2 * time.Minute),\n\t\t\t\t\tEndsAt:   now,\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(time.Minute),\n\t\t\t},\n\t\t\tRes: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-2 * time.Minute),\n\t\t\t\t\tEndsAt:   now,\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(time.Minute),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// Both alerts are resolved (EndsAt < now).\n\t\t\t// StartsAt is defined by Alert B.\n\t\t\t// EndsAt is defined by Alert A.\n\t\t\tA: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-3 * time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(-time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(-time.Minute),\n\t\t\t},\n\t\t\tB: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-4 * time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(-2 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(time.Minute),\n\t\t\t},\n\t\t\tRes: &Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: now.Add(-4 * time.Minute),\n\t\t\t\t\tEndsAt:   now.Add(-1 * time.Minute),\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(time.Minute),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i, p := range pairs {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tif res := p.A.Merge(p.B); !reflect.DeepEqual(p.Res, res) {\n\t\t\t\tt.Errorf(\"unexpected merged alert %#v\", res)\n\t\t\t}\n\t\t\tif res := p.B.Merge(p.A); !reflect.DeepEqual(p.Res, res) {\n\t\t\t\tt.Errorf(\"unexpected merged alert %#v\", res)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAlertSliceSort(t *testing.T) {\n\tvar (\n\t\ta1 = &Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"job\":       \"j1\",\n\t\t\t\t\t\"instance\":  \"i1\",\n\t\t\t\t\t\"alertname\": \"an1\",\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\ta2 = &Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"job\":       \"j1\",\n\t\t\t\t\t\"instance\":  \"i1\",\n\t\t\t\t\t\"alertname\": \"an2\",\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\ta3 = &Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"job\":       \"j2\",\n\t\t\t\t\t\"instance\":  \"i1\",\n\t\t\t\t\t\"alertname\": \"an1\",\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\ta4 = &Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"alertname\": \"an1\",\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\ta5 = &Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"alertname\": \"an2\",\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t)\n\n\tcases := []struct {\n\t\talerts AlertSlice\n\t\texp    AlertSlice\n\t}{\n\t\t{\n\t\t\talerts: AlertSlice{a2, a1},\n\t\t\texp:    AlertSlice{a1, a2},\n\t\t},\n\t\t{\n\t\t\talerts: AlertSlice{a3, a2, a1},\n\t\t\texp:    AlertSlice{a1, a2, a3},\n\t\t},\n\t\t{\n\t\t\talerts: AlertSlice{a4, a2, a4},\n\t\t\texp:    AlertSlice{a2, a4, a4},\n\t\t},\n\t\t{\n\t\t\talerts: AlertSlice{a5, a4},\n\t\t\texp:    AlertSlice{a4, a5},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tsort.Stable(tc.alerts)\n\t\tif !reflect.DeepEqual(tc.alerts, tc.exp) {\n\t\t\tt.Fatalf(\"expected %v but got %v\", tc.exp, tc.alerts)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "alert/state.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage alert\n\n// AlertState is used as part of AlertStatus.\ntype AlertState string\n\n// Possible values for AlertState.\nconst (\n\tAlertStateUnprocessed AlertState = \"unprocessed\"\n\tAlertStateActive      AlertState = \"active\"\n\tAlertStateSuppressed  AlertState = \"suppressed\"\n)\n"
  },
  {
    "path": "alert/status.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage alert\n\n// AlertStatus stores the state of an alert and, as applicable, the IDs of\n// silences silencing the alert and of other alerts inhibiting the alert. Note\n// that currently, SilencedBy is supposed to be the complete set of the relevant\n// silences while InhibitedBy may contain only a subset of the inhibiting alerts\n// – in practice exactly one ID. (This somewhat confusing semantics might change\n// in the future.)\ntype AlertStatus struct {\n\tState       AlertState `json:\"state\"`\n\tSilencedBy  []string   `json:\"silencedBy\"`\n\tInhibitedBy []string   `json:\"inhibitedBy\"`\n}\n"
  },
  {
    "path": "alert/validate.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage alert\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/prometheus/common/model\"\n\n\t\"github.com/prometheus/alertmanager/matcher/compat\"\n)\n\nfunc validateLs(ls model.LabelSet) error {\n\tfor ln, lv := range ls {\n\t\tif !compat.IsValidLabelName(ln) {\n\t\t\treturn fmt.Errorf(\"invalid name %q\", ln)\n\t\t}\n\t\tif !lv.IsValid() {\n\t\t\treturn fmt.Errorf(\"invalid value %q\", lv)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "alert/validate_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage alert\n\nimport (\n\t\"testing\"\n\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/prometheus/alertmanager/featurecontrol\"\n\t\"github.com/prometheus/alertmanager/matcher/compat\"\n)\n\nfunc TestValidateUTF8Ls(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tls   model.LabelSet\n\t\terr  string\n\t}{{\n\t\tname: \"valid UTF-8 label set\",\n\t\tls: model.LabelSet{\n\t\t\t\"a\":                \"a\",\n\t\t\t\"00\":               \"b\",\n\t\t\t\"Σ\":                \"c\",\n\t\t\t\"\\xf0\\x9f\\x99\\x82\": \"dΘ\",\n\t\t},\n\t}, {\n\t\tname: \"invalid UTF-8 label set\",\n\t\tls: model.LabelSet{\n\t\t\t\"\\xff\": \"a\",\n\t\t},\n\t\terr: \"invalid name \\\"\\\\xff\\\"\",\n\t}}\n\n\t// Change the mode to UTF-8 mode.\n\tff, err := featurecontrol.NewFlags(promslog.NewNopLogger(), featurecontrol.FeatureUTF8StrictMode)\n\trequire.NoError(t, err)\n\tcompat.InitFromFlags(promslog.NewNopLogger(), ff)\n\n\t// Restore the mode to classic at the end of the test.\n\tff, err = featurecontrol.NewFlags(promslog.NewNopLogger(), featurecontrol.FeatureClassicMode)\n\trequire.NoError(t, err)\n\tdefer compat.InitFromFlags(promslog.NewNopLogger(), ff)\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\terr := validateLs(test.ls)\n\t\t\tif err != nil && err.Error() != test.err {\n\t\t\t\tt.Errorf(\"unexpected err for %s: %s\", test.ls, err)\n\t\t\t} else if err == nil && test.err != \"\" {\n\t\t\t\tt.Error(\"expected error, got nil\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "api/api.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage api\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/prometheus/common/route\"\n\t\"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\"\n\n\tapiv2 \"github.com/prometheus/alertmanager/api/v2\"\n\t\"github.com/prometheus/alertmanager/cluster\"\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/dispatch\"\n\t\"github.com/prometheus/alertmanager/provider\"\n\t\"github.com/prometheus/alertmanager/silence\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// API represents all APIs of Alertmanager.\ntype API struct {\n\tv2                *apiv2.API\n\tdeprecationRouter *V1DeprecationRouter\n\n\trequestDuration          *prometheus.HistogramVec\n\trequestsInFlight         prometheus.Gauge\n\tconcurrencyLimitExceeded prometheus.Counter\n\ttimeout                  time.Duration\n\tinFlightSem              chan struct{}\n}\n\n// Options for the creation of an API object. Alerts, Silences, AlertStatusFunc\n// and GroupMutedFunc are mandatory. The zero value for everything else is a safe\n// default.\ntype Options struct {\n\t// Alerts to be used by the API. Mandatory.\n\tAlerts provider.Alerts\n\t// Silences to be used by the API. Mandatory.\n\tSilences *silence.Silences\n\t// AlertStatusFunc is used be the API to retrieve the AlertStatus of an\n\t// alert. Mandatory.\n\tAlertStatusFunc func(model.Fingerprint) types.AlertStatus\n\t// GroupMutedFunc is used be the API to know if an alert is muted.\n\t// Mandatory.\n\tGroupMutedFunc func(routeID, groupKey string) ([]string, bool)\n\t// Peer from the gossip cluster. If nil, no clustering will be used.\n\tPeer cluster.ClusterPeer\n\t// Timeout for all HTTP connections. The zero value (and negative\n\t// values) result in no timeout.\n\tTimeout time.Duration\n\t// Concurrency limit for GET requests. The zero value (and negative\n\t// values) result in a limit of GOMAXPROCS or 8, whichever is\n\t// larger. Status code 503 is served for GET requests that would exceed\n\t// the concurrency limit.\n\tConcurrency int\n\t// Logger is used for logging, if nil, no logging will happen.\n\tLogger *slog.Logger\n\t// Registry is used to register Prometheus metrics. If nil, no metrics\n\t// registration will happen.\n\tRegistry prometheus.Registerer\n\t// RequestDuration is used to measure the duration of HTTP requests.\n\tRequestDuration *prometheus.HistogramVec\n\t// GroupFunc returns a list of alert groups. The alerts are grouped\n\t// according to the current active configuration. Alerts returned are\n\t// filtered by the arguments provided to the function.\n\tGroupFunc func(context.Context, func(*dispatch.Route) bool, func(*types.Alert, time.Time) bool) (dispatch.AlertGroups, map[model.Fingerprint][]string, error)\n}\n\nfunc (o Options) validate() error {\n\tif o.Alerts == nil {\n\t\treturn errors.New(\"mandatory field Alerts not set\")\n\t}\n\tif o.Silences == nil {\n\t\treturn errors.New(\"mandatory field Silences not set\")\n\t}\n\tif o.AlertStatusFunc == nil {\n\t\treturn errors.New(\"mandatory field AlertStatusFunc not set\")\n\t}\n\tif o.GroupMutedFunc == nil {\n\t\treturn errors.New(\"mandatory field GroupMutedFunc not set\")\n\t}\n\tif o.GroupFunc == nil {\n\t\treturn errors.New(\"mandatory field GroupFunc not set\")\n\t}\n\treturn nil\n}\n\n// New creates a new API object combining all API versions. Note that an Update\n// call is also needed to get the APIs into an operational state.\nfunc New(opts Options) (*API, error) {\n\tif err := opts.validate(); err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid API options: %w\", err)\n\t}\n\tl := opts.Logger\n\tif l == nil {\n\t\tl = promslog.NewNopLogger()\n\t}\n\tconcurrency := opts.Concurrency\n\tif concurrency < 1 {\n\t\tconcurrency = max(runtime.GOMAXPROCS(0), 8)\n\t}\n\n\tv2, err := apiv2.NewAPI(\n\t\topts.Alerts,\n\t\topts.GroupFunc,\n\t\topts.AlertStatusFunc,\n\t\topts.GroupMutedFunc,\n\t\topts.Silences,\n\t\topts.Peer,\n\t\tl.With(\"version\", \"v2\"),\n\t\topts.Registry,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trequestsInFlight := prometheus.NewGauge(prometheus.GaugeOpts{\n\t\tName:        \"alertmanager_http_requests_in_flight\",\n\t\tHelp:        \"Current number of HTTP requests being processed.\",\n\t\tConstLabels: prometheus.Labels{\"method\": \"get\"},\n\t})\n\tconcurrencyLimitExceeded := prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName:        \"alertmanager_http_concurrency_limit_exceeded_total\",\n\t\tHelp:        \"Total number of times an HTTP request failed because the concurrency limit was reached.\",\n\t\tConstLabels: prometheus.Labels{\"method\": \"get\"},\n\t})\n\tif opts.Registry != nil {\n\t\tif err := opts.Registry.Register(requestsInFlight); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := opts.Registry.Register(concurrencyLimitExceeded); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &API{\n\t\tdeprecationRouter:        NewV1DeprecationRouter(l.With(\"version\", \"v1\")),\n\t\tv2:                       v2,\n\t\trequestDuration:          opts.RequestDuration,\n\t\trequestsInFlight:         requestsInFlight,\n\t\tconcurrencyLimitExceeded: concurrencyLimitExceeded,\n\t\ttimeout:                  opts.Timeout,\n\t\tinFlightSem:              make(chan struct{}, concurrency),\n\t}, nil\n}\n\n// Register API. As APIv2 works on the http.Handler level, this method also creates a new\n// http.ServeMux and then uses it to register both the provided router (to\n// handle \"/\") and APIv2 (to handle \"<routePrefix>/api/v2\"). The method returns\n// the newly created http.ServeMux. If a timeout has been set on construction of\n// API, it is enforced for all HTTP request going through this mux. The same is\n// true for the concurrency limit, with the exception that it is only applied to\n// GET requests.\nfunc (api *API) Register(r *route.Router, routePrefix string) *http.ServeMux {\n\t// TODO(gotjosh) API V1 was removed as of version 0.27, when we reach 1.0.0 we should removed these deprecation warnings.\n\tapi.deprecationRouter.Register(r.WithPrefix(\"/api/v1\"))\n\n\tmux := http.NewServeMux()\n\tmux.Handle(\"/\", api.limitHandler(r))\n\n\tapiPrefix := \"\"\n\tif routePrefix != \"/\" {\n\t\tapiPrefix = routePrefix\n\t}\n\tmux.Handle(\n\t\tapiPrefix+\"/api/v2/\",\n\t\tapi.instrumentHandler(\n\t\t\tapiPrefix,\n\t\t\tapi.limitHandler(\n\t\t\t\thttp.StripPrefix(\n\t\t\t\t\tapiPrefix,\n\t\t\t\t\tapi.v2.Handler,\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t)\n\n\treturn mux\n}\n\n// Update config and resolve timeout of each API. APIv2 also needs\n// setAlertStatus to be updated.\nfunc (api *API) Update(cfg *config.Config, setAlertStatus func(ctx context.Context, labels model.LabelSet)) {\n\tapi.v2.Update(cfg, setAlertStatus)\n}\n\nfunc (api *API) limitHandler(h http.Handler) http.Handler {\n\tconcLimiter := http.HandlerFunc(func(rsp http.ResponseWriter, req *http.Request) {\n\t\tif req.Method == http.MethodGet { // Only limit concurrency of GETs.\n\t\t\tselect {\n\t\t\tcase api.inFlightSem <- struct{}{}: // All good, carry on.\n\t\t\t\tapi.requestsInFlight.Inc()\n\t\t\t\tdefer func() {\n\t\t\t\t\t<-api.inFlightSem\n\t\t\t\t\tapi.requestsInFlight.Dec()\n\t\t\t\t}()\n\t\t\tdefault:\n\t\t\t\tapi.concurrencyLimitExceeded.Inc()\n\t\t\t\thttp.Error(rsp, fmt.Sprintf(\n\t\t\t\t\t\"Limit of concurrent GET requests reached (%d), try again later.\\n\", cap(api.inFlightSem),\n\t\t\t\t), http.StatusServiceUnavailable)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\th.ServeHTTP(rsp, req)\n\t})\n\tif api.timeout <= 0 {\n\t\treturn concLimiter\n\t}\n\treturn http.TimeoutHandler(concLimiter, api.timeout, fmt.Sprintf(\n\t\t\"Exceeded configured timeout of %v.\\n\", api.timeout,\n\t))\n}\n\nfunc (api *API) instrumentHandler(prefix string, h http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tpath, _ := strings.CutPrefix(r.URL.Path, prefix)\n\t\t// avoid high cardinality label values by replacing the actual silence IDs with a placeholder\n\t\tif strings.HasPrefix(path, \"/api/v2/silence/\") {\n\t\t\tpath = \"/api/v2/silence/{silenceID}\"\n\t\t}\n\t\tpromhttp.InstrumentHandlerDuration(\n\t\t\tapi.requestDuration.MustCurryWith(prometheus.Labels{\"handler\": path}),\n\t\t\totelhttp.NewHandler(h, path),\n\t\t).ServeHTTP(w, r)\n\t})\n}\n"
  },
  {
    "path": "api/metrics/metrics.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage metrics\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n)\n\n// Alerts stores metrics for alerts.\ntype Alerts struct {\n\tfiring   prometheus.Counter\n\tresolved prometheus.Counter\n\tinvalid  prometheus.Counter\n}\n\n// NewAlerts returns an *Alerts struct for the given API version.\n// Since v1 was deprecated in 0.27, v2 is now hardcoded.\nfunc NewAlerts(r prometheus.Registerer) *Alerts {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tnumReceivedAlerts := promauto.With(r).NewCounterVec(prometheus.CounterOpts{\n\t\tName:        \"alertmanager_alerts_received_total\",\n\t\tHelp:        \"The total number of received alerts.\",\n\t\tConstLabels: prometheus.Labels{\"version\": \"v2\"},\n\t}, []string{\"status\"})\n\tnumInvalidAlerts := promauto.With(r).NewCounter(prometheus.CounterOpts{\n\t\tName:        \"alertmanager_alerts_invalid_total\",\n\t\tHelp:        \"The total number of received alerts that were invalid.\",\n\t\tConstLabels: prometheus.Labels{\"version\": \"v2\"},\n\t})\n\treturn &Alerts{\n\t\tfiring:   numReceivedAlerts.WithLabelValues(\"firing\"),\n\t\tresolved: numReceivedAlerts.WithLabelValues(\"resolved\"),\n\t\tinvalid:  numInvalidAlerts,\n\t}\n}\n\n// Firing returns a counter of firing alerts.\nfunc (a *Alerts) Firing() prometheus.Counter { return a.firing }\n\n// Resolved returns a counter of resolved alerts.\nfunc (a *Alerts) Resolved() prometheus.Counter { return a.resolved }\n\n// Invalid returns a counter of invalid alerts.\nfunc (a *Alerts) Invalid() prometheus.Counter { return a.invalid }\n"
  },
  {
    "path": "api/v1_deprecation_router.go",
    "content": "// Copyright 2023 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n\npackage api\n\nimport (\n\t\"encoding/json\"\n\t\"log/slog\"\n\t\"net/http\"\n\n\t\"github.com/prometheus/common/route\"\n)\n\n// V1DeprecationRouter is the router to signal v1 users that the API v1 is now removed.\ntype V1DeprecationRouter struct {\n\tlogger *slog.Logger\n}\n\n// NewV1DeprecationRouter returns a new V1DeprecationRouter.\nfunc NewV1DeprecationRouter(l *slog.Logger) *V1DeprecationRouter {\n\treturn &V1DeprecationRouter{\n\t\tlogger: l,\n\t}\n}\n\n// Register registers all the API v1 routes with an endpoint that returns a JSON deprecation notice and a logs a warning.\nfunc (dr *V1DeprecationRouter) Register(r *route.Router) {\n\tr.Get(\"/status\", dr.deprecationHandler)\n\tr.Get(\"/receivers\", dr.deprecationHandler)\n\n\tr.Get(\"/alerts\", dr.deprecationHandler)\n\tr.Post(\"/alerts\", dr.deprecationHandler)\n\n\tr.Get(\"/silences\", dr.deprecationHandler)\n\tr.Post(\"/silences\", dr.deprecationHandler)\n\tr.Get(\"/silence/:sid\", dr.deprecationHandler)\n\tr.Del(\"/silence/:sid\", dr.deprecationHandler)\n}\n\nfunc (dr *V1DeprecationRouter) deprecationHandler(w http.ResponseWriter, req *http.Request) {\n\tdr.logger.Warn(\"v1 API received a request on a removed endpoint\", \"path\", req.URL.Path, \"method\", req.Method)\n\n\tresp := struct {\n\t\tStatus string `json:\"status\"`\n\t\tError  string `json:\"error\"`\n\t}{\n\t\t\"deprecated\",\n\t\t\"The Alertmanager v1 API was deprecated in version 0.16.0 and is removed as of version 0.27.0 - please use the equivalent route in the v2 API\",\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.WriteHeader(410)\n\n\tif err := json.NewEncoder(w).Encode(resp); err != nil {\n\t\tdr.logger.Error(\"failed to write response\", \"err\", err)\n\t}\n}\n"
  },
  {
    "path": "api/v2/api.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage v2\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"slices\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-openapi/analysis\"\n\t\"github.com/go-openapi/loads\"\n\t\"github.com/go-openapi/runtime/middleware\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\tprometheus_model \"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/version\"\n\t\"github.com/rs/cors\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/codes\"\n\n\t\"github.com/prometheus/alertmanager/api/metrics\"\n\topen_api_models \"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/api/v2/restapi\"\n\t\"github.com/prometheus/alertmanager/api/v2/restapi/operations\"\n\talert_ops \"github.com/prometheus/alertmanager/api/v2/restapi/operations/alert\"\n\talertgroup_ops \"github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroup\"\n\tgeneral_ops \"github.com/prometheus/alertmanager/api/v2/restapi/operations/general\"\n\treceiver_ops \"github.com/prometheus/alertmanager/api/v2/restapi/operations/receiver\"\n\tsilence_ops \"github.com/prometheus/alertmanager/api/v2/restapi/operations/silence\"\n\t\"github.com/prometheus/alertmanager/cluster\"\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/dispatch\"\n\t\"github.com/prometheus/alertmanager/matcher/compat\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n\t\"github.com/prometheus/alertmanager/provider\"\n\t\"github.com/prometheus/alertmanager/silence\"\n\t\"github.com/prometheus/alertmanager/silence/silencepb\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nvar tracer = otel.Tracer(\"github.com/prometheus/alertmanager/api/v2\")\n\n// API represents an Alertmanager API v2.\ntype API struct {\n\tpeer           cluster.ClusterPeer\n\tsilences       *silence.Silences\n\talerts         provider.Alerts\n\talertGroups    groupsFn\n\tgetAlertStatus getAlertStatusFn\n\tgroupMutedFunc groupMutedFunc\n\tuptime         time.Time\n\n\t// mtx protects alertmanagerConfig, setAlertStatus and route.\n\tmtx sync.RWMutex\n\t// resolveTimeout represents the default resolve timeout that an alert is\n\t// assigned if no end time is specified.\n\talertmanagerConfig *config.Config\n\troute              *dispatch.Route\n\tsetAlertStatus     setAlertStatusFn\n\n\tlogger *slog.Logger\n\tm      *metrics.Alerts\n\n\tHandler http.Handler\n}\n\ntype (\n\tgroupsFn         func(context.Context, func(*dispatch.Route) bool, func(*types.Alert, time.Time) bool) (dispatch.AlertGroups, map[prometheus_model.Fingerprint][]string, error)\n\tgroupMutedFunc   func(routeID, groupKey string) ([]string, bool)\n\tgetAlertStatusFn func(prometheus_model.Fingerprint) types.AlertStatus\n\tsetAlertStatusFn func(ctx context.Context, labels prometheus_model.LabelSet)\n)\n\n// NewAPI returns a new Alertmanager API v2.\nfunc NewAPI(\n\talerts provider.Alerts,\n\tgf groupsFn,\n\tasf getAlertStatusFn,\n\tgmf groupMutedFunc,\n\tsilences *silence.Silences,\n\tpeer cluster.ClusterPeer,\n\tl *slog.Logger,\n\tr prometheus.Registerer,\n) (*API, error) {\n\tapi := API{\n\t\talerts:         alerts,\n\t\tgetAlertStatus: asf,\n\t\talertGroups:    gf,\n\t\tgroupMutedFunc: gmf,\n\t\tpeer:           peer,\n\t\tsilences:       silences,\n\t\tlogger:         l,\n\t\tm:              metrics.NewAlerts(r),\n\t\tuptime:         time.Now(),\n\t}\n\n\t// Load embedded swagger file.\n\tswaggerSpec, swaggerSpecAnalysis, err := getSwaggerSpec()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create new service API.\n\topenAPI := operations.NewAlertmanagerAPI(swaggerSpec)\n\n\t// Skip the  redoc middleware, only serving the OpenAPI specification and\n\t// the API itself via RoutesHandler. See:\n\t// https://github.com/go-swagger/go-swagger/issues/1779\n\topenAPI.Middleware = func(b middleware.Builder) http.Handler {\n\t\t// Manually create the context so that we can use the singleton swaggerSpecAnalysis.\n\t\tswaggerContext := middleware.NewRoutableContextWithAnalyzedSpec(swaggerSpec, swaggerSpecAnalysis, openAPI, nil)\n\t\treturn middleware.Spec(\"\", swaggerSpec.Raw(), swaggerContext.RoutesHandler(b))\n\t}\n\n\topenAPI.AlertGetAlertsHandler = alert_ops.GetAlertsHandlerFunc(api.getAlertsHandler)\n\topenAPI.AlertPostAlertsHandler = alert_ops.PostAlertsHandlerFunc(api.postAlertsHandler)\n\topenAPI.AlertgroupGetAlertGroupsHandler = alertgroup_ops.GetAlertGroupsHandlerFunc(api.getAlertGroupsHandler)\n\topenAPI.GeneralGetStatusHandler = general_ops.GetStatusHandlerFunc(api.getStatusHandler)\n\topenAPI.ReceiverGetReceiversHandler = receiver_ops.GetReceiversHandlerFunc(api.getReceiversHandler)\n\topenAPI.SilenceDeleteSilenceHandler = silence_ops.DeleteSilenceHandlerFunc(api.deleteSilenceHandler)\n\topenAPI.SilenceGetSilenceHandler = silence_ops.GetSilenceHandlerFunc(api.getSilenceHandler)\n\topenAPI.SilenceGetSilencesHandler = silence_ops.GetSilencesHandlerFunc(api.getSilencesHandler)\n\topenAPI.SilencePostSilencesHandler = silence_ops.PostSilencesHandlerFunc(api.postSilencesHandler)\n\n\thandleCORS := cors.Default().Handler\n\tapi.Handler = handleCORS(setResponseHeaders(openAPI.Serve(nil)))\n\n\treturn &api, nil\n}\n\nvar responseHeaders = map[string]string{\n\t\"Cache-Control\": \"no-store\",\n}\n\nfunc setResponseHeaders(h http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfor h, v := range responseHeaders {\n\t\t\tw.Header().Set(h, v)\n\t\t}\n\t\th.ServeHTTP(w, r)\n\t})\n}\n\nfunc (api *API) requestLogger(req *http.Request) *slog.Logger {\n\treturn api.logger.With(\"path\", req.URL.Path, \"method\", req.Method)\n}\n\n// Update sets the API struct members that may change between reloads of alertmanager.\nfunc (api *API) Update(cfg *config.Config, setAlertStatus setAlertStatusFn) {\n\tapi.mtx.Lock()\n\tdefer api.mtx.Unlock()\n\n\tapi.alertmanagerConfig = cfg\n\tapi.route = dispatch.NewRoute(cfg.Route, nil)\n\tapi.setAlertStatus = setAlertStatus\n}\n\nfunc (api *API) getStatusHandler(params general_ops.GetStatusParams) middleware.Responder {\n\tapi.mtx.RLock()\n\tdefer api.mtx.RUnlock()\n\n\t_, span := tracer.Start(params.HTTPRequest.Context(), \"api.getStatusHandler\")\n\tdefer span.End()\n\n\toriginal := api.alertmanagerConfig.String()\n\tuptime := strfmt.DateTime(api.uptime)\n\n\tstatus := open_api_models.ClusterStatusStatusDisabled\n\n\tresp := open_api_models.AlertmanagerStatus{\n\t\tUptime: &uptime,\n\t\tVersionInfo: &open_api_models.VersionInfo{\n\t\t\tVersion:   &version.Version,\n\t\t\tRevision:  &version.Revision,\n\t\t\tBranch:    &version.Branch,\n\t\t\tBuildUser: &version.BuildUser,\n\t\t\tBuildDate: &version.BuildDate,\n\t\t\tGoVersion: &version.GoVersion,\n\t\t},\n\t\tConfig: &open_api_models.AlertmanagerConfig{\n\t\t\tOriginal: &original,\n\t\t},\n\t\tCluster: &open_api_models.ClusterStatus{\n\t\t\tStatus: &status,\n\t\t\tPeers:  []*open_api_models.PeerStatus{},\n\t\t},\n\t}\n\n\t// If alertmanager cluster feature is disabled, then api.peers == nil.\n\tif api.peer != nil {\n\t\tstatus := api.peer.Status()\n\n\t\tpeers := []*open_api_models.PeerStatus{}\n\t\tfor _, n := range api.peer.Peers() {\n\t\t\taddress := n.Address()\n\t\t\tname := n.Name()\n\t\t\tpeers = append(peers, &open_api_models.PeerStatus{\n\t\t\t\tName:    &name,\n\t\t\t\tAddress: &address,\n\t\t\t})\n\t\t}\n\n\t\tsort.Slice(peers, func(i, j int) bool {\n\t\t\treturn *peers[i].Name < *peers[j].Name\n\t\t})\n\n\t\tresp.Cluster = &open_api_models.ClusterStatus{\n\t\t\tName:   api.peer.Name(),\n\t\t\tStatus: &status,\n\t\t\tPeers:  peers,\n\t\t}\n\t}\n\n\treturn general_ops.NewGetStatusOK().WithPayload(&resp)\n}\n\nfunc (api *API) getReceiversHandler(params receiver_ops.GetReceiversParams) middleware.Responder {\n\tapi.mtx.RLock()\n\tdefer api.mtx.RUnlock()\n\n\t_, span := tracer.Start(params.HTTPRequest.Context(), \"api.getReceiversHandler\")\n\tdefer span.End()\n\n\treceivers := make([]*open_api_models.Receiver, 0, len(api.alertmanagerConfig.Receivers))\n\tfor i := range api.alertmanagerConfig.Receivers {\n\t\treceivers = append(receivers, &open_api_models.Receiver{Name: &api.alertmanagerConfig.Receivers[i].Name})\n\t}\n\n\treturn receiver_ops.NewGetReceiversOK().WithPayload(receivers)\n}\n\nfunc (api *API) getAlertsHandler(params alert_ops.GetAlertsParams) middleware.Responder {\n\tvar (\n\t\treceiverFilter *regexp.Regexp\n\t\t// Initialize result slice to prevent api returning `null` when there\n\t\t// are no alerts present\n\t\tres = open_api_models.GettableAlerts{}\n\n\t\tlogger = api.requestLogger(params.HTTPRequest)\n\t)\n\n\tctx, span := tracer.Start(params.HTTPRequest.Context(), \"api.getAlertsHandler\")\n\tdefer span.End()\n\n\tmatchers, err := parseFilter(params.Filter)\n\tif err != nil {\n\t\tlogger.Debug(\"Failed to parse matchers\", \"err\", err)\n\t\treturn alertgroup_ops.NewGetAlertGroupsBadRequest().WithPayload(err.Error())\n\t}\n\n\tif params.Receiver != nil {\n\t\treceiverFilter, err = regexp.Compile(\"^(?:\" + *params.Receiver + \")$\")\n\t\tif err != nil {\n\t\t\tlogger.Debug(\"Failed to compile receiver regex\", \"err\", err)\n\t\t\treturn alert_ops.\n\t\t\t\tNewGetAlertsBadRequest().\n\t\t\t\tWithPayload(\n\t\t\t\t\tfmt.Sprintf(\"failed to parse receiver param: %v\", err.Error()),\n\t\t\t\t)\n\t\t}\n\t}\n\n\talerts := api.alerts.GetPending()\n\tdefer alerts.Close()\n\n\talertFilter := api.alertFilter(matchers, *params.Silenced, *params.Inhibited, *params.Active)\n\tnow := time.Now()\n\n\tapi.mtx.RLock()\n\tfor a := range alerts.Next() {\n\t\talert := a.Data\n\t\tif err = alerts.Err(); err != nil {\n\t\t\tbreak\n\t\t}\n\t\tif err = ctx.Err(); err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\troutes := api.route.Match(alert.Labels)\n\t\treceivers := make([]string, 0, len(routes))\n\t\tfor _, r := range routes {\n\t\t\treceivers = append(receivers, r.RouteOpts.Receiver)\n\t\t}\n\n\t\tif receiverFilter != nil && !slices.ContainsFunc(receivers, receiverFilter.MatchString) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !alertFilter(alert, now) {\n\t\t\tcontinue\n\t\t}\n\n\t\topenAlert := AlertToOpenAPIAlert(alert, api.getAlertStatus(alert.Fingerprint()), receivers, nil)\n\n\t\tres = append(res, openAlert)\n\t}\n\tapi.mtx.RUnlock()\n\n\tif err != nil {\n\t\tlogger.Error(\"Failed to get alerts\", \"err\", err)\n\t\treturn alert_ops.NewGetAlertsInternalServerError().WithPayload(err.Error())\n\t}\n\tsort.Slice(res, func(i, j int) bool {\n\t\treturn *res[i].Fingerprint < *res[j].Fingerprint\n\t})\n\n\treturn alert_ops.NewGetAlertsOK().WithPayload(res)\n}\n\nfunc (api *API) postAlertsHandler(params alert_ops.PostAlertsParams) middleware.Responder {\n\tlogger := api.requestLogger(params.HTTPRequest)\n\n\tctx, span := tracer.Start(params.HTTPRequest.Context(), \"api.postAlertsHandler\")\n\tdefer span.End()\n\n\talerts := OpenAPIAlertsToAlerts(ctx, params.Alerts)\n\n\tnow := time.Now()\n\n\tapi.mtx.RLock()\n\tresolveTimeout := time.Duration(api.alertmanagerConfig.Global.ResolveTimeout)\n\tapi.mtx.RUnlock()\n\n\tfor _, alert := range alerts {\n\t\talert.UpdatedAt = now\n\n\t\t// Ensure StartsAt is set.\n\t\tif alert.StartsAt.IsZero() {\n\t\t\tif alert.EndsAt.IsZero() {\n\t\t\t\talert.StartsAt = now\n\t\t\t} else {\n\t\t\t\talert.StartsAt = alert.EndsAt\n\t\t\t}\n\t\t}\n\t\t// If no end time is defined, set a timeout after which an alert\n\t\t// is marked resolved if it is not updated.\n\t\tif alert.EndsAt.IsZero() {\n\t\t\talert.Timeout = true\n\t\t\talert.EndsAt = now.Add(resolveTimeout)\n\t\t}\n\t\tif alert.EndsAt.After(time.Now()) {\n\t\t\tapi.m.Firing().Inc()\n\t\t} else {\n\t\t\tapi.m.Resolved().Inc()\n\t\t}\n\t}\n\n\t// Make a best effort to insert all alerts that are valid.\n\tvar (\n\t\tvalidAlerts    = make([]*types.Alert, 0, len(alerts))\n\t\tvalidationErrs error\n\t)\n\tfor _, a := range alerts {\n\t\tremoveEmptyLabels(a.Labels)\n\n\t\tif err := a.Validate(); err != nil {\n\t\t\tvalidationErrs = errors.Join(validationErrs, err)\n\t\t\tapi.m.Invalid().Inc()\n\t\t\tcontinue\n\t\t}\n\t\tvalidAlerts = append(validAlerts, a)\n\t}\n\tif err := api.alerts.Put(ctx, validAlerts...); err != nil {\n\t\tmessage := \"Failed to create alerts\"\n\t\tlogger.Error(message, \"err\", err)\n\t\tspan.SetStatus(codes.Error, message)\n\t\tspan.RecordError(err)\n\t\treturn alert_ops.NewPostAlertsInternalServerError().WithPayload(err.Error())\n\t}\n\n\tif validationErrs != nil {\n\t\tmessage := \"Failed to validate alerts\"\n\t\tlogger.Error(message, \"err\", validationErrs.Error())\n\t\tspan.SetStatus(codes.Error, message)\n\t\tspan.RecordError(validationErrs)\n\t\treturn alert_ops.NewPostAlertsBadRequest().WithPayload(validationErrs.Error())\n\t}\n\n\treturn alert_ops.NewPostAlertsOK()\n}\n\nfunc (api *API) getAlertGroupsHandler(params alertgroup_ops.GetAlertGroupsParams) middleware.Responder {\n\tlogger := api.requestLogger(params.HTTPRequest)\n\n\tctx, span := tracer.Start(params.HTTPRequest.Context(), \"api.getAlertGroupsHandler\")\n\tdefer span.End()\n\n\tmatchers, err := parseFilter(params.Filter)\n\tif err != nil {\n\t\tlogger.Debug(\"Failed to parse matchers\", \"err\", err)\n\t\treturn alertgroup_ops.NewGetAlertGroupsBadRequest().WithPayload(err.Error())\n\t}\n\n\tvar receiverFilter *regexp.Regexp\n\tif params.Receiver != nil {\n\t\treceiverFilter, err = regexp.Compile(\"^(?:\" + *params.Receiver + \")$\")\n\t\tif err != nil {\n\t\t\tlogger.Error(\"Failed to compile receiver regex\", \"err\", err)\n\t\t\treturn alertgroup_ops.\n\t\t\t\tNewGetAlertGroupsBadRequest().\n\t\t\t\tWithPayload(\n\t\t\t\t\tfmt.Sprintf(\"failed to parse receiver param: %v\", err.Error()),\n\t\t\t\t)\n\t\t}\n\t}\n\n\trf := func(receiverFilter *regexp.Regexp) func(r *dispatch.Route) bool {\n\t\treturn func(r *dispatch.Route) bool {\n\t\t\treceiver := r.RouteOpts.Receiver\n\t\t\tif receiverFilter != nil && !receiverFilter.MatchString(receiver) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn true\n\t\t}\n\t}(receiverFilter)\n\n\taf := api.alertFilter(matchers, *params.Silenced, *params.Inhibited, *params.Active)\n\talertGroups, allReceivers, err := api.alertGroups(ctx, rf, af)\n\tif err != nil {\n\t\tmessage := \"Failed to get alert groups\"\n\t\tlogger.Error(message, \"err\", err)\n\t\tspan.SetStatus(codes.Error, message)\n\t\tspan.RecordError(err)\n\t\treturn alertgroup_ops.NewGetAlertGroupsInternalServerError()\n\t}\n\n\tres := make(open_api_models.AlertGroups, 0, len(alertGroups))\n\n\tfor _, alertGroup := range alertGroups {\n\t\tmutedBy, isMuted := api.groupMutedFunc(alertGroup.RouteID, alertGroup.GroupKey)\n\t\tif !*params.Muted && isMuted {\n\t\t\tcontinue\n\t\t}\n\n\t\tag := &open_api_models.AlertGroup{\n\t\t\tReceiver: &open_api_models.Receiver{Name: &alertGroup.Receiver},\n\t\t\tLabels:   ModelLabelSetToAPILabelSet(alertGroup.Labels),\n\t\t\tAlerts:   make([]*open_api_models.GettableAlert, 0, len(alertGroup.Alerts)),\n\t\t}\n\n\t\tfor _, alert := range alertGroup.Alerts {\n\t\t\tfp := alert.Fingerprint()\n\t\t\treceivers := allReceivers[fp]\n\t\t\tstatus := api.getAlertStatus(fp)\n\t\t\tapiAlert := AlertToOpenAPIAlert(alert, status, receivers, mutedBy)\n\t\t\tag.Alerts = append(ag.Alerts, apiAlert)\n\t\t}\n\t\tres = append(res, ag)\n\t}\n\n\treturn alertgroup_ops.NewGetAlertGroupsOK().WithPayload(res)\n}\n\nfunc (api *API) alertFilter(matchers []*labels.Matcher, silenced, inhibited, active bool) func(a *types.Alert, now time.Time) bool {\n\treturn func(a *types.Alert, now time.Time) bool {\n\t\tctx, span := tracer.Start(context.Background(), \"alertFilter\")\n\t\tdefer span.End()\n\n\t\tif !a.EndsAt.IsZero() && a.EndsAt.Before(now) {\n\t\t\treturn false\n\t\t}\n\n\t\t// Set alert's current status based on its label set.\n\t\tapi.setAlertStatus(ctx, a.Labels)\n\n\t\t// Get alert's current status after seeing if it is suppressed.\n\t\tstatus := api.getAlertStatus(a.Fingerprint())\n\n\t\tif !active && status.State == types.AlertStateActive {\n\t\t\treturn false\n\t\t}\n\n\t\tif !silenced && len(status.SilencedBy) != 0 {\n\t\t\treturn false\n\t\t}\n\n\t\tif !inhibited && len(status.InhibitedBy) != 0 {\n\t\t\treturn false\n\t\t}\n\n\t\treturn alertMatchesFilterLabels(&a.Alert, matchers)\n\t}\n}\n\nfunc removeEmptyLabels(ls prometheus_model.LabelSet) {\n\tfor k, v := range ls {\n\t\tif string(v) == \"\" {\n\t\t\tdelete(ls, k)\n\t\t}\n\t}\n}\n\nfunc alertMatchesFilterLabels(a *prometheus_model.Alert, matchers []*labels.Matcher) bool {\n\tsms := make(map[string]string)\n\tfor name, value := range a.Labels {\n\t\tsms[string(name)] = string(value)\n\t}\n\treturn matchFilterLabels(matchers, sms)\n}\n\nfunc matchFilterLabels(matchers []*labels.Matcher, sms map[string]string) bool {\n\tfor _, m := range matchers {\n\t\tv, prs := sms[m.Name]\n\t\tswitch m.Type {\n\t\tcase labels.MatchNotRegexp, labels.MatchNotEqual:\n\t\t\tif m.Value == \"\" && prs {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !m.Matches(v) {\n\t\t\t\treturn false\n\t\t\t}\n\t\tdefault:\n\t\t\tif m.Value == \"\" && !prs {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !m.Matches(v) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc (api *API) getSilencesHandler(params silence_ops.GetSilencesParams) middleware.Responder {\n\tlogger := api.requestLogger(params.HTTPRequest)\n\n\tctx, span := tracer.Start(params.HTTPRequest.Context(), \"api.getSilencesHandler\")\n\tdefer span.End()\n\n\tmatchers, err := parseFilter(params.Filter)\n\tif err != nil {\n\t\tlogger.Debug(\"Failed to parse matchers\", \"err\", err)\n\t\treturn silence_ops.NewGetSilencesBadRequest().WithPayload(err.Error())\n\t}\n\n\tpsils, _, err := api.silences.Query(ctx)\n\tif err != nil {\n\t\tlogger.Error(\"Failed to get silences\", \"err\", err)\n\t\treturn silence_ops.NewGetSilencesInternalServerError().WithPayload(err.Error())\n\t}\n\n\tsils := open_api_models.GettableSilences{}\n\tfor _, ps := range psils {\n\t\tif !CheckSilenceMatchesFilterLabels(ps, matchers) {\n\t\t\tcontinue\n\t\t}\n\t\tsilence, err := GettableSilenceFromProto(ps)\n\t\tif err != nil {\n\t\t\tlogger.Error(\"Failed to unmarshal silence from proto\", \"err\", err)\n\t\t\treturn silence_ops.NewGetSilencesInternalServerError().WithPayload(err.Error())\n\t\t}\n\t\tsils = append(sils, &silence)\n\t}\n\n\tSortSilences(sils)\n\n\treturn silence_ops.NewGetSilencesOK().WithPayload(sils)\n}\n\nvar silenceStateOrder = map[silence.SilenceState]int{\n\tsilence.SilenceStateActive:  1,\n\tsilence.SilenceStatePending: 2,\n\tsilence.SilenceStateExpired: 3,\n}\n\n// SortSilences sorts first according to the state \"active, pending, expired\"\n// then by end time or start time depending on the state.\n// Active silences should show the next to expire first\n// pending silences are ordered based on which one starts next\n// expired are ordered based on which one expired most recently.\nfunc SortSilences(sils open_api_models.GettableSilences) {\n\tsort.Slice(sils, func(i, j int) bool {\n\t\tstate1 := silence.SilenceState(*sils[i].Status.State)\n\t\tstate2 := silence.SilenceState(*sils[j].Status.State)\n\t\tif state1 != state2 {\n\t\t\treturn silenceStateOrder[state1] < silenceStateOrder[state2]\n\t\t}\n\t\tswitch state1 {\n\t\tcase silence.SilenceStateActive:\n\t\t\tendsAt1 := time.Time(*sils[i].EndsAt)\n\t\t\tendsAt2 := time.Time(*sils[j].EndsAt)\n\t\t\treturn endsAt1.Before(endsAt2)\n\t\tcase silence.SilenceStatePending:\n\t\t\tstartsAt1 := time.Time(*sils[i].StartsAt)\n\t\t\tstartsAt2 := time.Time(*sils[j].StartsAt)\n\t\t\treturn startsAt1.Before(startsAt2)\n\t\tcase silence.SilenceStateExpired:\n\t\t\tendsAt1 := time.Time(*sils[i].EndsAt)\n\t\t\tendsAt2 := time.Time(*sils[j].EndsAt)\n\t\t\treturn endsAt1.After(endsAt2)\n\t\t}\n\t\treturn false\n\t})\n}\n\n// CheckSilenceMatchesFilterLabels returns true if\n// a given silence matches a list of matchers.\n// A silence matches a filter (list of matchers) if\n// for all matchers in the filter, there exists a matcher in the silence\n// such that their names, types, and values are equivalent.\nfunc CheckSilenceMatchesFilterLabels(s *silencepb.Silence, matchers []*labels.Matcher) bool {\n\t// Check if any matcher set matches (OR logic)\n\tfor _, ms := range s.MatcherSets {\n\t\tif checkMatcherSetMatchesFilterLabels(ms, matchers) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc checkMatcherSetMatchesFilterLabels(ms *silencepb.MatcherSet, matchers []*labels.Matcher) bool {\n\tfor _, matcher := range matchers {\n\t\tfound := false\n\t\tfor _, m := range ms.Matchers {\n\t\t\tif matcher.Name == m.Name &&\n\t\t\t\t(matcher.Type == labels.MatchEqual && m.Type == silencepb.Matcher_EQUAL ||\n\t\t\t\t\tmatcher.Type == labels.MatchRegexp && m.Type == silencepb.Matcher_REGEXP ||\n\t\t\t\t\tmatcher.Type == labels.MatchNotEqual && m.Type == silencepb.Matcher_NOT_EQUAL ||\n\t\t\t\t\tmatcher.Type == labels.MatchNotRegexp && m.Type == silencepb.Matcher_NOT_REGEXP) &&\n\t\t\t\tmatcher.Value == m.Pattern {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc (api *API) getSilenceHandler(params silence_ops.GetSilenceParams) middleware.Responder {\n\tlogger := api.requestLogger(params.HTTPRequest)\n\n\tctx, span := tracer.Start(params.HTTPRequest.Context(), \"api.getSilenceHandler\")\n\tdefer span.End()\n\n\tsils, _, err := api.silences.Query(ctx, silence.QIDs(params.SilenceID.String()))\n\tif err != nil {\n\t\tlogger.Error(\"Failed to get silence by id\", \"err\", err, \"id\", params.SilenceID.String())\n\t\treturn silence_ops.NewGetSilenceInternalServerError().WithPayload(err.Error())\n\t}\n\n\tif len(sils) == 0 {\n\t\tlogger.Error(\"Failed to find silence\", \"err\", err, \"id\", params.SilenceID.String())\n\t\treturn silence_ops.NewGetSilenceNotFound()\n\t}\n\n\tsil, err := GettableSilenceFromProto(sils[0])\n\tif err != nil {\n\t\tlogger.Error(\"Failed to convert unmarshal from proto\", \"err\", err)\n\t\treturn silence_ops.NewGetSilenceInternalServerError().WithPayload(err.Error())\n\t}\n\n\treturn silence_ops.NewGetSilenceOK().WithPayload(&sil)\n}\n\nfunc (api *API) deleteSilenceHandler(params silence_ops.DeleteSilenceParams) middleware.Responder {\n\tlogger := api.requestLogger(params.HTTPRequest)\n\n\tctx, span := tracer.Start(params.HTTPRequest.Context(), \"api.deleteSilenceHandler\")\n\tdefer span.End()\n\n\tsid := params.SilenceID.String()\n\tif err := api.silences.Expire(ctx, sid); err != nil {\n\t\tlogger.Error(\"Failed to expire silence\", \"err\", err)\n\t\tif errors.Is(err, silence.ErrNotFound) {\n\t\t\treturn silence_ops.NewDeleteSilenceNotFound()\n\t\t}\n\t\treturn silence_ops.NewDeleteSilenceInternalServerError().WithPayload(err.Error())\n\t}\n\treturn silence_ops.NewDeleteSilenceOK()\n}\n\nfunc (api *API) postSilencesHandler(params silence_ops.PostSilencesParams) middleware.Responder {\n\tlogger := api.requestLogger(params.HTTPRequest)\n\n\tctx, span := tracer.Start(params.HTTPRequest.Context(), \"api.postSilencesHandler\")\n\tdefer span.End()\n\n\tsil, err := PostableSilenceToProto(params.Silence)\n\tif err != nil {\n\t\tlogger.Error(\"Failed to marshal silence to proto\", \"err\", err)\n\t\treturn silence_ops.NewPostSilencesBadRequest().WithPayload(\n\t\t\tfmt.Sprintf(\"failed to convert API silence to internal silence: %v\", err.Error()),\n\t\t)\n\t}\n\n\tif sil.StartsAt.AsTime().After(sil.EndsAt.AsTime()) || sil.StartsAt.AsTime().Equal(sil.EndsAt.AsTime()) {\n\t\tmsg := \"Failed to create silence: start time must be before end time\"\n\t\tlogger.Error(msg, \"starts_at\", sil.StartsAt, \"ends_at\", sil.EndsAt)\n\t\treturn silence_ops.NewPostSilencesBadRequest().WithPayload(msg)\n\t}\n\n\tif sil.EndsAt.AsTime().Before(time.Now()) {\n\t\tmsg := \"Failed to create silence: end time can't be in the past\"\n\t\tlogger.Error(msg, \"ends_at\", sil.EndsAt)\n\t\treturn silence_ops.NewPostSilencesBadRequest().WithPayload(msg)\n\t}\n\n\tif err = api.silences.Set(ctx, sil); err != nil {\n\t\tlogger.Error(\"Failed to create silence\", \"err\", err)\n\t\tif errors.Is(err, silence.ErrNotFound) {\n\t\t\treturn silence_ops.NewPostSilencesNotFound().WithPayload(err.Error())\n\t\t}\n\t\treturn silence_ops.NewPostSilencesBadRequest().WithPayload(err.Error())\n\t}\n\n\treturn silence_ops.NewPostSilencesOK().WithPayload(&silence_ops.PostSilencesOKBody{\n\t\tSilenceID: sil.Id,\n\t})\n}\n\nfunc parseFilter(filter []string) ([]*labels.Matcher, error) {\n\tmatchers := make([]*labels.Matcher, 0, len(filter))\n\tfor _, matcherString := range filter {\n\t\tmatcher, err := compat.Matcher(matcherString, \"api\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tmatchers = append(matchers, matcher)\n\t}\n\treturn matchers, nil\n}\n\nvar (\n\tswaggerSpecCacheMx       sync.Mutex\n\tswaggerSpecCache         *loads.Document\n\tswaggerSpecAnalysisCache *analysis.Spec\n)\n\n// getSwaggerSpec loads and caches the swagger spec. If a cached version already exists,\n// it returns the cached one. The reason why we cache it is because some downstream projects\n// (e.g. Grafana Mimir) creates many Alertmanager instances in the same process, so they would\n// incur in a significant memory penalty if we would reload the swagger spec each time.\nfunc getSwaggerSpec() (*loads.Document, *analysis.Spec, error) {\n\tswaggerSpecCacheMx.Lock()\n\tdefer swaggerSpecCacheMx.Unlock()\n\n\t// Check if a cached version exists.\n\tif swaggerSpecCache != nil {\n\t\treturn swaggerSpecCache, swaggerSpecAnalysisCache, nil\n\t}\n\n\t// Load embedded swagger file.\n\tswaggerSpec, err := loads.Analyzed(restapi.SwaggerJSON, \"\")\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to load embedded swagger file: %w\", err)\n\t}\n\n\tswaggerSpecCache = swaggerSpec\n\tswaggerSpecAnalysisCache = analysis.New(swaggerSpec.Spec())\n\treturn swaggerSpec, swaggerSpecAnalysisCache, nil\n}\n"
  },
  {
    "path": "api/v2/api_test.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage v2\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-openapi/runtime\"\n\t\"github.com/go-openapi/runtime/middleware\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\topen_api_models \"github.com/prometheus/alertmanager/api/v2/models\"\n\tgeneral_ops \"github.com/prometheus/alertmanager/api/v2/restapi/operations/general\"\n\treceiver_ops \"github.com/prometheus/alertmanager/api/v2/restapi/operations/receiver\"\n\tsilence_ops \"github.com/prometheus/alertmanager/api/v2/restapi/operations/silence\"\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n\t\"github.com/prometheus/alertmanager/silence\"\n\t\"github.com/prometheus/alertmanager/silence/silencepb\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// If api.peers == nil, Alertmanager cluster feature is disabled. Make sure to\n// not try to access properties of peer, which would trigger a nil pointer\n// dereference.\nfunc TestGetStatusHandlerWithNilPeer(t *testing.T) {\n\tapi := API{\n\t\tuptime:             time.Now(),\n\t\tpeer:               nil,\n\t\talertmanagerConfig: &config.Config{},\n\t}\n\n\t// Test ensures this method call does not panic.\n\tstatus := api.getStatusHandler(\n\t\tgeneral_ops.GetStatusParams{\n\t\t\tHTTPRequest: httptest.NewRequest(\n\t\t\t\t\"GET\",\n\t\t\t\t\"/api/v2/status\",\n\t\t\t\tnil,\n\t\t\t),\n\t\t},\n\t).(*general_ops.GetStatusOK)\n\n\tc := status.Payload.Cluster\n\n\tif c == nil || c.Status == nil {\n\t\tt.Fatal(\"expected cluster status not to be nil, violating the openapi specification\")\n\t}\n\n\tif c.Peers == nil {\n\t\tt.Fatal(\"expected cluster peers to be not nil when api.peer is nil, violating the openapi specification\")\n\t}\n\tif len(c.Peers) != 0 {\n\t\tt.Fatal(\"expected cluster peers to be empty when api.peer is nil, violating the openapi specification\")\n\t}\n\n\tif c.Name != \"\" {\n\t\tt.Fatal(\"expected cluster name to be empty, violating the openapi specification\")\n\t}\n}\n\nfunc assertEqualStrings(t *testing.T, expected, actual string) {\n\tif expected != actual {\n\t\tt.Fatal(\"expected: \", expected, \", actual: \", actual)\n\t}\n}\n\nvar (\n\ttestComment = \"comment\"\n\tcreatedBy   = \"test\"\n)\n\nfunc newSilences(t *testing.T) *silence.Silences {\n\tsilences, err := silence.New(silence.Options{Metrics: prometheus.NewRegistry()})\n\trequire.NoError(t, err)\n\n\treturn silences\n}\n\nfunc gettableSilence(id, state string,\n\tupdatedAt, start, end string,\n) *open_api_models.GettableSilence {\n\tupdAt, err := strfmt.ParseDateTime(updatedAt)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tstrAt, err := strfmt.ParseDateTime(start)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tendAt, err := strfmt.ParseDateTime(end)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn &open_api_models.GettableSilence{\n\t\tSilence: open_api_models.Silence{\n\t\t\tStartsAt:  &strAt,\n\t\t\tEndsAt:    &endAt,\n\t\t\tComment:   &testComment,\n\t\t\tCreatedBy: &createdBy,\n\t\t},\n\t\tID:        &id,\n\t\tUpdatedAt: &updAt,\n\t\tStatus: &open_api_models.SilenceStatus{\n\t\t\tState: &state,\n\t\t},\n\t}\n}\n\nfunc TestGetSilencesHandler(t *testing.T) {\n\tupdateTime := \"2019-01-01T12:00:00+00:00\"\n\tsilences := []*open_api_models.GettableSilence{\n\t\tgettableSilence(\"silence-6-expired\", \"expired\", updateTime,\n\t\t\t\"2019-01-01T12:00:00+00:00\", \"2019-01-01T11:00:00+00:00\"),\n\t\tgettableSilence(\"silence-1-active\", \"active\", updateTime,\n\t\t\t\"2019-01-01T12:00:00+00:00\", \"2019-01-01T13:00:00+00:00\"),\n\t\tgettableSilence(\"silence-7-expired\", \"expired\", updateTime,\n\t\t\t\"2019-01-01T12:00:00+00:00\", \"2019-01-01T10:00:00+00:00\"),\n\t\tgettableSilence(\"silence-5-expired\", \"expired\", updateTime,\n\t\t\t\"2019-01-01T12:00:00+00:00\", \"2019-01-01T12:00:00+00:00\"),\n\t\tgettableSilence(\"silence-0-active\", \"active\", updateTime,\n\t\t\t\"2019-01-01T12:00:00+00:00\", \"2019-01-01T12:00:00+00:00\"),\n\t\tgettableSilence(\"silence-4-pending\", \"pending\", updateTime,\n\t\t\t\"2019-01-01T13:00:00+00:00\", \"2019-01-01T12:00:00+00:00\"),\n\t\tgettableSilence(\"silence-3-pending\", \"pending\", updateTime,\n\t\t\t\"2019-01-01T12:00:00+00:00\", \"2019-01-01T12:00:00+00:00\"),\n\t\tgettableSilence(\"silence-2-active\", \"active\", updateTime,\n\t\t\t\"2019-01-01T12:00:00+00:00\", \"2019-01-01T14:00:00+00:00\"),\n\t}\n\tSortSilences(open_api_models.GettableSilences(silences))\n\n\tfor i, sil := range silences {\n\t\tassertEqualStrings(t, \"silence-\"+strconv.Itoa(i)+\"-\"+*sil.Status.State, *sil.ID)\n\t}\n}\n\nfunc TestDeleteSilenceHandler(t *testing.T) {\n\tnow := timestamppb.Now()\n\tsilences := newSilences(t)\n\n\tm := &silencepb.Matcher{Type: silencepb.Matcher_EQUAL, Name: \"a\", Pattern: \"b\"}\n\n\tunexpiredSil := &silencepb.Silence{\n\t\tMatcherSets: []*silencepb.MatcherSet{{\n\t\t\tMatchers: []*silencepb.Matcher{m},\n\t\t}},\n\t\tStartsAt:  now,\n\t\tEndsAt:    timestamppb.New(now.AsTime().Add(time.Hour)),\n\t\tUpdatedAt: now,\n\t}\n\trequire.NoError(t, silences.Set(t.Context(), unexpiredSil))\n\n\texpiredSil := &silencepb.Silence{\n\t\tMatcherSets: []*silencepb.MatcherSet{{\n\t\t\tMatchers: []*silencepb.Matcher{m},\n\t\t}},\n\t\tStartsAt:  timestamppb.New(now.AsTime().Add(-time.Hour)),\n\t\tEndsAt:    timestamppb.New(now.AsTime().Add(time.Hour)),\n\t\tUpdatedAt: now,\n\t}\n\trequire.NoError(t, silences.Set(t.Context(), expiredSil))\n\trequire.NoError(t, silences.Expire(t.Context(), expiredSil.Id))\n\n\tfor i, tc := range []struct {\n\t\tsid          string\n\t\texpectedCode int\n\t}{\n\t\t{\n\t\t\t\"unknownSid\",\n\t\t\t404,\n\t\t},\n\t\t{\n\t\t\tunexpiredSil.Id,\n\t\t\t200,\n\t\t},\n\t\t{\n\t\t\texpiredSil.Id,\n\t\t\t200,\n\t\t},\n\t} {\n\t\tapi := API{\n\t\t\tuptime:   time.Now(),\n\t\t\tsilences: silences,\n\t\t\tlogger:   promslog.NewNopLogger(),\n\t\t}\n\n\t\tr, err := http.NewRequest(\"DELETE\", \"/api/v2/silence/${tc.sid}\", nil)\n\t\trequire.NoError(t, err)\n\n\t\tw := httptest.NewRecorder()\n\t\tp := runtime.TextProducer()\n\t\tresponder := api.deleteSilenceHandler(silence_ops.DeleteSilenceParams{\n\t\t\tSilenceID:   strfmt.UUID(tc.sid),\n\t\t\tHTTPRequest: r,\n\t\t})\n\t\tresponder.WriteResponse(w, p)\n\t\tbody, _ := io.ReadAll(w.Result().Body)\n\n\t\trequire.Equal(t, tc.expectedCode, w.Code, \"test case: %d, response: %s\", i, string(body))\n\t}\n}\n\nfunc TestPostSilencesHandler(t *testing.T) {\n\tnow := timestamppb.Now()\n\tsilences := newSilences(t)\n\n\tm := &silencepb.Matcher{Type: silencepb.Matcher_EQUAL, Name: \"a\", Pattern: \"b\"}\n\n\tunexpiredSil := &silencepb.Silence{\n\t\tMatcherSets: []*silencepb.MatcherSet{{\n\t\t\tMatchers: []*silencepb.Matcher{m},\n\t\t}},\n\t\tStartsAt:  now,\n\t\tEndsAt:    timestamppb.New(now.AsTime().Add(time.Hour)),\n\t\tUpdatedAt: now,\n\t}\n\trequire.NoError(t, silences.Set(t.Context(), unexpiredSil))\n\n\texpiredSil := &silencepb.Silence{\n\t\tMatcherSets: []*silencepb.MatcherSet{{\n\t\t\tMatchers: []*silencepb.Matcher{m},\n\t\t}},\n\t\tStartsAt:  timestamppb.New(now.AsTime().Add(-time.Hour)),\n\t\tEndsAt:    timestamppb.New(now.AsTime().Add(time.Hour)),\n\t\tUpdatedAt: now,\n\t}\n\trequire.NoError(t, silences.Set(t.Context(), expiredSil))\n\trequire.NoError(t, silences.Expire(t.Context(), expiredSil.Id))\n\n\tt.Run(\"Silences CRUD\", func(t *testing.T) {\n\t\tfor i, tc := range []struct {\n\t\t\tname         string\n\t\t\tsid          string\n\t\t\tstart, end   time.Time\n\t\t\texpectedCode int\n\t\t}{\n\t\t\t{\n\t\t\t\t\"with an non-existent silence ID - it returns 404\",\n\t\t\t\t\"unknownSid\",\n\t\t\t\tnow.AsTime().Add(time.Hour),\n\t\t\t\tnow.AsTime().Add(time.Hour * 2),\n\t\t\t\t404,\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"with no silence ID - it creates the silence\",\n\t\t\t\t\"\",\n\t\t\t\tnow.AsTime().Add(time.Hour),\n\t\t\t\tnow.AsTime().Add(time.Hour * 2),\n\t\t\t\t200,\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"with an active silence ID - it extends the silence\",\n\t\t\t\tunexpiredSil.Id,\n\t\t\t\tnow.AsTime().Add(time.Hour),\n\t\t\t\tnow.AsTime().Add(time.Hour * 2),\n\t\t\t\t200,\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"with an expired silence ID - it re-creates the silence\",\n\t\t\t\texpiredSil.Id,\n\t\t\t\tnow.AsTime().Add(time.Hour),\n\t\t\t\tnow.AsTime().Add(time.Hour * 2),\n\t\t\t\t200,\n\t\t\t},\n\t\t} {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tapi := API{\n\t\t\t\t\tuptime:   time.Now(),\n\t\t\t\t\tsilences: silences,\n\t\t\t\t\tlogger:   promslog.NewNopLogger(),\n\t\t\t\t}\n\n\t\t\t\tsil := createSilence(t, tc.sid, \"silenceCreator\", tc.start, tc.end)\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\tpostSilences(t, w, api.postSilencesHandler, sil)\n\t\t\t\tbody, _ := io.ReadAll(w.Result().Body)\n\t\t\t\trequire.Equal(t, tc.expectedCode, w.Code, \"test case: %d, response: %s\", i, string(body))\n\t\t\t})\n\t\t}\n\t})\n}\n\nfunc TestPostSilencesHandlerMissingIdCreatesSilence(t *testing.T) {\n\tnow := time.Now()\n\tsilences := newSilences(t)\n\tapi := API{\n\t\tuptime:   time.Now(),\n\t\tsilences: silences,\n\t\tlogger:   promslog.NewNopLogger(),\n\t}\n\n\t// Create a new silence. It should be assigned a random UUID.\n\tsil := createSilence(t, \"\", \"silenceCreator\", now.Add(time.Hour), now.Add(time.Hour*2))\n\tw := httptest.NewRecorder()\n\tpostSilences(t, w, api.postSilencesHandler, sil)\n\trequire.Equal(t, http.StatusOK, w.Code)\n\n\t// Get the silences from the API.\n\tw = httptest.NewRecorder()\n\tgetSilences(t, w, api.getSilencesHandler)\n\trequire.Equal(t, http.StatusOK, w.Code)\n\tvar resp []open_api_models.GettableSilence\n\trequire.NoError(t, json.NewDecoder(w.Body).Decode(&resp))\n\trequire.Len(t, resp, 1)\n\n\t// Change the ID. It should return 404 Not Found.\n\tsil = open_api_models.PostableSilence{\n\t\tID:      \"unknownID\",\n\t\tSilence: resp[0].Silence,\n\t}\n\tw = httptest.NewRecorder()\n\tpostSilences(t, w, api.postSilencesHandler, sil)\n\trequire.Equal(t, http.StatusNotFound, w.Code)\n\n\t// Remove the ID. It should duplicate the silence with a different UUID.\n\tsil = open_api_models.PostableSilence{\n\t\tID:      \"\",\n\t\tSilence: resp[0].Silence,\n\t}\n\tw = httptest.NewRecorder()\n\tpostSilences(t, w, api.postSilencesHandler, sil)\n\trequire.Equal(t, http.StatusOK, w.Code)\n\n\t// Get the silences from the API. There should now be 2 silences.\n\tw = httptest.NewRecorder()\n\tgetSilences(t, w, api.getSilencesHandler)\n\trequire.Equal(t, http.StatusOK, w.Code)\n\trequire.NoError(t, json.NewDecoder(w.Body).Decode(&resp))\n\trequire.Len(t, resp, 2)\n\trequire.NotEqual(t, resp[0].ID, resp[1].ID)\n}\n\nfunc getSilences(\n\tt *testing.T,\n\tw *httptest.ResponseRecorder,\n\thandlerFunc func(params silence_ops.GetSilencesParams) middleware.Responder,\n) {\n\tr, err := http.NewRequest(\"GET\", \"/api/v2/silences\", nil)\n\trequire.NoError(t, err)\n\n\tp := runtime.TextProducer()\n\tresponder := handlerFunc(silence_ops.GetSilencesParams{\n\t\tHTTPRequest: r,\n\t\tFilter:      nil,\n\t})\n\tresponder.WriteResponse(w, p)\n}\n\nfunc postSilences(\n\tt *testing.T,\n\tw *httptest.ResponseRecorder,\n\thandlerFunc func(params silence_ops.PostSilencesParams) middleware.Responder,\n\tsil open_api_models.PostableSilence,\n) {\n\tb, err := json.Marshal(sil)\n\trequire.NoError(t, err)\n\n\tr, err := http.NewRequest(\"POST\", \"/api/v2/silences\", bytes.NewReader(b))\n\trequire.NoError(t, err)\n\n\tp := runtime.TextProducer()\n\tresponder := handlerFunc(silence_ops.PostSilencesParams{\n\t\tHTTPRequest: r,\n\t\tSilence:     &sil,\n\t})\n\tresponder.WriteResponse(w, p)\n}\n\nfunc TestCheckSilenceMatchesFilterLabels(t *testing.T) {\n\ttype test struct {\n\t\tsilenceMatchers []*silencepb.Matcher\n\t\tfilterMatchers  []*labels.Matcher\n\t\texpected        bool\n\t}\n\n\ttests := []test{\n\t\t{\n\t\t\t[]*silencepb.Matcher{createSilenceMatcher(t, \"label\", \"value\", silencepb.Matcher_EQUAL)},\n\t\t\t[]*labels.Matcher{createLabelMatcher(t, \"label\", \"value\", labels.MatchEqual)},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t[]*silencepb.Matcher{createSilenceMatcher(t, \"label\", \"value\", silencepb.Matcher_EQUAL)},\n\t\t\t[]*labels.Matcher{createLabelMatcher(t, \"label\", \"novalue\", labels.MatchEqual)},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t[]*silencepb.Matcher{createSilenceMatcher(t, \"label\", \"(foo|bar)\", silencepb.Matcher_REGEXP)},\n\t\t\t[]*labels.Matcher{createLabelMatcher(t, \"label\", \"(foo|bar)\", labels.MatchRegexp)},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t[]*silencepb.Matcher{createSilenceMatcher(t, \"label\", \"foo\", silencepb.Matcher_REGEXP)},\n\t\t\t[]*labels.Matcher{createLabelMatcher(t, \"label\", \"(foo|bar)\", labels.MatchRegexp)},\n\t\t\tfalse,\n\t\t},\n\n\t\t{\n\t\t\t[]*silencepb.Matcher{createSilenceMatcher(t, \"label\", \"value\", silencepb.Matcher_EQUAL)},\n\t\t\t[]*labels.Matcher{createLabelMatcher(t, \"label\", \"value\", labels.MatchRegexp)},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t[]*silencepb.Matcher{createSilenceMatcher(t, \"label\", \"value\", silencepb.Matcher_REGEXP)},\n\t\t\t[]*labels.Matcher{createLabelMatcher(t, \"label\", \"value\", labels.MatchEqual)},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t[]*silencepb.Matcher{createSilenceMatcher(t, \"label\", \"value\", silencepb.Matcher_NOT_EQUAL)},\n\t\t\t[]*labels.Matcher{createLabelMatcher(t, \"label\", \"value\", labels.MatchNotEqual)},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t[]*silencepb.Matcher{createSilenceMatcher(t, \"label\", \"value\", silencepb.Matcher_NOT_REGEXP)},\n\t\t\t[]*labels.Matcher{createLabelMatcher(t, \"label\", \"value\", labels.MatchNotRegexp)},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t[]*silencepb.Matcher{createSilenceMatcher(t, \"label\", \"value\", silencepb.Matcher_EQUAL)},\n\t\t\t[]*labels.Matcher{createLabelMatcher(t, \"label\", \"value\", labels.MatchNotEqual)},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t[]*silencepb.Matcher{createSilenceMatcher(t, \"label\", \"value\", silencepb.Matcher_REGEXP)},\n\t\t\t[]*labels.Matcher{createLabelMatcher(t, \"label\", \"value\", labels.MatchNotRegexp)},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t[]*silencepb.Matcher{createSilenceMatcher(t, \"label\", \"value\", silencepb.Matcher_NOT_EQUAL)},\n\t\t\t[]*labels.Matcher{createLabelMatcher(t, \"label\", \"value\", labels.MatchNotRegexp)},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t[]*silencepb.Matcher{createSilenceMatcher(t, \"label\", \"value\", silencepb.Matcher_NOT_REGEXP)},\n\t\t\t[]*labels.Matcher{createLabelMatcher(t, \"label\", \"value\", labels.MatchNotEqual)},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t[]*silencepb.Matcher{\n\t\t\t\tcreateSilenceMatcher(t, \"label\", \"(foo|bar)\", silencepb.Matcher_REGEXP),\n\t\t\t\tcreateSilenceMatcher(t, \"label\", \"value\", silencepb.Matcher_EQUAL),\n\t\t\t},\n\t\t\t[]*labels.Matcher{createLabelMatcher(t, \"label\", \"(foo|bar)\", labels.MatchRegexp)},\n\t\t\ttrue,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tsilence := silencepb.Silence{\n\t\t\tMatcherSets: []*silencepb.MatcherSet{{\n\t\t\t\tMatchers: test.silenceMatchers,\n\t\t\t}},\n\t\t}\n\t\tactual := CheckSilenceMatchesFilterLabels(&silence, test.filterMatchers)\n\t\tif test.expected != actual {\n\t\t\tt.Fatal(\"unexpected match result between silence and filter. expected:\", test.expected, \", actual:\", actual)\n\t\t}\n\t}\n}\n\nfunc convertDateTime(ts time.Time) *strfmt.DateTime {\n\tdt := strfmt.DateTime(ts)\n\treturn &dt\n}\n\nfunc TestAlertToOpenAPIAlert(t *testing.T) {\n\tvar (\n\t\tstart     = time.Now().Add(-time.Minute)\n\t\tupdated   = time.Now()\n\t\tactive    = \"active\"\n\t\tfp        = \"0223b772b51c29e1\"\n\t\treceivers = []string{\"receiver1\", \"receiver2\"}\n\n\t\talert = &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels:   model.LabelSet{\"severity\": \"critical\", \"alertname\": \"alert1\"},\n\t\t\t\tStartsAt: start,\n\t\t\t},\n\t\t\tUpdatedAt: updated,\n\t\t}\n\t)\n\topenAPIAlert := AlertToOpenAPIAlert(alert, types.AlertStatus{State: types.AlertStateActive}, receivers, nil)\n\trequire.Equal(t, &open_api_models.GettableAlert{\n\t\tAnnotations: open_api_models.LabelSet{},\n\t\tAlert: open_api_models.Alert{\n\t\t\tLabels: open_api_models.LabelSet{\"severity\": \"critical\", \"alertname\": \"alert1\"},\n\t\t},\n\t\tStartsAt:    convertDateTime(start),\n\t\tEndsAt:      convertDateTime(time.Time{}),\n\t\tUpdatedAt:   convertDateTime(updated),\n\t\tFingerprint: &fp,\n\t\tReceivers: []*open_api_models.Receiver{\n\t\t\t{Name: &receivers[0]},\n\t\t\t{Name: &receivers[1]},\n\t\t},\n\t\tStatus: &open_api_models.AlertStatus{\n\t\t\tState:       &active,\n\t\t\tInhibitedBy: []string{},\n\t\t\tSilencedBy:  []string{},\n\t\t\tMutedBy:     []string{},\n\t\t},\n\t}, openAPIAlert)\n}\n\nfunc TestMatchFilterLabels(t *testing.T) {\n\tsms := map[string]string{\n\t\t\"foo\": \"bar\",\n\t}\n\n\ttestCases := []struct {\n\t\tmatcher  labels.MatchType\n\t\tname     string\n\t\tval      string\n\t\texpected bool\n\t}{\n\t\t{labels.MatchEqual, \"foo\", \"bar\", true},\n\t\t{labels.MatchEqual, \"baz\", \"\", true},\n\t\t{labels.MatchEqual, \"baz\", \"qux\", false},\n\t\t{labels.MatchEqual, \"baz\", \"qux|\", false},\n\t\t{labels.MatchRegexp, \"foo\", \"bar\", true},\n\t\t{labels.MatchRegexp, \"baz\", \"\", true},\n\t\t{labels.MatchRegexp, \"baz\", \"qux\", false},\n\t\t{labels.MatchRegexp, \"baz\", \"qux|\", true},\n\t\t{labels.MatchNotEqual, \"foo\", \"bar\", false},\n\t\t{labels.MatchNotEqual, \"baz\", \"\", false},\n\t\t{labels.MatchNotEqual, \"baz\", \"qux\", true},\n\t\t{labels.MatchNotEqual, \"baz\", \"qux|\", true},\n\t\t{labels.MatchNotRegexp, \"foo\", \"bar\", false},\n\t\t{labels.MatchNotRegexp, \"baz\", \"\", false},\n\t\t{labels.MatchNotRegexp, \"baz\", \"qux\", true},\n\t\t{labels.MatchNotRegexp, \"baz\", \"qux|\", false},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tm, err := labels.NewMatcher(tc.matcher, tc.name, tc.val)\n\t\trequire.NoError(t, err)\n\n\t\tms := []*labels.Matcher{m}\n\t\trequire.Equal(t, tc.expected, matchFilterLabels(ms, sms))\n\t}\n}\n\nfunc TestGetReceiversHandler(t *testing.T) {\n\tin := `\nroute:\n    receiver: team-X\n\nreceivers:\n- name: 'team-X'\n- name: 'team-Y'\n`\n\tcfg, _ := config.Load(in)\n\tapi := API{\n\t\tuptime:             time.Now(),\n\t\tlogger:             promslog.NewNopLogger(),\n\t\talertmanagerConfig: cfg,\n\t}\n\n\tfor _, tc := range []struct {\n\t\tbody         string\n\t\texpectedCode int\n\t}{\n\t\t{\n\t\t\t`[{\"name\":\"team-X\"},{\"name\":\"team-Y\"}]`,\n\t\t\t200,\n\t\t},\n\t} {\n\t\tr, err := http.NewRequest(\"GET\", \"/api/v2/receivers\", nil)\n\t\trequire.NoError(t, err)\n\n\t\tw := httptest.NewRecorder()\n\t\tp := runtime.TextProducer()\n\t\tresponder := api.getReceiversHandler(receiver_ops.GetReceiversParams{\n\t\t\tHTTPRequest: r,\n\t\t})\n\t\tresponder.WriteResponse(w, p)\n\t\tbody, _ := io.ReadAll(w.Result().Body)\n\n\t\trequire.Equal(t, tc.expectedCode, w.Code)\n\t\trequire.Equal(t, tc.body, string(body))\n\t}\n}\n\nfunc BenchmarkOpenAPIAlertsToAlerts(b *testing.B) {\n\tnow := strfmt.DateTime(time.Now())\n\tapiAlerts := make(open_api_models.PostableAlerts, 100)\n\tfor i := range apiAlerts {\n\t\tapiAlerts[i] = &open_api_models.PostableAlert{\n\t\t\tAlert: open_api_models.Alert{\n\t\t\t\tLabels: open_api_models.LabelSet{\"alertname\": \"test\", \"i\": strconv.Itoa(i)},\n\t\t\t},\n\t\t\tStartsAt: now,\n\t\t\tEndsAt:   now,\n\t\t}\n\t}\n\n\tb.Run(\"PreAllocated\", func(b *testing.B) {\n\t\tctx := context.Background()\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tOpenAPIAlertsToAlerts(ctx, apiAlerts)\n\t\t}\n\t})\n\n\tb.Run(\"AppendGrowth\", func(b *testing.B) {\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\talerts := []*types.Alert{}\n\t\t\tfor _, apiAlert := range apiAlerts {\n\t\t\t\talerts = append(alerts, &types.Alert{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels:       APILabelSetToModelLabelSet(apiAlert.Labels),\n\t\t\t\t\t\tAnnotations:  APILabelSetToModelLabelSet(apiAlert.Annotations),\n\t\t\t\t\t\tStartsAt:     time.Time(apiAlert.StartsAt),\n\t\t\t\t\t\tEndsAt:       time.Time(apiAlert.EndsAt),\n\t\t\t\t\t\tGeneratorURL: string(apiAlert.GeneratorURL),\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t\t_ = alerts\n\t\t}\n\t})\n}\n\nfunc TestPostSilences_QuotedMatchers(t *testing.T) {\n\t// This test ensures that quoted values in matchers are preserved during JSON unmarshalling\n\tjsonBlob := `{\"comment\":\"foo\", \"createdBy\": \"author\", \"startsAt\":\"2023-03-06T00:22:15Z\", \"endsAt\":\"2024-03-06T00:22:15Z\", \"matchers\":[{\"isRegex\":true, \"name\":\"instance\", \"value\":\"\\\"bar\\\"\"}]}`\n\n\tvar ps open_api_models.PostableSilence\n\terr := json.Unmarshal([]byte(jsonBlob), &ps)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, ps.Matchers, 1)\n\trequire.Equal(t, \"\\\"bar\\\"\", *ps.Matchers[0].Value)\n\n\tsilProto, err := PostableSilenceToProto(&ps)\n\trequire.NoError(t, err)\n\trequire.Len(t, silProto.MatcherSets, 1)\n\trequire.Len(t, silProto.MatcherSets[0].Matchers, 1)\n\trequire.Equal(t, \"\\\"bar\\\"\", silProto.MatcherSets[0].Matchers[0].Pattern)\n}\n"
  },
  {
    "path": "api/v2/client/alert/alert_client.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alert\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-openapi/runtime\"\n\thttptransport \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n)\n\n// New creates a new alert API client.\nfunc New(transport runtime.ClientTransport, formats strfmt.Registry) ClientService {\n\treturn &Client{transport: transport, formats: formats}\n}\n\n// New creates a new alert API client with basic auth credentials.\n// It takes the following parameters:\n// - host: http host (github.com).\n// - basePath: any base path for the API client (\"/v1\", \"/v3\").\n// - scheme: http scheme (\"http\", \"https\").\n// - user: user for basic authentication header.\n// - password: password for basic authentication header.\nfunc NewClientWithBasicAuth(host, basePath, scheme, user, password string) ClientService {\n\ttransport := httptransport.New(host, basePath, []string{scheme})\n\ttransport.DefaultAuthentication = httptransport.BasicAuth(user, password)\n\treturn &Client{transport: transport, formats: strfmt.Default}\n}\n\n// New creates a new alert API client with a bearer token for authentication.\n// It takes the following parameters:\n// - host: http host (github.com).\n// - basePath: any base path for the API client (\"/v1\", \"/v3\").\n// - scheme: http scheme (\"http\", \"https\").\n// - bearerToken: bearer token for Bearer authentication header.\nfunc NewClientWithBearerToken(host, basePath, scheme, bearerToken string) ClientService {\n\ttransport := httptransport.New(host, basePath, []string{scheme})\n\ttransport.DefaultAuthentication = httptransport.BearerToken(bearerToken)\n\treturn &Client{transport: transport, formats: strfmt.Default}\n}\n\n/*\nClient for alert API\n*/\ntype Client struct {\n\ttransport runtime.ClientTransport\n\tformats   strfmt.Registry\n}\n\n// ClientOption may be used to customize the behavior of Client methods.\ntype ClientOption func(*runtime.ClientOperation)\n\n// ClientService is the interface for Client methods\ntype ClientService interface {\n\tGetAlerts(params *GetAlertsParams, opts ...ClientOption) (*GetAlertsOK, error)\n\n\tPostAlerts(params *PostAlertsParams, opts ...ClientOption) (*PostAlertsOK, error)\n\n\tSetTransport(transport runtime.ClientTransport)\n}\n\n/*\nGetAlerts Get a list of alerts\n*/\nfunc (a *Client) GetAlerts(params *GetAlertsParams, opts ...ClientOption) (*GetAlertsOK, error) {\n\t// NOTE: parameters are not validated before sending\n\tif params == nil {\n\t\tparams = NewGetAlertsParams()\n\t}\n\top := &runtime.ClientOperation{\n\t\tID:                 \"getAlerts\",\n\t\tMethod:             \"GET\",\n\t\tPathPattern:        \"/alerts\",\n\t\tProducesMediaTypes: []string{\"application/json\"},\n\t\tConsumesMediaTypes: []string{\"application/json\"},\n\t\tSchemes:            []string{\"http\"},\n\t\tParams:             params,\n\t\tReader:             &GetAlertsReader{formats: a.formats},\n\t\tContext:            params.Context,\n\t\tClient:             params.HTTPClient,\n\t}\n\tfor _, opt := range opts {\n\t\topt(op)\n\t}\n\tresult, err := a.transport.Submit(op)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// only one success response has to be checked\n\tsuccess, ok := result.(*GetAlertsOK)\n\tif ok {\n\t\treturn success, nil\n\t}\n\n\t// unexpected success response.\n\n\t// no default response is defined.\n\t//\n\t// safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue\n\tmsg := fmt.Sprintf(\"unexpected success response for getAlerts: API contract not enforced by server. Client expected to get an error, but got: %T\", result)\n\tpanic(msg)\n}\n\n/*\nPostAlerts Create new Alerts\n*/\nfunc (a *Client) PostAlerts(params *PostAlertsParams, opts ...ClientOption) (*PostAlertsOK, error) {\n\t// NOTE: parameters are not validated before sending\n\tif params == nil {\n\t\tparams = NewPostAlertsParams()\n\t}\n\top := &runtime.ClientOperation{\n\t\tID:                 \"postAlerts\",\n\t\tMethod:             \"POST\",\n\t\tPathPattern:        \"/alerts\",\n\t\tProducesMediaTypes: []string{\"application/json\"},\n\t\tConsumesMediaTypes: []string{\"application/json\"},\n\t\tSchemes:            []string{\"http\"},\n\t\tParams:             params,\n\t\tReader:             &PostAlertsReader{formats: a.formats},\n\t\tContext:            params.Context,\n\t\tClient:             params.HTTPClient,\n\t}\n\tfor _, opt := range opts {\n\t\topt(op)\n\t}\n\tresult, err := a.transport.Submit(op)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// only one success response has to be checked\n\tsuccess, ok := result.(*PostAlertsOK)\n\tif ok {\n\t\treturn success, nil\n\t}\n\n\t// unexpected success response.\n\n\t// no default response is defined.\n\t//\n\t// safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue\n\tmsg := fmt.Sprintf(\"unexpected success response for postAlerts: API contract not enforced by server. Client expected to get an error, but got: %T\", result)\n\tpanic(msg)\n}\n\n// SetTransport changes the transport on the client\nfunc (a *Client) SetTransport(transport runtime.ClientTransport) {\n\ta.transport = transport\n}\n"
  },
  {
    "path": "api/v2/client/alert/get_alerts_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alert\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime\"\n\tcr \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n)\n\n// NewGetAlertsParams creates a new GetAlertsParams object,\n// with the default timeout for this client.\n//\n// Default values are not hydrated, since defaults are normally applied by the API server side.\n//\n// To enforce default values in parameter, use SetDefaults or WithDefaults.\nfunc NewGetAlertsParams() *GetAlertsParams {\n\treturn &GetAlertsParams{\n\t\ttimeout: cr.DefaultTimeout,\n\t}\n}\n\n// NewGetAlertsParamsWithTimeout creates a new GetAlertsParams object\n// with the ability to set a timeout on a request.\nfunc NewGetAlertsParamsWithTimeout(timeout time.Duration) *GetAlertsParams {\n\treturn &GetAlertsParams{\n\t\ttimeout: timeout,\n\t}\n}\n\n// NewGetAlertsParamsWithContext creates a new GetAlertsParams object\n// with the ability to set a context for a request.\nfunc NewGetAlertsParamsWithContext(ctx context.Context) *GetAlertsParams {\n\treturn &GetAlertsParams{\n\t\tContext: ctx,\n\t}\n}\n\n// NewGetAlertsParamsWithHTTPClient creates a new GetAlertsParams object\n// with the ability to set a custom HTTPClient for a request.\nfunc NewGetAlertsParamsWithHTTPClient(client *http.Client) *GetAlertsParams {\n\treturn &GetAlertsParams{\n\t\tHTTPClient: client,\n\t}\n}\n\n/*\nGetAlertsParams contains all the parameters to send to the API endpoint\n\n\tfor the get alerts operation.\n\n\tTypically these are written to a http.Request.\n*/\ntype GetAlertsParams struct {\n\n\t/* Active.\n\n\t   Include active alerts in results. If false, excludes active alerts and returns only suppressed (silenced or inhibited) alerts.\n\n\t   Default: true\n\t*/\n\tActive *bool\n\n\t/* Filter.\n\n\t   A matcher expression to filter alerts. For example `alertname=\"MyAlert\"`. It can be repeated to apply multiple matchers.\n\t*/\n\tFilter []string\n\n\t/* Inhibited.\n\n\t   Include inhibited alerts in results. If false, excludes inhibited alerts. Note that true (default) shows both inhibited and non-inhibited alerts.\n\n\t   Default: true\n\t*/\n\tInhibited *bool\n\n\t/* Receiver.\n\n\t   A regex matching receivers to filter alerts by\n\t*/\n\tReceiver *string\n\n\t/* Silenced.\n\n\t   Include silenced alerts in results. If false, excludes silenced alerts. Note that true (default) shows both silenced and non-silenced alerts.\n\n\t   Default: true\n\t*/\n\tSilenced *bool\n\n\t/* Unprocessed.\n\n\t   Include unprocessed alerts in results. If false, excludes unprocessed alerts. Note that true (default) shows both processed and unprocessed alerts.\n\n\t   Default: true\n\t*/\n\tUnprocessed *bool\n\n\ttimeout    time.Duration\n\tContext    context.Context\n\tHTTPClient *http.Client\n}\n\n// WithDefaults hydrates default values in the get alerts params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *GetAlertsParams) WithDefaults() *GetAlertsParams {\n\to.SetDefaults()\n\treturn o\n}\n\n// SetDefaults hydrates default values in the get alerts params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *GetAlertsParams) SetDefaults() {\n\tvar (\n\t\tactiveDefault = bool(true)\n\n\t\tinhibitedDefault = bool(true)\n\n\t\tsilencedDefault = bool(true)\n\n\t\tunprocessedDefault = bool(true)\n\t)\n\n\tval := GetAlertsParams{\n\t\tActive:      &activeDefault,\n\t\tInhibited:   &inhibitedDefault,\n\t\tSilenced:    &silencedDefault,\n\t\tUnprocessed: &unprocessedDefault,\n\t}\n\n\tval.timeout = o.timeout\n\tval.Context = o.Context\n\tval.HTTPClient = o.HTTPClient\n\t*o = val\n}\n\n// WithTimeout adds the timeout to the get alerts params\nfunc (o *GetAlertsParams) WithTimeout(timeout time.Duration) *GetAlertsParams {\n\to.SetTimeout(timeout)\n\treturn o\n}\n\n// SetTimeout adds the timeout to the get alerts params\nfunc (o *GetAlertsParams) SetTimeout(timeout time.Duration) {\n\to.timeout = timeout\n}\n\n// WithContext adds the context to the get alerts params\nfunc (o *GetAlertsParams) WithContext(ctx context.Context) *GetAlertsParams {\n\to.SetContext(ctx)\n\treturn o\n}\n\n// SetContext adds the context to the get alerts params\nfunc (o *GetAlertsParams) SetContext(ctx context.Context) {\n\to.Context = ctx\n}\n\n// WithHTTPClient adds the HTTPClient to the get alerts params\nfunc (o *GetAlertsParams) WithHTTPClient(client *http.Client) *GetAlertsParams {\n\to.SetHTTPClient(client)\n\treturn o\n}\n\n// SetHTTPClient adds the HTTPClient to the get alerts params\nfunc (o *GetAlertsParams) SetHTTPClient(client *http.Client) {\n\to.HTTPClient = client\n}\n\n// WithActive adds the active to the get alerts params\nfunc (o *GetAlertsParams) WithActive(active *bool) *GetAlertsParams {\n\to.SetActive(active)\n\treturn o\n}\n\n// SetActive adds the active to the get alerts params\nfunc (o *GetAlertsParams) SetActive(active *bool) {\n\to.Active = active\n}\n\n// WithFilter adds the filter to the get alerts params\nfunc (o *GetAlertsParams) WithFilter(filter []string) *GetAlertsParams {\n\to.SetFilter(filter)\n\treturn o\n}\n\n// SetFilter adds the filter to the get alerts params\nfunc (o *GetAlertsParams) SetFilter(filter []string) {\n\to.Filter = filter\n}\n\n// WithInhibited adds the inhibited to the get alerts params\nfunc (o *GetAlertsParams) WithInhibited(inhibited *bool) *GetAlertsParams {\n\to.SetInhibited(inhibited)\n\treturn o\n}\n\n// SetInhibited adds the inhibited to the get alerts params\nfunc (o *GetAlertsParams) SetInhibited(inhibited *bool) {\n\to.Inhibited = inhibited\n}\n\n// WithReceiver adds the receiver to the get alerts params\nfunc (o *GetAlertsParams) WithReceiver(receiver *string) *GetAlertsParams {\n\to.SetReceiver(receiver)\n\treturn o\n}\n\n// SetReceiver adds the receiver to the get alerts params\nfunc (o *GetAlertsParams) SetReceiver(receiver *string) {\n\to.Receiver = receiver\n}\n\n// WithSilenced adds the silenced to the get alerts params\nfunc (o *GetAlertsParams) WithSilenced(silenced *bool) *GetAlertsParams {\n\to.SetSilenced(silenced)\n\treturn o\n}\n\n// SetSilenced adds the silenced to the get alerts params\nfunc (o *GetAlertsParams) SetSilenced(silenced *bool) {\n\to.Silenced = silenced\n}\n\n// WithUnprocessed adds the unprocessed to the get alerts params\nfunc (o *GetAlertsParams) WithUnprocessed(unprocessed *bool) *GetAlertsParams {\n\to.SetUnprocessed(unprocessed)\n\treturn o\n}\n\n// SetUnprocessed adds the unprocessed to the get alerts params\nfunc (o *GetAlertsParams) SetUnprocessed(unprocessed *bool) {\n\to.Unprocessed = unprocessed\n}\n\n// WriteToRequest writes these params to a swagger request\nfunc (o *GetAlertsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {\n\n\tif err := r.SetTimeout(o.timeout); err != nil {\n\t\treturn err\n\t}\n\tvar res []error\n\n\tif o.Active != nil {\n\n\t\t// query param active\n\t\tvar qrActive bool\n\n\t\tif o.Active != nil {\n\t\t\tqrActive = *o.Active\n\t\t}\n\t\tqActive := swag.FormatBool(qrActive)\n\t\tif qActive != \"\" {\n\n\t\t\tif err := r.SetQueryParam(\"active\", qActive); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tif o.Filter != nil {\n\n\t\t// binding items for filter\n\t\tjoinedFilter := o.bindParamFilter(reg)\n\n\t\t// query array param filter\n\t\tif err := r.SetQueryParam(\"filter\", joinedFilter...); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif o.Inhibited != nil {\n\n\t\t// query param inhibited\n\t\tvar qrInhibited bool\n\n\t\tif o.Inhibited != nil {\n\t\t\tqrInhibited = *o.Inhibited\n\t\t}\n\t\tqInhibited := swag.FormatBool(qrInhibited)\n\t\tif qInhibited != \"\" {\n\n\t\t\tif err := r.SetQueryParam(\"inhibited\", qInhibited); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tif o.Receiver != nil {\n\n\t\t// query param receiver\n\t\tvar qrReceiver string\n\n\t\tif o.Receiver != nil {\n\t\t\tqrReceiver = *o.Receiver\n\t\t}\n\t\tqReceiver := qrReceiver\n\t\tif qReceiver != \"\" {\n\n\t\t\tif err := r.SetQueryParam(\"receiver\", qReceiver); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tif o.Silenced != nil {\n\n\t\t// query param silenced\n\t\tvar qrSilenced bool\n\n\t\tif o.Silenced != nil {\n\t\t\tqrSilenced = *o.Silenced\n\t\t}\n\t\tqSilenced := swag.FormatBool(qrSilenced)\n\t\tif qSilenced != \"\" {\n\n\t\t\tif err := r.SetQueryParam(\"silenced\", qSilenced); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tif o.Unprocessed != nil {\n\n\t\t// query param unprocessed\n\t\tvar qrUnprocessed bool\n\n\t\tif o.Unprocessed != nil {\n\t\t\tqrUnprocessed = *o.Unprocessed\n\t\t}\n\t\tqUnprocessed := swag.FormatBool(qrUnprocessed)\n\t\tif qUnprocessed != \"\" {\n\n\t\t\tif err := r.SetQueryParam(\"unprocessed\", qUnprocessed); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\n// bindParamGetAlerts binds the parameter filter\nfunc (o *GetAlertsParams) bindParamFilter(formats strfmt.Registry) []string {\n\tfilterIR := o.Filter\n\n\tvar filterIC []string\n\tfor _, filterIIR := range filterIR { // explode []string\n\n\t\tfilterIIV := filterIIR // string as string\n\t\tfilterIC = append(filterIC, filterIIV)\n\t}\n\n\t// items.CollectionFormat: \"multi\"\n\tfilterIS := swag.JoinByFormat(filterIC, \"multi\")\n\n\treturn filterIS\n}\n"
  },
  {
    "path": "api/v2/client/alert/get_alerts_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alert\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"encoding/json\"\n\tstderrors \"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/go-openapi/runtime\"\n\t\"github.com/go-openapi/strfmt\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\n// GetAlertsReader is a Reader for the GetAlerts structure.\ntype GetAlertsReader struct {\n\tformats strfmt.Registry\n}\n\n// ReadResponse reads a server response into the received o.\nfunc (o *GetAlertsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {\n\tswitch response.Code() {\n\tcase 200:\n\t\tresult := NewGetAlertsOK()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn result, nil\n\tcase 400:\n\t\tresult := NewGetAlertsBadRequest()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, result\n\tcase 500:\n\t\tresult := NewGetAlertsInternalServerError()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, result\n\tdefault:\n\t\treturn nil, runtime.NewAPIError(\"[GET /alerts] getAlerts\", response, response.Code())\n\t}\n}\n\n// NewGetAlertsOK creates a GetAlertsOK with default headers values\nfunc NewGetAlertsOK() *GetAlertsOK {\n\treturn &GetAlertsOK{}\n}\n\n/*\nGetAlertsOK describes a response with status code 200, with default header values.\n\nGet alerts response\n*/\ntype GetAlertsOK struct {\n\tPayload models.GettableAlerts\n}\n\n// IsSuccess returns true when this get alerts o k response has a 2xx status code\nfunc (o *GetAlertsOK) IsSuccess() bool {\n\treturn true\n}\n\n// IsRedirect returns true when this get alerts o k response has a 3xx status code\nfunc (o *GetAlertsOK) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this get alerts o k response has a 4xx status code\nfunc (o *GetAlertsOK) IsClientError() bool {\n\treturn false\n}\n\n// IsServerError returns true when this get alerts o k response has a 5xx status code\nfunc (o *GetAlertsOK) IsServerError() bool {\n\treturn false\n}\n\n// IsCode returns true when this get alerts o k response a status code equal to that given\nfunc (o *GetAlertsOK) IsCode(code int) bool {\n\treturn code == 200\n}\n\n// Code gets the status code for the get alerts o k response\nfunc (o *GetAlertsOK) Code() int {\n\treturn 200\n}\n\nfunc (o *GetAlertsOK) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /alerts][%d] getAlertsOK %s\", 200, payload)\n}\n\nfunc (o *GetAlertsOK) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /alerts][%d] getAlertsOK %s\", 200, payload)\n}\n\nfunc (o *GetAlertsOK) GetPayload() models.GettableAlerts {\n\treturn o.Payload\n}\n\nfunc (o *GetAlertsOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// NewGetAlertsBadRequest creates a GetAlertsBadRequest with default headers values\nfunc NewGetAlertsBadRequest() *GetAlertsBadRequest {\n\treturn &GetAlertsBadRequest{}\n}\n\n/*\nGetAlertsBadRequest describes a response with status code 400, with default header values.\n\nBad request\n*/\ntype GetAlertsBadRequest struct {\n\tPayload string\n}\n\n// IsSuccess returns true when this get alerts bad request response has a 2xx status code\nfunc (o *GetAlertsBadRequest) IsSuccess() bool {\n\treturn false\n}\n\n// IsRedirect returns true when this get alerts bad request response has a 3xx status code\nfunc (o *GetAlertsBadRequest) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this get alerts bad request response has a 4xx status code\nfunc (o *GetAlertsBadRequest) IsClientError() bool {\n\treturn true\n}\n\n// IsServerError returns true when this get alerts bad request response has a 5xx status code\nfunc (o *GetAlertsBadRequest) IsServerError() bool {\n\treturn false\n}\n\n// IsCode returns true when this get alerts bad request response a status code equal to that given\nfunc (o *GetAlertsBadRequest) IsCode(code int) bool {\n\treturn code == 400\n}\n\n// Code gets the status code for the get alerts bad request response\nfunc (o *GetAlertsBadRequest) Code() int {\n\treturn 400\n}\n\nfunc (o *GetAlertsBadRequest) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /alerts][%d] getAlertsBadRequest %s\", 400, payload)\n}\n\nfunc (o *GetAlertsBadRequest) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /alerts][%d] getAlertsBadRequest %s\", 400, payload)\n}\n\nfunc (o *GetAlertsBadRequest) GetPayload() string {\n\treturn o.Payload\n}\n\nfunc (o *GetAlertsBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// NewGetAlertsInternalServerError creates a GetAlertsInternalServerError with default headers values\nfunc NewGetAlertsInternalServerError() *GetAlertsInternalServerError {\n\treturn &GetAlertsInternalServerError{}\n}\n\n/*\nGetAlertsInternalServerError describes a response with status code 500, with default header values.\n\nInternal server error\n*/\ntype GetAlertsInternalServerError struct {\n\tPayload string\n}\n\n// IsSuccess returns true when this get alerts internal server error response has a 2xx status code\nfunc (o *GetAlertsInternalServerError) IsSuccess() bool {\n\treturn false\n}\n\n// IsRedirect returns true when this get alerts internal server error response has a 3xx status code\nfunc (o *GetAlertsInternalServerError) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this get alerts internal server error response has a 4xx status code\nfunc (o *GetAlertsInternalServerError) IsClientError() bool {\n\treturn false\n}\n\n// IsServerError returns true when this get alerts internal server error response has a 5xx status code\nfunc (o *GetAlertsInternalServerError) IsServerError() bool {\n\treturn true\n}\n\n// IsCode returns true when this get alerts internal server error response a status code equal to that given\nfunc (o *GetAlertsInternalServerError) IsCode(code int) bool {\n\treturn code == 500\n}\n\n// Code gets the status code for the get alerts internal server error response\nfunc (o *GetAlertsInternalServerError) Code() int {\n\treturn 500\n}\n\nfunc (o *GetAlertsInternalServerError) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /alerts][%d] getAlertsInternalServerError %s\", 500, payload)\n}\n\nfunc (o *GetAlertsInternalServerError) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /alerts][%d] getAlertsInternalServerError %s\", 500, payload)\n}\n\nfunc (o *GetAlertsInternalServerError) GetPayload() string {\n\treturn o.Payload\n}\n\nfunc (o *GetAlertsInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/client/alert/post_alerts_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alert\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime\"\n\tcr \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\n// NewPostAlertsParams creates a new PostAlertsParams object,\n// with the default timeout for this client.\n//\n// Default values are not hydrated, since defaults are normally applied by the API server side.\n//\n// To enforce default values in parameter, use SetDefaults or WithDefaults.\nfunc NewPostAlertsParams() *PostAlertsParams {\n\treturn &PostAlertsParams{\n\t\ttimeout: cr.DefaultTimeout,\n\t}\n}\n\n// NewPostAlertsParamsWithTimeout creates a new PostAlertsParams object\n// with the ability to set a timeout on a request.\nfunc NewPostAlertsParamsWithTimeout(timeout time.Duration) *PostAlertsParams {\n\treturn &PostAlertsParams{\n\t\ttimeout: timeout,\n\t}\n}\n\n// NewPostAlertsParamsWithContext creates a new PostAlertsParams object\n// with the ability to set a context for a request.\nfunc NewPostAlertsParamsWithContext(ctx context.Context) *PostAlertsParams {\n\treturn &PostAlertsParams{\n\t\tContext: ctx,\n\t}\n}\n\n// NewPostAlertsParamsWithHTTPClient creates a new PostAlertsParams object\n// with the ability to set a custom HTTPClient for a request.\nfunc NewPostAlertsParamsWithHTTPClient(client *http.Client) *PostAlertsParams {\n\treturn &PostAlertsParams{\n\t\tHTTPClient: client,\n\t}\n}\n\n/*\nPostAlertsParams contains all the parameters to send to the API endpoint\n\n\tfor the post alerts operation.\n\n\tTypically these are written to a http.Request.\n*/\ntype PostAlertsParams struct {\n\n\t/* Alerts.\n\n\t   The alerts to create\n\t*/\n\tAlerts models.PostableAlerts\n\n\ttimeout    time.Duration\n\tContext    context.Context\n\tHTTPClient *http.Client\n}\n\n// WithDefaults hydrates default values in the post alerts params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *PostAlertsParams) WithDefaults() *PostAlertsParams {\n\to.SetDefaults()\n\treturn o\n}\n\n// SetDefaults hydrates default values in the post alerts params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *PostAlertsParams) SetDefaults() {\n\t// no default values defined for this parameter\n}\n\n// WithTimeout adds the timeout to the post alerts params\nfunc (o *PostAlertsParams) WithTimeout(timeout time.Duration) *PostAlertsParams {\n\to.SetTimeout(timeout)\n\treturn o\n}\n\n// SetTimeout adds the timeout to the post alerts params\nfunc (o *PostAlertsParams) SetTimeout(timeout time.Duration) {\n\to.timeout = timeout\n}\n\n// WithContext adds the context to the post alerts params\nfunc (o *PostAlertsParams) WithContext(ctx context.Context) *PostAlertsParams {\n\to.SetContext(ctx)\n\treturn o\n}\n\n// SetContext adds the context to the post alerts params\nfunc (o *PostAlertsParams) SetContext(ctx context.Context) {\n\to.Context = ctx\n}\n\n// WithHTTPClient adds the HTTPClient to the post alerts params\nfunc (o *PostAlertsParams) WithHTTPClient(client *http.Client) *PostAlertsParams {\n\to.SetHTTPClient(client)\n\treturn o\n}\n\n// SetHTTPClient adds the HTTPClient to the post alerts params\nfunc (o *PostAlertsParams) SetHTTPClient(client *http.Client) {\n\to.HTTPClient = client\n}\n\n// WithAlerts adds the alerts to the post alerts params\nfunc (o *PostAlertsParams) WithAlerts(alerts models.PostableAlerts) *PostAlertsParams {\n\to.SetAlerts(alerts)\n\treturn o\n}\n\n// SetAlerts adds the alerts to the post alerts params\nfunc (o *PostAlertsParams) SetAlerts(alerts models.PostableAlerts) {\n\to.Alerts = alerts\n}\n\n// WriteToRequest writes these params to a swagger request\nfunc (o *PostAlertsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {\n\n\tif err := r.SetTimeout(o.timeout); err != nil {\n\t\treturn err\n\t}\n\tvar res []error\n\tif o.Alerts != nil {\n\t\tif err := r.SetBodyParam(o.Alerts); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/client/alert/post_alerts_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alert\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"encoding/json\"\n\tstderrors \"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/go-openapi/runtime\"\n\t\"github.com/go-openapi/strfmt\"\n)\n\n// PostAlertsReader is a Reader for the PostAlerts structure.\ntype PostAlertsReader struct {\n\tformats strfmt.Registry\n}\n\n// ReadResponse reads a server response into the received o.\nfunc (o *PostAlertsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {\n\tswitch response.Code() {\n\tcase 200:\n\t\tresult := NewPostAlertsOK()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn result, nil\n\tcase 400:\n\t\tresult := NewPostAlertsBadRequest()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, result\n\tcase 500:\n\t\tresult := NewPostAlertsInternalServerError()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, result\n\tdefault:\n\t\treturn nil, runtime.NewAPIError(\"[POST /alerts] postAlerts\", response, response.Code())\n\t}\n}\n\n// NewPostAlertsOK creates a PostAlertsOK with default headers values\nfunc NewPostAlertsOK() *PostAlertsOK {\n\treturn &PostAlertsOK{}\n}\n\n/*\nPostAlertsOK describes a response with status code 200, with default header values.\n\nCreate alerts response\n*/\ntype PostAlertsOK struct {\n}\n\n// IsSuccess returns true when this post alerts o k response has a 2xx status code\nfunc (o *PostAlertsOK) IsSuccess() bool {\n\treturn true\n}\n\n// IsRedirect returns true when this post alerts o k response has a 3xx status code\nfunc (o *PostAlertsOK) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this post alerts o k response has a 4xx status code\nfunc (o *PostAlertsOK) IsClientError() bool {\n\treturn false\n}\n\n// IsServerError returns true when this post alerts o k response has a 5xx status code\nfunc (o *PostAlertsOK) IsServerError() bool {\n\treturn false\n}\n\n// IsCode returns true when this post alerts o k response a status code equal to that given\nfunc (o *PostAlertsOK) IsCode(code int) bool {\n\treturn code == 200\n}\n\n// Code gets the status code for the post alerts o k response\nfunc (o *PostAlertsOK) Code() int {\n\treturn 200\n}\n\nfunc (o *PostAlertsOK) Error() string {\n\treturn fmt.Sprintf(\"[POST /alerts][%d] postAlertsOK\", 200)\n}\n\nfunc (o *PostAlertsOK) String() string {\n\treturn fmt.Sprintf(\"[POST /alerts][%d] postAlertsOK\", 200)\n}\n\nfunc (o *PostAlertsOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\treturn nil\n}\n\n// NewPostAlertsBadRequest creates a PostAlertsBadRequest with default headers values\nfunc NewPostAlertsBadRequest() *PostAlertsBadRequest {\n\treturn &PostAlertsBadRequest{}\n}\n\n/*\nPostAlertsBadRequest describes a response with status code 400, with default header values.\n\nBad request\n*/\ntype PostAlertsBadRequest struct {\n\tPayload string\n}\n\n// IsSuccess returns true when this post alerts bad request response has a 2xx status code\nfunc (o *PostAlertsBadRequest) IsSuccess() bool {\n\treturn false\n}\n\n// IsRedirect returns true when this post alerts bad request response has a 3xx status code\nfunc (o *PostAlertsBadRequest) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this post alerts bad request response has a 4xx status code\nfunc (o *PostAlertsBadRequest) IsClientError() bool {\n\treturn true\n}\n\n// IsServerError returns true when this post alerts bad request response has a 5xx status code\nfunc (o *PostAlertsBadRequest) IsServerError() bool {\n\treturn false\n}\n\n// IsCode returns true when this post alerts bad request response a status code equal to that given\nfunc (o *PostAlertsBadRequest) IsCode(code int) bool {\n\treturn code == 400\n}\n\n// Code gets the status code for the post alerts bad request response\nfunc (o *PostAlertsBadRequest) Code() int {\n\treturn 400\n}\n\nfunc (o *PostAlertsBadRequest) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[POST /alerts][%d] postAlertsBadRequest %s\", 400, payload)\n}\n\nfunc (o *PostAlertsBadRequest) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[POST /alerts][%d] postAlertsBadRequest %s\", 400, payload)\n}\n\nfunc (o *PostAlertsBadRequest) GetPayload() string {\n\treturn o.Payload\n}\n\nfunc (o *PostAlertsBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// NewPostAlertsInternalServerError creates a PostAlertsInternalServerError with default headers values\nfunc NewPostAlertsInternalServerError() *PostAlertsInternalServerError {\n\treturn &PostAlertsInternalServerError{}\n}\n\n/*\nPostAlertsInternalServerError describes a response with status code 500, with default header values.\n\nInternal server error\n*/\ntype PostAlertsInternalServerError struct {\n\tPayload string\n}\n\n// IsSuccess returns true when this post alerts internal server error response has a 2xx status code\nfunc (o *PostAlertsInternalServerError) IsSuccess() bool {\n\treturn false\n}\n\n// IsRedirect returns true when this post alerts internal server error response has a 3xx status code\nfunc (o *PostAlertsInternalServerError) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this post alerts internal server error response has a 4xx status code\nfunc (o *PostAlertsInternalServerError) IsClientError() bool {\n\treturn false\n}\n\n// IsServerError returns true when this post alerts internal server error response has a 5xx status code\nfunc (o *PostAlertsInternalServerError) IsServerError() bool {\n\treturn true\n}\n\n// IsCode returns true when this post alerts internal server error response a status code equal to that given\nfunc (o *PostAlertsInternalServerError) IsCode(code int) bool {\n\treturn code == 500\n}\n\n// Code gets the status code for the post alerts internal server error response\nfunc (o *PostAlertsInternalServerError) Code() int {\n\treturn 500\n}\n\nfunc (o *PostAlertsInternalServerError) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[POST /alerts][%d] postAlertsInternalServerError %s\", 500, payload)\n}\n\nfunc (o *PostAlertsInternalServerError) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[POST /alerts][%d] postAlertsInternalServerError %s\", 500, payload)\n}\n\nfunc (o *PostAlertsInternalServerError) GetPayload() string {\n\treturn o.Payload\n}\n\nfunc (o *PostAlertsInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/client/alertgroup/alertgroup_client.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alertgroup\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-openapi/runtime\"\n\thttptransport \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n)\n\n// New creates a new alertgroup API client.\nfunc New(transport runtime.ClientTransport, formats strfmt.Registry) ClientService {\n\treturn &Client{transport: transport, formats: formats}\n}\n\n// New creates a new alertgroup API client with basic auth credentials.\n// It takes the following parameters:\n// - host: http host (github.com).\n// - basePath: any base path for the API client (\"/v1\", \"/v3\").\n// - scheme: http scheme (\"http\", \"https\").\n// - user: user for basic authentication header.\n// - password: password for basic authentication header.\nfunc NewClientWithBasicAuth(host, basePath, scheme, user, password string) ClientService {\n\ttransport := httptransport.New(host, basePath, []string{scheme})\n\ttransport.DefaultAuthentication = httptransport.BasicAuth(user, password)\n\treturn &Client{transport: transport, formats: strfmt.Default}\n}\n\n// New creates a new alertgroup API client with a bearer token for authentication.\n// It takes the following parameters:\n// - host: http host (github.com).\n// - basePath: any base path for the API client (\"/v1\", \"/v3\").\n// - scheme: http scheme (\"http\", \"https\").\n// - bearerToken: bearer token for Bearer authentication header.\nfunc NewClientWithBearerToken(host, basePath, scheme, bearerToken string) ClientService {\n\ttransport := httptransport.New(host, basePath, []string{scheme})\n\ttransport.DefaultAuthentication = httptransport.BearerToken(bearerToken)\n\treturn &Client{transport: transport, formats: strfmt.Default}\n}\n\n/*\nClient for alertgroup API\n*/\ntype Client struct {\n\ttransport runtime.ClientTransport\n\tformats   strfmt.Registry\n}\n\n// ClientOption may be used to customize the behavior of Client methods.\ntype ClientOption func(*runtime.ClientOperation)\n\n// ClientService is the interface for Client methods\ntype ClientService interface {\n\tGetAlertGroups(params *GetAlertGroupsParams, opts ...ClientOption) (*GetAlertGroupsOK, error)\n\n\tSetTransport(transport runtime.ClientTransport)\n}\n\n/*\nGetAlertGroups Get a list of alert groups\n*/\nfunc (a *Client) GetAlertGroups(params *GetAlertGroupsParams, opts ...ClientOption) (*GetAlertGroupsOK, error) {\n\t// NOTE: parameters are not validated before sending\n\tif params == nil {\n\t\tparams = NewGetAlertGroupsParams()\n\t}\n\top := &runtime.ClientOperation{\n\t\tID:                 \"getAlertGroups\",\n\t\tMethod:             \"GET\",\n\t\tPathPattern:        \"/alerts/groups\",\n\t\tProducesMediaTypes: []string{\"application/json\"},\n\t\tConsumesMediaTypes: []string{\"application/json\"},\n\t\tSchemes:            []string{\"http\"},\n\t\tParams:             params,\n\t\tReader:             &GetAlertGroupsReader{formats: a.formats},\n\t\tContext:            params.Context,\n\t\tClient:             params.HTTPClient,\n\t}\n\tfor _, opt := range opts {\n\t\topt(op)\n\t}\n\tresult, err := a.transport.Submit(op)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// only one success response has to be checked\n\tsuccess, ok := result.(*GetAlertGroupsOK)\n\tif ok {\n\t\treturn success, nil\n\t}\n\n\t// unexpected success response.\n\n\t// no default response is defined.\n\t//\n\t// safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue\n\tmsg := fmt.Sprintf(\"unexpected success response for getAlertGroups: API contract not enforced by server. Client expected to get an error, but got: %T\", result)\n\tpanic(msg)\n}\n\n// SetTransport changes the transport on the client\nfunc (a *Client) SetTransport(transport runtime.ClientTransport) {\n\ta.transport = transport\n}\n"
  },
  {
    "path": "api/v2/client/alertgroup/get_alert_groups_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alertgroup\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime\"\n\tcr \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n)\n\n// NewGetAlertGroupsParams creates a new GetAlertGroupsParams object,\n// with the default timeout for this client.\n//\n// Default values are not hydrated, since defaults are normally applied by the API server side.\n//\n// To enforce default values in parameter, use SetDefaults or WithDefaults.\nfunc NewGetAlertGroupsParams() *GetAlertGroupsParams {\n\treturn &GetAlertGroupsParams{\n\t\ttimeout: cr.DefaultTimeout,\n\t}\n}\n\n// NewGetAlertGroupsParamsWithTimeout creates a new GetAlertGroupsParams object\n// with the ability to set a timeout on a request.\nfunc NewGetAlertGroupsParamsWithTimeout(timeout time.Duration) *GetAlertGroupsParams {\n\treturn &GetAlertGroupsParams{\n\t\ttimeout: timeout,\n\t}\n}\n\n// NewGetAlertGroupsParamsWithContext creates a new GetAlertGroupsParams object\n// with the ability to set a context for a request.\nfunc NewGetAlertGroupsParamsWithContext(ctx context.Context) *GetAlertGroupsParams {\n\treturn &GetAlertGroupsParams{\n\t\tContext: ctx,\n\t}\n}\n\n// NewGetAlertGroupsParamsWithHTTPClient creates a new GetAlertGroupsParams object\n// with the ability to set a custom HTTPClient for a request.\nfunc NewGetAlertGroupsParamsWithHTTPClient(client *http.Client) *GetAlertGroupsParams {\n\treturn &GetAlertGroupsParams{\n\t\tHTTPClient: client,\n\t}\n}\n\n/*\nGetAlertGroupsParams contains all the parameters to send to the API endpoint\n\n\tfor the get alert groups operation.\n\n\tTypically these are written to a http.Request.\n*/\ntype GetAlertGroupsParams struct {\n\n\t/* Active.\n\n\t   Include active alerts within the returned groups. If false, excludes active alerts from groups and only shows suppressed (silenced or inhibited) alerts.\n\n\t   Default: true\n\t*/\n\tActive *bool\n\n\t/* Filter.\n\n\t   A matcher expression to filter alert groups. For example `alertname=\"MyAlert\"`. It can be repeated to apply multiple matchers.\n\t*/\n\tFilter []string\n\n\t/* Inhibited.\n\n\t   Include inhibited alerts within the returned groups. If false, excludes inhibited alerts from groups. Note that true (default) shows both inhibited and non-inhibited alerts.\n\n\t   Default: true\n\t*/\n\tInhibited *bool\n\n\t/* Muted.\n\n\t   Include muted (silenced or inhibited) alert groups in results. If false, excludes entire groups where all alerts are muted.\n\n\t   Default: true\n\t*/\n\tMuted *bool\n\n\t/* Receiver.\n\n\t   A regex matching receivers to filter alerts by\n\t*/\n\tReceiver *string\n\n\t/* Silenced.\n\n\t   Include silenced alerts within the returned groups. If false, excludes silenced alerts from groups. Note that true (default) shows both silenced and non-silenced alerts.\n\n\t   Default: true\n\t*/\n\tSilenced *bool\n\n\ttimeout    time.Duration\n\tContext    context.Context\n\tHTTPClient *http.Client\n}\n\n// WithDefaults hydrates default values in the get alert groups params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *GetAlertGroupsParams) WithDefaults() *GetAlertGroupsParams {\n\to.SetDefaults()\n\treturn o\n}\n\n// SetDefaults hydrates default values in the get alert groups params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *GetAlertGroupsParams) SetDefaults() {\n\tvar (\n\t\tactiveDefault = bool(true)\n\n\t\tinhibitedDefault = bool(true)\n\n\t\tmutedDefault = bool(true)\n\n\t\tsilencedDefault = bool(true)\n\t)\n\n\tval := GetAlertGroupsParams{\n\t\tActive:    &activeDefault,\n\t\tInhibited: &inhibitedDefault,\n\t\tMuted:     &mutedDefault,\n\t\tSilenced:  &silencedDefault,\n\t}\n\n\tval.timeout = o.timeout\n\tval.Context = o.Context\n\tval.HTTPClient = o.HTTPClient\n\t*o = val\n}\n\n// WithTimeout adds the timeout to the get alert groups params\nfunc (o *GetAlertGroupsParams) WithTimeout(timeout time.Duration) *GetAlertGroupsParams {\n\to.SetTimeout(timeout)\n\treturn o\n}\n\n// SetTimeout adds the timeout to the get alert groups params\nfunc (o *GetAlertGroupsParams) SetTimeout(timeout time.Duration) {\n\to.timeout = timeout\n}\n\n// WithContext adds the context to the get alert groups params\nfunc (o *GetAlertGroupsParams) WithContext(ctx context.Context) *GetAlertGroupsParams {\n\to.SetContext(ctx)\n\treturn o\n}\n\n// SetContext adds the context to the get alert groups params\nfunc (o *GetAlertGroupsParams) SetContext(ctx context.Context) {\n\to.Context = ctx\n}\n\n// WithHTTPClient adds the HTTPClient to the get alert groups params\nfunc (o *GetAlertGroupsParams) WithHTTPClient(client *http.Client) *GetAlertGroupsParams {\n\to.SetHTTPClient(client)\n\treturn o\n}\n\n// SetHTTPClient adds the HTTPClient to the get alert groups params\nfunc (o *GetAlertGroupsParams) SetHTTPClient(client *http.Client) {\n\to.HTTPClient = client\n}\n\n// WithActive adds the active to the get alert groups params\nfunc (o *GetAlertGroupsParams) WithActive(active *bool) *GetAlertGroupsParams {\n\to.SetActive(active)\n\treturn o\n}\n\n// SetActive adds the active to the get alert groups params\nfunc (o *GetAlertGroupsParams) SetActive(active *bool) {\n\to.Active = active\n}\n\n// WithFilter adds the filter to the get alert groups params\nfunc (o *GetAlertGroupsParams) WithFilter(filter []string) *GetAlertGroupsParams {\n\to.SetFilter(filter)\n\treturn o\n}\n\n// SetFilter adds the filter to the get alert groups params\nfunc (o *GetAlertGroupsParams) SetFilter(filter []string) {\n\to.Filter = filter\n}\n\n// WithInhibited adds the inhibited to the get alert groups params\nfunc (o *GetAlertGroupsParams) WithInhibited(inhibited *bool) *GetAlertGroupsParams {\n\to.SetInhibited(inhibited)\n\treturn o\n}\n\n// SetInhibited adds the inhibited to the get alert groups params\nfunc (o *GetAlertGroupsParams) SetInhibited(inhibited *bool) {\n\to.Inhibited = inhibited\n}\n\n// WithMuted adds the muted to the get alert groups params\nfunc (o *GetAlertGroupsParams) WithMuted(muted *bool) *GetAlertGroupsParams {\n\to.SetMuted(muted)\n\treturn o\n}\n\n// SetMuted adds the muted to the get alert groups params\nfunc (o *GetAlertGroupsParams) SetMuted(muted *bool) {\n\to.Muted = muted\n}\n\n// WithReceiver adds the receiver to the get alert groups params\nfunc (o *GetAlertGroupsParams) WithReceiver(receiver *string) *GetAlertGroupsParams {\n\to.SetReceiver(receiver)\n\treturn o\n}\n\n// SetReceiver adds the receiver to the get alert groups params\nfunc (o *GetAlertGroupsParams) SetReceiver(receiver *string) {\n\to.Receiver = receiver\n}\n\n// WithSilenced adds the silenced to the get alert groups params\nfunc (o *GetAlertGroupsParams) WithSilenced(silenced *bool) *GetAlertGroupsParams {\n\to.SetSilenced(silenced)\n\treturn o\n}\n\n// SetSilenced adds the silenced to the get alert groups params\nfunc (o *GetAlertGroupsParams) SetSilenced(silenced *bool) {\n\to.Silenced = silenced\n}\n\n// WriteToRequest writes these params to a swagger request\nfunc (o *GetAlertGroupsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {\n\n\tif err := r.SetTimeout(o.timeout); err != nil {\n\t\treturn err\n\t}\n\tvar res []error\n\n\tif o.Active != nil {\n\n\t\t// query param active\n\t\tvar qrActive bool\n\n\t\tif o.Active != nil {\n\t\t\tqrActive = *o.Active\n\t\t}\n\t\tqActive := swag.FormatBool(qrActive)\n\t\tif qActive != \"\" {\n\n\t\t\tif err := r.SetQueryParam(\"active\", qActive); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tif o.Filter != nil {\n\n\t\t// binding items for filter\n\t\tjoinedFilter := o.bindParamFilter(reg)\n\n\t\t// query array param filter\n\t\tif err := r.SetQueryParam(\"filter\", joinedFilter...); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif o.Inhibited != nil {\n\n\t\t// query param inhibited\n\t\tvar qrInhibited bool\n\n\t\tif o.Inhibited != nil {\n\t\t\tqrInhibited = *o.Inhibited\n\t\t}\n\t\tqInhibited := swag.FormatBool(qrInhibited)\n\t\tif qInhibited != \"\" {\n\n\t\t\tif err := r.SetQueryParam(\"inhibited\", qInhibited); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tif o.Muted != nil {\n\n\t\t// query param muted\n\t\tvar qrMuted bool\n\n\t\tif o.Muted != nil {\n\t\t\tqrMuted = *o.Muted\n\t\t}\n\t\tqMuted := swag.FormatBool(qrMuted)\n\t\tif qMuted != \"\" {\n\n\t\t\tif err := r.SetQueryParam(\"muted\", qMuted); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tif o.Receiver != nil {\n\n\t\t// query param receiver\n\t\tvar qrReceiver string\n\n\t\tif o.Receiver != nil {\n\t\t\tqrReceiver = *o.Receiver\n\t\t}\n\t\tqReceiver := qrReceiver\n\t\tif qReceiver != \"\" {\n\n\t\t\tif err := r.SetQueryParam(\"receiver\", qReceiver); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tif o.Silenced != nil {\n\n\t\t// query param silenced\n\t\tvar qrSilenced bool\n\n\t\tif o.Silenced != nil {\n\t\t\tqrSilenced = *o.Silenced\n\t\t}\n\t\tqSilenced := swag.FormatBool(qrSilenced)\n\t\tif qSilenced != \"\" {\n\n\t\t\tif err := r.SetQueryParam(\"silenced\", qSilenced); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\n// bindParamGetAlertGroups binds the parameter filter\nfunc (o *GetAlertGroupsParams) bindParamFilter(formats strfmt.Registry) []string {\n\tfilterIR := o.Filter\n\n\tvar filterIC []string\n\tfor _, filterIIR := range filterIR { // explode []string\n\n\t\tfilterIIV := filterIIR // string as string\n\t\tfilterIC = append(filterIC, filterIIV)\n\t}\n\n\t// items.CollectionFormat: \"multi\"\n\tfilterIS := swag.JoinByFormat(filterIC, \"multi\")\n\n\treturn filterIS\n}\n"
  },
  {
    "path": "api/v2/client/alertgroup/get_alert_groups_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alertgroup\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"encoding/json\"\n\tstderrors \"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/go-openapi/runtime\"\n\t\"github.com/go-openapi/strfmt\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\n// GetAlertGroupsReader is a Reader for the GetAlertGroups structure.\ntype GetAlertGroupsReader struct {\n\tformats strfmt.Registry\n}\n\n// ReadResponse reads a server response into the received o.\nfunc (o *GetAlertGroupsReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {\n\tswitch response.Code() {\n\tcase 200:\n\t\tresult := NewGetAlertGroupsOK()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn result, nil\n\tcase 400:\n\t\tresult := NewGetAlertGroupsBadRequest()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, result\n\tcase 500:\n\t\tresult := NewGetAlertGroupsInternalServerError()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, result\n\tdefault:\n\t\treturn nil, runtime.NewAPIError(\"[GET /alerts/groups] getAlertGroups\", response, response.Code())\n\t}\n}\n\n// NewGetAlertGroupsOK creates a GetAlertGroupsOK with default headers values\nfunc NewGetAlertGroupsOK() *GetAlertGroupsOK {\n\treturn &GetAlertGroupsOK{}\n}\n\n/*\nGetAlertGroupsOK describes a response with status code 200, with default header values.\n\nGet alert groups response\n*/\ntype GetAlertGroupsOK struct {\n\tPayload models.AlertGroups\n}\n\n// IsSuccess returns true when this get alert groups o k response has a 2xx status code\nfunc (o *GetAlertGroupsOK) IsSuccess() bool {\n\treturn true\n}\n\n// IsRedirect returns true when this get alert groups o k response has a 3xx status code\nfunc (o *GetAlertGroupsOK) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this get alert groups o k response has a 4xx status code\nfunc (o *GetAlertGroupsOK) IsClientError() bool {\n\treturn false\n}\n\n// IsServerError returns true when this get alert groups o k response has a 5xx status code\nfunc (o *GetAlertGroupsOK) IsServerError() bool {\n\treturn false\n}\n\n// IsCode returns true when this get alert groups o k response a status code equal to that given\nfunc (o *GetAlertGroupsOK) IsCode(code int) bool {\n\treturn code == 200\n}\n\n// Code gets the status code for the get alert groups o k response\nfunc (o *GetAlertGroupsOK) Code() int {\n\treturn 200\n}\n\nfunc (o *GetAlertGroupsOK) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /alerts/groups][%d] getAlertGroupsOK %s\", 200, payload)\n}\n\nfunc (o *GetAlertGroupsOK) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /alerts/groups][%d] getAlertGroupsOK %s\", 200, payload)\n}\n\nfunc (o *GetAlertGroupsOK) GetPayload() models.AlertGroups {\n\treturn o.Payload\n}\n\nfunc (o *GetAlertGroupsOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// NewGetAlertGroupsBadRequest creates a GetAlertGroupsBadRequest with default headers values\nfunc NewGetAlertGroupsBadRequest() *GetAlertGroupsBadRequest {\n\treturn &GetAlertGroupsBadRequest{}\n}\n\n/*\nGetAlertGroupsBadRequest describes a response with status code 400, with default header values.\n\nBad request\n*/\ntype GetAlertGroupsBadRequest struct {\n\tPayload string\n}\n\n// IsSuccess returns true when this get alert groups bad request response has a 2xx status code\nfunc (o *GetAlertGroupsBadRequest) IsSuccess() bool {\n\treturn false\n}\n\n// IsRedirect returns true when this get alert groups bad request response has a 3xx status code\nfunc (o *GetAlertGroupsBadRequest) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this get alert groups bad request response has a 4xx status code\nfunc (o *GetAlertGroupsBadRequest) IsClientError() bool {\n\treturn true\n}\n\n// IsServerError returns true when this get alert groups bad request response has a 5xx status code\nfunc (o *GetAlertGroupsBadRequest) IsServerError() bool {\n\treturn false\n}\n\n// IsCode returns true when this get alert groups bad request response a status code equal to that given\nfunc (o *GetAlertGroupsBadRequest) IsCode(code int) bool {\n\treturn code == 400\n}\n\n// Code gets the status code for the get alert groups bad request response\nfunc (o *GetAlertGroupsBadRequest) Code() int {\n\treturn 400\n}\n\nfunc (o *GetAlertGroupsBadRequest) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /alerts/groups][%d] getAlertGroupsBadRequest %s\", 400, payload)\n}\n\nfunc (o *GetAlertGroupsBadRequest) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /alerts/groups][%d] getAlertGroupsBadRequest %s\", 400, payload)\n}\n\nfunc (o *GetAlertGroupsBadRequest) GetPayload() string {\n\treturn o.Payload\n}\n\nfunc (o *GetAlertGroupsBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// NewGetAlertGroupsInternalServerError creates a GetAlertGroupsInternalServerError with default headers values\nfunc NewGetAlertGroupsInternalServerError() *GetAlertGroupsInternalServerError {\n\treturn &GetAlertGroupsInternalServerError{}\n}\n\n/*\nGetAlertGroupsInternalServerError describes a response with status code 500, with default header values.\n\nInternal server error\n*/\ntype GetAlertGroupsInternalServerError struct {\n\tPayload string\n}\n\n// IsSuccess returns true when this get alert groups internal server error response has a 2xx status code\nfunc (o *GetAlertGroupsInternalServerError) IsSuccess() bool {\n\treturn false\n}\n\n// IsRedirect returns true when this get alert groups internal server error response has a 3xx status code\nfunc (o *GetAlertGroupsInternalServerError) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this get alert groups internal server error response has a 4xx status code\nfunc (o *GetAlertGroupsInternalServerError) IsClientError() bool {\n\treturn false\n}\n\n// IsServerError returns true when this get alert groups internal server error response has a 5xx status code\nfunc (o *GetAlertGroupsInternalServerError) IsServerError() bool {\n\treturn true\n}\n\n// IsCode returns true when this get alert groups internal server error response a status code equal to that given\nfunc (o *GetAlertGroupsInternalServerError) IsCode(code int) bool {\n\treturn code == 500\n}\n\n// Code gets the status code for the get alert groups internal server error response\nfunc (o *GetAlertGroupsInternalServerError) Code() int {\n\treturn 500\n}\n\nfunc (o *GetAlertGroupsInternalServerError) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /alerts/groups][%d] getAlertGroupsInternalServerError %s\", 500, payload)\n}\n\nfunc (o *GetAlertGroupsInternalServerError) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /alerts/groups][%d] getAlertGroupsInternalServerError %s\", 500, payload)\n}\n\nfunc (o *GetAlertGroupsInternalServerError) GetPayload() string {\n\treturn o.Payload\n}\n\nfunc (o *GetAlertGroupsInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/client/alertmanager_api_client.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage client\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"github.com/go-openapi/runtime\"\n\thttptransport \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/client/alert\"\n\t\"github.com/prometheus/alertmanager/api/v2/client/alertgroup\"\n\t\"github.com/prometheus/alertmanager/api/v2/client/general\"\n\t\"github.com/prometheus/alertmanager/api/v2/client/receiver\"\n\t\"github.com/prometheus/alertmanager/api/v2/client/silence\"\n)\n\n// Default alertmanager API HTTP client.\nvar Default = NewHTTPClient(nil)\n\nconst (\n\t// DefaultHost is the default Host\n\t// found in Meta (info) section of spec file\n\tDefaultHost string = \"localhost\"\n\t// DefaultBasePath is the default BasePath\n\t// found in Meta (info) section of spec file\n\tDefaultBasePath string = \"/api/v2/\"\n)\n\n// DefaultSchemes are the default schemes found in Meta (info) section of spec file\nvar DefaultSchemes = []string{\"http\"}\n\n// NewHTTPClient creates a new alertmanager API HTTP client.\nfunc NewHTTPClient(formats strfmt.Registry) *AlertmanagerAPI {\n\treturn NewHTTPClientWithConfig(formats, nil)\n}\n\n// NewHTTPClientWithConfig creates a new alertmanager API HTTP client,\n// using a customizable transport config.\nfunc NewHTTPClientWithConfig(formats strfmt.Registry, cfg *TransportConfig) *AlertmanagerAPI {\n\t// ensure nullable parameters have default\n\tif cfg == nil {\n\t\tcfg = DefaultTransportConfig()\n\t}\n\n\t// create transport and client\n\ttransport := httptransport.New(cfg.Host, cfg.BasePath, cfg.Schemes)\n\treturn New(transport, formats)\n}\n\n// New creates a new alertmanager API client\nfunc New(transport runtime.ClientTransport, formats strfmt.Registry) *AlertmanagerAPI {\n\t// ensure nullable parameters have default\n\tif formats == nil {\n\t\tformats = strfmt.Default\n\t}\n\n\tcli := new(AlertmanagerAPI)\n\tcli.Transport = transport\n\tcli.Alert = alert.New(transport, formats)\n\tcli.Alertgroup = alertgroup.New(transport, formats)\n\tcli.General = general.New(transport, formats)\n\tcli.Receiver = receiver.New(transport, formats)\n\tcli.Silence = silence.New(transport, formats)\n\treturn cli\n}\n\n// DefaultTransportConfig creates a TransportConfig with the\n// default settings taken from the meta section of the spec file.\nfunc DefaultTransportConfig() *TransportConfig {\n\treturn &TransportConfig{\n\t\tHost:     DefaultHost,\n\t\tBasePath: DefaultBasePath,\n\t\tSchemes:  DefaultSchemes,\n\t}\n}\n\n// TransportConfig contains the transport related info,\n// found in the meta section of the spec file.\ntype TransportConfig struct {\n\tHost     string\n\tBasePath string\n\tSchemes  []string\n}\n\n// WithHost overrides the default host,\n// provided by the meta section of the spec file.\nfunc (cfg *TransportConfig) WithHost(host string) *TransportConfig {\n\tcfg.Host = host\n\treturn cfg\n}\n\n// WithBasePath overrides the default basePath,\n// provided by the meta section of the spec file.\nfunc (cfg *TransportConfig) WithBasePath(basePath string) *TransportConfig {\n\tcfg.BasePath = basePath\n\treturn cfg\n}\n\n// WithSchemes overrides the default schemes,\n// provided by the meta section of the spec file.\nfunc (cfg *TransportConfig) WithSchemes(schemes []string) *TransportConfig {\n\tcfg.Schemes = schemes\n\treturn cfg\n}\n\n// AlertmanagerAPI is a client for alertmanager API\ntype AlertmanagerAPI struct {\n\tAlert alert.ClientService\n\n\tAlertgroup alertgroup.ClientService\n\n\tGeneral general.ClientService\n\n\tReceiver receiver.ClientService\n\n\tSilence silence.ClientService\n\n\tTransport runtime.ClientTransport\n}\n\n// SetTransport changes the transport on the client and all its subresources\nfunc (c *AlertmanagerAPI) SetTransport(transport runtime.ClientTransport) {\n\tc.Transport = transport\n\tc.Alert.SetTransport(transport)\n\tc.Alertgroup.SetTransport(transport)\n\tc.General.SetTransport(transport)\n\tc.Receiver.SetTransport(transport)\n\tc.Silence.SetTransport(transport)\n}\n"
  },
  {
    "path": "api/v2/client/general/general_client.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage general\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-openapi/runtime\"\n\thttptransport \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n)\n\n// New creates a new general API client.\nfunc New(transport runtime.ClientTransport, formats strfmt.Registry) ClientService {\n\treturn &Client{transport: transport, formats: formats}\n}\n\n// New creates a new general API client with basic auth credentials.\n// It takes the following parameters:\n// - host: http host (github.com).\n// - basePath: any base path for the API client (\"/v1\", \"/v3\").\n// - scheme: http scheme (\"http\", \"https\").\n// - user: user for basic authentication header.\n// - password: password for basic authentication header.\nfunc NewClientWithBasicAuth(host, basePath, scheme, user, password string) ClientService {\n\ttransport := httptransport.New(host, basePath, []string{scheme})\n\ttransport.DefaultAuthentication = httptransport.BasicAuth(user, password)\n\treturn &Client{transport: transport, formats: strfmt.Default}\n}\n\n// New creates a new general API client with a bearer token for authentication.\n// It takes the following parameters:\n// - host: http host (github.com).\n// - basePath: any base path for the API client (\"/v1\", \"/v3\").\n// - scheme: http scheme (\"http\", \"https\").\n// - bearerToken: bearer token for Bearer authentication header.\nfunc NewClientWithBearerToken(host, basePath, scheme, bearerToken string) ClientService {\n\ttransport := httptransport.New(host, basePath, []string{scheme})\n\ttransport.DefaultAuthentication = httptransport.BearerToken(bearerToken)\n\treturn &Client{transport: transport, formats: strfmt.Default}\n}\n\n/*\nClient for general API\n*/\ntype Client struct {\n\ttransport runtime.ClientTransport\n\tformats   strfmt.Registry\n}\n\n// ClientOption may be used to customize the behavior of Client methods.\ntype ClientOption func(*runtime.ClientOperation)\n\n// ClientService is the interface for Client methods\ntype ClientService interface {\n\tGetStatus(params *GetStatusParams, opts ...ClientOption) (*GetStatusOK, error)\n\n\tSetTransport(transport runtime.ClientTransport)\n}\n\n/*\nGetStatus Get current status of an Alertmanager instance and its cluster\n*/\nfunc (a *Client) GetStatus(params *GetStatusParams, opts ...ClientOption) (*GetStatusOK, error) {\n\t// NOTE: parameters are not validated before sending\n\tif params == nil {\n\t\tparams = NewGetStatusParams()\n\t}\n\top := &runtime.ClientOperation{\n\t\tID:                 \"getStatus\",\n\t\tMethod:             \"GET\",\n\t\tPathPattern:        \"/status\",\n\t\tProducesMediaTypes: []string{\"application/json\"},\n\t\tConsumesMediaTypes: []string{\"application/json\"},\n\t\tSchemes:            []string{\"http\"},\n\t\tParams:             params,\n\t\tReader:             &GetStatusReader{formats: a.formats},\n\t\tContext:            params.Context,\n\t\tClient:             params.HTTPClient,\n\t}\n\tfor _, opt := range opts {\n\t\topt(op)\n\t}\n\tresult, err := a.transport.Submit(op)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// only one success response has to be checked\n\tsuccess, ok := result.(*GetStatusOK)\n\tif ok {\n\t\treturn success, nil\n\t}\n\n\t// unexpected success response.\n\n\t// no default response is defined.\n\t//\n\t// safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue\n\tmsg := fmt.Sprintf(\"unexpected success response for getStatus: API contract not enforced by server. Client expected to get an error, but got: %T\", result)\n\tpanic(msg)\n}\n\n// SetTransport changes the transport on the client\nfunc (a *Client) SetTransport(transport runtime.ClientTransport) {\n\ta.transport = transport\n}\n"
  },
  {
    "path": "api/v2/client/general/get_status_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage general\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime\"\n\tcr \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n)\n\n// NewGetStatusParams creates a new GetStatusParams object,\n// with the default timeout for this client.\n//\n// Default values are not hydrated, since defaults are normally applied by the API server side.\n//\n// To enforce default values in parameter, use SetDefaults or WithDefaults.\nfunc NewGetStatusParams() *GetStatusParams {\n\treturn &GetStatusParams{\n\t\ttimeout: cr.DefaultTimeout,\n\t}\n}\n\n// NewGetStatusParamsWithTimeout creates a new GetStatusParams object\n// with the ability to set a timeout on a request.\nfunc NewGetStatusParamsWithTimeout(timeout time.Duration) *GetStatusParams {\n\treturn &GetStatusParams{\n\t\ttimeout: timeout,\n\t}\n}\n\n// NewGetStatusParamsWithContext creates a new GetStatusParams object\n// with the ability to set a context for a request.\nfunc NewGetStatusParamsWithContext(ctx context.Context) *GetStatusParams {\n\treturn &GetStatusParams{\n\t\tContext: ctx,\n\t}\n}\n\n// NewGetStatusParamsWithHTTPClient creates a new GetStatusParams object\n// with the ability to set a custom HTTPClient for a request.\nfunc NewGetStatusParamsWithHTTPClient(client *http.Client) *GetStatusParams {\n\treturn &GetStatusParams{\n\t\tHTTPClient: client,\n\t}\n}\n\n/*\nGetStatusParams contains all the parameters to send to the API endpoint\n\n\tfor the get status operation.\n\n\tTypically these are written to a http.Request.\n*/\ntype GetStatusParams struct {\n\ttimeout    time.Duration\n\tContext    context.Context\n\tHTTPClient *http.Client\n}\n\n// WithDefaults hydrates default values in the get status params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *GetStatusParams) WithDefaults() *GetStatusParams {\n\to.SetDefaults()\n\treturn o\n}\n\n// SetDefaults hydrates default values in the get status params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *GetStatusParams) SetDefaults() {\n\t// no default values defined for this parameter\n}\n\n// WithTimeout adds the timeout to the get status params\nfunc (o *GetStatusParams) WithTimeout(timeout time.Duration) *GetStatusParams {\n\to.SetTimeout(timeout)\n\treturn o\n}\n\n// SetTimeout adds the timeout to the get status params\nfunc (o *GetStatusParams) SetTimeout(timeout time.Duration) {\n\to.timeout = timeout\n}\n\n// WithContext adds the context to the get status params\nfunc (o *GetStatusParams) WithContext(ctx context.Context) *GetStatusParams {\n\to.SetContext(ctx)\n\treturn o\n}\n\n// SetContext adds the context to the get status params\nfunc (o *GetStatusParams) SetContext(ctx context.Context) {\n\to.Context = ctx\n}\n\n// WithHTTPClient adds the HTTPClient to the get status params\nfunc (o *GetStatusParams) WithHTTPClient(client *http.Client) *GetStatusParams {\n\to.SetHTTPClient(client)\n\treturn o\n}\n\n// SetHTTPClient adds the HTTPClient to the get status params\nfunc (o *GetStatusParams) SetHTTPClient(client *http.Client) {\n\to.HTTPClient = client\n}\n\n// WriteToRequest writes these params to a swagger request\nfunc (o *GetStatusParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {\n\n\tif err := r.SetTimeout(o.timeout); err != nil {\n\t\treturn err\n\t}\n\tvar res []error\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/client/general/get_status_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage general\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"encoding/json\"\n\tstderrors \"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/go-openapi/runtime\"\n\t\"github.com/go-openapi/strfmt\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\n// GetStatusReader is a Reader for the GetStatus structure.\ntype GetStatusReader struct {\n\tformats strfmt.Registry\n}\n\n// ReadResponse reads a server response into the received o.\nfunc (o *GetStatusReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {\n\tswitch response.Code() {\n\tcase 200:\n\t\tresult := NewGetStatusOK()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn result, nil\n\tdefault:\n\t\treturn nil, runtime.NewAPIError(\"[GET /status] getStatus\", response, response.Code())\n\t}\n}\n\n// NewGetStatusOK creates a GetStatusOK with default headers values\nfunc NewGetStatusOK() *GetStatusOK {\n\treturn &GetStatusOK{}\n}\n\n/*\nGetStatusOK describes a response with status code 200, with default header values.\n\nGet status response\n*/\ntype GetStatusOK struct {\n\tPayload *models.AlertmanagerStatus\n}\n\n// IsSuccess returns true when this get status o k response has a 2xx status code\nfunc (o *GetStatusOK) IsSuccess() bool {\n\treturn true\n}\n\n// IsRedirect returns true when this get status o k response has a 3xx status code\nfunc (o *GetStatusOK) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this get status o k response has a 4xx status code\nfunc (o *GetStatusOK) IsClientError() bool {\n\treturn false\n}\n\n// IsServerError returns true when this get status o k response has a 5xx status code\nfunc (o *GetStatusOK) IsServerError() bool {\n\treturn false\n}\n\n// IsCode returns true when this get status o k response a status code equal to that given\nfunc (o *GetStatusOK) IsCode(code int) bool {\n\treturn code == 200\n}\n\n// Code gets the status code for the get status o k response\nfunc (o *GetStatusOK) Code() int {\n\treturn 200\n}\n\nfunc (o *GetStatusOK) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /status][%d] getStatusOK %s\", 200, payload)\n}\n\nfunc (o *GetStatusOK) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /status][%d] getStatusOK %s\", 200, payload)\n}\n\nfunc (o *GetStatusOK) GetPayload() *models.AlertmanagerStatus {\n\treturn o.Payload\n}\n\nfunc (o *GetStatusOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\to.Payload = new(models.AlertmanagerStatus)\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/client/receiver/get_receivers_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage receiver\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime\"\n\tcr \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n)\n\n// NewGetReceiversParams creates a new GetReceiversParams object,\n// with the default timeout for this client.\n//\n// Default values are not hydrated, since defaults are normally applied by the API server side.\n//\n// To enforce default values in parameter, use SetDefaults or WithDefaults.\nfunc NewGetReceiversParams() *GetReceiversParams {\n\treturn &GetReceiversParams{\n\t\ttimeout: cr.DefaultTimeout,\n\t}\n}\n\n// NewGetReceiversParamsWithTimeout creates a new GetReceiversParams object\n// with the ability to set a timeout on a request.\nfunc NewGetReceiversParamsWithTimeout(timeout time.Duration) *GetReceiversParams {\n\treturn &GetReceiversParams{\n\t\ttimeout: timeout,\n\t}\n}\n\n// NewGetReceiversParamsWithContext creates a new GetReceiversParams object\n// with the ability to set a context for a request.\nfunc NewGetReceiversParamsWithContext(ctx context.Context) *GetReceiversParams {\n\treturn &GetReceiversParams{\n\t\tContext: ctx,\n\t}\n}\n\n// NewGetReceiversParamsWithHTTPClient creates a new GetReceiversParams object\n// with the ability to set a custom HTTPClient for a request.\nfunc NewGetReceiversParamsWithHTTPClient(client *http.Client) *GetReceiversParams {\n\treturn &GetReceiversParams{\n\t\tHTTPClient: client,\n\t}\n}\n\n/*\nGetReceiversParams contains all the parameters to send to the API endpoint\n\n\tfor the get receivers operation.\n\n\tTypically these are written to a http.Request.\n*/\ntype GetReceiversParams struct {\n\ttimeout    time.Duration\n\tContext    context.Context\n\tHTTPClient *http.Client\n}\n\n// WithDefaults hydrates default values in the get receivers params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *GetReceiversParams) WithDefaults() *GetReceiversParams {\n\to.SetDefaults()\n\treturn o\n}\n\n// SetDefaults hydrates default values in the get receivers params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *GetReceiversParams) SetDefaults() {\n\t// no default values defined for this parameter\n}\n\n// WithTimeout adds the timeout to the get receivers params\nfunc (o *GetReceiversParams) WithTimeout(timeout time.Duration) *GetReceiversParams {\n\to.SetTimeout(timeout)\n\treturn o\n}\n\n// SetTimeout adds the timeout to the get receivers params\nfunc (o *GetReceiversParams) SetTimeout(timeout time.Duration) {\n\to.timeout = timeout\n}\n\n// WithContext adds the context to the get receivers params\nfunc (o *GetReceiversParams) WithContext(ctx context.Context) *GetReceiversParams {\n\to.SetContext(ctx)\n\treturn o\n}\n\n// SetContext adds the context to the get receivers params\nfunc (o *GetReceiversParams) SetContext(ctx context.Context) {\n\to.Context = ctx\n}\n\n// WithHTTPClient adds the HTTPClient to the get receivers params\nfunc (o *GetReceiversParams) WithHTTPClient(client *http.Client) *GetReceiversParams {\n\to.SetHTTPClient(client)\n\treturn o\n}\n\n// SetHTTPClient adds the HTTPClient to the get receivers params\nfunc (o *GetReceiversParams) SetHTTPClient(client *http.Client) {\n\to.HTTPClient = client\n}\n\n// WriteToRequest writes these params to a swagger request\nfunc (o *GetReceiversParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {\n\n\tif err := r.SetTimeout(o.timeout); err != nil {\n\t\treturn err\n\t}\n\tvar res []error\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/client/receiver/get_receivers_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage receiver\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"encoding/json\"\n\tstderrors \"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/go-openapi/runtime\"\n\t\"github.com/go-openapi/strfmt\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\n// GetReceiversReader is a Reader for the GetReceivers structure.\ntype GetReceiversReader struct {\n\tformats strfmt.Registry\n}\n\n// ReadResponse reads a server response into the received o.\nfunc (o *GetReceiversReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {\n\tswitch response.Code() {\n\tcase 200:\n\t\tresult := NewGetReceiversOK()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn result, nil\n\tdefault:\n\t\treturn nil, runtime.NewAPIError(\"[GET /receivers] getReceivers\", response, response.Code())\n\t}\n}\n\n// NewGetReceiversOK creates a GetReceiversOK with default headers values\nfunc NewGetReceiversOK() *GetReceiversOK {\n\treturn &GetReceiversOK{}\n}\n\n/*\nGetReceiversOK describes a response with status code 200, with default header values.\n\nGet receivers response\n*/\ntype GetReceiversOK struct {\n\tPayload []*models.Receiver\n}\n\n// IsSuccess returns true when this get receivers o k response has a 2xx status code\nfunc (o *GetReceiversOK) IsSuccess() bool {\n\treturn true\n}\n\n// IsRedirect returns true when this get receivers o k response has a 3xx status code\nfunc (o *GetReceiversOK) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this get receivers o k response has a 4xx status code\nfunc (o *GetReceiversOK) IsClientError() bool {\n\treturn false\n}\n\n// IsServerError returns true when this get receivers o k response has a 5xx status code\nfunc (o *GetReceiversOK) IsServerError() bool {\n\treturn false\n}\n\n// IsCode returns true when this get receivers o k response a status code equal to that given\nfunc (o *GetReceiversOK) IsCode(code int) bool {\n\treturn code == 200\n}\n\n// Code gets the status code for the get receivers o k response\nfunc (o *GetReceiversOK) Code() int {\n\treturn 200\n}\n\nfunc (o *GetReceiversOK) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /receivers][%d] getReceiversOK %s\", 200, payload)\n}\n\nfunc (o *GetReceiversOK) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /receivers][%d] getReceiversOK %s\", 200, payload)\n}\n\nfunc (o *GetReceiversOK) GetPayload() []*models.Receiver {\n\treturn o.Payload\n}\n\nfunc (o *GetReceiversOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/client/receiver/receiver_client.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage receiver\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-openapi/runtime\"\n\thttptransport \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n)\n\n// New creates a new receiver API client.\nfunc New(transport runtime.ClientTransport, formats strfmt.Registry) ClientService {\n\treturn &Client{transport: transport, formats: formats}\n}\n\n// New creates a new receiver API client with basic auth credentials.\n// It takes the following parameters:\n// - host: http host (github.com).\n// - basePath: any base path for the API client (\"/v1\", \"/v3\").\n// - scheme: http scheme (\"http\", \"https\").\n// - user: user for basic authentication header.\n// - password: password for basic authentication header.\nfunc NewClientWithBasicAuth(host, basePath, scheme, user, password string) ClientService {\n\ttransport := httptransport.New(host, basePath, []string{scheme})\n\ttransport.DefaultAuthentication = httptransport.BasicAuth(user, password)\n\treturn &Client{transport: transport, formats: strfmt.Default}\n}\n\n// New creates a new receiver API client with a bearer token for authentication.\n// It takes the following parameters:\n// - host: http host (github.com).\n// - basePath: any base path for the API client (\"/v1\", \"/v3\").\n// - scheme: http scheme (\"http\", \"https\").\n// - bearerToken: bearer token for Bearer authentication header.\nfunc NewClientWithBearerToken(host, basePath, scheme, bearerToken string) ClientService {\n\ttransport := httptransport.New(host, basePath, []string{scheme})\n\ttransport.DefaultAuthentication = httptransport.BearerToken(bearerToken)\n\treturn &Client{transport: transport, formats: strfmt.Default}\n}\n\n/*\nClient for receiver API\n*/\ntype Client struct {\n\ttransport runtime.ClientTransport\n\tformats   strfmt.Registry\n}\n\n// ClientOption may be used to customize the behavior of Client methods.\ntype ClientOption func(*runtime.ClientOperation)\n\n// ClientService is the interface for Client methods\ntype ClientService interface {\n\tGetReceivers(params *GetReceiversParams, opts ...ClientOption) (*GetReceiversOK, error)\n\n\tSetTransport(transport runtime.ClientTransport)\n}\n\n/*\nGetReceivers Get list of all receivers (name of notification integrations)\n*/\nfunc (a *Client) GetReceivers(params *GetReceiversParams, opts ...ClientOption) (*GetReceiversOK, error) {\n\t// NOTE: parameters are not validated before sending\n\tif params == nil {\n\t\tparams = NewGetReceiversParams()\n\t}\n\top := &runtime.ClientOperation{\n\t\tID:                 \"getReceivers\",\n\t\tMethod:             \"GET\",\n\t\tPathPattern:        \"/receivers\",\n\t\tProducesMediaTypes: []string{\"application/json\"},\n\t\tConsumesMediaTypes: []string{\"application/json\"},\n\t\tSchemes:            []string{\"http\"},\n\t\tParams:             params,\n\t\tReader:             &GetReceiversReader{formats: a.formats},\n\t\tContext:            params.Context,\n\t\tClient:             params.HTTPClient,\n\t}\n\tfor _, opt := range opts {\n\t\topt(op)\n\t}\n\tresult, err := a.transport.Submit(op)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// only one success response has to be checked\n\tsuccess, ok := result.(*GetReceiversOK)\n\tif ok {\n\t\treturn success, nil\n\t}\n\n\t// unexpected success response.\n\n\t// no default response is defined.\n\t//\n\t// safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue\n\tmsg := fmt.Sprintf(\"unexpected success response for getReceivers: API contract not enforced by server. Client expected to get an error, but got: %T\", result)\n\tpanic(msg)\n}\n\n// SetTransport changes the transport on the client\nfunc (a *Client) SetTransport(transport runtime.ClientTransport) {\n\ta.transport = transport\n}\n"
  },
  {
    "path": "api/v2/client/silence/delete_silence_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime\"\n\tcr \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n)\n\n// NewDeleteSilenceParams creates a new DeleteSilenceParams object,\n// with the default timeout for this client.\n//\n// Default values are not hydrated, since defaults are normally applied by the API server side.\n//\n// To enforce default values in parameter, use SetDefaults or WithDefaults.\nfunc NewDeleteSilenceParams() *DeleteSilenceParams {\n\treturn &DeleteSilenceParams{\n\t\ttimeout: cr.DefaultTimeout,\n\t}\n}\n\n// NewDeleteSilenceParamsWithTimeout creates a new DeleteSilenceParams object\n// with the ability to set a timeout on a request.\nfunc NewDeleteSilenceParamsWithTimeout(timeout time.Duration) *DeleteSilenceParams {\n\treturn &DeleteSilenceParams{\n\t\ttimeout: timeout,\n\t}\n}\n\n// NewDeleteSilenceParamsWithContext creates a new DeleteSilenceParams object\n// with the ability to set a context for a request.\nfunc NewDeleteSilenceParamsWithContext(ctx context.Context) *DeleteSilenceParams {\n\treturn &DeleteSilenceParams{\n\t\tContext: ctx,\n\t}\n}\n\n// NewDeleteSilenceParamsWithHTTPClient creates a new DeleteSilenceParams object\n// with the ability to set a custom HTTPClient for a request.\nfunc NewDeleteSilenceParamsWithHTTPClient(client *http.Client) *DeleteSilenceParams {\n\treturn &DeleteSilenceParams{\n\t\tHTTPClient: client,\n\t}\n}\n\n/*\nDeleteSilenceParams contains all the parameters to send to the API endpoint\n\n\tfor the delete silence operation.\n\n\tTypically these are written to a http.Request.\n*/\ntype DeleteSilenceParams struct {\n\n\t/* SilenceID.\n\n\t   ID of the silence to get\n\n\t   Format: uuid\n\t*/\n\tSilenceID strfmt.UUID\n\n\ttimeout    time.Duration\n\tContext    context.Context\n\tHTTPClient *http.Client\n}\n\n// WithDefaults hydrates default values in the delete silence params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *DeleteSilenceParams) WithDefaults() *DeleteSilenceParams {\n\to.SetDefaults()\n\treturn o\n}\n\n// SetDefaults hydrates default values in the delete silence params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *DeleteSilenceParams) SetDefaults() {\n\t// no default values defined for this parameter\n}\n\n// WithTimeout adds the timeout to the delete silence params\nfunc (o *DeleteSilenceParams) WithTimeout(timeout time.Duration) *DeleteSilenceParams {\n\to.SetTimeout(timeout)\n\treturn o\n}\n\n// SetTimeout adds the timeout to the delete silence params\nfunc (o *DeleteSilenceParams) SetTimeout(timeout time.Duration) {\n\to.timeout = timeout\n}\n\n// WithContext adds the context to the delete silence params\nfunc (o *DeleteSilenceParams) WithContext(ctx context.Context) *DeleteSilenceParams {\n\to.SetContext(ctx)\n\treturn o\n}\n\n// SetContext adds the context to the delete silence params\nfunc (o *DeleteSilenceParams) SetContext(ctx context.Context) {\n\to.Context = ctx\n}\n\n// WithHTTPClient adds the HTTPClient to the delete silence params\nfunc (o *DeleteSilenceParams) WithHTTPClient(client *http.Client) *DeleteSilenceParams {\n\to.SetHTTPClient(client)\n\treturn o\n}\n\n// SetHTTPClient adds the HTTPClient to the delete silence params\nfunc (o *DeleteSilenceParams) SetHTTPClient(client *http.Client) {\n\to.HTTPClient = client\n}\n\n// WithSilenceID adds the silenceID to the delete silence params\nfunc (o *DeleteSilenceParams) WithSilenceID(silenceID strfmt.UUID) *DeleteSilenceParams {\n\to.SetSilenceID(silenceID)\n\treturn o\n}\n\n// SetSilenceID adds the silenceId to the delete silence params\nfunc (o *DeleteSilenceParams) SetSilenceID(silenceID strfmt.UUID) {\n\to.SilenceID = silenceID\n}\n\n// WriteToRequest writes these params to a swagger request\nfunc (o *DeleteSilenceParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {\n\n\tif err := r.SetTimeout(o.timeout); err != nil {\n\t\treturn err\n\t}\n\tvar res []error\n\n\t// path param silenceID\n\tif err := r.SetPathParam(\"silenceID\", o.SilenceID.String()); err != nil {\n\t\treturn err\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/client/silence/delete_silence_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"encoding/json\"\n\tstderrors \"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/go-openapi/runtime\"\n\t\"github.com/go-openapi/strfmt\"\n)\n\n// DeleteSilenceReader is a Reader for the DeleteSilence structure.\ntype DeleteSilenceReader struct {\n\tformats strfmt.Registry\n}\n\n// ReadResponse reads a server response into the received o.\nfunc (o *DeleteSilenceReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {\n\tswitch response.Code() {\n\tcase 200:\n\t\tresult := NewDeleteSilenceOK()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn result, nil\n\tcase 404:\n\t\tresult := NewDeleteSilenceNotFound()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, result\n\tcase 500:\n\t\tresult := NewDeleteSilenceInternalServerError()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, result\n\tdefault:\n\t\treturn nil, runtime.NewAPIError(\"[DELETE /silence/{silenceID}] deleteSilence\", response, response.Code())\n\t}\n}\n\n// NewDeleteSilenceOK creates a DeleteSilenceOK with default headers values\nfunc NewDeleteSilenceOK() *DeleteSilenceOK {\n\treturn &DeleteSilenceOK{}\n}\n\n/*\nDeleteSilenceOK describes a response with status code 200, with default header values.\n\nDelete silence response\n*/\ntype DeleteSilenceOK struct {\n}\n\n// IsSuccess returns true when this delete silence o k response has a 2xx status code\nfunc (o *DeleteSilenceOK) IsSuccess() bool {\n\treturn true\n}\n\n// IsRedirect returns true when this delete silence o k response has a 3xx status code\nfunc (o *DeleteSilenceOK) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this delete silence o k response has a 4xx status code\nfunc (o *DeleteSilenceOK) IsClientError() bool {\n\treturn false\n}\n\n// IsServerError returns true when this delete silence o k response has a 5xx status code\nfunc (o *DeleteSilenceOK) IsServerError() bool {\n\treturn false\n}\n\n// IsCode returns true when this delete silence o k response a status code equal to that given\nfunc (o *DeleteSilenceOK) IsCode(code int) bool {\n\treturn code == 200\n}\n\n// Code gets the status code for the delete silence o k response\nfunc (o *DeleteSilenceOK) Code() int {\n\treturn 200\n}\n\nfunc (o *DeleteSilenceOK) Error() string {\n\treturn fmt.Sprintf(\"[DELETE /silence/{silenceID}][%d] deleteSilenceOK\", 200)\n}\n\nfunc (o *DeleteSilenceOK) String() string {\n\treturn fmt.Sprintf(\"[DELETE /silence/{silenceID}][%d] deleteSilenceOK\", 200)\n}\n\nfunc (o *DeleteSilenceOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\treturn nil\n}\n\n// NewDeleteSilenceNotFound creates a DeleteSilenceNotFound with default headers values\nfunc NewDeleteSilenceNotFound() *DeleteSilenceNotFound {\n\treturn &DeleteSilenceNotFound{}\n}\n\n/*\nDeleteSilenceNotFound describes a response with status code 404, with default header values.\n\nA silence with the specified ID was not found\n*/\ntype DeleteSilenceNotFound struct {\n}\n\n// IsSuccess returns true when this delete silence not found response has a 2xx status code\nfunc (o *DeleteSilenceNotFound) IsSuccess() bool {\n\treturn false\n}\n\n// IsRedirect returns true when this delete silence not found response has a 3xx status code\nfunc (o *DeleteSilenceNotFound) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this delete silence not found response has a 4xx status code\nfunc (o *DeleteSilenceNotFound) IsClientError() bool {\n\treturn true\n}\n\n// IsServerError returns true when this delete silence not found response has a 5xx status code\nfunc (o *DeleteSilenceNotFound) IsServerError() bool {\n\treturn false\n}\n\n// IsCode returns true when this delete silence not found response a status code equal to that given\nfunc (o *DeleteSilenceNotFound) IsCode(code int) bool {\n\treturn code == 404\n}\n\n// Code gets the status code for the delete silence not found response\nfunc (o *DeleteSilenceNotFound) Code() int {\n\treturn 404\n}\n\nfunc (o *DeleteSilenceNotFound) Error() string {\n\treturn fmt.Sprintf(\"[DELETE /silence/{silenceID}][%d] deleteSilenceNotFound\", 404)\n}\n\nfunc (o *DeleteSilenceNotFound) String() string {\n\treturn fmt.Sprintf(\"[DELETE /silence/{silenceID}][%d] deleteSilenceNotFound\", 404)\n}\n\nfunc (o *DeleteSilenceNotFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\treturn nil\n}\n\n// NewDeleteSilenceInternalServerError creates a DeleteSilenceInternalServerError with default headers values\nfunc NewDeleteSilenceInternalServerError() *DeleteSilenceInternalServerError {\n\treturn &DeleteSilenceInternalServerError{}\n}\n\n/*\nDeleteSilenceInternalServerError describes a response with status code 500, with default header values.\n\nInternal server error\n*/\ntype DeleteSilenceInternalServerError struct {\n\tPayload string\n}\n\n// IsSuccess returns true when this delete silence internal server error response has a 2xx status code\nfunc (o *DeleteSilenceInternalServerError) IsSuccess() bool {\n\treturn false\n}\n\n// IsRedirect returns true when this delete silence internal server error response has a 3xx status code\nfunc (o *DeleteSilenceInternalServerError) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this delete silence internal server error response has a 4xx status code\nfunc (o *DeleteSilenceInternalServerError) IsClientError() bool {\n\treturn false\n}\n\n// IsServerError returns true when this delete silence internal server error response has a 5xx status code\nfunc (o *DeleteSilenceInternalServerError) IsServerError() bool {\n\treturn true\n}\n\n// IsCode returns true when this delete silence internal server error response a status code equal to that given\nfunc (o *DeleteSilenceInternalServerError) IsCode(code int) bool {\n\treturn code == 500\n}\n\n// Code gets the status code for the delete silence internal server error response\nfunc (o *DeleteSilenceInternalServerError) Code() int {\n\treturn 500\n}\n\nfunc (o *DeleteSilenceInternalServerError) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[DELETE /silence/{silenceID}][%d] deleteSilenceInternalServerError %s\", 500, payload)\n}\n\nfunc (o *DeleteSilenceInternalServerError) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[DELETE /silence/{silenceID}][%d] deleteSilenceInternalServerError %s\", 500, payload)\n}\n\nfunc (o *DeleteSilenceInternalServerError) GetPayload() string {\n\treturn o.Payload\n}\n\nfunc (o *DeleteSilenceInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/client/silence/get_silence_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime\"\n\tcr \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n)\n\n// NewGetSilenceParams creates a new GetSilenceParams object,\n// with the default timeout for this client.\n//\n// Default values are not hydrated, since defaults are normally applied by the API server side.\n//\n// To enforce default values in parameter, use SetDefaults or WithDefaults.\nfunc NewGetSilenceParams() *GetSilenceParams {\n\treturn &GetSilenceParams{\n\t\ttimeout: cr.DefaultTimeout,\n\t}\n}\n\n// NewGetSilenceParamsWithTimeout creates a new GetSilenceParams object\n// with the ability to set a timeout on a request.\nfunc NewGetSilenceParamsWithTimeout(timeout time.Duration) *GetSilenceParams {\n\treturn &GetSilenceParams{\n\t\ttimeout: timeout,\n\t}\n}\n\n// NewGetSilenceParamsWithContext creates a new GetSilenceParams object\n// with the ability to set a context for a request.\nfunc NewGetSilenceParamsWithContext(ctx context.Context) *GetSilenceParams {\n\treturn &GetSilenceParams{\n\t\tContext: ctx,\n\t}\n}\n\n// NewGetSilenceParamsWithHTTPClient creates a new GetSilenceParams object\n// with the ability to set a custom HTTPClient for a request.\nfunc NewGetSilenceParamsWithHTTPClient(client *http.Client) *GetSilenceParams {\n\treturn &GetSilenceParams{\n\t\tHTTPClient: client,\n\t}\n}\n\n/*\nGetSilenceParams contains all the parameters to send to the API endpoint\n\n\tfor the get silence operation.\n\n\tTypically these are written to a http.Request.\n*/\ntype GetSilenceParams struct {\n\n\t/* SilenceID.\n\n\t   ID of the silence to get\n\n\t   Format: uuid\n\t*/\n\tSilenceID strfmt.UUID\n\n\ttimeout    time.Duration\n\tContext    context.Context\n\tHTTPClient *http.Client\n}\n\n// WithDefaults hydrates default values in the get silence params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *GetSilenceParams) WithDefaults() *GetSilenceParams {\n\to.SetDefaults()\n\treturn o\n}\n\n// SetDefaults hydrates default values in the get silence params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *GetSilenceParams) SetDefaults() {\n\t// no default values defined for this parameter\n}\n\n// WithTimeout adds the timeout to the get silence params\nfunc (o *GetSilenceParams) WithTimeout(timeout time.Duration) *GetSilenceParams {\n\to.SetTimeout(timeout)\n\treturn o\n}\n\n// SetTimeout adds the timeout to the get silence params\nfunc (o *GetSilenceParams) SetTimeout(timeout time.Duration) {\n\to.timeout = timeout\n}\n\n// WithContext adds the context to the get silence params\nfunc (o *GetSilenceParams) WithContext(ctx context.Context) *GetSilenceParams {\n\to.SetContext(ctx)\n\treturn o\n}\n\n// SetContext adds the context to the get silence params\nfunc (o *GetSilenceParams) SetContext(ctx context.Context) {\n\to.Context = ctx\n}\n\n// WithHTTPClient adds the HTTPClient to the get silence params\nfunc (o *GetSilenceParams) WithHTTPClient(client *http.Client) *GetSilenceParams {\n\to.SetHTTPClient(client)\n\treturn o\n}\n\n// SetHTTPClient adds the HTTPClient to the get silence params\nfunc (o *GetSilenceParams) SetHTTPClient(client *http.Client) {\n\to.HTTPClient = client\n}\n\n// WithSilenceID adds the silenceID to the get silence params\nfunc (o *GetSilenceParams) WithSilenceID(silenceID strfmt.UUID) *GetSilenceParams {\n\to.SetSilenceID(silenceID)\n\treturn o\n}\n\n// SetSilenceID adds the silenceId to the get silence params\nfunc (o *GetSilenceParams) SetSilenceID(silenceID strfmt.UUID) {\n\to.SilenceID = silenceID\n}\n\n// WriteToRequest writes these params to a swagger request\nfunc (o *GetSilenceParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {\n\n\tif err := r.SetTimeout(o.timeout); err != nil {\n\t\treturn err\n\t}\n\tvar res []error\n\n\t// path param silenceID\n\tif err := r.SetPathParam(\"silenceID\", o.SilenceID.String()); err != nil {\n\t\treturn err\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/client/silence/get_silence_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"encoding/json\"\n\tstderrors \"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/go-openapi/runtime\"\n\t\"github.com/go-openapi/strfmt\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\n// GetSilenceReader is a Reader for the GetSilence structure.\ntype GetSilenceReader struct {\n\tformats strfmt.Registry\n}\n\n// ReadResponse reads a server response into the received o.\nfunc (o *GetSilenceReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {\n\tswitch response.Code() {\n\tcase 200:\n\t\tresult := NewGetSilenceOK()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn result, nil\n\tcase 404:\n\t\tresult := NewGetSilenceNotFound()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, result\n\tcase 500:\n\t\tresult := NewGetSilenceInternalServerError()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, result\n\tdefault:\n\t\treturn nil, runtime.NewAPIError(\"[GET /silence/{silenceID}] getSilence\", response, response.Code())\n\t}\n}\n\n// NewGetSilenceOK creates a GetSilenceOK with default headers values\nfunc NewGetSilenceOK() *GetSilenceOK {\n\treturn &GetSilenceOK{}\n}\n\n/*\nGetSilenceOK describes a response with status code 200, with default header values.\n\nGet silence response\n*/\ntype GetSilenceOK struct {\n\tPayload *models.GettableSilence\n}\n\n// IsSuccess returns true when this get silence o k response has a 2xx status code\nfunc (o *GetSilenceOK) IsSuccess() bool {\n\treturn true\n}\n\n// IsRedirect returns true when this get silence o k response has a 3xx status code\nfunc (o *GetSilenceOK) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this get silence o k response has a 4xx status code\nfunc (o *GetSilenceOK) IsClientError() bool {\n\treturn false\n}\n\n// IsServerError returns true when this get silence o k response has a 5xx status code\nfunc (o *GetSilenceOK) IsServerError() bool {\n\treturn false\n}\n\n// IsCode returns true when this get silence o k response a status code equal to that given\nfunc (o *GetSilenceOK) IsCode(code int) bool {\n\treturn code == 200\n}\n\n// Code gets the status code for the get silence o k response\nfunc (o *GetSilenceOK) Code() int {\n\treturn 200\n}\n\nfunc (o *GetSilenceOK) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /silence/{silenceID}][%d] getSilenceOK %s\", 200, payload)\n}\n\nfunc (o *GetSilenceOK) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /silence/{silenceID}][%d] getSilenceOK %s\", 200, payload)\n}\n\nfunc (o *GetSilenceOK) GetPayload() *models.GettableSilence {\n\treturn o.Payload\n}\n\nfunc (o *GetSilenceOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\to.Payload = new(models.GettableSilence)\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// NewGetSilenceNotFound creates a GetSilenceNotFound with default headers values\nfunc NewGetSilenceNotFound() *GetSilenceNotFound {\n\treturn &GetSilenceNotFound{}\n}\n\n/*\nGetSilenceNotFound describes a response with status code 404, with default header values.\n\nA silence with the specified ID was not found\n*/\ntype GetSilenceNotFound struct {\n}\n\n// IsSuccess returns true when this get silence not found response has a 2xx status code\nfunc (o *GetSilenceNotFound) IsSuccess() bool {\n\treturn false\n}\n\n// IsRedirect returns true when this get silence not found response has a 3xx status code\nfunc (o *GetSilenceNotFound) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this get silence not found response has a 4xx status code\nfunc (o *GetSilenceNotFound) IsClientError() bool {\n\treturn true\n}\n\n// IsServerError returns true when this get silence not found response has a 5xx status code\nfunc (o *GetSilenceNotFound) IsServerError() bool {\n\treturn false\n}\n\n// IsCode returns true when this get silence not found response a status code equal to that given\nfunc (o *GetSilenceNotFound) IsCode(code int) bool {\n\treturn code == 404\n}\n\n// Code gets the status code for the get silence not found response\nfunc (o *GetSilenceNotFound) Code() int {\n\treturn 404\n}\n\nfunc (o *GetSilenceNotFound) Error() string {\n\treturn fmt.Sprintf(\"[GET /silence/{silenceID}][%d] getSilenceNotFound\", 404)\n}\n\nfunc (o *GetSilenceNotFound) String() string {\n\treturn fmt.Sprintf(\"[GET /silence/{silenceID}][%d] getSilenceNotFound\", 404)\n}\n\nfunc (o *GetSilenceNotFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\treturn nil\n}\n\n// NewGetSilenceInternalServerError creates a GetSilenceInternalServerError with default headers values\nfunc NewGetSilenceInternalServerError() *GetSilenceInternalServerError {\n\treturn &GetSilenceInternalServerError{}\n}\n\n/*\nGetSilenceInternalServerError describes a response with status code 500, with default header values.\n\nInternal server error\n*/\ntype GetSilenceInternalServerError struct {\n\tPayload string\n}\n\n// IsSuccess returns true when this get silence internal server error response has a 2xx status code\nfunc (o *GetSilenceInternalServerError) IsSuccess() bool {\n\treturn false\n}\n\n// IsRedirect returns true when this get silence internal server error response has a 3xx status code\nfunc (o *GetSilenceInternalServerError) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this get silence internal server error response has a 4xx status code\nfunc (o *GetSilenceInternalServerError) IsClientError() bool {\n\treturn false\n}\n\n// IsServerError returns true when this get silence internal server error response has a 5xx status code\nfunc (o *GetSilenceInternalServerError) IsServerError() bool {\n\treturn true\n}\n\n// IsCode returns true when this get silence internal server error response a status code equal to that given\nfunc (o *GetSilenceInternalServerError) IsCode(code int) bool {\n\treturn code == 500\n}\n\n// Code gets the status code for the get silence internal server error response\nfunc (o *GetSilenceInternalServerError) Code() int {\n\treturn 500\n}\n\nfunc (o *GetSilenceInternalServerError) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /silence/{silenceID}][%d] getSilenceInternalServerError %s\", 500, payload)\n}\n\nfunc (o *GetSilenceInternalServerError) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /silence/{silenceID}][%d] getSilenceInternalServerError %s\", 500, payload)\n}\n\nfunc (o *GetSilenceInternalServerError) GetPayload() string {\n\treturn o.Payload\n}\n\nfunc (o *GetSilenceInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/client/silence/get_silences_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime\"\n\tcr \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n)\n\n// NewGetSilencesParams creates a new GetSilencesParams object,\n// with the default timeout for this client.\n//\n// Default values are not hydrated, since defaults are normally applied by the API server side.\n//\n// To enforce default values in parameter, use SetDefaults or WithDefaults.\nfunc NewGetSilencesParams() *GetSilencesParams {\n\treturn &GetSilencesParams{\n\t\ttimeout: cr.DefaultTimeout,\n\t}\n}\n\n// NewGetSilencesParamsWithTimeout creates a new GetSilencesParams object\n// with the ability to set a timeout on a request.\nfunc NewGetSilencesParamsWithTimeout(timeout time.Duration) *GetSilencesParams {\n\treturn &GetSilencesParams{\n\t\ttimeout: timeout,\n\t}\n}\n\n// NewGetSilencesParamsWithContext creates a new GetSilencesParams object\n// with the ability to set a context for a request.\nfunc NewGetSilencesParamsWithContext(ctx context.Context) *GetSilencesParams {\n\treturn &GetSilencesParams{\n\t\tContext: ctx,\n\t}\n}\n\n// NewGetSilencesParamsWithHTTPClient creates a new GetSilencesParams object\n// with the ability to set a custom HTTPClient for a request.\nfunc NewGetSilencesParamsWithHTTPClient(client *http.Client) *GetSilencesParams {\n\treturn &GetSilencesParams{\n\t\tHTTPClient: client,\n\t}\n}\n\n/*\nGetSilencesParams contains all the parameters to send to the API endpoint\n\n\tfor the get silences operation.\n\n\tTypically these are written to a http.Request.\n*/\ntype GetSilencesParams struct {\n\n\t/* Filter.\n\n\t   A matcher expression to filter silences. For example `alertname=\"MyAlert\"`. It can be repeated to apply multiple matchers.\n\t*/\n\tFilter []string\n\n\ttimeout    time.Duration\n\tContext    context.Context\n\tHTTPClient *http.Client\n}\n\n// WithDefaults hydrates default values in the get silences params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *GetSilencesParams) WithDefaults() *GetSilencesParams {\n\to.SetDefaults()\n\treturn o\n}\n\n// SetDefaults hydrates default values in the get silences params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *GetSilencesParams) SetDefaults() {\n\t// no default values defined for this parameter\n}\n\n// WithTimeout adds the timeout to the get silences params\nfunc (o *GetSilencesParams) WithTimeout(timeout time.Duration) *GetSilencesParams {\n\to.SetTimeout(timeout)\n\treturn o\n}\n\n// SetTimeout adds the timeout to the get silences params\nfunc (o *GetSilencesParams) SetTimeout(timeout time.Duration) {\n\to.timeout = timeout\n}\n\n// WithContext adds the context to the get silences params\nfunc (o *GetSilencesParams) WithContext(ctx context.Context) *GetSilencesParams {\n\to.SetContext(ctx)\n\treturn o\n}\n\n// SetContext adds the context to the get silences params\nfunc (o *GetSilencesParams) SetContext(ctx context.Context) {\n\to.Context = ctx\n}\n\n// WithHTTPClient adds the HTTPClient to the get silences params\nfunc (o *GetSilencesParams) WithHTTPClient(client *http.Client) *GetSilencesParams {\n\to.SetHTTPClient(client)\n\treturn o\n}\n\n// SetHTTPClient adds the HTTPClient to the get silences params\nfunc (o *GetSilencesParams) SetHTTPClient(client *http.Client) {\n\to.HTTPClient = client\n}\n\n// WithFilter adds the filter to the get silences params\nfunc (o *GetSilencesParams) WithFilter(filter []string) *GetSilencesParams {\n\to.SetFilter(filter)\n\treturn o\n}\n\n// SetFilter adds the filter to the get silences params\nfunc (o *GetSilencesParams) SetFilter(filter []string) {\n\to.Filter = filter\n}\n\n// WriteToRequest writes these params to a swagger request\nfunc (o *GetSilencesParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {\n\n\tif err := r.SetTimeout(o.timeout); err != nil {\n\t\treturn err\n\t}\n\tvar res []error\n\n\tif o.Filter != nil {\n\n\t\t// binding items for filter\n\t\tjoinedFilter := o.bindParamFilter(reg)\n\n\t\t// query array param filter\n\t\tif err := r.SetQueryParam(\"filter\", joinedFilter...); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\n// bindParamGetSilences binds the parameter filter\nfunc (o *GetSilencesParams) bindParamFilter(formats strfmt.Registry) []string {\n\tfilterIR := o.Filter\n\n\tvar filterIC []string\n\tfor _, filterIIR := range filterIR { // explode []string\n\n\t\tfilterIIV := filterIIR // string as string\n\t\tfilterIC = append(filterIC, filterIIV)\n\t}\n\n\t// items.CollectionFormat: \"multi\"\n\tfilterIS := swag.JoinByFormat(filterIC, \"multi\")\n\n\treturn filterIS\n}\n"
  },
  {
    "path": "api/v2/client/silence/get_silences_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"encoding/json\"\n\tstderrors \"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/go-openapi/runtime\"\n\t\"github.com/go-openapi/strfmt\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\n// GetSilencesReader is a Reader for the GetSilences structure.\ntype GetSilencesReader struct {\n\tformats strfmt.Registry\n}\n\n// ReadResponse reads a server response into the received o.\nfunc (o *GetSilencesReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {\n\tswitch response.Code() {\n\tcase 200:\n\t\tresult := NewGetSilencesOK()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn result, nil\n\tcase 400:\n\t\tresult := NewGetSilencesBadRequest()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, result\n\tcase 500:\n\t\tresult := NewGetSilencesInternalServerError()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, result\n\tdefault:\n\t\treturn nil, runtime.NewAPIError(\"[GET /silences] getSilences\", response, response.Code())\n\t}\n}\n\n// NewGetSilencesOK creates a GetSilencesOK with default headers values\nfunc NewGetSilencesOK() *GetSilencesOK {\n\treturn &GetSilencesOK{}\n}\n\n/*\nGetSilencesOK describes a response with status code 200, with default header values.\n\nGet silences response\n*/\ntype GetSilencesOK struct {\n\tPayload models.GettableSilences\n}\n\n// IsSuccess returns true when this get silences o k response has a 2xx status code\nfunc (o *GetSilencesOK) IsSuccess() bool {\n\treturn true\n}\n\n// IsRedirect returns true when this get silences o k response has a 3xx status code\nfunc (o *GetSilencesOK) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this get silences o k response has a 4xx status code\nfunc (o *GetSilencesOK) IsClientError() bool {\n\treturn false\n}\n\n// IsServerError returns true when this get silences o k response has a 5xx status code\nfunc (o *GetSilencesOK) IsServerError() bool {\n\treturn false\n}\n\n// IsCode returns true when this get silences o k response a status code equal to that given\nfunc (o *GetSilencesOK) IsCode(code int) bool {\n\treturn code == 200\n}\n\n// Code gets the status code for the get silences o k response\nfunc (o *GetSilencesOK) Code() int {\n\treturn 200\n}\n\nfunc (o *GetSilencesOK) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /silences][%d] getSilencesOK %s\", 200, payload)\n}\n\nfunc (o *GetSilencesOK) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /silences][%d] getSilencesOK %s\", 200, payload)\n}\n\nfunc (o *GetSilencesOK) GetPayload() models.GettableSilences {\n\treturn o.Payload\n}\n\nfunc (o *GetSilencesOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// NewGetSilencesBadRequest creates a GetSilencesBadRequest with default headers values\nfunc NewGetSilencesBadRequest() *GetSilencesBadRequest {\n\treturn &GetSilencesBadRequest{}\n}\n\n/*\nGetSilencesBadRequest describes a response with status code 400, with default header values.\n\nBad request\n*/\ntype GetSilencesBadRequest struct {\n\tPayload string\n}\n\n// IsSuccess returns true when this get silences bad request response has a 2xx status code\nfunc (o *GetSilencesBadRequest) IsSuccess() bool {\n\treturn false\n}\n\n// IsRedirect returns true when this get silences bad request response has a 3xx status code\nfunc (o *GetSilencesBadRequest) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this get silences bad request response has a 4xx status code\nfunc (o *GetSilencesBadRequest) IsClientError() bool {\n\treturn true\n}\n\n// IsServerError returns true when this get silences bad request response has a 5xx status code\nfunc (o *GetSilencesBadRequest) IsServerError() bool {\n\treturn false\n}\n\n// IsCode returns true when this get silences bad request response a status code equal to that given\nfunc (o *GetSilencesBadRequest) IsCode(code int) bool {\n\treturn code == 400\n}\n\n// Code gets the status code for the get silences bad request response\nfunc (o *GetSilencesBadRequest) Code() int {\n\treturn 400\n}\n\nfunc (o *GetSilencesBadRequest) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /silences][%d] getSilencesBadRequest %s\", 400, payload)\n}\n\nfunc (o *GetSilencesBadRequest) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /silences][%d] getSilencesBadRequest %s\", 400, payload)\n}\n\nfunc (o *GetSilencesBadRequest) GetPayload() string {\n\treturn o.Payload\n}\n\nfunc (o *GetSilencesBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// NewGetSilencesInternalServerError creates a GetSilencesInternalServerError with default headers values\nfunc NewGetSilencesInternalServerError() *GetSilencesInternalServerError {\n\treturn &GetSilencesInternalServerError{}\n}\n\n/*\nGetSilencesInternalServerError describes a response with status code 500, with default header values.\n\nInternal server error\n*/\ntype GetSilencesInternalServerError struct {\n\tPayload string\n}\n\n// IsSuccess returns true when this get silences internal server error response has a 2xx status code\nfunc (o *GetSilencesInternalServerError) IsSuccess() bool {\n\treturn false\n}\n\n// IsRedirect returns true when this get silences internal server error response has a 3xx status code\nfunc (o *GetSilencesInternalServerError) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this get silences internal server error response has a 4xx status code\nfunc (o *GetSilencesInternalServerError) IsClientError() bool {\n\treturn false\n}\n\n// IsServerError returns true when this get silences internal server error response has a 5xx status code\nfunc (o *GetSilencesInternalServerError) IsServerError() bool {\n\treturn true\n}\n\n// IsCode returns true when this get silences internal server error response a status code equal to that given\nfunc (o *GetSilencesInternalServerError) IsCode(code int) bool {\n\treturn code == 500\n}\n\n// Code gets the status code for the get silences internal server error response\nfunc (o *GetSilencesInternalServerError) Code() int {\n\treturn 500\n}\n\nfunc (o *GetSilencesInternalServerError) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /silences][%d] getSilencesInternalServerError %s\", 500, payload)\n}\n\nfunc (o *GetSilencesInternalServerError) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[GET /silences][%d] getSilencesInternalServerError %s\", 500, payload)\n}\n\nfunc (o *GetSilencesInternalServerError) GetPayload() string {\n\treturn o.Payload\n}\n\nfunc (o *GetSilencesInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/client/silence/post_silences_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime\"\n\tcr \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\n// NewPostSilencesParams creates a new PostSilencesParams object,\n// with the default timeout for this client.\n//\n// Default values are not hydrated, since defaults are normally applied by the API server side.\n//\n// To enforce default values in parameter, use SetDefaults or WithDefaults.\nfunc NewPostSilencesParams() *PostSilencesParams {\n\treturn &PostSilencesParams{\n\t\ttimeout: cr.DefaultTimeout,\n\t}\n}\n\n// NewPostSilencesParamsWithTimeout creates a new PostSilencesParams object\n// with the ability to set a timeout on a request.\nfunc NewPostSilencesParamsWithTimeout(timeout time.Duration) *PostSilencesParams {\n\treturn &PostSilencesParams{\n\t\ttimeout: timeout,\n\t}\n}\n\n// NewPostSilencesParamsWithContext creates a new PostSilencesParams object\n// with the ability to set a context for a request.\nfunc NewPostSilencesParamsWithContext(ctx context.Context) *PostSilencesParams {\n\treturn &PostSilencesParams{\n\t\tContext: ctx,\n\t}\n}\n\n// NewPostSilencesParamsWithHTTPClient creates a new PostSilencesParams object\n// with the ability to set a custom HTTPClient for a request.\nfunc NewPostSilencesParamsWithHTTPClient(client *http.Client) *PostSilencesParams {\n\treturn &PostSilencesParams{\n\t\tHTTPClient: client,\n\t}\n}\n\n/*\nPostSilencesParams contains all the parameters to send to the API endpoint\n\n\tfor the post silences operation.\n\n\tTypically these are written to a http.Request.\n*/\ntype PostSilencesParams struct {\n\n\t/* Silence.\n\n\t   The silence to create\n\t*/\n\tSilence *models.PostableSilence\n\n\ttimeout    time.Duration\n\tContext    context.Context\n\tHTTPClient *http.Client\n}\n\n// WithDefaults hydrates default values in the post silences params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *PostSilencesParams) WithDefaults() *PostSilencesParams {\n\to.SetDefaults()\n\treturn o\n}\n\n// SetDefaults hydrates default values in the post silences params (not the query body).\n//\n// All values with no default are reset to their zero value.\nfunc (o *PostSilencesParams) SetDefaults() {\n\t// no default values defined for this parameter\n}\n\n// WithTimeout adds the timeout to the post silences params\nfunc (o *PostSilencesParams) WithTimeout(timeout time.Duration) *PostSilencesParams {\n\to.SetTimeout(timeout)\n\treturn o\n}\n\n// SetTimeout adds the timeout to the post silences params\nfunc (o *PostSilencesParams) SetTimeout(timeout time.Duration) {\n\to.timeout = timeout\n}\n\n// WithContext adds the context to the post silences params\nfunc (o *PostSilencesParams) WithContext(ctx context.Context) *PostSilencesParams {\n\to.SetContext(ctx)\n\treturn o\n}\n\n// SetContext adds the context to the post silences params\nfunc (o *PostSilencesParams) SetContext(ctx context.Context) {\n\to.Context = ctx\n}\n\n// WithHTTPClient adds the HTTPClient to the post silences params\nfunc (o *PostSilencesParams) WithHTTPClient(client *http.Client) *PostSilencesParams {\n\to.SetHTTPClient(client)\n\treturn o\n}\n\n// SetHTTPClient adds the HTTPClient to the post silences params\nfunc (o *PostSilencesParams) SetHTTPClient(client *http.Client) {\n\to.HTTPClient = client\n}\n\n// WithSilence adds the silence to the post silences params\nfunc (o *PostSilencesParams) WithSilence(silence *models.PostableSilence) *PostSilencesParams {\n\to.SetSilence(silence)\n\treturn o\n}\n\n// SetSilence adds the silence to the post silences params\nfunc (o *PostSilencesParams) SetSilence(silence *models.PostableSilence) {\n\to.Silence = silence\n}\n\n// WriteToRequest writes these params to a swagger request\nfunc (o *PostSilencesParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {\n\n\tif err := r.SetTimeout(o.timeout); err != nil {\n\t\treturn err\n\t}\n\tvar res []error\n\tif o.Silence != nil {\n\t\tif err := r.SetBodyParam(o.Silence); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/client/silence/post_silences_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\tstderrors \"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/go-openapi/runtime\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n)\n\n// PostSilencesReader is a Reader for the PostSilences structure.\ntype PostSilencesReader struct {\n\tformats strfmt.Registry\n}\n\n// ReadResponse reads a server response into the received o.\nfunc (o *PostSilencesReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {\n\tswitch response.Code() {\n\tcase 200:\n\t\tresult := NewPostSilencesOK()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn result, nil\n\tcase 400:\n\t\tresult := NewPostSilencesBadRequest()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, result\n\tcase 404:\n\t\tresult := NewPostSilencesNotFound()\n\t\tif err := result.readResponse(response, consumer, o.formats); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, result\n\tdefault:\n\t\treturn nil, runtime.NewAPIError(\"[POST /silences] postSilences\", response, response.Code())\n\t}\n}\n\n// NewPostSilencesOK creates a PostSilencesOK with default headers values\nfunc NewPostSilencesOK() *PostSilencesOK {\n\treturn &PostSilencesOK{}\n}\n\n/*\nPostSilencesOK describes a response with status code 200, with default header values.\n\nCreate / update silence response\n*/\ntype PostSilencesOK struct {\n\tPayload *PostSilencesOKBody\n}\n\n// IsSuccess returns true when this post silences o k response has a 2xx status code\nfunc (o *PostSilencesOK) IsSuccess() bool {\n\treturn true\n}\n\n// IsRedirect returns true when this post silences o k response has a 3xx status code\nfunc (o *PostSilencesOK) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this post silences o k response has a 4xx status code\nfunc (o *PostSilencesOK) IsClientError() bool {\n\treturn false\n}\n\n// IsServerError returns true when this post silences o k response has a 5xx status code\nfunc (o *PostSilencesOK) IsServerError() bool {\n\treturn false\n}\n\n// IsCode returns true when this post silences o k response a status code equal to that given\nfunc (o *PostSilencesOK) IsCode(code int) bool {\n\treturn code == 200\n}\n\n// Code gets the status code for the post silences o k response\nfunc (o *PostSilencesOK) Code() int {\n\treturn 200\n}\n\nfunc (o *PostSilencesOK) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[POST /silences][%d] postSilencesOK %s\", 200, payload)\n}\n\nfunc (o *PostSilencesOK) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[POST /silences][%d] postSilencesOK %s\", 200, payload)\n}\n\nfunc (o *PostSilencesOK) GetPayload() *PostSilencesOKBody {\n\treturn o.Payload\n}\n\nfunc (o *PostSilencesOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\to.Payload = new(PostSilencesOKBody)\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// NewPostSilencesBadRequest creates a PostSilencesBadRequest with default headers values\nfunc NewPostSilencesBadRequest() *PostSilencesBadRequest {\n\treturn &PostSilencesBadRequest{}\n}\n\n/*\nPostSilencesBadRequest describes a response with status code 400, with default header values.\n\nBad request\n*/\ntype PostSilencesBadRequest struct {\n\tPayload string\n}\n\n// IsSuccess returns true when this post silences bad request response has a 2xx status code\nfunc (o *PostSilencesBadRequest) IsSuccess() bool {\n\treturn false\n}\n\n// IsRedirect returns true when this post silences bad request response has a 3xx status code\nfunc (o *PostSilencesBadRequest) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this post silences bad request response has a 4xx status code\nfunc (o *PostSilencesBadRequest) IsClientError() bool {\n\treturn true\n}\n\n// IsServerError returns true when this post silences bad request response has a 5xx status code\nfunc (o *PostSilencesBadRequest) IsServerError() bool {\n\treturn false\n}\n\n// IsCode returns true when this post silences bad request response a status code equal to that given\nfunc (o *PostSilencesBadRequest) IsCode(code int) bool {\n\treturn code == 400\n}\n\n// Code gets the status code for the post silences bad request response\nfunc (o *PostSilencesBadRequest) Code() int {\n\treturn 400\n}\n\nfunc (o *PostSilencesBadRequest) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[POST /silences][%d] postSilencesBadRequest %s\", 400, payload)\n}\n\nfunc (o *PostSilencesBadRequest) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[POST /silences][%d] postSilencesBadRequest %s\", 400, payload)\n}\n\nfunc (o *PostSilencesBadRequest) GetPayload() string {\n\treturn o.Payload\n}\n\nfunc (o *PostSilencesBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// NewPostSilencesNotFound creates a PostSilencesNotFound with default headers values\nfunc NewPostSilencesNotFound() *PostSilencesNotFound {\n\treturn &PostSilencesNotFound{}\n}\n\n/*\nPostSilencesNotFound describes a response with status code 404, with default header values.\n\nA silence with the specified ID was not found\n*/\ntype PostSilencesNotFound struct {\n\tPayload string\n}\n\n// IsSuccess returns true when this post silences not found response has a 2xx status code\nfunc (o *PostSilencesNotFound) IsSuccess() bool {\n\treturn false\n}\n\n// IsRedirect returns true when this post silences not found response has a 3xx status code\nfunc (o *PostSilencesNotFound) IsRedirect() bool {\n\treturn false\n}\n\n// IsClientError returns true when this post silences not found response has a 4xx status code\nfunc (o *PostSilencesNotFound) IsClientError() bool {\n\treturn true\n}\n\n// IsServerError returns true when this post silences not found response has a 5xx status code\nfunc (o *PostSilencesNotFound) IsServerError() bool {\n\treturn false\n}\n\n// IsCode returns true when this post silences not found response a status code equal to that given\nfunc (o *PostSilencesNotFound) IsCode(code int) bool {\n\treturn code == 404\n}\n\n// Code gets the status code for the post silences not found response\nfunc (o *PostSilencesNotFound) Code() int {\n\treturn 404\n}\n\nfunc (o *PostSilencesNotFound) Error() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[POST /silences][%d] postSilencesNotFound %s\", 404, payload)\n}\n\nfunc (o *PostSilencesNotFound) String() string {\n\tpayload, _ := json.Marshal(o.Payload)\n\treturn fmt.Sprintf(\"[POST /silences][%d] postSilencesNotFound %s\", 404, payload)\n}\n\nfunc (o *PostSilencesNotFound) GetPayload() string {\n\treturn o.Payload\n}\n\nfunc (o *PostSilencesNotFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {\n\n\t// response payload\n\tif err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n/*\nPostSilencesOKBody post silences o k body\nswagger:model PostSilencesOKBody\n*/\ntype PostSilencesOKBody struct {\n\n\t// silence ID\n\tSilenceID string `json:\"silenceID,omitempty\"`\n}\n\n// Validate validates this post silences o k body\nfunc (o *PostSilencesOKBody) Validate(formats strfmt.Registry) error {\n\treturn nil\n}\n\n// ContextValidate validates this post silences o k body based on context it is used\nfunc (o *PostSilencesOKBody) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (o *PostSilencesOKBody) MarshalBinary() ([]byte, error) {\n\tif o == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(o)\n}\n\n// UnmarshalBinary interface implementation\nfunc (o *PostSilencesOKBody) UnmarshalBinary(b []byte) error {\n\tvar res PostSilencesOKBody\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*o = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/client/silence/silence_client.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/go-openapi/runtime\"\n\thttptransport \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n)\n\n// New creates a new silence API client.\nfunc New(transport runtime.ClientTransport, formats strfmt.Registry) ClientService {\n\treturn &Client{transport: transport, formats: formats}\n}\n\n// New creates a new silence API client with basic auth credentials.\n// It takes the following parameters:\n// - host: http host (github.com).\n// - basePath: any base path for the API client (\"/v1\", \"/v3\").\n// - scheme: http scheme (\"http\", \"https\").\n// - user: user for basic authentication header.\n// - password: password for basic authentication header.\nfunc NewClientWithBasicAuth(host, basePath, scheme, user, password string) ClientService {\n\ttransport := httptransport.New(host, basePath, []string{scheme})\n\ttransport.DefaultAuthentication = httptransport.BasicAuth(user, password)\n\treturn &Client{transport: transport, formats: strfmt.Default}\n}\n\n// New creates a new silence API client with a bearer token for authentication.\n// It takes the following parameters:\n// - host: http host (github.com).\n// - basePath: any base path for the API client (\"/v1\", \"/v3\").\n// - scheme: http scheme (\"http\", \"https\").\n// - bearerToken: bearer token for Bearer authentication header.\nfunc NewClientWithBearerToken(host, basePath, scheme, bearerToken string) ClientService {\n\ttransport := httptransport.New(host, basePath, []string{scheme})\n\ttransport.DefaultAuthentication = httptransport.BearerToken(bearerToken)\n\treturn &Client{transport: transport, formats: strfmt.Default}\n}\n\n/*\nClient for silence API\n*/\ntype Client struct {\n\ttransport runtime.ClientTransport\n\tformats   strfmt.Registry\n}\n\n// ClientOption may be used to customize the behavior of Client methods.\ntype ClientOption func(*runtime.ClientOperation)\n\n// ClientService is the interface for Client methods\ntype ClientService interface {\n\tDeleteSilence(params *DeleteSilenceParams, opts ...ClientOption) (*DeleteSilenceOK, error)\n\n\tGetSilence(params *GetSilenceParams, opts ...ClientOption) (*GetSilenceOK, error)\n\n\tGetSilences(params *GetSilencesParams, opts ...ClientOption) (*GetSilencesOK, error)\n\n\tPostSilences(params *PostSilencesParams, opts ...ClientOption) (*PostSilencesOK, error)\n\n\tSetTransport(transport runtime.ClientTransport)\n}\n\n/*\nDeleteSilence Delete a silence by its ID\n*/\nfunc (a *Client) DeleteSilence(params *DeleteSilenceParams, opts ...ClientOption) (*DeleteSilenceOK, error) {\n\t// NOTE: parameters are not validated before sending\n\tif params == nil {\n\t\tparams = NewDeleteSilenceParams()\n\t}\n\top := &runtime.ClientOperation{\n\t\tID:                 \"deleteSilence\",\n\t\tMethod:             \"DELETE\",\n\t\tPathPattern:        \"/silence/{silenceID}\",\n\t\tProducesMediaTypes: []string{\"application/json\"},\n\t\tConsumesMediaTypes: []string{\"application/json\"},\n\t\tSchemes:            []string{\"http\"},\n\t\tParams:             params,\n\t\tReader:             &DeleteSilenceReader{formats: a.formats},\n\t\tContext:            params.Context,\n\t\tClient:             params.HTTPClient,\n\t}\n\tfor _, opt := range opts {\n\t\topt(op)\n\t}\n\tresult, err := a.transport.Submit(op)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// only one success response has to be checked\n\tsuccess, ok := result.(*DeleteSilenceOK)\n\tif ok {\n\t\treturn success, nil\n\t}\n\n\t// unexpected success response.\n\n\t// no default response is defined.\n\t//\n\t// safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue\n\tmsg := fmt.Sprintf(\"unexpected success response for deleteSilence: API contract not enforced by server. Client expected to get an error, but got: %T\", result)\n\tpanic(msg)\n}\n\n/*\nGetSilence Get a silence by its ID\n*/\nfunc (a *Client) GetSilence(params *GetSilenceParams, opts ...ClientOption) (*GetSilenceOK, error) {\n\t// NOTE: parameters are not validated before sending\n\tif params == nil {\n\t\tparams = NewGetSilenceParams()\n\t}\n\top := &runtime.ClientOperation{\n\t\tID:                 \"getSilence\",\n\t\tMethod:             \"GET\",\n\t\tPathPattern:        \"/silence/{silenceID}\",\n\t\tProducesMediaTypes: []string{\"application/json\"},\n\t\tConsumesMediaTypes: []string{\"application/json\"},\n\t\tSchemes:            []string{\"http\"},\n\t\tParams:             params,\n\t\tReader:             &GetSilenceReader{formats: a.formats},\n\t\tContext:            params.Context,\n\t\tClient:             params.HTTPClient,\n\t}\n\tfor _, opt := range opts {\n\t\topt(op)\n\t}\n\tresult, err := a.transport.Submit(op)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// only one success response has to be checked\n\tsuccess, ok := result.(*GetSilenceOK)\n\tif ok {\n\t\treturn success, nil\n\t}\n\n\t// unexpected success response.\n\n\t// no default response is defined.\n\t//\n\t// safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue\n\tmsg := fmt.Sprintf(\"unexpected success response for getSilence: API contract not enforced by server. Client expected to get an error, but got: %T\", result)\n\tpanic(msg)\n}\n\n/*\nGetSilences Get a list of silences\n*/\nfunc (a *Client) GetSilences(params *GetSilencesParams, opts ...ClientOption) (*GetSilencesOK, error) {\n\t// NOTE: parameters are not validated before sending\n\tif params == nil {\n\t\tparams = NewGetSilencesParams()\n\t}\n\top := &runtime.ClientOperation{\n\t\tID:                 \"getSilences\",\n\t\tMethod:             \"GET\",\n\t\tPathPattern:        \"/silences\",\n\t\tProducesMediaTypes: []string{\"application/json\"},\n\t\tConsumesMediaTypes: []string{\"application/json\"},\n\t\tSchemes:            []string{\"http\"},\n\t\tParams:             params,\n\t\tReader:             &GetSilencesReader{formats: a.formats},\n\t\tContext:            params.Context,\n\t\tClient:             params.HTTPClient,\n\t}\n\tfor _, opt := range opts {\n\t\topt(op)\n\t}\n\tresult, err := a.transport.Submit(op)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// only one success response has to be checked\n\tsuccess, ok := result.(*GetSilencesOK)\n\tif ok {\n\t\treturn success, nil\n\t}\n\n\t// unexpected success response.\n\n\t// no default response is defined.\n\t//\n\t// safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue\n\tmsg := fmt.Sprintf(\"unexpected success response for getSilences: API contract not enforced by server. Client expected to get an error, but got: %T\", result)\n\tpanic(msg)\n}\n\n/*\nPostSilences Post a new silence or update an existing one\n*/\nfunc (a *Client) PostSilences(params *PostSilencesParams, opts ...ClientOption) (*PostSilencesOK, error) {\n\t// NOTE: parameters are not validated before sending\n\tif params == nil {\n\t\tparams = NewPostSilencesParams()\n\t}\n\top := &runtime.ClientOperation{\n\t\tID:                 \"postSilences\",\n\t\tMethod:             \"POST\",\n\t\tPathPattern:        \"/silences\",\n\t\tProducesMediaTypes: []string{\"application/json\"},\n\t\tConsumesMediaTypes: []string{\"application/json\"},\n\t\tSchemes:            []string{\"http\"},\n\t\tParams:             params,\n\t\tReader:             &PostSilencesReader{formats: a.formats},\n\t\tContext:            params.Context,\n\t\tClient:             params.HTTPClient,\n\t}\n\tfor _, opt := range opts {\n\t\topt(op)\n\t}\n\tresult, err := a.transport.Submit(op)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// only one success response has to be checked\n\tsuccess, ok := result.(*PostSilencesOK)\n\tif ok {\n\t\treturn success, nil\n\t}\n\n\t// unexpected success response.\n\n\t// no default response is defined.\n\t//\n\t// safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue\n\tmsg := fmt.Sprintf(\"unexpected success response for postSilences: API contract not enforced by server. Client expected to get an error, but got: %T\", result)\n\tpanic(msg)\n}\n\n// SetTransport changes the transport on the client\nfunc (a *Client) SetTransport(transport runtime.ClientTransport) {\n\ta.transport = transport\n}\n"
  },
  {
    "path": "api/v2/compat.go",
    "content": "// Copyright 2021 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage v2\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-openapi/strfmt\"\n\tprometheus_model \"github.com/prometheus/common/model\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\topen_api_models \"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/silence\"\n\t\"github.com/prometheus/alertmanager/silence/silencepb\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// GettableSilenceFromProto converts *silencepb.Silence to open_api_models.GettableSilence.\nfunc GettableSilenceFromProto(s *silencepb.Silence) (open_api_models.GettableSilence, error) {\n\tstart := strfmt.DateTime(s.StartsAt.AsTime())\n\tend := strfmt.DateTime(s.EndsAt.AsTime())\n\tupdated := strfmt.DateTime(s.UpdatedAt.AsTime())\n\tstate := string(silence.CurrentState(s.StartsAt.AsTime(), s.EndsAt.AsTime()))\n\tsil := open_api_models.GettableSilence{\n\t\tSilence: open_api_models.Silence{\n\t\t\tStartsAt:    &start,\n\t\t\tEndsAt:      &end,\n\t\t\tComment:     &s.Comment,\n\t\t\tCreatedBy:   &s.CreatedBy,\n\t\t\tAnnotations: s.Annotations,\n\t\t},\n\t\tID:        &s.Id,\n\t\tUpdatedAt: &updated,\n\t\tStatus: &open_api_models.SilenceStatus{\n\t\t\tState: &state,\n\t\t},\n\t}\n\n\t// For backward compatibility, only return silences with a single matcher set\n\tif len(s.MatcherSets) > 1 {\n\t\treturn sil, fmt.Errorf(\"silence '%v' has multiple matcher sets which is not supported by this API version\", s.Id)\n\t}\n\n\tif len(s.MatcherSets) == 0 {\n\t\treturn sil, nil\n\t}\n\n\tfor _, m := range s.MatcherSets[0].Matchers {\n\t\tmatcher := &open_api_models.Matcher{\n\t\t\tName:  &m.Name,\n\t\t\tValue: &m.Pattern,\n\t\t}\n\t\tf := false\n\t\tt := true\n\t\tswitch m.Type {\n\t\tcase silencepb.Matcher_EQUAL:\n\t\t\tmatcher.IsEqual = &t\n\t\t\tmatcher.IsRegex = &f\n\t\tcase silencepb.Matcher_NOT_EQUAL:\n\t\t\tmatcher.IsEqual = &f\n\t\t\tmatcher.IsRegex = &f\n\t\tcase silencepb.Matcher_REGEXP:\n\t\t\tmatcher.IsEqual = &t\n\t\t\tmatcher.IsRegex = &t\n\t\tcase silencepb.Matcher_NOT_REGEXP:\n\t\t\tmatcher.IsEqual = &f\n\t\t\tmatcher.IsRegex = &t\n\t\tdefault:\n\t\t\treturn sil, fmt.Errorf(\n\t\t\t\t\"unknown matcher type for matcher '%v' in silence '%v'\",\n\t\t\t\tm.Name,\n\t\t\t\ts.Id,\n\t\t\t)\n\t\t}\n\t\tsil.Matchers = append(sil.Matchers, matcher)\n\t}\n\n\treturn sil, nil\n}\n\n// PostableSilenceToProto converts *open_api_models.PostableSilenc to *silencepb.Silence.\nfunc PostableSilenceToProto(s *open_api_models.PostableSilence) (*silencepb.Silence, error) {\n\tsil := &silencepb.Silence{\n\t\tId:          s.ID,\n\t\tStartsAt:    timestamppb.New(time.Time(*s.StartsAt)),\n\t\tEndsAt:      timestamppb.New(time.Time(*s.EndsAt)),\n\t\tComment:     *s.Comment,\n\t\tCreatedBy:   *s.CreatedBy,\n\t\tAnnotations: map[string]string{},\n\t}\n\n\tmatcherSet := &silencepb.MatcherSet{}\n\tfor _, m := range s.Matchers {\n\t\tmatcher := &silencepb.Matcher{\n\t\t\tName:    *m.Name,\n\t\t\tPattern: *m.Value,\n\t\t}\n\t\tisEqual := true\n\t\tif m.IsEqual != nil {\n\t\t\tisEqual = *m.IsEqual\n\t\t}\n\t\tisRegex := false\n\t\tif m.IsRegex != nil {\n\t\t\tisRegex = *m.IsRegex\n\t\t}\n\n\t\tswitch {\n\t\tcase isEqual && !isRegex:\n\t\t\tmatcher.Type = silencepb.Matcher_EQUAL\n\t\tcase !isEqual && !isRegex:\n\t\t\tmatcher.Type = silencepb.Matcher_NOT_EQUAL\n\t\tcase isEqual && isRegex:\n\t\t\tmatcher.Type = silencepb.Matcher_REGEXP\n\t\tcase !isEqual && isRegex:\n\t\t\tmatcher.Type = silencepb.Matcher_NOT_REGEXP\n\t\t}\n\t\tmatcherSet.Matchers = append(matcherSet.Matchers, matcher)\n\t}\n\tsil.MatcherSets = append(sil.MatcherSets, matcherSet)\n\n\tif s.Annotations != nil {\n\t\tsil.Annotations = s.Annotations\n\t}\n\n\treturn sil, nil\n}\n\n// AlertToOpenAPIAlert converts internal alerts, alert types, and receivers to *open_api_models.GettableAlert.\nfunc AlertToOpenAPIAlert(alert *types.Alert, status types.AlertStatus, receivers, mutedBy []string) *open_api_models.GettableAlert {\n\tstartsAt := strfmt.DateTime(alert.StartsAt)\n\tupdatedAt := strfmt.DateTime(alert.UpdatedAt)\n\tendsAt := strfmt.DateTime(alert.EndsAt)\n\n\tapiReceivers := make([]*open_api_models.Receiver, 0, len(receivers))\n\tfor i := range receivers {\n\t\tapiReceivers = append(apiReceivers, &open_api_models.Receiver{Name: &receivers[i]})\n\t}\n\n\tfp := alert.Fingerprint().String()\n\n\tstate := string(status.State)\n\tif len(mutedBy) > 0 {\n\t\t// If the alert is muted, change the state to suppressed.\n\t\tstate = open_api_models.AlertStatusStateSuppressed\n\t}\n\n\taa := &open_api_models.GettableAlert{\n\t\tAlert: open_api_models.Alert{\n\t\t\tGeneratorURL: strfmt.URI(alert.GeneratorURL),\n\t\t\tLabels:       ModelLabelSetToAPILabelSet(alert.Labels),\n\t\t},\n\t\tAnnotations: ModelLabelSetToAPILabelSet(alert.Annotations),\n\t\tStartsAt:    &startsAt,\n\t\tUpdatedAt:   &updatedAt,\n\t\tEndsAt:      &endsAt,\n\t\tFingerprint: &fp,\n\t\tReceivers:   apiReceivers,\n\t\tStatus: &open_api_models.AlertStatus{\n\t\t\tState:       &state,\n\t\t\tSilencedBy:  status.SilencedBy,\n\t\t\tInhibitedBy: status.InhibitedBy,\n\t\t\tMutedBy:     mutedBy,\n\t\t},\n\t}\n\n\tif aa.Status.SilencedBy == nil {\n\t\taa.Status.SilencedBy = []string{}\n\t}\n\n\tif aa.Status.InhibitedBy == nil {\n\t\taa.Status.InhibitedBy = []string{}\n\t}\n\n\tif aa.Status.MutedBy == nil {\n\t\taa.Status.MutedBy = []string{}\n\t}\n\n\treturn aa\n}\n\n// OpenAPIAlertsToAlerts converts open_api_models.PostableAlerts to []*types.Alert.\nfunc OpenAPIAlertsToAlerts(ctx context.Context, apiAlerts open_api_models.PostableAlerts) []*types.Alert {\n\t_, span := tracer.Start(ctx, \"OpenAPIAlertsToAlerts\")\n\tdefer span.End()\n\n\talerts := make([]*types.Alert, 0, len(apiAlerts))\n\tfor _, apiAlert := range apiAlerts {\n\t\talerts = append(alerts, &types.Alert{\n\t\t\tAlert: prometheus_model.Alert{\n\t\t\t\tLabels:       APILabelSetToModelLabelSet(apiAlert.Labels),\n\t\t\t\tAnnotations:  APILabelSetToModelLabelSet(apiAlert.Annotations),\n\t\t\t\tStartsAt:     time.Time(apiAlert.StartsAt),\n\t\t\t\tEndsAt:       time.Time(apiAlert.EndsAt),\n\t\t\t\tGeneratorURL: string(apiAlert.GeneratorURL),\n\t\t\t},\n\t\t})\n\t}\n\n\treturn alerts\n}\n\n// ModelLabelSetToAPILabelSet converts prometheus_model.LabelSet to open_api_models.LabelSet.\nfunc ModelLabelSetToAPILabelSet(modelLabelSet prometheus_model.LabelSet) open_api_models.LabelSet {\n\tapiLabelSet := open_api_models.LabelSet{}\n\tfor key, value := range modelLabelSet {\n\t\tapiLabelSet[string(key)] = string(value)\n\t}\n\n\treturn apiLabelSet\n}\n\n// APILabelSetToModelLabelSet converts open_api_models.LabelSet to prometheus_model.LabelSet.\nfunc APILabelSetToModelLabelSet(apiLabelSet open_api_models.LabelSet) prometheus_model.LabelSet {\n\tmodelLabelSet := prometheus_model.LabelSet{}\n\tfor key, value := range apiLabelSet {\n\t\tmodelLabelSet[prometheus_model.LabelName(key)] = prometheus_model.LabelValue(value)\n\t}\n\n\treturn modelLabelSet\n}\n"
  },
  {
    "path": "api/v2/models/alert.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\tstderrors \"errors\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// Alert alert\n//\n// swagger:model alert\ntype Alert struct {\n\n\t// generator URL\n\t// Format: uri\n\tGeneratorURL strfmt.URI `json:\"generatorURL,omitempty\"`\n\n\t// labels\n\t// Required: true\n\tLabels LabelSet `json:\"labels\"`\n}\n\n// Validate validates this alert\nfunc (m *Alert) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.validateGeneratorURL(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateLabels(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *Alert) validateGeneratorURL(formats strfmt.Registry) error {\n\tif swag.IsZero(m.GeneratorURL) { // not required\n\t\treturn nil\n\t}\n\n\tif err := validate.FormatOf(\"generatorURL\", \"body\", \"uri\", m.GeneratorURL.String(), formats); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *Alert) validateLabels(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"labels\", \"body\", m.Labels); err != nil {\n\t\treturn err\n\t}\n\n\tif m.Labels != nil {\n\t\tif err := m.Labels.Validate(formats); err != nil {\n\t\t\tve := new(errors.Validation)\n\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\treturn ve.ValidateName(\"labels\")\n\t\t\t}\n\t\t\tce := new(errors.CompositeError)\n\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\treturn ce.ValidateName(\"labels\")\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// ContextValidate validate this alert based on the context it is used\nfunc (m *Alert) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.contextValidateLabels(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *Alert) contextValidateLabels(ctx context.Context, formats strfmt.Registry) error {\n\n\tif err := m.Labels.ContextValidate(ctx, formats); err != nil {\n\t\tve := new(errors.Validation)\n\t\tif stderrors.As(err, &ve) {\n\t\t\treturn ve.ValidateName(\"labels\")\n\t\t}\n\t\tce := new(errors.CompositeError)\n\t\tif stderrors.As(err, &ce) {\n\t\t\treturn ce.ValidateName(\"labels\")\n\t\t}\n\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (m *Alert) MarshalBinary() ([]byte, error) {\n\tif m == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(m)\n}\n\n// UnmarshalBinary interface implementation\nfunc (m *Alert) UnmarshalBinary(b []byte) error {\n\tvar res Alert\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*m = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/alert_group.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\tstderrors \"errors\"\n\t\"strconv\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// AlertGroup alert group\n//\n// swagger:model alertGroup\ntype AlertGroup struct {\n\n\t// alerts\n\t// Required: true\n\tAlerts []*GettableAlert `json:\"alerts\"`\n\n\t// labels\n\t// Required: true\n\tLabels LabelSet `json:\"labels\"`\n\n\t// receiver\n\t// Required: true\n\tReceiver *Receiver `json:\"receiver\"`\n}\n\n// Validate validates this alert group\nfunc (m *AlertGroup) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.validateAlerts(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateLabels(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateReceiver(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *AlertGroup) validateAlerts(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"alerts\", \"body\", m.Alerts); err != nil {\n\t\treturn err\n\t}\n\n\tfor i := 0; i < len(m.Alerts); i++ {\n\t\tif swag.IsZero(m.Alerts[i]) { // not required\n\t\t\tcontinue\n\t\t}\n\n\t\tif m.Alerts[i] != nil {\n\t\t\tif err := m.Alerts[i].Validate(formats); err != nil {\n\t\t\t\tve := new(errors.Validation)\n\t\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\t\treturn ve.ValidateName(\"alerts\" + \".\" + strconv.Itoa(i))\n\t\t\t\t}\n\t\t\t\tce := new(errors.CompositeError)\n\t\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\t\treturn ce.ValidateName(\"alerts\" + \".\" + strconv.Itoa(i))\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t}\n\n\treturn nil\n}\n\nfunc (m *AlertGroup) validateLabels(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"labels\", \"body\", m.Labels); err != nil {\n\t\treturn err\n\t}\n\n\tif m.Labels != nil {\n\t\tif err := m.Labels.Validate(formats); err != nil {\n\t\t\tve := new(errors.Validation)\n\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\treturn ve.ValidateName(\"labels\")\n\t\t\t}\n\t\t\tce := new(errors.CompositeError)\n\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\treturn ce.ValidateName(\"labels\")\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (m *AlertGroup) validateReceiver(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"receiver\", \"body\", m.Receiver); err != nil {\n\t\treturn err\n\t}\n\n\tif m.Receiver != nil {\n\t\tif err := m.Receiver.Validate(formats); err != nil {\n\t\t\tve := new(errors.Validation)\n\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\treturn ve.ValidateName(\"receiver\")\n\t\t\t}\n\t\t\tce := new(errors.CompositeError)\n\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\treturn ce.ValidateName(\"receiver\")\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// ContextValidate validate this alert group based on the context it is used\nfunc (m *AlertGroup) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.contextValidateAlerts(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.contextValidateLabels(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.contextValidateReceiver(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *AlertGroup) contextValidateAlerts(ctx context.Context, formats strfmt.Registry) error {\n\n\tfor i := 0; i < len(m.Alerts); i++ {\n\n\t\tif m.Alerts[i] != nil {\n\n\t\t\tif swag.IsZero(m.Alerts[i]) { // not required\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif err := m.Alerts[i].ContextValidate(ctx, formats); err != nil {\n\t\t\t\tve := new(errors.Validation)\n\t\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\t\treturn ve.ValidateName(\"alerts\" + \".\" + strconv.Itoa(i))\n\t\t\t\t}\n\t\t\t\tce := new(errors.CompositeError)\n\t\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\t\treturn ce.ValidateName(\"alerts\" + \".\" + strconv.Itoa(i))\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t}\n\n\treturn nil\n}\n\nfunc (m *AlertGroup) contextValidateLabels(ctx context.Context, formats strfmt.Registry) error {\n\n\tif err := m.Labels.ContextValidate(ctx, formats); err != nil {\n\t\tve := new(errors.Validation)\n\t\tif stderrors.As(err, &ve) {\n\t\t\treturn ve.ValidateName(\"labels\")\n\t\t}\n\t\tce := new(errors.CompositeError)\n\t\tif stderrors.As(err, &ce) {\n\t\t\treturn ce.ValidateName(\"labels\")\n\t\t}\n\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *AlertGroup) contextValidateReceiver(ctx context.Context, formats strfmt.Registry) error {\n\n\tif m.Receiver != nil {\n\n\t\tif err := m.Receiver.ContextValidate(ctx, formats); err != nil {\n\t\t\tve := new(errors.Validation)\n\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\treturn ve.ValidateName(\"receiver\")\n\t\t\t}\n\t\t\tce := new(errors.CompositeError)\n\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\treturn ce.ValidateName(\"receiver\")\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (m *AlertGroup) MarshalBinary() ([]byte, error) {\n\tif m == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(m)\n}\n\n// UnmarshalBinary interface implementation\nfunc (m *AlertGroup) UnmarshalBinary(b []byte) error {\n\tvar res AlertGroup\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*m = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/alert_groups.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\tstderrors \"errors\"\n\t\"strconv\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n)\n\n// AlertGroups alert groups\n//\n// swagger:model alertGroups\ntype AlertGroups []*AlertGroup\n\n// Validate validates this alert groups\nfunc (m AlertGroups) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tfor i := 0; i < len(m); i++ {\n\t\tif swag.IsZero(m[i]) { // not required\n\t\t\tcontinue\n\t\t}\n\n\t\tif m[i] != nil {\n\t\t\tif err := m[i].Validate(formats); err != nil {\n\t\t\t\tve := new(errors.Validation)\n\t\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\t\treturn ve.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\t\t\t\tce := new(errors.CompositeError)\n\t\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\t\treturn ce.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\n// ContextValidate validate this alert groups based on the context it is used\nfunc (m AlertGroups) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\tvar res []error\n\n\tfor i := 0; i < len(m); i++ {\n\n\t\tif m[i] != nil {\n\n\t\t\tif swag.IsZero(m[i]) { // not required\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif err := m[i].ContextValidate(ctx, formats); err != nil {\n\t\t\t\tve := new(errors.Validation)\n\t\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\t\treturn ve.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\t\t\t\tce := new(errors.CompositeError)\n\t\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\t\treturn ce.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/alert_status.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// AlertStatus alert status\n//\n// swagger:model alertStatus\ntype AlertStatus struct {\n\n\t// inhibited by\n\t// Required: true\n\tInhibitedBy []string `json:\"inhibitedBy\"`\n\n\t// muted by\n\t// Required: true\n\tMutedBy []string `json:\"mutedBy\"`\n\n\t// silenced by\n\t// Required: true\n\tSilencedBy []string `json:\"silencedBy\"`\n\n\t// state\n\t// Required: true\n\t// Enum: [\"unprocessed\",\"active\",\"suppressed\"]\n\tState *string `json:\"state\"`\n}\n\n// Validate validates this alert status\nfunc (m *AlertStatus) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.validateInhibitedBy(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateMutedBy(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateSilencedBy(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateState(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *AlertStatus) validateInhibitedBy(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"inhibitedBy\", \"body\", m.InhibitedBy); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *AlertStatus) validateMutedBy(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"mutedBy\", \"body\", m.MutedBy); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *AlertStatus) validateSilencedBy(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"silencedBy\", \"body\", m.SilencedBy); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nvar alertStatusTypeStatePropEnum []any\n\nfunc init() {\n\tvar res []string\n\tif err := json.Unmarshal([]byte(`[\"unprocessed\",\"active\",\"suppressed\"]`), &res); err != nil {\n\t\tpanic(err)\n\t}\n\tfor _, v := range res {\n\t\talertStatusTypeStatePropEnum = append(alertStatusTypeStatePropEnum, v)\n\t}\n}\n\nconst (\n\n\t// AlertStatusStateUnprocessed captures enum value \"unprocessed\"\n\tAlertStatusStateUnprocessed string = \"unprocessed\"\n\n\t// AlertStatusStateActive captures enum value \"active\"\n\tAlertStatusStateActive string = \"active\"\n\n\t// AlertStatusStateSuppressed captures enum value \"suppressed\"\n\tAlertStatusStateSuppressed string = \"suppressed\"\n)\n\n// prop value enum\nfunc (m *AlertStatus) validateStateEnum(path, location string, value string) error {\n\tif err := validate.EnumCase(path, location, value, alertStatusTypeStatePropEnum, true); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (m *AlertStatus) validateState(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"state\", \"body\", m.State); err != nil {\n\t\treturn err\n\t}\n\n\t// value enum\n\tif err := m.validateStateEnum(\"state\", \"body\", *m.State); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// ContextValidate validates this alert status based on context it is used\nfunc (m *AlertStatus) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (m *AlertStatus) MarshalBinary() ([]byte, error) {\n\tif m == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(m)\n}\n\n// UnmarshalBinary interface implementation\nfunc (m *AlertStatus) UnmarshalBinary(b []byte) error {\n\tvar res AlertStatus\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*m = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/alertmanager_config.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// AlertmanagerConfig alertmanager config\n//\n// swagger:model alertmanagerConfig\ntype AlertmanagerConfig struct {\n\n\t// original\n\t// Required: true\n\tOriginal *string `json:\"original\"`\n}\n\n// Validate validates this alertmanager config\nfunc (m *AlertmanagerConfig) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.validateOriginal(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *AlertmanagerConfig) validateOriginal(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"original\", \"body\", m.Original); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// ContextValidate validates this alertmanager config based on context it is used\nfunc (m *AlertmanagerConfig) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (m *AlertmanagerConfig) MarshalBinary() ([]byte, error) {\n\tif m == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(m)\n}\n\n// UnmarshalBinary interface implementation\nfunc (m *AlertmanagerConfig) UnmarshalBinary(b []byte) error {\n\tvar res AlertmanagerConfig\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*m = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/alertmanager_status.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\tstderrors \"errors\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// AlertmanagerStatus alertmanager status\n//\n// swagger:model alertmanagerStatus\ntype AlertmanagerStatus struct {\n\n\t// cluster\n\t// Required: true\n\tCluster *ClusterStatus `json:\"cluster\"`\n\n\t// config\n\t// Required: true\n\tConfig *AlertmanagerConfig `json:\"config\"`\n\n\t// uptime\n\t// Required: true\n\t// Format: date-time\n\tUptime *strfmt.DateTime `json:\"uptime\"`\n\n\t// version info\n\t// Required: true\n\tVersionInfo *VersionInfo `json:\"versionInfo\"`\n}\n\n// Validate validates this alertmanager status\nfunc (m *AlertmanagerStatus) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.validateCluster(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateConfig(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateUptime(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateVersionInfo(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *AlertmanagerStatus) validateCluster(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"cluster\", \"body\", m.Cluster); err != nil {\n\t\treturn err\n\t}\n\n\tif m.Cluster != nil {\n\t\tif err := m.Cluster.Validate(formats); err != nil {\n\t\t\tve := new(errors.Validation)\n\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\treturn ve.ValidateName(\"cluster\")\n\t\t\t}\n\t\t\tce := new(errors.CompositeError)\n\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\treturn ce.ValidateName(\"cluster\")\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (m *AlertmanagerStatus) validateConfig(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"config\", \"body\", m.Config); err != nil {\n\t\treturn err\n\t}\n\n\tif m.Config != nil {\n\t\tif err := m.Config.Validate(formats); err != nil {\n\t\t\tve := new(errors.Validation)\n\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\treturn ve.ValidateName(\"config\")\n\t\t\t}\n\t\t\tce := new(errors.CompositeError)\n\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\treturn ce.ValidateName(\"config\")\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (m *AlertmanagerStatus) validateUptime(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"uptime\", \"body\", m.Uptime); err != nil {\n\t\treturn err\n\t}\n\n\tif err := validate.FormatOf(\"uptime\", \"body\", \"date-time\", m.Uptime.String(), formats); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *AlertmanagerStatus) validateVersionInfo(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"versionInfo\", \"body\", m.VersionInfo); err != nil {\n\t\treturn err\n\t}\n\n\tif m.VersionInfo != nil {\n\t\tif err := m.VersionInfo.Validate(formats); err != nil {\n\t\t\tve := new(errors.Validation)\n\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\treturn ve.ValidateName(\"versionInfo\")\n\t\t\t}\n\t\t\tce := new(errors.CompositeError)\n\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\treturn ce.ValidateName(\"versionInfo\")\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// ContextValidate validate this alertmanager status based on the context it is used\nfunc (m *AlertmanagerStatus) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.contextValidateCluster(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.contextValidateConfig(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.contextValidateVersionInfo(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *AlertmanagerStatus) contextValidateCluster(ctx context.Context, formats strfmt.Registry) error {\n\n\tif m.Cluster != nil {\n\n\t\tif err := m.Cluster.ContextValidate(ctx, formats); err != nil {\n\t\t\tve := new(errors.Validation)\n\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\treturn ve.ValidateName(\"cluster\")\n\t\t\t}\n\t\t\tce := new(errors.CompositeError)\n\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\treturn ce.ValidateName(\"cluster\")\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (m *AlertmanagerStatus) contextValidateConfig(ctx context.Context, formats strfmt.Registry) error {\n\n\tif m.Config != nil {\n\n\t\tif err := m.Config.ContextValidate(ctx, formats); err != nil {\n\t\t\tve := new(errors.Validation)\n\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\treturn ve.ValidateName(\"config\")\n\t\t\t}\n\t\t\tce := new(errors.CompositeError)\n\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\treturn ce.ValidateName(\"config\")\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (m *AlertmanagerStatus) contextValidateVersionInfo(ctx context.Context, formats strfmt.Registry) error {\n\n\tif m.VersionInfo != nil {\n\n\t\tif err := m.VersionInfo.ContextValidate(ctx, formats); err != nil {\n\t\t\tve := new(errors.Validation)\n\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\treturn ve.ValidateName(\"versionInfo\")\n\t\t\t}\n\t\t\tce := new(errors.CompositeError)\n\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\treturn ce.ValidateName(\"versionInfo\")\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (m *AlertmanagerStatus) MarshalBinary() ([]byte, error) {\n\tif m == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(m)\n}\n\n// UnmarshalBinary interface implementation\nfunc (m *AlertmanagerStatus) UnmarshalBinary(b []byte) error {\n\tvar res AlertmanagerStatus\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*m = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/cluster_status.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\tstderrors \"errors\"\n\t\"strconv\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// ClusterStatus cluster status\n//\n// swagger:model clusterStatus\ntype ClusterStatus struct {\n\n\t// name\n\tName string `json:\"name,omitempty\"`\n\n\t// peers\n\tPeers []*PeerStatus `json:\"peers\"`\n\n\t// status\n\t// Required: true\n\t// Enum: [\"ready\",\"settling\",\"disabled\"]\n\tStatus *string `json:\"status\"`\n}\n\n// Validate validates this cluster status\nfunc (m *ClusterStatus) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.validatePeers(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateStatus(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *ClusterStatus) validatePeers(formats strfmt.Registry) error {\n\tif swag.IsZero(m.Peers) { // not required\n\t\treturn nil\n\t}\n\n\tfor i := 0; i < len(m.Peers); i++ {\n\t\tif swag.IsZero(m.Peers[i]) { // not required\n\t\t\tcontinue\n\t\t}\n\n\t\tif m.Peers[i] != nil {\n\t\t\tif err := m.Peers[i].Validate(formats); err != nil {\n\t\t\t\tve := new(errors.Validation)\n\t\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\t\treturn ve.ValidateName(\"peers\" + \".\" + strconv.Itoa(i))\n\t\t\t\t}\n\t\t\t\tce := new(errors.CompositeError)\n\t\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\t\treturn ce.ValidateName(\"peers\" + \".\" + strconv.Itoa(i))\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t}\n\n\treturn nil\n}\n\nvar clusterStatusTypeStatusPropEnum []any\n\nfunc init() {\n\tvar res []string\n\tif err := json.Unmarshal([]byte(`[\"ready\",\"settling\",\"disabled\"]`), &res); err != nil {\n\t\tpanic(err)\n\t}\n\tfor _, v := range res {\n\t\tclusterStatusTypeStatusPropEnum = append(clusterStatusTypeStatusPropEnum, v)\n\t}\n}\n\nconst (\n\n\t// ClusterStatusStatusReady captures enum value \"ready\"\n\tClusterStatusStatusReady string = \"ready\"\n\n\t// ClusterStatusStatusSettling captures enum value \"settling\"\n\tClusterStatusStatusSettling string = \"settling\"\n\n\t// ClusterStatusStatusDisabled captures enum value \"disabled\"\n\tClusterStatusStatusDisabled string = \"disabled\"\n)\n\n// prop value enum\nfunc (m *ClusterStatus) validateStatusEnum(path, location string, value string) error {\n\tif err := validate.EnumCase(path, location, value, clusterStatusTypeStatusPropEnum, true); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (m *ClusterStatus) validateStatus(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"status\", \"body\", m.Status); err != nil {\n\t\treturn err\n\t}\n\n\t// value enum\n\tif err := m.validateStatusEnum(\"status\", \"body\", *m.Status); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// ContextValidate validate this cluster status based on the context it is used\nfunc (m *ClusterStatus) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.contextValidatePeers(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *ClusterStatus) contextValidatePeers(ctx context.Context, formats strfmt.Registry) error {\n\n\tfor i := 0; i < len(m.Peers); i++ {\n\n\t\tif m.Peers[i] != nil {\n\n\t\t\tif swag.IsZero(m.Peers[i]) { // not required\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif err := m.Peers[i].ContextValidate(ctx, formats); err != nil {\n\t\t\t\tve := new(errors.Validation)\n\t\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\t\treturn ve.ValidateName(\"peers\" + \".\" + strconv.Itoa(i))\n\t\t\t\t}\n\t\t\t\tce := new(errors.CompositeError)\n\t\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\t\treturn ce.ValidateName(\"peers\" + \".\" + strconv.Itoa(i))\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t}\n\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (m *ClusterStatus) MarshalBinary() ([]byte, error) {\n\tif m == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(m)\n}\n\n// UnmarshalBinary interface implementation\nfunc (m *ClusterStatus) UnmarshalBinary(b []byte) error {\n\tvar res ClusterStatus\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*m = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/gettable_alert.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\tstderrors \"errors\"\n\t\"strconv\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// GettableAlert gettable alert\n//\n// swagger:model gettableAlert\ntype GettableAlert struct {\n\n\t// annotations\n\t// Required: true\n\tAnnotations LabelSet `json:\"annotations\"`\n\n\t// ends at\n\t// Required: true\n\t// Format: date-time\n\tEndsAt *strfmt.DateTime `json:\"endsAt\"`\n\n\t// fingerprint\n\t// Required: true\n\tFingerprint *string `json:\"fingerprint\"`\n\n\t// receivers\n\t// Required: true\n\tReceivers []*Receiver `json:\"receivers\"`\n\n\t// starts at\n\t// Required: true\n\t// Format: date-time\n\tStartsAt *strfmt.DateTime `json:\"startsAt\"`\n\n\t// status\n\t// Required: true\n\tStatus *AlertStatus `json:\"status\"`\n\n\t// updated at\n\t// Required: true\n\t// Format: date-time\n\tUpdatedAt *strfmt.DateTime `json:\"updatedAt\"`\n\n\tAlert\n}\n\n// UnmarshalJSON unmarshals this object from a JSON structure\nfunc (m *GettableAlert) UnmarshalJSON(raw []byte) error {\n\t// AO0\n\tvar dataAO0 struct {\n\t\tAnnotations LabelSet `json:\"annotations\"`\n\n\t\tEndsAt *strfmt.DateTime `json:\"endsAt\"`\n\n\t\tFingerprint *string `json:\"fingerprint\"`\n\n\t\tReceivers []*Receiver `json:\"receivers\"`\n\n\t\tStartsAt *strfmt.DateTime `json:\"startsAt\"`\n\n\t\tStatus *AlertStatus `json:\"status\"`\n\n\t\tUpdatedAt *strfmt.DateTime `json:\"updatedAt\"`\n\t}\n\tif err := swag.ReadJSON(raw, &dataAO0); err != nil {\n\t\treturn err\n\t}\n\n\tm.Annotations = dataAO0.Annotations\n\n\tm.EndsAt = dataAO0.EndsAt\n\n\tm.Fingerprint = dataAO0.Fingerprint\n\n\tm.Receivers = dataAO0.Receivers\n\n\tm.StartsAt = dataAO0.StartsAt\n\n\tm.Status = dataAO0.Status\n\n\tm.UpdatedAt = dataAO0.UpdatedAt\n\n\t// AO1\n\tvar aO1 Alert\n\tif err := swag.ReadJSON(raw, &aO1); err != nil {\n\t\treturn err\n\t}\n\tm.Alert = aO1\n\n\treturn nil\n}\n\n// MarshalJSON marshals this object to a JSON structure\nfunc (m GettableAlert) MarshalJSON() ([]byte, error) {\n\t_parts := make([][]byte, 0, 2)\n\n\tvar dataAO0 struct {\n\t\tAnnotations LabelSet `json:\"annotations\"`\n\n\t\tEndsAt *strfmt.DateTime `json:\"endsAt\"`\n\n\t\tFingerprint *string `json:\"fingerprint\"`\n\n\t\tReceivers []*Receiver `json:\"receivers\"`\n\n\t\tStartsAt *strfmt.DateTime `json:\"startsAt\"`\n\n\t\tStatus *AlertStatus `json:\"status\"`\n\n\t\tUpdatedAt *strfmt.DateTime `json:\"updatedAt\"`\n\t}\n\n\tdataAO0.Annotations = m.Annotations\n\n\tdataAO0.EndsAt = m.EndsAt\n\n\tdataAO0.Fingerprint = m.Fingerprint\n\n\tdataAO0.Receivers = m.Receivers\n\n\tdataAO0.StartsAt = m.StartsAt\n\n\tdataAO0.Status = m.Status\n\n\tdataAO0.UpdatedAt = m.UpdatedAt\n\n\tjsonDataAO0, errAO0 := swag.WriteJSON(dataAO0)\n\tif errAO0 != nil {\n\t\treturn nil, errAO0\n\t}\n\t_parts = append(_parts, jsonDataAO0)\n\n\taO1, err := swag.WriteJSON(m.Alert)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t_parts = append(_parts, aO1)\n\treturn swag.ConcatJSON(_parts...), nil\n}\n\n// Validate validates this gettable alert\nfunc (m *GettableAlert) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.validateAnnotations(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateEndsAt(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateFingerprint(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateReceivers(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateStartsAt(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateStatus(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateUpdatedAt(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\t// validation for a type composition with Alert\n\tif err := m.Alert.Validate(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *GettableAlert) validateAnnotations(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"annotations\", \"body\", m.Annotations); err != nil {\n\t\treturn err\n\t}\n\n\tif m.Annotations != nil {\n\t\tif err := m.Annotations.Validate(formats); err != nil {\n\t\t\tve := new(errors.Validation)\n\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\treturn ve.ValidateName(\"annotations\")\n\t\t\t}\n\t\t\tce := new(errors.CompositeError)\n\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\treturn ce.ValidateName(\"annotations\")\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (m *GettableAlert) validateEndsAt(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"endsAt\", \"body\", m.EndsAt); err != nil {\n\t\treturn err\n\t}\n\n\tif err := validate.FormatOf(\"endsAt\", \"body\", \"date-time\", m.EndsAt.String(), formats); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *GettableAlert) validateFingerprint(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"fingerprint\", \"body\", m.Fingerprint); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *GettableAlert) validateReceivers(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"receivers\", \"body\", m.Receivers); err != nil {\n\t\treturn err\n\t}\n\n\tfor i := 0; i < len(m.Receivers); i++ {\n\t\tif swag.IsZero(m.Receivers[i]) { // not required\n\t\t\tcontinue\n\t\t}\n\n\t\tif m.Receivers[i] != nil {\n\t\t\tif err := m.Receivers[i].Validate(formats); err != nil {\n\t\t\t\tve := new(errors.Validation)\n\t\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\t\treturn ve.ValidateName(\"receivers\" + \".\" + strconv.Itoa(i))\n\t\t\t\t}\n\t\t\t\tce := new(errors.CompositeError)\n\t\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\t\treturn ce.ValidateName(\"receivers\" + \".\" + strconv.Itoa(i))\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t}\n\n\treturn nil\n}\n\nfunc (m *GettableAlert) validateStartsAt(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"startsAt\", \"body\", m.StartsAt); err != nil {\n\t\treturn err\n\t}\n\n\tif err := validate.FormatOf(\"startsAt\", \"body\", \"date-time\", m.StartsAt.String(), formats); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *GettableAlert) validateStatus(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"status\", \"body\", m.Status); err != nil {\n\t\treturn err\n\t}\n\n\tif m.Status != nil {\n\t\tif err := m.Status.Validate(formats); err != nil {\n\t\t\tve := new(errors.Validation)\n\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\treturn ve.ValidateName(\"status\")\n\t\t\t}\n\t\t\tce := new(errors.CompositeError)\n\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\treturn ce.ValidateName(\"status\")\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (m *GettableAlert) validateUpdatedAt(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"updatedAt\", \"body\", m.UpdatedAt); err != nil {\n\t\treturn err\n\t}\n\n\tif err := validate.FormatOf(\"updatedAt\", \"body\", \"date-time\", m.UpdatedAt.String(), formats); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// ContextValidate validate this gettable alert based on the context it is used\nfunc (m *GettableAlert) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.contextValidateAnnotations(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.contextValidateReceivers(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.contextValidateStatus(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\t// validation for a type composition with Alert\n\tif err := m.Alert.ContextValidate(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *GettableAlert) contextValidateAnnotations(ctx context.Context, formats strfmt.Registry) error {\n\n\tif err := m.Annotations.ContextValidate(ctx, formats); err != nil {\n\t\tve := new(errors.Validation)\n\t\tif stderrors.As(err, &ve) {\n\t\t\treturn ve.ValidateName(\"annotations\")\n\t\t}\n\t\tce := new(errors.CompositeError)\n\t\tif stderrors.As(err, &ce) {\n\t\t\treturn ce.ValidateName(\"annotations\")\n\t\t}\n\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *GettableAlert) contextValidateReceivers(ctx context.Context, formats strfmt.Registry) error {\n\n\tfor i := 0; i < len(m.Receivers); i++ {\n\n\t\tif m.Receivers[i] != nil {\n\n\t\t\tif swag.IsZero(m.Receivers[i]) { // not required\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif err := m.Receivers[i].ContextValidate(ctx, formats); err != nil {\n\t\t\t\tve := new(errors.Validation)\n\t\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\t\treturn ve.ValidateName(\"receivers\" + \".\" + strconv.Itoa(i))\n\t\t\t\t}\n\t\t\t\tce := new(errors.CompositeError)\n\t\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\t\treturn ce.ValidateName(\"receivers\" + \".\" + strconv.Itoa(i))\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t}\n\n\treturn nil\n}\n\nfunc (m *GettableAlert) contextValidateStatus(ctx context.Context, formats strfmt.Registry) error {\n\n\tif m.Status != nil {\n\n\t\tif err := m.Status.ContextValidate(ctx, formats); err != nil {\n\t\t\tve := new(errors.Validation)\n\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\treturn ve.ValidateName(\"status\")\n\t\t\t}\n\t\t\tce := new(errors.CompositeError)\n\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\treturn ce.ValidateName(\"status\")\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (m *GettableAlert) MarshalBinary() ([]byte, error) {\n\tif m == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(m)\n}\n\n// UnmarshalBinary interface implementation\nfunc (m *GettableAlert) UnmarshalBinary(b []byte) error {\n\tvar res GettableAlert\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*m = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/gettable_alerts.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\tstderrors \"errors\"\n\t\"strconv\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n)\n\n// GettableAlerts gettable alerts\n//\n// swagger:model gettableAlerts\ntype GettableAlerts []*GettableAlert\n\n// Validate validates this gettable alerts\nfunc (m GettableAlerts) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tfor i := 0; i < len(m); i++ {\n\t\tif swag.IsZero(m[i]) { // not required\n\t\t\tcontinue\n\t\t}\n\n\t\tif m[i] != nil {\n\t\t\tif err := m[i].Validate(formats); err != nil {\n\t\t\t\tve := new(errors.Validation)\n\t\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\t\treturn ve.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\t\t\t\tce := new(errors.CompositeError)\n\t\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\t\treturn ce.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\n// ContextValidate validate this gettable alerts based on the context it is used\nfunc (m GettableAlerts) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\tvar res []error\n\n\tfor i := 0; i < len(m); i++ {\n\n\t\tif m[i] != nil {\n\n\t\t\tif swag.IsZero(m[i]) { // not required\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif err := m[i].ContextValidate(ctx, formats); err != nil {\n\t\t\t\tve := new(errors.Validation)\n\t\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\t\treturn ve.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\t\t\t\tce := new(errors.CompositeError)\n\t\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\t\treturn ce.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/gettable_silence.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\tstderrors \"errors\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// GettableSilence gettable silence\n//\n// swagger:model gettableSilence\ntype GettableSilence struct {\n\n\t// id\n\t// Required: true\n\tID *string `json:\"id\"`\n\n\t// status\n\t// Required: true\n\tStatus *SilenceStatus `json:\"status\"`\n\n\t// updated at\n\t// Required: true\n\t// Format: date-time\n\tUpdatedAt *strfmt.DateTime `json:\"updatedAt\"`\n\n\tSilence\n}\n\n// UnmarshalJSON unmarshals this object from a JSON structure\nfunc (m *GettableSilence) UnmarshalJSON(raw []byte) error {\n\t// AO0\n\tvar dataAO0 struct {\n\t\tID *string `json:\"id\"`\n\n\t\tStatus *SilenceStatus `json:\"status\"`\n\n\t\tUpdatedAt *strfmt.DateTime `json:\"updatedAt\"`\n\t}\n\tif err := swag.ReadJSON(raw, &dataAO0); err != nil {\n\t\treturn err\n\t}\n\n\tm.ID = dataAO0.ID\n\n\tm.Status = dataAO0.Status\n\n\tm.UpdatedAt = dataAO0.UpdatedAt\n\n\t// AO1\n\tvar aO1 Silence\n\tif err := swag.ReadJSON(raw, &aO1); err != nil {\n\t\treturn err\n\t}\n\tm.Silence = aO1\n\n\treturn nil\n}\n\n// MarshalJSON marshals this object to a JSON structure\nfunc (m GettableSilence) MarshalJSON() ([]byte, error) {\n\t_parts := make([][]byte, 0, 2)\n\n\tvar dataAO0 struct {\n\t\tID *string `json:\"id\"`\n\n\t\tStatus *SilenceStatus `json:\"status\"`\n\n\t\tUpdatedAt *strfmt.DateTime `json:\"updatedAt\"`\n\t}\n\n\tdataAO0.ID = m.ID\n\n\tdataAO0.Status = m.Status\n\n\tdataAO0.UpdatedAt = m.UpdatedAt\n\n\tjsonDataAO0, errAO0 := swag.WriteJSON(dataAO0)\n\tif errAO0 != nil {\n\t\treturn nil, errAO0\n\t}\n\t_parts = append(_parts, jsonDataAO0)\n\n\taO1, err := swag.WriteJSON(m.Silence)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t_parts = append(_parts, aO1)\n\treturn swag.ConcatJSON(_parts...), nil\n}\n\n// Validate validates this gettable silence\nfunc (m *GettableSilence) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.validateID(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateStatus(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateUpdatedAt(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\t// validation for a type composition with Silence\n\tif err := m.Silence.Validate(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *GettableSilence) validateID(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"id\", \"body\", m.ID); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *GettableSilence) validateStatus(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"status\", \"body\", m.Status); err != nil {\n\t\treturn err\n\t}\n\n\tif m.Status != nil {\n\t\tif err := m.Status.Validate(formats); err != nil {\n\t\t\tve := new(errors.Validation)\n\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\treturn ve.ValidateName(\"status\")\n\t\t\t}\n\t\t\tce := new(errors.CompositeError)\n\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\treturn ce.ValidateName(\"status\")\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (m *GettableSilence) validateUpdatedAt(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"updatedAt\", \"body\", m.UpdatedAt); err != nil {\n\t\treturn err\n\t}\n\n\tif err := validate.FormatOf(\"updatedAt\", \"body\", \"date-time\", m.UpdatedAt.String(), formats); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// ContextValidate validate this gettable silence based on the context it is used\nfunc (m *GettableSilence) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.contextValidateStatus(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\t// validation for a type composition with Silence\n\tif err := m.Silence.ContextValidate(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *GettableSilence) contextValidateStatus(ctx context.Context, formats strfmt.Registry) error {\n\n\tif m.Status != nil {\n\n\t\tif err := m.Status.ContextValidate(ctx, formats); err != nil {\n\t\t\tve := new(errors.Validation)\n\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\treturn ve.ValidateName(\"status\")\n\t\t\t}\n\t\t\tce := new(errors.CompositeError)\n\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\treturn ce.ValidateName(\"status\")\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (m *GettableSilence) MarshalBinary() ([]byte, error) {\n\tif m == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(m)\n}\n\n// UnmarshalBinary interface implementation\nfunc (m *GettableSilence) UnmarshalBinary(b []byte) error {\n\tvar res GettableSilence\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*m = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/gettable_silences.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\tstderrors \"errors\"\n\t\"strconv\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n)\n\n// GettableSilences gettable silences\n//\n// swagger:model gettableSilences\ntype GettableSilences []*GettableSilence\n\n// Validate validates this gettable silences\nfunc (m GettableSilences) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tfor i := 0; i < len(m); i++ {\n\t\tif swag.IsZero(m[i]) { // not required\n\t\t\tcontinue\n\t\t}\n\n\t\tif m[i] != nil {\n\t\t\tif err := m[i].Validate(formats); err != nil {\n\t\t\t\tve := new(errors.Validation)\n\t\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\t\treturn ve.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\t\t\t\tce := new(errors.CompositeError)\n\t\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\t\treturn ce.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\n// ContextValidate validate this gettable silences based on the context it is used\nfunc (m GettableSilences) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\tvar res []error\n\n\tfor i := 0; i < len(m); i++ {\n\n\t\tif m[i] != nil {\n\n\t\t\tif swag.IsZero(m[i]) { // not required\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif err := m[i].ContextValidate(ctx, formats); err != nil {\n\t\t\t\tve := new(errors.Validation)\n\t\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\t\treturn ve.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\t\t\t\tce := new(errors.CompositeError)\n\t\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\t\treturn ce.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/label_set.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-openapi/strfmt\"\n)\n\n// LabelSet label set\n//\n// swagger:model labelSet\ntype LabelSet map[string]string\n\n// Validate validates this label set\nfunc (m LabelSet) Validate(formats strfmt.Registry) error {\n\treturn nil\n}\n\n// ContextValidate validates this label set based on context it is used\nfunc (m LabelSet) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/matcher.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// Matcher matcher\n//\n// swagger:model matcher\ntype Matcher struct {\n\n\t// is equal\n\tIsEqual *bool `json:\"isEqual,omitempty\"`\n\n\t// is regex\n\t// Required: true\n\tIsRegex *bool `json:\"isRegex\"`\n\n\t// name\n\t// Required: true\n\tName *string `json:\"name\"`\n\n\t// value\n\t// Required: true\n\tValue *string `json:\"value\"`\n}\n\n// Validate validates this matcher\nfunc (m *Matcher) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.validateIsRegex(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateName(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateValue(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *Matcher) validateIsRegex(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"isRegex\", \"body\", m.IsRegex); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *Matcher) validateName(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"name\", \"body\", m.Name); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *Matcher) validateValue(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"value\", \"body\", m.Value); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// ContextValidate validates this matcher based on context it is used\nfunc (m *Matcher) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (m *Matcher) MarshalBinary() ([]byte, error) {\n\tif m == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(m)\n}\n\n// UnmarshalBinary interface implementation\nfunc (m *Matcher) UnmarshalBinary(b []byte) error {\n\tvar res Matcher\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*m = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/matchers.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\tstderrors \"errors\"\n\t\"strconv\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// Matchers matchers\n//\n// swagger:model matchers\ntype Matchers []*Matcher\n\n// Validate validates this matchers\nfunc (m Matchers) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tiMatchersSize := int64(len(m))\n\n\tif err := validate.MinItems(\"\", \"body\", iMatchersSize, 1); err != nil {\n\t\treturn err\n\t}\n\n\tfor i := 0; i < len(m); i++ {\n\t\tif swag.IsZero(m[i]) { // not required\n\t\t\tcontinue\n\t\t}\n\n\t\tif m[i] != nil {\n\t\t\tif err := m[i].Validate(formats); err != nil {\n\t\t\t\tve := new(errors.Validation)\n\t\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\t\treturn ve.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\t\t\t\tce := new(errors.CompositeError)\n\t\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\t\treturn ce.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\n// ContextValidate validate this matchers based on the context it is used\nfunc (m Matchers) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\tvar res []error\n\n\tfor i := 0; i < len(m); i++ {\n\n\t\tif m[i] != nil {\n\n\t\t\tif swag.IsZero(m[i]) { // not required\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif err := m[i].ContextValidate(ctx, formats); err != nil {\n\t\t\t\tve := new(errors.Validation)\n\t\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\t\treturn ve.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\t\t\t\tce := new(errors.CompositeError)\n\t\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\t\treturn ce.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/peer_status.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// PeerStatus peer status\n//\n// swagger:model peerStatus\ntype PeerStatus struct {\n\n\t// address\n\t// Required: true\n\tAddress *string `json:\"address\"`\n\n\t// name\n\t// Required: true\n\tName *string `json:\"name\"`\n}\n\n// Validate validates this peer status\nfunc (m *PeerStatus) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.validateAddress(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateName(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *PeerStatus) validateAddress(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"address\", \"body\", m.Address); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *PeerStatus) validateName(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"name\", \"body\", m.Name); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// ContextValidate validates this peer status based on context it is used\nfunc (m *PeerStatus) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (m *PeerStatus) MarshalBinary() ([]byte, error) {\n\tif m == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(m)\n}\n\n// UnmarshalBinary interface implementation\nfunc (m *PeerStatus) UnmarshalBinary(b []byte) error {\n\tvar res PeerStatus\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*m = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/postable_alert.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\tstderrors \"errors\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// PostableAlert postable alert\n//\n// swagger:model postableAlert\ntype PostableAlert struct {\n\n\t// annotations\n\tAnnotations LabelSet `json:\"annotations,omitempty\"`\n\n\t// ends at\n\t// Format: date-time\n\tEndsAt strfmt.DateTime `json:\"endsAt,omitempty\"`\n\n\t// starts at\n\t// Format: date-time\n\tStartsAt strfmt.DateTime `json:\"startsAt,omitempty\"`\n\n\tAlert\n}\n\n// UnmarshalJSON unmarshals this object from a JSON structure\nfunc (m *PostableAlert) UnmarshalJSON(raw []byte) error {\n\t// AO0\n\tvar dataAO0 struct {\n\t\tAnnotations LabelSet `json:\"annotations,omitempty\"`\n\n\t\tEndsAt strfmt.DateTime `json:\"endsAt,omitempty\"`\n\n\t\tStartsAt strfmt.DateTime `json:\"startsAt,omitempty\"`\n\t}\n\tif err := swag.ReadJSON(raw, &dataAO0); err != nil {\n\t\treturn err\n\t}\n\n\tm.Annotations = dataAO0.Annotations\n\n\tm.EndsAt = dataAO0.EndsAt\n\n\tm.StartsAt = dataAO0.StartsAt\n\n\t// AO1\n\tvar aO1 Alert\n\tif err := swag.ReadJSON(raw, &aO1); err != nil {\n\t\treturn err\n\t}\n\tm.Alert = aO1\n\n\treturn nil\n}\n\n// MarshalJSON marshals this object to a JSON structure\nfunc (m PostableAlert) MarshalJSON() ([]byte, error) {\n\t_parts := make([][]byte, 0, 2)\n\n\tvar dataAO0 struct {\n\t\tAnnotations LabelSet `json:\"annotations,omitempty\"`\n\n\t\tEndsAt strfmt.DateTime `json:\"endsAt,omitempty\"`\n\n\t\tStartsAt strfmt.DateTime `json:\"startsAt,omitempty\"`\n\t}\n\n\tdataAO0.Annotations = m.Annotations\n\n\tdataAO0.EndsAt = m.EndsAt\n\n\tdataAO0.StartsAt = m.StartsAt\n\n\tjsonDataAO0, errAO0 := swag.WriteJSON(dataAO0)\n\tif errAO0 != nil {\n\t\treturn nil, errAO0\n\t}\n\t_parts = append(_parts, jsonDataAO0)\n\n\taO1, err := swag.WriteJSON(m.Alert)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t_parts = append(_parts, aO1)\n\treturn swag.ConcatJSON(_parts...), nil\n}\n\n// Validate validates this postable alert\nfunc (m *PostableAlert) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.validateAnnotations(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateEndsAt(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateStartsAt(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\t// validation for a type composition with Alert\n\tif err := m.Alert.Validate(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *PostableAlert) validateAnnotations(formats strfmt.Registry) error {\n\n\tif swag.IsZero(m.Annotations) { // not required\n\t\treturn nil\n\t}\n\n\tif m.Annotations != nil {\n\t\tif err := m.Annotations.Validate(formats); err != nil {\n\t\t\tve := new(errors.Validation)\n\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\treturn ve.ValidateName(\"annotations\")\n\t\t\t}\n\t\t\tce := new(errors.CompositeError)\n\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\treturn ce.ValidateName(\"annotations\")\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (m *PostableAlert) validateEndsAt(formats strfmt.Registry) error {\n\n\tif swag.IsZero(m.EndsAt) { // not required\n\t\treturn nil\n\t}\n\n\tif err := validate.FormatOf(\"endsAt\", \"body\", \"date-time\", m.EndsAt.String(), formats); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *PostableAlert) validateStartsAt(formats strfmt.Registry) error {\n\n\tif swag.IsZero(m.StartsAt) { // not required\n\t\treturn nil\n\t}\n\n\tif err := validate.FormatOf(\"startsAt\", \"body\", \"date-time\", m.StartsAt.String(), formats); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// ContextValidate validate this postable alert based on the context it is used\nfunc (m *PostableAlert) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.contextValidateAnnotations(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\t// validation for a type composition with Alert\n\tif err := m.Alert.ContextValidate(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *PostableAlert) contextValidateAnnotations(ctx context.Context, formats strfmt.Registry) error {\n\n\tif swag.IsZero(m.Annotations) { // not required\n\t\treturn nil\n\t}\n\n\tif err := m.Annotations.ContextValidate(ctx, formats); err != nil {\n\t\tve := new(errors.Validation)\n\t\tif stderrors.As(err, &ve) {\n\t\t\treturn ve.ValidateName(\"annotations\")\n\t\t}\n\t\tce := new(errors.CompositeError)\n\t\tif stderrors.As(err, &ce) {\n\t\t\treturn ce.ValidateName(\"annotations\")\n\t\t}\n\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (m *PostableAlert) MarshalBinary() ([]byte, error) {\n\tif m == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(m)\n}\n\n// UnmarshalBinary interface implementation\nfunc (m *PostableAlert) UnmarshalBinary(b []byte) error {\n\tvar res PostableAlert\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*m = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/postable_alerts.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\tstderrors \"errors\"\n\t\"strconv\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n)\n\n// PostableAlerts postable alerts\n//\n// swagger:model postableAlerts\ntype PostableAlerts []*PostableAlert\n\n// Validate validates this postable alerts\nfunc (m PostableAlerts) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tfor i := 0; i < len(m); i++ {\n\t\tif swag.IsZero(m[i]) { // not required\n\t\t\tcontinue\n\t\t}\n\n\t\tif m[i] != nil {\n\t\t\tif err := m[i].Validate(formats); err != nil {\n\t\t\t\tve := new(errors.Validation)\n\t\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\t\treturn ve.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\t\t\t\tce := new(errors.CompositeError)\n\t\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\t\treturn ce.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\n// ContextValidate validate this postable alerts based on the context it is used\nfunc (m PostableAlerts) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\tvar res []error\n\n\tfor i := 0; i < len(m); i++ {\n\n\t\tif m[i] != nil {\n\n\t\t\tif swag.IsZero(m[i]) { // not required\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif err := m[i].ContextValidate(ctx, formats); err != nil {\n\t\t\t\tve := new(errors.Validation)\n\t\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\t\treturn ve.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\t\t\t\tce := new(errors.CompositeError)\n\t\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\t\treturn ce.ValidateName(strconv.Itoa(i))\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/postable_silence.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n)\n\n// PostableSilence postable silence\n//\n// swagger:model postableSilence\ntype PostableSilence struct {\n\n\t// id\n\tID string `json:\"id,omitempty\"`\n\n\tSilence\n}\n\n// UnmarshalJSON unmarshals this object from a JSON structure\nfunc (m *PostableSilence) UnmarshalJSON(raw []byte) error {\n\t// AO0\n\tvar dataAO0 struct {\n\t\tID string `json:\"id,omitempty\"`\n\t}\n\tif err := swag.ReadJSON(raw, &dataAO0); err != nil {\n\t\treturn err\n\t}\n\n\tm.ID = dataAO0.ID\n\n\t// AO1\n\tvar aO1 Silence\n\tif err := swag.ReadJSON(raw, &aO1); err != nil {\n\t\treturn err\n\t}\n\tm.Silence = aO1\n\n\treturn nil\n}\n\n// MarshalJSON marshals this object to a JSON structure\nfunc (m PostableSilence) MarshalJSON() ([]byte, error) {\n\t_parts := make([][]byte, 0, 2)\n\n\tvar dataAO0 struct {\n\t\tID string `json:\"id,omitempty\"`\n\t}\n\n\tdataAO0.ID = m.ID\n\n\tjsonDataAO0, errAO0 := swag.WriteJSON(dataAO0)\n\tif errAO0 != nil {\n\t\treturn nil, errAO0\n\t}\n\t_parts = append(_parts, jsonDataAO0)\n\n\taO1, err := swag.WriteJSON(m.Silence)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t_parts = append(_parts, aO1)\n\treturn swag.ConcatJSON(_parts...), nil\n}\n\n// Validate validates this postable silence\nfunc (m *PostableSilence) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\t// validation for a type composition with Silence\n\tif err := m.Silence.Validate(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\n// ContextValidate validate this postable silence based on the context it is used\nfunc (m *PostableSilence) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\tvar res []error\n\n\t// validation for a type composition with Silence\n\tif err := m.Silence.ContextValidate(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (m *PostableSilence) MarshalBinary() ([]byte, error) {\n\tif m == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(m)\n}\n\n// UnmarshalBinary interface implementation\nfunc (m *PostableSilence) UnmarshalBinary(b []byte) error {\n\tvar res PostableSilence\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*m = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/receiver.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// Receiver receiver\n//\n// swagger:model receiver\ntype Receiver struct {\n\n\t// name\n\t// Required: true\n\tName *string `json:\"name\"`\n}\n\n// Validate validates this receiver\nfunc (m *Receiver) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.validateName(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *Receiver) validateName(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"name\", \"body\", m.Name); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// ContextValidate validates this receiver based on context it is used\nfunc (m *Receiver) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (m *Receiver) MarshalBinary() ([]byte, error) {\n\tif m == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(m)\n}\n\n// UnmarshalBinary interface implementation\nfunc (m *Receiver) UnmarshalBinary(b []byte) error {\n\tvar res Receiver\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*m = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/silence.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\tstderrors \"errors\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// Silence silence\n//\n// swagger:model silence\ntype Silence struct {\n\n\t// annotations\n\tAnnotations LabelSet `json:\"annotations,omitempty\"`\n\n\t// comment\n\t// Required: true\n\tComment *string `json:\"comment\"`\n\n\t// created by\n\t// Required: true\n\tCreatedBy *string `json:\"createdBy\"`\n\n\t// ends at\n\t// Required: true\n\t// Format: date-time\n\tEndsAt *strfmt.DateTime `json:\"endsAt\"`\n\n\t// matchers\n\t// Required: true\n\tMatchers Matchers `json:\"matchers\"`\n\n\t// starts at\n\t// Required: true\n\t// Format: date-time\n\tStartsAt *strfmt.DateTime `json:\"startsAt\"`\n}\n\n// Validate validates this silence\nfunc (m *Silence) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.validateAnnotations(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateComment(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateCreatedBy(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateEndsAt(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateMatchers(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateStartsAt(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *Silence) validateAnnotations(formats strfmt.Registry) error {\n\tif swag.IsZero(m.Annotations) { // not required\n\t\treturn nil\n\t}\n\n\tif m.Annotations != nil {\n\t\tif err := m.Annotations.Validate(formats); err != nil {\n\t\t\tve := new(errors.Validation)\n\t\t\tif stderrors.As(err, &ve) {\n\t\t\t\treturn ve.ValidateName(\"annotations\")\n\t\t\t}\n\t\t\tce := new(errors.CompositeError)\n\t\t\tif stderrors.As(err, &ce) {\n\t\t\t\treturn ce.ValidateName(\"annotations\")\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (m *Silence) validateComment(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"comment\", \"body\", m.Comment); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *Silence) validateCreatedBy(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"createdBy\", \"body\", m.CreatedBy); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *Silence) validateEndsAt(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"endsAt\", \"body\", m.EndsAt); err != nil {\n\t\treturn err\n\t}\n\n\tif err := validate.FormatOf(\"endsAt\", \"body\", \"date-time\", m.EndsAt.String(), formats); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *Silence) validateMatchers(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"matchers\", \"body\", m.Matchers); err != nil {\n\t\treturn err\n\t}\n\n\tif err := m.Matchers.Validate(formats); err != nil {\n\t\tve := new(errors.Validation)\n\t\tif stderrors.As(err, &ve) {\n\t\t\treturn ve.ValidateName(\"matchers\")\n\t\t}\n\t\tce := new(errors.CompositeError)\n\t\tif stderrors.As(err, &ce) {\n\t\t\treturn ce.ValidateName(\"matchers\")\n\t\t}\n\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *Silence) validateStartsAt(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"startsAt\", \"body\", m.StartsAt); err != nil {\n\t\treturn err\n\t}\n\n\tif err := validate.FormatOf(\"startsAt\", \"body\", \"date-time\", m.StartsAt.String(), formats); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// ContextValidate validate this silence based on the context it is used\nfunc (m *Silence) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.contextValidateAnnotations(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.contextValidateMatchers(ctx, formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *Silence) contextValidateAnnotations(ctx context.Context, formats strfmt.Registry) error {\n\n\tif swag.IsZero(m.Annotations) { // not required\n\t\treturn nil\n\t}\n\n\tif err := m.Annotations.ContextValidate(ctx, formats); err != nil {\n\t\tve := new(errors.Validation)\n\t\tif stderrors.As(err, &ve) {\n\t\t\treturn ve.ValidateName(\"annotations\")\n\t\t}\n\t\tce := new(errors.CompositeError)\n\t\tif stderrors.As(err, &ce) {\n\t\t\treturn ce.ValidateName(\"annotations\")\n\t\t}\n\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *Silence) contextValidateMatchers(ctx context.Context, formats strfmt.Registry) error {\n\n\tif err := m.Matchers.ContextValidate(ctx, formats); err != nil {\n\t\tve := new(errors.Validation)\n\t\tif stderrors.As(err, &ve) {\n\t\t\treturn ve.ValidateName(\"matchers\")\n\t\t}\n\t\tce := new(errors.CompositeError)\n\t\tif stderrors.As(err, &ce) {\n\t\t\treturn ce.ValidateName(\"matchers\")\n\t\t}\n\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (m *Silence) MarshalBinary() ([]byte, error) {\n\tif m == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(m)\n}\n\n// UnmarshalBinary interface implementation\nfunc (m *Silence) UnmarshalBinary(b []byte) error {\n\tvar res Silence\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*m = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/silence_status.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// SilenceStatus silence status\n//\n// swagger:model silenceStatus\ntype SilenceStatus struct {\n\n\t// state\n\t// Required: true\n\t// Enum: [\"expired\",\"active\",\"pending\"]\n\tState *string `json:\"state\"`\n}\n\n// Validate validates this silence status\nfunc (m *SilenceStatus) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.validateState(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nvar silenceStatusTypeStatePropEnum []any\n\nfunc init() {\n\tvar res []string\n\tif err := json.Unmarshal([]byte(`[\"expired\",\"active\",\"pending\"]`), &res); err != nil {\n\t\tpanic(err)\n\t}\n\tfor _, v := range res {\n\t\tsilenceStatusTypeStatePropEnum = append(silenceStatusTypeStatePropEnum, v)\n\t}\n}\n\nconst (\n\n\t// SilenceStatusStateExpired captures enum value \"expired\"\n\tSilenceStatusStateExpired string = \"expired\"\n\n\t// SilenceStatusStateActive captures enum value \"active\"\n\tSilenceStatusStateActive string = \"active\"\n\n\t// SilenceStatusStatePending captures enum value \"pending\"\n\tSilenceStatusStatePending string = \"pending\"\n)\n\n// prop value enum\nfunc (m *SilenceStatus) validateStateEnum(path, location string, value string) error {\n\tif err := validate.EnumCase(path, location, value, silenceStatusTypeStatePropEnum, true); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (m *SilenceStatus) validateState(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"state\", \"body\", m.State); err != nil {\n\t\treturn err\n\t}\n\n\t// value enum\n\tif err := m.validateStateEnum(\"state\", \"body\", *m.State); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// ContextValidate validates this silence status based on context it is used\nfunc (m *SilenceStatus) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (m *SilenceStatus) MarshalBinary() ([]byte, error) {\n\tif m == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(m)\n}\n\n// UnmarshalBinary interface implementation\nfunc (m *SilenceStatus) UnmarshalBinary(b []byte) error {\n\tvar res SilenceStatus\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*m = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/models/version_info.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage models\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// VersionInfo version info\n//\n// swagger:model versionInfo\ntype VersionInfo struct {\n\n\t// branch\n\t// Required: true\n\tBranch *string `json:\"branch\"`\n\n\t// build date\n\t// Required: true\n\tBuildDate *string `json:\"buildDate\"`\n\n\t// build user\n\t// Required: true\n\tBuildUser *string `json:\"buildUser\"`\n\n\t// go version\n\t// Required: true\n\tGoVersion *string `json:\"goVersion\"`\n\n\t// revision\n\t// Required: true\n\tRevision *string `json:\"revision\"`\n\n\t// version\n\t// Required: true\n\tVersion *string `json:\"version\"`\n}\n\n// Validate validates this version info\nfunc (m *VersionInfo) Validate(formats strfmt.Registry) error {\n\tvar res []error\n\n\tif err := m.validateBranch(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateBuildDate(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateBuildUser(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateGoVersion(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateRevision(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif err := m.validateVersion(formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\nfunc (m *VersionInfo) validateBranch(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"branch\", \"body\", m.Branch); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *VersionInfo) validateBuildDate(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"buildDate\", \"body\", m.BuildDate); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *VersionInfo) validateBuildUser(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"buildUser\", \"body\", m.BuildUser); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *VersionInfo) validateGoVersion(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"goVersion\", \"body\", m.GoVersion); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *VersionInfo) validateRevision(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"revision\", \"body\", m.Revision); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *VersionInfo) validateVersion(formats strfmt.Registry) error {\n\n\tif err := validate.Required(\"version\", \"body\", m.Version); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// ContextValidate validates this version info based on context it is used\nfunc (m *VersionInfo) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (m *VersionInfo) MarshalBinary() ([]byte, error) {\n\tif m == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(m)\n}\n\n// UnmarshalBinary interface implementation\nfunc (m *VersionInfo) UnmarshalBinary(b []byte) error {\n\tvar res VersionInfo\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*m = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/openapi.yaml",
    "content": "---\n\nswagger: '2.0'\n\ninfo:\n  version: 0.0.1\n  title: Alertmanager API\n  description: API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n  license:\n    name: Apache 2.0\n    url: http://www.apache.org/licenses/LICENSE-2.0.html\n\nconsumes:\n  - \"application/json\"\nproduces:\n  - \"application/json\"\n\nbasePath: \"/api/v2/\"\n\npaths:\n  /status:\n    get:\n      tags:\n        - general\n      operationId: getStatus\n      description: Get current status of an Alertmanager instance and its cluster\n      responses:\n        '200':\n          description: Get status response\n          schema:\n            $ref: '#/definitions/alertmanagerStatus'\n  /receivers:\n    get:\n      tags:\n        - receiver\n      operationId: getReceivers\n      description: Get list of all receivers (name of notification integrations)\n      responses:\n        '200':\n          description: Get receivers response\n          schema:\n            type: array\n            items:\n              $ref: '#/definitions/receiver'\n  /silences:\n    get:\n      tags:\n        - silence\n      operationId: getSilences\n      description: Get a list of silences\n      responses:\n        '200':\n          description: Get silences response\n          schema:\n            $ref: '#/definitions/gettableSilences'\n        '400':\n          $ref: '#/responses/BadRequest'\n        '500':\n          $ref: '#/responses/InternalServerError'\n      parameters:\n        - name: filter\n          in: query\n          description: A matcher expression to filter silences. For example `alertname=\"MyAlert\"`. It can be repeated to apply multiple matchers.\n          required: false\n          type: array\n          collectionFormat: multi\n          items:\n            type: string\n    post:\n      tags:\n        - silence\n      operationId: postSilences\n      description: Post a new silence or update an existing one\n      parameters:\n        - in: body\n          name: silence\n          description: The silence to create\n          required: true\n          schema:\n            $ref: '#/definitions/postableSilence'\n      responses:\n        '200':\n          description: Create / update silence response\n          schema:\n            type: object\n            properties:\n              silenceID:\n                type: string\n        '400':\n          $ref: '#/responses/BadRequest'\n        '404':\n          description: A silence with the specified ID was not found\n          schema:\n            type: string\n  /silence/{silenceID}:\n    parameters:\n      - in: path\n        name: silenceID\n        type: string\n        format: uuid\n        required: true\n        description: ID of the silence to get\n    get:\n      tags:\n        - silence\n      operationId: getSilence\n      description: Get a silence by its ID\n      responses:\n        '200':\n          description: Get silence response\n          schema:\n            $ref: '#/definitions/gettableSilence'\n        '404':\n          description: A silence with the specified ID was not found\n        '500':\n          $ref: '#/responses/InternalServerError'\n    delete:\n      tags:\n        - silence\n      operationId: deleteSilence\n      description: Delete a silence by its ID\n      parameters:\n        - in: path\n          name: silenceID\n          type: string\n          format: uuid\n          required: true\n          description: ID of the silence to get\n      responses:\n        '200':\n          description: Delete silence response\n        '404':\n          description: A silence with the specified ID was not found\n        '500':\n          $ref: '#/responses/InternalServerError'\n  /alerts:\n    get:\n      tags:\n        - alert\n      operationId: getAlerts\n      description: Get a list of alerts\n      parameters:\n        - in: query\n          name: active\n          type: boolean\n          description: Include active alerts in results. If false, excludes active alerts and returns only suppressed (silenced or inhibited) alerts.\n          default: true\n        - in: query\n          name: silenced\n          type: boolean\n          description: Include silenced alerts in results. If false, excludes silenced alerts. Note that true (default) shows both silenced and non-silenced alerts.\n          default: true\n        - in: query\n          name: inhibited\n          type: boolean\n          description: Include inhibited alerts in results. If false, excludes inhibited alerts. Note that true (default) shows both inhibited and non-inhibited alerts.\n          default: true\n        - in: query\n          name: unprocessed\n          type: boolean\n          description: Include unprocessed alerts in results. If false, excludes unprocessed alerts. Note that true (default) shows both processed and unprocessed alerts.\n          default: true\n        - name: filter\n          in: query\n          description: A matcher expression to filter alerts. For example `alertname=\"MyAlert\"`. It can be repeated to apply multiple matchers.\n          required: false\n          type: array\n          collectionFormat: multi\n          items:\n            type: string\n        - name: receiver\n          in: query\n          description: A regex matching receivers to filter alerts by\n          required: false\n          type: string\n      responses:\n        '200':\n          description: Get alerts response\n          schema:\n            '$ref': '#/definitions/gettableAlerts'\n        '400':\n          $ref: '#/responses/BadRequest'\n        '500':\n          $ref: '#/responses/InternalServerError'\n    post:\n      tags:\n        - alert\n      operationId: postAlerts\n      description: Create new Alerts\n      parameters:\n        - in: body\n          name: alerts\n          description: The alerts to create\n          required: true\n          schema:\n            $ref: '#/definitions/postableAlerts'\n      responses:\n        '200':\n          description: Create alerts response\n        '500':\n          $ref: '#/responses/InternalServerError'\n        '400':\n          $ref: '#/responses/BadRequest'\n  /alerts/groups:\n    get:\n      tags:\n        - alertgroup\n      operationId: getAlertGroups\n      description: Get a list of alert groups\n      parameters:\n        - in: query\n          name: active\n          type: boolean\n          description: Include active alerts within the returned groups. If false, excludes active alerts from groups and only shows suppressed (silenced or inhibited) alerts.\n          default: true\n        - in: query\n          name: silenced\n          type: boolean\n          description: Include silenced alerts within the returned groups. If false, excludes silenced alerts from groups. Note that true (default) shows both silenced and non-silenced alerts.\n          default: true\n        - in: query\n          name: inhibited\n          type: boolean\n          description: Include inhibited alerts within the returned groups. If false, excludes inhibited alerts from groups. Note that true (default) shows both inhibited and non-inhibited alerts.\n          default: true\n        - in: query\n          name: muted\n          type: boolean\n          description: Include muted (silenced or inhibited) alert groups in results. If false, excludes entire groups where all alerts are muted.\n          default: true\n        - name: filter\n          in: query\n          description: A matcher expression to filter alert groups. For example `alertname=\"MyAlert\"`. It can be repeated to apply multiple matchers.\n          required: false\n          type: array\n          collectionFormat: multi\n          items:\n            type: string\n        - name: receiver\n          in: query\n          description: A regex matching receivers to filter alerts by\n          required: false\n          type: string\n      responses:\n        '200':\n          description: Get alert groups response\n          schema:\n            '$ref': '#/definitions/alertGroups'\n        '400':\n          $ref: '#/responses/BadRequest'\n        '500':\n          $ref: '#/responses/InternalServerError'\n\nresponses:\n  BadRequest:\n    description: Bad request\n    schema:\n      type: string\n  InternalServerError:\n    description: Internal server error\n    schema:\n      type: string\n\n\ndefinitions:\n  alertmanagerStatus:\n    type: object\n    properties:\n      cluster:\n        $ref: '#/definitions/clusterStatus'\n      versionInfo:\n        $ref: '#/definitions/versionInfo'\n      config:\n        $ref: '#/definitions/alertmanagerConfig'\n      uptime:\n        type: string\n        format: date-time\n    required:\n      - cluster\n      - versionInfo\n      - config\n      - uptime\n  clusterStatus:\n    type: object\n    properties:\n      name:\n        type: string\n      status:\n        type: string\n        enum: [\"ready\", \"settling\", \"disabled\"]\n      peers:\n        type: array\n        items:\n          $ref: '#/definitions/peerStatus'\n    required:\n      - status\n  alertmanagerConfig:\n    type: object\n    properties:\n      original:\n        type: string\n    required:\n      - original\n  versionInfo:\n    type: object\n    properties:\n      version:\n        type: string\n      revision:\n        type: string\n      branch:\n        type: string\n      buildUser:\n        type: string\n      buildDate:\n        type: string\n      goVersion:\n        type: string\n    required:\n      - version\n      - revision\n      - branch\n      - buildUser\n      - buildDate\n      - goVersion\n  peerStatus:\n    type: object\n    properties:\n      name:\n        type: string\n      address:\n        type: string\n    required:\n      - name\n      - address\n  silence:\n    type: object\n    properties:\n      matchers:\n        $ref: '#/definitions/matchers'\n      startsAt:\n        type: string\n        format: date-time\n      endsAt:\n        type: string\n        format: date-time\n      createdBy:\n        type: string\n      comment:\n        type: string\n      annotations:\n        $ref: \"#/definitions/labelSet\"\n    required:\n      - matchers\n      - startsAt\n      - endsAt\n      - createdBy\n      - comment\n  gettableSilence:\n    allOf:\n      - type: object\n        properties:\n          id:\n            type: string\n          status:\n            $ref: '#/definitions/silenceStatus'\n          updatedAt:\n            type: string\n            format: date-time\n        required:\n          - id\n          - status\n          - updatedAt\n          - annotations\n      - $ref: '#/definitions/silence'\n  postableSilence:\n    allOf:\n      - type: object\n        properties:\n          id:\n            type: string\n      - $ref: '#/definitions/silence'\n  silenceStatus:\n    type: object\n    properties:\n      state:\n        type: string\n        enum: [\"expired\", \"active\", \"pending\"]\n    required:\n      - state\n  gettableSilences:\n    type: array\n    items:\n      $ref: '#/definitions/gettableSilence'\n  matchers:\n    type: array\n    items:\n      $ref: '#/definitions/matcher'\n    minItems: 1\n  matcher:\n    type: object\n    properties:\n      name:\n        type: string\n      value:\n        type: string\n      isRegex:\n        type: boolean\n      isEqual:\n        type: boolean\n        default: true\n    required:\n      - name\n      - value\n      - isRegex\n  alert:\n    type: object\n    properties:\n      labels:\n        $ref: '#/definitions/labelSet'\n      generatorURL:\n        type: string\n        format: uri\n    required:\n      - labels\n  gettableAlerts:\n    type: array\n    items:\n      $ref: '#/definitions/gettableAlert'\n  gettableAlert:\n    allOf:\n      - type: object\n        properties:\n          annotations:\n            $ref: '#/definitions/labelSet'\n          receivers:\n            type: array\n            items:\n              $ref: '#/definitions/receiver'\n          fingerprint:\n            type: string\n          startsAt:\n            type: string\n            format: date-time\n          updatedAt:\n            type: string\n            format: date-time\n          endsAt:\n            type: string\n            format: date-time\n          status:\n            $ref: '#/definitions/alertStatus'\n        required:\n          - receivers\n          - fingerprint\n          - startsAt\n          - updatedAt\n          - endsAt\n          - annotations\n          - status\n      - $ref: '#/definitions/alert'\n  postableAlerts:\n    type: array\n    items:\n      $ref: '#/definitions/postableAlert'\n  postableAlert:\n    allOf:\n      - type: object\n        properties:\n          startsAt:\n            type: string\n            format: date-time\n          endsAt:\n            type: string\n            format: date-time\n          annotations:\n            $ref: '#/definitions/labelSet'\n      - $ref: '#/definitions/alert'\n  alertGroups:\n    type: array\n    items:\n      $ref: '#/definitions/alertGroup'\n  alertGroup:\n    type: object\n    properties:\n      labels:\n        $ref: '#/definitions/labelSet'\n      receiver:\n        $ref: '#/definitions/receiver'\n      alerts:\n        type: array\n        items:\n          $ref: '#/definitions/gettableAlert'\n    required:\n      - labels\n      - receiver\n      - alerts\n  alertStatus:\n    type: object\n    properties:\n      state:\n        type: string\n        enum: ['unprocessed', 'active', 'suppressed']\n      silencedBy:\n        type: array\n        items:\n          type: string\n      inhibitedBy:\n        type: array\n        items:\n          type: string\n      mutedBy:\n        type: array\n        items:\n          type: string\n    required:\n      - state\n      - silencedBy\n      - inhibitedBy\n      - mutedBy\n  receiver:\n    type: object\n    properties:\n      name:\n        type: string\n    required:\n      - name\n  labelSet:\n    type: object\n    additionalProperties:\n      type: string\n\n\ntags:\n  - name: general\n    description: General Alertmanager operations\n  - name: receiver\n    description: Everything related to Alertmanager receivers\n  - name: silence\n    description: Everything related to Alertmanager silences\n  - name: alert\n    description: Everything related to Alertmanager alerts\n"
  },
  {
    "path": "api/v2/restapi/configure_alertmanager.go",
    "content": "// This file is safe to edit. Once it exists it will not be overwritten\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage restapi\n\nimport (\n\t\"crypto/tls\"\n\t\"net/http\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime\"\n\t\"github.com/go-openapi/runtime/middleware\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/restapi/operations\"\n\t\"github.com/prometheus/alertmanager/api/v2/restapi/operations/alert\"\n\t\"github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroup\"\n\t\"github.com/prometheus/alertmanager/api/v2/restapi/operations/general\"\n\t\"github.com/prometheus/alertmanager/api/v2/restapi/operations/receiver\"\n\t\"github.com/prometheus/alertmanager/api/v2/restapi/operations/silence\"\n)\n\n//go:generate swagger generate server --target ../../v2 --name Alertmanager --spec ../openapi.yaml --principal any --exclude-main\n\nfunc configureFlags(api *operations.AlertmanagerAPI) {\n\t// api.CommandLineOptionsGroups = []swag.CommandLineOptionsGroup{ ... }\n\t_ = api\n}\n\nfunc configureAPI(api *operations.AlertmanagerAPI) http.Handler {\n\t// configure the api here\n\tapi.ServeError = errors.ServeError\n\n\t// Set your custom logger if needed. Default one is log.Printf\n\t// Expected interface func(string, ...any)\n\t//\n\t// Example:\n\t// api.Logger = log.Printf\n\n\tapi.UseSwaggerUI()\n\t// To continue using redoc as your UI, uncomment the following line\n\t// api.UseRedoc()\n\n\tapi.JSONConsumer = runtime.JSONConsumer()\n\n\tapi.JSONProducer = runtime.JSONProducer()\n\n\tif api.SilenceDeleteSilenceHandler == nil {\n\t\tapi.SilenceDeleteSilenceHandler = silence.DeleteSilenceHandlerFunc(func(params silence.DeleteSilenceParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation silence.DeleteSilence has not yet been implemented\")\n\t\t})\n\t}\n\tif api.AlertgroupGetAlertGroupsHandler == nil {\n\t\tapi.AlertgroupGetAlertGroupsHandler = alertgroup.GetAlertGroupsHandlerFunc(func(params alertgroup.GetAlertGroupsParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation alertgroup.GetAlertGroups has not yet been implemented\")\n\t\t})\n\t}\n\tif api.AlertGetAlertsHandler == nil {\n\t\tapi.AlertGetAlertsHandler = alert.GetAlertsHandlerFunc(func(params alert.GetAlertsParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation alert.GetAlerts has not yet been implemented\")\n\t\t})\n\t}\n\tif api.ReceiverGetReceiversHandler == nil {\n\t\tapi.ReceiverGetReceiversHandler = receiver.GetReceiversHandlerFunc(func(params receiver.GetReceiversParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation receiver.GetReceivers has not yet been implemented\")\n\t\t})\n\t}\n\tif api.SilenceGetSilenceHandler == nil {\n\t\tapi.SilenceGetSilenceHandler = silence.GetSilenceHandlerFunc(func(params silence.GetSilenceParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation silence.GetSilence has not yet been implemented\")\n\t\t})\n\t}\n\tif api.SilenceGetSilencesHandler == nil {\n\t\tapi.SilenceGetSilencesHandler = silence.GetSilencesHandlerFunc(func(params silence.GetSilencesParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation silence.GetSilences has not yet been implemented\")\n\t\t})\n\t}\n\tif api.GeneralGetStatusHandler == nil {\n\t\tapi.GeneralGetStatusHandler = general.GetStatusHandlerFunc(func(params general.GetStatusParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation general.GetStatus has not yet been implemented\")\n\t\t})\n\t}\n\tif api.AlertPostAlertsHandler == nil {\n\t\tapi.AlertPostAlertsHandler = alert.PostAlertsHandlerFunc(func(params alert.PostAlertsParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation alert.PostAlerts has not yet been implemented\")\n\t\t})\n\t}\n\tif api.SilencePostSilencesHandler == nil {\n\t\tapi.SilencePostSilencesHandler = silence.PostSilencesHandlerFunc(func(params silence.PostSilencesParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation silence.PostSilences has not yet been implemented\")\n\t\t})\n\t}\n\n\tapi.PreServerShutdown = func() {}\n\n\tapi.ServerShutdown = func() {}\n\n\treturn setupGlobalMiddleware(api.Serve(setupMiddlewares))\n}\n\n// The TLS configuration before HTTPS server starts.\nfunc configureTLS(tlsConfig *tls.Config) {\n\t// Make all necessary changes to the TLS configuration here.\n\t_ = tlsConfig\n}\n\n// As soon as server is initialized but not run yet, this function will be called.\n// If you need to modify a config, store server instance to stop it individually later, this is the place.\n// This function can be called multiple times, depending on the number of serving schemes.\n// scheme value will be set accordingly: \"http\", \"https\" or \"unix\".\nfunc configureServer(server *http.Server, scheme, addr string) {\n\t_ = server\n\t_ = scheme\n\t_ = addr\n}\n\n// The middleware configuration is for the handler executors. These do not apply to the swagger.json document.\n// The middleware executes after routing but before authentication, binding and validation.\nfunc setupMiddlewares(handler http.Handler) http.Handler {\n\treturn handler\n}\n\n// The middleware configuration happens before anything, this middleware also applies to serving the swagger.json document.\n// So this is a good place to plug in a panic handling middleware, logging and metrics.\nfunc setupGlobalMiddleware(handler http.Handler) http.Handler {\n\treturn handler\n}\n"
  },
  {
    "path": "api/v2/restapi/doc.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\n// Package restapi Alertmanager API\n//\n//\tAPI of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n//\tSchemes:\n//\t  http\n//\tHost: localhost\n//\tBasePath: /api/v2/\n//\tVersion: 0.0.1\n//\tLicense: Apache 2.0 http://www.apache.org/licenses/LICENSE-2.0.html\n//\n//\tConsumes:\n//\t  - application/json\n//\n//\tProduces:\n//\t  - application/json\n//\n// swagger:meta\npackage restapi\n"
  },
  {
    "path": "api/v2/restapi/embedded_spec.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage restapi\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"encoding/json\"\n)\n\nvar (\n\t// SwaggerJSON embedded version of the swagger document used at generation time\n\tSwaggerJSON json.RawMessage\n\t// FlatSwaggerJSON embedded flattened version of the swagger document used at generation time\n\tFlatSwaggerJSON json.RawMessage\n)\n\nfunc init() {\n\tSwaggerJSON = json.RawMessage([]byte(`{\n  \"consumes\": [\n    \"application/json\"\n  ],\n  \"produces\": [\n    \"application/json\"\n  ],\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"description\": \"API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\",\n    \"title\": \"Alertmanager API\",\n    \"license\": {\n      \"name\": \"Apache 2.0\",\n      \"url\": \"http://www.apache.org/licenses/LICENSE-2.0.html\"\n    },\n    \"version\": \"0.0.1\"\n  },\n  \"basePath\": \"/api/v2/\",\n  \"paths\": {\n    \"/alerts\": {\n      \"get\": {\n        \"description\": \"Get a list of alerts\",\n        \"tags\": [\n          \"alert\"\n        ],\n        \"operationId\": \"getAlerts\",\n        \"parameters\": [\n          {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Include active alerts in results. If false, excludes active alerts and returns only suppressed (silenced or inhibited) alerts.\",\n            \"name\": \"active\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Include silenced alerts in results. If false, excludes silenced alerts. Note that true (default) shows both silenced and non-silenced alerts.\",\n            \"name\": \"silenced\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Include inhibited alerts in results. If false, excludes inhibited alerts. Note that true (default) shows both inhibited and non-inhibited alerts.\",\n            \"name\": \"inhibited\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Include unprocessed alerts in results. If false, excludes unprocessed alerts. Note that true (default) shows both processed and unprocessed alerts.\",\n            \"name\": \"unprocessed\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            },\n            \"collectionFormat\": \"multi\",\n            \"description\": \"A matcher expression to filter alerts. For example ` + \"`\" + `alertname=\\\"MyAlert\\\"` + \"`\" + `. It can be repeated to apply multiple matchers.\",\n            \"name\": \"filter\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"string\",\n            \"description\": \"A regex matching receivers to filter alerts by\",\n            \"name\": \"receiver\",\n            \"in\": \"query\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get alerts response\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/gettableAlerts\"\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/responses/BadRequest\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/InternalServerError\"\n          }\n        }\n      },\n      \"post\": {\n        \"description\": \"Create new Alerts\",\n        \"tags\": [\n          \"alert\"\n        ],\n        \"operationId\": \"postAlerts\",\n        \"parameters\": [\n          {\n            \"description\": \"The alerts to create\",\n            \"name\": \"alerts\",\n            \"in\": \"body\",\n            \"required\": true,\n            \"schema\": {\n              \"$ref\": \"#/definitions/postableAlerts\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Create alerts response\"\n          },\n          \"400\": {\n            \"$ref\": \"#/responses/BadRequest\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/InternalServerError\"\n          }\n        }\n      }\n    },\n    \"/alerts/groups\": {\n      \"get\": {\n        \"description\": \"Get a list of alert groups\",\n        \"tags\": [\n          \"alertgroup\"\n        ],\n        \"operationId\": \"getAlertGroups\",\n        \"parameters\": [\n          {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Include active alerts within the returned groups. If false, excludes active alerts from groups and only shows suppressed (silenced or inhibited) alerts.\",\n            \"name\": \"active\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Include silenced alerts within the returned groups. If false, excludes silenced alerts from groups. Note that true (default) shows both silenced and non-silenced alerts.\",\n            \"name\": \"silenced\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Include inhibited alerts within the returned groups. If false, excludes inhibited alerts from groups. Note that true (default) shows both inhibited and non-inhibited alerts.\",\n            \"name\": \"inhibited\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Include muted (silenced or inhibited) alert groups in results. If false, excludes entire groups where all alerts are muted.\",\n            \"name\": \"muted\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            },\n            \"collectionFormat\": \"multi\",\n            \"description\": \"A matcher expression to filter alert groups. For example ` + \"`\" + `alertname=\\\"MyAlert\\\"` + \"`\" + `. It can be repeated to apply multiple matchers.\",\n            \"name\": \"filter\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"string\",\n            \"description\": \"A regex matching receivers to filter alerts by\",\n            \"name\": \"receiver\",\n            \"in\": \"query\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get alert groups response\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/alertGroups\"\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/responses/BadRequest\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/InternalServerError\"\n          }\n        }\n      }\n    },\n    \"/receivers\": {\n      \"get\": {\n        \"description\": \"Get list of all receivers (name of notification integrations)\",\n        \"tags\": [\n          \"receiver\"\n        ],\n        \"operationId\": \"getReceivers\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get receivers response\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"$ref\": \"#/definitions/receiver\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/silence/{silenceID}\": {\n      \"get\": {\n        \"description\": \"Get a silence by its ID\",\n        \"tags\": [\n          \"silence\"\n        ],\n        \"operationId\": \"getSilence\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get silence response\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/gettableSilence\"\n            }\n          },\n          \"404\": {\n            \"description\": \"A silence with the specified ID was not found\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/InternalServerError\"\n          }\n        }\n      },\n      \"delete\": {\n        \"description\": \"Delete a silence by its ID\",\n        \"tags\": [\n          \"silence\"\n        ],\n        \"operationId\": \"deleteSilence\",\n        \"parameters\": [\n          {\n            \"type\": \"string\",\n            \"format\": \"uuid\",\n            \"description\": \"ID of the silence to get\",\n            \"name\": \"silenceID\",\n            \"in\": \"path\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Delete silence response\"\n          },\n          \"404\": {\n            \"description\": \"A silence with the specified ID was not found\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/InternalServerError\"\n          }\n        }\n      },\n      \"parameters\": [\n        {\n          \"type\": \"string\",\n          \"format\": \"uuid\",\n          \"description\": \"ID of the silence to get\",\n          \"name\": \"silenceID\",\n          \"in\": \"path\",\n          \"required\": true\n        }\n      ]\n    },\n    \"/silences\": {\n      \"get\": {\n        \"description\": \"Get a list of silences\",\n        \"tags\": [\n          \"silence\"\n        ],\n        \"operationId\": \"getSilences\",\n        \"parameters\": [\n          {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            },\n            \"collectionFormat\": \"multi\",\n            \"description\": \"A matcher expression to filter silences. For example ` + \"`\" + `alertname=\\\"MyAlert\\\"` + \"`\" + `. It can be repeated to apply multiple matchers.\",\n            \"name\": \"filter\",\n            \"in\": \"query\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get silences response\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/gettableSilences\"\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/responses/BadRequest\"\n          },\n          \"500\": {\n            \"$ref\": \"#/responses/InternalServerError\"\n          }\n        }\n      },\n      \"post\": {\n        \"description\": \"Post a new silence or update an existing one\",\n        \"tags\": [\n          \"silence\"\n        ],\n        \"operationId\": \"postSilences\",\n        \"parameters\": [\n          {\n            \"description\": \"The silence to create\",\n            \"name\": \"silence\",\n            \"in\": \"body\",\n            \"required\": true,\n            \"schema\": {\n              \"$ref\": \"#/definitions/postableSilence\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Create / update silence response\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"silenceID\": {\n                  \"type\": \"string\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"$ref\": \"#/responses/BadRequest\"\n          },\n          \"404\": {\n            \"description\": \"A silence with the specified ID was not found\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      }\n    },\n    \"/status\": {\n      \"get\": {\n        \"description\": \"Get current status of an Alertmanager instance and its cluster\",\n        \"tags\": [\n          \"general\"\n        ],\n        \"operationId\": \"getStatus\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get status response\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/alertmanagerStatus\"\n            }\n          }\n        }\n      }\n    }\n  },\n  \"definitions\": {\n    \"alert\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"labels\"\n      ],\n      \"properties\": {\n        \"generatorURL\": {\n          \"type\": \"string\",\n          \"format\": \"uri\"\n        },\n        \"labels\": {\n          \"$ref\": \"#/definitions/labelSet\"\n        }\n      }\n    },\n    \"alertGroup\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"labels\",\n        \"receiver\",\n        \"alerts\"\n      ],\n      \"properties\": {\n        \"alerts\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/gettableAlert\"\n          }\n        },\n        \"labels\": {\n          \"$ref\": \"#/definitions/labelSet\"\n        },\n        \"receiver\": {\n          \"$ref\": \"#/definitions/receiver\"\n        }\n      }\n    },\n    \"alertGroups\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/alertGroup\"\n      }\n    },\n    \"alertStatus\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"state\",\n        \"silencedBy\",\n        \"inhibitedBy\",\n        \"mutedBy\"\n      ],\n      \"properties\": {\n        \"inhibitedBy\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"mutedBy\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"silencedBy\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"state\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"unprocessed\",\n            \"active\",\n            \"suppressed\"\n          ]\n        }\n      }\n    },\n    \"alertmanagerConfig\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"original\"\n      ],\n      \"properties\": {\n        \"original\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"alertmanagerStatus\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"cluster\",\n        \"versionInfo\",\n        \"config\",\n        \"uptime\"\n      ],\n      \"properties\": {\n        \"cluster\": {\n          \"$ref\": \"#/definitions/clusterStatus\"\n        },\n        \"config\": {\n          \"$ref\": \"#/definitions/alertmanagerConfig\"\n        },\n        \"uptime\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"versionInfo\": {\n          \"$ref\": \"#/definitions/versionInfo\"\n        }\n      }\n    },\n    \"clusterStatus\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"status\"\n      ],\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"peers\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/peerStatus\"\n          }\n        },\n        \"status\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"ready\",\n            \"settling\",\n            \"disabled\"\n          ]\n        }\n      }\n    },\n    \"gettableAlert\": {\n      \"allOf\": [\n        {\n          \"type\": \"object\",\n          \"required\": [\n            \"receivers\",\n            \"fingerprint\",\n            \"startsAt\",\n            \"updatedAt\",\n            \"endsAt\",\n            \"annotations\",\n            \"status\"\n          ],\n          \"properties\": {\n            \"annotations\": {\n              \"$ref\": \"#/definitions/labelSet\"\n            },\n            \"endsAt\": {\n              \"type\": \"string\",\n              \"format\": \"date-time\"\n            },\n            \"fingerprint\": {\n              \"type\": \"string\"\n            },\n            \"receivers\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"$ref\": \"#/definitions/receiver\"\n              }\n            },\n            \"startsAt\": {\n              \"type\": \"string\",\n              \"format\": \"date-time\"\n            },\n            \"status\": {\n              \"$ref\": \"#/definitions/alertStatus\"\n            },\n            \"updatedAt\": {\n              \"type\": \"string\",\n              \"format\": \"date-time\"\n            }\n          }\n        },\n        {\n          \"$ref\": \"#/definitions/alert\"\n        }\n      ]\n    },\n    \"gettableAlerts\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/gettableAlert\"\n      }\n    },\n    \"gettableSilence\": {\n      \"allOf\": [\n        {\n          \"type\": \"object\",\n          \"required\": [\n            \"id\",\n            \"status\",\n            \"updatedAt\",\n            \"annotations\"\n          ],\n          \"properties\": {\n            \"id\": {\n              \"type\": \"string\"\n            },\n            \"status\": {\n              \"$ref\": \"#/definitions/silenceStatus\"\n            },\n            \"updatedAt\": {\n              \"type\": \"string\",\n              \"format\": \"date-time\"\n            }\n          }\n        },\n        {\n          \"$ref\": \"#/definitions/silence\"\n        }\n      ]\n    },\n    \"gettableSilences\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/gettableSilence\"\n      }\n    },\n    \"labelSet\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"type\": \"string\"\n      }\n    },\n    \"matcher\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"value\",\n        \"isRegex\"\n      ],\n      \"properties\": {\n        \"isEqual\": {\n          \"type\": \"boolean\",\n          \"default\": true\n        },\n        \"isRegex\": {\n          \"type\": \"boolean\"\n        },\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"value\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"matchers\": {\n      \"type\": \"array\",\n      \"minItems\": 1,\n      \"items\": {\n        \"$ref\": \"#/definitions/matcher\"\n      }\n    },\n    \"peerStatus\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"address\"\n      ],\n      \"properties\": {\n        \"address\": {\n          \"type\": \"string\"\n        },\n        \"name\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"postableAlert\": {\n      \"allOf\": [\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"annotations\": {\n              \"$ref\": \"#/definitions/labelSet\"\n            },\n            \"endsAt\": {\n              \"type\": \"string\",\n              \"format\": \"date-time\"\n            },\n            \"startsAt\": {\n              \"type\": \"string\",\n              \"format\": \"date-time\"\n            }\n          }\n        },\n        {\n          \"$ref\": \"#/definitions/alert\"\n        }\n      ]\n    },\n    \"postableAlerts\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/postableAlert\"\n      }\n    },\n    \"postableSilence\": {\n      \"allOf\": [\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"id\": {\n              \"type\": \"string\"\n            }\n          }\n        },\n        {\n          \"$ref\": \"#/definitions/silence\"\n        }\n      ]\n    },\n    \"receiver\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"name\"\n      ],\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"silence\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"matchers\",\n        \"startsAt\",\n        \"endsAt\",\n        \"createdBy\",\n        \"comment\"\n      ],\n      \"properties\": {\n        \"annotations\": {\n          \"$ref\": \"#/definitions/labelSet\"\n        },\n        \"comment\": {\n          \"type\": \"string\"\n        },\n        \"createdBy\": {\n          \"type\": \"string\"\n        },\n        \"endsAt\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"matchers\": {\n          \"$ref\": \"#/definitions/matchers\"\n        },\n        \"startsAt\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        }\n      }\n    },\n    \"silenceStatus\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"state\"\n      ],\n      \"properties\": {\n        \"state\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"expired\",\n            \"active\",\n            \"pending\"\n          ]\n        }\n      }\n    },\n    \"versionInfo\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"version\",\n        \"revision\",\n        \"branch\",\n        \"buildUser\",\n        \"buildDate\",\n        \"goVersion\"\n      ],\n      \"properties\": {\n        \"branch\": {\n          \"type\": \"string\"\n        },\n        \"buildDate\": {\n          \"type\": \"string\"\n        },\n        \"buildUser\": {\n          \"type\": \"string\"\n        },\n        \"goVersion\": {\n          \"type\": \"string\"\n        },\n        \"revision\": {\n          \"type\": \"string\"\n        },\n        \"version\": {\n          \"type\": \"string\"\n        }\n      }\n    }\n  },\n  \"responses\": {\n    \"BadRequest\": {\n      \"description\": \"Bad request\",\n      \"schema\": {\n        \"type\": \"string\"\n      }\n    },\n    \"InternalServerError\": {\n      \"description\": \"Internal server error\",\n      \"schema\": {\n        \"type\": \"string\"\n      }\n    }\n  },\n  \"tags\": [\n    {\n      \"description\": \"General Alertmanager operations\",\n      \"name\": \"general\"\n    },\n    {\n      \"description\": \"Everything related to Alertmanager receivers\",\n      \"name\": \"receiver\"\n    },\n    {\n      \"description\": \"Everything related to Alertmanager silences\",\n      \"name\": \"silence\"\n    },\n    {\n      \"description\": \"Everything related to Alertmanager alerts\",\n      \"name\": \"alert\"\n    }\n  ]\n}`))\n\tFlatSwaggerJSON = json.RawMessage([]byte(`{\n  \"consumes\": [\n    \"application/json\"\n  ],\n  \"produces\": [\n    \"application/json\"\n  ],\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"description\": \"API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\",\n    \"title\": \"Alertmanager API\",\n    \"license\": {\n      \"name\": \"Apache 2.0\",\n      \"url\": \"http://www.apache.org/licenses/LICENSE-2.0.html\"\n    },\n    \"version\": \"0.0.1\"\n  },\n  \"basePath\": \"/api/v2/\",\n  \"paths\": {\n    \"/alerts\": {\n      \"get\": {\n        \"description\": \"Get a list of alerts\",\n        \"tags\": [\n          \"alert\"\n        ],\n        \"operationId\": \"getAlerts\",\n        \"parameters\": [\n          {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Include active alerts in results. If false, excludes active alerts and returns only suppressed (silenced or inhibited) alerts.\",\n            \"name\": \"active\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Include silenced alerts in results. If false, excludes silenced alerts. Note that true (default) shows both silenced and non-silenced alerts.\",\n            \"name\": \"silenced\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Include inhibited alerts in results. If false, excludes inhibited alerts. Note that true (default) shows both inhibited and non-inhibited alerts.\",\n            \"name\": \"inhibited\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Include unprocessed alerts in results. If false, excludes unprocessed alerts. Note that true (default) shows both processed and unprocessed alerts.\",\n            \"name\": \"unprocessed\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            },\n            \"collectionFormat\": \"multi\",\n            \"description\": \"A matcher expression to filter alerts. For example ` + \"`\" + `alertname=\\\"MyAlert\\\"` + \"`\" + `. It can be repeated to apply multiple matchers.\",\n            \"name\": \"filter\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"string\",\n            \"description\": \"A regex matching receivers to filter alerts by\",\n            \"name\": \"receiver\",\n            \"in\": \"query\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get alerts response\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/gettableAlerts\"\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad request\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal server error\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"description\": \"Create new Alerts\",\n        \"tags\": [\n          \"alert\"\n        ],\n        \"operationId\": \"postAlerts\",\n        \"parameters\": [\n          {\n            \"description\": \"The alerts to create\",\n            \"name\": \"alerts\",\n            \"in\": \"body\",\n            \"required\": true,\n            \"schema\": {\n              \"$ref\": \"#/definitions/postableAlerts\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Create alerts response\"\n          },\n          \"400\": {\n            \"description\": \"Bad request\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal server error\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      }\n    },\n    \"/alerts/groups\": {\n      \"get\": {\n        \"description\": \"Get a list of alert groups\",\n        \"tags\": [\n          \"alertgroup\"\n        ],\n        \"operationId\": \"getAlertGroups\",\n        \"parameters\": [\n          {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Include active alerts within the returned groups. If false, excludes active alerts from groups and only shows suppressed (silenced or inhibited) alerts.\",\n            \"name\": \"active\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Include silenced alerts within the returned groups. If false, excludes silenced alerts from groups. Note that true (default) shows both silenced and non-silenced alerts.\",\n            \"name\": \"silenced\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Include inhibited alerts within the returned groups. If false, excludes inhibited alerts from groups. Note that true (default) shows both inhibited and non-inhibited alerts.\",\n            \"name\": \"inhibited\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Include muted (silenced or inhibited) alert groups in results. If false, excludes entire groups where all alerts are muted.\",\n            \"name\": \"muted\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            },\n            \"collectionFormat\": \"multi\",\n            \"description\": \"A matcher expression to filter alert groups. For example ` + \"`\" + `alertname=\\\"MyAlert\\\"` + \"`\" + `. It can be repeated to apply multiple matchers.\",\n            \"name\": \"filter\",\n            \"in\": \"query\"\n          },\n          {\n            \"type\": \"string\",\n            \"description\": \"A regex matching receivers to filter alerts by\",\n            \"name\": \"receiver\",\n            \"in\": \"query\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get alert groups response\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/alertGroups\"\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad request\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal server error\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      }\n    },\n    \"/receivers\": {\n      \"get\": {\n        \"description\": \"Get list of all receivers (name of notification integrations)\",\n        \"tags\": [\n          \"receiver\"\n        ],\n        \"operationId\": \"getReceivers\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get receivers response\",\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"$ref\": \"#/definitions/receiver\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/silence/{silenceID}\": {\n      \"get\": {\n        \"description\": \"Get a silence by its ID\",\n        \"tags\": [\n          \"silence\"\n        ],\n        \"operationId\": \"getSilence\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get silence response\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/gettableSilence\"\n            }\n          },\n          \"404\": {\n            \"description\": \"A silence with the specified ID was not found\"\n          },\n          \"500\": {\n            \"description\": \"Internal server error\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"description\": \"Delete a silence by its ID\",\n        \"tags\": [\n          \"silence\"\n        ],\n        \"operationId\": \"deleteSilence\",\n        \"parameters\": [\n          {\n            \"type\": \"string\",\n            \"format\": \"uuid\",\n            \"description\": \"ID of the silence to get\",\n            \"name\": \"silenceID\",\n            \"in\": \"path\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Delete silence response\"\n          },\n          \"404\": {\n            \"description\": \"A silence with the specified ID was not found\"\n          },\n          \"500\": {\n            \"description\": \"Internal server error\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"parameters\": [\n        {\n          \"type\": \"string\",\n          \"format\": \"uuid\",\n          \"description\": \"ID of the silence to get\",\n          \"name\": \"silenceID\",\n          \"in\": \"path\",\n          \"required\": true\n        }\n      ]\n    },\n    \"/silences\": {\n      \"get\": {\n        \"description\": \"Get a list of silences\",\n        \"tags\": [\n          \"silence\"\n        ],\n        \"operationId\": \"getSilences\",\n        \"parameters\": [\n          {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            },\n            \"collectionFormat\": \"multi\",\n            \"description\": \"A matcher expression to filter silences. For example ` + \"`\" + `alertname=\\\"MyAlert\\\"` + \"`\" + `. It can be repeated to apply multiple matchers.\",\n            \"name\": \"filter\",\n            \"in\": \"query\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get silences response\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/gettableSilences\"\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad request\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal server error\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"description\": \"Post a new silence or update an existing one\",\n        \"tags\": [\n          \"silence\"\n        ],\n        \"operationId\": \"postSilences\",\n        \"parameters\": [\n          {\n            \"description\": \"The silence to create\",\n            \"name\": \"silence\",\n            \"in\": \"body\",\n            \"required\": true,\n            \"schema\": {\n              \"$ref\": \"#/definitions/postableSilence\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Create / update silence response\",\n            \"schema\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"silenceID\": {\n                  \"type\": \"string\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad request\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          \"404\": {\n            \"description\": \"A silence with the specified ID was not found\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      }\n    },\n    \"/status\": {\n      \"get\": {\n        \"description\": \"Get current status of an Alertmanager instance and its cluster\",\n        \"tags\": [\n          \"general\"\n        ],\n        \"operationId\": \"getStatus\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Get status response\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/alertmanagerStatus\"\n            }\n          }\n        }\n      }\n    }\n  },\n  \"definitions\": {\n    \"alert\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"labels\"\n      ],\n      \"properties\": {\n        \"generatorURL\": {\n          \"type\": \"string\",\n          \"format\": \"uri\"\n        },\n        \"labels\": {\n          \"$ref\": \"#/definitions/labelSet\"\n        }\n      }\n    },\n    \"alertGroup\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"labels\",\n        \"receiver\",\n        \"alerts\"\n      ],\n      \"properties\": {\n        \"alerts\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/gettableAlert\"\n          }\n        },\n        \"labels\": {\n          \"$ref\": \"#/definitions/labelSet\"\n        },\n        \"receiver\": {\n          \"$ref\": \"#/definitions/receiver\"\n        }\n      }\n    },\n    \"alertGroups\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/alertGroup\"\n      }\n    },\n    \"alertStatus\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"state\",\n        \"silencedBy\",\n        \"inhibitedBy\",\n        \"mutedBy\"\n      ],\n      \"properties\": {\n        \"inhibitedBy\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"mutedBy\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"silencedBy\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"state\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"unprocessed\",\n            \"active\",\n            \"suppressed\"\n          ]\n        }\n      }\n    },\n    \"alertmanagerConfig\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"original\"\n      ],\n      \"properties\": {\n        \"original\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"alertmanagerStatus\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"cluster\",\n        \"versionInfo\",\n        \"config\",\n        \"uptime\"\n      ],\n      \"properties\": {\n        \"cluster\": {\n          \"$ref\": \"#/definitions/clusterStatus\"\n        },\n        \"config\": {\n          \"$ref\": \"#/definitions/alertmanagerConfig\"\n        },\n        \"uptime\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"versionInfo\": {\n          \"$ref\": \"#/definitions/versionInfo\"\n        }\n      }\n    },\n    \"clusterStatus\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"status\"\n      ],\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"peers\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/peerStatus\"\n          }\n        },\n        \"status\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"ready\",\n            \"settling\",\n            \"disabled\"\n          ]\n        }\n      }\n    },\n    \"gettableAlert\": {\n      \"allOf\": [\n        {\n          \"type\": \"object\",\n          \"required\": [\n            \"receivers\",\n            \"fingerprint\",\n            \"startsAt\",\n            \"updatedAt\",\n            \"endsAt\",\n            \"annotations\",\n            \"status\"\n          ],\n          \"properties\": {\n            \"annotations\": {\n              \"$ref\": \"#/definitions/labelSet\"\n            },\n            \"endsAt\": {\n              \"type\": \"string\",\n              \"format\": \"date-time\"\n            },\n            \"fingerprint\": {\n              \"type\": \"string\"\n            },\n            \"receivers\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"$ref\": \"#/definitions/receiver\"\n              }\n            },\n            \"startsAt\": {\n              \"type\": \"string\",\n              \"format\": \"date-time\"\n            },\n            \"status\": {\n              \"$ref\": \"#/definitions/alertStatus\"\n            },\n            \"updatedAt\": {\n              \"type\": \"string\",\n              \"format\": \"date-time\"\n            }\n          }\n        },\n        {\n          \"$ref\": \"#/definitions/alert\"\n        }\n      ]\n    },\n    \"gettableAlerts\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/gettableAlert\"\n      }\n    },\n    \"gettableSilence\": {\n      \"allOf\": [\n        {\n          \"type\": \"object\",\n          \"required\": [\n            \"id\",\n            \"status\",\n            \"updatedAt\",\n            \"annotations\"\n          ],\n          \"properties\": {\n            \"id\": {\n              \"type\": \"string\"\n            },\n            \"status\": {\n              \"$ref\": \"#/definitions/silenceStatus\"\n            },\n            \"updatedAt\": {\n              \"type\": \"string\",\n              \"format\": \"date-time\"\n            }\n          }\n        },\n        {\n          \"$ref\": \"#/definitions/silence\"\n        }\n      ]\n    },\n    \"gettableSilences\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/gettableSilence\"\n      }\n    },\n    \"labelSet\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"type\": \"string\"\n      }\n    },\n    \"matcher\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"value\",\n        \"isRegex\"\n      ],\n      \"properties\": {\n        \"isEqual\": {\n          \"type\": \"boolean\",\n          \"default\": true\n        },\n        \"isRegex\": {\n          \"type\": \"boolean\"\n        },\n        \"name\": {\n          \"type\": \"string\"\n        },\n        \"value\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"matchers\": {\n      \"type\": \"array\",\n      \"minItems\": 1,\n      \"items\": {\n        \"$ref\": \"#/definitions/matcher\"\n      }\n    },\n    \"peerStatus\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"name\",\n        \"address\"\n      ],\n      \"properties\": {\n        \"address\": {\n          \"type\": \"string\"\n        },\n        \"name\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"postableAlert\": {\n      \"allOf\": [\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"annotations\": {\n              \"$ref\": \"#/definitions/labelSet\"\n            },\n            \"endsAt\": {\n              \"type\": \"string\",\n              \"format\": \"date-time\"\n            },\n            \"startsAt\": {\n              \"type\": \"string\",\n              \"format\": \"date-time\"\n            }\n          }\n        },\n        {\n          \"$ref\": \"#/definitions/alert\"\n        }\n      ]\n    },\n    \"postableAlerts\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/postableAlert\"\n      }\n    },\n    \"postableSilence\": {\n      \"allOf\": [\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"id\": {\n              \"type\": \"string\"\n            }\n          }\n        },\n        {\n          \"$ref\": \"#/definitions/silence\"\n        }\n      ]\n    },\n    \"receiver\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"name\"\n      ],\n      \"properties\": {\n        \"name\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"silence\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"matchers\",\n        \"startsAt\",\n        \"endsAt\",\n        \"createdBy\",\n        \"comment\"\n      ],\n      \"properties\": {\n        \"annotations\": {\n          \"$ref\": \"#/definitions/labelSet\"\n        },\n        \"comment\": {\n          \"type\": \"string\"\n        },\n        \"createdBy\": {\n          \"type\": \"string\"\n        },\n        \"endsAt\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"matchers\": {\n          \"$ref\": \"#/definitions/matchers\"\n        },\n        \"startsAt\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        }\n      }\n    },\n    \"silenceStatus\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"state\"\n      ],\n      \"properties\": {\n        \"state\": {\n          \"type\": \"string\",\n          \"enum\": [\n            \"expired\",\n            \"active\",\n            \"pending\"\n          ]\n        }\n      }\n    },\n    \"versionInfo\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"version\",\n        \"revision\",\n        \"branch\",\n        \"buildUser\",\n        \"buildDate\",\n        \"goVersion\"\n      ],\n      \"properties\": {\n        \"branch\": {\n          \"type\": \"string\"\n        },\n        \"buildDate\": {\n          \"type\": \"string\"\n        },\n        \"buildUser\": {\n          \"type\": \"string\"\n        },\n        \"goVersion\": {\n          \"type\": \"string\"\n        },\n        \"revision\": {\n          \"type\": \"string\"\n        },\n        \"version\": {\n          \"type\": \"string\"\n        }\n      }\n    }\n  },\n  \"responses\": {\n    \"BadRequest\": {\n      \"description\": \"Bad request\",\n      \"schema\": {\n        \"type\": \"string\"\n      }\n    },\n    \"InternalServerError\": {\n      \"description\": \"Internal server error\",\n      \"schema\": {\n        \"type\": \"string\"\n      }\n    }\n  },\n  \"tags\": [\n    {\n      \"description\": \"General Alertmanager operations\",\n      \"name\": \"general\"\n    },\n    {\n      \"description\": \"Everything related to Alertmanager receivers\",\n      \"name\": \"receiver\"\n    },\n    {\n      \"description\": \"Everything related to Alertmanager silences\",\n      \"name\": \"silence\"\n    },\n    {\n      \"description\": \"Everything related to Alertmanager alerts\",\n      \"name\": \"alert\"\n    }\n  ]\n}`))\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/alert/get_alerts.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alert\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime/middleware\"\n)\n\n// GetAlertsHandlerFunc turns a function with the right signature into a get alerts handler\ntype GetAlertsHandlerFunc func(GetAlertsParams) middleware.Responder\n\n// Handle executing the request and returning a response\nfunc (fn GetAlertsHandlerFunc) Handle(params GetAlertsParams) middleware.Responder {\n\treturn fn(params)\n}\n\n// GetAlertsHandler interface for that can handle valid get alerts params\ntype GetAlertsHandler interface {\n\tHandle(GetAlertsParams) middleware.Responder\n}\n\n// NewGetAlerts creates a new http.Handler for the get alerts operation\nfunc NewGetAlerts(ctx *middleware.Context, handler GetAlertsHandler) *GetAlerts {\n\treturn &GetAlerts{Context: ctx, Handler: handler}\n}\n\n/*\n\tGetAlerts swagger:route GET /alerts alert getAlerts\n\nGet a list of alerts\n*/\ntype GetAlerts struct {\n\tContext *middleware.Context\n\tHandler GetAlertsHandler\n}\n\nfunc (o *GetAlerts) ServeHTTP(rw http.ResponseWriter, r *http.Request) {\n\troute, rCtx, _ := o.Context.RouteInfo(r)\n\tif rCtx != nil {\n\t\t*r = *rCtx\n\t}\n\tvar Params = NewGetAlertsParams()\n\tif err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params\n\t\to.Context.Respond(rw, r, route.Produces, route, err)\n\t\treturn\n\t}\n\n\tres := o.Handler.Handle(Params) // actually handle the request\n\n\to.Context.Respond(rw, r, route.Produces, route, res)\n\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/alert/get_alerts_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alert\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime\"\n\t\"github.com/go-openapi/runtime/middleware\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n)\n\n// NewGetAlertsParams creates a new GetAlertsParams object\n// with the default values initialized.\nfunc NewGetAlertsParams() GetAlertsParams {\n\n\tvar (\n\t\t// initialize parameters with default values\n\n\t\tactiveDefault = bool(true)\n\n\t\tinhibitedDefault = bool(true)\n\n\t\tsilencedDefault    = bool(true)\n\t\tunprocessedDefault = bool(true)\n\t)\n\n\treturn GetAlertsParams{\n\t\tActive: &activeDefault,\n\n\t\tInhibited: &inhibitedDefault,\n\n\t\tSilenced: &silencedDefault,\n\n\t\tUnprocessed: &unprocessedDefault,\n\t}\n}\n\n// GetAlertsParams contains all the bound params for the get alerts operation\n// typically these are obtained from a http.Request\n//\n// swagger:parameters getAlerts\ntype GetAlertsParams struct {\n\t// HTTP Request Object\n\tHTTPRequest *http.Request `json:\"-\"`\n\n\t/*Include active alerts in results. If false, excludes active alerts and returns only suppressed (silenced or inhibited) alerts.\n\t  In: query\n\t  Default: true\n\t*/\n\tActive *bool\n\n\t/*A matcher expression to filter alerts. For example `alertname=\"MyAlert\"`. It can be repeated to apply multiple matchers.\n\t  In: query\n\t  Collection Format: multi\n\t*/\n\tFilter []string\n\n\t/*Include inhibited alerts in results. If false, excludes inhibited alerts. Note that true (default) shows both inhibited and non-inhibited alerts.\n\t  In: query\n\t  Default: true\n\t*/\n\tInhibited *bool\n\n\t/*A regex matching receivers to filter alerts by\n\t  In: query\n\t*/\n\tReceiver *string\n\n\t/*Include silenced alerts in results. If false, excludes silenced alerts. Note that true (default) shows both silenced and non-silenced alerts.\n\t  In: query\n\t  Default: true\n\t*/\n\tSilenced *bool\n\n\t/*Include unprocessed alerts in results. If false, excludes unprocessed alerts. Note that true (default) shows both processed and unprocessed alerts.\n\t  In: query\n\t  Default: true\n\t*/\n\tUnprocessed *bool\n}\n\n// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface\n// for simple values it will use straight method calls.\n//\n// To ensure default values, the struct must have been initialized with NewGetAlertsParams() beforehand.\nfunc (o *GetAlertsParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {\n\tvar res []error\n\n\to.HTTPRequest = r\n\tqs := runtime.Values(r.URL.Query())\n\n\tqActive, qhkActive, _ := qs.GetOK(\"active\")\n\tif err := o.bindActive(qActive, qhkActive, route.Formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tqFilter, qhkFilter, _ := qs.GetOK(\"filter\")\n\tif err := o.bindFilter(qFilter, qhkFilter, route.Formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tqInhibited, qhkInhibited, _ := qs.GetOK(\"inhibited\")\n\tif err := o.bindInhibited(qInhibited, qhkInhibited, route.Formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tqReceiver, qhkReceiver, _ := qs.GetOK(\"receiver\")\n\tif err := o.bindReceiver(qReceiver, qhkReceiver, route.Formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tqSilenced, qhkSilenced, _ := qs.GetOK(\"silenced\")\n\tif err := o.bindSilenced(qSilenced, qhkSilenced, route.Formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tqUnprocessed, qhkUnprocessed, _ := qs.GetOK(\"unprocessed\")\n\tif err := o.bindUnprocessed(qUnprocessed, qhkUnprocessed, route.Formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\n// bindActive binds and validates parameter Active from query.\nfunc (o *GetAlertsParams) bindActive(rawData []string, hasKey bool, formats strfmt.Registry) error {\n\tvar raw string\n\tif len(rawData) > 0 {\n\t\traw = rawData[len(rawData)-1]\n\t}\n\n\t// Required: false\n\t// AllowEmptyValue: false\n\n\tif raw == \"\" { // empty values pass all other validations\n\t\t// Default values have been previously initialized by NewGetAlertsParams()\n\t\treturn nil\n\t}\n\n\tvalue, err := swag.ConvertBool(raw)\n\tif err != nil {\n\t\treturn errors.InvalidType(\"active\", \"query\", \"bool\", raw)\n\t}\n\to.Active = &value\n\n\treturn nil\n}\n\n// bindFilter binds and validates array parameter Filter from query.\n//\n// Arrays are parsed according to CollectionFormat: \"multi\" (defaults to \"csv\" when empty).\nfunc (o *GetAlertsParams) bindFilter(rawData []string, hasKey bool, formats strfmt.Registry) error {\n\t// CollectionFormat: multi\n\tfilterIC := rawData\n\tif len(filterIC) == 0 {\n\t\treturn nil\n\t}\n\n\tvar filterIR []string\n\tfor _, filterIV := range filterIC {\n\t\tfilterI := filterIV\n\n\t\tfilterIR = append(filterIR, filterI)\n\t}\n\n\to.Filter = filterIR\n\n\treturn nil\n}\n\n// bindInhibited binds and validates parameter Inhibited from query.\nfunc (o *GetAlertsParams) bindInhibited(rawData []string, hasKey bool, formats strfmt.Registry) error {\n\tvar raw string\n\tif len(rawData) > 0 {\n\t\traw = rawData[len(rawData)-1]\n\t}\n\n\t// Required: false\n\t// AllowEmptyValue: false\n\n\tif raw == \"\" { // empty values pass all other validations\n\t\t// Default values have been previously initialized by NewGetAlertsParams()\n\t\treturn nil\n\t}\n\n\tvalue, err := swag.ConvertBool(raw)\n\tif err != nil {\n\t\treturn errors.InvalidType(\"inhibited\", \"query\", \"bool\", raw)\n\t}\n\to.Inhibited = &value\n\n\treturn nil\n}\n\n// bindReceiver binds and validates parameter Receiver from query.\nfunc (o *GetAlertsParams) bindReceiver(rawData []string, hasKey bool, formats strfmt.Registry) error {\n\tvar raw string\n\tif len(rawData) > 0 {\n\t\traw = rawData[len(rawData)-1]\n\t}\n\n\t// Required: false\n\t// AllowEmptyValue: false\n\n\tif raw == \"\" { // empty values pass all other validations\n\t\treturn nil\n\t}\n\to.Receiver = &raw\n\n\treturn nil\n}\n\n// bindSilenced binds and validates parameter Silenced from query.\nfunc (o *GetAlertsParams) bindSilenced(rawData []string, hasKey bool, formats strfmt.Registry) error {\n\tvar raw string\n\tif len(rawData) > 0 {\n\t\traw = rawData[len(rawData)-1]\n\t}\n\n\t// Required: false\n\t// AllowEmptyValue: false\n\n\tif raw == \"\" { // empty values pass all other validations\n\t\t// Default values have been previously initialized by NewGetAlertsParams()\n\t\treturn nil\n\t}\n\n\tvalue, err := swag.ConvertBool(raw)\n\tif err != nil {\n\t\treturn errors.InvalidType(\"silenced\", \"query\", \"bool\", raw)\n\t}\n\to.Silenced = &value\n\n\treturn nil\n}\n\n// bindUnprocessed binds and validates parameter Unprocessed from query.\nfunc (o *GetAlertsParams) bindUnprocessed(rawData []string, hasKey bool, formats strfmt.Registry) error {\n\tvar raw string\n\tif len(rawData) > 0 {\n\t\traw = rawData[len(rawData)-1]\n\t}\n\n\t// Required: false\n\t// AllowEmptyValue: false\n\n\tif raw == \"\" { // empty values pass all other validations\n\t\t// Default values have been previously initialized by NewGetAlertsParams()\n\t\treturn nil\n\t}\n\n\tvalue, err := swag.ConvertBool(raw)\n\tif err != nil {\n\t\treturn errors.InvalidType(\"unprocessed\", \"query\", \"bool\", raw)\n\t}\n\to.Unprocessed = &value\n\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/alert/get_alerts_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alert\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\n// GetAlertsOKCode is the HTTP code returned for type GetAlertsOK\nconst GetAlertsOKCode int = 200\n\n/*\nGetAlertsOK Get alerts response\n\nswagger:response getAlertsOK\n*/\ntype GetAlertsOK struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload models.GettableAlerts `json:\"body,omitempty\"`\n}\n\n// NewGetAlertsOK creates GetAlertsOK with default headers values\nfunc NewGetAlertsOK() *GetAlertsOK {\n\n\treturn &GetAlertsOK{}\n}\n\n// WithPayload adds the payload to the get alerts o k response\nfunc (o *GetAlertsOK) WithPayload(payload models.GettableAlerts) *GetAlertsOK {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the get alerts o k response\nfunc (o *GetAlertsOK) SetPayload(payload models.GettableAlerts) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *GetAlertsOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(200)\n\tpayload := o.Payload\n\tif payload == nil {\n\t\t// return empty array\n\t\tpayload = models.GettableAlerts{}\n\t}\n\n\tif err := producer.Produce(rw, payload); err != nil {\n\t\tpanic(err) // let the recovery middleware deal with this\n\t}\n}\n\n// GetAlertsBadRequestCode is the HTTP code returned for type GetAlertsBadRequest\nconst GetAlertsBadRequestCode int = 400\n\n/*\nGetAlertsBadRequest Bad request\n\nswagger:response getAlertsBadRequest\n*/\ntype GetAlertsBadRequest struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload string `json:\"body,omitempty\"`\n}\n\n// NewGetAlertsBadRequest creates GetAlertsBadRequest with default headers values\nfunc NewGetAlertsBadRequest() *GetAlertsBadRequest {\n\n\treturn &GetAlertsBadRequest{}\n}\n\n// WithPayload adds the payload to the get alerts bad request response\nfunc (o *GetAlertsBadRequest) WithPayload(payload string) *GetAlertsBadRequest {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the get alerts bad request response\nfunc (o *GetAlertsBadRequest) SetPayload(payload string) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *GetAlertsBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(400)\n\tpayload := o.Payload\n\tif err := producer.Produce(rw, payload); err != nil {\n\t\tpanic(err) // let the recovery middleware deal with this\n\t}\n}\n\n// GetAlertsInternalServerErrorCode is the HTTP code returned for type GetAlertsInternalServerError\nconst GetAlertsInternalServerErrorCode int = 500\n\n/*\nGetAlertsInternalServerError Internal server error\n\nswagger:response getAlertsInternalServerError\n*/\ntype GetAlertsInternalServerError struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload string `json:\"body,omitempty\"`\n}\n\n// NewGetAlertsInternalServerError creates GetAlertsInternalServerError with default headers values\nfunc NewGetAlertsInternalServerError() *GetAlertsInternalServerError {\n\n\treturn &GetAlertsInternalServerError{}\n}\n\n// WithPayload adds the payload to the get alerts internal server error response\nfunc (o *GetAlertsInternalServerError) WithPayload(payload string) *GetAlertsInternalServerError {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the get alerts internal server error response\nfunc (o *GetAlertsInternalServerError) SetPayload(payload string) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *GetAlertsInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(500)\n\tpayload := o.Payload\n\tif err := producer.Produce(rw, payload); err != nil {\n\t\tpanic(err) // let the recovery middleware deal with this\n\t}\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/alert/get_alerts_urlbuilder.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alert\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\tgolangswaggerpaths \"path\"\n\n\t\"github.com/go-openapi/swag\"\n)\n\n// GetAlertsURL generates an URL for the get alerts operation\ntype GetAlertsURL struct {\n\tActive      *bool\n\tFilter      []string\n\tInhibited   *bool\n\tReceiver    *string\n\tSilenced    *bool\n\tUnprocessed *bool\n\n\t_basePath string\n\t// avoid unkeyed usage\n\t_ struct{}\n}\n\n// WithBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *GetAlertsURL) WithBasePath(bp string) *GetAlertsURL {\n\to.SetBasePath(bp)\n\treturn o\n}\n\n// SetBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *GetAlertsURL) SetBasePath(bp string) {\n\to._basePath = bp\n}\n\n// Build a url path and query string\nfunc (o *GetAlertsURL) Build() (*url.URL, error) {\n\tvar _result url.URL\n\n\tvar _path = \"/alerts\"\n\n\t_basePath := o._basePath\n\tif _basePath == \"\" {\n\t\t_basePath = \"/api/v2/\"\n\t}\n\t_result.Path = golangswaggerpaths.Join(_basePath, _path)\n\n\tqs := make(url.Values)\n\n\tvar activeQ string\n\tif o.Active != nil {\n\t\tactiveQ = swag.FormatBool(*o.Active)\n\t}\n\tif activeQ != \"\" {\n\t\tqs.Set(\"active\", activeQ)\n\t}\n\n\tvar filterIR []string\n\tfor _, filterI := range o.Filter {\n\t\tfilterIS := filterI\n\t\tif filterIS != \"\" {\n\t\t\tfilterIR = append(filterIR, filterIS)\n\t\t}\n\t}\n\n\tfilter := swag.JoinByFormat(filterIR, \"multi\")\n\n\tfor _, qsv := range filter {\n\t\tqs.Add(\"filter\", qsv)\n\t}\n\n\tvar inhibitedQ string\n\tif o.Inhibited != nil {\n\t\tinhibitedQ = swag.FormatBool(*o.Inhibited)\n\t}\n\tif inhibitedQ != \"\" {\n\t\tqs.Set(\"inhibited\", inhibitedQ)\n\t}\n\n\tvar receiverQ string\n\tif o.Receiver != nil {\n\t\treceiverQ = *o.Receiver\n\t}\n\tif receiverQ != \"\" {\n\t\tqs.Set(\"receiver\", receiverQ)\n\t}\n\n\tvar silencedQ string\n\tif o.Silenced != nil {\n\t\tsilencedQ = swag.FormatBool(*o.Silenced)\n\t}\n\tif silencedQ != \"\" {\n\t\tqs.Set(\"silenced\", silencedQ)\n\t}\n\n\tvar unprocessedQ string\n\tif o.Unprocessed != nil {\n\t\tunprocessedQ = swag.FormatBool(*o.Unprocessed)\n\t}\n\tif unprocessedQ != \"\" {\n\t\tqs.Set(\"unprocessed\", unprocessedQ)\n\t}\n\n\t_result.RawQuery = qs.Encode()\n\n\treturn &_result, nil\n}\n\n// Must is a helper function to panic when the url builder returns an error\nfunc (o *GetAlertsURL) Must(u *url.URL, err error) *url.URL {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif u == nil {\n\t\tpanic(\"url can't be nil\")\n\t}\n\treturn u\n}\n\n// String returns the string representation of the path with query string\nfunc (o *GetAlertsURL) String() string {\n\treturn o.Must(o.Build()).String()\n}\n\n// BuildFull builds a full url with scheme, host, path and query string\nfunc (o *GetAlertsURL) BuildFull(scheme, host string) (*url.URL, error) {\n\tif scheme == \"\" {\n\t\treturn nil, errors.New(\"scheme is required for a full url on GetAlertsURL\")\n\t}\n\tif host == \"\" {\n\t\treturn nil, errors.New(\"host is required for a full url on GetAlertsURL\")\n\t}\n\n\tbase, err := o.Build()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbase.Scheme = scheme\n\tbase.Host = host\n\treturn base, nil\n}\n\n// StringFull returns the string representation of a complete url\nfunc (o *GetAlertsURL) StringFull(scheme, host string) string {\n\treturn o.Must(o.BuildFull(scheme, host)).String()\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/alert/post_alerts.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alert\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime/middleware\"\n)\n\n// PostAlertsHandlerFunc turns a function with the right signature into a post alerts handler\ntype PostAlertsHandlerFunc func(PostAlertsParams) middleware.Responder\n\n// Handle executing the request and returning a response\nfunc (fn PostAlertsHandlerFunc) Handle(params PostAlertsParams) middleware.Responder {\n\treturn fn(params)\n}\n\n// PostAlertsHandler interface for that can handle valid post alerts params\ntype PostAlertsHandler interface {\n\tHandle(PostAlertsParams) middleware.Responder\n}\n\n// NewPostAlerts creates a new http.Handler for the post alerts operation\nfunc NewPostAlerts(ctx *middleware.Context, handler PostAlertsHandler) *PostAlerts {\n\treturn &PostAlerts{Context: ctx, Handler: handler}\n}\n\n/*\n\tPostAlerts swagger:route POST /alerts alert postAlerts\n\nCreate new Alerts\n*/\ntype PostAlerts struct {\n\tContext *middleware.Context\n\tHandler PostAlertsHandler\n}\n\nfunc (o *PostAlerts) ServeHTTP(rw http.ResponseWriter, r *http.Request) {\n\troute, rCtx, _ := o.Context.RouteInfo(r)\n\tif rCtx != nil {\n\t\t*r = *rCtx\n\t}\n\tvar Params = NewPostAlertsParams()\n\tif err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params\n\t\to.Context.Respond(rw, r, route.Produces, route, err)\n\t\treturn\n\t}\n\n\tres := o.Handler.Handle(Params) // actually handle the request\n\n\to.Context.Respond(rw, r, route.Produces, route, res)\n\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/alert/post_alerts_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alert\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\tstderrors \"errors\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime\"\n\t\"github.com/go-openapi/runtime/middleware\"\n\t\"github.com/go-openapi/validate\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\n// NewPostAlertsParams creates a new PostAlertsParams object\n//\n// There are no default values defined in the spec.\nfunc NewPostAlertsParams() PostAlertsParams {\n\n\treturn PostAlertsParams{}\n}\n\n// PostAlertsParams contains all the bound params for the post alerts operation\n// typically these are obtained from a http.Request\n//\n// swagger:parameters postAlerts\ntype PostAlertsParams struct {\n\t// HTTP Request Object\n\tHTTPRequest *http.Request `json:\"-\"`\n\n\t/*The alerts to create\n\t  Required: true\n\t  In: body\n\t*/\n\tAlerts models.PostableAlerts\n}\n\n// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface\n// for simple values it will use straight method calls.\n//\n// To ensure default values, the struct must have been initialized with NewPostAlertsParams() beforehand.\nfunc (o *PostAlertsParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {\n\tvar res []error\n\n\to.HTTPRequest = r\n\n\tif runtime.HasBody(r) {\n\t\tdefer func() {\n\t\t\t_ = r.Body.Close()\n\t\t}()\n\t\tvar body models.PostableAlerts\n\t\tif err := route.Consumer.Consume(r.Body, &body); err != nil {\n\t\t\tif stderrors.Is(err, io.EOF) {\n\t\t\t\tres = append(res, errors.Required(\"alerts\", \"body\", \"\"))\n\t\t\t} else {\n\t\t\t\tres = append(res, errors.NewParseError(\"alerts\", \"body\", \"\", err))\n\t\t\t}\n\t\t} else {\n\t\t\t// validate body object\n\t\t\tif err := body.Validate(route.Formats); err != nil {\n\t\t\t\tres = append(res, err)\n\t\t\t}\n\n\t\t\tctx := validate.WithOperationRequest(r.Context())\n\t\t\tif err := body.ContextValidate(ctx, route.Formats); err != nil {\n\t\t\t\tres = append(res, err)\n\t\t\t}\n\n\t\t\tif len(res) == 0 {\n\t\t\t\to.Alerts = body\n\t\t\t}\n\t\t}\n\t} else {\n\t\tres = append(res, errors.Required(\"alerts\", \"body\", \"\"))\n\t}\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/alert/post_alerts_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alert\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime\"\n)\n\n// PostAlertsOKCode is the HTTP code returned for type PostAlertsOK\nconst PostAlertsOKCode int = 200\n\n/*\nPostAlertsOK Create alerts response\n\nswagger:response postAlertsOK\n*/\ntype PostAlertsOK struct {\n}\n\n// NewPostAlertsOK creates PostAlertsOK with default headers values\nfunc NewPostAlertsOK() *PostAlertsOK {\n\n\treturn &PostAlertsOK{}\n}\n\n// WriteResponse to the client\nfunc (o *PostAlertsOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.Header().Del(runtime.HeaderContentType) // Remove Content-Type on empty responses\n\n\trw.WriteHeader(200)\n}\n\n// PostAlertsBadRequestCode is the HTTP code returned for type PostAlertsBadRequest\nconst PostAlertsBadRequestCode int = 400\n\n/*\nPostAlertsBadRequest Bad request\n\nswagger:response postAlertsBadRequest\n*/\ntype PostAlertsBadRequest struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload string `json:\"body,omitempty\"`\n}\n\n// NewPostAlertsBadRequest creates PostAlertsBadRequest with default headers values\nfunc NewPostAlertsBadRequest() *PostAlertsBadRequest {\n\n\treturn &PostAlertsBadRequest{}\n}\n\n// WithPayload adds the payload to the post alerts bad request response\nfunc (o *PostAlertsBadRequest) WithPayload(payload string) *PostAlertsBadRequest {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the post alerts bad request response\nfunc (o *PostAlertsBadRequest) SetPayload(payload string) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *PostAlertsBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(400)\n\tpayload := o.Payload\n\tif err := producer.Produce(rw, payload); err != nil {\n\t\tpanic(err) // let the recovery middleware deal with this\n\t}\n}\n\n// PostAlertsInternalServerErrorCode is the HTTP code returned for type PostAlertsInternalServerError\nconst PostAlertsInternalServerErrorCode int = 500\n\n/*\nPostAlertsInternalServerError Internal server error\n\nswagger:response postAlertsInternalServerError\n*/\ntype PostAlertsInternalServerError struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload string `json:\"body,omitempty\"`\n}\n\n// NewPostAlertsInternalServerError creates PostAlertsInternalServerError with default headers values\nfunc NewPostAlertsInternalServerError() *PostAlertsInternalServerError {\n\n\treturn &PostAlertsInternalServerError{}\n}\n\n// WithPayload adds the payload to the post alerts internal server error response\nfunc (o *PostAlertsInternalServerError) WithPayload(payload string) *PostAlertsInternalServerError {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the post alerts internal server error response\nfunc (o *PostAlertsInternalServerError) SetPayload(payload string) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *PostAlertsInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(500)\n\tpayload := o.Payload\n\tif err := producer.Produce(rw, payload); err != nil {\n\t\tpanic(err) // let the recovery middleware deal with this\n\t}\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/alert/post_alerts_urlbuilder.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alert\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\tgolangswaggerpaths \"path\"\n)\n\n// PostAlertsURL generates an URL for the post alerts operation\ntype PostAlertsURL struct {\n\t_basePath string\n}\n\n// WithBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *PostAlertsURL) WithBasePath(bp string) *PostAlertsURL {\n\to.SetBasePath(bp)\n\treturn o\n}\n\n// SetBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *PostAlertsURL) SetBasePath(bp string) {\n\to._basePath = bp\n}\n\n// Build a url path and query string\nfunc (o *PostAlertsURL) Build() (*url.URL, error) {\n\tvar _result url.URL\n\n\tvar _path = \"/alerts\"\n\n\t_basePath := o._basePath\n\tif _basePath == \"\" {\n\t\t_basePath = \"/api/v2/\"\n\t}\n\t_result.Path = golangswaggerpaths.Join(_basePath, _path)\n\n\treturn &_result, nil\n}\n\n// Must is a helper function to panic when the url builder returns an error\nfunc (o *PostAlertsURL) Must(u *url.URL, err error) *url.URL {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif u == nil {\n\t\tpanic(\"url can't be nil\")\n\t}\n\treturn u\n}\n\n// String returns the string representation of the path with query string\nfunc (o *PostAlertsURL) String() string {\n\treturn o.Must(o.Build()).String()\n}\n\n// BuildFull builds a full url with scheme, host, path and query string\nfunc (o *PostAlertsURL) BuildFull(scheme, host string) (*url.URL, error) {\n\tif scheme == \"\" {\n\t\treturn nil, errors.New(\"scheme is required for a full url on PostAlertsURL\")\n\t}\n\tif host == \"\" {\n\t\treturn nil, errors.New(\"host is required for a full url on PostAlertsURL\")\n\t}\n\n\tbase, err := o.Build()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbase.Scheme = scheme\n\tbase.Host = host\n\treturn base, nil\n}\n\n// StringFull returns the string representation of a complete url\nfunc (o *PostAlertsURL) StringFull(scheme, host string) string {\n\treturn o.Must(o.BuildFull(scheme, host)).String()\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/alertgroup/get_alert_groups.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alertgroup\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime/middleware\"\n)\n\n// GetAlertGroupsHandlerFunc turns a function with the right signature into a get alert groups handler\ntype GetAlertGroupsHandlerFunc func(GetAlertGroupsParams) middleware.Responder\n\n// Handle executing the request and returning a response\nfunc (fn GetAlertGroupsHandlerFunc) Handle(params GetAlertGroupsParams) middleware.Responder {\n\treturn fn(params)\n}\n\n// GetAlertGroupsHandler interface for that can handle valid get alert groups params\ntype GetAlertGroupsHandler interface {\n\tHandle(GetAlertGroupsParams) middleware.Responder\n}\n\n// NewGetAlertGroups creates a new http.Handler for the get alert groups operation\nfunc NewGetAlertGroups(ctx *middleware.Context, handler GetAlertGroupsHandler) *GetAlertGroups {\n\treturn &GetAlertGroups{Context: ctx, Handler: handler}\n}\n\n/*\n\tGetAlertGroups swagger:route GET /alerts/groups alertgroup getAlertGroups\n\nGet a list of alert groups\n*/\ntype GetAlertGroups struct {\n\tContext *middleware.Context\n\tHandler GetAlertGroupsHandler\n}\n\nfunc (o *GetAlertGroups) ServeHTTP(rw http.ResponseWriter, r *http.Request) {\n\troute, rCtx, _ := o.Context.RouteInfo(r)\n\tif rCtx != nil {\n\t\t*r = *rCtx\n\t}\n\tvar Params = NewGetAlertGroupsParams()\n\tif err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params\n\t\to.Context.Respond(rw, r, route.Produces, route, err)\n\t\treturn\n\t}\n\n\tres := o.Handler.Handle(Params) // actually handle the request\n\n\to.Context.Respond(rw, r, route.Produces, route, res)\n\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/alertgroup/get_alert_groups_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alertgroup\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime\"\n\t\"github.com/go-openapi/runtime/middleware\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n)\n\n// NewGetAlertGroupsParams creates a new GetAlertGroupsParams object\n// with the default values initialized.\nfunc NewGetAlertGroupsParams() GetAlertGroupsParams {\n\n\tvar (\n\t\t// initialize parameters with default values\n\n\t\tactiveDefault = bool(true)\n\n\t\tinhibitedDefault = bool(true)\n\t\tmutedDefault     = bool(true)\n\n\t\tsilencedDefault = bool(true)\n\t)\n\n\treturn GetAlertGroupsParams{\n\t\tActive: &activeDefault,\n\n\t\tInhibited: &inhibitedDefault,\n\n\t\tMuted: &mutedDefault,\n\n\t\tSilenced: &silencedDefault,\n\t}\n}\n\n// GetAlertGroupsParams contains all the bound params for the get alert groups operation\n// typically these are obtained from a http.Request\n//\n// swagger:parameters getAlertGroups\ntype GetAlertGroupsParams struct {\n\t// HTTP Request Object\n\tHTTPRequest *http.Request `json:\"-\"`\n\n\t/*Include active alerts within the returned groups. If false, excludes active alerts from groups and only shows suppressed (silenced or inhibited) alerts.\n\t  In: query\n\t  Default: true\n\t*/\n\tActive *bool\n\n\t/*A matcher expression to filter alert groups. For example `alertname=\"MyAlert\"`. It can be repeated to apply multiple matchers.\n\t  In: query\n\t  Collection Format: multi\n\t*/\n\tFilter []string\n\n\t/*Include inhibited alerts within the returned groups. If false, excludes inhibited alerts from groups. Note that true (default) shows both inhibited and non-inhibited alerts.\n\t  In: query\n\t  Default: true\n\t*/\n\tInhibited *bool\n\n\t/*Include muted (silenced or inhibited) alert groups in results. If false, excludes entire groups where all alerts are muted.\n\t  In: query\n\t  Default: true\n\t*/\n\tMuted *bool\n\n\t/*A regex matching receivers to filter alerts by\n\t  In: query\n\t*/\n\tReceiver *string\n\n\t/*Include silenced alerts within the returned groups. If false, excludes silenced alerts from groups. Note that true (default) shows both silenced and non-silenced alerts.\n\t  In: query\n\t  Default: true\n\t*/\n\tSilenced *bool\n}\n\n// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface\n// for simple values it will use straight method calls.\n//\n// To ensure default values, the struct must have been initialized with NewGetAlertGroupsParams() beforehand.\nfunc (o *GetAlertGroupsParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {\n\tvar res []error\n\n\to.HTTPRequest = r\n\tqs := runtime.Values(r.URL.Query())\n\n\tqActive, qhkActive, _ := qs.GetOK(\"active\")\n\tif err := o.bindActive(qActive, qhkActive, route.Formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tqFilter, qhkFilter, _ := qs.GetOK(\"filter\")\n\tif err := o.bindFilter(qFilter, qhkFilter, route.Formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tqInhibited, qhkInhibited, _ := qs.GetOK(\"inhibited\")\n\tif err := o.bindInhibited(qInhibited, qhkInhibited, route.Formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tqMuted, qhkMuted, _ := qs.GetOK(\"muted\")\n\tif err := o.bindMuted(qMuted, qhkMuted, route.Formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tqReceiver, qhkReceiver, _ := qs.GetOK(\"receiver\")\n\tif err := o.bindReceiver(qReceiver, qhkReceiver, route.Formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\n\tqSilenced, qhkSilenced, _ := qs.GetOK(\"silenced\")\n\tif err := o.bindSilenced(qSilenced, qhkSilenced, route.Formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\n// bindActive binds and validates parameter Active from query.\nfunc (o *GetAlertGroupsParams) bindActive(rawData []string, hasKey bool, formats strfmt.Registry) error {\n\tvar raw string\n\tif len(rawData) > 0 {\n\t\traw = rawData[len(rawData)-1]\n\t}\n\n\t// Required: false\n\t// AllowEmptyValue: false\n\n\tif raw == \"\" { // empty values pass all other validations\n\t\t// Default values have been previously initialized by NewGetAlertGroupsParams()\n\t\treturn nil\n\t}\n\n\tvalue, err := swag.ConvertBool(raw)\n\tif err != nil {\n\t\treturn errors.InvalidType(\"active\", \"query\", \"bool\", raw)\n\t}\n\to.Active = &value\n\n\treturn nil\n}\n\n// bindFilter binds and validates array parameter Filter from query.\n//\n// Arrays are parsed according to CollectionFormat: \"multi\" (defaults to \"csv\" when empty).\nfunc (o *GetAlertGroupsParams) bindFilter(rawData []string, hasKey bool, formats strfmt.Registry) error {\n\t// CollectionFormat: multi\n\tfilterIC := rawData\n\tif len(filterIC) == 0 {\n\t\treturn nil\n\t}\n\n\tvar filterIR []string\n\tfor _, filterIV := range filterIC {\n\t\tfilterI := filterIV\n\n\t\tfilterIR = append(filterIR, filterI)\n\t}\n\n\to.Filter = filterIR\n\n\treturn nil\n}\n\n// bindInhibited binds and validates parameter Inhibited from query.\nfunc (o *GetAlertGroupsParams) bindInhibited(rawData []string, hasKey bool, formats strfmt.Registry) error {\n\tvar raw string\n\tif len(rawData) > 0 {\n\t\traw = rawData[len(rawData)-1]\n\t}\n\n\t// Required: false\n\t// AllowEmptyValue: false\n\n\tif raw == \"\" { // empty values pass all other validations\n\t\t// Default values have been previously initialized by NewGetAlertGroupsParams()\n\t\treturn nil\n\t}\n\n\tvalue, err := swag.ConvertBool(raw)\n\tif err != nil {\n\t\treturn errors.InvalidType(\"inhibited\", \"query\", \"bool\", raw)\n\t}\n\to.Inhibited = &value\n\n\treturn nil\n}\n\n// bindMuted binds and validates parameter Muted from query.\nfunc (o *GetAlertGroupsParams) bindMuted(rawData []string, hasKey bool, formats strfmt.Registry) error {\n\tvar raw string\n\tif len(rawData) > 0 {\n\t\traw = rawData[len(rawData)-1]\n\t}\n\n\t// Required: false\n\t// AllowEmptyValue: false\n\n\tif raw == \"\" { // empty values pass all other validations\n\t\t// Default values have been previously initialized by NewGetAlertGroupsParams()\n\t\treturn nil\n\t}\n\n\tvalue, err := swag.ConvertBool(raw)\n\tif err != nil {\n\t\treturn errors.InvalidType(\"muted\", \"query\", \"bool\", raw)\n\t}\n\to.Muted = &value\n\n\treturn nil\n}\n\n// bindReceiver binds and validates parameter Receiver from query.\nfunc (o *GetAlertGroupsParams) bindReceiver(rawData []string, hasKey bool, formats strfmt.Registry) error {\n\tvar raw string\n\tif len(rawData) > 0 {\n\t\traw = rawData[len(rawData)-1]\n\t}\n\n\t// Required: false\n\t// AllowEmptyValue: false\n\n\tif raw == \"\" { // empty values pass all other validations\n\t\treturn nil\n\t}\n\to.Receiver = &raw\n\n\treturn nil\n}\n\n// bindSilenced binds and validates parameter Silenced from query.\nfunc (o *GetAlertGroupsParams) bindSilenced(rawData []string, hasKey bool, formats strfmt.Registry) error {\n\tvar raw string\n\tif len(rawData) > 0 {\n\t\traw = rawData[len(rawData)-1]\n\t}\n\n\t// Required: false\n\t// AllowEmptyValue: false\n\n\tif raw == \"\" { // empty values pass all other validations\n\t\t// Default values have been previously initialized by NewGetAlertGroupsParams()\n\t\treturn nil\n\t}\n\n\tvalue, err := swag.ConvertBool(raw)\n\tif err != nil {\n\t\treturn errors.InvalidType(\"silenced\", \"query\", \"bool\", raw)\n\t}\n\to.Silenced = &value\n\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/alertgroup/get_alert_groups_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alertgroup\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\n// GetAlertGroupsOKCode is the HTTP code returned for type GetAlertGroupsOK\nconst GetAlertGroupsOKCode int = 200\n\n/*\nGetAlertGroupsOK Get alert groups response\n\nswagger:response getAlertGroupsOK\n*/\ntype GetAlertGroupsOK struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload models.AlertGroups `json:\"body,omitempty\"`\n}\n\n// NewGetAlertGroupsOK creates GetAlertGroupsOK with default headers values\nfunc NewGetAlertGroupsOK() *GetAlertGroupsOK {\n\n\treturn &GetAlertGroupsOK{}\n}\n\n// WithPayload adds the payload to the get alert groups o k response\nfunc (o *GetAlertGroupsOK) WithPayload(payload models.AlertGroups) *GetAlertGroupsOK {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the get alert groups o k response\nfunc (o *GetAlertGroupsOK) SetPayload(payload models.AlertGroups) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *GetAlertGroupsOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(200)\n\tpayload := o.Payload\n\tif payload == nil {\n\t\t// return empty array\n\t\tpayload = models.AlertGroups{}\n\t}\n\n\tif err := producer.Produce(rw, payload); err != nil {\n\t\tpanic(err) // let the recovery middleware deal with this\n\t}\n}\n\n// GetAlertGroupsBadRequestCode is the HTTP code returned for type GetAlertGroupsBadRequest\nconst GetAlertGroupsBadRequestCode int = 400\n\n/*\nGetAlertGroupsBadRequest Bad request\n\nswagger:response getAlertGroupsBadRequest\n*/\ntype GetAlertGroupsBadRequest struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload string `json:\"body,omitempty\"`\n}\n\n// NewGetAlertGroupsBadRequest creates GetAlertGroupsBadRequest with default headers values\nfunc NewGetAlertGroupsBadRequest() *GetAlertGroupsBadRequest {\n\n\treturn &GetAlertGroupsBadRequest{}\n}\n\n// WithPayload adds the payload to the get alert groups bad request response\nfunc (o *GetAlertGroupsBadRequest) WithPayload(payload string) *GetAlertGroupsBadRequest {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the get alert groups bad request response\nfunc (o *GetAlertGroupsBadRequest) SetPayload(payload string) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *GetAlertGroupsBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(400)\n\tpayload := o.Payload\n\tif err := producer.Produce(rw, payload); err != nil {\n\t\tpanic(err) // let the recovery middleware deal with this\n\t}\n}\n\n// GetAlertGroupsInternalServerErrorCode is the HTTP code returned for type GetAlertGroupsInternalServerError\nconst GetAlertGroupsInternalServerErrorCode int = 500\n\n/*\nGetAlertGroupsInternalServerError Internal server error\n\nswagger:response getAlertGroupsInternalServerError\n*/\ntype GetAlertGroupsInternalServerError struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload string `json:\"body,omitempty\"`\n}\n\n// NewGetAlertGroupsInternalServerError creates GetAlertGroupsInternalServerError with default headers values\nfunc NewGetAlertGroupsInternalServerError() *GetAlertGroupsInternalServerError {\n\n\treturn &GetAlertGroupsInternalServerError{}\n}\n\n// WithPayload adds the payload to the get alert groups internal server error response\nfunc (o *GetAlertGroupsInternalServerError) WithPayload(payload string) *GetAlertGroupsInternalServerError {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the get alert groups internal server error response\nfunc (o *GetAlertGroupsInternalServerError) SetPayload(payload string) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *GetAlertGroupsInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(500)\n\tpayload := o.Payload\n\tif err := producer.Produce(rw, payload); err != nil {\n\t\tpanic(err) // let the recovery middleware deal with this\n\t}\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/alertgroup/get_alert_groups_urlbuilder.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage alertgroup\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\tgolangswaggerpaths \"path\"\n\n\t\"github.com/go-openapi/swag\"\n)\n\n// GetAlertGroupsURL generates an URL for the get alert groups operation\ntype GetAlertGroupsURL struct {\n\tActive    *bool\n\tFilter    []string\n\tInhibited *bool\n\tMuted     *bool\n\tReceiver  *string\n\tSilenced  *bool\n\n\t_basePath string\n\t// avoid unkeyed usage\n\t_ struct{}\n}\n\n// WithBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *GetAlertGroupsURL) WithBasePath(bp string) *GetAlertGroupsURL {\n\to.SetBasePath(bp)\n\treturn o\n}\n\n// SetBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *GetAlertGroupsURL) SetBasePath(bp string) {\n\to._basePath = bp\n}\n\n// Build a url path and query string\nfunc (o *GetAlertGroupsURL) Build() (*url.URL, error) {\n\tvar _result url.URL\n\n\tvar _path = \"/alerts/groups\"\n\n\t_basePath := o._basePath\n\tif _basePath == \"\" {\n\t\t_basePath = \"/api/v2/\"\n\t}\n\t_result.Path = golangswaggerpaths.Join(_basePath, _path)\n\n\tqs := make(url.Values)\n\n\tvar activeQ string\n\tif o.Active != nil {\n\t\tactiveQ = swag.FormatBool(*o.Active)\n\t}\n\tif activeQ != \"\" {\n\t\tqs.Set(\"active\", activeQ)\n\t}\n\n\tvar filterIR []string\n\tfor _, filterI := range o.Filter {\n\t\tfilterIS := filterI\n\t\tif filterIS != \"\" {\n\t\t\tfilterIR = append(filterIR, filterIS)\n\t\t}\n\t}\n\n\tfilter := swag.JoinByFormat(filterIR, \"multi\")\n\n\tfor _, qsv := range filter {\n\t\tqs.Add(\"filter\", qsv)\n\t}\n\n\tvar inhibitedQ string\n\tif o.Inhibited != nil {\n\t\tinhibitedQ = swag.FormatBool(*o.Inhibited)\n\t}\n\tif inhibitedQ != \"\" {\n\t\tqs.Set(\"inhibited\", inhibitedQ)\n\t}\n\n\tvar mutedQ string\n\tif o.Muted != nil {\n\t\tmutedQ = swag.FormatBool(*o.Muted)\n\t}\n\tif mutedQ != \"\" {\n\t\tqs.Set(\"muted\", mutedQ)\n\t}\n\n\tvar receiverQ string\n\tif o.Receiver != nil {\n\t\treceiverQ = *o.Receiver\n\t}\n\tif receiverQ != \"\" {\n\t\tqs.Set(\"receiver\", receiverQ)\n\t}\n\n\tvar silencedQ string\n\tif o.Silenced != nil {\n\t\tsilencedQ = swag.FormatBool(*o.Silenced)\n\t}\n\tif silencedQ != \"\" {\n\t\tqs.Set(\"silenced\", silencedQ)\n\t}\n\n\t_result.RawQuery = qs.Encode()\n\n\treturn &_result, nil\n}\n\n// Must is a helper function to panic when the url builder returns an error\nfunc (o *GetAlertGroupsURL) Must(u *url.URL, err error) *url.URL {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif u == nil {\n\t\tpanic(\"url can't be nil\")\n\t}\n\treturn u\n}\n\n// String returns the string representation of the path with query string\nfunc (o *GetAlertGroupsURL) String() string {\n\treturn o.Must(o.Build()).String()\n}\n\n// BuildFull builds a full url with scheme, host, path and query string\nfunc (o *GetAlertGroupsURL) BuildFull(scheme, host string) (*url.URL, error) {\n\tif scheme == \"\" {\n\t\treturn nil, errors.New(\"scheme is required for a full url on GetAlertGroupsURL\")\n\t}\n\tif host == \"\" {\n\t\treturn nil, errors.New(\"host is required for a full url on GetAlertGroupsURL\")\n\t}\n\n\tbase, err := o.Build()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbase.Scheme = scheme\n\tbase.Host = host\n\treturn base, nil\n}\n\n// StringFull returns the string representation of a complete url\nfunc (o *GetAlertGroupsURL) StringFull(scheme, host string) string {\n\treturn o.Must(o.BuildFull(scheme, host)).String()\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/alertmanager_api.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage operations\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/loads\"\n\t\"github.com/go-openapi/runtime\"\n\t\"github.com/go-openapi/runtime/middleware\"\n\t\"github.com/go-openapi/runtime/security\"\n\t\"github.com/go-openapi/spec\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/restapi/operations/alert\"\n\t\"github.com/prometheus/alertmanager/api/v2/restapi/operations/alertgroup\"\n\t\"github.com/prometheus/alertmanager/api/v2/restapi/operations/general\"\n\t\"github.com/prometheus/alertmanager/api/v2/restapi/operations/receiver\"\n\t\"github.com/prometheus/alertmanager/api/v2/restapi/operations/silence\"\n)\n\n// NewAlertmanagerAPI creates a new Alertmanager instance\nfunc NewAlertmanagerAPI(spec *loads.Document) *AlertmanagerAPI {\n\treturn &AlertmanagerAPI{\n\t\thandlers:            make(map[string]map[string]http.Handler),\n\t\tformats:             strfmt.Default,\n\t\tdefaultConsumes:     \"application/json\",\n\t\tdefaultProduces:     \"application/json\",\n\t\tcustomConsumers:     make(map[string]runtime.Consumer),\n\t\tcustomProducers:     make(map[string]runtime.Producer),\n\t\tPreServerShutdown:   func() {},\n\t\tServerShutdown:      func() {},\n\t\tspec:                spec,\n\t\tuseSwaggerUI:        false,\n\t\tServeError:          errors.ServeError,\n\t\tBasicAuthenticator:  security.BasicAuth,\n\t\tAPIKeyAuthenticator: security.APIKeyAuth,\n\t\tBearerAuthenticator: security.BearerAuth,\n\n\t\tJSONConsumer: runtime.JSONConsumer(),\n\n\t\tJSONProducer: runtime.JSONProducer(),\n\n\t\tSilenceDeleteSilenceHandler: silence.DeleteSilenceHandlerFunc(func(params silence.DeleteSilenceParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation silence.DeleteSilence has not yet been implemented\")\n\t\t}),\n\n\t\tAlertgroupGetAlertGroupsHandler: alertgroup.GetAlertGroupsHandlerFunc(func(params alertgroup.GetAlertGroupsParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation alertgroup.GetAlertGroups has not yet been implemented\")\n\t\t}),\n\n\t\tAlertGetAlertsHandler: alert.GetAlertsHandlerFunc(func(params alert.GetAlertsParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation alert.GetAlerts has not yet been implemented\")\n\t\t}),\n\n\t\tReceiverGetReceiversHandler: receiver.GetReceiversHandlerFunc(func(params receiver.GetReceiversParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation receiver.GetReceivers has not yet been implemented\")\n\t\t}),\n\n\t\tSilenceGetSilenceHandler: silence.GetSilenceHandlerFunc(func(params silence.GetSilenceParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation silence.GetSilence has not yet been implemented\")\n\t\t}),\n\n\t\tSilenceGetSilencesHandler: silence.GetSilencesHandlerFunc(func(params silence.GetSilencesParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation silence.GetSilences has not yet been implemented\")\n\t\t}),\n\n\t\tGeneralGetStatusHandler: general.GetStatusHandlerFunc(func(params general.GetStatusParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation general.GetStatus has not yet been implemented\")\n\t\t}),\n\n\t\tAlertPostAlertsHandler: alert.PostAlertsHandlerFunc(func(params alert.PostAlertsParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation alert.PostAlerts has not yet been implemented\")\n\t\t}),\n\n\t\tSilencePostSilencesHandler: silence.PostSilencesHandlerFunc(func(params silence.PostSilencesParams) middleware.Responder {\n\t\t\t_ = params\n\n\t\t\treturn middleware.NotImplemented(\"operation silence.PostSilences has not yet been implemented\")\n\t\t}),\n\t}\n}\n\n/*AlertmanagerAPI API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager) */\ntype AlertmanagerAPI struct {\n\tspec            *loads.Document\n\tcontext         *middleware.Context\n\thandlers        map[string]map[string]http.Handler\n\tformats         strfmt.Registry\n\tcustomConsumers map[string]runtime.Consumer\n\tcustomProducers map[string]runtime.Producer\n\tdefaultConsumes string\n\tdefaultProduces string\n\tMiddleware      func(middleware.Builder) http.Handler\n\tuseSwaggerUI    bool\n\n\t// BasicAuthenticator generates a runtime.Authenticator from the supplied basic auth function.\n\t// It has a default implementation in the security package, however you can replace it for your particular usage.\n\tBasicAuthenticator func(security.UserPassAuthentication) runtime.Authenticator\n\n\t// APIKeyAuthenticator generates a runtime.Authenticator from the supplied token auth function.\n\t// It has a default implementation in the security package, however you can replace it for your particular usage.\n\tAPIKeyAuthenticator func(string, string, security.TokenAuthentication) runtime.Authenticator\n\n\t// BearerAuthenticator generates a runtime.Authenticator from the supplied bearer token auth function.\n\t// It has a default implementation in the security package, however you can replace it for your particular usage.\n\tBearerAuthenticator func(string, security.ScopedTokenAuthentication) runtime.Authenticator\n\n\t// JSONConsumer registers a consumer for the following mime types:\n\t//   - application/json\n\tJSONConsumer runtime.Consumer\n\n\t// JSONProducer registers a producer for the following mime types:\n\t//   - application/json\n\tJSONProducer runtime.Producer\n\n\t// SilenceDeleteSilenceHandler sets the operation handler for the delete silence operation\n\tSilenceDeleteSilenceHandler silence.DeleteSilenceHandler\n\t// AlertgroupGetAlertGroupsHandler sets the operation handler for the get alert groups operation\n\tAlertgroupGetAlertGroupsHandler alertgroup.GetAlertGroupsHandler\n\t// AlertGetAlertsHandler sets the operation handler for the get alerts operation\n\tAlertGetAlertsHandler alert.GetAlertsHandler\n\t// ReceiverGetReceiversHandler sets the operation handler for the get receivers operation\n\tReceiverGetReceiversHandler receiver.GetReceiversHandler\n\t// SilenceGetSilenceHandler sets the operation handler for the get silence operation\n\tSilenceGetSilenceHandler silence.GetSilenceHandler\n\t// SilenceGetSilencesHandler sets the operation handler for the get silences operation\n\tSilenceGetSilencesHandler silence.GetSilencesHandler\n\t// GeneralGetStatusHandler sets the operation handler for the get status operation\n\tGeneralGetStatusHandler general.GetStatusHandler\n\t// AlertPostAlertsHandler sets the operation handler for the post alerts operation\n\tAlertPostAlertsHandler alert.PostAlertsHandler\n\t// SilencePostSilencesHandler sets the operation handler for the post silences operation\n\tSilencePostSilencesHandler silence.PostSilencesHandler\n\n\t// ServeError is called when an error is received, there is a default handler\n\t// but you can set your own with this\n\tServeError func(http.ResponseWriter, *http.Request, error)\n\n\t// PreServerShutdown is called before the HTTP(S) server is shutdown\n\t// This allows for custom functions to get executed before the HTTP(S) server stops accepting traffic\n\tPreServerShutdown func()\n\n\t// ServerShutdown is called when the HTTP(S) server is shut down and done\n\t// handling all active connections and does not accept connections any more\n\tServerShutdown func()\n\n\t// Custom command line argument groups with their descriptions\n\tCommandLineOptionsGroups []swag.CommandLineOptionsGroup\n\n\t// User defined logger function.\n\tLogger func(string, ...any)\n}\n\n// UseRedoc for documentation at /docs\nfunc (o *AlertmanagerAPI) UseRedoc() {\n\to.useSwaggerUI = false\n}\n\n// UseSwaggerUI for documentation at /docs\nfunc (o *AlertmanagerAPI) UseSwaggerUI() {\n\to.useSwaggerUI = true\n}\n\n// SetDefaultProduces sets the default produces media type\nfunc (o *AlertmanagerAPI) SetDefaultProduces(mediaType string) {\n\to.defaultProduces = mediaType\n}\n\n// SetDefaultConsumes returns the default consumes media type\nfunc (o *AlertmanagerAPI) SetDefaultConsumes(mediaType string) {\n\to.defaultConsumes = mediaType\n}\n\n// SetSpec sets a spec that will be served for the clients.\nfunc (o *AlertmanagerAPI) SetSpec(spec *loads.Document) {\n\to.spec = spec\n}\n\n// DefaultProduces returns the default produces media type\nfunc (o *AlertmanagerAPI) DefaultProduces() string {\n\treturn o.defaultProduces\n}\n\n// DefaultConsumes returns the default consumes media type\nfunc (o *AlertmanagerAPI) DefaultConsumes() string {\n\treturn o.defaultConsumes\n}\n\n// Formats returns the registered string formats\nfunc (o *AlertmanagerAPI) Formats() strfmt.Registry {\n\treturn o.formats\n}\n\n// RegisterFormat registers a custom format validator\nfunc (o *AlertmanagerAPI) RegisterFormat(name string, format strfmt.Format, validator strfmt.Validator) {\n\to.formats.Add(name, format, validator)\n}\n\n// Validate validates the registrations in the AlertmanagerAPI\nfunc (o *AlertmanagerAPI) Validate() error {\n\tvar unregistered []string\n\n\tif o.JSONConsumer == nil {\n\t\tunregistered = append(unregistered, \"JSONConsumer\")\n\t}\n\n\tif o.JSONProducer == nil {\n\t\tunregistered = append(unregistered, \"JSONProducer\")\n\t}\n\n\tif o.SilenceDeleteSilenceHandler == nil {\n\t\tunregistered = append(unregistered, \"silence.DeleteSilenceHandler\")\n\t}\n\tif o.AlertgroupGetAlertGroupsHandler == nil {\n\t\tunregistered = append(unregistered, \"alertgroup.GetAlertGroupsHandler\")\n\t}\n\tif o.AlertGetAlertsHandler == nil {\n\t\tunregistered = append(unregistered, \"alert.GetAlertsHandler\")\n\t}\n\tif o.ReceiverGetReceiversHandler == nil {\n\t\tunregistered = append(unregistered, \"receiver.GetReceiversHandler\")\n\t}\n\tif o.SilenceGetSilenceHandler == nil {\n\t\tunregistered = append(unregistered, \"silence.GetSilenceHandler\")\n\t}\n\tif o.SilenceGetSilencesHandler == nil {\n\t\tunregistered = append(unregistered, \"silence.GetSilencesHandler\")\n\t}\n\tif o.GeneralGetStatusHandler == nil {\n\t\tunregistered = append(unregistered, \"general.GetStatusHandler\")\n\t}\n\tif o.AlertPostAlertsHandler == nil {\n\t\tunregistered = append(unregistered, \"alert.PostAlertsHandler\")\n\t}\n\tif o.SilencePostSilencesHandler == nil {\n\t\tunregistered = append(unregistered, \"silence.PostSilencesHandler\")\n\t}\n\n\tif len(unregistered) > 0 {\n\t\treturn fmt.Errorf(\"missing registration: %s\", strings.Join(unregistered, \", \"))\n\t}\n\n\treturn nil\n}\n\n// ServeErrorFor gets a error handler for a given operation id\nfunc (o *AlertmanagerAPI) ServeErrorFor(operationID string) func(http.ResponseWriter, *http.Request, error) {\n\treturn o.ServeError\n}\n\n// AuthenticatorsFor gets the authenticators for the specified security schemes\nfunc (o *AlertmanagerAPI) AuthenticatorsFor(schemes map[string]spec.SecurityScheme) map[string]runtime.Authenticator {\n\treturn nil\n}\n\n// Authorizer returns the registered authorizer\nfunc (o *AlertmanagerAPI) Authorizer() runtime.Authorizer {\n\treturn nil\n}\n\n// ConsumersFor gets the consumers for the specified media types.\n//\n// MIME type parameters are ignored here.\nfunc (o *AlertmanagerAPI) ConsumersFor(mediaTypes []string) map[string]runtime.Consumer {\n\tresult := make(map[string]runtime.Consumer, len(mediaTypes))\n\tfor _, mt := range mediaTypes {\n\t\tif mt == \"application/json\" {\n\t\t\tresult[\"application/json\"] = o.JSONConsumer\n\t\t}\n\n\t\tif c, ok := o.customConsumers[mt]; ok {\n\t\t\tresult[mt] = c\n\t\t}\n\t}\n\n\treturn result\n}\n\n// ProducersFor gets the producers for the specified media types.\n//\n// MIME type parameters are ignored here.\nfunc (o *AlertmanagerAPI) ProducersFor(mediaTypes []string) map[string]runtime.Producer {\n\tresult := make(map[string]runtime.Producer, len(mediaTypes))\n\tfor _, mt := range mediaTypes {\n\t\tif mt == \"application/json\" {\n\t\t\tresult[\"application/json\"] = o.JSONProducer\n\t\t}\n\n\t\tif p, ok := o.customProducers[mt]; ok {\n\t\t\tresult[mt] = p\n\t\t}\n\t}\n\n\treturn result\n}\n\n// HandlerFor gets a http.Handler for the provided operation method and path\nfunc (o *AlertmanagerAPI) HandlerFor(method, path string) (http.Handler, bool) {\n\tif o.handlers == nil {\n\t\treturn nil, false\n\t}\n\tum := strings.ToUpper(method)\n\tif _, ok := o.handlers[um]; !ok {\n\t\treturn nil, false\n\t}\n\tif path == \"/\" {\n\t\tpath = \"\"\n\t}\n\th, ok := o.handlers[um][path]\n\treturn h, ok\n}\n\n// Context returns the middleware context for the alertmanager API\nfunc (o *AlertmanagerAPI) Context() *middleware.Context {\n\tif o.context == nil {\n\t\to.context = middleware.NewRoutableContext(o.spec, o, nil)\n\t}\n\n\treturn o.context\n}\n\nfunc (o *AlertmanagerAPI) initHandlerCache() {\n\to.Context() // don't care about the result, just that the initialization happened\n\tif o.handlers == nil {\n\t\to.handlers = make(map[string]map[string]http.Handler)\n\t}\n\n\tif o.handlers[\"DELETE\"] == nil {\n\t\to.handlers[\"DELETE\"] = make(map[string]http.Handler)\n\t}\n\to.handlers[\"DELETE\"][\"/silence/{silenceID}\"] = silence.NewDeleteSilence(o.context, o.SilenceDeleteSilenceHandler)\n\tif o.handlers[\"GET\"] == nil {\n\t\to.handlers[\"GET\"] = make(map[string]http.Handler)\n\t}\n\to.handlers[\"GET\"][\"/alerts/groups\"] = alertgroup.NewGetAlertGroups(o.context, o.AlertgroupGetAlertGroupsHandler)\n\tif o.handlers[\"GET\"] == nil {\n\t\to.handlers[\"GET\"] = make(map[string]http.Handler)\n\t}\n\to.handlers[\"GET\"][\"/alerts\"] = alert.NewGetAlerts(o.context, o.AlertGetAlertsHandler)\n\tif o.handlers[\"GET\"] == nil {\n\t\to.handlers[\"GET\"] = make(map[string]http.Handler)\n\t}\n\to.handlers[\"GET\"][\"/receivers\"] = receiver.NewGetReceivers(o.context, o.ReceiverGetReceiversHandler)\n\tif o.handlers[\"GET\"] == nil {\n\t\to.handlers[\"GET\"] = make(map[string]http.Handler)\n\t}\n\to.handlers[\"GET\"][\"/silence/{silenceID}\"] = silence.NewGetSilence(o.context, o.SilenceGetSilenceHandler)\n\tif o.handlers[\"GET\"] == nil {\n\t\to.handlers[\"GET\"] = make(map[string]http.Handler)\n\t}\n\to.handlers[\"GET\"][\"/silences\"] = silence.NewGetSilences(o.context, o.SilenceGetSilencesHandler)\n\tif o.handlers[\"GET\"] == nil {\n\t\to.handlers[\"GET\"] = make(map[string]http.Handler)\n\t}\n\to.handlers[\"GET\"][\"/status\"] = general.NewGetStatus(o.context, o.GeneralGetStatusHandler)\n\tif o.handlers[\"POST\"] == nil {\n\t\to.handlers[\"POST\"] = make(map[string]http.Handler)\n\t}\n\to.handlers[\"POST\"][\"/alerts\"] = alert.NewPostAlerts(o.context, o.AlertPostAlertsHandler)\n\tif o.handlers[\"POST\"] == nil {\n\t\to.handlers[\"POST\"] = make(map[string]http.Handler)\n\t}\n\to.handlers[\"POST\"][\"/silences\"] = silence.NewPostSilences(o.context, o.SilencePostSilencesHandler)\n}\n\n// Serve creates a http handler to serve the API over HTTP\n// can be used directly in http.ListenAndServe(\":8000\", api.Serve(nil))\nfunc (o *AlertmanagerAPI) Serve(builder middleware.Builder) http.Handler {\n\to.Init()\n\n\tif o.Middleware != nil {\n\t\treturn o.Middleware(builder)\n\t}\n\tif o.useSwaggerUI {\n\t\treturn o.context.APIHandlerSwaggerUI(builder)\n\t}\n\treturn o.context.APIHandler(builder)\n}\n\n// Init allows you to just initialize the handler cache, you can then recompose the middleware as you see fit\nfunc (o *AlertmanagerAPI) Init() {\n\tif len(o.handlers) == 0 {\n\t\to.initHandlerCache()\n\t}\n}\n\n// RegisterConsumer allows you to add (or override) a consumer for a media type.\nfunc (o *AlertmanagerAPI) RegisterConsumer(mediaType string, consumer runtime.Consumer) {\n\to.customConsumers[mediaType] = consumer\n}\n\n// RegisterProducer allows you to add (or override) a producer for a media type.\nfunc (o *AlertmanagerAPI) RegisterProducer(mediaType string, producer runtime.Producer) {\n\to.customProducers[mediaType] = producer\n}\n\n// AddMiddlewareFor adds a http middleware to existing handler\nfunc (o *AlertmanagerAPI) AddMiddlewareFor(method, path string, builder middleware.Builder) {\n\tum := strings.ToUpper(method)\n\tif path == \"/\" {\n\t\tpath = \"\"\n\t}\n\to.Init()\n\tif h, ok := o.handlers[um][path]; ok {\n\t\to.handlers[um][path] = builder(h)\n\t}\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/general/get_status.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage general\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime/middleware\"\n)\n\n// GetStatusHandlerFunc turns a function with the right signature into a get status handler\ntype GetStatusHandlerFunc func(GetStatusParams) middleware.Responder\n\n// Handle executing the request and returning a response\nfunc (fn GetStatusHandlerFunc) Handle(params GetStatusParams) middleware.Responder {\n\treturn fn(params)\n}\n\n// GetStatusHandler interface for that can handle valid get status params\ntype GetStatusHandler interface {\n\tHandle(GetStatusParams) middleware.Responder\n}\n\n// NewGetStatus creates a new http.Handler for the get status operation\nfunc NewGetStatus(ctx *middleware.Context, handler GetStatusHandler) *GetStatus {\n\treturn &GetStatus{Context: ctx, Handler: handler}\n}\n\n/*\n\tGetStatus swagger:route GET /status general getStatus\n\nGet current status of an Alertmanager instance and its cluster\n*/\ntype GetStatus struct {\n\tContext *middleware.Context\n\tHandler GetStatusHandler\n}\n\nfunc (o *GetStatus) ServeHTTP(rw http.ResponseWriter, r *http.Request) {\n\troute, rCtx, _ := o.Context.RouteInfo(r)\n\tif rCtx != nil {\n\t\t*r = *rCtx\n\t}\n\tvar Params = NewGetStatusParams()\n\tif err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params\n\t\to.Context.Respond(rw, r, route.Produces, route, err)\n\t\treturn\n\t}\n\n\tres := o.Handler.Handle(Params) // actually handle the request\n\n\to.Context.Respond(rw, r, route.Produces, route, res)\n\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/general/get_status_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage general\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime/middleware\"\n)\n\n// NewGetStatusParams creates a new GetStatusParams object\n//\n// There are no default values defined in the spec.\nfunc NewGetStatusParams() GetStatusParams {\n\n\treturn GetStatusParams{}\n}\n\n// GetStatusParams contains all the bound params for the get status operation\n// typically these are obtained from a http.Request\n//\n// swagger:parameters getStatus\ntype GetStatusParams struct {\n\t// HTTP Request Object\n\tHTTPRequest *http.Request `json:\"-\"`\n}\n\n// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface\n// for simple values it will use straight method calls.\n//\n// To ensure default values, the struct must have been initialized with NewGetStatusParams() beforehand.\nfunc (o *GetStatusParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {\n\tvar res []error\n\n\to.HTTPRequest = r\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/general/get_status_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage general\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\n// GetStatusOKCode is the HTTP code returned for type GetStatusOK\nconst GetStatusOKCode int = 200\n\n/*\nGetStatusOK Get status response\n\nswagger:response getStatusOK\n*/\ntype GetStatusOK struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload *models.AlertmanagerStatus `json:\"body,omitempty\"`\n}\n\n// NewGetStatusOK creates GetStatusOK with default headers values\nfunc NewGetStatusOK() *GetStatusOK {\n\n\treturn &GetStatusOK{}\n}\n\n// WithPayload adds the payload to the get status o k response\nfunc (o *GetStatusOK) WithPayload(payload *models.AlertmanagerStatus) *GetStatusOK {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the get status o k response\nfunc (o *GetStatusOK) SetPayload(payload *models.AlertmanagerStatus) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *GetStatusOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(200)\n\tif o.Payload != nil {\n\t\tpayload := o.Payload\n\t\tif err := producer.Produce(rw, payload); err != nil {\n\t\t\tpanic(err) // let the recovery middleware deal with this\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/general/get_status_urlbuilder.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage general\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\tgolangswaggerpaths \"path\"\n)\n\n// GetStatusURL generates an URL for the get status operation\ntype GetStatusURL struct {\n\t_basePath string\n}\n\n// WithBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *GetStatusURL) WithBasePath(bp string) *GetStatusURL {\n\to.SetBasePath(bp)\n\treturn o\n}\n\n// SetBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *GetStatusURL) SetBasePath(bp string) {\n\to._basePath = bp\n}\n\n// Build a url path and query string\nfunc (o *GetStatusURL) Build() (*url.URL, error) {\n\tvar _result url.URL\n\n\tvar _path = \"/status\"\n\n\t_basePath := o._basePath\n\tif _basePath == \"\" {\n\t\t_basePath = \"/api/v2/\"\n\t}\n\t_result.Path = golangswaggerpaths.Join(_basePath, _path)\n\n\treturn &_result, nil\n}\n\n// Must is a helper function to panic when the url builder returns an error\nfunc (o *GetStatusURL) Must(u *url.URL, err error) *url.URL {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif u == nil {\n\t\tpanic(\"url can't be nil\")\n\t}\n\treturn u\n}\n\n// String returns the string representation of the path with query string\nfunc (o *GetStatusURL) String() string {\n\treturn o.Must(o.Build()).String()\n}\n\n// BuildFull builds a full url with scheme, host, path and query string\nfunc (o *GetStatusURL) BuildFull(scheme, host string) (*url.URL, error) {\n\tif scheme == \"\" {\n\t\treturn nil, errors.New(\"scheme is required for a full url on GetStatusURL\")\n\t}\n\tif host == \"\" {\n\t\treturn nil, errors.New(\"host is required for a full url on GetStatusURL\")\n\t}\n\n\tbase, err := o.Build()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbase.Scheme = scheme\n\tbase.Host = host\n\treturn base, nil\n}\n\n// StringFull returns the string representation of a complete url\nfunc (o *GetStatusURL) StringFull(scheme, host string) string {\n\treturn o.Must(o.BuildFull(scheme, host)).String()\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/receiver/get_receivers.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage receiver\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime/middleware\"\n)\n\n// GetReceiversHandlerFunc turns a function with the right signature into a get receivers handler\ntype GetReceiversHandlerFunc func(GetReceiversParams) middleware.Responder\n\n// Handle executing the request and returning a response\nfunc (fn GetReceiversHandlerFunc) Handle(params GetReceiversParams) middleware.Responder {\n\treturn fn(params)\n}\n\n// GetReceiversHandler interface for that can handle valid get receivers params\ntype GetReceiversHandler interface {\n\tHandle(GetReceiversParams) middleware.Responder\n}\n\n// NewGetReceivers creates a new http.Handler for the get receivers operation\nfunc NewGetReceivers(ctx *middleware.Context, handler GetReceiversHandler) *GetReceivers {\n\treturn &GetReceivers{Context: ctx, Handler: handler}\n}\n\n/*\n\tGetReceivers swagger:route GET /receivers receiver getReceivers\n\nGet list of all receivers (name of notification integrations)\n*/\ntype GetReceivers struct {\n\tContext *middleware.Context\n\tHandler GetReceiversHandler\n}\n\nfunc (o *GetReceivers) ServeHTTP(rw http.ResponseWriter, r *http.Request) {\n\troute, rCtx, _ := o.Context.RouteInfo(r)\n\tif rCtx != nil {\n\t\t*r = *rCtx\n\t}\n\tvar Params = NewGetReceiversParams()\n\tif err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params\n\t\to.Context.Respond(rw, r, route.Produces, route, err)\n\t\treturn\n\t}\n\n\tres := o.Handler.Handle(Params) // actually handle the request\n\n\to.Context.Respond(rw, r, route.Produces, route, res)\n\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/receiver/get_receivers_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage receiver\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime/middleware\"\n)\n\n// NewGetReceiversParams creates a new GetReceiversParams object\n//\n// There are no default values defined in the spec.\nfunc NewGetReceiversParams() GetReceiversParams {\n\n\treturn GetReceiversParams{}\n}\n\n// GetReceiversParams contains all the bound params for the get receivers operation\n// typically these are obtained from a http.Request\n//\n// swagger:parameters getReceivers\ntype GetReceiversParams struct {\n\t// HTTP Request Object\n\tHTTPRequest *http.Request `json:\"-\"`\n}\n\n// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface\n// for simple values it will use straight method calls.\n//\n// To ensure default values, the struct must have been initialized with NewGetReceiversParams() beforehand.\nfunc (o *GetReceiversParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {\n\tvar res []error\n\n\to.HTTPRequest = r\n\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/receiver/get_receivers_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage receiver\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\n// GetReceiversOKCode is the HTTP code returned for type GetReceiversOK\nconst GetReceiversOKCode int = 200\n\n/*\nGetReceiversOK Get receivers response\n\nswagger:response getReceiversOK\n*/\ntype GetReceiversOK struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload []*models.Receiver `json:\"body,omitempty\"`\n}\n\n// NewGetReceiversOK creates GetReceiversOK with default headers values\nfunc NewGetReceiversOK() *GetReceiversOK {\n\n\treturn &GetReceiversOK{}\n}\n\n// WithPayload adds the payload to the get receivers o k response\nfunc (o *GetReceiversOK) WithPayload(payload []*models.Receiver) *GetReceiversOK {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the get receivers o k response\nfunc (o *GetReceiversOK) SetPayload(payload []*models.Receiver) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *GetReceiversOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(200)\n\tpayload := o.Payload\n\tif payload == nil {\n\t\t// return empty array\n\t\tpayload = make([]*models.Receiver, 0, 50)\n\t}\n\n\tif err := producer.Produce(rw, payload); err != nil {\n\t\tpanic(err) // let the recovery middleware deal with this\n\t}\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/receiver/get_receivers_urlbuilder.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage receiver\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\tgolangswaggerpaths \"path\"\n)\n\n// GetReceiversURL generates an URL for the get receivers operation\ntype GetReceiversURL struct {\n\t_basePath string\n}\n\n// WithBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *GetReceiversURL) WithBasePath(bp string) *GetReceiversURL {\n\to.SetBasePath(bp)\n\treturn o\n}\n\n// SetBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *GetReceiversURL) SetBasePath(bp string) {\n\to._basePath = bp\n}\n\n// Build a url path and query string\nfunc (o *GetReceiversURL) Build() (*url.URL, error) {\n\tvar _result url.URL\n\n\tvar _path = \"/receivers\"\n\n\t_basePath := o._basePath\n\tif _basePath == \"\" {\n\t\t_basePath = \"/api/v2/\"\n\t}\n\t_result.Path = golangswaggerpaths.Join(_basePath, _path)\n\n\treturn &_result, nil\n}\n\n// Must is a helper function to panic when the url builder returns an error\nfunc (o *GetReceiversURL) Must(u *url.URL, err error) *url.URL {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif u == nil {\n\t\tpanic(\"url can't be nil\")\n\t}\n\treturn u\n}\n\n// String returns the string representation of the path with query string\nfunc (o *GetReceiversURL) String() string {\n\treturn o.Must(o.Build()).String()\n}\n\n// BuildFull builds a full url with scheme, host, path and query string\nfunc (o *GetReceiversURL) BuildFull(scheme, host string) (*url.URL, error) {\n\tif scheme == \"\" {\n\t\treturn nil, errors.New(\"scheme is required for a full url on GetReceiversURL\")\n\t}\n\tif host == \"\" {\n\t\treturn nil, errors.New(\"host is required for a full url on GetReceiversURL\")\n\t}\n\n\tbase, err := o.Build()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbase.Scheme = scheme\n\tbase.Host = host\n\treturn base, nil\n}\n\n// StringFull returns the string representation of a complete url\nfunc (o *GetReceiversURL) StringFull(scheme, host string) string {\n\treturn o.Must(o.BuildFull(scheme, host)).String()\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/silence/delete_silence.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime/middleware\"\n)\n\n// DeleteSilenceHandlerFunc turns a function with the right signature into a delete silence handler\ntype DeleteSilenceHandlerFunc func(DeleteSilenceParams) middleware.Responder\n\n// Handle executing the request and returning a response\nfunc (fn DeleteSilenceHandlerFunc) Handle(params DeleteSilenceParams) middleware.Responder {\n\treturn fn(params)\n}\n\n// DeleteSilenceHandler interface for that can handle valid delete silence params\ntype DeleteSilenceHandler interface {\n\tHandle(DeleteSilenceParams) middleware.Responder\n}\n\n// NewDeleteSilence creates a new http.Handler for the delete silence operation\nfunc NewDeleteSilence(ctx *middleware.Context, handler DeleteSilenceHandler) *DeleteSilence {\n\treturn &DeleteSilence{Context: ctx, Handler: handler}\n}\n\n/*\n\tDeleteSilence swagger:route DELETE /silence/{silenceID} silence deleteSilence\n\nDelete a silence by its ID\n*/\ntype DeleteSilence struct {\n\tContext *middleware.Context\n\tHandler DeleteSilenceHandler\n}\n\nfunc (o *DeleteSilence) ServeHTTP(rw http.ResponseWriter, r *http.Request) {\n\troute, rCtx, _ := o.Context.RouteInfo(r)\n\tif rCtx != nil {\n\t\t*r = *rCtx\n\t}\n\tvar Params = NewDeleteSilenceParams()\n\tif err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params\n\t\to.Context.Respond(rw, r, route.Produces, route, err)\n\t\treturn\n\t}\n\n\tres := o.Handler.Handle(Params) // actually handle the request\n\n\to.Context.Respond(rw, r, route.Produces, route, res)\n\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/silence/delete_silence_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime/middleware\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// NewDeleteSilenceParams creates a new DeleteSilenceParams object\n//\n// There are no default values defined in the spec.\nfunc NewDeleteSilenceParams() DeleteSilenceParams {\n\n\treturn DeleteSilenceParams{}\n}\n\n// DeleteSilenceParams contains all the bound params for the delete silence operation\n// typically these are obtained from a http.Request\n//\n// swagger:parameters deleteSilence\ntype DeleteSilenceParams struct {\n\t// HTTP Request Object\n\tHTTPRequest *http.Request `json:\"-\"`\n\n\t/*ID of the silence to get\n\t  Required: true\n\t  In: path\n\t*/\n\tSilenceID strfmt.UUID\n}\n\n// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface\n// for simple values it will use straight method calls.\n//\n// To ensure default values, the struct must have been initialized with NewDeleteSilenceParams() beforehand.\nfunc (o *DeleteSilenceParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {\n\tvar res []error\n\n\to.HTTPRequest = r\n\n\trSilenceID, rhkSilenceID, _ := route.Params.GetOK(\"silenceID\")\n\tif err := o.bindSilenceID(rSilenceID, rhkSilenceID, route.Formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\n// bindSilenceID binds and validates parameter SilenceID from path.\nfunc (o *DeleteSilenceParams) bindSilenceID(rawData []string, hasKey bool, formats strfmt.Registry) error {\n\tvar raw string\n\tif len(rawData) > 0 {\n\t\traw = rawData[len(rawData)-1]\n\t}\n\n\t// Required: true\n\t// Parameter is provided by construction from the route\n\n\t// Format: uuid\n\tvalue, err := formats.Parse(\"uuid\", raw)\n\tif err != nil {\n\t\treturn errors.InvalidType(\"silenceID\", \"path\", \"strfmt.UUID\", raw)\n\t}\n\to.SilenceID = *(value.(*strfmt.UUID))\n\n\tif err := o.validateSilenceID(formats); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// validateSilenceID carries out validations for parameter SilenceID\nfunc (o *DeleteSilenceParams) validateSilenceID(formats strfmt.Registry) error {\n\n\tif err := validate.FormatOf(\"silenceID\", \"path\", \"uuid\", o.SilenceID.String(), formats); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/silence/delete_silence_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime\"\n)\n\n// DeleteSilenceOKCode is the HTTP code returned for type DeleteSilenceOK\nconst DeleteSilenceOKCode int = 200\n\n/*\nDeleteSilenceOK Delete silence response\n\nswagger:response deleteSilenceOK\n*/\ntype DeleteSilenceOK struct {\n}\n\n// NewDeleteSilenceOK creates DeleteSilenceOK with default headers values\nfunc NewDeleteSilenceOK() *DeleteSilenceOK {\n\n\treturn &DeleteSilenceOK{}\n}\n\n// WriteResponse to the client\nfunc (o *DeleteSilenceOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.Header().Del(runtime.HeaderContentType) // Remove Content-Type on empty responses\n\n\trw.WriteHeader(200)\n}\n\n// DeleteSilenceNotFoundCode is the HTTP code returned for type DeleteSilenceNotFound\nconst DeleteSilenceNotFoundCode int = 404\n\n/*\nDeleteSilenceNotFound A silence with the specified ID was not found\n\nswagger:response deleteSilenceNotFound\n*/\ntype DeleteSilenceNotFound struct {\n}\n\n// NewDeleteSilenceNotFound creates DeleteSilenceNotFound with default headers values\nfunc NewDeleteSilenceNotFound() *DeleteSilenceNotFound {\n\n\treturn &DeleteSilenceNotFound{}\n}\n\n// WriteResponse to the client\nfunc (o *DeleteSilenceNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.Header().Del(runtime.HeaderContentType) // Remove Content-Type on empty responses\n\n\trw.WriteHeader(404)\n}\n\n// DeleteSilenceInternalServerErrorCode is the HTTP code returned for type DeleteSilenceInternalServerError\nconst DeleteSilenceInternalServerErrorCode int = 500\n\n/*\nDeleteSilenceInternalServerError Internal server error\n\nswagger:response deleteSilenceInternalServerError\n*/\ntype DeleteSilenceInternalServerError struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload string `json:\"body,omitempty\"`\n}\n\n// NewDeleteSilenceInternalServerError creates DeleteSilenceInternalServerError with default headers values\nfunc NewDeleteSilenceInternalServerError() *DeleteSilenceInternalServerError {\n\n\treturn &DeleteSilenceInternalServerError{}\n}\n\n// WithPayload adds the payload to the delete silence internal server error response\nfunc (o *DeleteSilenceInternalServerError) WithPayload(payload string) *DeleteSilenceInternalServerError {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the delete silence internal server error response\nfunc (o *DeleteSilenceInternalServerError) SetPayload(payload string) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *DeleteSilenceInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(500)\n\tpayload := o.Payload\n\tif err := producer.Produce(rw, payload); err != nil {\n\t\tpanic(err) // let the recovery middleware deal with this\n\t}\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/silence/delete_silence_urlbuilder.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\tgolangswaggerpaths \"path\"\n\t\"strings\"\n\n\t\"github.com/go-openapi/strfmt\"\n)\n\n// DeleteSilenceURL generates an URL for the delete silence operation\ntype DeleteSilenceURL struct {\n\tSilenceID strfmt.UUID\n\n\t_basePath string\n\t// avoid unkeyed usage\n\t_ struct{}\n}\n\n// WithBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *DeleteSilenceURL) WithBasePath(bp string) *DeleteSilenceURL {\n\to.SetBasePath(bp)\n\treturn o\n}\n\n// SetBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *DeleteSilenceURL) SetBasePath(bp string) {\n\to._basePath = bp\n}\n\n// Build a url path and query string\nfunc (o *DeleteSilenceURL) Build() (*url.URL, error) {\n\tvar _result url.URL\n\n\tvar _path = \"/silence/{silenceID}\"\n\n\tsilenceID := o.SilenceID.String()\n\tif silenceID != \"\" {\n\t\t_path = strings.ReplaceAll(_path, \"{silenceID}\", silenceID)\n\t} else {\n\t\treturn nil, errors.New(\"silenceId is required on DeleteSilenceURL\")\n\t}\n\n\t_basePath := o._basePath\n\tif _basePath == \"\" {\n\t\t_basePath = \"/api/v2/\"\n\t}\n\t_result.Path = golangswaggerpaths.Join(_basePath, _path)\n\n\treturn &_result, nil\n}\n\n// Must is a helper function to panic when the url builder returns an error\nfunc (o *DeleteSilenceURL) Must(u *url.URL, err error) *url.URL {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif u == nil {\n\t\tpanic(\"url can't be nil\")\n\t}\n\treturn u\n}\n\n// String returns the string representation of the path with query string\nfunc (o *DeleteSilenceURL) String() string {\n\treturn o.Must(o.Build()).String()\n}\n\n// BuildFull builds a full url with scheme, host, path and query string\nfunc (o *DeleteSilenceURL) BuildFull(scheme, host string) (*url.URL, error) {\n\tif scheme == \"\" {\n\t\treturn nil, errors.New(\"scheme is required for a full url on DeleteSilenceURL\")\n\t}\n\tif host == \"\" {\n\t\treturn nil, errors.New(\"host is required for a full url on DeleteSilenceURL\")\n\t}\n\n\tbase, err := o.Build()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbase.Scheme = scheme\n\tbase.Host = host\n\treturn base, nil\n}\n\n// StringFull returns the string representation of a complete url\nfunc (o *DeleteSilenceURL) StringFull(scheme, host string) string {\n\treturn o.Must(o.BuildFull(scheme, host)).String()\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/silence/get_silence.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime/middleware\"\n)\n\n// GetSilenceHandlerFunc turns a function with the right signature into a get silence handler\ntype GetSilenceHandlerFunc func(GetSilenceParams) middleware.Responder\n\n// Handle executing the request and returning a response\nfunc (fn GetSilenceHandlerFunc) Handle(params GetSilenceParams) middleware.Responder {\n\treturn fn(params)\n}\n\n// GetSilenceHandler interface for that can handle valid get silence params\ntype GetSilenceHandler interface {\n\tHandle(GetSilenceParams) middleware.Responder\n}\n\n// NewGetSilence creates a new http.Handler for the get silence operation\nfunc NewGetSilence(ctx *middleware.Context, handler GetSilenceHandler) *GetSilence {\n\treturn &GetSilence{Context: ctx, Handler: handler}\n}\n\n/*\n\tGetSilence swagger:route GET /silence/{silenceID} silence getSilence\n\nGet a silence by its ID\n*/\ntype GetSilence struct {\n\tContext *middleware.Context\n\tHandler GetSilenceHandler\n}\n\nfunc (o *GetSilence) ServeHTTP(rw http.ResponseWriter, r *http.Request) {\n\troute, rCtx, _ := o.Context.RouteInfo(r)\n\tif rCtx != nil {\n\t\t*r = *rCtx\n\t}\n\tvar Params = NewGetSilenceParams()\n\tif err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params\n\t\to.Context.Respond(rw, r, route.Produces, route, err)\n\t\treturn\n\t}\n\n\tres := o.Handler.Handle(Params) // actually handle the request\n\n\to.Context.Respond(rw, r, route.Produces, route, res)\n\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/silence/get_silence_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime/middleware\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/validate\"\n)\n\n// NewGetSilenceParams creates a new GetSilenceParams object\n//\n// There are no default values defined in the spec.\nfunc NewGetSilenceParams() GetSilenceParams {\n\n\treturn GetSilenceParams{}\n}\n\n// GetSilenceParams contains all the bound params for the get silence operation\n// typically these are obtained from a http.Request\n//\n// swagger:parameters getSilence\ntype GetSilenceParams struct {\n\t// HTTP Request Object\n\tHTTPRequest *http.Request `json:\"-\"`\n\n\t/*ID of the silence to get\n\t  Required: true\n\t  In: path\n\t*/\n\tSilenceID strfmt.UUID\n}\n\n// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface\n// for simple values it will use straight method calls.\n//\n// To ensure default values, the struct must have been initialized with NewGetSilenceParams() beforehand.\nfunc (o *GetSilenceParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {\n\tvar res []error\n\n\to.HTTPRequest = r\n\n\trSilenceID, rhkSilenceID, _ := route.Params.GetOK(\"silenceID\")\n\tif err := o.bindSilenceID(rSilenceID, rhkSilenceID, route.Formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\n// bindSilenceID binds and validates parameter SilenceID from path.\nfunc (o *GetSilenceParams) bindSilenceID(rawData []string, hasKey bool, formats strfmt.Registry) error {\n\tvar raw string\n\tif len(rawData) > 0 {\n\t\traw = rawData[len(rawData)-1]\n\t}\n\n\t// Required: true\n\t// Parameter is provided by construction from the route\n\n\t// Format: uuid\n\tvalue, err := formats.Parse(\"uuid\", raw)\n\tif err != nil {\n\t\treturn errors.InvalidType(\"silenceID\", \"path\", \"strfmt.UUID\", raw)\n\t}\n\to.SilenceID = *(value.(*strfmt.UUID))\n\n\tif err := o.validateSilenceID(formats); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// validateSilenceID carries out validations for parameter SilenceID\nfunc (o *GetSilenceParams) validateSilenceID(formats strfmt.Registry) error {\n\n\tif err := validate.FormatOf(\"silenceID\", \"path\", \"uuid\", o.SilenceID.String(), formats); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/silence/get_silence_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\n// GetSilenceOKCode is the HTTP code returned for type GetSilenceOK\nconst GetSilenceOKCode int = 200\n\n/*\nGetSilenceOK Get silence response\n\nswagger:response getSilenceOK\n*/\ntype GetSilenceOK struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload *models.GettableSilence `json:\"body,omitempty\"`\n}\n\n// NewGetSilenceOK creates GetSilenceOK with default headers values\nfunc NewGetSilenceOK() *GetSilenceOK {\n\n\treturn &GetSilenceOK{}\n}\n\n// WithPayload adds the payload to the get silence o k response\nfunc (o *GetSilenceOK) WithPayload(payload *models.GettableSilence) *GetSilenceOK {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the get silence o k response\nfunc (o *GetSilenceOK) SetPayload(payload *models.GettableSilence) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *GetSilenceOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(200)\n\tif o.Payload != nil {\n\t\tpayload := o.Payload\n\t\tif err := producer.Produce(rw, payload); err != nil {\n\t\t\tpanic(err) // let the recovery middleware deal with this\n\t\t}\n\t}\n}\n\n// GetSilenceNotFoundCode is the HTTP code returned for type GetSilenceNotFound\nconst GetSilenceNotFoundCode int = 404\n\n/*\nGetSilenceNotFound A silence with the specified ID was not found\n\nswagger:response getSilenceNotFound\n*/\ntype GetSilenceNotFound struct {\n}\n\n// NewGetSilenceNotFound creates GetSilenceNotFound with default headers values\nfunc NewGetSilenceNotFound() *GetSilenceNotFound {\n\n\treturn &GetSilenceNotFound{}\n}\n\n// WriteResponse to the client\nfunc (o *GetSilenceNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.Header().Del(runtime.HeaderContentType) // Remove Content-Type on empty responses\n\n\trw.WriteHeader(404)\n}\n\n// GetSilenceInternalServerErrorCode is the HTTP code returned for type GetSilenceInternalServerError\nconst GetSilenceInternalServerErrorCode int = 500\n\n/*\nGetSilenceInternalServerError Internal server error\n\nswagger:response getSilenceInternalServerError\n*/\ntype GetSilenceInternalServerError struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload string `json:\"body,omitempty\"`\n}\n\n// NewGetSilenceInternalServerError creates GetSilenceInternalServerError with default headers values\nfunc NewGetSilenceInternalServerError() *GetSilenceInternalServerError {\n\n\treturn &GetSilenceInternalServerError{}\n}\n\n// WithPayload adds the payload to the get silence internal server error response\nfunc (o *GetSilenceInternalServerError) WithPayload(payload string) *GetSilenceInternalServerError {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the get silence internal server error response\nfunc (o *GetSilenceInternalServerError) SetPayload(payload string) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *GetSilenceInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(500)\n\tpayload := o.Payload\n\tif err := producer.Produce(rw, payload); err != nil {\n\t\tpanic(err) // let the recovery middleware deal with this\n\t}\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/silence/get_silence_urlbuilder.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\tgolangswaggerpaths \"path\"\n\t\"strings\"\n\n\t\"github.com/go-openapi/strfmt\"\n)\n\n// GetSilenceURL generates an URL for the get silence operation\ntype GetSilenceURL struct {\n\tSilenceID strfmt.UUID\n\n\t_basePath string\n\t// avoid unkeyed usage\n\t_ struct{}\n}\n\n// WithBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *GetSilenceURL) WithBasePath(bp string) *GetSilenceURL {\n\to.SetBasePath(bp)\n\treturn o\n}\n\n// SetBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *GetSilenceURL) SetBasePath(bp string) {\n\to._basePath = bp\n}\n\n// Build a url path and query string\nfunc (o *GetSilenceURL) Build() (*url.URL, error) {\n\tvar _result url.URL\n\n\tvar _path = \"/silence/{silenceID}\"\n\n\tsilenceID := o.SilenceID.String()\n\tif silenceID != \"\" {\n\t\t_path = strings.ReplaceAll(_path, \"{silenceID}\", silenceID)\n\t} else {\n\t\treturn nil, errors.New(\"silenceId is required on GetSilenceURL\")\n\t}\n\n\t_basePath := o._basePath\n\tif _basePath == \"\" {\n\t\t_basePath = \"/api/v2/\"\n\t}\n\t_result.Path = golangswaggerpaths.Join(_basePath, _path)\n\n\treturn &_result, nil\n}\n\n// Must is a helper function to panic when the url builder returns an error\nfunc (o *GetSilenceURL) Must(u *url.URL, err error) *url.URL {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif u == nil {\n\t\tpanic(\"url can't be nil\")\n\t}\n\treturn u\n}\n\n// String returns the string representation of the path with query string\nfunc (o *GetSilenceURL) String() string {\n\treturn o.Must(o.Build()).String()\n}\n\n// BuildFull builds a full url with scheme, host, path and query string\nfunc (o *GetSilenceURL) BuildFull(scheme, host string) (*url.URL, error) {\n\tif scheme == \"\" {\n\t\treturn nil, errors.New(\"scheme is required for a full url on GetSilenceURL\")\n\t}\n\tif host == \"\" {\n\t\treturn nil, errors.New(\"host is required for a full url on GetSilenceURL\")\n\t}\n\n\tbase, err := o.Build()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbase.Scheme = scheme\n\tbase.Host = host\n\treturn base, nil\n}\n\n// StringFull returns the string representation of a complete url\nfunc (o *GetSilenceURL) StringFull(scheme, host string) string {\n\treturn o.Must(o.BuildFull(scheme, host)).String()\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/silence/get_silences.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime/middleware\"\n)\n\n// GetSilencesHandlerFunc turns a function with the right signature into a get silences handler\ntype GetSilencesHandlerFunc func(GetSilencesParams) middleware.Responder\n\n// Handle executing the request and returning a response\nfunc (fn GetSilencesHandlerFunc) Handle(params GetSilencesParams) middleware.Responder {\n\treturn fn(params)\n}\n\n// GetSilencesHandler interface for that can handle valid get silences params\ntype GetSilencesHandler interface {\n\tHandle(GetSilencesParams) middleware.Responder\n}\n\n// NewGetSilences creates a new http.Handler for the get silences operation\nfunc NewGetSilences(ctx *middleware.Context, handler GetSilencesHandler) *GetSilences {\n\treturn &GetSilences{Context: ctx, Handler: handler}\n}\n\n/*\n\tGetSilences swagger:route GET /silences silence getSilences\n\nGet a list of silences\n*/\ntype GetSilences struct {\n\tContext *middleware.Context\n\tHandler GetSilencesHandler\n}\n\nfunc (o *GetSilences) ServeHTTP(rw http.ResponseWriter, r *http.Request) {\n\troute, rCtx, _ := o.Context.RouteInfo(r)\n\tif rCtx != nil {\n\t\t*r = *rCtx\n\t}\n\tvar Params = NewGetSilencesParams()\n\tif err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params\n\t\to.Context.Respond(rw, r, route.Produces, route, err)\n\t\treturn\n\t}\n\n\tres := o.Handler.Handle(Params) // actually handle the request\n\n\to.Context.Respond(rw, r, route.Produces, route, res)\n\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/silence/get_silences_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime\"\n\t\"github.com/go-openapi/runtime/middleware\"\n\t\"github.com/go-openapi/strfmt\"\n)\n\n// NewGetSilencesParams creates a new GetSilencesParams object\n//\n// There are no default values defined in the spec.\nfunc NewGetSilencesParams() GetSilencesParams {\n\n\treturn GetSilencesParams{}\n}\n\n// GetSilencesParams contains all the bound params for the get silences operation\n// typically these are obtained from a http.Request\n//\n// swagger:parameters getSilences\ntype GetSilencesParams struct {\n\t// HTTP Request Object\n\tHTTPRequest *http.Request `json:\"-\"`\n\n\t/*A matcher expression to filter silences. For example `alertname=\"MyAlert\"`. It can be repeated to apply multiple matchers.\n\t  In: query\n\t  Collection Format: multi\n\t*/\n\tFilter []string\n}\n\n// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface\n// for simple values it will use straight method calls.\n//\n// To ensure default values, the struct must have been initialized with NewGetSilencesParams() beforehand.\nfunc (o *GetSilencesParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {\n\tvar res []error\n\n\to.HTTPRequest = r\n\tqs := runtime.Values(r.URL.Query())\n\n\tqFilter, qhkFilter, _ := qs.GetOK(\"filter\")\n\tif err := o.bindFilter(qFilter, qhkFilter, route.Formats); err != nil {\n\t\tres = append(res, err)\n\t}\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n\n// bindFilter binds and validates array parameter Filter from query.\n//\n// Arrays are parsed according to CollectionFormat: \"multi\" (defaults to \"csv\" when empty).\nfunc (o *GetSilencesParams) bindFilter(rawData []string, hasKey bool, formats strfmt.Registry) error {\n\t// CollectionFormat: multi\n\tfilterIC := rawData\n\tif len(filterIC) == 0 {\n\t\treturn nil\n\t}\n\n\tvar filterIR []string\n\tfor _, filterIV := range filterIC {\n\t\tfilterI := filterIV\n\n\t\tfilterIR = append(filterIR, filterI)\n\t}\n\n\to.Filter = filterIR\n\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/silence/get_silences_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\n// GetSilencesOKCode is the HTTP code returned for type GetSilencesOK\nconst GetSilencesOKCode int = 200\n\n/*\nGetSilencesOK Get silences response\n\nswagger:response getSilencesOK\n*/\ntype GetSilencesOK struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload models.GettableSilences `json:\"body,omitempty\"`\n}\n\n// NewGetSilencesOK creates GetSilencesOK with default headers values\nfunc NewGetSilencesOK() *GetSilencesOK {\n\n\treturn &GetSilencesOK{}\n}\n\n// WithPayload adds the payload to the get silences o k response\nfunc (o *GetSilencesOK) WithPayload(payload models.GettableSilences) *GetSilencesOK {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the get silences o k response\nfunc (o *GetSilencesOK) SetPayload(payload models.GettableSilences) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *GetSilencesOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(200)\n\tpayload := o.Payload\n\tif payload == nil {\n\t\t// return empty array\n\t\tpayload = models.GettableSilences{}\n\t}\n\n\tif err := producer.Produce(rw, payload); err != nil {\n\t\tpanic(err) // let the recovery middleware deal with this\n\t}\n}\n\n// GetSilencesBadRequestCode is the HTTP code returned for type GetSilencesBadRequest\nconst GetSilencesBadRequestCode int = 400\n\n/*\nGetSilencesBadRequest Bad request\n\nswagger:response getSilencesBadRequest\n*/\ntype GetSilencesBadRequest struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload string `json:\"body,omitempty\"`\n}\n\n// NewGetSilencesBadRequest creates GetSilencesBadRequest with default headers values\nfunc NewGetSilencesBadRequest() *GetSilencesBadRequest {\n\n\treturn &GetSilencesBadRequest{}\n}\n\n// WithPayload adds the payload to the get silences bad request response\nfunc (o *GetSilencesBadRequest) WithPayload(payload string) *GetSilencesBadRequest {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the get silences bad request response\nfunc (o *GetSilencesBadRequest) SetPayload(payload string) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *GetSilencesBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(400)\n\tpayload := o.Payload\n\tif err := producer.Produce(rw, payload); err != nil {\n\t\tpanic(err) // let the recovery middleware deal with this\n\t}\n}\n\n// GetSilencesInternalServerErrorCode is the HTTP code returned for type GetSilencesInternalServerError\nconst GetSilencesInternalServerErrorCode int = 500\n\n/*\nGetSilencesInternalServerError Internal server error\n\nswagger:response getSilencesInternalServerError\n*/\ntype GetSilencesInternalServerError struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload string `json:\"body,omitempty\"`\n}\n\n// NewGetSilencesInternalServerError creates GetSilencesInternalServerError with default headers values\nfunc NewGetSilencesInternalServerError() *GetSilencesInternalServerError {\n\n\treturn &GetSilencesInternalServerError{}\n}\n\n// WithPayload adds the payload to the get silences internal server error response\nfunc (o *GetSilencesInternalServerError) WithPayload(payload string) *GetSilencesInternalServerError {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the get silences internal server error response\nfunc (o *GetSilencesInternalServerError) SetPayload(payload string) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *GetSilencesInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(500)\n\tpayload := o.Payload\n\tif err := producer.Produce(rw, payload); err != nil {\n\t\tpanic(err) // let the recovery middleware deal with this\n\t}\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/silence/get_silences_urlbuilder.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\tgolangswaggerpaths \"path\"\n\n\t\"github.com/go-openapi/swag\"\n)\n\n// GetSilencesURL generates an URL for the get silences operation\ntype GetSilencesURL struct {\n\tFilter []string\n\n\t_basePath string\n\t// avoid unkeyed usage\n\t_ struct{}\n}\n\n// WithBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *GetSilencesURL) WithBasePath(bp string) *GetSilencesURL {\n\to.SetBasePath(bp)\n\treturn o\n}\n\n// SetBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *GetSilencesURL) SetBasePath(bp string) {\n\to._basePath = bp\n}\n\n// Build a url path and query string\nfunc (o *GetSilencesURL) Build() (*url.URL, error) {\n\tvar _result url.URL\n\n\tvar _path = \"/silences\"\n\n\t_basePath := o._basePath\n\tif _basePath == \"\" {\n\t\t_basePath = \"/api/v2/\"\n\t}\n\t_result.Path = golangswaggerpaths.Join(_basePath, _path)\n\n\tqs := make(url.Values)\n\n\tvar filterIR []string\n\tfor _, filterI := range o.Filter {\n\t\tfilterIS := filterI\n\t\tif filterIS != \"\" {\n\t\t\tfilterIR = append(filterIR, filterIS)\n\t\t}\n\t}\n\n\tfilter := swag.JoinByFormat(filterIR, \"multi\")\n\n\tfor _, qsv := range filter {\n\t\tqs.Add(\"filter\", qsv)\n\t}\n\n\t_result.RawQuery = qs.Encode()\n\n\treturn &_result, nil\n}\n\n// Must is a helper function to panic when the url builder returns an error\nfunc (o *GetSilencesURL) Must(u *url.URL, err error) *url.URL {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif u == nil {\n\t\tpanic(\"url can't be nil\")\n\t}\n\treturn u\n}\n\n// String returns the string representation of the path with query string\nfunc (o *GetSilencesURL) String() string {\n\treturn o.Must(o.Build()).String()\n}\n\n// BuildFull builds a full url with scheme, host, path and query string\nfunc (o *GetSilencesURL) BuildFull(scheme, host string) (*url.URL, error) {\n\tif scheme == \"\" {\n\t\treturn nil, errors.New(\"scheme is required for a full url on GetSilencesURL\")\n\t}\n\tif host == \"\" {\n\t\treturn nil, errors.New(\"host is required for a full url on GetSilencesURL\")\n\t}\n\n\tbase, err := o.Build()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbase.Scheme = scheme\n\tbase.Host = host\n\treturn base, nil\n}\n\n// StringFull returns the string representation of a complete url\nfunc (o *GetSilencesURL) StringFull(scheme, host string) string {\n\treturn o.Must(o.BuildFull(scheme, host)).String()\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/silence/post_silences.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime/middleware\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/go-openapi/swag\"\n)\n\n// PostSilencesHandlerFunc turns a function with the right signature into a post silences handler\ntype PostSilencesHandlerFunc func(PostSilencesParams) middleware.Responder\n\n// Handle executing the request and returning a response\nfunc (fn PostSilencesHandlerFunc) Handle(params PostSilencesParams) middleware.Responder {\n\treturn fn(params)\n}\n\n// PostSilencesHandler interface for that can handle valid post silences params\ntype PostSilencesHandler interface {\n\tHandle(PostSilencesParams) middleware.Responder\n}\n\n// NewPostSilences creates a new http.Handler for the post silences operation\nfunc NewPostSilences(ctx *middleware.Context, handler PostSilencesHandler) *PostSilences {\n\treturn &PostSilences{Context: ctx, Handler: handler}\n}\n\n/*\n\tPostSilences swagger:route POST /silences silence postSilences\n\nPost a new silence or update an existing one\n*/\ntype PostSilences struct {\n\tContext *middleware.Context\n\tHandler PostSilencesHandler\n}\n\nfunc (o *PostSilences) ServeHTTP(rw http.ResponseWriter, r *http.Request) {\n\troute, rCtx, _ := o.Context.RouteInfo(r)\n\tif rCtx != nil {\n\t\t*r = *rCtx\n\t}\n\tvar Params = NewPostSilencesParams()\n\tif err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params\n\t\to.Context.Respond(rw, r, route.Produces, route, err)\n\t\treturn\n\t}\n\n\tres := o.Handler.Handle(Params) // actually handle the request\n\n\to.Context.Respond(rw, r, route.Produces, route, res)\n\n}\n\n// PostSilencesOKBody post silences o k body\n//\n// swagger:model PostSilencesOKBody\ntype PostSilencesOKBody struct {\n\n\t// silence ID\n\tSilenceID string `json:\"silenceID,omitempty\"`\n}\n\n// Validate validates this post silences o k body\nfunc (o *PostSilencesOKBody) Validate(formats strfmt.Registry) error {\n\treturn nil\n}\n\n// ContextValidate validates this post silences o k body based on context it is used\nfunc (o *PostSilencesOKBody) ContextValidate(ctx context.Context, formats strfmt.Registry) error {\n\treturn nil\n}\n\n// MarshalBinary interface implementation\nfunc (o *PostSilencesOKBody) MarshalBinary() ([]byte, error) {\n\tif o == nil {\n\t\treturn nil, nil\n\t}\n\treturn swag.WriteJSON(o)\n}\n\n// UnmarshalBinary interface implementation\nfunc (o *PostSilencesOKBody) UnmarshalBinary(b []byte) error {\n\tvar res PostSilencesOKBody\n\tif err := swag.ReadJSON(b, &res); err != nil {\n\t\treturn err\n\t}\n\t*o = res\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/silence/post_silences_parameters.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\tstderrors \"errors\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/go-openapi/errors\"\n\t\"github.com/go-openapi/runtime\"\n\t\"github.com/go-openapi/runtime/middleware\"\n\t\"github.com/go-openapi/validate\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\n// NewPostSilencesParams creates a new PostSilencesParams object\n//\n// There are no default values defined in the spec.\nfunc NewPostSilencesParams() PostSilencesParams {\n\n\treturn PostSilencesParams{}\n}\n\n// PostSilencesParams contains all the bound params for the post silences operation\n// typically these are obtained from a http.Request\n//\n// swagger:parameters postSilences\ntype PostSilencesParams struct {\n\t// HTTP Request Object\n\tHTTPRequest *http.Request `json:\"-\"`\n\n\t/*The silence to create\n\t  Required: true\n\t  In: body\n\t*/\n\tSilence *models.PostableSilence\n}\n\n// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface\n// for simple values it will use straight method calls.\n//\n// To ensure default values, the struct must have been initialized with NewPostSilencesParams() beforehand.\nfunc (o *PostSilencesParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {\n\tvar res []error\n\n\to.HTTPRequest = r\n\n\tif runtime.HasBody(r) {\n\t\tdefer func() {\n\t\t\t_ = r.Body.Close()\n\t\t}()\n\t\tvar body models.PostableSilence\n\t\tif err := route.Consumer.Consume(r.Body, &body); err != nil {\n\t\t\tif stderrors.Is(err, io.EOF) {\n\t\t\t\tres = append(res, errors.Required(\"silence\", \"body\", \"\"))\n\t\t\t} else {\n\t\t\t\tres = append(res, errors.NewParseError(\"silence\", \"body\", \"\", err))\n\t\t\t}\n\t\t} else {\n\t\t\t// validate body object\n\t\t\tif err := body.Validate(route.Formats); err != nil {\n\t\t\t\tres = append(res, err)\n\t\t\t}\n\n\t\t\tctx := validate.WithOperationRequest(r.Context())\n\t\t\tif err := body.ContextValidate(ctx, route.Formats); err != nil {\n\t\t\t\tres = append(res, err)\n\t\t\t}\n\n\t\t\tif len(res) == 0 {\n\t\t\t\to.Silence = &body\n\t\t\t}\n\t\t}\n\t} else {\n\t\tres = append(res, errors.Required(\"silence\", \"body\", \"\"))\n\t}\n\tif len(res) > 0 {\n\t\treturn errors.CompositeValidationError(res...)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/silence/post_silences_responses.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the swagger generate command\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-openapi/runtime\"\n)\n\n// PostSilencesOKCode is the HTTP code returned for type PostSilencesOK\nconst PostSilencesOKCode int = 200\n\n/*\nPostSilencesOK Create / update silence response\n\nswagger:response postSilencesOK\n*/\ntype PostSilencesOK struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload *PostSilencesOKBody `json:\"body,omitempty\"`\n}\n\n// NewPostSilencesOK creates PostSilencesOK with default headers values\nfunc NewPostSilencesOK() *PostSilencesOK {\n\n\treturn &PostSilencesOK{}\n}\n\n// WithPayload adds the payload to the post silences o k response\nfunc (o *PostSilencesOK) WithPayload(payload *PostSilencesOKBody) *PostSilencesOK {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the post silences o k response\nfunc (o *PostSilencesOK) SetPayload(payload *PostSilencesOKBody) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *PostSilencesOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(200)\n\tif o.Payload != nil {\n\t\tpayload := o.Payload\n\t\tif err := producer.Produce(rw, payload); err != nil {\n\t\t\tpanic(err) // let the recovery middleware deal with this\n\t\t}\n\t}\n}\n\n// PostSilencesBadRequestCode is the HTTP code returned for type PostSilencesBadRequest\nconst PostSilencesBadRequestCode int = 400\n\n/*\nPostSilencesBadRequest Bad request\n\nswagger:response postSilencesBadRequest\n*/\ntype PostSilencesBadRequest struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload string `json:\"body,omitempty\"`\n}\n\n// NewPostSilencesBadRequest creates PostSilencesBadRequest with default headers values\nfunc NewPostSilencesBadRequest() *PostSilencesBadRequest {\n\n\treturn &PostSilencesBadRequest{}\n}\n\n// WithPayload adds the payload to the post silences bad request response\nfunc (o *PostSilencesBadRequest) WithPayload(payload string) *PostSilencesBadRequest {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the post silences bad request response\nfunc (o *PostSilencesBadRequest) SetPayload(payload string) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *PostSilencesBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(400)\n\tpayload := o.Payload\n\tif err := producer.Produce(rw, payload); err != nil {\n\t\tpanic(err) // let the recovery middleware deal with this\n\t}\n}\n\n// PostSilencesNotFoundCode is the HTTP code returned for type PostSilencesNotFound\nconst PostSilencesNotFoundCode int = 404\n\n/*\nPostSilencesNotFound A silence with the specified ID was not found\n\nswagger:response postSilencesNotFound\n*/\ntype PostSilencesNotFound struct {\n\n\t/*\n\t  In: Body\n\t*/\n\tPayload string `json:\"body,omitempty\"`\n}\n\n// NewPostSilencesNotFound creates PostSilencesNotFound with default headers values\nfunc NewPostSilencesNotFound() *PostSilencesNotFound {\n\n\treturn &PostSilencesNotFound{}\n}\n\n// WithPayload adds the payload to the post silences not found response\nfunc (o *PostSilencesNotFound) WithPayload(payload string) *PostSilencesNotFound {\n\to.Payload = payload\n\treturn o\n}\n\n// SetPayload sets the payload to the post silences not found response\nfunc (o *PostSilencesNotFound) SetPayload(payload string) {\n\to.Payload = payload\n}\n\n// WriteResponse to the client\nfunc (o *PostSilencesNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {\n\n\trw.WriteHeader(404)\n\tpayload := o.Payload\n\tif err := producer.Produce(rw, payload); err != nil {\n\t\tpanic(err) // let the recovery middleware deal with this\n\t}\n}\n"
  },
  {
    "path": "api/v2/restapi/operations/silence/post_silences_urlbuilder.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage silence\n\n// This file was generated by the swagger tool.\n// Editing this file might prove futile when you re-run the generate command\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\tgolangswaggerpaths \"path\"\n)\n\n// PostSilencesURL generates an URL for the post silences operation\ntype PostSilencesURL struct {\n\t_basePath string\n}\n\n// WithBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *PostSilencesURL) WithBasePath(bp string) *PostSilencesURL {\n\to.SetBasePath(bp)\n\treturn o\n}\n\n// SetBasePath sets the base path for this url builder, only required when it's different from the\n// base path specified in the swagger spec.\n// When the value of the base path is an empty string\nfunc (o *PostSilencesURL) SetBasePath(bp string) {\n\to._basePath = bp\n}\n\n// Build a url path and query string\nfunc (o *PostSilencesURL) Build() (*url.URL, error) {\n\tvar _result url.URL\n\n\tvar _path = \"/silences\"\n\n\t_basePath := o._basePath\n\tif _basePath == \"\" {\n\t\t_basePath = \"/api/v2/\"\n\t}\n\t_result.Path = golangswaggerpaths.Join(_basePath, _path)\n\n\treturn &_result, nil\n}\n\n// Must is a helper function to panic when the url builder returns an error\nfunc (o *PostSilencesURL) Must(u *url.URL, err error) *url.URL {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif u == nil {\n\t\tpanic(\"url can't be nil\")\n\t}\n\treturn u\n}\n\n// String returns the string representation of the path with query string\nfunc (o *PostSilencesURL) String() string {\n\treturn o.Must(o.Build()).String()\n}\n\n// BuildFull builds a full url with scheme, host, path and query string\nfunc (o *PostSilencesURL) BuildFull(scheme, host string) (*url.URL, error) {\n\tif scheme == \"\" {\n\t\treturn nil, errors.New(\"scheme is required for a full url on PostSilencesURL\")\n\t}\n\tif host == \"\" {\n\t\treturn nil, errors.New(\"host is required for a full url on PostSilencesURL\")\n\t}\n\n\tbase, err := o.Build()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbase.Scheme = scheme\n\tbase.Host = host\n\treturn base, nil\n}\n\n// StringFull returns the string representation of a complete url\nfunc (o *PostSilencesURL) StringFull(scheme, host string) string {\n\treturn o.Must(o.BuildFull(scheme, host)).String()\n}\n"
  },
  {
    "path": "api/v2/restapi/server.go",
    "content": "// Code generated by go-swagger; DO NOT EDIT.\n\n// Copyright Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\npackage restapi\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strconv\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"syscall\"\n\t\"time\"\n\n\tflags \"github.com/jessevdk/go-flags\"\n\t\"golang.org/x/net/netutil\"\n\n\t\"github.com/go-openapi/runtime/flagext\"\n\t\"github.com/go-openapi/swag\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/restapi/operations\"\n)\n\nconst (\n\tschemeHTTP  = \"http\"\n\tschemeHTTPS = \"https\"\n\tschemeUnix  = \"unix\"\n)\n\nvar defaultSchemes []string\n\nfunc init() {\n\tdefaultSchemes = []string{\n\t\tschemeHTTP,\n\t}\n}\n\n// NewServer creates a new api alertmanager server but does not configure it\nfunc NewServer(api *operations.AlertmanagerAPI) *Server {\n\ts := new(Server)\n\n\ts.shutdown = make(chan struct{})\n\ts.api = api\n\ts.interrupt = make(chan os.Signal, 1)\n\treturn s\n}\n\n// ConfigureAPI configures the API and handlers.\nfunc (s *Server) ConfigureAPI() {\n\tif s.api != nil {\n\t\ts.handler = configureAPI(s.api)\n\t}\n}\n\n// ConfigureFlags configures the additional flags defined by the handlers. Needs to be called before the parser.Parse\nfunc (s *Server) ConfigureFlags() {\n\tif s.api != nil {\n\t\tconfigureFlags(s.api)\n\t}\n}\n\n// Server for the alertmanager API\ntype Server struct {\n\tEnabledListeners []string         `long:\"scheme\" description:\"the listeners to enable, this can be repeated and defaults to the schemes in the swagger spec\"`\n\tCleanupTimeout   time.Duration    `long:\"cleanup-timeout\" description:\"grace period for which to wait before killing idle connections\" default:\"10s\"`\n\tGracefulTimeout  time.Duration    `long:\"graceful-timeout\" description:\"grace period for which to wait before shutting down the server\" default:\"15s\"`\n\tMaxHeaderSize    flagext.ByteSize `long:\"max-header-size\" description:\"controls the maximum number of bytes the server will read parsing the request header's keys and values, including the request line. It does not limit the size of the request body.\" default:\"1MiB\"`\n\n\tSocketPath    flags.Filename `long:\"socket-path\" description:\"the unix socket to listen on\" default:\"/var/run/alertmanager.sock\"`\n\tdomainSocketL net.Listener\n\n\tHost         string        `long:\"host\" description:\"the IP to listen on\" default:\"localhost\" env:\"HOST\"`\n\tPort         int           `long:\"port\" description:\"the port to listen on for insecure connections, defaults to a random value\" env:\"PORT\"`\n\tListenLimit  int           `long:\"listen-limit\" description:\"limit the number of outstanding requests\"`\n\tKeepAlive    time.Duration `long:\"keep-alive\" description:\"sets the TCP keep-alive timeouts on accepted connections. It prunes dead TCP connections ( e.g. closing laptop mid-download)\" default:\"3m\"`\n\tReadTimeout  time.Duration `long:\"read-timeout\" description:\"maximum duration before timing out read of the request\" default:\"30s\"`\n\tWriteTimeout time.Duration `long:\"write-timeout\" description:\"maximum duration before timing out write of the response\" default:\"30s\"`\n\thttpServerL  net.Listener\n\n\tTLSHost           string         `long:\"tls-host\" description:\"the IP to listen on for tls, when not specified it's the same as --host\" env:\"TLS_HOST\"`\n\tTLSPort           int            `long:\"tls-port\" description:\"the port to listen on for secure connections, defaults to a random value\" env:\"TLS_PORT\"`\n\tTLSCertificate    flags.Filename `long:\"tls-certificate\" description:\"the certificate to use for secure connections\" env:\"TLS_CERTIFICATE\"`\n\tTLSCertificateKey flags.Filename `long:\"tls-key\" description:\"the private key to use for secure connections\" env:\"TLS_PRIVATE_KEY\"`\n\tTLSCACertificate  flags.Filename `long:\"tls-ca\" description:\"the certificate authority file to be used with mutual tls auth\" env:\"TLS_CA_CERTIFICATE\"`\n\tTLSListenLimit    int            `long:\"tls-listen-limit\" description:\"limit the number of outstanding requests\"`\n\tTLSKeepAlive      time.Duration  `long:\"tls-keep-alive\" description:\"sets the TCP keep-alive timeouts on accepted connections. It prunes dead TCP connections ( e.g. closing laptop mid-download)\"`\n\tTLSReadTimeout    time.Duration  `long:\"tls-read-timeout\" description:\"maximum duration before timing out read of the request\"`\n\tTLSWriteTimeout   time.Duration  `long:\"tls-write-timeout\" description:\"maximum duration before timing out write of the response\"`\n\thttpsServerL      net.Listener\n\n\tapi          *operations.AlertmanagerAPI\n\thandler      http.Handler\n\thasListeners bool\n\tshutdown     chan struct{}\n\tshuttingDown int32\n\tinterrupted  bool\n\tinterrupt    chan os.Signal\n}\n\n// Logf logs message either via defined user logger or via system one if no user logger is defined.\nfunc (s *Server) Logf(f string, args ...any) {\n\tif s.api != nil && s.api.Logger != nil {\n\t\ts.api.Logger(f, args...)\n\t} else {\n\t\tlog.Printf(f, args...)\n\t}\n}\n\n// Fatalf logs message either via defined user logger or via system one if no user logger is defined.\n// Exits with non-zero status after printing\nfunc (s *Server) Fatalf(f string, args ...any) {\n\tif s.api != nil && s.api.Logger != nil {\n\t\ts.api.Logger(f, args...)\n\t\tos.Exit(1)\n\t} else {\n\t\tlog.Fatalf(f, args...)\n\t}\n}\n\n// SetAPI configures the server with the specified API. Needs to be called before Serve\nfunc (s *Server) SetAPI(api *operations.AlertmanagerAPI) {\n\tif api == nil {\n\t\ts.api = nil\n\t\ts.handler = nil\n\t\treturn\n\t}\n\n\ts.api = api\n\ts.handler = configureAPI(api)\n}\n\nfunc (s *Server) hasScheme(scheme string) bool {\n\tschemes := s.EnabledListeners\n\tif len(schemes) == 0 {\n\t\tschemes = defaultSchemes\n\t}\n\n\tfor _, v := range schemes {\n\t\tif v == scheme {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Serve the api\nfunc (s *Server) Serve() (err error) {\n\tif !s.hasListeners {\n\t\tif err = s.Listen(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// set default handler, if none is set\n\tif s.handler == nil {\n\t\tif s.api == nil {\n\t\t\treturn errors.New(\"can't create the default handler, as no api is set\")\n\t\t}\n\n\t\ts.SetHandler(s.api.Serve(nil))\n\t}\n\n\twg := new(sync.WaitGroup)\n\tonce := new(sync.Once)\n\tsignalNotify(s.interrupt)\n\tgo handleInterrupt(once, s)\n\n\tservers := []*http.Server{}\n\n\tif s.hasScheme(schemeUnix) {\n\t\tdomainSocket := new(http.Server)\n\t\tdomainSocket.MaxHeaderBytes = int(s.MaxHeaderSize)\n\t\tdomainSocket.Handler = s.handler\n\t\tif int64(s.CleanupTimeout) > 0 {\n\t\t\tdomainSocket.IdleTimeout = s.CleanupTimeout\n\t\t}\n\n\t\tconfigureServer(domainSocket, \"unix\", string(s.SocketPath))\n\n\t\tservers = append(servers, domainSocket)\n\t\twg.Add(1)\n\t\ts.Logf(\"Serving alertmanager at unix://%s\", s.SocketPath)\n\t\tgo func(l net.Listener) {\n\t\t\tdefer wg.Done()\n\t\t\tif errServe := domainSocket.Serve(l); errServe != nil && !errors.Is(errServe, http.ErrServerClosed) {\n\t\t\t\ts.Fatalf(\"%v\", errServe)\n\t\t\t}\n\t\t\ts.Logf(\"Stopped serving alertmanager at unix://%s\", s.SocketPath)\n\t\t}(s.domainSocketL)\n\t}\n\n\tif s.hasScheme(schemeHTTP) {\n\t\thttpServer := new(http.Server)\n\t\thttpServer.MaxHeaderBytes = int(s.MaxHeaderSize)\n\t\thttpServer.ReadTimeout = s.ReadTimeout\n\t\thttpServer.WriteTimeout = s.WriteTimeout\n\t\thttpServer.SetKeepAlivesEnabled(int64(s.KeepAlive) > 0)\n\t\tif s.ListenLimit > 0 {\n\t\t\ts.httpServerL = netutil.LimitListener(s.httpServerL, s.ListenLimit)\n\t\t}\n\n\t\tif int64(s.CleanupTimeout) > 0 {\n\t\t\thttpServer.IdleTimeout = s.CleanupTimeout\n\t\t}\n\n\t\thttpServer.Handler = s.handler\n\n\t\tconfigureServer(httpServer, \"http\", s.httpServerL.Addr().String())\n\n\t\tservers = append(servers, httpServer)\n\t\twg.Add(1)\n\t\ts.Logf(\"Serving alertmanager at http://%s\", s.httpServerL.Addr())\n\t\tgo func(l net.Listener) {\n\t\t\tdefer wg.Done()\n\t\t\tif errServe := httpServer.Serve(l); errServe != nil && !errors.Is(errServe, http.ErrServerClosed) {\n\t\t\t\ts.Fatalf(\"%v\", errServe)\n\t\t\t}\n\t\t\ts.Logf(\"Stopped serving alertmanager at http://%s\", l.Addr())\n\t\t}(s.httpServerL)\n\t}\n\n\tif s.hasScheme(schemeHTTPS) {\n\t\thttpsServer := new(http.Server)\n\t\thttpsServer.MaxHeaderBytes = int(s.MaxHeaderSize)\n\t\thttpsServer.ReadTimeout = s.TLSReadTimeout\n\t\thttpsServer.WriteTimeout = s.TLSWriteTimeout\n\t\thttpsServer.SetKeepAlivesEnabled(int64(s.TLSKeepAlive) > 0)\n\t\tif s.TLSListenLimit > 0 {\n\t\t\ts.httpsServerL = netutil.LimitListener(s.httpsServerL, s.TLSListenLimit)\n\t\t}\n\t\tif int64(s.CleanupTimeout) > 0 {\n\t\t\thttpsServer.IdleTimeout = s.CleanupTimeout\n\t\t}\n\t\thttpsServer.Handler = s.handler\n\n\t\t// Inspired by https://blog.bracebin.com/achieving-perfect-ssl-labs-score-with-go\n\t\thttpsServer.TLSConfig = &tls.Config{\n\t\t\t// Causes servers to use Go's default ciphersuite preferences,\n\t\t\t// which are tuned to avoid attacks. Does nothing on clients.\n\t\t\tPreferServerCipherSuites: true,\n\t\t\t// Only use curves which have assembly implementations\n\t\t\t// https://github.com/golang/go/tree/master/src/crypto/elliptic\n\t\t\tCurvePreferences: []tls.CurveID{tls.CurveP256},\n\t\t\t// Use modern tls mode https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility\n\t\t\tNextProtos: []string{\"h2\", \"http/1.1\"},\n\t\t\t// https://www.owasp.org/index.php/Transport_Layer_Protection_Cheat_Sheet#Rule_-_Only_Support_Strong_Protocols\n\t\t\tMinVersion: tls.VersionTLS12,\n\t\t\t// These ciphersuites support Forward Secrecy: https://en.wikipedia.org/wiki/Forward_secrecy\n\t\t\tCipherSuites: []uint16{\n\t\t\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,\n\t\t\t\ttls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,\n\t\t\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,\n\t\t\t\ttls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,\n\t\t\t\ttls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,\n\t\t\t\ttls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,\n\t\t\t},\n\t\t}\n\n\t\t// build standard config from server options\n\t\tif s.TLSCertificate != \"\" && s.TLSCertificateKey != \"\" {\n\t\t\thttpsServer.TLSConfig.Certificates = make([]tls.Certificate, 1)\n\t\t\thttpsServer.TLSConfig.Certificates[0], err = tls.LoadX509KeyPair(string(s.TLSCertificate), string(s.TLSCertificateKey))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif s.TLSCACertificate != \"\" {\n\t\t\t// include specified CA certificate\n\t\t\tcaCert, caCertErr := os.ReadFile(string(s.TLSCACertificate))\n\t\t\tif caCertErr != nil {\n\t\t\t\treturn caCertErr\n\t\t\t}\n\t\t\tcaCertPool := x509.NewCertPool()\n\t\t\tok := caCertPool.AppendCertsFromPEM(caCert)\n\t\t\tif !ok {\n\t\t\t\treturn errors.New(\"cannot parse CA certificate\")\n\t\t\t}\n\t\t\thttpsServer.TLSConfig.ClientCAs = caCertPool\n\t\t\thttpsServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert\n\t\t}\n\n\t\t// call custom TLS configurator\n\t\tconfigureTLS(httpsServer.TLSConfig)\n\n\t\tif len(httpsServer.TLSConfig.Certificates) == 0 && httpsServer.TLSConfig.GetCertificate == nil {\n\t\t\t// after standard and custom config are passed, this ends up with no certificate\n\t\t\tif s.TLSCertificate == \"\" {\n\t\t\t\tif s.TLSCertificateKey == \"\" {\n\t\t\t\t\ts.Fatalf(\"the required flags `--tls-certificate` and `--tls-key` were not specified\")\n\t\t\t\t}\n\t\t\t\ts.Fatalf(\"the required flag `--tls-certificate` was not specified\")\n\t\t\t}\n\t\t\tif s.TLSCertificateKey == \"\" {\n\t\t\t\ts.Fatalf(\"the required flag `--tls-key` was not specified\")\n\t\t\t}\n\t\t\t// this happens with a wrong custom TLS configurator\n\t\t\ts.Fatalf(\"no certificate was configured for TLS\")\n\t\t}\n\n\t\tconfigureServer(httpsServer, \"https\", s.httpsServerL.Addr().String())\n\n\t\tservers = append(servers, httpsServer)\n\t\twg.Add(1)\n\t\ts.Logf(\"Serving alertmanager at https://%s\", s.httpsServerL.Addr())\n\t\tgo func(l net.Listener) {\n\t\t\tdefer wg.Done()\n\t\t\tif errServe := httpsServer.Serve(l); errServe != nil && !errors.Is(errServe, http.ErrServerClosed) {\n\t\t\t\ts.Fatalf(\"%v\", errServe)\n\t\t\t}\n\t\t\ts.Logf(\"Stopped serving alertmanager at https://%s\", l.Addr())\n\t\t}(tls.NewListener(s.httpsServerL, httpsServer.TLSConfig))\n\t}\n\n\twg.Add(1)\n\tgo s.handleShutdown(wg, &servers)\n\n\twg.Wait()\n\treturn nil\n}\n\n// Listen creates the listeners for the server\nfunc (s *Server) Listen() error {\n\tif s.hasListeners { // already done this\n\t\treturn nil\n\t}\n\n\tif s.hasScheme(schemeHTTPS) {\n\t\t// Use http host if https host wasn't defined\n\t\tif s.TLSHost == \"\" {\n\t\t\ts.TLSHost = s.Host\n\t\t}\n\t\t// Use http listen limit if https listen limit wasn't defined\n\t\tif s.TLSListenLimit == 0 {\n\t\t\ts.TLSListenLimit = s.ListenLimit\n\t\t}\n\t\t// Use http tcp keep alive if https tcp keep alive wasn't defined\n\t\tif int64(s.TLSKeepAlive) == 0 {\n\t\t\ts.TLSKeepAlive = s.KeepAlive\n\t\t}\n\t\t// Use http read timeout if https read timeout wasn't defined\n\t\tif int64(s.TLSReadTimeout) == 0 {\n\t\t\ts.TLSReadTimeout = s.ReadTimeout\n\t\t}\n\t\t// Use http write timeout if https write timeout wasn't defined\n\t\tif int64(s.TLSWriteTimeout) == 0 {\n\t\t\ts.TLSWriteTimeout = s.WriteTimeout\n\t\t}\n\t}\n\n\tif s.hasScheme(schemeUnix) {\n\t\tdomSockListener, err := net.Listen(\"unix\", string(s.SocketPath))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ts.domainSocketL = domSockListener\n\t}\n\n\tif s.hasScheme(schemeHTTP) {\n\t\tlistener, err := net.Listen(\"tcp\", net.JoinHostPort(s.Host, strconv.Itoa(s.Port)))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\th, p, err := swag.SplitHostPort(listener.Addr().String())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ts.Host = h\n\t\ts.Port = p\n\t\ts.httpServerL = listener\n\t}\n\n\tif s.hasScheme(schemeHTTPS) {\n\t\ttlsListener, err := net.Listen(\"tcp\", net.JoinHostPort(s.TLSHost, strconv.Itoa(s.TLSPort)))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tsh, sp, err := swag.SplitHostPort(tlsListener.Addr().String())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ts.TLSHost = sh\n\t\ts.TLSPort = sp\n\t\ts.httpsServerL = tlsListener\n\t}\n\n\ts.hasListeners = true\n\treturn nil\n}\n\n// Shutdown server and clean up resources\nfunc (s *Server) Shutdown() error {\n\tif atomic.CompareAndSwapInt32(&s.shuttingDown, 0, 1) {\n\t\tclose(s.shutdown)\n\t}\n\treturn nil\n}\n\nfunc (s *Server) handleShutdown(wg *sync.WaitGroup, serversPtr *[]*http.Server) {\n\t// wg.Done must occur last, after s.api.ServerShutdown()\n\t// (to preserve old behaviour)\n\tdefer wg.Done()\n\n\t<-s.shutdown\n\n\tservers := *serversPtr\n\n\tctx, cancel := context.WithTimeout(context.TODO(), s.GracefulTimeout)\n\tdefer cancel()\n\n\t// first execute the pre-shutdown hook\n\ts.api.PreServerShutdown()\n\n\tshutdownChan := make(chan bool)\n\tfor i := range servers {\n\t\tserver := servers[i]\n\t\tgo func() {\n\t\t\tvar success bool\n\t\t\tdefer func() {\n\t\t\t\tshutdownChan <- success\n\t\t\t}()\n\t\t\tif err := server.Shutdown(ctx); err != nil {\n\t\t\t\t// Error from closing listeners, or context timeout:\n\t\t\t\ts.Logf(\"HTTP server Shutdown: %v\", err)\n\t\t\t} else {\n\t\t\t\tsuccess = true\n\t\t\t}\n\t\t}()\n\t}\n\n\t// Wait until all listeners have successfully shut down before calling ServerShutdown\n\tsuccess := true\n\tfor range servers {\n\t\tsuccess = success && <-shutdownChan\n\t}\n\tif success {\n\t\ts.api.ServerShutdown()\n\t}\n}\n\n// GetHandler returns a handler useful for testing\nfunc (s *Server) GetHandler() http.Handler {\n\treturn s.handler\n}\n\n// SetHandler allows for setting a http handler on this server\nfunc (s *Server) SetHandler(handler http.Handler) {\n\ts.handler = handler\n}\n\n// UnixListener returns the domain socket listener\nfunc (s *Server) UnixListener() (net.Listener, error) {\n\tif !s.hasListeners {\n\t\tif err := s.Listen(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn s.domainSocketL, nil\n}\n\n// HTTPListener returns the http listener\nfunc (s *Server) HTTPListener() (net.Listener, error) {\n\tif !s.hasListeners {\n\t\tif err := s.Listen(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn s.httpServerL, nil\n}\n\n// TLSListener returns the https listener\nfunc (s *Server) TLSListener() (net.Listener, error) {\n\tif !s.hasListeners {\n\t\tif err := s.Listen(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn s.httpsServerL, nil\n}\n\nfunc handleInterrupt(once *sync.Once, s *Server) {\n\tonce.Do(func() {\n\t\tfor range s.interrupt {\n\t\t\tif s.interrupted {\n\t\t\t\ts.Logf(\"Server already shutting down\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ts.interrupted = true\n\t\t\ts.Logf(\"Shutting down... \")\n\t\t\tif err := s.Shutdown(); err != nil {\n\t\t\t\ts.Logf(\"HTTP server Shutdown: %v\", err)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc signalNotify(interrupt chan<- os.Signal) {\n\tsignal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)\n}\n"
  },
  {
    "path": "api/v2/testing.go",
    "content": "// Copyright 2022 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage v2\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-openapi/strfmt\"\n\n\topen_api_models \"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n\t\"github.com/prometheus/alertmanager/silence/silencepb\"\n)\n\nfunc createSilence(t *testing.T, ID, creator string, start, ends time.Time) open_api_models.PostableSilence {\n\tt.Helper()\n\n\tcomment := \"test\"\n\tmatcherName := \"a\"\n\tmatcherValue := \"b\"\n\tisRegex := false\n\tstartsAt := strfmt.DateTime(start)\n\tendsAt := strfmt.DateTime(ends)\n\n\tsil := open_api_models.PostableSilence{\n\t\tID: ID,\n\t\tSilence: open_api_models.Silence{\n\t\t\tMatchers:  open_api_models.Matchers{&open_api_models.Matcher{Name: &matcherName, Value: &matcherValue, IsRegex: &isRegex}},\n\t\t\tStartsAt:  &startsAt,\n\t\t\tEndsAt:    &endsAt,\n\t\t\tCreatedBy: &creator,\n\t\t\tComment:   &comment,\n\t\t},\n\t}\n\treturn sil\n}\n\nfunc createSilenceMatcher(t *testing.T, name, pattern string, matcherType silencepb.Matcher_Type) *silencepb.Matcher {\n\tt.Helper()\n\n\treturn &silencepb.Matcher{\n\t\tName:    name,\n\t\tPattern: pattern,\n\t\tType:    matcherType,\n\t}\n}\n\nfunc createLabelMatcher(t *testing.T, name, value string, matchType labels.MatchType) *labels.Matcher {\n\tt.Helper()\n\n\tmatcher, _ := labels.NewMatcher(matchType, name, value)\n\treturn matcher\n}\n"
  },
  {
    "path": "buf.gen.yaml",
    "content": "version: v2\nplugins:\n  - local: ['go', 'tool', '-modfile=internal/tools/go.mod', 'protoc-gen-go']\n    out: .\n    opt:\n      - module=github.com/prometheus/alertmanager\n"
  },
  {
    "path": "buf.yaml",
    "content": "# For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yaml\nversion: v2\nmodules:\n  - path: nflog/nflogpb\n    name: prometheus/alertmanager/nflog\n  - path: cluster/clusterpb\n    name: prometheus/alertmanager/cluster\n  - path: silence/silencepb\n    name: prometheus/alertmanager/silence\n"
  },
  {
    "path": "cli/alert.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"github.com/alecthomas/kingpin/v2\"\n)\n\nfunc configureAlertCmd(app *kingpin.Application) {\n\talertCmd := app.Command(\"alert\", \"Add or query alerts.\").PreAction(requireAlertManagerURL)\n\tconfigureQueryAlertsCmd(alertCmd)\n\tconfigureAddAlertCmd(alertCmd)\n}\n"
  },
  {
    "path": "cli/alert_add.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kingpin/v2\"\n\t\"github.com/go-openapi/strfmt\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/client/alert\"\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/matcher/compat\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n)\n\ntype alertAddCmd struct {\n\tannotations  []string\n\tgeneratorURL string\n\tlabels       []string\n\tstart        string\n\tend          string\n}\n\nconst alertAddHelp = `Add a new alert.\n\nThis command is used to add a new alert to Alertmanager.\n\nTo add a new alert with labels:\n\n\tamtool alert add alertname=foo node=bar\n\nIf alertname is omitted and the first argument does not contain a '=' then it will\nbe assumed to be the value of the alertname pair.\n\n\tamtool alert add foo node=bar\n\nOne or more annotations can be added using the --annotation flag:\n\n\tamtool alert add foo node=bar \\\n\t\t--annotation=runbook='http://runbook.biz' \\\n\t\t--annotation=summary='summary of the alert' \\\n\t\t--annotation=description='description of the alert'\n\nAdditional flags such as --generator-url, --start, and --end are also supported.\n`\n\nfunc configureAddAlertCmd(cc *kingpin.CmdClause) {\n\tvar (\n\t\ta      = &alertAddCmd{}\n\t\taddCmd = cc.Command(\"add\", alertAddHelp)\n\t)\n\taddCmd.Arg(\"labels\", \"List of labels to be included with the alert\").StringsVar(&a.labels)\n\taddCmd.Flag(\"generator-url\", \"Set the URL of the source that generated the alert\").StringVar(&a.generatorURL)\n\taddCmd.Flag(\"start\", \"Set when the alert should start. RFC3339 format 2006-01-02T15:04:05-07:00\").StringVar(&a.start)\n\taddCmd.Flag(\"end\", \"Set when the alert should end. RFC3339 format 2006-01-02T15:04:05-07:00\").StringVar(&a.end)\n\taddCmd.Flag(\"annotation\", \"Set an annotation to be included with the alert\").StringsVar(&a.annotations)\n\taddCmd.Action(execWithTimeout(a.addAlert))\n}\n\nfunc (a *alertAddCmd) addAlert(ctx context.Context, _ *kingpin.ParseContext) error {\n\tif len(a.labels) > 0 {\n\t\t// Allow the alertname label to be defined implicitly as the first argument rather\n\t\t// than explicitly as a key=value pair.\n\t\tif _, err := compat.Matcher(a.labels[0], \"cli\"); err != nil {\n\t\t\ta.labels[0] = fmt.Sprintf(\"alertname=%s\", strconv.Quote(a.labels[0]))\n\t\t}\n\t}\n\n\tls := make(models.LabelSet, len(a.labels))\n\tfor _, l := range a.labels {\n\t\tmatcher, err := compat.Matcher(l, \"cli\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif matcher.Type != labels.MatchEqual {\n\t\t\treturn errors.New(\"labels must be specified as key=value pairs\")\n\t\t}\n\t\tls[matcher.Name] = matcher.Value\n\t}\n\n\tannotations := make(models.LabelSet, len(a.annotations))\n\tfor _, a := range a.annotations {\n\t\tmatcher, err := compat.Matcher(a, \"cli\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif matcher.Type != labels.MatchEqual {\n\t\t\treturn errors.New(\"annotations must be specified as key=value pairs\")\n\t\t}\n\t\tannotations[matcher.Name] = matcher.Value\n\t}\n\n\tvar startsAt, endsAt time.Time\n\tif a.start != \"\" {\n\t\tvar err error\n\t\tstartsAt, err = time.Parse(time.RFC3339, a.start)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif a.end != \"\" {\n\t\tvar err error\n\t\tendsAt, err = time.Parse(time.RFC3339, a.end)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tpa := &models.PostableAlert{\n\t\tAlert: models.Alert{\n\t\t\tGeneratorURL: strfmt.URI(a.generatorURL),\n\t\t\tLabels:       ls,\n\t\t},\n\t\tAnnotations: annotations,\n\t\tStartsAt:    strfmt.DateTime(startsAt),\n\t\tEndsAt:      strfmt.DateTime(endsAt),\n\t}\n\talertParams := alert.NewPostAlertsParams().WithContext(ctx).\n\t\tWithAlerts(models.PostableAlerts{pa})\n\n\tamclient := NewAlertmanagerClient(alertmanagerURL)\n\n\t_, err := amclient.Alert.PostAlerts(alertParams)\n\treturn err\n}\n"
  },
  {
    "path": "cli/alert_query.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/alecthomas/kingpin/v2\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/client/alert\"\n\t\"github.com/prometheus/alertmanager/cli/format\"\n\t\"github.com/prometheus/alertmanager/matcher/compat\"\n)\n\ntype alertQueryCmd struct {\n\tinhibited, silenced, active, unprocessed bool\n\treceiver                                 string\n\tmatcherGroups                            []string\n}\n\nconst alertQueryHelp = `View and search through current alerts.\n\nAmtool has a simplified prometheus query syntax, but contains robust support for\nbash variable expansions. The non-option section of arguments constructs a list\nof \"Matcher Groups\" that will be used to filter your query. The following\nexamples will attempt to show this behaviour in action:\n\namtool alert query alertname=foo node=bar\n\n\tThis query will match all alerts with the alertname=foo and node=bar label\n\tvalue pairs set.\n\namtool alert query foo node=bar\n\n\tIf alertname is omitted and the first argument does not contain a '=' or a\n\t'=~' then it will be assumed to be the value of the alertname pair.\n\namtool alert query 'alertname=~foo.*'\n\n\tAs well as direct equality, regex matching is also supported. The '=~' syntax\n\t(similar to prometheus) is used to represent a regex match. Regex matching\n\tcan be used in combination with a direct match.\n\nAmtool supports several flags for filtering the returned alerts by state\n(inhibited, silenced, active, unprocessed). If none of these flags is given,\nonly active alerts are returned.\n`\n\nfunc configureQueryAlertsCmd(cc *kingpin.CmdClause) {\n\tvar (\n\t\ta        = &alertQueryCmd{}\n\t\tqueryCmd = cc.Command(\"query\", alertQueryHelp).Default()\n\t)\n\tqueryCmd.Flag(\"inhibited\", \"Show inhibited alerts\").Short('i').BoolVar(&a.inhibited)\n\tqueryCmd.Flag(\"silenced\", \"Show silenced alerts\").Short('s').BoolVar(&a.silenced)\n\tqueryCmd.Flag(\"active\", \"Show active alerts\").Short('a').BoolVar(&a.active)\n\tqueryCmd.Flag(\"unprocessed\", \"Show unprocessed alerts\").Short('u').BoolVar(&a.unprocessed)\n\tqueryCmd.Flag(\"receiver\", \"Show alerts matching receiver (Supports regex syntax)\").Short('r').StringVar(&a.receiver)\n\tqueryCmd.Arg(\"matcher-groups\", \"Query filter\").StringsVar(&a.matcherGroups)\n\tqueryCmd.Action(execWithTimeout(a.queryAlerts))\n}\n\nfunc (a *alertQueryCmd) queryAlerts(ctx context.Context, _ *kingpin.ParseContext) error {\n\tif len(a.matcherGroups) > 0 {\n\t\t// Attempt to parse the first argument. If the parser fails\n\t\t// then we likely don't have a (=|=~|!=|!~) so lets assume that\n\t\t// the user wants alertname=<arg> and prepend `alertname=` to\n\t\t// the front.\n\t\tm := a.matcherGroups[0]\n\t\t_, err := compat.Matcher(m, \"cli\")\n\t\tif err != nil {\n\t\t\ta.matcherGroups[0] = fmt.Sprintf(\"alertname=%s\", strconv.Quote(m))\n\t\t}\n\t}\n\n\t// If no selector was passed, default to showing active alerts.\n\tif !a.silenced && !a.inhibited && !a.active && !a.unprocessed {\n\t\ta.active = true\n\t}\n\n\talertParams := alert.NewGetAlertsParams().WithContext(ctx).\n\t\tWithActive(&a.active).\n\t\tWithInhibited(&a.inhibited).\n\t\tWithSilenced(&a.silenced).\n\t\tWithUnprocessed(&a.unprocessed).\n\t\tWithReceiver(&a.receiver).\n\t\tWithFilter(a.matcherGroups)\n\n\tamclient := NewAlertmanagerClient(alertmanagerURL)\n\n\tgetOk, err := amclient.Alert.GetAlerts(alertParams)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tformatter, found := format.Formatters[output]\n\tif !found {\n\t\treturn errors.New(\"unknown output formatter\")\n\t}\n\treturn formatter.FormatAlerts(getOk.Payload)\n}\n"
  },
  {
    "path": "cli/check_config.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/alecthomas/kingpin/v2\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/template\"\n)\n\n// TODO: This can just be a type that is []string, doesn't have to be a struct.\ntype checkConfigCmd struct {\n\tfiles []string\n}\n\nconst checkConfigHelp = `Validate alertmanager config files\n\nWill validate the syntax and schema for alertmanager config file\nand associated templates. Non existing templates will not trigger\nerrors.\n`\n\nfunc configureCheckConfigCmd(app *kingpin.Application) {\n\tvar (\n\t\tc        = &checkConfigCmd{}\n\t\tcheckCmd = app.Command(\"check-config\", checkConfigHelp)\n\t)\n\tcheckCmd.Arg(\"check-files\", \"Files to be validated\").ExistingFilesVar(&c.files)\n\tcheckCmd.Action(c.checkConfig)\n}\n\nfunc (c *checkConfigCmd) checkConfig(ctx *kingpin.ParseContext) error {\n\treturn CheckConfig(c.files)\n}\n\nfunc CheckConfig(args []string) error {\n\tif len(args) == 0 {\n\t\tstat, err := os.Stdin.Stat()\n\t\tif err != nil {\n\t\t\tkingpin.Fatalf(\"Failed to stat standard input: %v\", err)\n\t\t}\n\t\tif (stat.Mode() & os.ModeCharDevice) != 0 {\n\t\t\tkingpin.Fatalf(\"Failed to read from standard input\")\n\t\t}\n\t\targs = []string{os.Stdin.Name()}\n\t}\n\n\tfailed := 0\n\n\tfor _, arg := range args {\n\t\tfmt.Printf(\"Checking '%s'\", arg)\n\t\tcfg, err := config.LoadFile(arg)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"  FAILED: %s\\n\", err)\n\t\t\tfailed++\n\t\t} else {\n\t\t\tfmt.Printf(\"  SUCCESS\\n\")\n\t\t}\n\n\t\tif cfg != nil {\n\t\t\tfmt.Println(\"Found:\")\n\t\t\tif cfg.Global != nil {\n\t\t\t\tfmt.Println(\" - global config\")\n\t\t\t}\n\t\t\tif cfg.Route != nil {\n\t\t\t\tfmt.Println(\" - route\")\n\t\t\t}\n\t\t\tfmt.Printf(\" - %d inhibit rules\\n\", len(cfg.InhibitRules))\n\t\t\tfmt.Printf(\" - %d receivers\\n\", len(cfg.Receivers))\n\t\t\tfmt.Printf(\" - %d templates\\n\", len(cfg.Templates))\n\t\t\tif len(cfg.Templates) > 0 {\n\t\t\t\t_, err = template.FromGlobs(cfg.Templates)\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Printf(\"  FAILED: %s\\n\", err)\n\t\t\t\t\tfailed++\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"  SUCCESS\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfmt.Printf(\"\\n\")\n\t}\n\tif failed > 0 {\n\t\treturn fmt.Errorf(\"failed to validate %d file(s)\", failed)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cli/check_config_test.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"testing\"\n)\n\nfunc TestCheckConfig(t *testing.T) {\n\terr := CheckConfig([]string{\"testdata/conf.good.yml\"})\n\tif err != nil {\n\t\tt.Fatalf(\"checking valid config file failed with: %v\", err)\n\t}\n\n\terr = CheckConfig([]string{\"testdata/conf.bad.yml\"})\n\tif err == nil {\n\t\tt.Fatalf(\"failed to detect invalid file.\")\n\t}\n}\n"
  },
  {
    "path": "cli/cluster.go",
    "content": "// Copyright 2020 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/alecthomas/kingpin/v2\"\n\n\t\"github.com/prometheus/alertmanager/cli/format\"\n)\n\nconst clusterHelp = `View cluster status and peers.`\n\n// configureClusterCmd represents the cluster command.\nfunc configureClusterCmd(app *kingpin.Application) {\n\tclusterCmd := app.Command(\"cluster\", clusterHelp)\n\tclusterCmd.Command(\"show\", clusterHelp).Default().Action(execWithTimeout(showStatus)).PreAction(requireAlertManagerURL)\n}\n\nfunc showStatus(ctx context.Context, _ *kingpin.ParseContext) error {\n\talertManagerStatus, err := getRemoteAlertmanagerConfigStatus(ctx, alertmanagerURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\tformatter, found := format.Formatters[output]\n\tif !found {\n\t\treturn errors.New(\"unknown output formatter\")\n\t}\n\treturn formatter.FormatClusterStatus(alertManagerStatus.Cluster)\n}\n"
  },
  {
    "path": "cli/config/config.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage config\n\nimport (\n\t\"os\"\n\n\t\"github.com/alecthomas/kingpin/v2\"\n\t\"gopkg.in/yaml.v2\"\n)\n\ntype getFlagger interface {\n\tGetFlag(name string) *kingpin.FlagClause\n}\n\n// Resolver represents a configuration file resolver for kingpin.\ntype Resolver struct {\n\tflags map[string]string\n}\n\n// NewResolver returns a Resolver structure.\nfunc NewResolver(files []string, legacyFlags map[string]string) (*Resolver, error) {\n\tflags := map[string]string{}\n\tfor _, f := range files {\n\t\tif _, err := os.Stat(f); err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tb, err := os.ReadFile(f)\n\t\tif err != nil {\n\t\t\tif os.IsNotExist(err) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\n\t\tvar m map[string]string\n\t\terr = yaml.Unmarshal(b, &m)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor k, v := range m {\n\t\t\tif flag, ok := legacyFlags[k]; ok {\n\t\t\t\tif _, ok := m[flag]; ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tk = flag\n\t\t\t}\n\t\t\tif _, ok := flags[k]; !ok {\n\t\t\t\tflags[k] = v\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &Resolver{flags: flags}, nil\n}\n\nfunc (c *Resolver) setDefault(v getFlagger) {\n\tfor name, value := range c.flags {\n\t\tf := v.GetFlag(name)\n\t\tif f != nil {\n\t\t\tf.Default(value)\n\t\t}\n\t}\n}\n\n// Bind sets active flags with their default values from the configuration file(s).\nfunc (c *Resolver) Bind(app *kingpin.Application, args []string) error {\n\t// Parse the command line arguments to get the selected command.\n\tpc, err := app.ParseContext(args)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.setDefault(app)\n\tif pc.SelectedCommand != nil {\n\t\tc.setDefault(pc.SelectedCommand)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cli/config/config_test.go",
    "content": "// Copyright 2015 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage config\n\nimport (\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/alecthomas/kingpin/v2\"\n)\n\nvar (\n\turl *string\n\tid  *string\n)\n\nfunc newApp() *kingpin.Application {\n\turl = new(string)\n\tid = new(string)\n\n\tapp := kingpin.New(\"app\", \"\")\n\tapp.UsageWriter(io.Discard)\n\tapp.ErrorWriter(io.Discard)\n\tapp.Terminate(nil)\n\n\tapp.Flag(\"url\", \"\").StringVar(url)\n\n\tsilence := app.Command(\"silence\", \"\")\n\tsilenceDel := silence.Command(\"del\", \"\")\n\tsilenceDel.Flag(\"id\", \"\").StringVar(id)\n\n\treturn app\n}\n\nfunc TestNewConfigResolver(t *testing.T) {\n\tfor i, tc := range []struct {\n\t\tfiles []string\n\t\terr   bool\n\t}{\n\t\t{[]string{}, false},\n\t\t{[]string{\"testdata/amtool.good1.yml\", \"testdata/amtool.good2.yml\"}, false},\n\t\t{[]string{\"testdata/amtool.good1.yml\", \"testdata/not_existing.yml\"}, false},\n\t\t{[]string{\"testdata/amtool.good1.yml\", \"testdata/amtool.bad.yml\"}, true},\n\t} {\n\t\t_, err := NewResolver(tc.files, nil)\n\t\tif tc.err != (err != nil) {\n\t\t\tif tc.err {\n\t\t\t\tt.Fatalf(\"%d: expected error but got none\", i)\n\t\t\t} else {\n\t\t\t\tt.Fatalf(\"%d: expected no error but got %v\", i, err)\n\t\t\t}\n\t\t}\n\t}\n}\n\ntype expectFn func()\n\nfunc TestConfigResolverBind(t *testing.T) {\n\texpectURL := func(expected string) expectFn {\n\t\treturn func() {\n\t\t\tif *url != expected {\n\t\t\t\tt.Fatalf(\"expected url flag %q but got %q\", expected, *url)\n\t\t\t}\n\t\t}\n\t}\n\texpectID := func(expected string) expectFn {\n\t\treturn func() {\n\t\t\tif *id != expected {\n\t\t\t\tt.Fatalf(\"expected ID flag %q but got %q\", expected, *id)\n\t\t\t}\n\t\t}\n\t}\n\n\tfor i, tc := range []struct {\n\t\tfiles       []string\n\t\tlegacyFlags map[string]string\n\t\targs        []string\n\n\t\terr    bool\n\t\texpCmd string\n\t\texpFns []expectFn\n\t}{\n\t\t{\n\t\t\t[]string{\"testdata/amtool.good1.yml\", \"testdata/amtool.good2.yml\"},\n\t\t\tnil,\n\t\t\t[]string{},\n\n\t\t\ttrue,\n\t\t\t\"\",\n\t\t\t[]expectFn{expectURL(\"url1\")}, // from amtool.good1.yml\n\t\t},\n\t\t{\n\t\t\t[]string{\"testdata/amtool.good2.yml\"},\n\t\t\tnil,\n\t\t\t[]string{},\n\n\t\t\ttrue,\n\t\t\t\"\",\n\t\t\t[]expectFn{expectURL(\"url2\")}, // from amtool.good2.yml\n\t\t},\n\t\t{\n\t\t\t[]string{\"testdata/amtool.good1.yml\", \"testdata/amtool.good2.yml\"},\n\t\t\tnil,\n\t\t\t[]string{\"--url\", \"url3\"},\n\n\t\t\ttrue,\n\t\t\t\"\",\n\t\t\t[]expectFn{expectURL(\"url3\")}, // from command line\n\t\t},\n\t\t{\n\t\t\t[]string{\"testdata/amtool.good1.yml\", \"testdata/amtool.good2.yml\"},\n\t\t\tmap[string]string{\"old-id\": \"id\"},\n\t\t\t[]string{\"silence\", \"del\"},\n\n\t\t\tfalse,\n\t\t\t\"silence del\",\n\t\t\t[]expectFn{\n\t\t\t\texpectURL(\"url1\"), // from amtool.good1.yml\n\t\t\t\texpectID(\"id1\"),   // from amtool.good1.yml\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[]string{\"testdata/amtool.good2.yml\"},\n\t\t\tmap[string]string{\"old-id\": \"id\"},\n\t\t\t[]string{\"silence\", \"del\"},\n\n\t\t\tfalse,\n\t\t\t\"silence del\",\n\t\t\t[]expectFn{\n\t\t\t\texpectURL(\"url2\"), // from amtool.good2.yml\n\t\t\t\texpectID(\"id2\"),   // from amtool.good2.yml\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t[]string{\"testdata/amtool.good2.yml\"},\n\t\t\tmap[string]string{\"old-id\": \"id\"},\n\t\t\t[]string{\"silence\", \"del\", \"--id\", \"id3\"},\n\n\t\t\tfalse,\n\t\t\t\"silence del\",\n\t\t\t[]expectFn{\n\t\t\t\texpectURL(\"url2\"), // from amtool.good2.yml\n\t\t\t\texpectID(\"id3\"),   // from command line\n\t\t\t},\n\t\t},\n\t} {\n\t\tr, err := NewResolver(tc.files, tc.legacyFlags)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"%d: expected no error but got: %v\", i, err)\n\t\t}\n\n\t\tapp := newApp()\n\t\terr = r.Bind(app, tc.args)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"%d: expected Bind() to return no error but got: %v\", i, err)\n\t\t}\n\n\t\tcmd, err := app.Parse(tc.args)\n\t\tif tc.err != (err != nil) {\n\t\t\tif tc.err {\n\t\t\t\tt.Fatalf(\"%d: expected Parse() to return an error but got none\", i)\n\t\t\t} else {\n\t\t\t\tt.Fatalf(\"%d: expected Parse() to return no error but got: %v\", i, err)\n\t\t\t}\n\t\t}\n\t\tif cmd != tc.expCmd {\n\t\t\tt.Fatalf(\"%d: expected command %q but got %q\", i, tc.expCmd, cmd)\n\t\t}\n\t\tfor _, fn := range tc.expFns {\n\t\t\tfn()\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "cli/config/http_config.go",
    "content": "// Copyright 2021 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\tpromconfig \"github.com/prometheus/common/config\"\n\t\"gopkg.in/yaml.v2\"\n)\n\n// LoadHTTPConfigFile returns HTTPClientConfig for the given http_config file.\nfunc LoadHTTPConfigFile(filename string) (*promconfig.HTTPClientConfig, error) {\n\tb, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thttpConfig := &promconfig.HTTPClientConfig{}\n\terr = yaml.UnmarshalStrict(b, httpConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\thttpConfig.SetDirectory(filepath.Dir(filepath.Dir(filename)))\n\n\treturn httpConfig, nil\n}\n"
  },
  {
    "path": "cli/config/testdata/amtool.bad.yml",
    "content": "BAD\n"
  },
  {
    "path": "cli/config/testdata/amtool.good1.yml",
    "content": "id: id1\nurl: url1\n"
  },
  {
    "path": "cli/config/testdata/amtool.good2.yml",
    "content": "old-id: id2\nurl: url2\n"
  },
  {
    "path": "cli/config/testdata/http_config.bad.yml",
    "content": "authorization:\n  type: Basic\n"
  },
  {
    "path": "cli/config/testdata/http_config.basic_auth.good.yml",
    "content": "basic_auth:\n  username: user\n  password: password\n"
  },
  {
    "path": "cli/config/testdata/http_config.good.yml",
    "content": "authorization:\n  type: Bearer\n  credentials: theanswertothegreatquestionoflifetheuniverseandeverythingisfortytwo\nproxy_url: \"http://remote.host\"\n"
  },
  {
    "path": "cli/config.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\tkingpin \"github.com/alecthomas/kingpin/v2\"\n\n\t\"github.com/prometheus/alertmanager/cli/format\"\n)\n\nconst configHelp = `View current config.\n\nThe amount of output is controlled by the output selection flag:\n\t- Simple: Print just the running config\n\t- Extended: Print the running config as well as uptime and all version info\n\t- Json: Print entire config object as json\n`\n\n// configureConfigCmd represents the config command.\nfunc configureConfigCmd(app *kingpin.Application) {\n\tconfigCmd := app.Command(\"config\", configHelp)\n\tconfigCmd.Command(\"show\", configHelp).Default().Action(execWithTimeout(queryConfig)).PreAction(requireAlertManagerURL)\n\tconfigureRoutingCmd(configCmd)\n}\n\nfunc queryConfig(ctx context.Context, _ *kingpin.ParseContext) error {\n\tstatus, err := getRemoteAlertmanagerConfigStatus(ctx, alertmanagerURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tformatter, found := format.Formatters[output]\n\tif !found {\n\t\treturn errors.New(\"unknown output formatter\")\n\t}\n\n\treturn formatter.FormatConfig(status)\n}\n"
  },
  {
    "path": "cli/format/format.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage format\n\nimport (\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kingpin/v2\"\n\t\"github.com/go-openapi/strfmt\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n)\n\nconst DefaultDateFormat = \"2006-01-02 15:04:05 MST\"\n\nvar dateFormat *string\n\nfunc InitFormatFlags(app *kingpin.Application) {\n\tdateFormat = app.Flag(\"date.format\", \"Format of date output\").Default(DefaultDateFormat).String()\n}\n\n// Formatter needs to be implemented for each new output formatter.\ntype Formatter interface {\n\tSetOutput(io.Writer)\n\tFormatSilences([]models.GettableSilence) error\n\tFormatAlerts([]*models.GettableAlert) error\n\tFormatConfig(*models.AlertmanagerStatus) error\n\tFormatClusterStatus(status *models.ClusterStatus) error\n}\n\n// Formatters is a map of cli argument names to formatter interface object.\nvar Formatters = map[string]Formatter{}\n\nfunc FormatDate(input strfmt.DateTime) string {\n\treturn time.Time(input).Format(*dateFormat)\n}\n\nfunc labelsMatcher(m models.Matcher) *labels.Matcher {\n\tvar t labels.MatchType\n\t// Support for older alertmanager releases, which did not support isEqual.\n\tif m.IsEqual == nil {\n\t\tisEqual := true\n\t\tm.IsEqual = &isEqual\n\t}\n\tswitch {\n\tcase !*m.IsRegex && *m.IsEqual:\n\t\tt = labels.MatchEqual\n\tcase !*m.IsRegex && !*m.IsEqual:\n\t\tt = labels.MatchNotEqual\n\tcase *m.IsRegex && *m.IsEqual:\n\t\tt = labels.MatchRegexp\n\tcase *m.IsRegex && !*m.IsEqual:\n\t\tt = labels.MatchNotRegexp\n\t}\n\n\treturn &labels.Matcher{Type: t, Name: *m.Name, Value: *m.Value}\n}\n"
  },
  {
    "path": "cli/format/format_extended.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage format\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"text/tabwriter\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n)\n\ntype ExtendedFormatter struct {\n\twriter io.Writer\n}\n\nfunc init() {\n\tFormatters[\"extended\"] = &ExtendedFormatter{writer: os.Stdout}\n}\n\nfunc (formatter *ExtendedFormatter) SetOutput(writer io.Writer) {\n\tformatter.writer = writer\n}\n\n// FormatSilences formats the silences into a readable string.\nfunc (formatter *ExtendedFormatter) FormatSilences(silences []models.GettableSilence) error {\n\tw := tabwriter.NewWriter(formatter.writer, 0, 0, 2, ' ', 0)\n\tsort.Sort(ByEndAt(silences))\n\tfmt.Fprintln(w, \"ID\\tMatchers\\tStarts At\\tEnds At\\tUpdated At\\tCreated By\\tComment\\t\")\n\tfor _, silence := range silences {\n\t\tfmt.Fprintf(\n\t\t\tw,\n\t\t\t\"%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t\\n\",\n\t\t\t*silence.ID,\n\t\t\textendedFormatMatchers(silence.Matchers),\n\t\t\tFormatDate(*silence.StartsAt),\n\t\t\tFormatDate(*silence.EndsAt),\n\t\t\tFormatDate(*silence.UpdatedAt),\n\t\t\t*silence.CreatedBy,\n\t\t\t*silence.Comment,\n\t\t)\n\t}\n\treturn w.Flush()\n}\n\n// FormatAlerts formats the alerts into a readable string.\nfunc (formatter *ExtendedFormatter) FormatAlerts(alerts []*models.GettableAlert) error {\n\tw := tabwriter.NewWriter(formatter.writer, 0, 0, 2, ' ', 0)\n\tsort.Sort(ByStartsAt(alerts))\n\tfmt.Fprintln(w, \"Labels\\tAnnotations\\tStarts At\\tEnds At\\tGenerator URL\\tState\\t\")\n\tfor _, alert := range alerts {\n\t\tfmt.Fprintf(\n\t\t\tw,\n\t\t\t\"%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t\\n\",\n\t\t\textendedFormatLabels(alert.Labels),\n\t\t\textendedFormatAnnotations(alert.Annotations),\n\t\t\tFormatDate(*alert.StartsAt),\n\t\t\tFormatDate(*alert.EndsAt),\n\t\t\talert.GeneratorURL,\n\t\t\t*alert.Status.State,\n\t\t)\n\t}\n\treturn w.Flush()\n}\n\n// FormatConfig formats the alertmanager status information into a readable string.\nfunc (formatter *ExtendedFormatter) FormatConfig(status *models.AlertmanagerStatus) error {\n\tfmt.Fprintln(formatter.writer, status.Config.Original)\n\tfmt.Fprintln(formatter.writer, \"buildUser\", status.VersionInfo.BuildUser)\n\tfmt.Fprintln(formatter.writer, \"goVersion\", status.VersionInfo.GoVersion)\n\tfmt.Fprintln(formatter.writer, \"revision\", status.VersionInfo.Revision)\n\tfmt.Fprintln(formatter.writer, \"version\", status.VersionInfo.Version)\n\tfmt.Fprintln(formatter.writer, \"branch\", status.VersionInfo.Branch)\n\tfmt.Fprintln(formatter.writer, \"buildDate\", status.VersionInfo.BuildDate)\n\tfmt.Fprintln(formatter.writer, \"uptime\", status.Uptime)\n\treturn nil\n}\n\n// FormatClusterStatus formats the cluster status with peers into a readable string.\nfunc (formatter *ExtendedFormatter) FormatClusterStatus(status *models.ClusterStatus) error {\n\tw := tabwriter.NewWriter(formatter.writer, 0, 0, 2, ' ', 0)\n\tfmt.Fprintf(w,\n\t\t\"Cluster Status:\\t%s\\nNode Name:\\t%s\\n\\n\",\n\t\t*status.Status,\n\t\tstatus.Name,\n\t)\n\tfmt.Fprintln(w, \"Address\\tName\")\n\tsort.Sort(ByAddress(status.Peers))\n\tfor _, peer := range status.Peers {\n\t\tfmt.Fprintf(\n\t\t\tw,\n\t\t\t\"%s\\t%s\\t\\n\",\n\t\t\t*peer.Address,\n\t\t\t*peer.Name,\n\t\t)\n\t}\n\treturn w.Flush()\n}\n\nfunc extendedFormatLabels(labels models.LabelSet) string {\n\toutput := []string{}\n\tfor name, value := range labels {\n\t\toutput = append(output, fmt.Sprintf(\"%s=\\\"%s\\\"\", name, value))\n\t}\n\tsort.Strings(output)\n\treturn strings.Join(output, \" \")\n}\n\nfunc extendedFormatAnnotations(labels models.LabelSet) string {\n\toutput := []string{}\n\tfor name, value := range labels {\n\t\toutput = append(output, fmt.Sprintf(\"%s=\\\"%s\\\"\", name, value))\n\t}\n\tsort.Strings(output)\n\treturn strings.Join(output, \" \")\n}\n\nfunc extendedFormatMatchers(matchers models.Matchers) string {\n\tlms := labels.Matchers{}\n\tfor _, matcher := range matchers {\n\t\tlms = append(lms, labelsMatcher(*matcher))\n\t}\n\treturn lms.String()\n}\n"
  },
  {
    "path": "cli/format/format_json.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage format\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\ntype JSONFormatter struct {\n\twriter io.Writer\n}\n\nfunc init() {\n\tFormatters[\"json\"] = &JSONFormatter{writer: os.Stdout}\n}\n\nfunc (formatter *JSONFormatter) SetOutput(writer io.Writer) {\n\tformatter.writer = writer\n}\n\nfunc (formatter *JSONFormatter) FormatSilences(silences []models.GettableSilence) error {\n\tenc := json.NewEncoder(formatter.writer)\n\treturn enc.Encode(silences)\n}\n\nfunc (formatter *JSONFormatter) FormatAlerts(alerts []*models.GettableAlert) error {\n\tenc := json.NewEncoder(formatter.writer)\n\treturn enc.Encode(alerts)\n}\n\nfunc (formatter *JSONFormatter) FormatConfig(status *models.AlertmanagerStatus) error {\n\tenc := json.NewEncoder(formatter.writer)\n\treturn enc.Encode(status)\n}\n\nfunc (formatter *JSONFormatter) FormatClusterStatus(status *models.ClusterStatus) error {\n\tenc := json.NewEncoder(formatter.writer)\n\treturn enc.Encode(status)\n}\n"
  },
  {
    "path": "cli/format/format_simple.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage format\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"text/tabwriter\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\ntype SimpleFormatter struct {\n\twriter io.Writer\n}\n\nfunc init() {\n\tFormatters[\"simple\"] = &SimpleFormatter{writer: os.Stdout}\n}\n\nfunc (formatter *SimpleFormatter) SetOutput(writer io.Writer) {\n\tformatter.writer = writer\n}\n\nfunc (formatter *SimpleFormatter) FormatSilences(silences []models.GettableSilence) error {\n\tw := tabwriter.NewWriter(formatter.writer, 0, 0, 2, ' ', 0)\n\tsort.Sort(ByEndAt(silences))\n\tfmt.Fprintln(w, \"ID\\tMatchers\\tEnds At\\tCreated By\\tComment\\t\")\n\tfor _, silence := range silences {\n\t\tfmt.Fprintf(\n\t\t\tw,\n\t\t\t\"%s\\t%s\\t%s\\t%s\\t%s\\t\\n\",\n\t\t\t*silence.ID,\n\t\t\tsimpleFormatMatchers(silence.Matchers),\n\t\t\tFormatDate(*silence.EndsAt),\n\t\t\t*silence.CreatedBy,\n\t\t\t*silence.Comment,\n\t\t)\n\t}\n\treturn w.Flush()\n}\n\nfunc (formatter *SimpleFormatter) FormatAlerts(alerts []*models.GettableAlert) error {\n\tw := tabwriter.NewWriter(formatter.writer, 0, 0, 2, ' ', 0)\n\tsort.Sort(ByStartsAt(alerts))\n\tfmt.Fprintln(w, \"Alertname\\tStarts At\\tSummary\\tState\\t\")\n\tfor _, alert := range alerts {\n\t\tfmt.Fprintf(\n\t\t\tw,\n\t\t\t\"%s\\t%s\\t%s\\t%s\\t\\n\",\n\t\t\talert.Labels[\"alertname\"],\n\t\t\tFormatDate(*alert.StartsAt),\n\t\t\talert.Annotations[\"summary\"],\n\t\t\t*alert.Status.State,\n\t\t)\n\t}\n\treturn w.Flush()\n}\n\nfunc (formatter *SimpleFormatter) FormatConfig(status *models.AlertmanagerStatus) error {\n\tfmt.Fprintln(formatter.writer, *status.Config.Original)\n\treturn nil\n}\n\nfunc (formatter *SimpleFormatter) FormatClusterStatus(status *models.ClusterStatus) error {\n\tw := tabwriter.NewWriter(formatter.writer, 0, 0, 2, ' ', 0)\n\tfmt.Fprintf(w,\n\t\t\"Cluster Status:\\t%s\\nNode Name:\\t%s\\n\",\n\t\t*status.Status,\n\t\tstatus.Name,\n\t)\n\treturn w.Flush()\n}\n\nfunc simpleFormatMatchers(matchers models.Matchers) string {\n\toutput := []string{}\n\tfor _, matcher := range matchers {\n\t\toutput = append(output, simpleFormatMatcher(*matcher))\n\t}\n\treturn strings.Join(output, \" \")\n}\n\nfunc simpleFormatMatcher(m models.Matcher) string {\n\treturn labelsMatcher(m).String()\n}\n"
  },
  {
    "path": "cli/format/sort.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage format\n\nimport (\n\t\"bytes\"\n\t\"net\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\ntype ByEndAt []models.GettableSilence\n\nfunc (s ByEndAt) Len() int      { return len(s) }\nfunc (s ByEndAt) Swap(i, j int) { s[i], s[j] = s[j], s[i] }\nfunc (s ByEndAt) Less(i, j int) bool {\n\treturn time.Time(*s[i].Silence.EndsAt).Before(time.Time(*s[j].EndsAt))\n}\n\ntype ByStartsAt []*models.GettableAlert\n\nfunc (s ByStartsAt) Len() int      { return len(s) }\nfunc (s ByStartsAt) Swap(i, j int) { s[i], s[j] = s[j], s[i] }\nfunc (s ByStartsAt) Less(i, j int) bool {\n\treturn time.Time(*s[i].StartsAt).Before(time.Time(*s[j].StartsAt))\n}\n\ntype ByAddress []*models.PeerStatus\n\nfunc (s ByAddress) Len() int      { return len(s) }\nfunc (s ByAddress) Swap(i, j int) { s[i], s[j] = s[j], s[i] }\nfunc (s ByAddress) Less(i, j int) bool {\n\tip1, port1, _ := net.SplitHostPort(*s[i].Address)\n\tip2, port2, _ := net.SplitHostPort(*s[j].Address)\n\tif ip1 == ip2 {\n\t\tp1, _ := strconv.Atoi(port1)\n\t\tp2, _ := strconv.Atoi(port2)\n\t\treturn p1 < p2\n\t}\n\treturn bytes.Compare(net.ParseIP(ip1), net.ParseIP(ip2)) < 0\n}\n"
  },
  {
    "path": "cli/root.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kingpin/v2\"\n\tclientruntime \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n\tpromconfig \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/prometheus/common/version\"\n\t\"golang.org/x/mod/semver\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/client\"\n\t\"github.com/prometheus/alertmanager/cli/config\"\n\t\"github.com/prometheus/alertmanager/cli/format\"\n\t\"github.com/prometheus/alertmanager/featurecontrol\"\n\t\"github.com/prometheus/alertmanager/matcher/compat\"\n)\n\nvar (\n\tverbose         bool\n\talertmanagerURL *url.URL\n\toutput          string\n\ttimeout         time.Duration\n\thttpConfigFile  string\n\tversionCheck    bool\n\tfeatureFlags    string\n\n\tconfigFiles = []string{os.ExpandEnv(\"$HOME/.config/amtool/config.yml\"), \"/etc/amtool/config.yml\"}\n\tlegacyFlags = map[string]string{\"comment_required\": \"require-comment\"}\n)\n\nfunc initMatchersCompat(_ *kingpin.ParseContext) error {\n\tpromslogConfig := &promslog.Config{Writer: os.Stdout}\n\tif verbose {\n\t\tpromslogConfig.Level = promslog.NewLevel()\n\t\t_ = promslogConfig.Level.Set(\"debug\")\n\t}\n\tlogger := promslog.New(promslogConfig)\n\tfeatureConfig, err := featurecontrol.NewFlags(logger, featureFlags)\n\tif err != nil {\n\t\tkingpin.Fatalf(\"error parsing the feature flag list: %v\\n\", err)\n\t}\n\tcompat.InitFromFlags(logger, featureConfig)\n\treturn nil\n}\n\nfunc requireAlertManagerURL(pc *kingpin.ParseContext) error {\n\t// Return without error if any help flag is set.\n\tfor _, elem := range pc.Elements {\n\t\tf, ok := elem.Clause.(*kingpin.FlagClause)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tname := f.Model().Name\n\t\tif name == \"help\" || name == \"help-long\" || name == \"help-man\" {\n\t\t\treturn nil\n\t\t}\n\t}\n\tif alertmanagerURL == nil {\n\t\tkingpin.Fatalf(\"required flag --alertmanager.url not provided\")\n\t}\n\treturn nil\n}\n\nconst (\n\tdefaultAmHost      = \"localhost\"\n\tdefaultAmPort      = \"9093\"\n\tdefaultAmApiv2path = \"/api/v2\"\n)\n\n// NewAlertmanagerClient initializes an alertmanager client with the given URL.\nfunc NewAlertmanagerClient(amURL *url.URL) *client.AlertmanagerAPI {\n\taddress := defaultAmHost + \":\" + defaultAmPort\n\tschemes := []string{\"http\"}\n\n\tif amURL.Host != \"\" {\n\t\taddress = amURL.Host // URL documents host as host or host:port\n\t}\n\tif amURL.Scheme != \"\" {\n\t\tschemes = []string{amURL.Scheme}\n\t}\n\n\tcr := clientruntime.New(address, path.Join(amURL.Path, defaultAmApiv2path), schemes)\n\n\tif amURL.User != nil && httpConfigFile != \"\" {\n\t\tkingpin.Fatalf(\"basic authentication and http.config.file are mutually exclusive\")\n\t}\n\n\tif amURL.User != nil {\n\t\tpassword, _ := amURL.User.Password()\n\t\tcr.DefaultAuthentication = clientruntime.BasicAuth(amURL.User.Username(), password)\n\t}\n\n\tif httpConfigFile != \"\" {\n\t\tvar err error\n\t\thttpConfig, _, err := promconfig.LoadHTTPConfigFile(httpConfigFile)\n\t\tif err != nil {\n\t\t\tkingpin.Fatalf(\"failed to load HTTP config file: %v\", err)\n\t\t}\n\n\t\thttpclient, err := promconfig.NewClientFromConfig(*httpConfig, \"amtool\")\n\t\tif err != nil {\n\t\t\tkingpin.Fatalf(\"failed to create a new HTTP client: %v\", err)\n\t\t}\n\t\tcr = clientruntime.NewWithClient(address, path.Join(amURL.Path, defaultAmApiv2path), schemes, httpclient)\n\t}\n\n\tc := client.New(cr, strfmt.Default)\n\n\tif !versionCheck {\n\t\treturn c\n\t}\n\n\tstatus, err := c.General.GetStatus(nil)\n\tif err != nil || status.Payload.VersionInfo == nil || version.Version == \"\" {\n\t\t// We can not get version info, or we do not know our own version. Let amtool continue.\n\t\treturn c\n\t}\n\n\tif semver.MajorMinor(\"v\"+*status.Payload.VersionInfo.Version) != semver.MajorMinor(\"v\"+version.Version) {\n\t\tfmt.Fprintf(os.Stderr, \"Warning: amtool version (%s) and alertmanager version (%s) are different.\\n\", version.Version, *status.Payload.VersionInfo.Version)\n\t}\n\n\treturn c\n}\n\n// Execute is the main function for the amtool command.\nfunc Execute() {\n\tapp := kingpin.New(\"amtool\", helpRoot).UsageWriter(os.Stdout)\n\n\tformat.InitFormatFlags(app)\n\n\tapp.Flag(\"verbose\", \"Verbose running information\").Short('v').BoolVar(&verbose)\n\tapp.Flag(\"alertmanager.url\", \"Alertmanager to talk to\").URLVar(&alertmanagerURL)\n\tapp.Flag(\"output\", \"Output formatter (simple, extended, json)\").Short('o').Default(\"simple\").EnumVar(&output, \"simple\", \"extended\", \"json\")\n\tapp.Flag(\"timeout\", \"Timeout for the executed command\").Default(\"30s\").DurationVar(&timeout)\n\tapp.Flag(\"http.config.file\", \"HTTP client configuration file for amtool to connect to Alertmanager.\").PlaceHolder(\"<filename>\").ExistingFileVar(&httpConfigFile)\n\tapp.Flag(\"version-check\", \"Check alertmanager version. Use --no-version-check to disable.\").Default(\"true\").BoolVar(&versionCheck)\n\tapp.Flag(\"enable-feature\", fmt.Sprintf(\"Experimental features to enable, comma separated. Valid options: %s\", strings.Join(featurecontrol.AllowedFlags, \", \"))).Default(\"\").StringVar(&featureFlags)\n\n\tapp.Version(version.Print(\"amtool\"))\n\tapp.GetFlag(\"help\").Short('h')\n\tapp.UsageTemplate(kingpin.CompactUsageTemplate)\n\n\tresolver, err := config.NewResolver(configFiles, legacyFlags)\n\tif err != nil {\n\t\tkingpin.Fatalf(\"could not load config file: %v\\n\", err)\n\t}\n\n\tconfigureAlertCmd(app)\n\tconfigureSilenceCmd(app)\n\tconfigureCheckConfigCmd(app)\n\tconfigureClusterCmd(app)\n\tconfigureConfigCmd(app)\n\tconfigureTemplateCmd(app)\n\n\tapp.Action(initMatchersCompat)\n\n\terr = resolver.Bind(app, os.Args[1:])\n\tif err != nil {\n\t\tkingpin.Fatalf(\"%v\\n\", err)\n\t}\n\n\t_, err = app.Parse(os.Args[1:])\n\tif err != nil {\n\t\tkingpin.Fatalf(\"%v\\n\", err)\n\t}\n}\n\nconst (\n\thelpRoot = `View and modify the current Alertmanager state.\n\nConfig File:\nThe alertmanager tool will read a config file in YAML format from one of two\ndefault config locations: $HOME/.config/amtool/config.yml or\n/etc/amtool/config.yml\n\nAll flags can be given in the config file, but the following are the suited for\nstatic configuration:\n\n\talertmanager.url\n\t\tSet a default alertmanager url for each request\n\n\tauthor\n\t\tSet a default author value for new silences. If this argument is not\n\t\tspecified then the username will be used\n\n\trequire-comment\n\t\tBool, whether to require a comment on silence creation. Defaults to true\n\n\toutput\n\t\tSet a default output type. Options are (simple, extended, json)\n\n\tdate.format\n\t\tSets the output format for dates. Defaults to \"2006-01-02 15:04:05 MST\"\n\n\thttp.config.file\n\t\tHTTP client configuration file for amtool to connect to Alertmanager.\n\t\tThe format is https://prometheus.io/docs/alerting/latest/configuration/#http_config.\n`\n)\n"
  },
  {
    "path": "cli/routing.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/alecthomas/kingpin/v2\"\n\t\"github.com/xlab/treeprint\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/dispatch\"\n)\n\ntype routingShow struct {\n\tconfigFile        string\n\tlabels            []string\n\texpectedReceivers string\n\tdebugTree         bool\n}\n\nconst (\n\troutingHelp = `Prints alert routing tree\n\nWill print whole routing tree in form of ASCII tree view.\n\nRouting is loaded from a local configuration file or a running Alertmanager configuration.\nSpecifying --config.file takes precedence over --alertmanager.url.\n\nExample:\n\n./amtool config routes [show] --config.file=doc/examples/simple.yml\n\n`\n\tbranchSlugSeparator = \"  \"\n)\n\nfunc configureRoutingCmd(app *kingpin.CmdClause) {\n\tvar (\n\t\tc              = &routingShow{}\n\t\troutingCmd     = app.Command(\"routes\", routingHelp)\n\t\troutingShowCmd = routingCmd.Command(\"show\", routingHelp).Default()\n\t\tconfigFlag     = routingCmd.Flag(\"config.file\", \"Config file to be tested.\")\n\t)\n\tconfigFlag.ExistingFileVar(&c.configFile)\n\troutingShowCmd.Action(execWithTimeout(c.routingShowAction))\n\tconfigureRoutingTestCmd(routingCmd, c)\n}\n\nfunc (c *routingShow) routingShowAction(ctx context.Context, _ *kingpin.ParseContext) error {\n\t// Load configuration from file or URL.\n\tcfg, err := loadAlertmanagerConfig(ctx, alertmanagerURL, c.configFile)\n\tif err != nil {\n\t\tkingpin.Fatalf(\"%s\", err)\n\t\treturn err\n\t}\n\troute := dispatch.NewRoute(cfg.Route, nil)\n\ttree := treeprint.New()\n\tconvertRouteToTree(route, tree)\n\tfmt.Println(\"Routing tree:\")\n\tfmt.Println(tree.String())\n\treturn nil\n}\n\nfunc getRouteTreeSlug(route *dispatch.Route, showContinue, showReceiver bool) string {\n\tvar branchSlug bytes.Buffer\n\tif route.Matchers.Len() == 0 {\n\t\tbranchSlug.WriteString(\"default-route\")\n\t} else {\n\t\tbranchSlug.WriteString(route.Matchers.String())\n\t}\n\tif route.Continue && showContinue {\n\t\tbranchSlug.WriteString(branchSlugSeparator)\n\t\tbranchSlug.WriteString(\"continue: true\")\n\t}\n\tif showReceiver {\n\t\tbranchSlug.WriteString(branchSlugSeparator)\n\t\tbranchSlug.WriteString(\"receiver: \")\n\t\tbranchSlug.WriteString(route.RouteOpts.Receiver)\n\t}\n\treturn branchSlug.String()\n}\n\nfunc convertRouteToTree(route *dispatch.Route, tree treeprint.Tree) {\n\tbranch := tree.AddBranch(getRouteTreeSlug(route, true, true))\n\tfor _, r := range route.Routes {\n\t\tconvertRouteToTree(r, branch)\n\t}\n}\n\nfunc getMatchingTree(route *dispatch.Route, tree treeprint.Tree, lset models.LabelSet) {\n\tfinal := true\n\tbranch := tree.AddBranch(getRouteTreeSlug(route, false, false))\n\tfor _, r := range route.Routes {\n\t\tif r.Matchers.Matches(convertClientToCommonLabelSet(lset)) {\n\t\t\tgetMatchingTree(r, branch, lset)\n\t\t\tfinal = false\n\t\t\tif !r.Continue {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\tif final {\n\t\tbranch.SetValue(getRouteTreeSlug(route, false, true))\n\t}\n}\n"
  },
  {
    "path": "cli/silence.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"github.com/alecthomas/kingpin/v2\"\n)\n\n// configureSilenceCmd represents the silence command.\nfunc configureSilenceCmd(app *kingpin.Application) {\n\tsilenceCmd := app.Command(\"silence\", \"Add, expire or view silences. For more information and additional flags see query help\").PreAction(requireAlertManagerURL)\n\tconfigureSilenceAddCmd(silenceCmd)\n\tconfigureSilenceExpireCmd(silenceCmd)\n\tconfigureSilenceImportCmd(silenceCmd)\n\tconfigureSilenceQueryCmd(silenceCmd)\n\tconfigureSilenceUpdateCmd(silenceCmd)\n}\n"
  },
  {
    "path": "cli/silence_add.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os/user\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kingpin/v2\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/prometheus/common/model\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/client/silence\"\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/matcher/compat\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n)\n\nfunc username() string {\n\tuser, err := user.Current()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn user.Username\n}\n\ntype silenceAddCmd struct {\n\tauthor         string\n\trequireComment bool\n\tduration       string\n\tstart          string\n\tend            string\n\tcomment        string\n\tmatchers       []string\n\tannotations    []string\n}\n\nconst silenceAddHelp = `Add a new alertmanager silence\n\n  Amtool uses a simplified Prometheus syntax to represent silences. The\n  non-option section of arguments constructs a list of \"Matcher Groups\"\n  that will be used to create a number of silences. The following examples\n  will attempt to show this behaviour in action:\n\n  amtool silence add alertname=foo node=bar\n\n\tThis statement will add a silence that matches alerts with the\n\talertname=foo and node=bar label value pairs set.\n\n  amtool silence add foo node=bar\n\n\tIf alertname is omitted and the first argument does not contain a '=' or a\n\t'=~' then it will be assumed to be the value of the alertname pair.\n\n  amtool silence add 'alertname=~foo.*'\n\n\tAs well as direct equality, regex matching is also supported. The '=~' syntax\n\t(similar to Prometheus) is used to represent a regex match. Regex matching\n\tcan be used in combination with a direct match.\n`\n\nfunc configureSilenceAddCmd(cc *kingpin.CmdClause) {\n\tvar (\n\t\tc      = &silenceAddCmd{}\n\t\taddCmd = cc.Command(\"add\", silenceAddHelp)\n\t)\n\taddCmd.Flag(\"author\", \"Username for CreatedBy field\").Short('a').Default(username()).StringVar(&c.author)\n\taddCmd.Flag(\"require-comment\", \"Require comment to be set\").Hidden().Default(\"true\").BoolVar(&c.requireComment)\n\taddCmd.Flag(\"duration\", \"Duration of silence\").Short('d').Default(\"1h\").StringVar(&c.duration)\n\taddCmd.Flag(\"start\", \"Set when the silence should start. RFC3339 format 2006-01-02T15:04:05-07:00\").StringVar(&c.start)\n\taddCmd.Flag(\"end\", \"Set when the silence should end (overwrites duration). RFC3339 format 2006-01-02T15:04:05-07:00\").StringVar(&c.end)\n\taddCmd.Flag(\"comment\", \"A comment to help describe the silence\").Short('c').StringVar(&c.comment)\n\taddCmd.Arg(\"matcher-groups\", \"Query filter\").StringsVar(&c.matchers)\n\taddCmd.Flag(\"annotation\", \"Set an annotation to be included with the silence\").StringsVar(&c.annotations)\n\taddCmd.Action(execWithTimeout(c.add))\n}\n\nfunc (c *silenceAddCmd) add(ctx context.Context, _ *kingpin.ParseContext) error {\n\tvar err error\n\n\tif len(c.matchers) > 0 {\n\t\t// If the parser fails then we likely don't have a (=|=~|!=|!~) so lets\n\t\t// assume that the user wants alertname=<arg> and prepend `alertname=`\n\t\t// to the front.\n\t\t_, err := compat.Matcher(c.matchers[0], \"cli\")\n\t\tif err != nil {\n\t\t\tc.matchers[0] = fmt.Sprintf(\"alertname=%s\", strconv.Quote(c.matchers[0]))\n\t\t}\n\t}\n\n\tmatchers := make([]labels.Matcher, 0, len(c.matchers))\n\tfor _, s := range c.matchers {\n\t\tm, err := compat.Matcher(s, \"cli\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tmatchers = append(matchers, *m)\n\t}\n\tif len(matchers) < 1 {\n\t\treturn fmt.Errorf(\"no matchers specified\")\n\t}\n\n\tvar startsAt time.Time\n\tif c.start != \"\" {\n\t\tstartsAt, err = time.Parse(time.RFC3339, c.start)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t} else {\n\t\tstartsAt = time.Now().UTC()\n\t}\n\n\tvar endsAt time.Time\n\tif c.end != \"\" {\n\t\tendsAt, err = time.Parse(time.RFC3339, c.end)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\td, err := model.ParseDuration(c.duration)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif d == 0 {\n\t\t\treturn fmt.Errorf(\"silence duration must be greater than 0\")\n\t\t}\n\t\tendsAt = startsAt.UTC().Add(time.Duration(d))\n\t}\n\n\tif startsAt.After(endsAt) {\n\t\treturn errors.New(\"silence cannot start after it ends\")\n\t}\n\n\tif c.requireComment && c.comment == \"\" {\n\t\treturn errors.New(\"comment required by config\")\n\t}\n\n\tvar annotations models.LabelSet\n\tif len(c.annotations) > 0 {\n\t\tannotations = make(models.LabelSet, len(c.annotations))\n\t\tfor _, a := range c.annotations {\n\t\t\tmatcher, err := compat.Matcher(a, \"cli\")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif matcher.Type != labels.MatchEqual {\n\t\t\t\treturn errors.New(\"annotations must be specified as key=value pairs\")\n\t\t\t}\n\t\t\tannotations[matcher.Name] = matcher.Value\n\t\t}\n\t}\n\n\tstart := strfmt.DateTime(startsAt)\n\tend := strfmt.DateTime(endsAt)\n\tps := &models.PostableSilence{\n\t\tSilence: models.Silence{\n\t\t\tMatchers:    TypeMatchers(matchers),\n\t\t\tStartsAt:    &start,\n\t\t\tEndsAt:      &end,\n\t\t\tCreatedBy:   &c.author,\n\t\t\tComment:     &c.comment,\n\t\t\tAnnotations: annotations,\n\t\t},\n\t}\n\tsilenceParams := silence.NewPostSilencesParams().WithContext(ctx).\n\t\tWithSilence(ps)\n\n\tamclient := NewAlertmanagerClient(alertmanagerURL)\n\n\tpostOk, err := amclient.Silence.PostSilences(silenceParams)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = fmt.Println(postOk.Payload.SilenceID)\n\treturn err\n}\n"
  },
  {
    "path": "cli/silence_expire.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/alecthomas/kingpin/v2\"\n\t\"github.com/go-openapi/strfmt\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/client/silence\"\n)\n\ntype silenceExpireCmd struct {\n\tids []string\n}\n\nfunc configureSilenceExpireCmd(cc *kingpin.CmdClause) {\n\tvar (\n\t\tc         = &silenceExpireCmd{}\n\t\texpireCmd = cc.Command(\"expire\", \"expire an alertmanager silence\")\n\t)\n\texpireCmd.Arg(\"silence-ids\", \"Ids of silences to expire\").StringsVar(&c.ids)\n\texpireCmd.Action(execWithTimeout(c.expire))\n}\n\nfunc (c *silenceExpireCmd) expire(ctx context.Context, _ *kingpin.ParseContext) error {\n\tif len(c.ids) < 1 {\n\t\treturn errors.New(\"no silence IDs specified\")\n\t}\n\n\tamclient := NewAlertmanagerClient(alertmanagerURL)\n\n\tfor _, id := range c.ids {\n\t\tparams := silence.NewDeleteSilenceParams().WithContext(ctx)\n\t\tparams.SilenceID = strfmt.UUID(id)\n\t\t_, err := amclient.Silence.DeleteSilence(params)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cli/silence_import.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"sync\"\n\n\tkingpin \"github.com/alecthomas/kingpin/v2\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/client/silence\"\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\ntype silenceImportCmd struct {\n\tforce   bool\n\tworkers int\n\tfile    string\n}\n\nconst silenceImportHelp = `Import alertmanager silences from JSON file or stdin\n\nThis command can be used to bulk import silences from a JSON file\ncreated by query command. For example:\n\namtool silence query -o json foo > foo.json\n\namtool silence import foo.json\n\nJSON data can also come from stdin if no param is specified.\n`\n\nfunc configureSilenceImportCmd(cc *kingpin.CmdClause) {\n\tvar (\n\t\tc         = &silenceImportCmd{}\n\t\timportCmd = cc.Command(\"import\", silenceImportHelp)\n\t)\n\n\timportCmd.Flag(\"force\", \"Force adding new silences even if it already exists\").Short('f').BoolVar(&c.force)\n\timportCmd.Flag(\"worker\", \"Number of concurrent workers to use for import\").Short('w').Default(\"8\").IntVar(&c.workers)\n\timportCmd.Arg(\"input-file\", \"JSON file with silences\").ExistingFileVar(&c.file)\n\timportCmd.Action(execWithTimeout(c.bulkImport))\n}\n\nfunc addSilenceWorker(ctx context.Context, sclient silence.ClientService, silencec <-chan *models.PostableSilence, errc chan<- error) {\n\tfor s := range silencec {\n\t\tsid := s.ID\n\t\tparams := silence.NewPostSilencesParams().WithContext(ctx).WithSilence(s)\n\t\tpostOk, err := sclient.PostSilences(params)\n\t\tvar e *silence.PostSilencesNotFound\n\t\tif errors.As(err, &e) {\n\t\t\t// silence doesn't exists yet, retry to create as a new one\n\t\t\tparams.Silence.ID = \"\"\n\t\t\tpostOk, err = sclient.PostSilences(params)\n\t\t}\n\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"Error adding silence id='%v': %v\\n\", sid, err)\n\t\t} else {\n\t\t\tfmt.Println(postOk.Payload.SilenceID)\n\t\t}\n\t\terrc <- err\n\t}\n}\n\nfunc (c *silenceImportCmd) bulkImport(ctx context.Context, _ *kingpin.ParseContext) error {\n\tinput := os.Stdin\n\tvar err error\n\tif c.file != \"\" {\n\t\tinput, err = os.Open(c.file)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer input.Close()\n\t}\n\n\tdec := json.NewDecoder(input)\n\t// read open square bracket\n\t_, err = dec.Token()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"couldn't unmarshal input data, is it JSON?: %w\", err)\n\t}\n\n\tamclient := NewAlertmanagerClient(alertmanagerURL)\n\tsilencec := make(chan *models.PostableSilence, 100)\n\terrc := make(chan error, 100)\n\terrDone := make(chan struct{})\n\n\tvar wg sync.WaitGroup\n\tvar once sync.Once\n\n\tcloseChannels := func() {\n\t\tonce.Do(func() {\n\t\t\tclose(silencec)\n\t\t\twg.Wait()\n\t\t\tclose(errc)\n\t\t\t<-errDone\n\t\t\tclose(errDone)\n\t\t})\n\t}\n\tdefer closeChannels()\n\tfor w := 0; w < c.workers; w++ {\n\t\twg.Go(func() {\n\t\t\taddSilenceWorker(ctx, amclient.Silence, silencec, errc)\n\t\t})\n\t}\n\n\terrCount := 0\n\tgo func() {\n\t\tfor err := range errc {\n\t\t\tif err != nil {\n\t\t\t\terrCount++\n\t\t\t}\n\t\t}\n\t\terrDone <- struct{}{}\n\t}()\n\n\tcount := 0\n\tfor dec.More() {\n\t\tvar s models.PostableSilence\n\t\terr := dec.Decode(&s)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"couldn't unmarshal input data, is it JSON?: %w\", err)\n\t\t}\n\n\t\tif c.force {\n\t\t\t// reset the silence ID so Alertmanager will always create new silence\n\t\t\ts.ID = \"\"\n\t\t}\n\n\t\tsilencec <- &s\n\t\tcount++\n\t}\n\n\tcloseChannels()\n\n\tif errCount > 0 {\n\t\treturn fmt.Errorf(\"couldn't import %v out of %v silences\", errCount, count)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cli/silence_query.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\tkingpin \"github.com/alecthomas/kingpin/v2\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/client/silence\"\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/cli/format\"\n\t\"github.com/prometheus/alertmanager/matcher/compat\"\n)\n\ntype silenceQueryCmd struct {\n\texpired   bool\n\tquiet     bool\n\tcreatedBy string\n\tID        string\n\tmatchers  []string\n\twithin    time.Duration\n}\n\nconst querySilenceHelp = `Query Alertmanager silences.\n\nAmtool has a simplified prometheus query syntax, but contains robust support for\nbash variable expansions. The non-option section of arguments constructs a list\nof \"Matcher Groups\" that will be used to filter your query. The following\nexamples will attempt to show this behaviour in action:\n\namtool silence query alertname=foo node=bar\n\n\tThis query will match all silences with the alertname=foo and node=bar label\n\tvalue pairs set.\n\namtool silence query foo node=bar\n\n\tIf alertname is omitted and the first argument does not contain a '=' or a\n\t'=~' then it will be assumed to be the value of the alertname pair.\n\namtool silence query 'alertname=~foo.*'\n\n\tAs well as direct equality, regex matching is also supported. The '=~' syntax\n\t(similar to prometheus) is used to represent a regex match. Regex matching\n\tcan be used in combination with a direct match.\n\nIn addition to filtering by silence labels, one can also query for silences\nthat are due to expire soon with the \"--within\" parameter. In the event that\nyou want to preemptively act upon expiring silences by either fixing them or\nextending them. For example:\n\namtool silence query --within 8h\n\nreturns all the silences due to expire within the next 8 hours. This syntax can\nalso be combined with the label based filtering above for more flexibility.\n\nThe \"--expired\" parameter returns only expired silences. Used in combination\nwith \"--within=TIME\", amtool returns the silences that expired within the\npreceding duration.\n\namtool silence query --within 2h --expired\n\nreturns all silences that expired within the preceding 2 hours.\n`\n\nfunc configureSilenceQueryCmd(cc *kingpin.CmdClause) {\n\tvar (\n\t\tc        = &silenceQueryCmd{}\n\t\tqueryCmd = cc.Command(\"query\", querySilenceHelp).Default()\n\t)\n\n\tqueryCmd.Flag(\"expired\", \"Show expired silences instead of active\").BoolVar(&c.expired)\n\tqueryCmd.Flag(\"quiet\", \"Only show silence ids\").Short('q').BoolVar(&c.quiet)\n\tqueryCmd.Flag(\"created-by\", \"Show silences that belong to this creator\").StringVar(&c.createdBy)\n\tqueryCmd.Flag(\"id\", \"Get a single silence by its ID\").StringVar(&c.ID)\n\tqueryCmd.Arg(\"matcher-groups\", \"Query filter\").StringsVar(&c.matchers)\n\tqueryCmd.Flag(\"within\", \"Show silences that will expire or have expired within a duration\").DurationVar(&c.within)\n\tqueryCmd.Action(execWithTimeout(c.query))\n}\n\nfunc (c *silenceQueryCmd) query(ctx context.Context, _ *kingpin.ParseContext) error {\n\tif len(c.matchers) > 0 {\n\t\t// If the parser fails then we likely don't have a (=|=~|!=|!~) so lets\n\t\t// assume that the user wants alertname=<arg> and prepend `alertname=`\n\t\t// to the front.\n\t\t_, err := compat.Matcher(c.matchers[0], \"cli\")\n\t\tif err != nil {\n\t\t\tc.matchers[0] = fmt.Sprintf(\"alertname=%s\", strconv.Quote(c.matchers[0]))\n\t\t}\n\t}\n\n\tsilenceParams := silence.NewGetSilencesParams().WithContext(ctx).WithFilter(c.matchers)\n\n\tamclient := NewAlertmanagerClient(alertmanagerURL)\n\n\tgetOk, err := amclient.Silence.GetSilences(silenceParams)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdisplaySilences := []models.GettableSilence{}\n\tfor _, silence := range getOk.Payload {\n\t\t// skip expired silences if --expired is not set\n\t\tif !c.expired && time.Time(*silence.EndsAt).Before(time.Now()) {\n\t\t\tcontinue\n\t\t}\n\t\t// skip active silences if --expired is set\n\t\tif c.expired && time.Time(*silence.EndsAt).After(time.Now()) {\n\t\t\tcontinue\n\t\t}\n\t\t// skip active silences expiring after \"--within\"\n\t\tif !c.expired && int64(c.within) > 0 && time.Time(*silence.EndsAt).After(time.Now().UTC().Add(c.within)) {\n\t\t\tcontinue\n\t\t}\n\t\t// skip silences that expired before \"--within\"\n\t\tif c.expired && int64(c.within) > 0 && time.Time(*silence.EndsAt).Before(time.Now().UTC().Add(-c.within)) {\n\t\t\tcontinue\n\t\t}\n\t\t// Skip silences if the author doesn't match.\n\t\tif c.createdBy != \"\" && *silence.CreatedBy != c.createdBy {\n\t\t\tcontinue\n\t\t}\n\t\t// Skip silences if the ID doesn't match.\n\t\tif c.ID != \"\" && c.ID != *silence.ID {\n\t\t\tcontinue\n\t\t}\n\n\t\tdisplaySilences = append(displaySilences, *silence)\n\t}\n\n\tif c.quiet {\n\t\tfor _, silence := range displaySilences {\n\t\t\tfmt.Println(*silence.ID)\n\t\t}\n\t} else {\n\t\tformatter, found := format.Formatters[output]\n\t\tif !found {\n\t\t\treturn errors.New(\"unknown output formatter\")\n\t\t}\n\t\tif err := formatter.FormatSilences(displaySilences); err != nil {\n\t\t\treturn fmt.Errorf(\"error formatting silences: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cli/silence_update.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kingpin/v2\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/prometheus/common/model\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/client/silence\"\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/cli/format\"\n)\n\ntype silenceUpdateCmd struct {\n\tquiet    bool\n\tduration string\n\tstart    string\n\tend      string\n\tcomment  string\n\tids      []string\n}\n\nfunc configureSilenceUpdateCmd(cc *kingpin.CmdClause) {\n\tvar (\n\t\tc         = &silenceUpdateCmd{}\n\t\tupdateCmd = cc.Command(\"update\", \"Update silences\")\n\t)\n\tupdateCmd.Flag(\"quiet\", \"Only show silence ids\").Short('q').BoolVar(&c.quiet)\n\tupdateCmd.Flag(\"duration\", \"Duration of silence\").Short('d').StringVar(&c.duration)\n\tupdateCmd.Flag(\"start\", \"Set when the silence should start. RFC3339 format 2006-01-02T15:04:05-07:00\").StringVar(&c.start)\n\tupdateCmd.Flag(\"end\", \"Set when the silence should end (overwrites duration). RFC3339 format 2006-01-02T15:04:05-07:00\").StringVar(&c.end)\n\tupdateCmd.Flag(\"comment\", \"A comment to help describe the silence\").Short('c').StringVar(&c.comment)\n\tupdateCmd.Arg(\"update-ids\", \"Silence IDs to update\").StringsVar(&c.ids)\n\n\tupdateCmd.Action(execWithTimeout(c.update))\n}\n\nfunc (c *silenceUpdateCmd) update(ctx context.Context, _ *kingpin.ParseContext) error {\n\tif len(c.ids) < 1 {\n\t\treturn fmt.Errorf(\"no silence IDs specified\")\n\t}\n\n\tamclient := NewAlertmanagerClient(alertmanagerURL)\n\n\tvar updatedSilences []models.GettableSilence\n\tfor _, silenceID := range c.ids {\n\t\tparams := silence.NewGetSilenceParams()\n\t\tparams.SilenceID = strfmt.UUID(silenceID)\n\t\tresponse, err := amclient.Silence.GetSilence(params)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsil := response.Payload\n\t\tif c.start != \"\" {\n\t\t\tstartsAtTime, err := time.Parse(time.RFC3339, c.start)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tstartsAt := strfmt.DateTime(startsAtTime)\n\t\t\tsil.StartsAt = &startsAt\n\t\t}\n\n\t\tif c.end != \"\" {\n\t\t\tendsAtTime, err := time.Parse(time.RFC3339, c.end)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tendsAt := strfmt.DateTime(endsAtTime)\n\t\t\tsil.EndsAt = &endsAt\n\t\t} else if c.duration != \"\" {\n\t\t\td, err := model.ParseDuration(c.duration)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif d == 0 {\n\t\t\t\treturn fmt.Errorf(\"silence duration must be greater than 0\")\n\t\t\t}\n\t\t\tendsAt := strfmt.DateTime(time.Time(*sil.StartsAt).UTC().Add(time.Duration(d)))\n\t\t\tsil.EndsAt = &endsAt\n\t\t}\n\n\t\tif time.Time(*sil.StartsAt).After(time.Time(*sil.EndsAt)) {\n\t\t\treturn errors.New(\"silence cannot start after it ends\")\n\t\t}\n\n\t\tif c.comment != \"\" {\n\t\t\tsil.Comment = &c.comment\n\t\t}\n\n\t\tps := &models.PostableSilence{\n\t\t\tID:      *sil.ID,\n\t\t\tSilence: sil.Silence,\n\t\t}\n\n\t\tamclient := NewAlertmanagerClient(alertmanagerURL)\n\n\t\tsilenceParams := silence.NewPostSilencesParams().WithContext(ctx).WithSilence(ps)\n\t\tpostOk, err := amclient.Silence.PostSilences(silenceParams)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tsil.ID = &postOk.Payload.SilenceID\n\t\tupdatedSilences = append(updatedSilences, *sil)\n\t}\n\n\tif c.quiet {\n\t\tfor _, silence := range updatedSilences {\n\t\t\tfmt.Println(silence.ID)\n\t\t}\n\t} else {\n\t\tformatter, found := format.Formatters[output]\n\t\tif !found {\n\t\t\treturn fmt.Errorf(\"unknown output formatter\")\n\t\t}\n\t\tif err := formatter.FormatSilences(updatedSilences); err != nil {\n\t\t\treturn fmt.Errorf(\"error formatting silences: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cli/template.go",
    "content": "// Copyright 2021 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"github.com/alecthomas/kingpin/v2\"\n)\n\n// configureTemplateCmd represents the template command.\nfunc configureTemplateCmd(app *kingpin.Application) {\n\ttemplateCmd := app.Command(\"template\", \"Render template files.\")\n\tconfigureTemplateRenderCmd(templateCmd)\n}\n"
  },
  {
    "path": "cli/template_render.go",
    "content": "// Copyright 2021 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kingpin/v2\"\n\t\"github.com/prometheus/common/model\"\n\n\t\"github.com/prometheus/alertmanager/template\"\n)\n\nvar defaultData = template.Data{\n\tReceiver: \"receiver\",\n\tStatus:   \"alertstatus\",\n\tAlerts: template.Alerts{\n\t\ttemplate.Alert{\n\t\t\tStatus: string(model.AlertFiring),\n\t\t\tLabels: template.KV{\n\t\t\t\t\"label1\":          \"value1\",\n\t\t\t\t\"label2\":          \"value2\",\n\t\t\t\t\"instance\":        \"foo.bar:1234\",\n\t\t\t\t\"commonlabelkey1\": \"commonlabelvalue1\",\n\t\t\t\t\"commonlabelkey2\": \"commonlabelvalue2\",\n\t\t\t},\n\t\t\tAnnotations: template.KV{\n\t\t\t\t\"annotation1\":          \"value1\",\n\t\t\t\t\"annotation2\":          \"value2\",\n\t\t\t\t\"commonannotationkey1\": \"commonannotationvalue1\",\n\t\t\t\t\"commonannotationkey2\": \"commonannotationvalue2\",\n\t\t\t},\n\t\t\tStartsAt:     time.Now().Add(-5 * time.Minute),\n\t\t\tEndsAt:       time.Now(),\n\t\t\tGeneratorURL: \"https://generatorurl.com\",\n\t\t\tFingerprint:  \"fingerprint1\",\n\t\t},\n\t\ttemplate.Alert{\n\t\t\tStatus: string(model.AlertResolved),\n\t\t\tLabels: template.KV{\n\t\t\t\t\"foo\":             \"bar\",\n\t\t\t\t\"baz\":             \"qux\",\n\t\t\t\t\"commonlabelkey1\": \"commonlabelvalue1\",\n\t\t\t\t\"commonlabelkey2\": \"commonlabelvalue2\",\n\t\t\t},\n\t\t\tAnnotations: template.KV{\n\t\t\t\t\"aaa\":                  \"bbb\",\n\t\t\t\t\"ccc\":                  \"ddd\",\n\t\t\t\t\"commonannotationkey1\": \"commonannotationvalue1\",\n\t\t\t\t\"commonannotationkey2\": \"commonannotationvalue2\",\n\t\t\t},\n\t\t\tStartsAt:     time.Now().Add(-10 * time.Minute),\n\t\t\tEndsAt:       time.Now(),\n\t\t\tGeneratorURL: \"https://generatorurl.com\",\n\t\t\tFingerprint:  \"fingerprint2\",\n\t\t},\n\t},\n\tGroupLabels: template.KV{\n\t\t\"grouplabelkey1\": \"grouplabelvalue1\",\n\t\t\"grouplabelkey2\": \"grouplabelvalue2\",\n\t},\n\tCommonLabels: template.KV{\n\t\t\"commonlabelkey1\": \"commonlabelvalue1\",\n\t\t\"commonlabelkey2\": \"commonlabelvalue2\",\n\t},\n\tCommonAnnotations: template.KV{\n\t\t\"commonannotationkey1\": \"commonannotationvalue1\",\n\t\t\"commonannotationkey2\": \"commonannotationvalue2\",\n\t},\n\tExternalURL: \"https://example.com\",\n}\n\ntype templateRenderCmd struct {\n\ttemplateFilesGlobs []string\n\ttemplateType       string\n\ttemplateText       string\n\ttemplateData       *os.File\n}\n\nfunc configureTemplateRenderCmd(cc *kingpin.CmdClause) {\n\tvar (\n\t\tc         = &templateRenderCmd{}\n\t\trenderCmd = cc.Command(\"render\", \"Render a given definition in a template file to standard output.\")\n\t)\n\n\trenderCmd.Flag(\"template.glob\", \"Glob of paths that will be expanded and used for rendering.\").Required().StringsVar(&c.templateFilesGlobs)\n\trenderCmd.Flag(\"template.text\", \"The template that will be rendered.\").Required().StringVar(&c.templateText)\n\trenderCmd.Flag(\"template.type\", \"The type of the template. Can be either text (default) or html.\").EnumVar(&c.templateType, \"html\", \"text\")\n\trenderCmd.Flag(\"template.data\", \"Full path to a file which contains the data of the alert(-s) with which the --template.text will be rendered. Must be in JSON. File must be formatted according to the following layout: https://pkg.go.dev/github.com/prometheus/alertmanager/template#Data. If none has been specified then a predefined, simple alert will be used for rendering.\").FileVar(&c.templateData)\n\n\trenderCmd.Action(execWithTimeout(c.render))\n}\n\nfunc (c *templateRenderCmd) render(ctx context.Context, _ *kingpin.ParseContext) error {\n\ttmpl, err := template.FromGlobs(c.templateFilesGlobs)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf := tmpl.ExecuteTextString\n\tif c.templateType == \"html\" {\n\t\tf = tmpl.ExecuteHTMLString\n\t}\n\n\tvar data template.Data\n\tif c.templateData == nil {\n\t\tdata = defaultData\n\t} else {\n\t\tcontent, err := io.ReadAll(c.templateData)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := json.Unmarshal(content, &data); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\trendered, err := f(c.templateText, data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Print(rendered)\n\treturn nil\n}\n"
  },
  {
    "path": "cli/test_routing.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/alecthomas/kingpin/v2\"\n\t\"github.com/xlab/treeprint\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/dispatch\"\n\t\"github.com/prometheus/alertmanager/matcher/compat\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n)\n\nconst routingTestHelp = `Test alert routing\n\nWill return receiver names which the alert with given labels resolves to.\nIf the labelset resolves to multiple receivers, they are printed out in order as defined in the routing tree.\n\nRouting is loaded from a local configuration file or a running Alertmanager configuration.\nSpecifying --config.file takes precedence over --alertmanager.url.\n\nExample:\n\n./amtool config routes test --config.file=doc/examples/simple.yml --verify.receivers=team-DB-pager service=database\n\n`\n\nfunc configureRoutingTestCmd(cc *kingpin.CmdClause, c *routingShow) {\n\troutingTestCmd := cc.Command(\"test\", routingTestHelp)\n\n\troutingTestCmd.Flag(\"verify.receivers\", \"Checks if specified receivers matches resolved receivers. The command fails if the labelset does not route to the specified receivers.\").StringVar(&c.expectedReceivers)\n\troutingTestCmd.Flag(\"tree\", \"Prints out matching routes tree.\").BoolVar(&c.debugTree)\n\troutingTestCmd.Arg(\"labels\", \"List of labels to be tested against the configured routes.\").StringsVar(&c.labels)\n\troutingTestCmd.Action(execWithTimeout(c.routingTestAction))\n}\n\n// resolveAlertReceivers returns list of receiver names which given LabelSet resolves to.\nfunc resolveAlertReceivers(mainRoute *dispatch.Route, labels *models.LabelSet) ([]string, error) {\n\tvar (\n\t\tfinalRoutes []*dispatch.Route\n\t\treceivers   []string\n\t)\n\tfinalRoutes = mainRoute.Match(convertClientToCommonLabelSet(*labels))\n\tfor _, r := range finalRoutes {\n\t\treceivers = append(receivers, r.RouteOpts.Receiver)\n\t}\n\treturn receivers, nil\n}\n\nfunc printMatchingTree(mainRoute *dispatch.Route, ls models.LabelSet) {\n\ttree := treeprint.New()\n\tgetMatchingTree(mainRoute, tree, ls)\n\tfmt.Println(\"Matching routes:\")\n\tfmt.Println(tree.String())\n\tfmt.Print(\"\\n\")\n}\n\nfunc (c *routingShow) routingTestAction(ctx context.Context, _ *kingpin.ParseContext) error {\n\tcfg, err := loadAlertmanagerConfig(ctx, alertmanagerURL, c.configFile)\n\tif err != nil {\n\t\tkingpin.Fatalf(\"%v\\n\", err)\n\t\treturn err\n\t}\n\n\tmainRoute := dispatch.NewRoute(cfg.Route, nil)\n\n\t// Parse labels to LabelSet.\n\tls := make(models.LabelSet, len(c.labels))\n\tfor _, l := range c.labels {\n\t\tmatcher, err := compat.Matcher(l, \"cli\")\n\t\tif err != nil {\n\t\t\tkingpin.Fatalf(\"Failed to parse labels: %v\\n\", err)\n\t\t}\n\t\tif matcher.Type != labels.MatchEqual {\n\t\t\tkingpin.Fatalf(\"%s\\n\", \"Labels must be specified as key=value pairs\")\n\t\t}\n\t\tls[matcher.Name] = matcher.Value\n\t}\n\n\tif c.debugTree {\n\t\tprintMatchingTree(mainRoute, ls)\n\t}\n\n\treceivers, err := resolveAlertReceivers(mainRoute, &ls)\n\treceiversSlug := strings.Join(receivers, \",\")\n\tfmt.Printf(\"%s\\n\", receiversSlug)\n\n\tif c.expectedReceivers != \"\" && c.expectedReceivers != receiversSlug {\n\t\tfmt.Printf(\"WARNING: Expected receivers did not match resolved receivers.\\n\")\n\t\tos.Exit(1)\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "cli/test_routing_test.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/dispatch\"\n)\n\ntype routingTestDefinition struct {\n\talert             models.LabelSet\n\texpectedReceivers []string\n\tconfigFile        string\n}\n\nfunc checkResolvedReceivers(mainRoute *dispatch.Route, ls models.LabelSet, expectedReceivers []string) error {\n\tresolvedReceivers, err := resolveAlertReceivers(mainRoute, &ls)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !reflect.DeepEqual(expectedReceivers, resolvedReceivers) {\n\t\treturn fmt.Errorf(\"unexpected routing result want: `%s`, got: `%s`\", strings.Join(expectedReceivers, \",\"), strings.Join(resolvedReceivers, \",\"))\n\t}\n\treturn nil\n}\n\nfunc TestRoutingTest(t *testing.T) {\n\ttests := []*routingTestDefinition{\n\t\t{configFile: \"testdata/conf.routing.yml\", alert: models.LabelSet{\"test\": \"1\"}, expectedReceivers: []string{\"test1\"}},\n\t\t{configFile: \"testdata/conf.routing.yml\", alert: models.LabelSet{\"test\": \"2\"}, expectedReceivers: []string{\"test1\", \"test2\"}},\n\t\t{configFile: \"testdata/conf.routing-reverted.yml\", alert: models.LabelSet{\"test\": \"2\"}, expectedReceivers: []string{\"test2\", \"test1\"}},\n\t\t{configFile: \"testdata/conf.routing.yml\", alert: models.LabelSet{\"test\": \"volovina\"}, expectedReceivers: []string{\"default\"}},\n\t}\n\n\tfor _, test := range tests {\n\t\tcfg, err := config.LoadFile(test.configFile)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to load test configuration: %v\", err)\n\t\t}\n\t\tmainRoute := dispatch.NewRoute(cfg.Route, nil)\n\t\terr = checkResolvedReceivers(mainRoute, test.alert, test.expectedReceivers)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"%v\", err)\n\t\t}\n\t\tfmt.Println(\"  OK\")\n\t}\n}\n"
  },
  {
    "path": "cli/testdata/conf.bad.yml",
    "content": "BAD\n"
  },
  {
    "path": "cli/testdata/conf.good.yml",
    "content": "global:\n  smtp_smarthost: 'localhost:25'\n\ntemplates:\n  - '/etc/alertmanager/template/*.tmpl'\n\nroute:\n  receiver: default\n\nreceivers:\n  - name: default\n"
  },
  {
    "path": "cli/testdata/conf.routing-reverted.yml",
    "content": "global:\n  smtp_smarthost: 'localhost:25'\n\ntemplates:\n  - '/etc/alertmanager/template/*.tmpl'\n\nroute:\n  receiver: default\n  routes:\n    - match:\n        test: 2\n      receiver: test2\n      continue: true\n    - match_re:\n        test: ^[12]$\n      receiver: test1\n      continue: true\n\nreceivers:\n  - name: default\n  - name: test1\n  - name: test2\n"
  },
  {
    "path": "cli/testdata/conf.routing.yml",
    "content": "global:\n  smtp_smarthost: 'localhost:25'\n\ntemplates:\n  - '/etc/alertmanager/template/*.tmpl'\n\nroute:\n  receiver: default\n  routes:\n    - match_re:\n        test: ^[12]$\n      receiver: test1\n      continue: true\n    - match:\n        test: 2\n      receiver: test2\n\nreceivers:\n  - name: default\n  - name: test1\n  - name: test2\n"
  },
  {
    "path": "cli/utils.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cli\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\n\t\"github.com/alecthomas/kingpin/v2\"\n\t\"github.com/prometheus/common/model\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/client/general\"\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n)\n\n// getRemoteAlertmanagerConfigStatus returns status responsecontaining configuration from remote Alertmanager.\nfunc getRemoteAlertmanagerConfigStatus(ctx context.Context, alertmanagerURL *url.URL) (*models.AlertmanagerStatus, error) {\n\tamclient := NewAlertmanagerClient(alertmanagerURL)\n\tparams := general.NewGetStatusParams().WithContext(ctx)\n\tgetOk, err := amclient.General.GetStatus(params)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn getOk.Payload, nil\n}\n\nfunc checkRoutingConfigInputFlags(alertmanagerURL *url.URL, configFile string) {\n\tif alertmanagerURL != nil && configFile != \"\" {\n\t\tfmt.Fprintln(os.Stderr, \"Warning: --config.file flag overrides the --alertmanager.url.\")\n\t}\n\tif alertmanagerURL == nil && configFile == \"\" {\n\t\tkingpin.Fatalf(\"You have to specify one of --config.file or --alertmanager.url flags.\")\n\t}\n}\n\nfunc loadAlertmanagerConfig(ctx context.Context, alertmanagerURL *url.URL, configFile string) (*config.Config, error) {\n\tcheckRoutingConfigInputFlags(alertmanagerURL, configFile)\n\tif configFile != \"\" {\n\t\tcfg, err := config.LoadFile(configFile)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn cfg, nil\n\t}\n\tif alertmanagerURL == nil {\n\t\treturn nil, errors.New(\"failed to get Alertmanager configuration\")\n\t}\n\tconfigStatus, err := getRemoteAlertmanagerConfigStatus(ctx, alertmanagerURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn config.Load(*configStatus.Config.Original)\n}\n\n// convertClientToCommonLabelSet converts client.LabelSet to model.Labelset.\nfunc convertClientToCommonLabelSet(cls models.LabelSet) model.LabelSet {\n\tmls := make(model.LabelSet, len(cls))\n\tfor ln, lv := range cls {\n\t\tmls[model.LabelName(ln)] = model.LabelValue(lv)\n\t}\n\treturn mls\n}\n\n// TypeMatchers only valid for when you are going to add a silence.\nfunc TypeMatchers(matchers []labels.Matcher) models.Matchers {\n\ttypeMatchers := make(models.Matchers, len(matchers))\n\tfor i, matcher := range matchers {\n\t\ttypeMatchers[i] = TypeMatcher(matcher)\n\t}\n\treturn typeMatchers\n}\n\n// TypeMatcher only valid for when you are going to add a silence.\nfunc TypeMatcher(matcher labels.Matcher) *models.Matcher {\n\tname := matcher.Name\n\tvalue := matcher.Value\n\ttypeMatcher := models.Matcher{\n\t\tName:  &name,\n\t\tValue: &value,\n\t}\n\n\tisEqual := (matcher.Type == labels.MatchEqual) || (matcher.Type == labels.MatchRegexp)\n\tisRegex := (matcher.Type == labels.MatchRegexp) || (matcher.Type == labels.MatchNotRegexp)\n\ttypeMatcher.IsEqual = &isEqual\n\ttypeMatcher.IsRegex = &isRegex\n\treturn &typeMatcher\n}\n\n// Helper function for adding the ctx with timeout into an action.\nfunc execWithTimeout(fn func(context.Context, *kingpin.ParseContext) error) func(*kingpin.ParseContext) error {\n\treturn func(x *kingpin.ParseContext) error {\n\t\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\t\tdefer cancel()\n\t\treturn fn(ctx, x)\n\t}\n}\n"
  },
  {
    "path": "cluster/advertise.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cluster\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\n\t\"github.com/hashicorp/go-sockaddr\"\n)\n\ntype getIPFunc func() (string, error)\n\n// These are overridden in unit tests to mock the sockaddr functions.\nvar (\n\tgetPrivateAddress getIPFunc = sockaddr.GetPrivateIP\n\tgetPublicAddress  getIPFunc = sockaddr.GetPublicIP\n)\n\n// calculateAdvertiseAddress attempts to clone logic from deep within memberlist\n// (NetTransport.FinalAdvertiseAddr) in order to surface its conclusions to the\n// application, so we can provide more actionable error messages if the user has\n// inadvertently misconfigured their cluster.\n//\n// https://github.com/hashicorp/memberlist/blob/022f081/net_transport.go#L126\nfunc calculateAdvertiseAddress(bindAddr, advertiseAddr string, allowInsecureAdvertise bool) (net.IP, error) {\n\tif advertiseAddr != \"\" {\n\t\tip := net.ParseIP(advertiseAddr)\n\t\tif ip == nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse advertise addr '%s'\", advertiseAddr)\n\t\t}\n\t\tif ip4 := ip.To4(); ip4 != nil {\n\t\t\tip = ip4\n\t\t}\n\t\treturn ip, nil\n\t}\n\n\tif isAny(bindAddr) {\n\t\treturn discoverAdvertiseAddress(allowInsecureAdvertise)\n\t}\n\n\tip := net.ParseIP(bindAddr)\n\tif ip == nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse bind addr '%s'\", bindAddr)\n\t}\n\treturn ip, nil\n}\n\n// discoverAdvertiseAddress will attempt to get a single IP address to use as\n// the advertise address when one is not explicitly provided. It defaults to\n// using a private IP address, and if not found then using a public IP if\n// insecure advertising is allowed.\nfunc discoverAdvertiseAddress(allowInsecureAdvertise bool) (net.IP, error) {\n\taddr, err := getPrivateAddress()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get private IP: %w\", err)\n\t}\n\tif addr == \"\" && !allowInsecureAdvertise {\n\t\treturn nil, errors.New(\"no private IP found, explicit advertise addr not provided\")\n\t}\n\n\tif addr == \"\" {\n\t\taddr, err = getPublicAddress()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get public IP: %w\", err)\n\t\t}\n\t\tif addr == \"\" {\n\t\t\treturn nil, errors.New(\"no private/public IP found, explicit advertise addr not provided\")\n\t\t}\n\t}\n\n\tip := net.ParseIP(addr)\n\tif ip == nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse discovered IP '%s'\", addr)\n\t}\n\treturn ip, nil\n}\n"
  },
  {
    "path": "cluster/advertise_test.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cluster\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCalculateAdvertiseAddress(t *testing.T) {\n\told := getPrivateAddress\n\tdefer func() {\n\t\tgetPrivateAddress = old\n\t}()\n\n\tcases := []struct {\n\t\tname                   string\n\t\tprivateIPFn            getIPFunc\n\t\tpublicIPFn             getIPFunc\n\t\tbind, advertise        string\n\t\tallowInsecureAdvertise bool\n\n\t\texpectedIP net.IP\n\t\terr        bool\n\t}{\n\t\t{\n\t\t\tname:      \"use provided bind address\",\n\t\t\tbind:      \"192.0.2.1\",\n\t\t\tadvertise: \"\",\n\n\t\t\texpectedIP: net.ParseIP(\"192.0.2.1\"),\n\t\t\terr:        false,\n\t\t},\n\t\t{\n\t\t\tname:      \"use provided advertise address\",\n\t\t\tbind:      \"192.0.2.1\",\n\t\t\tadvertise: \"192.0.2.2\",\n\n\t\t\texpectedIP: net.ParseIP(\"192.0.2.2\"),\n\t\t\terr:        false,\n\t\t},\n\t\t{\n\t\t\tname:        \"discover private ip address\",\n\t\t\tprivateIPFn: func() (string, error) { return \"192.0.2.1\", nil },\n\t\t\tbind:        \"0.0.0.0\",\n\t\t\tadvertise:   \"\",\n\n\t\t\texpectedIP: net.ParseIP(\"192.0.2.1\"),\n\t\t\terr:        false,\n\t\t},\n\t\t{\n\t\t\tname:        \"error if getPrivateAddress errors\",\n\t\t\tprivateIPFn: func() (string, error) { return \"\", errors.New(\"some error\") },\n\t\t\tbind:        \"0.0.0.0\",\n\t\t\tadvertise:   \"\",\n\n\t\t\terr: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"error if getPrivateAddress returns an invalid address\",\n\t\t\tprivateIPFn: func() (string, error) { return \"invalid\", nil },\n\t\t\tbind:        \"0.0.0.0\",\n\t\t\tadvertise:   \"\",\n\n\t\t\terr: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"error if getPrivateAddress returns an empty address\",\n\t\t\tprivateIPFn: func() (string, error) { return \"\", nil },\n\t\t\tbind:        \"0.0.0.0\",\n\t\t\tadvertise:   \"\",\n\n\t\t\terr: true,\n\t\t},\n\n\t\t{\n\t\t\tname:                   \"discover public advertise address\",\n\t\t\tprivateIPFn:            func() (string, error) { return \"\", nil },\n\t\t\tpublicIPFn:             func() (string, error) { return \"192.0.2.1\", nil },\n\t\t\tbind:                   \"0.0.0.0\",\n\t\t\tadvertise:              \"\",\n\t\t\tallowInsecureAdvertise: true,\n\n\t\t\texpectedIP: net.ParseIP(\"192.0.2.1\"),\n\t\t\terr:        false,\n\t\t},\n\t\t{\n\t\t\tname:                   \"error if getPublicAddress errors\",\n\t\t\tprivateIPFn:            func() (string, error) { return \"\", nil },\n\t\t\tpublicIPFn:             func() (string, error) { return \"\", errors.New(\"some error\") },\n\t\t\tbind:                   \"0.0.0.0\",\n\t\t\tadvertise:              \"\",\n\t\t\tallowInsecureAdvertise: true,\n\n\t\t\terr: true,\n\t\t},\n\t\t{\n\t\t\tname:                   \"error if getPublicAddress returns an invalid address\",\n\t\t\tprivateIPFn:            func() (string, error) { return \"\", nil },\n\t\t\tpublicIPFn:             func() (string, error) { return \"invalid\", nil },\n\t\t\tbind:                   \"0.0.0.0\",\n\t\t\tadvertise:              \"\",\n\t\t\tallowInsecureAdvertise: true,\n\n\t\t\terr: true,\n\t\t},\n\t\t{\n\t\t\tname:                   \"error if getPublicAddress returns an empty address\",\n\t\t\tprivateIPFn:            func() (string, error) { return \"\", nil },\n\t\t\tpublicIPFn:             func() (string, error) { return \"\", nil },\n\t\t\tbind:                   \"0.0.0.0\",\n\t\t\tadvertise:              \"\",\n\t\t\tallowInsecureAdvertise: true,\n\n\t\t\terr: true,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.name, func(t *testing.T) {\n\t\t\tgetPrivateAddress = c.privateIPFn\n\t\t\tgetPublicAddress = c.publicIPFn\n\t\t\tgot, err := calculateAdvertiseAddress(c.bind, c.advertise, c.allowInsecureAdvertise)\n\t\t\tif c.err {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, c.expectedIP.String(), got.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cluster/channel.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cluster\n\nimport (\n\t\"log/slog\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/hashicorp/memberlist\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"github.com/prometheus/alertmanager/cluster/clusterpb\"\n)\n\n// Channel allows clients to send messages for a specific state type that will be\n// broadcasted in a best-effort manner.\ntype Channel struct {\n\tkey          string\n\tsend         func([]byte)\n\tpeers        func() []*memberlist.Node\n\tsendOversize func(*memberlist.Node, []byte) error\n\n\tmsgc   chan []byte\n\tlogger *slog.Logger\n\n\toversizeGossipMessageFailureTotal prometheus.Counter\n\toversizeGossipMessageDroppedTotal prometheus.Counter\n\toversizeGossipMessageSentTotal    prometheus.Counter\n\toversizeGossipDuration            prometheus.Histogram\n}\n\n// NewChannel creates a new Channel struct, which handles sending normal and\n// oversize messages to peers.\nfunc NewChannel(\n\tkey string,\n\tsend func([]byte),\n\tpeers func() []*memberlist.Node,\n\tsendOversize func(*memberlist.Node, []byte) error,\n\tlogger *slog.Logger,\n\tstopc <-chan struct{},\n\treg prometheus.Registerer,\n) *Channel {\n\tif reg == nil {\n\t\treturn nil\n\t}\n\toversizeGossipMessageFailureTotal := promauto.With(reg).NewCounter(prometheus.CounterOpts{\n\t\tName:        \"alertmanager_oversized_gossip_message_failure_total\",\n\t\tHelp:        \"Number of oversized gossip message sends that failed.\",\n\t\tConstLabels: prometheus.Labels{\"key\": key},\n\t})\n\toversizeGossipMessageSentTotal := promauto.With(reg).NewCounter(prometheus.CounterOpts{\n\t\tName:        \"alertmanager_oversized_gossip_message_sent_total\",\n\t\tHelp:        \"Number of oversized gossip message sent.\",\n\t\tConstLabels: prometheus.Labels{\"key\": key},\n\t})\n\toversizeGossipMessageDroppedTotal := promauto.With(reg).NewCounter(prometheus.CounterOpts{\n\t\tName:        \"alertmanager_oversized_gossip_message_dropped_total\",\n\t\tHelp:        \"Number of oversized gossip messages that were dropped due to a full message queue.\",\n\t\tConstLabels: prometheus.Labels{\"key\": key},\n\t})\n\toversizeGossipDuration := promauto.With(reg).NewHistogram(prometheus.HistogramOpts{\n\t\tName:                            \"alertmanager_oversize_gossip_message_duration_seconds\",\n\t\tHelp:                            \"Duration of oversized gossip message requests.\",\n\t\tConstLabels:                     prometheus.Labels{\"key\": key},\n\t\tBuckets:                         prometheus.DefBuckets,\n\t\tNativeHistogramBucketFactor:     1.1,\n\t\tNativeHistogramMaxBucketNumber:  100,\n\t\tNativeHistogramMinResetDuration: 1 * time.Hour,\n\t})\n\n\tc := &Channel{\n\t\tkey:                               key,\n\t\tsend:                              send,\n\t\tpeers:                             peers,\n\t\tlogger:                            logger,\n\t\tmsgc:                              make(chan []byte, 200),\n\t\tsendOversize:                      sendOversize,\n\t\toversizeGossipMessageFailureTotal: oversizeGossipMessageFailureTotal,\n\t\toversizeGossipMessageDroppedTotal: oversizeGossipMessageDroppedTotal,\n\t\toversizeGossipMessageSentTotal:    oversizeGossipMessageSentTotal,\n\t\toversizeGossipDuration:            oversizeGossipDuration,\n\t}\n\n\tgo c.handleOverSizedMessages(stopc)\n\n\treturn c\n}\n\n// handleOverSizedMessages prevents memberlist from opening too many parallel\n// TCP connections to its peers.\nfunc (c *Channel) handleOverSizedMessages(stopc <-chan struct{}) {\n\tvar wg sync.WaitGroup\n\tfor {\n\t\tselect {\n\t\tcase b := <-c.msgc:\n\t\t\tfor _, n := range c.peers() {\n\t\t\t\twg.Add(1)\n\t\t\t\tgo func(n *memberlist.Node) {\n\t\t\t\t\tdefer wg.Done()\n\t\t\t\t\tc.oversizeGossipMessageSentTotal.Inc()\n\t\t\t\t\tstart := time.Now()\n\t\t\t\t\tif err := c.sendOversize(n, b); err != nil {\n\t\t\t\t\t\tc.logger.Debug(\"failed to send reliable\", \"key\", c.key, \"node\", n, \"err\", err)\n\t\t\t\t\t\tc.oversizeGossipMessageFailureTotal.Inc()\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tc.oversizeGossipDuration.Observe(time.Since(start).Seconds())\n\t\t\t\t}(n)\n\t\t\t}\n\n\t\t\twg.Wait()\n\t\tcase <-stopc:\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// Broadcast enqueues a message for broadcasting.\nfunc (c *Channel) Broadcast(b []byte) {\n\tb, err := proto.Marshal(&clusterpb.Part{Key: c.key, Data: b})\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif OversizedMessage(b) {\n\t\tselect {\n\t\tcase c.msgc <- b:\n\t\tdefault:\n\t\t\tc.logger.Debug(\"oversized gossip channel full\")\n\t\t\tc.oversizeGossipMessageDroppedTotal.Inc()\n\t\t}\n\t} else {\n\t\tc.send(b)\n\t}\n}\n\n// OversizedMessage indicates whether or not the byte payload should be sent\n// via TCP.\nfunc OversizedMessage(b []byte) bool {\n\treturn len(b) > MaxGossipPacketSize/2\n}\n"
  },
  {
    "path": "cluster/channel_test.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cluster\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"io\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/hashicorp/memberlist\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/common/promslog\"\n)\n\nfunc TestNormalMessagesGossiped(t *testing.T) {\n\tvar sent bool\n\tc := newChannel(\n\t\tfunc(_ []byte) { sent = true },\n\t\tfunc() []*memberlist.Node { return nil },\n\t\tfunc(_ *memberlist.Node, _ []byte) error { return nil },\n\t)\n\n\tc.Broadcast([]byte{})\n\n\tif sent != true {\n\t\tt.Fatalf(\"small message not sent\")\n\t}\n}\n\nfunc TestOversizedMessagesGossiped(t *testing.T) {\n\tvar sent bool\n\tctx, cancel := context.WithCancel(context.Background())\n\tc := newChannel(\n\t\tfunc(_ []byte) {},\n\t\tfunc() []*memberlist.Node { return []*memberlist.Node{{}} },\n\t\tfunc(_ *memberlist.Node, _ []byte) error { sent = true; cancel(); return nil },\n\t)\n\n\tf, err := os.Open(\"/dev/zero\")\n\tif err != nil {\n\t\tt.Fatalf(\"failed to open /dev/zero: %v\", err)\n\t}\n\tdefer f.Close()\n\n\tbuf := new(bytes.Buffer)\n\ttoCopy := int64(800)\n\tif n, err := io.CopyN(buf, f, toCopy); err != nil {\n\t\tt.Fatalf(\"failed to copy bytes: %v\", err)\n\t} else if n != toCopy {\n\t\tt.Fatalf(\"wanted to copy %d bytes, only copied %d\", toCopy, n)\n\t}\n\n\tc.Broadcast(buf.Bytes())\n\n\t<-ctx.Done()\n\n\tif sent != true {\n\t\tt.Fatalf(\"oversized message not sent\")\n\t}\n}\n\nfunc newChannel(\n\tsend func([]byte),\n\tpeers func() []*memberlist.Node,\n\tsendOversize func(*memberlist.Node, []byte) error,\n) *Channel {\n\treturn NewChannel(\n\t\t\"test\",\n\t\tsend,\n\t\tpeers,\n\t\tsendOversize,\n\t\tpromslog.NewNopLogger(),\n\t\tmake(chan struct{}),\n\t\tprometheus.NewRegistry(),\n\t)\n}\n"
  },
  {
    "path": "cluster/cluster.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cluster\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/hashicorp/memberlist\"\n\t\"github.com/oklog/ulid/v2\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n)\n\n// ClusterPeer represents a single Peer in a gossip cluster.\ntype ClusterPeer interface {\n\t// Name returns the unique identifier of this peer in the cluster.\n\tName() string\n\t// Status returns a status string representing the peer state.\n\tStatus() string\n\t// Peers returns the peer nodes in the cluster.\n\tPeers() []ClusterMember\n}\n\n// ClusterMember interface that represents node peers in a cluster.\ntype ClusterMember interface {\n\t// Name returns the name of the node\n\tName() string\n\t// Address returns the IP address of the node\n\tAddress() string\n}\n\n// ClusterChannel supports state broadcasting across peers.\ntype ClusterChannel interface {\n\tBroadcast([]byte)\n}\n\n// Peer is a single peer in a gossip cluster.\ntype Peer struct {\n\tmlist    *memberlist.Memberlist\n\tdelegate *delegate\n\n\tresolvedPeers       []string\n\tresolvePeersTimeout time.Duration\n\n\tmtx    sync.RWMutex\n\tstates map[string]State\n\tstopc  chan struct{}\n\treadyc chan struct{}\n\n\tpeerLock    sync.RWMutex\n\tpeers       map[string]peer\n\tfailedPeers []peer\n\n\tknownPeers    []string\n\tadvertiseAddr string\n\n\tfailedReconnectionsCounter prometheus.Counter\n\treconnectionsCounter       prometheus.Counter\n\tfailedRefreshCounter       prometheus.Counter\n\trefreshCounter             prometheus.Counter\n\tpeerLeaveCounter           prometheus.Counter\n\tpeerUpdateCounter          prometheus.Counter\n\tpeerJoinCounter            prometheus.Counter\n\n\tlogger *slog.Logger\n}\n\n// peer is an internal type used for bookkeeping. It holds the state of peers\n// in the cluster.\ntype peer struct {\n\tstatus    PeerStatus\n\tleaveTime time.Time\n\n\t*memberlist.Node\n}\n\n// PeerStatus is the state that a peer is in.\ntype PeerStatus int\n\nconst (\n\tStatusNone PeerStatus = iota\n\tStatusAlive\n\tStatusFailed\n)\n\nfunc (s PeerStatus) String() string {\n\tswitch s {\n\tcase StatusNone:\n\t\treturn \"none\"\n\tcase StatusAlive:\n\t\treturn \"alive\"\n\tcase StatusFailed:\n\t\treturn \"failed\"\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unknown PeerStatus: %d\", s))\n\t}\n}\n\nconst (\n\tDefaultPushPullInterval    = 60 * time.Second\n\tDefaultGossipInterval      = 200 * time.Millisecond\n\tDefaultTCPTimeout          = 10 * time.Second\n\tDefaultProbeTimeout        = 500 * time.Millisecond\n\tDefaultProbeInterval       = 1 * time.Second\n\tDefaultReconnectInterval   = 10 * time.Second\n\tDefaultReconnectTimeout    = 6 * time.Hour\n\tDefaultRefreshInterval     = 15 * time.Second\n\tDefaultResolvePeersTimeout = 15 * time.Second\n\tMaxGossipPacketSize        = 1400\n)\n\nfunc Create(\n\tl *slog.Logger,\n\treg prometheus.Registerer,\n\tbindAddr string,\n\tadvertiseAddr string,\n\tknownPeers []string,\n\twaitIfEmpty bool,\n\tpushPullInterval time.Duration,\n\tgossipInterval time.Duration,\n\ttcpTimeout time.Duration,\n\tresolveTimeout time.Duration,\n\tprobeTimeout time.Duration,\n\tprobeInterval time.Duration,\n\ttlsTransportConfig *TLSTransportConfig,\n\tallowInsecureAdvertise bool,\n\tlabel string,\n\tname string,\n) (*Peer, error) {\n\tbindHost, bindPortStr, err := net.SplitHostPort(bindAddr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid listen address: %w\", err)\n\t}\n\tbindPort, err := strconv.Atoi(bindPortStr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"address %s: invalid port: %w\", bindAddr, err)\n\t}\n\n\tvar advertiseHost string\n\tvar advertisePort int\n\tif advertiseAddr != \"\" {\n\t\tvar advertisePortStr string\n\t\tadvertiseHost, advertisePortStr, err = net.SplitHostPort(advertiseAddr)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid advertise address: %w\", err)\n\t\t}\n\t\tadvertisePort, err = strconv.Atoi(advertisePortStr)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"address %s: invalid port: %w\", advertiseAddr, err)\n\t\t}\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), resolveTimeout)\n\tdefer cancel()\n\tresolvedPeers, err := resolvePeers(ctx, knownPeers, advertiseAddr, &net.Resolver{}, waitIfEmpty)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"resolve peers: %w\", err)\n\t}\n\tl.Debug(\"resolved peers to following addresses\", \"peers\", strings.Join(resolvedPeers, \",\"))\n\n\t// Initial validation of user-specified advertise address.\n\taddr, err := calculateAdvertiseAddress(bindHost, advertiseHost, allowInsecureAdvertise)\n\tif err != nil {\n\t\tl.Warn(\"couldn't deduce an advertise address: \" + err.Error())\n\t} else if hasNonlocal(resolvedPeers) && isUnroutable(addr.String()) {\n\t\tl.Warn(\"this node advertises itself on an unroutable address\", \"addr\", addr.String())\n\t\tl.Warn(\"this node will be unreachable in the cluster\")\n\t\tl.Warn(\"provide --cluster.advertise-address as a routable IP address or hostname\")\n\t} else if isAny(bindAddr) && advertiseHost == \"\" {\n\t\t// memberlist doesn't advertise properly when the bind address is empty or unspecified.\n\t\tl.Info(\"setting advertise address explicitly\", \"addr\", addr.String(), \"port\", bindPort)\n\t\tadvertiseHost = addr.String()\n\t\tadvertisePort = bindPort\n\t}\n\n\t// Generate a random name if none is provided.\n\tif name == \"\" {\n\t\tid, err := ulid.New(ulid.Now(), rand.Reader)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tname = id.String()\n\t}\n\n\tp := &Peer{\n\t\tstates:              map[string]State{},\n\t\tstopc:               make(chan struct{}),\n\t\treadyc:              make(chan struct{}),\n\t\tlogger:              l,\n\t\tpeers:               map[string]peer{},\n\t\tresolvedPeers:       resolvedPeers,\n\t\tresolvePeersTimeout: resolveTimeout,\n\t\tknownPeers:          knownPeers,\n\t}\n\n\tp.register(reg, name)\n\n\tretransmit := max(len(knownPeers)/2, 3)\n\tp.delegate = newDelegate(l, reg, p, retransmit)\n\n\tcfg := memberlist.DefaultLANConfig()\n\tcfg.Name = name\n\tcfg.BindAddr = bindHost\n\tcfg.BindPort = bindPort\n\tcfg.Delegate = p.delegate\n\tcfg.Ping = p.delegate\n\tcfg.Alive = p.delegate\n\tcfg.Events = p.delegate\n\tcfg.Conflict = p.delegate\n\tcfg.GossipInterval = gossipInterval\n\tcfg.PushPullInterval = pushPullInterval\n\tcfg.TCPTimeout = tcpTimeout\n\tcfg.ProbeTimeout = probeTimeout\n\tcfg.ProbeInterval = probeInterval\n\tcfg.Logger = slog.NewLogLogger(l.Handler(), slog.LevelDebug)\n\tcfg.GossipNodes = retransmit\n\tcfg.UDPBufferSize = MaxGossipPacketSize\n\tcfg.Label = label\n\n\tif advertiseHost != \"\" {\n\t\tcfg.AdvertiseAddr = advertiseHost\n\t\tcfg.AdvertisePort = advertisePort\n\t\tp.setInitialFailed(resolvedPeers, fmt.Sprintf(\"%s:%d\", advertiseHost, advertisePort))\n\t} else {\n\t\tp.setInitialFailed(resolvedPeers, bindAddr)\n\t}\n\n\tif tlsTransportConfig != nil {\n\t\tl.Info(\"using TLS for gossip\")\n\t\tcfg.Transport, err = NewTLSTransport(context.Background(), l, reg, cfg.BindAddr, cfg.BindPort, tlsTransportConfig)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"tls transport: %w\", err)\n\t\t}\n\t}\n\n\tml, err := memberlist.Create(cfg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create memberlist: %w\", err)\n\t}\n\tp.mlist = ml\n\treturn p, nil\n}\n\nfunc (p *Peer) Join(\n\treconnectInterval time.Duration,\n\treconnectTimeout time.Duration,\n) error {\n\tn, err := p.mlist.Join(p.resolvedPeers)\n\tif err != nil {\n\t\tp.logger.Warn(\"failed to join cluster\", \"err\", err)\n\t\tif reconnectInterval != 0 {\n\t\t\tp.logger.Info(fmt.Sprintf(\"will retry joining cluster every %v\", reconnectInterval.String()))\n\t\t}\n\t} else {\n\t\tp.logger.Debug(\"joined cluster\", \"peers\", n)\n\t}\n\n\tif reconnectInterval != 0 {\n\t\tgo p.runPeriodicTask(\n\t\t\treconnectInterval,\n\t\t\tp.reconnect,\n\t\t)\n\t}\n\tif reconnectTimeout != 0 {\n\t\tgo p.runPeriodicTask(\n\t\t\t5*time.Minute,\n\t\t\tfunc() { p.removeFailedPeers(reconnectTimeout) },\n\t\t)\n\t}\n\tgo p.runPeriodicTask(\n\t\tDefaultRefreshInterval,\n\t\tp.refresh,\n\t)\n\n\treturn err\n}\n\n// All peers are initially added to the failed list. They will be removed from\n// this list in peerJoin when making their initial connection.\nfunc (p *Peer) setInitialFailed(peers []string, myAddr string) {\n\tif len(peers) == 0 {\n\t\treturn\n\t}\n\n\tp.peerLock.Lock()\n\tdefer p.peerLock.Unlock()\n\n\tnow := time.Now()\n\tfor _, peerAddr := range peers {\n\t\tif peerAddr == myAddr {\n\t\t\t// Don't add ourselves to the initially failing list,\n\t\t\t// we don't connect to ourselves.\n\t\t\tcontinue\n\t\t}\n\t\thost, port, err := net.SplitHostPort(peerAddr)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tip := net.ParseIP(host)\n\t\tif ip == nil {\n\t\t\t// Don't add textual addresses since memberlist only advertises\n\t\t\t// dotted decimal or IPv6 addresses.\n\t\t\tcontinue\n\t\t}\n\t\tportUint, err := strconv.ParseUint(port, 10, 16)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tpr := peer{\n\t\t\tstatus:    StatusFailed,\n\t\t\tleaveTime: now,\n\t\t\tNode: &memberlist.Node{\n\t\t\t\tAddr: ip,\n\t\t\t\tPort: uint16(portUint),\n\t\t\t},\n\t\t}\n\t\tp.failedPeers = append(p.failedPeers, pr)\n\t\tp.peers[peerAddr] = pr\n\t}\n}\n\nfunc (p *Peer) register(reg prometheus.Registerer, name string) {\n\tpeerInfo := promauto.With(reg).NewGauge(\n\t\tprometheus.GaugeOpts{\n\t\t\tName:        \"alertmanager_cluster_peer_info\",\n\t\t\tHelp:        \"A metric with a constant '1' value labeled by peer name.\",\n\t\t\tConstLabels: prometheus.Labels{\"peer\": name},\n\t\t},\n\t)\n\tpeerInfo.Set(1)\n\tpromauto.With(reg).NewGaugeFunc(prometheus.GaugeOpts{\n\t\tName: \"alertmanager_cluster_failed_peers\",\n\t\tHelp: \"Number indicating the current number of failed peers in the cluster.\",\n\t}, func() float64 {\n\t\tp.peerLock.RLock()\n\t\tdefer p.peerLock.RUnlock()\n\n\t\treturn float64(len(p.failedPeers))\n\t})\n\tp.failedReconnectionsCounter = promauto.With(reg).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_cluster_reconnections_failed_total\",\n\t\tHelp: \"A counter of the number of failed cluster peer reconnection attempts.\",\n\t})\n\n\tp.reconnectionsCounter = promauto.With(reg).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_cluster_reconnections_total\",\n\t\tHelp: \"A counter of the number of cluster peer reconnections.\",\n\t})\n\n\tp.failedRefreshCounter = promauto.With(reg).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_cluster_refresh_join_failed_total\",\n\t\tHelp: \"A counter of the number of failed cluster peer joined attempts via refresh.\",\n\t})\n\tp.refreshCounter = promauto.With(reg).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_cluster_refresh_join_total\",\n\t\tHelp: \"A counter of the number of cluster peer joined via refresh.\",\n\t})\n\n\tp.peerLeaveCounter = promauto.With(reg).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_cluster_peers_left_total\",\n\t\tHelp: \"A counter of the number of peers that have left.\",\n\t})\n\tp.peerUpdateCounter = promauto.With(reg).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_cluster_peers_update_total\",\n\t\tHelp: \"A counter of the number of peers that have updated metadata.\",\n\t})\n\tp.peerJoinCounter = promauto.With(reg).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_cluster_peers_joined_total\",\n\t\tHelp: \"A counter of the number of peers that have joined.\",\n\t})\n}\n\nfunc (p *Peer) runPeriodicTask(d time.Duration, f func()) {\n\ttick := time.NewTicker(d)\n\tdefer tick.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-p.stopc:\n\t\t\treturn\n\t\tcase <-tick.C:\n\t\t\tf()\n\t\t}\n\t}\n}\n\nfunc (p *Peer) removeFailedPeers(timeout time.Duration) {\n\tp.peerLock.Lock()\n\tdefer p.peerLock.Unlock()\n\n\tnow := time.Now()\n\n\tkeep := make([]peer, 0, len(p.failedPeers))\n\tfor _, pr := range p.failedPeers {\n\t\tif pr.leaveTime.Add(timeout).After(now) {\n\t\t\tkeep = append(keep, pr)\n\t\t} else {\n\t\t\tp.logger.Debug(\"failed peer has timed out\", \"peer\", pr.Node, \"addr\", pr.Address())\n\t\t\tdelete(p.peers, pr.Name)\n\t\t}\n\t}\n\n\tp.failedPeers = keep\n}\n\nfunc (p *Peer) reconnect() {\n\tp.peerLock.RLock()\n\tfailedPeers := p.failedPeers\n\tp.peerLock.RUnlock()\n\n\tlogger := p.logger.With(\"msg\", \"reconnect\")\n\tfor _, pr := range failedPeers {\n\t\t// No need to do book keeping on failedPeers here. If a\n\t\t// reconnect is successful, they will be announced in\n\t\t// peerJoin().\n\t\tif _, err := p.mlist.Join([]string{pr.Address()}); err != nil {\n\t\t\tp.failedReconnectionsCounter.Inc()\n\t\t\tlogger.Debug(\"failure\", \"peer\", pr.Node, \"addr\", pr.Address(), \"err\", err)\n\t\t} else {\n\t\t\tp.reconnectionsCounter.Inc()\n\t\t\tlogger.Debug(\"success\", \"peer\", pr.Node, \"addr\", pr.Address())\n\t\t}\n\t}\n}\n\nfunc (p *Peer) refresh() {\n\tlogger := p.logger.With(\"msg\", \"refresh\")\n\n\tctx, cancel := context.WithTimeout(context.Background(), p.resolvePeersTimeout)\n\tdefer cancel()\n\tresolvedPeers, err := resolvePeers(ctx, p.knownPeers, p.advertiseAddr, &net.Resolver{}, false)\n\tif err != nil {\n\t\tlogger.Debug(fmt.Sprintf(\"%v\", p.knownPeers), \"err\", err)\n\t\treturn\n\t}\n\n\tmembers := p.mlist.Members()\n\tfor _, peer := range resolvedPeers {\n\t\tvar isPeerFound bool\n\t\tfor _, member := range members {\n\t\t\tif member.Address() == peer {\n\t\t\t\tisPeerFound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !isPeerFound {\n\t\t\tif _, err := p.mlist.Join([]string{peer}); err != nil {\n\t\t\t\tp.failedRefreshCounter.Inc()\n\t\t\t\tlogger.Warn(\"failure\", \"addr\", peer, \"err\", err)\n\t\t\t} else {\n\t\t\t\tp.refreshCounter.Inc()\n\t\t\t\tlogger.Debug(\"success\", \"addr\", peer)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (p *Peer) peerJoin(n *memberlist.Node) {\n\tp.peerLock.Lock()\n\tdefer p.peerLock.Unlock()\n\n\tvar oldStatus PeerStatus\n\tpr, ok := p.peers[n.Address()]\n\tif !ok {\n\t\toldStatus = StatusNone\n\t\tpr = peer{\n\t\t\tstatus: StatusAlive,\n\t\t\tNode:   n,\n\t\t}\n\t} else {\n\t\toldStatus = pr.status\n\t\tpr.Node = n\n\t\tpr.status = StatusAlive\n\t\tpr.leaveTime = time.Time{}\n\t}\n\n\tp.peers[n.Address()] = pr\n\tp.peerJoinCounter.Inc()\n\n\tif oldStatus == StatusFailed {\n\t\tp.logger.Debug(\"peer rejoined\", \"peer\", pr.Node)\n\t\tp.failedPeers = removeOldPeer(p.failedPeers, pr.Address())\n\t}\n}\n\nfunc (p *Peer) peerLeave(n *memberlist.Node) {\n\tp.peerLock.Lock()\n\tdefer p.peerLock.Unlock()\n\n\tpr, ok := p.peers[n.Address()]\n\tif !ok {\n\t\t// Why are we receiving a leave notification from a node that\n\t\t// never joined?\n\t\treturn\n\t}\n\n\tpr.status = StatusFailed\n\tpr.leaveTime = time.Now()\n\tp.failedPeers = append(p.failedPeers, pr)\n\tp.peers[n.Address()] = pr\n\n\tp.peerLeaveCounter.Inc()\n\tp.logger.Debug(\"peer left\", \"peer\", pr.Node)\n}\n\nfunc (p *Peer) peerUpdate(n *memberlist.Node) {\n\tp.peerLock.Lock()\n\tdefer p.peerLock.Unlock()\n\n\tpr, ok := p.peers[n.Address()]\n\tif !ok {\n\t\t// Why are we receiving an update from a node that never\n\t\t// joined?\n\t\treturn\n\t}\n\n\tpr.Node = n\n\tp.peers[n.Address()] = pr\n\n\tp.peerUpdateCounter.Inc()\n\tp.logger.Debug(\"peer updated\", \"peer\", pr.Node)\n}\n\n// AddState adds a new state that will be gossiped. It returns a channel to which\n// broadcast messages for the state can be sent.\nfunc (p *Peer) AddState(key string, s State, reg prometheus.Registerer) ClusterChannel {\n\tp.mtx.Lock()\n\tp.states[key] = s\n\tp.mtx.Unlock()\n\n\tsend := func(b []byte) {\n\t\tp.delegate.bcast.QueueBroadcast(simpleBroadcast(b))\n\t}\n\tpeers := func() []*memberlist.Node {\n\t\tnodes := p.mlist.Members()\n\t\tfor i, n := range nodes {\n\t\t\tif n.String() == p.Self().Name {\n\t\t\t\tnodes = append(nodes[:i], nodes[i+1:]...)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\treturn nodes\n\t}\n\tsendOversize := func(n *memberlist.Node, b []byte) error {\n\t\treturn p.mlist.SendReliable(n, b)\n\t}\n\treturn NewChannel(key, send, peers, sendOversize, p.logger, p.stopc, reg)\n}\n\n// Leave the cluster, waiting up to timeout.\nfunc (p *Peer) Leave(timeout time.Duration) error {\n\tclose(p.stopc)\n\tp.logger.Debug(\"leaving cluster\")\n\treturn p.mlist.Leave(timeout)\n}\n\n// Name returns the unique ID of this peer in the cluster.\nfunc (p *Peer) Name() string {\n\treturn p.mlist.LocalNode().Name\n}\n\n// ClusterSize returns the current number of alive members in the cluster.\nfunc (p *Peer) ClusterSize() int {\n\treturn p.mlist.NumMembers()\n}\n\n// Return true when router has settled.\nfunc (p *Peer) Ready() bool {\n\tselect {\n\tcase <-p.readyc:\n\t\treturn true\n\tdefault:\n\t}\n\treturn false\n}\n\n// Wait until Settle() has finished.\nfunc (p *Peer) WaitReady(ctx context.Context) error {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-p.readyc:\n\t\treturn nil\n\t}\n}\n\n// Return a status string representing the peer state.\nfunc (p *Peer) Status() string {\n\tif p.Ready() {\n\t\treturn \"ready\"\n\t}\n\n\treturn \"settling\"\n}\n\n// Info returns a JSON-serializable dump of cluster state.\n// Useful for debug.\nfunc (p *Peer) Info() map[string]any {\n\tp.mtx.RLock()\n\tdefer p.mtx.RUnlock()\n\n\treturn map[string]any{\n\t\t\"self\":    p.mlist.LocalNode(),\n\t\t\"members\": p.mlist.Members(),\n\t}\n}\n\n// Self returns the node information about the peer itself.\nfunc (p *Peer) Self() *memberlist.Node {\n\treturn p.mlist.LocalNode()\n}\n\n// Member represents a member in the cluster.\ntype Member struct {\n\tnode *memberlist.Node\n}\n\n// Name implements cluster.ClusterMember.\nfunc (m Member) Name() string { return m.node.Name }\n\n// Address implements cluster.ClusterMember.\nfunc (m Member) Address() string { return m.node.Address() }\n\n// Peers returns the peers in the cluster.\nfunc (p *Peer) Peers() []ClusterMember {\n\tpeers := make([]ClusterMember, 0, len(p.mlist.Members()))\n\tfor _, member := range p.mlist.Members() {\n\t\tpeers = append(peers, Member{\n\t\t\tnode: member,\n\t\t})\n\t}\n\treturn peers\n}\n\n// Position returns the position of the peer in the cluster.\nfunc (p *Peer) Position() int {\n\tall := p.mlist.Members()\n\tsort.Slice(all, func(i, j int) bool {\n\t\treturn all[i].Name < all[j].Name\n\t})\n\n\tk := 0\n\tfor _, n := range all {\n\t\tif n.Name == p.Self().Name {\n\t\t\tbreak\n\t\t}\n\t\tk++\n\t}\n\treturn k\n}\n\n// Settle waits until the mesh is ready (and sets the appropriate internal state when it is).\n// The idea is that we don't want to start \"working\" before we get a chance to know most of the alerts and/or silences.\n// Inspired from https://github.com/apache/cassandra/blob/7a40abb6a5108688fb1b10c375bb751cbb782ea4/src/java/org/apache/cassandra/gms/Gossiper.java\n// This is clearly not perfect or strictly correct but should prevent the alertmanager to send notification before it is obviously not ready.\n// This is especially important for those that do not have persistent storage.\nfunc (p *Peer) Settle(ctx context.Context, interval time.Duration) {\n\tconst NumOkayRequired = 3\n\tp.logger.Info(\"Waiting for gossip to settle...\", \"interval\", interval)\n\tstart := time.Now()\n\tnPeers := 0\n\tnOkay := 0\n\ttotalPolls := 0\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\telapsed := time.Since(start)\n\t\t\tp.logger.Info(\"gossip not settled but continuing anyway\", \"polls\", totalPolls, \"elapsed\", elapsed)\n\t\t\tclose(p.readyc)\n\t\t\treturn\n\t\tcase <-time.After(interval):\n\t\t}\n\t\telapsed := time.Since(start)\n\t\tn := len(p.Peers())\n\t\tif nOkay >= NumOkayRequired {\n\t\t\tp.logger.Info(\"gossip settled; proceeding\", \"elapsed\", elapsed)\n\t\t\tbreak\n\t\t}\n\t\tif n == nPeers {\n\t\t\tnOkay++\n\t\t\tp.logger.Debug(\"gossip looks settled\", \"elapsed\", elapsed)\n\t\t} else {\n\t\t\tnOkay = 0\n\t\t\tp.logger.Info(\"gossip not settled\", \"polls\", totalPolls, \"before\", nPeers, \"now\", n, \"elapsed\", elapsed)\n\t\t}\n\t\tnPeers = n\n\t\ttotalPolls++\n\t}\n\tclose(p.readyc)\n}\n\n// State is a piece of state that can be serialized and merged with other\n// serialized state.\ntype State interface {\n\t// MarshalBinary serializes the underlying state.\n\tMarshalBinary() ([]byte, error)\n\n\t// Merge merges serialized state into the underlying state.\n\tMerge(b []byte) error\n}\n\n// We use a simple broadcast implementation in which items are never invalidated by others.\ntype simpleBroadcast []byte\n\nfunc (b simpleBroadcast) Message() []byte                       { return []byte(b) }\nfunc (b simpleBroadcast) Invalidates(memberlist.Broadcast) bool { return false }\nfunc (b simpleBroadcast) Finished()                             {}\n\nfunc resolvePeers(ctx context.Context, peers []string, myAddress string, res *net.Resolver, waitIfEmpty bool) ([]string, error) {\n\tvar resolvedPeers []string\n\n\tfor _, peer := range peers {\n\t\thost, port, err := net.SplitHostPort(peer)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"split host/port for peer %s: %w\", peer, err)\n\t\t}\n\n\t\tretryCtx, cancel := context.WithCancel(ctx)\n\t\tdefer cancel()\n\n\t\tips, err := res.LookupIPAddr(ctx, host)\n\t\tif err != nil {\n\t\t\t// Assume direct address.\n\t\t\tresolvedPeers = append(resolvedPeers, peer)\n\t\t\tcontinue\n\t\t}\n\n\t\tif len(ips) == 0 {\n\t\t\tvar lookupErrSpotted bool\n\n\t\t\terr := retry(2*time.Second, retryCtx.Done(), func() error {\n\t\t\t\tif lookupErrSpotted {\n\t\t\t\t\t// We need to invoke cancel in next run of retry when lookupErrSpotted to preserve LookupIPAddr error.\n\t\t\t\t\tcancel()\n\t\t\t\t}\n\n\t\t\t\tips, err = res.LookupIPAddr(retryCtx, host)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlookupErrSpotted = true\n\t\t\t\t\treturn fmt.Errorf(\"IP Addr lookup for peer %s: %w\", peer, err)\n\t\t\t\t}\n\n\t\t\t\tips = removeMyAddr(ips, port, myAddress)\n\t\t\t\tif len(ips) == 0 {\n\t\t\t\t\tif !waitIfEmpty {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t\treturn errors.New(\"empty IPAddr result. Retrying\")\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tfor _, ip := range ips {\n\t\t\tresolvedPeers = append(resolvedPeers, net.JoinHostPort(ip.String(), port))\n\t\t}\n\t}\n\n\treturn resolvedPeers, nil\n}\n\nfunc removeMyAddr(ips []net.IPAddr, targetPort, myAddr string) []net.IPAddr {\n\tvar result []net.IPAddr\n\n\tfor _, ip := range ips {\n\t\tif net.JoinHostPort(ip.String(), targetPort) == myAddr {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, ip)\n\t}\n\n\treturn result\n}\n\nfunc hasNonlocal(clusterPeers []string) bool {\n\tfor _, peer := range clusterPeers {\n\t\tif host, _, err := net.SplitHostPort(peer); err == nil {\n\t\t\tpeer = host\n\t\t}\n\t\tif ip := net.ParseIP(peer); ip != nil && !ip.IsLoopback() {\n\t\t\treturn true\n\t\t} else if ip == nil && strings.ToLower(peer) != \"localhost\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isUnroutable(addr string) bool {\n\tif host, _, err := net.SplitHostPort(addr); err == nil {\n\t\taddr = host\n\t}\n\tif ip := net.ParseIP(addr); ip != nil && (ip.IsUnspecified() || ip.IsLoopback()) {\n\t\treturn true // typically 0.0.0.0 or localhost\n\t} else if ip == nil && strings.ToLower(addr) == \"localhost\" {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc isAny(addr string) bool {\n\tif host, _, err := net.SplitHostPort(addr); err == nil {\n\t\taddr = host\n\t}\n\treturn addr == \"\" || net.ParseIP(addr).IsUnspecified()\n}\n\n// retry executes f every interval seconds until timeout or no error is returned from f.\nfunc retry(interval time.Duration, stopc <-chan struct{}, f func() error) error {\n\ttick := time.NewTicker(interval)\n\tdefer tick.Stop()\n\n\tvar err error\n\tfor {\n\t\tif err = f(); err == nil {\n\t\t\treturn nil\n\t\t}\n\t\tselect {\n\t\tcase <-stopc:\n\t\t\treturn err\n\t\tcase <-tick.C:\n\t\t}\n\t}\n}\n\nfunc removeOldPeer(old []peer, addr string) []peer {\n\tnew := make([]peer, 0, len(old))\n\tfor _, p := range old {\n\t\tif p.Address() != addr {\n\t\t\tnew = append(new, p)\n\t\t}\n\t}\n\n\treturn new\n}\n"
  },
  {
    "path": "cluster/cluster_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cluster\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-sockaddr\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/common/promslog\"\n)\n\nfunc TestClusterJoinAndReconnect(t *testing.T) {\n\tip, _ := sockaddr.GetPrivateIP()\n\tif ip == \"\" {\n\t\tt.Skipf(\"skipping tests because no private IP address can be found\")\n\t\treturn\n\t}\n\tt.Run(\"TestJoinLeave\", testJoinLeave)\n\tt.Run(\"TestReconnect\", testReconnect)\n\tt.Run(\"TestRemoveFailedPeers\", testRemoveFailedPeers)\n\tt.Run(\"TestInitiallyFailingPeers\", testInitiallyFailingPeers)\n\tt.Run(\"TestTLSConnection\", testTLSConnection)\n\tt.Run(\"TestRandomPeerNames\", func(t *testing.T) { testPeerNames(t, \"\", \"\") })\n\tt.Run(\"TestSetPeerNames\", func(t *testing.T) { testPeerNames(t, \"peer1\", \"peer2\") })\n\tt.Run(\"TestDuplicatePeerNames\", func(t *testing.T) { testPeerNames(t, \"peer\", \"peer\") })\n}\n\nfunc testJoinLeave(t *testing.T) {\n\tlogger := promslog.NewNopLogger()\n\tp, err := Create(\n\t\tlogger,\n\t\tprometheus.NewRegistry(),\n\t\t\"127.0.0.1:0\",\n\t\t\"\",\n\t\t[]string{},\n\t\ttrue,\n\t\tDefaultPushPullInterval,\n\t\tDefaultGossipInterval,\n\t\tDefaultTCPTimeout,\n\t\tDefaultResolvePeersTimeout,\n\t\tDefaultProbeTimeout,\n\t\tDefaultProbeInterval,\n\t\tnil,\n\t\tfalse,\n\t\t\"\",\n\t\t\"\",\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, p)\n\terr = p.Join(\n\t\tDefaultReconnectInterval,\n\t\tDefaultReconnectTimeout,\n\t)\n\trequire.NoError(t, err)\n\trequire.False(t, p.Ready())\n\t{\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tcancel()\n\t\trequire.Equal(t, context.Canceled, p.WaitReady(ctx))\n\t}\n\trequire.Equal(t, \"settling\", p.Status())\n\tgo p.Settle(context.Background(), 0*time.Second)\n\trequire.NoError(t, p.WaitReady(context.Background()))\n\trequire.Equal(t, \"ready\", p.Status())\n\n\t// Create the peer who joins the first.\n\tp2, err := Create(\n\t\tlogger,\n\t\tprometheus.NewRegistry(),\n\t\t\"127.0.0.1:0\",\n\t\t\"\",\n\t\t[]string{p.Self().Address()},\n\t\ttrue,\n\t\tDefaultPushPullInterval,\n\t\tDefaultGossipInterval,\n\t\tDefaultTCPTimeout,\n\t\tDefaultResolvePeersTimeout,\n\t\tDefaultProbeTimeout,\n\t\tDefaultProbeInterval,\n\t\tnil,\n\t\tfalse,\n\t\t\"\",\n\t\t\"\",\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, p2)\n\terr = p2.Join(\n\t\tDefaultReconnectInterval,\n\t\tDefaultReconnectTimeout,\n\t)\n\trequire.NoError(t, err)\n\tgo p2.Settle(context.Background(), 0*time.Second)\n\trequire.NoError(t, p2.WaitReady(context.Background()))\n\n\trequire.Eventually(t, func() bool { return p.ClusterSize() == 2 }, 5*time.Second, time.Second)\n\tp2.Leave(0 * time.Second)\n\trequire.Eventually(t, func() bool { return p.ClusterSize() == 1 }, 5*time.Second, time.Second)\n\trequire.Len(t, p.failedPeers, 1)\n\trequire.Equal(t, p2.Self().Address(), p.peers[p2.Self().Address()].Address())\n\trequire.Equal(t, p2.Name(), p.failedPeers[0].Name)\n}\n\nfunc testReconnect(t *testing.T) {\n\tlogger := promslog.NewNopLogger()\n\tp, err := Create(\n\t\tlogger,\n\t\tprometheus.NewRegistry(),\n\t\t\"127.0.0.1:0\",\n\t\t\"\",\n\t\t[]string{},\n\t\ttrue,\n\t\tDefaultPushPullInterval,\n\t\tDefaultGossipInterval,\n\t\tDefaultTCPTimeout,\n\t\tDefaultResolvePeersTimeout,\n\t\tDefaultProbeTimeout,\n\t\tDefaultProbeInterval,\n\t\tnil,\n\t\tfalse,\n\t\t\"\",\n\t\t\"\",\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, p)\n\terr = p.Join(\n\t\tDefaultReconnectInterval,\n\t\tDefaultReconnectTimeout,\n\t)\n\trequire.NoError(t, err)\n\tgo p.Settle(context.Background(), 0*time.Second)\n\trequire.NoError(t, p.WaitReady(context.Background()))\n\n\tp2, err := Create(\n\t\tlogger,\n\t\tprometheus.NewRegistry(),\n\t\t\"127.0.0.1:0\",\n\t\t\"\",\n\t\t[]string{},\n\t\ttrue,\n\t\tDefaultPushPullInterval,\n\t\tDefaultGossipInterval,\n\t\tDefaultTCPTimeout,\n\t\tDefaultResolvePeersTimeout,\n\t\tDefaultProbeTimeout,\n\t\tDefaultProbeInterval,\n\t\tnil,\n\t\tfalse,\n\t\t\"\",\n\t\t\"\",\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, p2)\n\terr = p2.Join(\n\t\tDefaultReconnectInterval,\n\t\tDefaultReconnectTimeout,\n\t)\n\trequire.NoError(t, err)\n\tgo p2.Settle(context.Background(), 0*time.Second)\n\trequire.NoError(t, p2.WaitReady(context.Background()))\n\n\tp.peerJoin(p2.Self())\n\tp.peerLeave(p2.Self())\n\n\trequire.Equal(t, 1, p.ClusterSize())\n\trequire.Len(t, p.failedPeers, 1)\n\n\tp.reconnect()\n\n\trequire.Equal(t, 2, p.ClusterSize())\n\trequire.Empty(t, p.failedPeers)\n\trequire.Equal(t, StatusAlive, p.peers[p2.Self().Address()].status)\n}\n\nfunc testRemoveFailedPeers(t *testing.T) {\n\tlogger := promslog.NewNopLogger()\n\tp, err := Create(\n\t\tlogger,\n\t\tprometheus.NewRegistry(),\n\t\t\"127.0.0.1:0\",\n\t\t\"\",\n\t\t[]string{},\n\t\ttrue,\n\t\tDefaultPushPullInterval,\n\t\tDefaultGossipInterval,\n\t\tDefaultTCPTimeout,\n\t\tDefaultResolvePeersTimeout,\n\t\tDefaultProbeTimeout,\n\t\tDefaultProbeInterval,\n\t\tnil,\n\t\tfalse,\n\t\t\"\",\n\t\t\"\",\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, p)\n\terr = p.Join(\n\t\tDefaultReconnectInterval,\n\t\tDefaultReconnectTimeout,\n\t)\n\trequire.NoError(t, err)\n\tn := p.Self()\n\n\tnow := time.Now()\n\tp1 := peer{\n\t\tstatus:    StatusFailed,\n\t\tleaveTime: now,\n\t\tNode:      n,\n\t}\n\tp2 := peer{\n\t\tstatus:    StatusFailed,\n\t\tleaveTime: now.Add(-time.Hour),\n\t\tNode:      n,\n\t}\n\tp3 := peer{\n\t\tstatus:    StatusFailed,\n\t\tleaveTime: now.Add(30 * -time.Minute),\n\t\tNode:      n,\n\t}\n\tp.failedPeers = []peer{p1, p2, p3}\n\n\tp.removeFailedPeers(30 * time.Minute)\n\trequire.Len(t, p.failedPeers, 1)\n\trequire.Equal(t, p1, p.failedPeers[0])\n}\n\nfunc testInitiallyFailingPeers(t *testing.T) {\n\tlogger := promslog.NewNopLogger()\n\tmyAddr := \"1.2.3.4:5000\"\n\tpeerAddrs := []string{myAddr, \"2.3.4.5:5000\", \"3.4.5.6:5000\", \"foo.example.com:5000\"}\n\tp, err := Create(\n\t\tlogger,\n\t\tprometheus.NewRegistry(),\n\t\t\"127.0.0.1:0\",\n\t\t\"\",\n\t\t[]string{},\n\t\ttrue,\n\t\tDefaultPushPullInterval,\n\t\tDefaultGossipInterval,\n\t\tDefaultTCPTimeout,\n\t\tDefaultResolvePeersTimeout,\n\t\tDefaultProbeTimeout,\n\t\tDefaultProbeInterval,\n\t\tnil,\n\t\tfalse,\n\t\t\"\",\n\t\t\"\",\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, p)\n\terr = p.Join(\n\t\tDefaultReconnectInterval,\n\t\tDefaultReconnectTimeout,\n\t)\n\trequire.NoError(t, err)\n\n\tp.setInitialFailed(peerAddrs, myAddr)\n\n\t// We shouldn't have added \"our\" bind addr and the FQDN address to the\n\t// failed peers list.\n\trequire.Len(t, p.failedPeers, len(peerAddrs)-2)\n\tfor _, addr := range peerAddrs {\n\t\tif addr == myAddr || addr == \"foo.example.com:5000\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tpr, ok := p.peers[addr]\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, StatusFailed.String(), pr.status.String())\n\t\trequire.Equal(t, addr, pr.Address())\n\t\texpectedLen := len(p.failedPeers) - 1\n\t\tp.peerJoin(pr.Node)\n\t\trequire.Len(t, p.failedPeers, expectedLen)\n\t}\n}\n\nfunc testTLSConnection(t *testing.T) {\n\tlogger := promslog.NewNopLogger()\n\ttlsTransportConfig1, err := GetTLSTransportConfig(\"./testdata/tls_config_node1.yml\")\n\trequire.NoError(t, err)\n\tp1, err := Create(\n\t\tlogger,\n\t\tprometheus.NewRegistry(),\n\t\t\"127.0.0.1:0\",\n\t\t\"\",\n\t\t[]string{},\n\t\ttrue,\n\t\tDefaultPushPullInterval,\n\t\tDefaultGossipInterval,\n\t\tDefaultTCPTimeout,\n\t\tDefaultResolvePeersTimeout,\n\t\tDefaultProbeTimeout,\n\t\tDefaultProbeInterval,\n\t\ttlsTransportConfig1,\n\t\tfalse,\n\t\t\"\",\n\t\t\"\",\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, p1)\n\terr = p1.Join(\n\t\tDefaultReconnectInterval,\n\t\tDefaultReconnectTimeout,\n\t)\n\trequire.NoError(t, err)\n\trequire.False(t, p1.Ready())\n\trequire.Equal(t, \"settling\", p1.Status())\n\tgo p1.Settle(context.Background(), 0*time.Second)\n\tp1.WaitReady(context.Background())\n\trequire.Equal(t, \"ready\", p1.Status())\n\n\t// Create the peer who joins the first.\n\ttlsTransportConfig2, err := GetTLSTransportConfig(\"./testdata/tls_config_node2.yml\")\n\trequire.NoError(t, err)\n\tp2, err := Create(\n\t\tlogger,\n\t\tprometheus.NewRegistry(),\n\t\t\"127.0.0.1:0\",\n\t\t\"\",\n\t\t[]string{p1.Self().Address()},\n\t\ttrue,\n\t\tDefaultPushPullInterval,\n\t\tDefaultGossipInterval,\n\t\tDefaultTCPTimeout,\n\t\tDefaultResolvePeersTimeout,\n\t\tDefaultProbeTimeout,\n\t\tDefaultProbeInterval,\n\t\ttlsTransportConfig2,\n\t\tfalse,\n\t\t\"\",\n\t\t\"\",\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, p2)\n\terr = p2.Join(\n\t\tDefaultReconnectInterval,\n\t\tDefaultReconnectTimeout,\n\t)\n\trequire.NoError(t, err)\n\tgo p2.Settle(context.Background(), 0*time.Second)\n\tp2.WaitReady(context.Background())\n\trequire.Equal(t, \"ready\", p2.Status())\n\n\trequire.Eventually(t, func() bool { return p1.ClusterSize() == 2 }, 5*time.Second, time.Second)\n\tp2.Leave(0 * time.Second)\n\trequire.Eventually(t, func() bool { return p1.ClusterSize() == 1 }, 5*time.Second, time.Second)\n\trequire.Len(t, p1.failedPeers, 1)\n\trequire.Equal(t, p2.Self().Address(), p1.peers[p2.Self().Address()].Address())\n\trequire.Equal(t, p2.Name(), p1.failedPeers[0].Name)\n}\n\nfunc testPeerNames(t *testing.T, name1, name2 string) {\n\tt.Helper()\n\tlogger := promslog.NewNopLogger()\n\tp1, err := Create(\n\t\tlogger,\n\t\tprometheus.NewRegistry(),\n\t\t\"127.0.0.1:0\",\n\t\t\"\",\n\t\t[]string{},\n\t\ttrue,\n\t\tDefaultPushPullInterval,\n\t\tDefaultGossipInterval,\n\t\tDefaultTCPTimeout,\n\t\tDefaultResolvePeersTimeout,\n\t\tDefaultProbeTimeout,\n\t\tDefaultProbeInterval,\n\t\tnil,\n\t\tfalse,\n\t\t\"\",\n\t\tname1,\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, p1)\n\terr = p1.Join(\n\t\tDefaultReconnectInterval,\n\t\tDefaultReconnectTimeout,\n\t)\n\trequire.NoError(t, err)\n\trequire.False(t, p1.Ready())\n\t{\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tcancel()\n\t\trequire.Equal(t, context.Canceled, p1.WaitReady(ctx))\n\t}\n\trequire.Equal(t, \"settling\", p1.Status())\n\tgo p1.Settle(context.Background(), 0*time.Second)\n\trequire.NoError(t, p1.WaitReady(context.Background()))\n\trequire.Equal(t, \"ready\", p1.Status())\n\n\t// Create the peer who joins the first.\n\tp2, err := Create(\n\t\tlogger,\n\t\tprometheus.NewRegistry(),\n\t\t\"127.0.0.1:0\",\n\t\t\"\",\n\t\t[]string{p1.Self().Address()},\n\t\ttrue,\n\t\tDefaultPushPullInterval,\n\t\tDefaultGossipInterval,\n\t\tDefaultTCPTimeout,\n\t\tDefaultResolvePeersTimeout,\n\t\tDefaultProbeTimeout,\n\t\tDefaultProbeInterval,\n\t\tnil,\n\t\tfalse,\n\t\t\"\",\n\t\tname2,\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, p2)\n\terr = p2.Join(\n\t\tDefaultReconnectInterval,\n\t\tDefaultReconnectTimeout,\n\t)\n\trequire.NoError(t, err)\n\tgo p2.Settle(context.Background(), 0*time.Second)\n\trequire.NoError(t, p2.WaitReady(context.Background()))\n\n\tif name1 != name2 {\n\t\trequire.Eventually(t, func() bool { return p1.ClusterSize() == 2 }, 5*time.Second, time.Second)\n\t\trequire.Eventually(t, func() bool { return p2.ClusterSize() == 2 }, 5*time.Second, time.Second)\n\t\trequire.NotEqual(t, p1.Name(), p2.Name(), \"peers should have different names\")\n\t}\n}\n"
  },
  {
    "path": "cluster/clusterpb/cluster.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: cluster.proto\n\npackage clusterpb\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype MemberlistMessage_Kind int32\n\nconst (\n\tMemberlistMessage_STREAM MemberlistMessage_Kind = 0\n\tMemberlistMessage_PACKET MemberlistMessage_Kind = 1\n)\n\n// Enum value maps for MemberlistMessage_Kind.\nvar (\n\tMemberlistMessage_Kind_name = map[int32]string{\n\t\t0: \"STREAM\",\n\t\t1: \"PACKET\",\n\t}\n\tMemberlistMessage_Kind_value = map[string]int32{\n\t\t\"STREAM\": 0,\n\t\t\"PACKET\": 1,\n\t}\n)\n\nfunc (x MemberlistMessage_Kind) Enum() *MemberlistMessage_Kind {\n\tp := new(MemberlistMessage_Kind)\n\t*p = x\n\treturn p\n}\n\nfunc (x MemberlistMessage_Kind) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (MemberlistMessage_Kind) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_cluster_proto_enumTypes[0].Descriptor()\n}\n\nfunc (MemberlistMessage_Kind) Type() protoreflect.EnumType {\n\treturn &file_cluster_proto_enumTypes[0]\n}\n\nfunc (x MemberlistMessage_Kind) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use MemberlistMessage_Kind.Descriptor instead.\nfunc (MemberlistMessage_Kind) EnumDescriptor() ([]byte, []int) {\n\treturn file_cluster_proto_rawDescGZIP(), []int{2, 0}\n}\n\ntype Part struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tKey           string                 `protobuf:\"bytes,1,opt,name=key,proto3\" json:\"key,omitempty\"`\n\tData          []byte                 `protobuf:\"bytes,2,opt,name=data,proto3\" json:\"data,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Part) Reset() {\n\t*x = Part{}\n\tmi := &file_cluster_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Part) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Part) ProtoMessage() {}\n\nfunc (x *Part) ProtoReflect() protoreflect.Message {\n\tmi := &file_cluster_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Part.ProtoReflect.Descriptor instead.\nfunc (*Part) Descriptor() ([]byte, []int) {\n\treturn file_cluster_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Part) GetKey() string {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn \"\"\n}\n\nfunc (x *Part) GetData() []byte {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\ntype FullState struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tParts         []*Part                `protobuf:\"bytes,1,rep,name=parts,proto3\" json:\"parts,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *FullState) Reset() {\n\t*x = FullState{}\n\tmi := &file_cluster_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *FullState) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*FullState) ProtoMessage() {}\n\nfunc (x *FullState) ProtoReflect() protoreflect.Message {\n\tmi := &file_cluster_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use FullState.ProtoReflect.Descriptor instead.\nfunc (*FullState) Descriptor() ([]byte, []int) {\n\treturn file_cluster_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *FullState) GetParts() []*Part {\n\tif x != nil {\n\t\treturn x.Parts\n\t}\n\treturn nil\n}\n\ntype MemberlistMessage struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tVersion       string                 `protobuf:\"bytes,1,opt,name=version,proto3\" json:\"version,omitempty\"`\n\tKind          MemberlistMessage_Kind `protobuf:\"varint,2,opt,name=kind,proto3,enum=clusterpb.MemberlistMessage_Kind\" json:\"kind,omitempty\"`\n\tFromAddr      string                 `protobuf:\"bytes,3,opt,name=from_addr,json=fromAddr,proto3\" json:\"from_addr,omitempty\"`\n\tMsg           []byte                 `protobuf:\"bytes,4,opt,name=msg,proto3\" json:\"msg,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MemberlistMessage) Reset() {\n\t*x = MemberlistMessage{}\n\tmi := &file_cluster_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MemberlistMessage) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MemberlistMessage) ProtoMessage() {}\n\nfunc (x *MemberlistMessage) ProtoReflect() protoreflect.Message {\n\tmi := &file_cluster_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MemberlistMessage.ProtoReflect.Descriptor instead.\nfunc (*MemberlistMessage) Descriptor() ([]byte, []int) {\n\treturn file_cluster_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *MemberlistMessage) GetVersion() string {\n\tif x != nil {\n\t\treturn x.Version\n\t}\n\treturn \"\"\n}\n\nfunc (x *MemberlistMessage) GetKind() MemberlistMessage_Kind {\n\tif x != nil {\n\t\treturn x.Kind\n\t}\n\treturn MemberlistMessage_STREAM\n}\n\nfunc (x *MemberlistMessage) GetFromAddr() string {\n\tif x != nil {\n\t\treturn x.FromAddr\n\t}\n\treturn \"\"\n}\n\nfunc (x *MemberlistMessage) GetMsg() []byte {\n\tif x != nil {\n\t\treturn x.Msg\n\t}\n\treturn nil\n}\n\nvar File_cluster_proto protoreflect.FileDescriptor\n\nconst file_cluster_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\rcluster.proto\\x12\\tclusterpb\\\",\\n\" +\n\t\"\\x04Part\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x12\\n\" +\n\t\"\\x04data\\x18\\x02 \\x01(\\fR\\x04data\\\"2\\n\" +\n\t\"\\tFullState\\x12%\\n\" +\n\t\"\\x05parts\\x18\\x01 \\x03(\\v2\\x0f.clusterpb.PartR\\x05parts\\\"\\xb3\\x01\\n\" +\n\t\"\\x11MemberlistMessage\\x12\\x18\\n\" +\n\t\"\\aversion\\x18\\x01 \\x01(\\tR\\aversion\\x125\\n\" +\n\t\"\\x04kind\\x18\\x02 \\x01(\\x0e2!.clusterpb.MemberlistMessage.KindR\\x04kind\\x12\\x1b\\n\" +\n\t\"\\tfrom_addr\\x18\\x03 \\x01(\\tR\\bfromAddr\\x12\\x10\\n\" +\n\t\"\\x03msg\\x18\\x04 \\x01(\\fR\\x03msg\\\"\\x1e\\n\" +\n\t\"\\x04Kind\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06STREAM\\x10\\x00\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06PACKET\\x10\\x01B6Z4github.com/prometheus/alertmanager/cluster/clusterpbb\\x06proto3\"\n\nvar (\n\tfile_cluster_proto_rawDescOnce sync.Once\n\tfile_cluster_proto_rawDescData []byte\n)\n\nfunc file_cluster_proto_rawDescGZIP() []byte {\n\tfile_cluster_proto_rawDescOnce.Do(func() {\n\t\tfile_cluster_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_cluster_proto_rawDesc), len(file_cluster_proto_rawDesc)))\n\t})\n\treturn file_cluster_proto_rawDescData\n}\n\nvar file_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 1)\nvar file_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 3)\nvar file_cluster_proto_goTypes = []any{\n\t(MemberlistMessage_Kind)(0), // 0: clusterpb.MemberlistMessage.Kind\n\t(*Part)(nil),                // 1: clusterpb.Part\n\t(*FullState)(nil),           // 2: clusterpb.FullState\n\t(*MemberlistMessage)(nil),   // 3: clusterpb.MemberlistMessage\n}\nvar file_cluster_proto_depIdxs = []int32{\n\t1, // 0: clusterpb.FullState.parts:type_name -> clusterpb.Part\n\t0, // 1: clusterpb.MemberlistMessage.kind:type_name -> clusterpb.MemberlistMessage.Kind\n\t2, // [2:2] is the sub-list for method output_type\n\t2, // [2:2] is the sub-list for method input_type\n\t2, // [2:2] is the sub-list for extension type_name\n\t2, // [2:2] is the sub-list for extension extendee\n\t0, // [0:2] is the sub-list for field type_name\n}\n\nfunc init() { file_cluster_proto_init() }\nfunc file_cluster_proto_init() {\n\tif File_cluster_proto != nil {\n\t\treturn\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_cluster_proto_rawDesc), len(file_cluster_proto_rawDesc)),\n\t\t\tNumEnums:      1,\n\t\t\tNumMessages:   3,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_cluster_proto_goTypes,\n\t\tDependencyIndexes: file_cluster_proto_depIdxs,\n\t\tEnumInfos:         file_cluster_proto_enumTypes,\n\t\tMessageInfos:      file_cluster_proto_msgTypes,\n\t}.Build()\n\tFile_cluster_proto = out.File\n\tfile_cluster_proto_goTypes = nil\n\tfile_cluster_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "cluster/clusterpb/cluster.proto",
    "content": "syntax = \"proto3\";\n\npackage clusterpb;\n\noption go_package = \"github.com/prometheus/alertmanager/cluster/clusterpb\";\n\nmessage Part {\n  string key = 1;\n  bytes data = 2;\n}\n  \nmessage FullState {\n  repeated Part parts = 1;\n}\n\nmessage MemberlistMessage {\n  string version = 1;\n  enum Kind {\n    STREAM = 0;\n    PACKET = 1;\n  }\n  Kind kind = 2;\n  string from_addr = 3;\n  bytes msg = 4;\n}\n"
  },
  {
    "path": "cluster/connection_pool.go",
    "content": "// Copyright 2020 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cluster\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\tlru \"github.com/hashicorp/golang-lru/v2\"\n)\n\nconst capacity = 1024\n\ntype connectionPool struct {\n\tmtx       sync.Mutex\n\tcache     *lru.Cache[string, *tlsConn]\n\ttlsConfig *tls.Config\n}\n\nfunc newConnectionPool(tlsClientCfg *tls.Config) (*connectionPool, error) {\n\tcache, err := lru.NewWithEvict(\n\t\tcapacity, func(_ string, conn *tlsConn) {\n\t\t\tconn.Close()\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create new LRU: %w\", err)\n\t}\n\treturn &connectionPool{\n\t\tcache:     cache,\n\t\ttlsConfig: tlsClientCfg,\n\t}, nil\n}\n\n// borrowConnection returns a *tlsConn from the pool. The connection does not\n// need to be returned to the pool because each connection has its own locking.\nfunc (pool *connectionPool) borrowConnection(addr string, timeout time.Duration) (*tlsConn, error) {\n\tpool.mtx.Lock()\n\tdefer pool.mtx.Unlock()\n\tif pool.cache == nil {\n\t\treturn nil, errors.New(\"connection pool closed\")\n\t}\n\tkey := fmt.Sprintf(\"%s/%d\", addr, int64(timeout))\n\tconn, exists := pool.cache.Get(key)\n\tif exists && conn.alive() {\n\t\treturn conn, nil\n\t}\n\tconn, err := dialTLSConn(addr, timeout, pool.tlsConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpool.cache.Add(key, conn)\n\treturn conn, nil\n}\n\nfunc (pool *connectionPool) shutdown() {\n\tpool.mtx.Lock()\n\tdefer pool.mtx.Unlock()\n\tif pool.cache == nil {\n\t\treturn\n\t}\n\tpool.cache.Purge()\n\tpool.cache = nil\n}\n"
  },
  {
    "path": "cluster/delegate.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cluster\n\nimport (\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/hashicorp/memberlist\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"github.com/prometheus/alertmanager/cluster/clusterpb\"\n)\n\nconst (\n\t// Maximum number of messages to be held in the queue.\n\tmaxQueueSize = 4096\n\tfullState    = \"full_state\"\n\tupdate       = \"update\"\n)\n\n// delegate implements memberlist.Delegate and memberlist.EventDelegate\n// and broadcasts its peer's state in the cluster.\ntype delegate struct {\n\t*Peer\n\n\tlogger *slog.Logger\n\tbcast  *memberlist.TransmitLimitedQueue\n\n\tmessagesReceived     *prometheus.CounterVec\n\tmessagesReceivedSize *prometheus.CounterVec\n\tmessagesSent         *prometheus.CounterVec\n\tmessagesSentSize     *prometheus.CounterVec\n\tmessagesPruned       prometheus.Counter\n\tnodeAlive            *prometheus.CounterVec\n\tnodePingDuration     *prometheus.HistogramVec\n\tconflictsCount       prometheus.Counter\n}\n\nfunc newDelegate(l *slog.Logger, reg prometheus.Registerer, p *Peer, retransmit int) *delegate {\n\tbcast := &memberlist.TransmitLimitedQueue{\n\t\tNumNodes:       p.ClusterSize,\n\t\tRetransmitMult: retransmit,\n\t}\n\tmessagesReceived := promauto.With(reg).NewCounterVec(prometheus.CounterOpts{\n\t\tName: \"alertmanager_cluster_messages_received_total\",\n\t\tHelp: \"Total number of cluster messages received.\",\n\t}, []string{\"msg_type\"})\n\tmessagesReceivedSize := promauto.With(reg).NewCounterVec(prometheus.CounterOpts{\n\t\tName: \"alertmanager_cluster_messages_received_size_total\",\n\t\tHelp: \"Total size of cluster messages received.\",\n\t}, []string{\"msg_type\"})\n\tmessagesSent := promauto.With(reg).NewCounterVec(prometheus.CounterOpts{\n\t\tName: \"alertmanager_cluster_messages_sent_total\",\n\t\tHelp: \"Total number of cluster messages sent.\",\n\t}, []string{\"msg_type\"})\n\tmessagesSentSize := promauto.With(reg).NewCounterVec(prometheus.CounterOpts{\n\t\tName: \"alertmanager_cluster_messages_sent_size_total\",\n\t\tHelp: \"Total size of cluster messages sent.\",\n\t}, []string{\"msg_type\"})\n\tmessagesPruned := promauto.With(reg).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_cluster_messages_pruned_total\",\n\t\tHelp: \"Total number of cluster messages pruned.\",\n\t})\n\tpromauto.With(reg).NewGaugeFunc(prometheus.GaugeOpts{\n\t\tName: \"alertmanager_cluster_members\",\n\t\tHelp: \"Number indicating current number of members in cluster.\",\n\t}, func() float64 {\n\t\treturn float64(p.ClusterSize())\n\t})\n\tpromauto.With(reg).NewGaugeFunc(prometheus.GaugeOpts{\n\t\tName: \"alertmanager_peer_position\",\n\t\tHelp: \"Position the Alertmanager instance believes it's in. The position determines a peer's behavior in the cluster.\",\n\t}, func() float64 {\n\t\treturn float64(p.Position())\n\t})\n\tpromauto.With(reg).NewGaugeFunc(prometheus.GaugeOpts{\n\t\tName: \"alertmanager_cluster_health_score\",\n\t\tHelp: \"Health score of the cluster. Lower values are better and zero means 'totally healthy'.\",\n\t}, func() float64 {\n\t\treturn float64(p.mlist.GetHealthScore())\n\t})\n\tpromauto.With(reg).NewGaugeFunc(prometheus.GaugeOpts{\n\t\tName: \"alertmanager_cluster_messages_queued\",\n\t\tHelp: \"Number of cluster messages which are queued.\",\n\t}, func() float64 {\n\t\treturn float64(bcast.NumQueued())\n\t})\n\tnodeAlive := promauto.With(reg).NewCounterVec(prometheus.CounterOpts{\n\t\tName: \"alertmanager_cluster_alive_messages_total\",\n\t\tHelp: \"Total number of received alive messages.\",\n\t}, []string{\"peer\"},\n\t)\n\tnodePingDuration := promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{\n\t\tName:                            \"alertmanager_cluster_pings_seconds\",\n\t\tHelp:                            \"Histogram of latencies for ping messages.\",\n\t\tBuckets:                         []float64{.005, .01, .025, .05, .1, .25, .5},\n\t\tNativeHistogramBucketFactor:     1.1,\n\t\tNativeHistogramMaxBucketNumber:  100,\n\t\tNativeHistogramMinResetDuration: 1 * time.Hour,\n\t}, []string{\"peer\"},\n\t)\n\tconflictsCount := promauto.With(reg).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_cluster_peer_name_conflicts_total\",\n\t\tHelp: \"Total number of times memberlist has noticed conflicting peer names\",\n\t})\n\n\tmessagesReceived.WithLabelValues(fullState)\n\tmessagesReceivedSize.WithLabelValues(fullState)\n\tmessagesReceived.WithLabelValues(update)\n\tmessagesReceivedSize.WithLabelValues(update)\n\tmessagesSent.WithLabelValues(fullState)\n\tmessagesSentSize.WithLabelValues(fullState)\n\tmessagesSent.WithLabelValues(update)\n\tmessagesSentSize.WithLabelValues(update)\n\n\td := &delegate{\n\t\tlogger:               l,\n\t\tPeer:                 p,\n\t\tbcast:                bcast,\n\t\tmessagesReceived:     messagesReceived,\n\t\tmessagesReceivedSize: messagesReceivedSize,\n\t\tmessagesSent:         messagesSent,\n\t\tmessagesSentSize:     messagesSentSize,\n\t\tmessagesPruned:       messagesPruned,\n\t\tnodeAlive:            nodeAlive,\n\t\tnodePingDuration:     nodePingDuration,\n\t\tconflictsCount:       conflictsCount,\n\t}\n\n\tgo d.handleQueueDepth()\n\n\treturn d\n}\n\n// NodeMeta retrieves meta-data about the current node when broadcasting an alive message.\nfunc (d *delegate) NodeMeta(limit int) []byte {\n\treturn []byte{}\n}\n\n// NotifyMsg is the callback invoked when a user-level gossip message is received.\nfunc (d *delegate) NotifyMsg(b []byte) {\n\td.messagesReceived.WithLabelValues(update).Inc()\n\td.messagesReceivedSize.WithLabelValues(update).Add(float64(len(b)))\n\n\tvar p clusterpb.Part\n\tif err := proto.Unmarshal(b, &p); err != nil {\n\t\td.logger.Warn(\"decode broadcast\", \"err\", err)\n\t\treturn\n\t}\n\n\td.mtx.RLock()\n\ts, ok := d.states[p.Key]\n\td.mtx.RUnlock()\n\n\tif !ok {\n\t\treturn\n\t}\n\tif err := s.Merge(p.Data); err != nil {\n\t\td.logger.Warn(\"merge broadcast\", \"err\", err, \"key\", p.Key)\n\t\treturn\n\t}\n}\n\n// NotifyConflict is the callback when memberlist encounters two nodes with the same ID.\nfunc (d *delegate) NotifyConflict(existing, other *memberlist.Node) {\n\td.logger.Warn(\"Found conflicting peer IDs\", \"peer\", existing.Name)\n\td.conflictsCount.Inc()\n}\n\n// GetBroadcasts is called when user data messages can be broadcasted.\nfunc (d *delegate) GetBroadcasts(overhead, limit int) [][]byte {\n\tmsgs := d.bcast.GetBroadcasts(overhead, limit)\n\td.messagesSent.WithLabelValues(update).Add(float64(len(msgs)))\n\tfor _, m := range msgs {\n\t\td.messagesSentSize.WithLabelValues(update).Add(float64(len(m)))\n\t}\n\treturn msgs\n}\n\n// LocalState is called when gossip fetches local state.\nfunc (d *delegate) LocalState(_ bool) []byte {\n\td.mtx.RLock()\n\tdefer d.mtx.RUnlock()\n\tall := &clusterpb.FullState{\n\t\tParts: make([]*clusterpb.Part, 0, len(d.states)),\n\t}\n\n\tfor key, s := range d.states {\n\t\tb, err := s.MarshalBinary()\n\t\tif err != nil {\n\t\t\td.logger.Warn(\"encode local state\", \"err\", err, \"key\", key)\n\t\t\treturn nil\n\t\t}\n\t\tall.Parts = append(all.Parts, &clusterpb.Part{Key: key, Data: b})\n\t}\n\tb, err := proto.Marshal(all)\n\tif err != nil {\n\t\td.logger.Warn(\"encode local state\", \"err\", err)\n\t\treturn nil\n\t}\n\td.messagesSent.WithLabelValues(fullState).Inc()\n\td.messagesSentSize.WithLabelValues(fullState).Add(float64(len(b)))\n\treturn b\n}\n\nfunc (d *delegate) MergeRemoteState(buf []byte, _ bool) {\n\td.messagesReceived.WithLabelValues(fullState).Inc()\n\td.messagesReceivedSize.WithLabelValues(fullState).Add(float64(len(buf)))\n\n\tvar fs clusterpb.FullState\n\tif err := proto.Unmarshal(buf, &fs); err != nil {\n\t\td.logger.Warn(\"merge remote state\", \"err\", err)\n\t\treturn\n\t}\n\td.mtx.RLock()\n\tdefer d.mtx.RUnlock()\n\tfor _, p := range fs.Parts {\n\t\ts, ok := d.states[p.Key]\n\t\tif !ok {\n\t\t\td.logger.Warn(\"unknown state key\", \"len\", len(buf), \"key\", p.Key)\n\t\t\tcontinue\n\t\t}\n\t\tif err := s.Merge(p.Data); err != nil {\n\t\t\td.logger.Warn(\"merge remote state\", \"err\", err, \"key\", p.Key)\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// NotifyJoin is called if a peer joins the cluster.\nfunc (d *delegate) NotifyJoin(n *memberlist.Node) {\n\td.logger.Debug(\"NotifyJoin\", \"node\", n.Name, \"addr\", n.Address())\n\td.peerJoin(n)\n}\n\n// NotifyLeave is called if a peer leaves the cluster.\nfunc (d *delegate) NotifyLeave(n *memberlist.Node) {\n\td.logger.Debug(\"NotifyLeave\", \"node\", n.Name, \"addr\", n.Address())\n\td.peerLeave(n)\n}\n\n// NotifyUpdate is called if a cluster peer gets updated.\nfunc (d *delegate) NotifyUpdate(n *memberlist.Node) {\n\td.logger.Debug(\"NotifyUpdate\", \"node\", n.Name, \"addr\", n.Address())\n\td.peerUpdate(n)\n}\n\n// NotifyAlive implements the memberlist.AliveDelegate interface.\nfunc (d *delegate) NotifyAlive(peer *memberlist.Node) error {\n\td.nodeAlive.WithLabelValues(peer.Name).Inc()\n\treturn nil\n}\n\n// AckPayload implements the memberlist.PingDelegate interface.\nfunc (d *delegate) AckPayload() []byte {\n\treturn []byte{}\n}\n\n// NotifyPingComplete implements the memberlist.PingDelegate interface.\nfunc (d *delegate) NotifyPingComplete(peer *memberlist.Node, rtt time.Duration, payload []byte) {\n\td.nodePingDuration.WithLabelValues(peer.Name).Observe(rtt.Seconds())\n}\n\n// handleQueueDepth ensures that the queue doesn't grow unbounded by pruning\n// older messages at regular interval.\nfunc (d *delegate) handleQueueDepth() {\n\tfor {\n\t\tselect {\n\t\tcase <-d.stopc:\n\t\t\treturn\n\t\tcase <-time.After(15 * time.Minute):\n\t\t\tn := d.bcast.NumQueued()\n\t\t\tif n > maxQueueSize {\n\t\t\t\td.logger.Warn(\"dropping messages because too many are queued\", \"current\", n, \"limit\", maxQueueSize)\n\t\t\t\td.bcast.Prune(maxQueueSize)\n\t\t\t\td.messagesPruned.Add(float64(n - maxQueueSize))\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "cluster/testdata/certs/ca-config.json",
    "content": "{\n  \"signing\": {\n    \"default\": {\n      \"expiry\": \"876000h\"\n    },\n    \"profiles\": {\n      \"massl\": {\n        \"usages\": [\"signing\", \"key encipherment\", \"server auth\", \"client auth\"],\n        \"expiry\": \"876000h\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "cluster/testdata/certs/ca-csr.json",
    "content": "{\n  \"CN\": \"massl\",\n  \"key\": {\n    \"algo\": \"rsa\",\n    \"size\": 2048\n  },\n  \"names\": [\n    {\n      \"C\": \"AU\",\n      \"L\": \"Melbourne\",\n      \"O\": \"massl\",\n      \"OU\": \"VIC\",\n      \"ST\": \"Victoria\"\n    }\n  ]\n}"
  },
  {
    "path": "cluster/testdata/certs/ca-key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAuljDjKVGwlyiuKTSHc1QpoZPX9dbgwU/9113ctI8U/ZwMWLp\nnZ4f/zVpf4LW5foM9zSEUGPiyJe/NaTZUOXkRBSIQ13QroK4OJ1XGacQKpTxewCb\nChESZEfKWEhnP/Y7BYc4z1Li6Dkxh4TIElHwOVe62jbhNnzYlr4evmSuiuItAc8u\nhEYxncThPzmHEWPXKw8CFNhxCSYsjbb72UAIht0knMHQ7VXBX1VuuL0rolJBiToC\nva+I6CjG0c6qfi9/BcPsuW6cNjmQnwTg6SaSoGO/5zgbxBgy9MZQEot88d1T2XH6\nrBANYsfojvyCXuytWnj04mvdAWwmFh0hhq+nxQIDAQABAoIBAQCwcL1vXUq7W4UD\nOaRtbWrQ0dk0ETBnxT/E0y33fRJ8GZovWM2EXSVEuukSP+uEQ5elNYeWqo0fi3cT\nruvJSnMw9xPyXVDq+4C8slW3R1TqTK683VzvUizM4KC5qIyCpn1KBbgHrh6E7Sp1\ne4cIuaawVN3qIg5qThmx2YA4nBIcEt68q9cpy3NgEe+EQf44zM/St+y8kSkDUOVw\nfNKX0WfZ/hPL1TAYpWiIgSf+m/V3d/1l/scvMYONcuSjXSORCyoeAWYtOQgf78wW\n9j3kiBTaqDYCUZFnY/ltlZrm8ltAaKVJ0MmPKjVh8GJBXZp9fSVU8Y4ZIZRSeuBA\nOoStHGAdAoGBAMluMIE33hGny2V0dNzW23D84eXQK38AsdP632jQeuzxBknItg45\nqAfzh8F8W10DQnSv5tj0bmOHfo0mG09bu9eo5nLLINOE7Ju/7ly/76RNJNJ4ADjx\nJKZi/PpvfP+s/fzel0X3OPgA+CJKzUHuqlU4V9BLc7focZAYtaM2w7rHAoGBAOzU\neXpapkqYhbYRcsrVV57nZV0rLzsLVJBpJg2zC8un95ALrr0rlZfuPJfOCY/uuS1w\nf8ixRz2MkRWGreLHy35NB4GV0sF9VPn1jMp9SuBNvO0JRUMWuDAdVe8SCjXadrOh\n+m3yKJSkFKDchglUYnZKV1skgA/b9jjjnu2fvd0TAoGAVUTnFZxvzmuIp78fxWjS\n5ka23hE8iHvjy4e00WsHzovNjKiBoQ35Orx16ItbJcm+dSUNhSQcItf104yhHPwJ\nTab7PvcMQ15OxzP9lJfPu27Iuqv/9Bro1+Kpkt5lPNqffk9AHGcmX54RbHrb3yBI\nTOEYE14Nc3nbsRM0uQ3y13sCgYB5Om4QZpSWvKo9P4M+NqTKb3JglblwhOU9osVa\n39ra3dkIgCJrLQM/KTEVF9+nMLDThLG0fqKT6/9cQHuECXet6Co+d/3RE6HK7Zmr\nESWh2ckqoMM2i0uvPWT+ooJdfL2kR/bUDtAc/jyc9yUZY3ufR4Cd4/o1pAfOqR1y\nT4G1xwKBgQChE4VWawCVg2qanRjvZcdNk0zpZx4dxqqKYq/VHuSfjNLQixIZsgXT\nxx9BHuORn6c/nurqEStLwN3BzbpPU/j6YjMUmTslSH2sKhHwWNYGBZC52aJiOOda\nBz6nAkihG0n2PjYt2T84w6FWHgLJuSsmiEVJcb+AOdyKh1MlzJiwMQ==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "cluster/testdata/certs/ca.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIICpzCCAY8CAQAwYjELMAkGA1UEBhMCQVUxETAPBgNVBAgTCFZpY3RvcmlhMRIw\nEAYDVQQHEwlNZWxib3VybmUxDjAMBgNVBAoTBW1hc3NsMQwwCgYDVQQLEwNWSUMx\nDjAMBgNVBAMTBW1hc3NsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\nuljDjKVGwlyiuKTSHc1QpoZPX9dbgwU/9113ctI8U/ZwMWLpnZ4f/zVpf4LW5foM\n9zSEUGPiyJe/NaTZUOXkRBSIQ13QroK4OJ1XGacQKpTxewCbChESZEfKWEhnP/Y7\nBYc4z1Li6Dkxh4TIElHwOVe62jbhNnzYlr4evmSuiuItAc8uhEYxncThPzmHEWPX\nKw8CFNhxCSYsjbb72UAIht0knMHQ7VXBX1VuuL0rolJBiToCva+I6CjG0c6qfi9/\nBcPsuW6cNjmQnwTg6SaSoGO/5zgbxBgy9MZQEot88d1T2XH6rBANYsfojvyCXuyt\nWnj04mvdAWwmFh0hhq+nxQIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAJFmooMt\nTocElxCb3DGJTRUXxr4DqcATASIX35a2wV3MmPqUHHXr6BQkO/FRho66EsZf3DE/\nmumou01K+KByxgsmw04CACjSeZ2t/g6pAsDCKrx/BwL3tAo09lG2Y2Ah0BND2Cta\nEZpTliU2MimZlk7UZb8VIXh2Tx56fZRoHLzO4U4+FY8ZR+tspxPRM7hLg/aUqA5D\nzGj6kByX8aYjxsmQokP4rx/w2mz6vwt4cZ1pXwr0RderkMIh9Har/0k9X1WIAP61\nPNQx74qnaq+icjtN2+8gvJE/CJL/wfcwW6kQwEtX1xsTpnzyFaRoYpSPQrvkCtiW\n+WzgnOh7RvKyAYI=\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "cluster/testdata/certs/ca.pem",
    "content": "Certificate:\n    Data:\n        Version: 3 (0x2)\n        Serial Number:\n            7a:d7:1c:f3:22:da:b1:20:31:bf:25:16:b6:04:d5:29:1e:a3:7c:12\n        Signature Algorithm: sha256WithRSAEncryption\n        Issuer: C=AU, ST=Victoria, L=Melbourne, O=massl, OU=VIC, CN=massl\n        Validity\n            Not Before: Nov  6 22:02:17 2024 GMT\n            Not After : Nov  1 22:02:17 2044 GMT\n        Subject: C=AU, ST=Victoria, L=Melbourne, O=massl, OU=VIC, CN=massl\n        Subject Public Key Info:\n            Public Key Algorithm: rsaEncryption\n                Public-Key: (2048 bit)\n                Modulus:\n                    00:ba:58:c3:8c:a5:46:c2:5c:a2:b8:a4:d2:1d:cd:\n                    50:a6:86:4f:5f:d7:5b:83:05:3f:f7:5d:77:72:d2:\n                    3c:53:f6:70:31:62:e9:9d:9e:1f:ff:35:69:7f:82:\n                    d6:e5:fa:0c:f7:34:84:50:63:e2:c8:97:bf:35:a4:\n                    d9:50:e5:e4:44:14:88:43:5d:d0:ae:82:b8:38:9d:\n                    57:19:a7:10:2a:94:f1:7b:00:9b:0a:11:12:64:47:\n                    ca:58:48:67:3f:f6:3b:05:87:38:cf:52:e2:e8:39:\n                    31:87:84:c8:12:51:f0:39:57:ba:da:36:e1:36:7c:\n                    d8:96:be:1e:be:64:ae:8a:e2:2d:01:cf:2e:84:46:\n                    31:9d:c4:e1:3f:39:87:11:63:d7:2b:0f:02:14:d8:\n                    71:09:26:2c:8d:b6:fb:d9:40:08:86:dd:24:9c:c1:\n                    d0:ed:55:c1:5f:55:6e:b8:bd:2b:a2:52:41:89:3a:\n                    02:bd:af:88:e8:28:c6:d1:ce:aa:7e:2f:7f:05:c3:\n                    ec:b9:6e:9c:36:39:90:9f:04:e0:e9:26:92:a0:63:\n                    bf:e7:38:1b:c4:18:32:f4:c6:50:12:8b:7c:f1:dd:\n                    53:d9:71:fa:ac:10:0d:62:c7:e8:8e:fc:82:5e:ec:\n                    ad:5a:78:f4:e2:6b:dd:01:6c:26:16:1d:21:86:af:\n                    a7:c5\n                Exponent: 65537 (0x10001)\n        X509v3 extensions:\n            X509v3 Key Usage: critical\n                Certificate Sign, CRL Sign\n            X509v3 Basic Constraints: critical\n                CA:TRUE\n            X509v3 Subject Key Identifier:\n                77:80:D3:12:52:AA:EA:09:C6:60:32:59:80:9B:C2:FB:87:E5:AD:90\n    Signature Algorithm: sha256WithRSAEncryption\n    Signature Value:\n        92:f2:a4:8f:7d:04:f1:7e:08:b0:6b:3e:0c:b9:88:29:18:b6:\n        ce:88:4e:84:b0:10:8b:ca:b5:d6:6a:fb:12:52:14:f2:4e:01:\n        bb:b3:8b:a0:b4:65:d9:fd:d4:c7:6b:44:54:3a:e5:5b:c9:0e:\n        bd:3c:3b:f7:41:0a:67:1d:5a:21:32:7c:42:3b:b1:37:b4:c0:\n        78:07:4b:ae:e2:18:77:90:85:33:70:46:20:61:1a:7a:67:38:\n        0a:cf:fc:1c:bd:d2:c6:1a:0e:09:5a:d5:36:74:8a:8e:66:0f:\n        1f:47:69:7a:17:a7:d3:bf:74:40:85:3f:80:a2:53:00:2a:65:\n        3c:3f:ca:44:d9:ec:71:cf:17:4e:3d:b0:1e:5e:e8:73:ab:0a:\n        27:95:02:88:2b:b0:46:9a:4d:a4:7d:05:ba:df:4c:e5:65:d3:\n        2b:12:fd:17:74:51:f2:bb:d1:0e:32:8c:e9:ee:42:5c:d7:3c:\n        85:60:f0:1a:52:fc:11:31:e1:12:8c:c9:a0:1f:1f:52:7e:d9:\n        1e:a0:c7:f7:48:05:9d:dc:f5:c1:59:5a:9b:e7:bd:a3:37:54:\n        8a:42:c7:10:d7:51:19:99:e2:e7:d3:56:66:18:4a:d0:d1:f6:\n        25:1d:c9:f9:48:60:43:cc:6f:9c:ba:95:03:3e:a0:5a:ad:26:\n        d8:ce:4c:4a\n-----BEGIN CERTIFICATE-----\nMIIDlDCCAnygAwIBAgIUetcc8yLasSAxvyUWtgTVKR6jfBIwDQYJKoZIhvcNAQEL\nBQAwYjELMAkGA1UEBhMCQVUxETAPBgNVBAgTCFZpY3RvcmlhMRIwEAYDVQQHEwlN\nZWxib3VybmUxDjAMBgNVBAoTBW1hc3NsMQwwCgYDVQQLEwNWSUMxDjAMBgNVBAMT\nBW1hc3NsMB4XDTI0MTEwNjIyMDIxN1oXDTQ0MTEwMTIyMDIxN1owYjELMAkGA1UE\nBhMCQVUxETAPBgNVBAgTCFZpY3RvcmlhMRIwEAYDVQQHEwlNZWxib3VybmUxDjAM\nBgNVBAoTBW1hc3NsMQwwCgYDVQQLEwNWSUMxDjAMBgNVBAMTBW1hc3NsMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuljDjKVGwlyiuKTSHc1QpoZPX9db\ngwU/9113ctI8U/ZwMWLpnZ4f/zVpf4LW5foM9zSEUGPiyJe/NaTZUOXkRBSIQ13Q\nroK4OJ1XGacQKpTxewCbChESZEfKWEhnP/Y7BYc4z1Li6Dkxh4TIElHwOVe62jbh\nNnzYlr4evmSuiuItAc8uhEYxncThPzmHEWPXKw8CFNhxCSYsjbb72UAIht0knMHQ\n7VXBX1VuuL0rolJBiToCva+I6CjG0c6qfi9/BcPsuW6cNjmQnwTg6SaSoGO/5zgb\nxBgy9MZQEot88d1T2XH6rBANYsfojvyCXuytWnj04mvdAWwmFh0hhq+nxQIDAQAB\no0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU\nd4DTElKq6gnGYDJZgJvC+4flrZAwDQYJKoZIhvcNAQELBQADggEBAJLypI99BPF+\nCLBrPgy5iCkYts6IToSwEIvKtdZq+xJSFPJOAbuzi6C0Zdn91MdrRFQ65VvJDr08\nO/dBCmcdWiEyfEI7sTe0wHgHS67iGHeQhTNwRiBhGnpnOArP/By90sYaDgla1TZ0\nio5mDx9HaXoXp9O/dECFP4CiUwAqZTw/ykTZ7HHPF049sB5e6HOrCieVAogrsEaa\nTaR9BbrfTOVl0ysS/Rd0UfK70Q4yjOnuQlzXPIVg8BpS/BEx4RKMyaAfH1J+2R6g\nx/dIBZ3c9cFZWpvnvaM3VIpCxxDXURmZ4ufTVmYYStDR9iUdyflIYEPMb5y6lQM+\noFqtJtjOTEo=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "cluster/testdata/certs/node1-csr.json",
    "content": "{\n  \"CN\": \"system:server\",\n  \"key\": {\n    \"algo\": \"rsa\",\n    \"size\": 2048\n  },\n  \"names\": [\n    {\n      \"C\": \"AU\",\n      \"L\": \"Melbourne\",\n      \"O\": \"system:node1\",\n      \"OU\": \"massl\",\n      \"ST\": \"Victoria\"\n    }\n  ]\n}\n"
  },
  {
    "path": "cluster/testdata/certs/node1-key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA1b9bm4rvDtpYsqgtCC52+L535d4/Q2O10fWD2i2CfRXXfYJQ\n5cr4AV2iqScFsJSs7KwyQde/c4VWj/vEA2/SJHZFBlknKdCcrgHVebrvnzm6Guze\nICutZSKocFXy9Kw+YmWuA64nHVfmSCKG07GhXhEsLsSCn4PTDYOiGAUm1GdSDxUp\n8yUXec13Eb20mld0xE9kQnCnEWRnxMXtQJXoz9lpLc7DgXtN6nCXSG/CqdDPOduU\nnmseaxyAGpAFnUmxcqUuYAJUQ1hUOJhk0RVSsLTmu+FGdOxk79AxmmKQ2z9l/GuA\nVikVJGTxY4jRPezxHQ3bdqzzCIdJxTxLinftZQIDAQABAoIBADpxQtvphemau8vF\nfeKRycfDVEcOmF+VoL4SkgWSke4fjbbsbbAW6e59qp7zY3PfgtSHVIp6Mgek+oEN\nxo9mAKAlkkPlFncxadWN/M921FPF1ePMxgMnzhYr/sAQUAikG76NrKGm+VzljrpE\nbnbtR4DP0zPKWSjCQ2+bgTNuHSrPwUtEngVT6ugjfWU1RitlvjTsZ9hSuOSBlS7P\nrjbQGaEh53PraDut8PIlF4wIF+nLeERFP/a6DC8Btpbv9P50YRosag6yU/G+OYX9\nspvBPvRJGrubslKnNRz9AcjbVd3QhL+Tm7mV7iakK918jLWb95Ro4WW+9lT6IAi6\nxRSOr9UCgYEA5wI3JhKkYa4PST7ALqmJSDkPH8+tctiEx+ovmnqBufFuLWFoO/rc\nEOYslnaZM3UVCnhrFv7+LxezSI5DyQu8dBEzf0RMICvXUNBkGC7ZJQL428fjXPhX\n8mZIoJ0ol4hbamr8yTYlK0vGTwqN1bDj71w6NszuN4ecN1cKNWsMbnMCgYEA7N8Y\nMzHWNijMr7xZ1lXl4Ye9+kp3aTUjUYBBaxFr4bQ8y0fH48lzq3qOso5wgvp0DKYo\nuemD5QKbo81LKoTRLa+nxPq0UqKm9FiSWmnrcxMuph989oZ1ZFHA2B+nvbuMTF8J\n8sESclTSbgkG87DpycJOUwG3XAcXM+80pXuzJscCgYB+Dzxu/09KqoRW8PJIxGVQ\nzypMrrS07iiPO2FcyCtQf8oi43vQ91Ttt91vAisZ5HNl8k5mDyJAKovANToSVOAy\n6kwSz/9GswXdaMqmU7JVOyj4Lj0JN9AuS9ioJPrIrjVMfjORzYU8+i2uZlD94niP\n3uE5lF0OWmdJ36qHefIftwKBgQDcPQZcO19H1iGS2FbTYeSvEK5ENM7YRG8FTXIF\n4hnjrtjDzYb+tYVWEErznFrifYo/ZJMDYSqgWQ9reusDqqBvkR41mUDmgJMpJ91U\nMZ2YzmIWVbqz4QrvbtAWY0Bsuh/VtpwiWQAUy+coJj6PgJOvY3m91h+tcm5RfHz/\nzIcjawKBgA6kDcOLOnWcvhP3XwtW5dcWlNuBBNEKna+kIT/5Vlrh91y2w7F54DNK\ni0w5CZCpbTugJmZ67XLHnfongC7e2vAQ3atoT96RU4mf9614qs9LMtGAbnuCLB8+\nsT2rnaZKtzr83ensbYkbBxP/zmPBfFQ9FKcIYIA7En8zAIr2T3vJ\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "cluster/testdata/certs/node1.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIC5TCCAc0CAQAwczELMAkGA1UEBhMCQVUxETAPBgNVBAgTCFZpY3RvcmlhMRIw\nEAYDVQQHEwlNZWxib3VybmUxFTATBgNVBAoTDHN5c3RlbTpub2RlMTEOMAwGA1UE\nCxMFbWFzc2wxFjAUBgNVBAMTDXN5c3RlbTpzZXJ2ZXIwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQDVv1ubiu8O2liyqC0ILnb4vnfl3j9DY7XR9YPaLYJ9\nFdd9glDlyvgBXaKpJwWwlKzsrDJB179zhVaP+8QDb9IkdkUGWScp0JyuAdV5uu+f\nOboa7N4gK61lIqhwVfL0rD5iZa4DricdV+ZIIobTsaFeESwuxIKfg9MNg6IYBSbU\nZ1IPFSnzJRd5zXcRvbSaV3TET2RCcKcRZGfExe1AlejP2WktzsOBe03qcJdIb8Kp\n0M8525Seax5rHIAakAWdSbFypS5gAlRDWFQ4mGTRFVKwtOa74UZ07GTv0DGaYpDb\nP2X8a4BWKRUkZPFjiNE97PEdDdt2rPMIh0nFPEuKd+1lAgMBAAGgLTArBgkqhkiG\n9w0BCQ4xHjAcMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0B\nAQsFAAOCAQEAW/tTyJaBfWtbC9hYUmhh8lxUztv2+WT4xaR/jdQ46sk/87vKuwI6\n4AkkGfiPLLqgW3xbQOwk5/ynRabttbsgTUHt744RtRFLzfcQKEBZoNPvrfHvmDil\nYqHIOx2SJ5hzIBwVlVSBn50hdSSED1Ip22DaU8GukzuacB8+2rhg3MOWJbKVt5aR\n03H4XkAynLS1FHNOraDIv1eT58D3l4hanrNOZIa0xAuChd25qLO/JHvU/3wccGUA\nKNg3vGOy2Q8qVBrTFLn+yQHuOr/wSupXESO1jiI/h+txsBQnZ6oYfZnVJ+7o3Oln\n3Hguw77aYeTAeZQPPbmJbDLegLG0ZC6RmA==\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "cluster/testdata/certs/node1.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEAjCCAuqgAwIBAgIUbYMGwSgQF8iRZ5xmhflInj8VZ0owDQYJKoZIhvcNAQEL\nBQAwYjELMAkGA1UEBhMCQVUxETAPBgNVBAgTCFZpY3RvcmlhMRIwEAYDVQQHEwlN\nZWxib3VybmUxDjAMBgNVBAoTBW1hc3NsMQwwCgYDVQQLEwNWSUMxDjAMBgNVBAMT\nBW1hc3NsMCAXDTIxMDUwNTE2MTYwMFoYDzIxMjEwNDExMTYxNjAwWjBzMQswCQYD\nVQQGEwJBVTERMA8GA1UECBMIVmljdG9yaWExEjAQBgNVBAcTCU1lbGJvdXJuZTEV\nMBMGA1UEChMMc3lzdGVtOm5vZGUxMQ4wDAYDVQQLEwVtYXNzbDEWMBQGA1UEAxMN\nc3lzdGVtOnNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANW/\nW5uK7w7aWLKoLQgudvi+d+XeP0NjtdH1g9otgn0V132CUOXK+AFdoqknBbCUrOys\nMkHXv3OFVo/7xANv0iR2RQZZJynQnK4B1Xm67585uhrs3iArrWUiqHBV8vSsPmJl\nrgOuJx1X5kgihtOxoV4RLC7Egp+D0w2DohgFJtRnUg8VKfMlF3nNdxG9tJpXdMRP\nZEJwpxFkZ8TF7UCV6M/ZaS3Ow4F7Tepwl0hvwqnQzznblJ5rHmscgBqQBZ1JsXKl\nLmACVENYVDiYZNEVUrC05rvhRnTsZO/QMZpikNs/ZfxrgFYpFSRk8WOI0T3s8R0N\n23as8wiHScU8S4p37WUCAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0l\nBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYE\nFGprx5v+KrO4DeOtA6kps4BL/zKyMB8GA1UdIwQYMBaAFHeA0xJSquoJxmAyWYCb\nwvuH5a2QMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0BAQsF\nAAOCAQEAmWTdMLyWOrNAS0uY+u3FUV3Hm50xF1PfxbT6wK1hu6vH6B63E0o9K2/1\nU25Ie8Y2IzFocKMvbqC+mrY56G0bWoUlMONhthYqm8uTKtjlFO33A9I7WIT9Tw+B\nnnwZZO7+Ljkd30qSzBinCjrIEx31Vq2pr54ungd8+wK8nfz/zdZnJcqxcN9zvCXB\nGTE8yCuqGWKk/oDuIzVjr73U0QaWi+vThqJtBjhOIWQHHVJwbIyhuYzUaivgZPYB\n8eKXWk4JH3eAcq5z5koNGyCcZd/k4WnvxZYxNBAkoQ6AWVfEMGOCaRjD1FTnMbpG\nBW79ndJqLmn8OH+DeCnSWhTWxAgg+Q==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "cluster/testdata/certs/node2-csr.json",
    "content": "{\n  \"CN\": \"system:server\",\n  \"key\": {\n    \"algo\": \"rsa\",\n    \"size\": 2048\n  },\n  \"names\": [\n    {\n      \"C\": \"AU\",\n      \"L\": \"Melbourne\",\n      \"O\": \"system:node2\",\n      \"OU\": \"massl\",\n      \"ST\": \"Victoria\"\n    }\n  ]\n}\n"
  },
  {
    "path": "cluster/testdata/certs/node2-key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAtCtzT9vhRMTbhAg/pm8eBn+4IvVQeVqnHoEon9IKIx5fyvqS\nQ6Ui3xSik9kJq5FSAa1mScajJwfB1o6ycaSP6n+Q88Py4v7q65n0stCHoJCH0uPw\nMQyEhwX7nNilV9C4UZTyZ2StDdAjmMBHiN81EJAqH2d4Xtgrd/IIWhljSXm+aPbu\nQjSz8BtR/7+MswrCdlJ8y6gWi020kt6GSHjmaxI1jStGvBxxksK86v3J97wfNwWY\n7GJi70uBrvO0pk5bYckDzUTKeN1QGvBnZ8uDXs7pPysvftJr85GzX0iE9YLMDxO3\nqc/PlwCdxM8H6gHTTkLPizGZtpMF9Z497pW9YQIDAQABAoIBAFfQwdCPxHmnVbNB\n7fwqNsFGKTLozMOJeuE0ZN+ZGZXKbTha70WHTLrcrO1RIRR9rTHiGXQmHEmez0zL\nmpAnfHn4mWcm/9DCHTCehpVNbH3HVFxm+yB9EG9bbCsjsVtfASfKaGgauvp7k44V\nUgiVeqDLE6zg2tunk3BQCOAZdbpOiXrdvoZiGx2Q4SMLPfzmfIyH4BUT836pLTmp\no6/yNiFqQWfCgjeEAOQor4TcdzYIT+3wP51HfAjhZKMIvmjwL16ov1/QpmWRD4ni\n4svzYpeMYpl5OrZkKeDS4ZIQBGjxk+fzPmfFUbfVRSI2gDORsah8HoRVI4LnwKWn\n7kQDv0ECgYEA6V+KVb8bPzCZNbroEZFdug6YtT4yv5Mj3/kpMTIvA3vtu02v8e7F\nO56yT43QfUZA0Ar37O0HQ6mbpPsRE5RSr70i40RR+slMZVHX/AQViG7oQJGBijPt\n1tFdLnb+1wSON3jYt2975Kw2IfgOXprWtEmL5zGuplEUjx9Lbdf1HjkCgYEAxaNe\nXgXdAiWFoY4Qq6xBRO/WNZCdn3Ysqx6snCtDRilxeNyDoE/6x2Ma9/NRBtIiulAb\ns09vDRfJKLbzocUhIn8BQ+GkbAS/A6+x2vcuGhK3F84xqZdbrCqvqdJS8K824jug\nvUCfCBJlyNRDz8kEsN5odLM1xkij93Jv23HvGGkCgYEAptcz6ctfalSPI9eEs5KO\nREbNK73UwBssaaISreYnsED4G5EVuUuvW8k/xxomtHj2OwWsa4ilSd1GtbL8aVf/\nqT35ZCrixP0GjeTuGXC+CDTp+8dKqggoAAzbpi1SUVwjZEsT/EhKdZgcdzqE42Ol\nHWz7BQUCzEpo/U0tOtFKnxkCgYEAi05Vy8wyNbsg7/jlAzyNXPv4bxUaJTX00kDy\nxbkw2BmKI/i6xprZVwUiEzdsG3SuicjBXahVzFLBtXMPUy1R57DBwYkgjgriYMTM\nhlzIIBSk/aCXHMTVFwuXegoH8CJwexIwgHU2I0hkeiQ0EBfOuKRr2CYhdzvoZxhA\ng9tQ/lECgYAjPYoXfNI3rHCWUmaD5eDJZpE0xuJeiiy5auojykdAc7vVapNaIyMK\nG3EaU44RtXcSwH19TlH9UCm3MH1QiIwaBOzGcKj3Ut6ZyFKuWDUk4yqvps3uZU/h\nh16Tp49Ja7/4LY1uuEngg1KMEiWgk5jiU7G0H9zrtEiTj9c3FDKDvg==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "cluster/testdata/certs/node2.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIC5TCCAc0CAQAwczELMAkGA1UEBhMCQVUxETAPBgNVBAgTCFZpY3RvcmlhMRIw\nEAYDVQQHEwlNZWxib3VybmUxFTATBgNVBAoTDHN5c3RlbTpub2RlMjEOMAwGA1UE\nCxMFbWFzc2wxFjAUBgNVBAMTDXN5c3RlbTpzZXJ2ZXIwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQC0K3NP2+FExNuECD+mbx4Gf7gi9VB5WqcegSif0goj\nHl/K+pJDpSLfFKKT2QmrkVIBrWZJxqMnB8HWjrJxpI/qf5Dzw/Li/urrmfSy0Ieg\nkIfS4/AxDISHBfuc2KVX0LhRlPJnZK0N0COYwEeI3zUQkCofZ3he2Ct38ghaGWNJ\neb5o9u5CNLPwG1H/v4yzCsJ2UnzLqBaLTbSS3oZIeOZrEjWNK0a8HHGSwrzq/cn3\nvB83BZjsYmLvS4Gu87SmTlthyQPNRMp43VAa8Gdny4Nezuk/Ky9+0mvzkbNfSIT1\ngswPE7epz8+XAJ3EzwfqAdNOQs+LMZm2kwX1nj3ulb1hAgMBAAGgLTArBgkqhkiG\n9w0BCQ4xHjAcMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0B\nAQsFAAOCAQEARh0Pi36mNmyprU4j25GWNqQYCJ6cBGnaPeiwr8/F3rsGsF4LTQdP\nxW2oBrEWyYRidNCkSMrPkcSiXu1Loy9APwSAXgJZWMYy0Ccdbd3P7dtGNOZkKaLA\nQKntGA5E1YAbzNhlt7NviGpqZ49K2aOgcGBTnDZ7xDzmg4uo3tcHgzOCwarYZT8l\nqVpc3jAyxRBOrxVKPZNFb4hAFvUm8k6/Etn5n4otN0JT3KGewbfQY50CxW5ShK52\nQCs2PmFMYHHmG11FD3W755MxzhL6UmMy20GUgWWthGmR1LugcBgDtWO/7bqqC9tT\nXYDTDJ1j0g3Y0cvy2+kltrams4lGE3xs6g==\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "cluster/testdata/certs/node2.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEAjCCAuqgAwIBAgIUex5xEYsDJPUg8idU0Sql2ixGdTwwDQYJKoZIhvcNAQEL\nBQAwYjELMAkGA1UEBhMCQVUxETAPBgNVBAgTCFZpY3RvcmlhMRIwEAYDVQQHEwlN\nZWxib3VybmUxDjAMBgNVBAoTBW1hc3NsMQwwCgYDVQQLEwNWSUMxDjAMBgNVBAMT\nBW1hc3NsMCAXDTIxMDUwNTE2MTYwMFoYDzIxMjEwNDExMTYxNjAwWjBzMQswCQYD\nVQQGEwJBVTERMA8GA1UECBMIVmljdG9yaWExEjAQBgNVBAcTCU1lbGJvdXJuZTEV\nMBMGA1UEChMMc3lzdGVtOm5vZGUyMQ4wDAYDVQQLEwVtYXNzbDEWMBQGA1UEAxMN\nc3lzdGVtOnNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALQr\nc0/b4UTE24QIP6ZvHgZ/uCL1UHlapx6BKJ/SCiMeX8r6kkOlIt8UopPZCauRUgGt\nZknGoycHwdaOsnGkj+p/kPPD8uL+6uuZ9LLQh6CQh9Lj8DEMhIcF+5zYpVfQuFGU\n8mdkrQ3QI5jAR4jfNRCQKh9neF7YK3fyCFoZY0l5vmj27kI0s/AbUf+/jLMKwnZS\nfMuoFotNtJLehkh45msSNY0rRrwccZLCvOr9yfe8HzcFmOxiYu9Lga7ztKZOW2HJ\nA81EynjdUBrwZ2fLg17O6T8rL37Sa/ORs19IhPWCzA8Tt6nPz5cAncTPB+oB005C\nz4sxmbaTBfWePe6VvWECAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0l\nBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYE\nFDNgivphLRqKzV8n29GJq6S2I+CQMB8GA1UdIwQYMBaAFHeA0xJSquoJxmAyWYCb\nwvuH5a2QMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0BAQsF\nAAOCAQEAnNG3nzycALGf+N8PuG4sUIkD+SYA1nOEgfD2KiGNyuTYHhGgFXTw8KzB\nolH05VidldBvC0+pl5EqZAp9qdzpw6Z5Mb0gdoZY6TeKDUo022G3BHLMUGLp8y+i\nKE6+awwgdJZ6vPbdnWAh7VM/HCUrGIIPmLFan13j/2RiMfaDxdMAowPmbVc8MLgA\nJHI6pPo8D1DacEvMM09qGtwQEUoREOWJ/SzTWl1nc/IAS1yOL1LCyKLcoj/HWqjG\n3LXficQ7rf+Cpn1GnrKwMziT0OLDLxOs/+5d3nFSLxqF1lpykhPPkmHOHnuY8sMX\nQdndn9QILdp5GNvqiVNQYcQa/gOb6g==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "cluster/testdata/empty_tls_config.yml",
    "content": "{}\n"
  },
  {
    "path": "cluster/testdata/tls_config_node1.yml",
    "content": "tls_server_config:\n  cert_file: \"certs/node1.pem\"\n  key_file: \"certs/node1-key.pem\"\n  client_ca_file: \"certs/ca.pem\"\n  client_auth_type: \"VerifyClientCertIfGiven\"\ntls_client_config:\n  cert_file: \"certs/node1.pem\"\n  key_file: \"certs/node1-key.pem\"\n  ca_file: \"certs/ca.pem\"\n"
  },
  {
    "path": "cluster/testdata/tls_config_node2.yml",
    "content": "tls_server_config:\n  cert_file: \"certs/node2.pem\"\n  key_file: \"certs/node2-key.pem\"\n  client_ca_file: \"certs/ca.pem\"\n  client_auth_type: \"VerifyClientCertIfGiven\"\ntls_client_config:\n  cert_file: \"certs/node2.pem\"\n  key_file: \"certs/node2-key.pem\"\n  ca_file: \"certs/ca.pem\"\n"
  },
  {
    "path": "cluster/testdata/tls_config_with_missing_client.yml",
    "content": "tls_server_config:\n  cert_file: \"certs/node2.pem\"\n  key_file: \"certs/node2-key.pem\"\n  client_ca_file: \"certs/ca.pem\"\n  client_auth_type: \"VerifyClientCertIfGiven\"\n"
  },
  {
    "path": "cluster/testdata/tls_config_with_missing_server.yml",
    "content": "tls_client_config:\n  cert_file: \"certs/node1.pem\"\n  key_file: \"certs/node1-key.pem\"\n  ca_file: \"certs/ca.pem\"\n"
  },
  {
    "path": "cluster/tls_config.go",
    "content": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cluster\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/exporter-toolkit/web\"\n\t\"gopkg.in/yaml.v2\"\n)\n\ntype TLSTransportConfig struct {\n\tTLSServerConfig *web.TLSConfig    `yaml:\"tls_server_config\"`\n\tTLSClientConfig *config.TLSConfig `yaml:\"tls_client_config\"`\n}\n\nfunc GetTLSTransportConfig(configPath string) (*TLSTransportConfig, error) {\n\tif configPath == \"\" {\n\t\treturn nil, nil\n\t}\n\n\tbytes, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcfg := &TLSTransportConfig{\n\t\tTLSClientConfig: &config.TLSConfig{},\n\t}\n\tif err := yaml.UnmarshalStrict(bytes, cfg); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif cfg.TLSServerConfig == nil {\n\t\treturn nil, fmt.Errorf(\"missing 'tls_server_config' entry in the TLS configuration\")\n\t}\n\n\tcfg.TLSServerConfig.SetDirectory(filepath.Dir(configPath))\n\tcfg.TLSClientConfig.SetDirectory(filepath.Dir(configPath))\n\n\treturn cfg, nil\n}\n"
  },
  {
    "path": "cluster/tls_connection.go",
    "content": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cluster\n\nimport (\n\t\"bufio\"\n\t\"crypto/tls\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/hashicorp/memberlist\"\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"github.com/prometheus/alertmanager/cluster/clusterpb\"\n)\n\nconst (\n\tversion      = \"v0.1.0\"\n\tuint32length = 4\n)\n\n// tlsConn wraps net.Conn with connection pooling data.\ntype tlsConn struct {\n\tmtx        sync.Mutex\n\tconnection net.Conn\n\tlive       bool\n}\n\nfunc dialTLSConn(addr string, timeout time.Duration, tlsConfig *tls.Config) (*tlsConn, error) {\n\tdialer := &net.Dialer{Timeout: timeout}\n\tconn, err := tls.DialWithDialer(dialer, network, addr, tlsConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &tlsConn{\n\t\tconnection: conn,\n\t\tlive:       true,\n\t}, nil\n}\n\nfunc rcvTLSConn(conn net.Conn) *tlsConn {\n\treturn &tlsConn{\n\t\tconnection: conn,\n\t\tlive:       true,\n\t}\n}\n\n// Write writes a byte array into the connection. It returns the number of bytes written and an error.\nfunc (conn *tlsConn) Write(b []byte) (int, error) {\n\tconn.mtx.Lock()\n\tdefer conn.mtx.Unlock()\n\tn, err := conn.connection.Write(b)\n\tif err != nil {\n\t\tconn.live = false\n\t}\n\treturn n, err\n}\n\nfunc (conn *tlsConn) alive() bool {\n\tconn.mtx.Lock()\n\tdefer conn.mtx.Unlock()\n\treturn conn.live\n}\n\nfunc (conn *tlsConn) getRawConn() net.Conn {\n\tconn.mtx.Lock()\n\tdefer conn.mtx.Unlock()\n\traw := conn.connection\n\tconn.live = false\n\tconn.connection = nil\n\treturn raw\n}\n\n// writePacket writes all the bytes in one operation so no concurrent write happens in between.\n// It prefixes the message length.\nfunc (conn *tlsConn) writePacket(fromAddr string, b []byte) error {\n\tmsg, err := proto.Marshal(\n\t\t&clusterpb.MemberlistMessage{\n\t\t\tVersion:  version,\n\t\t\tKind:     clusterpb.MemberlistMessage_PACKET,\n\t\t\tFromAddr: fromAddr,\n\t\t\tMsg:      b,\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to marshal memeberlist packet message: %w\", err)\n\t}\n\tbuf := make([]byte, uint32length, uint32length+len(msg))\n\tbinary.LittleEndian.PutUint32(buf, uint32(len(msg)))\n\t_, err = conn.Write(append(buf, msg...))\n\treturn err\n}\n\n// writeStream simply signals that this is a stream connection by sending the connection type.\nfunc (conn *tlsConn) writeStream() error {\n\tmsg, err := proto.Marshal(\n\t\t&clusterpb.MemberlistMessage{\n\t\t\tVersion: version,\n\t\t\tKind:    clusterpb.MemberlistMessage_STREAM,\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to marshal memeberlist stream message: %w\", err)\n\t}\n\tbuf := make([]byte, uint32length, uint32length+len(msg))\n\tbinary.LittleEndian.PutUint32(buf, uint32(len(msg)))\n\t_, err = conn.Write(append(buf, msg...))\n\treturn err\n}\n\n// read returns a packet for packet connections or an error if there is one.\n// It returns nothing if the connection is meant to be streamed.\nfunc (conn *tlsConn) read() (*memberlist.Packet, error) {\n\tif conn.connection == nil {\n\t\treturn nil, errors.New(\"nil connection\")\n\t}\n\n\tconn.mtx.Lock()\n\treader := bufio.NewReader(conn.connection)\n\tlenBuf := make([]byte, uint32length)\n\t_, err := io.ReadFull(reader, lenBuf)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading message length: %w\", err)\n\t}\n\tmsgLen := binary.LittleEndian.Uint32(lenBuf)\n\tmsgBuf := make([]byte, msgLen)\n\t_, err = io.ReadFull(reader, msgBuf)\n\tconn.mtx.Unlock()\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading message: %w\", err)\n\t}\n\tpb := clusterpb.MemberlistMessage{}\n\terr = proto.Unmarshal(msgBuf, &pb)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error parsing message: %w\", err)\n\t}\n\tif pb.Version != version {\n\t\treturn nil, errors.New(\"tls memberlist message version incompatible\")\n\t}\n\tswitch pb.Kind {\n\tcase clusterpb.MemberlistMessage_STREAM:\n\t\treturn nil, nil\n\tcase clusterpb.MemberlistMessage_PACKET:\n\t\treturn toPacket(&pb)\n\tdefault:\n\t\treturn nil, errors.New(\"could not read from either stream or packet channel\")\n\t}\n}\n\nfunc toPacket(pb *clusterpb.MemberlistMessage) (*memberlist.Packet, error) {\n\taddr, err := net.ResolveTCPAddr(network, pb.FromAddr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error parsing packet sender address: %w\", err)\n\t}\n\treturn &memberlist.Packet{\n\t\tBuf:       pb.Msg,\n\t\tFrom:      addr,\n\t\tTimestamp: time.Now(),\n\t}, nil\n}\n\nfunc (conn *tlsConn) Close() error {\n\tconn.mtx.Lock()\n\tdefer conn.mtx.Unlock()\n\tconn.live = false\n\tif conn.connection == nil {\n\t\treturn nil\n\t}\n\treturn conn.connection.Close()\n}\n"
  },
  {
    "path": "cluster/tls_connection_test.go",
    "content": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cluster\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestWriteStream(t *testing.T) {\n\tw, r := net.Pipe()\n\tconn := &tlsConn{\n\t\tconnection: w,\n\t}\n\tdefer r.Close()\n\tgo func() {\n\t\tconn.writeStream()\n\t\tw.Close()\n\t}()\n\tpacket, err := rcvTLSConn(r).read()\n\trequire.NoError(t, err)\n\trequire.Nil(t, packet)\n}\n\nfunc TestWritePacket(t *testing.T) {\n\ttestCases := []struct {\n\t\tfromAddr string\n\t\tmsg      string\n\t}{\n\t\t{fromAddr: \"127.0.0.1:8001\", msg: \"\"},\n\t\t{fromAddr: \"10.0.0.4:9094\", msg: \"hello\"},\n\t\t{fromAddr: \"127.0.0.1:8001\", msg: \"0\"},\n\t}\n\tfor _, tc := range testCases {\n\t\tw, r := net.Pipe()\n\t\tdefer r.Close()\n\t\tgo func() {\n\t\t\tconn := &tlsConn{connection: w}\n\t\t\tconn.writePacket(tc.fromAddr, []byte(tc.msg))\n\t\t\tw.Close()\n\t\t}()\n\t\tpacket, err := rcvTLSConn(r).read()\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, tc.msg, string(packet.Buf))\n\t\trequire.Equal(t, tc.fromAddr, packet.From.String())\n\n\t}\n}\n\nfunc TestRead_Nil(t *testing.T) {\n\tpacket, err := (&tlsConn{}).read()\n\trequire.Nil(t, packet)\n\trequire.Error(t, err)\n}\n\nfunc TestTLSConn_Close(t *testing.T) {\n\ttestCases := []string{\n\t\t\"foo\",\n\t\t\"bar\",\n\t}\n\tfor _, tc := range testCases {\n\t\tc := &tlsConn{\n\t\t\tconnection: &mockConn{\n\t\t\t\terrMsg: tc,\n\t\t\t},\n\t\t\tlive: true,\n\t\t}\n\t\terr := c.Close()\n\t\trequire.Equal(t, errors.New(tc), err, tc)\n\t\trequire.False(t, c.alive())\n\t\trequire.True(t, c.connection.(*mockConn).closed)\n\t}\n}\n\ntype mockConn struct {\n\tclosed bool\n\terrMsg string\n}\n\nfunc (m *mockConn) Read(b []byte) (n int, err error) {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockConn) Write(b []byte) (n int, err error) {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockConn) Close() error {\n\tm.closed = true\n\treturn errors.New(m.errMsg)\n}\n\nfunc (m *mockConn) LocalAddr() net.Addr {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockConn) RemoteAddr() net.Addr {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockConn) SetDeadline(t time.Time) error {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockConn) SetReadDeadline(t time.Time) error {\n\tpanic(\"implement me\")\n}\n\nfunc (m *mockConn) SetWriteDeadline(t time.Time) error {\n\tpanic(\"implement me\")\n}\n"
  },
  {
    "path": "cluster/tls_transport.go",
    "content": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Forked from https://github.com/mxinden/memberlist-tls-transport.\n\n// Implements Transport interface so that all gossip communications occur via TLS over TCP.\n\npackage cluster\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-sockaddr\"\n\t\"github.com/hashicorp/memberlist\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n\tcommon \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/exporter-toolkit/web\"\n)\n\nconst (\n\tmetricNamespace = \"alertmanager\"\n\tmetricSubsystem = \"tls_transport\"\n\tnetwork         = \"tcp\"\n)\n\n// TLSTransport is a Transport implementation that uses TLS over TCP for both\n// packet and stream operations.\ntype TLSTransport struct {\n\tctx          context.Context\n\tcancel       context.CancelFunc\n\tlogger       *slog.Logger\n\tbindAddr     string\n\tbindPort     int\n\tdone         chan struct{}\n\tlistener     net.Listener\n\tpacketCh     chan *memberlist.Packet\n\tstreamCh     chan net.Conn\n\tconnPool     *connectionPool\n\ttlsServerCfg *tls.Config\n\ttlsClientCfg *tls.Config\n\n\tpacketsSent prometheus.Counter\n\tpacketsRcvd prometheus.Counter\n\tstreamsSent prometheus.Counter\n\tstreamsRcvd prometheus.Counter\n\treadErrs    prometheus.Counter\n\twriteErrs   *prometheus.CounterVec\n}\n\n// NewTLSTransport returns a TLS transport with the given configuration.\n// On successful initialization, a tls listener will be created and listening.\n// A valid bindAddr is required. If bindPort == 0, the system will assign\n// a free port automatically.\nfunc NewTLSTransport(\n\tctx context.Context,\n\tlogger *slog.Logger,\n\treg prometheus.Registerer,\n\tbindAddr string,\n\tbindPort int,\n\tcfg *TLSTransportConfig,\n) (*TLSTransport, error) {\n\tif reg == nil {\n\t\treturn nil, errors.New(\"missing Prometheus registry\")\n\t}\n\n\tif cfg == nil {\n\t\treturn nil, errors.New(\"must specify TLSTransportConfig\")\n\t}\n\n\ttlsServerCfg, err := web.ConfigToTLSConfig(cfg.TLSServerConfig)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid TLS server config: %w\", err)\n\t}\n\n\ttlsClientCfg, err := common.NewTLSConfig(cfg.TLSClientConfig)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid TLS client config: %w\", err)\n\t}\n\n\tip := net.ParseIP(bindAddr)\n\tif ip == nil {\n\t\treturn nil, fmt.Errorf(\"invalid bind address \\\"%s\\\"\", bindAddr)\n\t}\n\n\taddr := &net.TCPAddr{IP: ip, Port: bindPort}\n\tlistener, err := tls.Listen(network, addr.String(), tlsServerCfg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to start TLS listener on %q port %d: %w\", bindAddr, bindPort, err)\n\t}\n\n\tconnPool, err := newConnectionPool(tlsClientCfg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to initialize tls transport connection pool: %w\", err)\n\t}\n\n\tctx, cancel := context.WithCancel(ctx)\n\tt := &TLSTransport{\n\t\tctx:          ctx,\n\t\tcancel:       cancel,\n\t\tlogger:       logger,\n\t\tbindAddr:     bindAddr,\n\t\tbindPort:     bindPort,\n\t\tdone:         make(chan struct{}),\n\t\tlistener:     listener,\n\t\tpacketCh:     make(chan *memberlist.Packet),\n\t\tstreamCh:     make(chan net.Conn),\n\t\tconnPool:     connPool,\n\t\ttlsServerCfg: tlsServerCfg,\n\t\ttlsClientCfg: tlsClientCfg,\n\t}\n\n\tt.registerMetrics(reg)\n\n\tgo func() {\n\t\tt.listen()\n\t\tclose(t.done)\n\t}()\n\treturn t, nil\n}\n\n// FinalAdvertiseAddr is given the user's configured values (which\n// might be empty) and returns the desired IP and port to advertise to\n// the rest of the cluster.\nfunc (t *TLSTransport) FinalAdvertiseAddr(ip string, port int) (net.IP, int, error) {\n\tvar advertiseAddr net.IP\n\tvar advertisePort int\n\tif ip != \"\" {\n\t\tadvertiseAddr = net.ParseIP(ip)\n\t\tif advertiseAddr == nil {\n\t\t\treturn nil, 0, fmt.Errorf(\"failed to parse advertise address %q\", ip)\n\t\t}\n\n\t\tif ip4 := advertiseAddr.To4(); ip4 != nil {\n\t\t\tadvertiseAddr = ip4\n\t\t}\n\t\tadvertisePort = port\n\t} else {\n\t\tif t.bindAddr == \"0.0.0.0\" {\n\t\t\t// Otherwise, if we're not bound to a specific IP, let's\n\t\t\t// use a suitable private IP address.\n\t\t\tvar err error\n\t\t\tip, err = sockaddr.GetPrivateIP()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, 0, fmt.Errorf(\"failed to get interface addresses: %w\", err)\n\t\t\t}\n\t\t\tif ip == \"\" {\n\t\t\t\treturn nil, 0, fmt.Errorf(\"no private IP address found, and explicit IP not provided\")\n\t\t\t}\n\n\t\t\tadvertiseAddr = net.ParseIP(ip)\n\t\t\tif advertiseAddr == nil {\n\t\t\t\treturn nil, 0, fmt.Errorf(\"failed to parse advertise address: %q\", ip)\n\t\t\t}\n\t\t} else {\n\t\t\tadvertiseAddr = t.listener.Addr().(*net.TCPAddr).IP\n\t\t}\n\t\tadvertisePort = t.GetAutoBindPort()\n\t}\n\treturn advertiseAddr, advertisePort, nil\n}\n\n// PacketCh returns a channel that can be read to receive incoming\n// packets from other peers.\nfunc (t *TLSTransport) PacketCh() <-chan *memberlist.Packet {\n\treturn t.packetCh\n}\n\n// StreamCh returns a channel that can be read to handle incoming stream\n// connections from other peers.\nfunc (t *TLSTransport) StreamCh() <-chan net.Conn {\n\treturn t.streamCh\n}\n\n// Shutdown is called when memberlist is shutting down; this gives the\n// TLS Transport a chance to clean up the listener and other goroutines.\nfunc (t *TLSTransport) Shutdown() error {\n\tt.logger.Debug(\"shutting down tls transport\")\n\tt.cancel()\n\terr := t.listener.Close()\n\tt.connPool.shutdown()\n\t<-t.done\n\treturn err\n}\n\n// WriteTo is a packet-oriented interface that borrows a connection\n// from the pool, and writes to it. It also returns a timestamp of when\n// the packet was written.\nfunc (t *TLSTransport) WriteTo(b []byte, addr string) (time.Time, error) {\n\tconn, err := t.connPool.borrowConnection(addr, DefaultTCPTimeout)\n\tif err != nil {\n\t\tt.writeErrs.WithLabelValues(\"packet\").Inc()\n\t\treturn time.Now(), fmt.Errorf(\"failed to dial: %w\", err)\n\t}\n\tfromAddr := t.listener.Addr().String()\n\terr = conn.writePacket(fromAddr, b)\n\tif err != nil {\n\t\tt.writeErrs.WithLabelValues(\"packet\").Inc()\n\t\treturn time.Now(), fmt.Errorf(\"failed to write packet: %w\", err)\n\t}\n\tt.packetsSent.Add(float64(len(b)))\n\treturn time.Now(), nil\n}\n\n// DialTimeout is used to create a connection that allows memberlist\n// to perform two-way communications with a peer.\nfunc (t *TLSTransport) DialTimeout(addr string, timeout time.Duration) (net.Conn, error) {\n\tconn, err := dialTLSConn(addr, timeout, t.tlsClientCfg)\n\tif err != nil {\n\t\tt.writeErrs.WithLabelValues(\"stream\").Inc()\n\t\treturn nil, fmt.Errorf(\"failed to dial: %w\", err)\n\t}\n\terr = conn.writeStream()\n\tnetConn := conn.getRawConn()\n\tif err != nil {\n\t\tt.writeErrs.WithLabelValues(\"stream\").Inc()\n\t\treturn netConn, fmt.Errorf(\"failed to create stream connection: %w\", err)\n\t}\n\tt.streamsSent.Inc()\n\treturn netConn, nil\n}\n\n// GetAutoBindPort returns the bind port that was automatically given by the system\n// if a bindPort of 0 was specified during instantiation.\nfunc (t *TLSTransport) GetAutoBindPort() int {\n\treturn t.listener.Addr().(*net.TCPAddr).Port\n}\n\n// listen starts up multiple handlers accepting concurrent connections.\nfunc (t *TLSTransport) listen() {\n\tfor {\n\t\tselect {\n\t\tcase <-t.ctx.Done():\n\n\t\t\treturn\n\t\tdefault:\n\t\t\tconn, err := t.listener.Accept()\n\t\t\tif err != nil {\n\t\t\t\t// The error \"use of closed network connection\" is returned when the listener is closed.\n\t\t\t\t// It is not exported in a more reasonable way. See https://github.com/golang/go/issues/4373.\n\t\t\t\tif strings.Contains(err.Error(), \"use of closed network connection\") {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tt.readErrs.Inc()\n\t\t\t\tt.logger.Debug(\"error accepting connection\", \"err\", err)\n\n\t\t\t} else {\n\t\t\t\tgo t.handle(conn)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (t *TLSTransport) handle(conn net.Conn) {\n\tfor {\n\t\tpacket, err := rcvTLSConn(conn).read()\n\t\tif err != nil {\n\t\t\tt.logger.Debug(\"error reading from connection\", \"err\", err)\n\t\t\tt.readErrs.Inc()\n\t\t\treturn\n\t\t}\n\t\tselect {\n\t\tcase <-t.ctx.Done():\n\t\t\treturn\n\t\tdefault:\n\t\t\tif packet != nil {\n\t\t\t\tn := len(packet.Buf)\n\t\t\t\tt.packetCh <- packet\n\t\t\t\tt.packetsRcvd.Add(float64(n))\n\t\t\t} else {\n\t\t\t\tt.streamCh <- conn\n\t\t\t\tt.streamsRcvd.Inc()\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (t *TLSTransport) registerMetrics(reg prometheus.Registerer) {\n\tt.packetsSent = promauto.With(reg).NewCounter(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: metricNamespace,\n\t\t\tSubsystem: metricSubsystem,\n\t\t\tName:      \"packet_bytes_sent_total\",\n\t\t\tHelp:      \"The number of packet bytes sent to outgoing connections (excluding internal metadata).\",\n\t\t},\n\t)\n\tt.packetsRcvd = promauto.With(reg).NewCounter(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: metricNamespace,\n\t\t\tSubsystem: metricSubsystem,\n\t\t\tName:      \"packet_bytes_received_total\",\n\t\t\tHelp:      \"The number of packet bytes received from incoming connections (excluding internal metadata).\",\n\t\t},\n\t)\n\tt.streamsSent = promauto.With(reg).NewCounter(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: metricNamespace,\n\t\t\tSubsystem: metricSubsystem,\n\t\t\tName:      \"stream_connections_sent_total\",\n\t\t\tHelp:      \"The number of stream connections sent.\",\n\t\t},\n\t)\n\n\tt.streamsRcvd = promauto.With(reg).NewCounter(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: metricNamespace,\n\t\t\tSubsystem: metricSubsystem,\n\t\t\tName:      \"stream_connections_received_total\",\n\t\t\tHelp:      \"The number of stream connections received.\",\n\t\t},\n\t)\n\tt.readErrs = promauto.With(reg).NewCounter(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: metricNamespace,\n\t\t\tSubsystem: metricSubsystem,\n\t\t\tName:      \"read_errors_total\",\n\t\t\tHelp:      \"The number of errors encountered while reading from incoming connections.\",\n\t\t},\n\t)\n\tt.writeErrs = promauto.With(reg).NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: metricNamespace,\n\t\t\tSubsystem: metricSubsystem,\n\t\t\tName:      \"write_errors_total\",\n\t\t\tHelp:      \"The number of errors encountered while writing to outgoing connections.\",\n\t\t},\n\t\t[]string{\"connection_type\"},\n\t)\n}\n"
  },
  {
    "path": "cluster/tls_transport_test.go",
    "content": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage cluster\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\tcontext2 \"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar logger = promslog.NewNopLogger()\n\nfunc freeport() int {\n\tlis, _ := net.Listen(network, \"127.0.0.1:0\")\n\tdefer lis.Close()\n\n\treturn lis.Addr().(*net.TCPAddr).Port\n}\n\nfunc newTLSTransport(file, address string, port int) (*TLSTransport, error) {\n\tcfg, err := GetTLSTransportConfig(file)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn NewTLSTransport(context2.Background(), promslog.NewNopLogger(), prometheus.NewRegistry(), address, port, cfg)\n}\n\nfunc TestNewTLSTransport(t *testing.T) {\n\tport := freeport()\n\tfor _, tc := range []struct {\n\t\tbindAddr    string\n\t\tbindPort    int\n\t\ttlsConfFile string\n\t\terr         string\n\t}{\n\t\t{\n\t\t\terr: \"must specify TLSTransportConfig\",\n\t\t},\n\t\t{\n\t\t\ttlsConfFile: \"testdata/empty_tls_config.yml\",\n\t\t\terr:         \"missing 'tls_server_config' entry in the TLS configuration\",\n\t\t},\n\t\t{\n\t\t\ttlsConfFile: \"testdata/tls_config_with_missing_server.yml\",\n\t\t\terr:         \"missing 'tls_server_config' entry in the TLS configuration\",\n\t\t},\n\t\t{\n\t\t\terr:         \"invalid bind address \\\"\\\"\",\n\t\t\ttlsConfFile: \"testdata/tls_config_node1.yml\",\n\t\t},\n\t\t{\n\t\t\tbindAddr:    \"abc123\",\n\t\t\terr:         \"invalid bind address \\\"abc123\\\"\",\n\t\t\ttlsConfFile: \"testdata/tls_config_node1.yml\",\n\t\t},\n\t\t{\n\t\t\tbindAddr:    localhost,\n\t\t\tbindPort:    0,\n\t\t\ttlsConfFile: \"testdata/tls_config_node1.yml\",\n\t\t},\n\t\t{\n\t\t\tbindAddr:    localhost,\n\t\t\tbindPort:    port,\n\t\t\ttlsConfFile: \"testdata/tls_config_node2.yml\",\n\t\t},\n\t\t{\n\t\t\ttlsConfFile: \"testdata/tls_config_with_missing_client.yml\",\n\t\t\tbindAddr:    localhost,\n\t\t},\n\t} {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\ttransport, err := newTLSTransport(tc.tlsConfFile, tc.bindAddr, tc.bindPort)\n\t\t\tif len(tc.err) > 0 {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Equal(t, tc.err, err.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tdefer transport.Shutdown()\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.bindAddr, transport.bindAddr)\n\t\t\trequire.Equal(t, tc.bindPort, transport.bindPort)\n\t\t\trequire.NotNil(t, transport.listener)\n\t\t})\n\t}\n}\n\nconst localhost = \"127.0.0.1\"\n\nfunc TestFinalAdvertiseAddr(t *testing.T) {\n\tports := [...]int{freeport(), freeport(), freeport()}\n\ttestCases := []struct {\n\t\tbindAddr      string\n\t\tbindPort      int\n\t\tinputIP       string\n\t\tinputPort     int\n\t\texpectedIP    string\n\t\texpectedPort  int\n\t\texpectedError string\n\t}{\n\t\t{bindAddr: localhost, bindPort: ports[0], inputIP: \"10.0.0.5\", inputPort: 54231, expectedIP: \"10.0.0.5\", expectedPort: 54231},\n\t\t{bindAddr: localhost, bindPort: ports[1], inputIP: \"invalid\", inputPort: 54231, expectedError: \"failed to parse advertise address \\\"invalid\\\"\"},\n\t\t{bindAddr: \"0.0.0.0\", bindPort: 0, inputIP: \"\", inputPort: 0, expectedIP: \"random\"},\n\t\t{bindAddr: localhost, bindPort: 0, inputIP: \"\", inputPort: 0, expectedIP: localhost},\n\t\t{bindAddr: localhost, bindPort: ports[2], inputIP: \"\", inputPort: 0, expectedIP: localhost, expectedPort: ports[2]},\n\t}\n\tfor _, tc := range testCases {\n\t\ttlsConf := loadTLSTransportConfig(t, \"testdata/tls_config_node1.yml\")\n\t\ttransport, err := NewTLSTransport(context2.Background(), logger, prometheus.NewRegistry(), tc.bindAddr, tc.bindPort, tlsConf)\n\t\trequire.NoError(t, err)\n\t\tip, port, err := transport.FinalAdvertiseAddr(tc.inputIP, tc.inputPort)\n\t\tif len(tc.expectedError) > 0 {\n\t\t\trequire.Equal(t, tc.expectedError, err.Error())\n\t\t} else {\n\t\t\trequire.NoError(t, err)\n\t\t\tif tc.expectedPort == 0 {\n\t\t\t\trequire.Less(t, tc.expectedPort, port)\n\t\t\t} else {\n\t\t\t\trequire.Equal(t, tc.expectedPort, port)\n\t\t\t}\n\t\t\tif tc.expectedIP == \"random\" {\n\t\t\t\trequire.NotNil(t, ip)\n\t\t\t} else {\n\t\t\t\trequire.Equal(t, tc.expectedIP, ip.String())\n\t\t\t}\n\t\t}\n\t\ttransport.Shutdown()\n\t}\n}\n\nfunc TestWriteTo(t *testing.T) {\n\ttlsConf1 := loadTLSTransportConfig(t, \"testdata/tls_config_node1.yml\")\n\tt1, _ := NewTLSTransport(context2.Background(), logger, prometheus.NewRegistry(), \"127.0.0.1\", 0, tlsConf1)\n\tdefer t1.Shutdown()\n\n\ttlsConf2 := loadTLSTransportConfig(t, \"testdata/tls_config_node2.yml\")\n\tt2, _ := NewTLSTransport(context2.Background(), logger, prometheus.NewRegistry(), \"127.0.0.1\", 0, tlsConf2)\n\tdefer t2.Shutdown()\n\n\tfrom := fmt.Sprintf(\"%s:%d\", t1.bindAddr, t1.GetAutoBindPort())\n\tto := fmt.Sprintf(\"%s:%d\", t2.bindAddr, t2.GetAutoBindPort())\n\tsent := []byte((\"test packet\"))\n\t_, err := t1.WriteTo(sent, to)\n\trequire.NoError(t, err)\n\tpacket := <-t2.PacketCh()\n\trequire.Equal(t, sent, packet.Buf)\n\trequire.Equal(t, from, packet.From.String())\n}\n\nfunc BenchmarkWriteTo(b *testing.B) {\n\ttlsConf1 := loadTLSTransportConfig(b, \"testdata/tls_config_node1.yml\")\n\tt1, _ := NewTLSTransport(context2.Background(), logger, prometheus.NewRegistry(), \"127.0.0.1\", 0, tlsConf1)\n\tdefer t1.Shutdown()\n\n\ttlsConf2 := loadTLSTransportConfig(b, \"testdata/tls_config_node2.yml\")\n\tt2, _ := NewTLSTransport(context2.Background(), logger, prometheus.NewRegistry(), \"127.0.0.1\", 0, tlsConf2)\n\tdefer t2.Shutdown()\n\n\tb.ResetTimer()\n\tfrom := fmt.Sprintf(\"%s:%d\", t1.bindAddr, t1.GetAutoBindPort())\n\tto := fmt.Sprintf(\"%s:%d\", t2.bindAddr, t2.GetAutoBindPort())\n\tsent := []byte((\"test packet\"))\n\n\t_, err := t1.WriteTo(sent, to)\n\trequire.NoError(b, err)\n\tpacket := <-t2.PacketCh()\n\n\trequire.Equal(b, sent, packet.Buf)\n\trequire.Equal(b, from, packet.From.String())\n}\n\nfunc TestDialTimeout(t *testing.T) {\n\ttlsConf1 := loadTLSTransportConfig(t, \"testdata/tls_config_node1.yml\")\n\tt1, err := NewTLSTransport(context2.Background(), logger, prometheus.NewRegistry(), \"127.0.0.1\", 0, tlsConf1)\n\trequire.NoError(t, err)\n\tdefer t1.Shutdown()\n\n\ttlsConf2 := loadTLSTransportConfig(t, \"testdata/tls_config_node2.yml\")\n\tt2, err := NewTLSTransport(context2.Background(), logger, prometheus.NewRegistry(), \"127.0.0.1\", 0, tlsConf2)\n\trequire.NoError(t, err)\n\tdefer t2.Shutdown()\n\n\taddr := fmt.Sprintf(\"%s:%d\", t2.bindAddr, t2.GetAutoBindPort())\n\tfrom, err := t1.DialTimeout(addr, 5*time.Second)\n\trequire.NoError(t, err)\n\tdefer from.Close()\n\n\tvar to net.Conn\n\tvar wg sync.WaitGroup\n\twg.Go(func() {\n\t\tto = <-t2.StreamCh()\n\t})\n\n\tsent := []byte((\"test stream\"))\n\tm, err := from.Write(sent)\n\trequire.NoError(t, err)\n\trequire.Positive(t, m)\n\n\twg.Wait()\n\n\treader := bufio.NewReader(to)\n\tbuf := make([]byte, len(sent))\n\tn, err := io.ReadFull(reader, buf)\n\trequire.NoError(t, err)\n\trequire.Len(t, sent, n)\n\trequire.Equal(t, sent, buf)\n}\n\nfunc TestShutdown(t *testing.T) {\n\tvar buf bytes.Buffer\n\tpromslogConfig := &promslog.Config{Writer: &buf}\n\tlogger := promslog.New(promslogConfig)\n\t// Set logger to debug, otherwise it won't catch some logging from `Shutdown()` method.\n\t_ = promslogConfig.Level.Set(\"debug\")\n\n\ttlsConf1 := loadTLSTransportConfig(t, \"testdata/tls_config_node1.yml\")\n\tt1, _ := NewTLSTransport(context2.Background(), logger, prometheus.NewRegistry(), \"127.0.0.1\", 0, tlsConf1)\n\t// Sleeping to make sure listeners have started and can subsequently be shut down gracefully.\n\ttime.Sleep(500 * time.Millisecond)\n\terr := t1.Shutdown()\n\trequire.NoError(t, err)\n\trequire.NotContains(t, buf.String(), \"use of closed network connection\")\n\trequire.Contains(t, buf.String(), \"shutting down tls transport\")\n}\n\nfunc loadTLSTransportConfig(tb testing.TB, filename string) *TLSTransportConfig {\n\ttb.Helper()\n\n\tconfig, err := GetTLSTransportConfig(filename)\n\tif err != nil {\n\t\ttb.Fatal(err)\n\t}\n\n\treturn config\n}\n"
  },
  {
    "path": "cmd/alertmanager/main.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/KimMachineGun/automemlimit/memlimit\"\n\t\"github.com/alecthomas/kingpin/v2\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\tversioncollector \"github.com/prometheus/client_golang/prometheus/collectors/version\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\tpromslogflag \"github.com/prometheus/common/promslog/flag\"\n\t\"github.com/prometheus/common/route\"\n\t\"github.com/prometheus/common/version\"\n\t\"github.com/prometheus/exporter-toolkit/web\"\n\twebflag \"github.com/prometheus/exporter-toolkit/web/kingpinflag\"\n\n\t\"github.com/prometheus/alertmanager/api\"\n\t\"github.com/prometheus/alertmanager/cluster\"\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/config/receiver\"\n\t\"github.com/prometheus/alertmanager/dispatch\"\n\t\"github.com/prometheus/alertmanager/featurecontrol\"\n\t\"github.com/prometheus/alertmanager/inhibit\"\n\t\"github.com/prometheus/alertmanager/matcher/compat\"\n\t\"github.com/prometheus/alertmanager/nflog\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/provider/mem\"\n\t\"github.com/prometheus/alertmanager/silence\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/timeinterval\"\n\t\"github.com/prometheus/alertmanager/tracing\"\n\t\"github.com/prometheus/alertmanager/types\"\n\t\"github.com/prometheus/alertmanager/ui\"\n)\n\nvar (\n\trequestDuration = promauto.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tName:                            \"alertmanager_http_request_duration_seconds\",\n\t\t\tHelp:                            \"Histogram of latencies for HTTP requests.\",\n\t\t\tBuckets:                         prometheus.DefBuckets,\n\t\t\tNativeHistogramBucketFactor:     1.1,\n\t\t\tNativeHistogramMaxBucketNumber:  100,\n\t\t\tNativeHistogramMinResetDuration: 1 * time.Hour,\n\t\t},\n\t\t[]string{\"handler\", \"method\", \"code\"},\n\t)\n\tresponseSize = promauto.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tName:    \"alertmanager_http_response_size_bytes\",\n\t\t\tHelp:    \"Histogram of response size for HTTP requests.\",\n\t\t\tBuckets: prometheus.ExponentialBuckets(100, 10, 7),\n\t\t},\n\t\t[]string{\"handler\", \"method\"},\n\t)\n\tclusterEnabled = promauto.NewGauge(\n\t\tprometheus.GaugeOpts{\n\t\t\tName: \"alertmanager_cluster_enabled\",\n\t\t\tHelp: \"Indicates whether the clustering is enabled or not.\",\n\t\t},\n\t)\n\tconfiguredReceivers = promauto.NewGauge(\n\t\tprometheus.GaugeOpts{\n\t\t\tName: \"alertmanager_receivers\",\n\t\t\tHelp: \"Number of configured receivers.\",\n\t\t},\n\t)\n\tconfiguredIntegrations = promauto.NewGauge(\n\t\tprometheus.GaugeOpts{\n\t\t\tName: \"alertmanager_integrations\",\n\t\t\tHelp: \"Number of configured integrations.\",\n\t\t},\n\t)\n\tconfiguredInhibitionRules = promauto.NewGauge(\n\t\tprometheus.GaugeOpts{\n\t\t\tName: \"alertmanager_inhibition_rules\",\n\t\t\tHelp: \"Number of configured inhibition rules.\",\n\t\t},\n\t)\n\n\tpromslogConfig = promslog.Config{}\n)\n\nfunc instrumentHandler(handlerName string, handler http.HandlerFunc) http.HandlerFunc {\n\thandlerLabel := prometheus.Labels{\"handler\": handlerName}\n\treturn promhttp.InstrumentHandlerDuration(\n\t\trequestDuration.MustCurryWith(handlerLabel),\n\t\tpromhttp.InstrumentHandlerResponseSize(\n\t\t\tresponseSize.MustCurryWith(handlerLabel),\n\t\t\thandler,\n\t\t),\n\t)\n}\n\nconst defaultClusterAddr = \"0.0.0.0:9094\"\n\nfunc main() {\n\tos.Exit(run())\n}\n\nfunc run() int {\n\tif os.Getenv(\"DEBUG\") != \"\" {\n\t\truntime.SetBlockProfileRate(20)\n\t\truntime.SetMutexProfileFraction(20)\n\t}\n\n\tvar (\n\t\tconfigFile                  = kingpin.Flag(\"config.file\", \"Alertmanager configuration file name.\").Default(\"alertmanager.yml\").String()\n\t\tdataDir                     = kingpin.Flag(\"storage.path\", \"Base path for data storage.\").Default(\"data/\").String()\n\t\tretention                   = kingpin.Flag(\"data.retention\", \"How long to keep data for.\").Default(\"120h\").Duration()\n\t\tmaintenanceInterval         = kingpin.Flag(\"data.maintenance-interval\", \"Interval between garbage collection and snapshotting to disk of the silences and the notification logs.\").Default(\"15m\").Duration()\n\t\tmaxSilences                 = kingpin.Flag(\"silences.max-silences\", \"Maximum number of silences, including expired silences. If negative or zero, no limit is set.\").Default(\"0\").Int()\n\t\tmaxSilenceSizeBytes         = kingpin.Flag(\"silences.max-silence-size-bytes\", \"Maximum silence size in bytes. If negative or zero, no limit is set.\").Default(\"0\").Int()\n\t\talertGCInterval             = kingpin.Flag(\"alerts.gc-interval\", \"Interval between alert GC.\").Default(\"30m\").Duration()\n\t\tperAlertNameLimit           = kingpin.Flag(\"alerts.per-alertname-limit\", \"Maximum number of alerts per alertname. If negative or zero, no limit is set.\").Default(\"0\").Int()\n\t\tdispatchMaintenanceInterval = kingpin.Flag(\"dispatch.maintenance-interval\", \"Interval between maintenance of aggregation groups in the dispatcher.\").Default(\"30s\").Duration()\n\t\tDispatchStartDelay          = kingpin.Flag(\"dispatch.start-delay\", \"Minimum amount of time to wait before dispatching alerts. This option should be synced with value of --rules.alert.resend-delay on Prometheus.\").Default(\"0s\").Duration()\n\n\t\twebConfig      = webflag.AddFlags(kingpin.CommandLine, \":9093\")\n\t\texternalURL    = kingpin.Flag(\"web.external-url\", \"The URL under which Alertmanager is externally reachable (for example, if Alertmanager is served via a reverse proxy). Used for generating relative and absolute links back to Alertmanager itself. If the URL has a path portion, it will be used to prefix all HTTP endpoints served by Alertmanager. If omitted, relevant URL components will be derived automatically.\").String()\n\t\troutePrefix    = kingpin.Flag(\"web.route-prefix\", \"Prefix for the internal routes of web endpoints. Defaults to path of --web.external-url.\").String()\n\t\tgetConcurrency = kingpin.Flag(\"web.get-concurrency\", \"Maximum number of GET requests processed concurrently. If negative or zero, the limit is GOMAXPROC or 8, whichever is larger.\").Default(\"0\").Int()\n\t\thttpTimeout    = kingpin.Flag(\"web.timeout\", \"Timeout for HTTP requests. If negative or zero, no timeout is set.\").Default(\"0\").Duration()\n\n\t\tmemlimitRatio = kingpin.Flag(\"auto-gomemlimit.ratio\", \"The ratio of reserved GOMEMLIMIT memory to the detected maximum container or system memory. The value must be greater than 0 and less than or equal to 1.\").\n\t\t\t\tDefault(\"0.9\").Float64()\n\n\t\tclusterBindAddr = kingpin.Flag(\"cluster.listen-address\", \"Listen address for cluster. Set to empty string to disable HA mode.\").\n\t\t\t\tDefault(defaultClusterAddr).String()\n\t\tclusterAdvertiseAddr   = kingpin.Flag(\"cluster.advertise-address\", \"Explicit address to advertise in cluster.\").String()\n\t\tclusterPeerName        = kingpin.Flag(\"cluster.peer-name\", \"Explicit name of the peer, rather than generating a random one\").Default(\"\").String()\n\t\tpeers                  = kingpin.Flag(\"cluster.peer\", \"Initial peers (may be repeated).\").Strings()\n\t\tpeerTimeout            = kingpin.Flag(\"cluster.peer-timeout\", \"Time to wait between peers to send notifications.\").Default(\"15s\").Duration()\n\t\tpeersResolveTimeout    = kingpin.Flag(\"cluster.peers-resolve-timeout\", \"Time to resolve peers.\").Default(cluster.DefaultResolvePeersTimeout.String()).Duration()\n\t\tgossipInterval         = kingpin.Flag(\"cluster.gossip-interval\", \"Interval between sending gossip messages. By lowering this value (more frequent) gossip messages are propagated across the cluster more quickly at the expense of increased bandwidth.\").Default(cluster.DefaultGossipInterval.String()).Duration()\n\t\tpushPullInterval       = kingpin.Flag(\"cluster.pushpull-interval\", \"Interval for gossip state syncs. Setting this interval lower (more frequent) will increase convergence speeds across larger clusters at the expense of increased bandwidth usage.\").Default(cluster.DefaultPushPullInterval.String()).Duration()\n\t\ttcpTimeout             = kingpin.Flag(\"cluster.tcp-timeout\", \"Timeout for establishing a stream connection with a remote node for a full state sync, and for stream read and write operations.\").Default(cluster.DefaultTCPTimeout.String()).Duration()\n\t\tprobeTimeout           = kingpin.Flag(\"cluster.probe-timeout\", \"Timeout to wait for an ack from a probed node before assuming it is unhealthy. This should be set to 99-percentile of RTT (round-trip time) on your network.\").Default(cluster.DefaultProbeTimeout.String()).Duration()\n\t\tprobeInterval          = kingpin.Flag(\"cluster.probe-interval\", \"Interval between random node probes. Setting this lower (more frequent) will cause the cluster to detect failed nodes more quickly at the expense of increased bandwidth usage.\").Default(cluster.DefaultProbeInterval.String()).Duration()\n\t\tsettleTimeout          = kingpin.Flag(\"cluster.settle-timeout\", \"Maximum time to wait for cluster connections to settle before evaluating notifications.\").Default(cluster.DefaultPushPullInterval.String()).Duration()\n\t\treconnectInterval      = kingpin.Flag(\"cluster.reconnect-interval\", \"Interval between attempting to reconnect to lost peers.\").Default(cluster.DefaultReconnectInterval.String()).Duration()\n\t\tpeerReconnectTimeout   = kingpin.Flag(\"cluster.reconnect-timeout\", \"Length of time to attempt to reconnect to a lost peer.\").Default(cluster.DefaultReconnectTimeout.String()).Duration()\n\t\ttlsConfigFile          = kingpin.Flag(\"cluster.tls-config\", \"[EXPERIMENTAL] Path to config yaml file that can enable mutual TLS within the gossip protocol.\").Default(\"\").String()\n\t\tallowInsecureAdvertise = kingpin.Flag(\"cluster.allow-insecure-public-advertise-address-discovery\", \"[EXPERIMENTAL] Allow alertmanager to discover and listen on a public IP address.\").Bool()\n\t\tlabel                  = kingpin.Flag(\"cluster.label\", \"The cluster label is an optional string to include on each packet and stream. It uniquely identifies the cluster and prevents cross-communication issues when sending gossip messages.\").Default(\"\").String()\n\t\tfeatureFlags           = kingpin.Flag(\"enable-feature\", fmt.Sprintf(\"Comma-separated experimental features to enable. Valid options: %s\", strings.Join(featurecontrol.AllowedFlags, \", \"))).Default(\"\").String()\n\t)\n\n\tprometheus.MustRegister(versioncollector.NewCollector(\"alertmanager\"))\n\n\tpromslogflag.AddFlags(kingpin.CommandLine, &promslogConfig)\n\tkingpin.CommandLine.UsageWriter(os.Stdout)\n\n\tkingpin.Version(version.Print(\"alertmanager\"))\n\tkingpin.CommandLine.GetFlag(\"help\").Short('h')\n\tkingpin.Parse()\n\n\tlogger := promslog.New(&promslogConfig)\n\n\tlogger.Info(\"Starting Alertmanager\", \"version\", version.Info())\n\tstartTime := time.Now()\n\n\tlogger.Info(\"Build context\", \"build_context\", version.BuildContext())\n\n\tff, err := featurecontrol.NewFlags(logger, *featureFlags)\n\tif err != nil {\n\t\tlogger.Error(\"error parsing the feature flag list\", \"err\", err)\n\t\treturn 1\n\t}\n\tcompat.InitFromFlags(logger, ff)\n\n\tif ff.EnableAutoGOMEMLIMIT() {\n\t\tif *memlimitRatio <= 0.0 || *memlimitRatio > 1.0 {\n\t\t\tlogger.Error(\"--auto-gomemlimit.ratio must be greater than 0 and less than or equal to 1.\")\n\t\t\treturn 1\n\t\t}\n\n\t\tif _, err := memlimit.SetGoMemLimitWithOpts(\n\t\t\tmemlimit.WithRatio(*memlimitRatio),\n\t\t\tmemlimit.WithProvider(\n\t\t\t\tmemlimit.ApplyFallback(\n\t\t\t\t\tmemlimit.FromCgroup,\n\t\t\t\t\tmemlimit.FromSystem,\n\t\t\t\t),\n\t\t\t),\n\t\t); err != nil {\n\t\t\tlogger.Warn(\"automemlimit\", \"msg\", \"Failed to set GOMEMLIMIT automatically\", \"err\", err)\n\t\t}\n\t}\n\n\tif ff.EnableAutoGOMAXPROCS() {\n\t\tlogger.Warn(\"automaxprocs\", \"msg\", \"This flag is deprecated and will be removed in the next release\")\n\t}\n\n\terr = os.MkdirAll(*dataDir, 0o777)\n\tif err != nil {\n\t\tlogger.Error(\"Unable to create data directory\", \"err\", err)\n\t\treturn 1\n\t}\n\n\ttlsTransportConfig, err := cluster.GetTLSTransportConfig(*tlsConfigFile)\n\tif err != nil {\n\t\tlogger.Error(\"unable to initialize TLS transport configuration for gossip mesh\", \"err\", err)\n\t\treturn 1\n\t}\n\tvar peer *cluster.Peer\n\tif *clusterBindAddr != \"\" {\n\t\tpeer, err = cluster.Create(\n\t\t\tlogger.With(\"component\", \"cluster\"),\n\t\t\tprometheus.DefaultRegisterer,\n\t\t\t*clusterBindAddr,\n\t\t\t*clusterAdvertiseAddr,\n\t\t\t*peers,\n\t\t\ttrue,\n\t\t\t*pushPullInterval,\n\t\t\t*gossipInterval,\n\t\t\t*tcpTimeout,\n\t\t\t*peersResolveTimeout,\n\t\t\t*probeTimeout,\n\t\t\t*probeInterval,\n\t\t\ttlsTransportConfig,\n\t\t\t*allowInsecureAdvertise,\n\t\t\t*label,\n\t\t\t*clusterPeerName,\n\t\t)\n\t\tif err != nil {\n\t\t\tlogger.Error(\"unable to initialize gossip mesh\", \"err\", err)\n\t\t\treturn 1\n\t\t}\n\t\tclusterEnabled.Set(1)\n\t}\n\n\tstopc := make(chan struct{})\n\tvar wg sync.WaitGroup\n\n\tnotificationLogOpts := nflog.Options{\n\t\tSnapshotFile: filepath.Join(*dataDir, \"nflog\"),\n\t\tRetention:    *retention,\n\t\tLogger:       logger.With(\"component\", \"nflog\"),\n\t\tMetrics:      prometheus.DefaultRegisterer,\n\t}\n\n\tnotificationLog, err := nflog.New(notificationLogOpts)\n\tif err != nil {\n\t\tlogger.Error(\"error creating notification log\", \"err\", err)\n\t\treturn 1\n\t}\n\tif peer != nil {\n\t\tc := peer.AddState(\"nfl\", notificationLog, prometheus.DefaultRegisterer)\n\t\tnotificationLog.SetBroadcast(c.Broadcast)\n\t}\n\n\twg.Go(func() {\n\t\tnotificationLog.Maintenance(*maintenanceInterval, filepath.Join(*dataDir, \"nflog\"), stopc, nil)\n\t})\n\n\tmarker := types.NewMarker(prometheus.DefaultRegisterer)\n\n\tsilenceOpts := silence.Options{\n\t\tSnapshotFile: filepath.Join(*dataDir, \"silences\"),\n\t\tRetention:    *retention,\n\t\tLimits: silence.Limits{\n\t\t\tMaxSilences:         func() int { return *maxSilences },\n\t\t\tMaxSilenceSizeBytes: func() int { return *maxSilenceSizeBytes },\n\t\t},\n\t\tLogger:  logger.With(\"component\", \"silences\"),\n\t\tMetrics: prometheus.DefaultRegisterer,\n\t}\n\n\tsilences, err := silence.New(silenceOpts)\n\tif err != nil {\n\t\tlogger.Error(\"error creating silence\", \"err\", err)\n\t\treturn 1\n\t}\n\tif peer != nil {\n\t\tc := peer.AddState(\"sil\", silences, prometheus.DefaultRegisterer)\n\t\tsilences.SetBroadcast(c.Broadcast)\n\t}\n\n\t// Start providers before router potentially sends updates.\n\twg.Go(func() {\n\t\tsilences.Maintenance(*maintenanceInterval, filepath.Join(*dataDir, \"silences\"), stopc, nil)\n\t})\n\n\tdefer func() {\n\t\tclose(stopc)\n\t\twg.Wait()\n\t}()\n\n\tsilencer := silence.NewSilencer(silences, marker, logger)\n\n\t// Peer state listeners have been registered, now we can join and get the initial state.\n\tif peer != nil {\n\t\terr = peer.Join(\n\t\t\t*reconnectInterval,\n\t\t\t*peerReconnectTimeout,\n\t\t)\n\t\tif err != nil {\n\t\t\tlogger.Warn(\"unable to join gossip mesh\", \"err\", err)\n\t\t}\n\t\tctx, cancel := context.WithTimeout(context.Background(), *settleTimeout)\n\t\tdefer func() {\n\t\t\tcancel()\n\t\t\tif err := peer.Leave(10 * time.Second); err != nil {\n\t\t\t\tlogger.Warn(\"unable to leave gossip mesh\", \"err\", err)\n\t\t\t}\n\t\t}()\n\t\tgo peer.Settle(ctx, *gossipInterval*10)\n\t}\n\n\talerts, err := mem.NewAlerts(\n\t\tcontext.Background(),\n\t\tmarker,\n\t\t*alertGCInterval,\n\t\t*perAlertNameLimit,\n\t\tsilencer,\n\t\tlogger,\n\t\tprometheus.DefaultRegisterer,\n\t\tff,\n\t)\n\tif err != nil {\n\t\tlogger.Error(\"error creating memory provider\", \"err\", err)\n\t\treturn 1\n\t}\n\tdefer alerts.Close()\n\n\tvar disp atomic.Pointer[dispatch.Dispatcher]\n\tdefer func() {\n\t\tdisp.Load().Stop()\n\t}()\n\n\tgroupFn := func(ctx context.Context, routeFilter func(*dispatch.Route) bool, alertFilter func(*types.Alert, time.Time) bool) (dispatch.AlertGroups, map[model.Fingerprint][]string, error) {\n\t\treturn disp.Load().Groups(ctx, routeFilter, alertFilter)\n\t}\n\n\t// An interface value that holds a nil concrete value is non-nil.\n\t// Therefore we explicly pass an empty interface, to detect if the\n\t// cluster is not enabled in notify.\n\tvar clusterPeer cluster.ClusterPeer\n\tif peer != nil {\n\t\tclusterPeer = peer\n\t}\n\n\tapi, err := api.New(api.Options{\n\t\tAlerts:          alerts,\n\t\tSilences:        silences,\n\t\tAlertStatusFunc: marker.Status,\n\t\tGroupMutedFunc:  marker.Muted,\n\t\tPeer:            clusterPeer,\n\t\tTimeout:         *httpTimeout,\n\t\tConcurrency:     *getConcurrency,\n\t\tLogger:          logger.With(\"component\", \"api\"),\n\t\tRegistry:        prometheus.DefaultRegisterer,\n\t\tRequestDuration: requestDuration,\n\t\tGroupFunc:       groupFn,\n\t})\n\tif err != nil {\n\t\tlogger.Error(\"failed to create API\", \"err\", err)\n\t\treturn 1\n\t}\n\n\tamURL, err := extURL(logger, os.Hostname, (*webConfig.WebListenAddresses)[0], *externalURL)\n\tif err != nil {\n\t\tlogger.Error(\"failed to determine external URL\", \"err\", err)\n\t\treturn 1\n\t}\n\tlogger.Debug(\"external url\", \"externalUrl\", amURL.String())\n\n\twaitFunc := func() time.Duration { return 0 }\n\tif peer != nil {\n\t\twaitFunc = clusterWait(peer, *peerTimeout)\n\t}\n\ttimeoutFunc := func(d time.Duration) time.Duration {\n\t\tif d < notify.MinTimeout {\n\t\t\td = notify.MinTimeout\n\t\t}\n\t\treturn d + waitFunc()\n\t}\n\n\ttracingManager := tracing.NewManager(logger.With(\"component\", \"tracing\"))\n\n\tvar (\n\t\tinhibitor atomic.Pointer[inhibit.Inhibitor]\n\t\ttmpl      *template.Template\n\t)\n\n\tdispMetrics := dispatch.NewDispatcherMetrics(false, prometheus.DefaultRegisterer)\n\tpipelineBuilder := notify.NewPipelineBuilder(prometheus.DefaultRegisterer, ff)\n\tconfigLogger := logger.With(\"component\", \"configuration\")\n\tconfigCoordinator := config.NewCoordinator(\n\t\t*configFile,\n\t\tprometheus.DefaultRegisterer,\n\t\tconfigLogger,\n\t)\n\tconfigCoordinator.Subscribe(func(conf *config.Config) error {\n\t\ttmpl, err = template.FromGlobs(conf.Templates)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to parse templates: %w\", err)\n\t\t}\n\t\ttmpl.ExternalURL = amURL\n\n\t\t// Build the routing tree and record which receivers are used.\n\t\troutes := dispatch.NewRoute(conf.Route, nil)\n\t\tactiveReceivers := make(map[string]struct{})\n\t\troutes.Walk(func(r *dispatch.Route) {\n\t\t\tactiveReceivers[r.RouteOpts.Receiver] = struct{}{}\n\t\t})\n\n\t\t// Build the map of receiver to integrations.\n\t\treceivers := make(map[string][]notify.Integration, len(activeReceivers))\n\t\tvar integrationsNum int\n\t\tfor _, rcv := range conf.Receivers {\n\t\t\tif _, found := activeReceivers[rcv.Name]; !found {\n\t\t\t\t// No need to build a receiver if no route is using it.\n\t\t\t\tconfigLogger.Info(\"skipping creation of receiver not referenced by any route\", \"receiver\", rcv.Name)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tintegrations, err := receiver.BuildReceiverIntegrations(rcv, tmpl, logger)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// rcv.Name is guaranteed to be unique across all receivers.\n\t\t\treceivers[rcv.Name] = integrations\n\t\t\tintegrationsNum += len(integrations)\n\t\t}\n\n\t\t// Build the map of time interval names to time interval definitions.\n\t\ttimeIntervals := make(map[string][]timeinterval.TimeInterval, len(conf.MuteTimeIntervals)+len(conf.TimeIntervals))\n\t\tfor _, ti := range conf.MuteTimeIntervals {\n\t\t\ttimeIntervals[ti.Name] = ti.TimeIntervals\n\t\t}\n\n\t\tfor _, ti := range conf.TimeIntervals {\n\t\t\ttimeIntervals[ti.Name] = ti.TimeIntervals\n\t\t}\n\n\t\tintervener := timeinterval.NewIntervener(timeIntervals)\n\n\t\tinhibitor.Load().Stop()\n\t\tdisp.Load().Stop()\n\n\t\tnewInhibitor := inhibit.NewInhibitor(alerts, conf.InhibitRules, marker, logger)\n\t\tinhibitor.Store(newInhibitor)\n\n\t\t// An interface value that holds a nil concrete value is non-nil.\n\t\t// Therefore we explicly pass an empty interface, to detect if the\n\t\t// cluster is not enabled in notify.\n\t\tvar pipelinePeer notify.Peer\n\t\tif peer != nil {\n\t\t\tpipelinePeer = peer\n\t\t}\n\n\t\tpipeline := pipelineBuilder.New(\n\t\t\treceivers,\n\t\t\twaitFunc,\n\t\t\tnewInhibitor,\n\t\t\tsilencer,\n\t\t\tintervener,\n\t\t\tmarker,\n\t\t\tnotificationLog,\n\t\t\tpipelinePeer,\n\t\t)\n\n\t\tconfiguredReceivers.Set(float64(len(activeReceivers)))\n\t\tconfiguredIntegrations.Set(float64(integrationsNum))\n\t\tconfiguredInhibitionRules.Set(float64(len(conf.InhibitRules)))\n\n\t\tapi.Update(conf, func(ctx context.Context, labels model.LabelSet) {\n\t\t\tinhibitor.Load().Mutes(ctx, labels)\n\t\t\tsilencer.Mutes(ctx, labels)\n\t\t})\n\n\t\tnewDisp := dispatch.NewDispatcher(\n\t\t\talerts,\n\t\t\troutes,\n\t\t\tpipeline,\n\t\t\tmarker,\n\t\t\ttimeoutFunc,\n\t\t\t*dispatchMaintenanceInterval,\n\t\t\tnil,\n\t\t\tlogger,\n\t\t\tdispMetrics,\n\t\t)\n\t\troutes.Walk(func(r *dispatch.Route) {\n\t\t\tif r.RouteOpts.RepeatInterval > *retention {\n\t\t\t\tconfigLogger.Warn(\n\t\t\t\t\t\"repeat_interval is greater than the data retention period. It can lead to notifications being repeated more often than expected.\",\n\t\t\t\t\t\"repeat_interval\",\n\t\t\t\t\tr.RouteOpts.RepeatInterval,\n\t\t\t\t\t\"retention\",\n\t\t\t\t\t*retention,\n\t\t\t\t\t\"route\",\n\t\t\t\t\tr.Key(),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tif r.RouteOpts.RepeatInterval < r.RouteOpts.GroupInterval {\n\t\t\t\tconfigLogger.Warn(\n\t\t\t\t\t\"repeat_interval is less than group_interval. Notifications will not repeat until the next group_interval.\",\n\t\t\t\t\t\"repeat_interval\",\n\t\t\t\t\tr.RouteOpts.RepeatInterval,\n\t\t\t\t\t\"group_interval\",\n\t\t\t\t\tr.RouteOpts.GroupInterval,\n\t\t\t\t\t\"route\",\n\t\t\t\t\tr.Key(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\n\t\t// first, start the inhibitor so the inhibition cache can populate\n\t\t// wait for this to load alerts before starting the dispatcher so\n\t\t// we don't accidentially notify for an alert that will be inhibited\n\t\tgo newInhibitor.Run()\n\t\tnewInhibitor.WaitForLoading()\n\n\t\t// next, start the dispatcher and wait for it to load before swapping the disp pointer.\n\t\t// This ensures that the API doesn't see the new dispatcher before it finishes populating\n\t\t// the aggrGroups\n\t\tgo newDisp.Run(startTime.Add(*DispatchStartDelay))\n\t\tnewDisp.WaitForLoading()\n\t\tdisp.Store(newDisp)\n\n\t\terr = tracingManager.ApplyConfig(conf.TracingConfig)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to apply tracing config: %w\", err)\n\t\t}\n\n\t\tgo tracingManager.Run()\n\n\t\treturn nil\n\t})\n\n\tif err := configCoordinator.Reload(); err != nil {\n\t\treturn 1\n\t}\n\n\t// Make routePrefix default to externalURL path if empty string.\n\tif *routePrefix == \"\" {\n\t\t*routePrefix = amURL.Path\n\t}\n\t*routePrefix = \"/\" + strings.Trim(*routePrefix, \"/\")\n\tlogger.Debug(\"route prefix\", \"routePrefix\", *routePrefix)\n\n\trouter := route.New().WithInstrumentation(instrumentHandler)\n\tif *routePrefix != \"/\" {\n\t\trouter.Get(\"/\", func(w http.ResponseWriter, r *http.Request) {\n\t\t\thttp.Redirect(w, r, *routePrefix, http.StatusFound)\n\t\t})\n\t\trouter = router.WithPrefix(*routePrefix)\n\t}\n\n\twebReload := make(chan chan error)\n\n\tui.Register(router, webReload, logger)\n\n\tmux := api.Register(router, *routePrefix)\n\n\tsrv := &http.Server{\n\t\t// instrument all handlers with tracing\n\t\tHandler: tracing.Middleware(mux),\n\t}\n\tsrvc := make(chan struct{})\n\n\tgo func() {\n\t\tif err := web.ListenAndServe(srv, webConfig, logger); !errors.Is(err, http.ErrServerClosed) {\n\t\t\tlogger.Error(\"Listen error\", \"err\", err)\n\t\t\tclose(srvc)\n\t\t}\n\t\tdefer func() {\n\t\t\tif err := srv.Close(); err != nil {\n\t\t\t\tlogger.Error(\"Error on closing the server\", \"err\", err)\n\t\t\t}\n\t\t}()\n\t}()\n\n\tvar (\n\t\thup  = make(chan os.Signal, 1)\n\t\tterm = make(chan os.Signal, 1)\n\t)\n\tsignal.Notify(hup, syscall.SIGHUP)\n\tsignal.Notify(term, os.Interrupt, syscall.SIGTERM)\n\n\tfor {\n\t\tselect {\n\t\tcase <-hup:\n\t\t\t// ignore error, already logged in `reload()`\n\t\t\t_ = configCoordinator.Reload()\n\t\tcase errc := <-webReload:\n\t\t\terrc <- configCoordinator.Reload()\n\t\tcase <-term:\n\t\t\tlogger.Info(\"Received SIGTERM, exiting gracefully...\")\n\n\t\t\t// shut down the tracing manager to flush any remaining spans.\n\t\t\t// this blocks for up to 5s\n\t\t\ttracingManager.Stop()\n\n\t\t\treturn 0\n\t\tcase <-srvc:\n\t\t\treturn 1\n\t\t}\n\t}\n}\n\n// clusterWait returns a function that inspects the current peer state and returns\n// a duration of one base timeout for each peer with a higher ID than ourselves.\nfunc clusterWait(p *cluster.Peer, timeout time.Duration) func() time.Duration {\n\treturn func() time.Duration {\n\t\treturn time.Duration(p.Position()) * timeout\n\t}\n}\n\nfunc extURL(logger *slog.Logger, hostnamef func() (string, error), listen, external string) (*url.URL, error) {\n\tif external == \"\" {\n\t\thostname, err := hostnamef()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t_, port, err := net.SplitHostPort(listen)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif port == \"\" {\n\t\t\tlogger.Warn(\"no port found for listen address\", \"address\", listen)\n\t\t}\n\n\t\texternal = fmt.Sprintf(\"http://%s:%s/\", hostname, port)\n\t}\n\n\tu, err := url.Parse(external)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif u.Scheme != \"http\" && u.Scheme != \"https\" {\n\t\treturn nil, fmt.Errorf(\"%q: invalid %q scheme, only 'http' and 'https' are supported\", u.String(), u.Scheme)\n\t}\n\n\tppref := strings.TrimRight(u.Path, \"/\")\n\tif ppref != \"\" && !strings.HasPrefix(ppref, \"/\") {\n\t\tppref = \"/\" + ppref\n\t}\n\tu.Path = ppref\n\n\treturn u, nil\n}\n"
  },
  {
    "path": "cmd/alertmanager/main_test.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage main\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestExternalURL(t *testing.T) {\n\thostname := \"foo\"\n\tfor _, tc := range []struct {\n\t\thostnameResolver func() (string, error)\n\t\texternal         string\n\t\tlisten           string\n\n\t\texpURL string\n\t\terr    bool\n\t}{\n\t\t{\n\t\t\tlisten: \":9093\",\n\t\t\texpURL: \"http://\" + hostname + \":9093\",\n\t\t},\n\t\t{\n\t\t\tlisten: \"localhost:9093\",\n\t\t\texpURL: \"http://\" + hostname + \":9093\",\n\t\t},\n\t\t{\n\t\t\tlisten: \"localhost:\",\n\t\t\texpURL: \"http://\" + hostname + \":\",\n\t\t},\n\t\t{\n\t\t\texternal: \"https://host.example.com\",\n\t\t\texpURL:   \"https://host.example.com\",\n\t\t},\n\t\t{\n\t\t\texternal: \"https://host.example.com/\",\n\t\t\texpURL:   \"https://host.example.com\",\n\t\t},\n\t\t{\n\t\t\texternal: \"http://host.example.com/alertmanager\",\n\t\t\texpURL:   \"http://host.example.com/alertmanager\",\n\t\t},\n\t\t{\n\t\t\texternal: \"http://host.example.com/alertmanager/\",\n\t\t\texpURL:   \"http://host.example.com/alertmanager\",\n\t\t},\n\t\t{\n\t\t\texternal: \"http://host.example.com/////alertmanager//\",\n\t\t\texpURL:   \"http://host.example.com/////alertmanager\",\n\t\t},\n\t\t{\n\t\t\terr: true,\n\t\t},\n\t\t{\n\t\t\thostnameResolver: func() (string, error) { return \"\", fmt.Errorf(\"some error\") },\n\t\t\terr:              true,\n\t\t},\n\t\t{\n\t\t\texternal: \"://broken url string\",\n\t\t\terr:      true,\n\t\t},\n\t\t{\n\t\t\texternal: \"host.example.com:8080\",\n\t\t\terr:      true,\n\t\t},\n\t} {\n\t\tif tc.hostnameResolver == nil {\n\t\t\ttc.hostnameResolver = func() (string, error) {\n\t\t\t\treturn hostname, nil\n\t\t\t}\n\t\t}\n\t\tt.Run(fmt.Sprintf(\"external=%q,listen=%q\", tc.external, tc.listen), func(t *testing.T) {\n\t\t\tu, err := extURL(promslog.NewNopLogger(), tc.hostnameResolver, tc.listen, tc.external)\n\t\t\tif tc.err {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.expURL, u.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/amtool/README.md",
    "content": "# Generating amtool artifacts\n\nAmtool comes with the option to create a number of ease-of-use artifacts that can be created.\n\n## Shell completion\n\nA bash completion script can be generated by calling `amtool --completion-script-bash`.\n\nThe bash completion file can be added to `/etc/bash_completion.d/`.\n\n## Man pages\n\nA man page can be generated by calling `amtool --help-man`.\n\nMan pages can be added to the man directory of your choice\n\n    amtool --help-man > /usr/local/share/man/man1/amtool.1\n    sudo mandb\n\nThen you should be able to view the man pages as expected.\n"
  },
  {
    "path": "cmd/amtool/main.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage main\n\nimport \"github.com/prometheus/alertmanager/cli\"\n\nfunc main() {\n\tcli.Execute()\n}\n"
  },
  {
    "path": "config/common/inhibitrule.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage common\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/prometheus/common/model\"\n\n\t\"github.com/prometheus/alertmanager/matcher/compat\"\n)\n\n// InhibitRule defines an inhibition rule that mutes alerts that match the\n// target labels if an alert matching the source labels exists.\n// Both alerts have to have a set of labels being equal.\ntype InhibitRule struct {\n\t// Name is an optional name for the inhibition rule.\n\tName string `yaml:\"name,omitempty\" json:\"name,omitempty\"`\n\t// SourceMatch defines a set of labels that have to equal the given\n\t// value for source alerts. Deprecated. Remove before v1.0 release.\n\tSourceMatch map[string]string `yaml:\"source_match,omitempty\" json:\"source_match,omitempty\"`\n\t// SourceMatchRE defines pairs like SourceMatch but does regular expression\n\t// matching. Deprecated. Remove before v1.0 release.\n\tSourceMatchRE MatchRegexps `yaml:\"source_match_re,omitempty\" json:\"source_match_re,omitempty\"`\n\t// SourceMatchers defines a set of label matchers that have to be fulfilled for source alerts.\n\tSourceMatchers Matchers `yaml:\"source_matchers,omitempty\" json:\"source_matchers,omitempty\"`\n\t// TargetMatch defines a set of labels that have to equal the given\n\t// value for target alerts. Deprecated. Remove before v1.0 release.\n\tTargetMatch map[string]string `yaml:\"target_match,omitempty\" json:\"target_match,omitempty\"`\n\t// TargetMatchRE defines pairs like TargetMatch but does regular expression\n\t// matching. Deprecated. Remove before v1.0 release.\n\tTargetMatchRE MatchRegexps `yaml:\"target_match_re,omitempty\" json:\"target_match_re,omitempty\"`\n\t// TargetMatchers defines a set of label matchers that have to be fulfilled for target alerts.\n\tTargetMatchers Matchers `yaml:\"target_matchers,omitempty\" json:\"target_matchers,omitempty\"`\n\t// A set of labels that must be equal between the source and target alert\n\t// for them to be a match.\n\tEqual []string `yaml:\"equal,omitempty\" json:\"equal,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for InhibitRule.\nfunc (r *InhibitRule) UnmarshalYAML(unmarshal func(any) error) error {\n\ttype plain InhibitRule\n\tif err := unmarshal((*plain)(r)); err != nil {\n\t\treturn err\n\t}\n\n\tfor k := range r.SourceMatch {\n\t\tif !model.LabelNameRE.MatchString(k) {\n\t\t\treturn fmt.Errorf(\"invalid label name %q\", k)\n\t\t}\n\t}\n\n\tfor k := range r.TargetMatch {\n\t\tif !model.LabelNameRE.MatchString(k) {\n\t\t\treturn fmt.Errorf(\"invalid label name %q\", k)\n\t\t}\n\t}\n\n\tfor _, l := range r.Equal {\n\t\tlabelName := model.LabelName(l)\n\t\tif !compat.IsValidLabelName(labelName) {\n\t\t\treturn fmt.Errorf(\"invalid label name %q in equal list\", l)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "config/common/inhibitrule_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage common\n\nimport (\n\t\"testing\"\n\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\t\"gopkg.in/yaml.v2\"\n\n\t\"github.com/prometheus/alertmanager/featurecontrol\"\n\t\"github.com/prometheus/alertmanager/matcher/compat\"\n)\n\nfunc mustUnmarshalInhibitRule(t *testing.T, input string) InhibitRule {\n\tt.Helper()\n\tvar r InhibitRule\n\terr := yaml.Unmarshal([]byte(input), &r)\n\trequire.NoError(t, err)\n\treturn r\n}\n\nfunc unmarshalInhibitRule(input string) (InhibitRule, error) {\n\tvar r InhibitRule\n\terr := yaml.Unmarshal([]byte(input), &r)\n\treturn r, err\n}\n\nconst inhibitRuleEqualYAML = `\nsource_matchers: ['foo=bar']\ntarget_matchers: ['bar=baz']\nequal: ['qux', 'corge']\n`\n\nconst inhibitRuleEqualUTF8YAML = `\nsource_matchers: ['foo=bar']\ntarget_matchers: ['bar=baz']\nequal: ['qux🙂', 'corge']\n`\n\nfunc TestInhibitRuleEqual(t *testing.T) {\n\tr := mustUnmarshalInhibitRule(t, inhibitRuleEqualYAML)\n\n\t// The inhibition rule should have the expected equal labels.\n\trequire.Equal(t, []string{\"qux\", \"corge\"}, r.Equal)\n\n\t// Should not be able to unmarshal configuration with UTF-8 in equals list.\n\t_, err := unmarshalInhibitRule(inhibitRuleEqualUTF8YAML)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"invalid label name \\\"qux🙂\\\" in equal list\", err.Error())\n\n\t// Change the mode to UTF-8 mode.\n\tff, err := featurecontrol.NewFlags(promslog.NewNopLogger(), featurecontrol.FeatureUTF8StrictMode)\n\trequire.NoError(t, err)\n\tcompat.InitFromFlags(promslog.NewNopLogger(), ff)\n\n\t// Restore the mode to classic at the end of the test.\n\tff, err = featurecontrol.NewFlags(promslog.NewNopLogger(), featurecontrol.FeatureClassicMode)\n\trequire.NoError(t, err)\n\tdefer compat.InitFromFlags(promslog.NewNopLogger(), ff)\n\n\tr = mustUnmarshalInhibitRule(t, inhibitRuleEqualYAML)\n\n\t// The inhibition rule should have the expected equal labels.\n\trequire.Equal(t, []string{\"qux\", \"corge\"}, r.Equal)\n\n\t// Should also be able to unmarshal configuration with UTF-8 in equals list.\n\tr = mustUnmarshalInhibitRule(t, inhibitRuleEqualUTF8YAML)\n\n\t// The inhibition rule should have the expected equal labels.\n\trequire.Equal(t, []string{\"qux🙂\", \"corge\"}, r.Equal)\n}\n"
  },
  {
    "path": "config/common/matchers.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage common\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"sort\"\n\n\t\"github.com/prometheus/common/model\"\n\n\t\"github.com/prometheus/alertmanager/matcher/compat\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n)\n\n// MatchRegexps represents a map of Regexp.\ntype MatchRegexps map[string]Regexp\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for MatchRegexps.\nfunc (m *MatchRegexps) UnmarshalYAML(unmarshal func(any) error) error {\n\ttype plain MatchRegexps\n\tif err := unmarshal((*plain)(m)); err != nil {\n\t\treturn err\n\t}\n\tfor k, v := range *m {\n\t\tif !model.LabelNameRE.MatchString(k) {\n\t\t\treturn fmt.Errorf(\"invalid label name %q\", k)\n\t\t}\n\t\tif v.Regexp == nil {\n\t\t\treturn fmt.Errorf(\"invalid regexp value for %q\", k)\n\t\t}\n\t}\n\treturn nil\n}\n\n// Regexp encapsulates a regexp.Regexp and makes it YAML marshalable.\ntype Regexp struct {\n\t*regexp.Regexp\n\tOriginal string\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for Regexp.\nfunc (re *Regexp) UnmarshalYAML(unmarshal func(any) error) error {\n\tvar s string\n\tif err := unmarshal(&s); err != nil {\n\t\treturn err\n\t}\n\tregex, err := regexp.Compile(\"^(?:\" + s + \")$\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tre.Regexp = regex\n\tre.Original = s\n\treturn nil\n}\n\n// MarshalYAML implements the yaml.Marshaler interface for Regexp.\nfunc (re Regexp) MarshalYAML() (any, error) {\n\tif re.Original != \"\" {\n\t\treturn re.Original, nil\n\t}\n\treturn nil, nil\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface for Regexp.\nfunc (re *Regexp) UnmarshalJSON(data []byte) error {\n\tvar s string\n\tif err := json.Unmarshal(data, &s); err != nil {\n\t\treturn err\n\t}\n\tregex, err := regexp.Compile(\"^(?:\" + s + \")$\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tre.Regexp = regex\n\tre.Original = s\n\treturn nil\n}\n\n// MarshalJSON implements the json.Marshaler interface for Regexp.\nfunc (re Regexp) MarshalJSON() ([]byte, error) {\n\tif re.Original != \"\" {\n\t\treturn json.Marshal(re.Original)\n\t}\n\treturn []byte(\"null\"), nil\n}\n\n// Matchers is label.Matchers with an added UnmarshalYAML method to implement the yaml.Unmarshaler interface\n// and MarshalYAML to implement the yaml.Marshaler interface.\ntype Matchers labels.Matchers\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for Matchers.\nfunc (m *Matchers) UnmarshalYAML(unmarshal func(any) error) error {\n\tvar lines []string\n\tif err := unmarshal(&lines); err != nil {\n\t\treturn err\n\t}\n\tfor _, line := range lines {\n\t\tpm, err := compat.Matchers(line, \"config\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*m = append(*m, pm...)\n\t}\n\tsort.Sort(labels.Matchers(*m))\n\treturn nil\n}\n\n// MarshalYAML implements the yaml.Marshaler interface for Matchers.\nfunc (m Matchers) MarshalYAML() (any, error) {\n\tresult := make([]string, len(m))\n\tfor i, matcher := range m {\n\t\tresult[i] = matcher.String()\n\t}\n\treturn result, nil\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface for Matchers.\nfunc (m *Matchers) UnmarshalJSON(data []byte) error {\n\tvar lines []string\n\tif err := json.Unmarshal(data, &lines); err != nil {\n\t\treturn err\n\t}\n\tfor _, line := range lines {\n\t\tpm, err := compat.Matchers(line, \"config\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*m = append(*m, pm...)\n\t}\n\tsort.Sort(labels.Matchers(*m))\n\treturn nil\n}\n\n// MarshalJSON implements the json.Marshaler interface for Matchers.\nfunc (m Matchers) MarshalJSON() ([]byte, error) {\n\tif len(m) == 0 {\n\t\treturn []byte(\"[]\"), nil\n\t}\n\tresult := make([]string, len(m))\n\tfor i, matcher := range m {\n\t\tresult[i] = matcher.String()\n\t}\n\treturn json.Marshal(result)\n}\n"
  },
  {
    "path": "config/common/matchers_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage common\n\nimport (\n\t\"encoding/json\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"gopkg.in/yaml.v2\"\n)\n\nfunc TestMarshalRegexpWithNilValue(t *testing.T) {\n\tr := &Regexp{}\n\n\tout, err := json.Marshal(r)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"null\", string(out))\n\n\tout, err = yaml.Marshal(r)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"null\\n\", string(out))\n}\n\nfunc TestUnmarshalEmptyRegexp(t *testing.T) {\n\tb := []byte(`\"\"`)\n\n\t{\n\t\tvar re Regexp\n\t\terr := json.Unmarshal(b, &re)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, regexp.MustCompile(\"^(?:)$\"), re.Regexp)\n\t\trequire.Empty(t, re.Original)\n\t}\n\n\t{\n\t\tvar re Regexp\n\t\terr := yaml.Unmarshal(b, &re)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, regexp.MustCompile(\"^(?:)$\"), re.Regexp)\n\t\trequire.Empty(t, re.Original)\n\t}\n}\n\nfunc TestUnmarshalNullRegexp(t *testing.T) {\n\tinput := []byte(`null`)\n\n\t{\n\t\tvar re Regexp\n\t\terr := json.Unmarshal(input, &re)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, re.Original)\n\t}\n\n\t{\n\t\tvar re Regexp\n\t\terr := yaml.Unmarshal(input, &re) // Interestingly enough, unmarshalling `null` in YAML doesn't even call UnmarshalYAML.\n\t\trequire.NoError(t, err)\n\t\trequire.Nil(t, re.Regexp)\n\t\trequire.Empty(t, re.Original)\n\t}\n}\n\nfunc TestMarshalEmptyMatchers(t *testing.T) {\n\tr := Matchers{}\n\n\tout, err := json.Marshal(r)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"[]\", string(out))\n\n\tout, err = yaml.Marshal(r)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"[]\\n\", string(out))\n}\n"
  },
  {
    "path": "config/common/notifierconfig.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage common\n\n// NotifierConfig contains base options common across all notifier configurations.\ntype NotifierConfig struct {\n\tVSendResolved bool `yaml:\"send_resolved\" json:\"send_resolved\"`\n}\n\nfunc (nc *NotifierConfig) SendResolved() bool {\n\treturn nc.VSendResolved\n}\n"
  },
  {
    "path": "config/common/url.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage common\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"net/url\"\n\t\"strings\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n)\n\nconst SecretToken = \"<secret>\"\n\nvar SecretTokenJSON string\n\nfunc init() {\n\tb, err := json.Marshal(SecretToken)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tSecretTokenJSON = string(b)\n}\n\n// URL is a custom type that represents an HTTP or HTTPS URL and allows validation at configuration load time.\ntype URL struct {\n\t*url.URL\n}\n\n// Copy makes a deep-copy of the struct.\nfunc (u *URL) Copy() *URL {\n\tv := *u.URL\n\treturn &URL{&v}\n}\n\n// MarshalYAML implements the yaml.Marshaler interface for URL.\nfunc (u URL) MarshalYAML() (any, error) {\n\tif u.URL != nil {\n\t\treturn u.String(), nil\n\t}\n\treturn nil, nil\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for URL.\nfunc (u *URL) UnmarshalYAML(unmarshal func(any) error) error {\n\tvar s string\n\tif err := unmarshal(&s); err != nil {\n\t\treturn err\n\t}\n\turlp, err := ParseURL(s)\n\tif err != nil {\n\t\treturn err\n\t}\n\tu.URL = urlp.URL\n\treturn nil\n}\n\n// MarshalJSON implements the json.Marshaler interface for URL.\nfunc (u URL) MarshalJSON() ([]byte, error) {\n\tif u.URL != nil {\n\t\treturn json.Marshal(u.String())\n\t}\n\treturn []byte(\"null\"), nil\n}\n\n// UnmarshalJSON implements the json.Marshaler interface for URL.\nfunc (u *URL) UnmarshalJSON(data []byte) error {\n\tvar s string\n\tif err := json.Unmarshal(data, &s); err != nil {\n\t\treturn err\n\t}\n\turlp, err := ParseURL(s)\n\tif err != nil {\n\t\treturn err\n\t}\n\tu.URL = urlp.URL\n\treturn nil\n}\n\n// SecretURL is a URL that must not be revealed on marshaling.\ntype SecretURL URL\n\n// MarshalYAML implements the yaml.Marshaler interface for SecretURL.\nfunc (s SecretURL) MarshalYAML() (any, error) {\n\tif s.URL != nil {\n\t\tif commoncfg.MarshalSecretValue {\n\t\t\treturn s.String(), nil\n\t\t}\n\t\treturn SecretToken, nil\n\t}\n\treturn nil, nil\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for SecretURL.\nfunc (s *SecretURL) UnmarshalYAML(unmarshal func(any) error) error {\n\tvar str string\n\tif err := unmarshal(&str); err != nil {\n\t\treturn err\n\t}\n\t// In order to deserialize a previously serialized configuration (eg from\n\t// the Alertmanager API with amtool), `<secret>` needs to be treated\n\t// specially, as it isn't a valid URL.\n\tif str == SecretToken {\n\t\ts.URL = &url.URL{}\n\t\treturn nil\n\t}\n\treturn unmarshal((*URL)(s))\n}\n\n// MarshalJSON implements the json.Marshaler interface for SecretURL.\nfunc (s SecretURL) MarshalJSON() ([]byte, error) {\n\tif s.URL == nil {\n\t\treturn json.Marshal(\"\")\n\t}\n\tif commoncfg.MarshalSecretValue {\n\t\treturn json.Marshal(s.String())\n\t}\n\treturn json.Marshal(SecretToken)\n}\n\n// UnmarshalJSON implements the json.Marshaler interface for SecretURL.\nfunc (s *SecretURL) UnmarshalJSON(data []byte) error {\n\t// In order to deserialize a previously serialized configuration (eg from\n\t// the Alertmanager API with amtool), `<secret>` needs to be treated\n\t// specially, as it isn't a valid URL.\n\tif string(data) == SecretToken || string(data) == SecretTokenJSON {\n\t\ts.URL = &url.URL{}\n\t\treturn nil\n\t}\n\t// Redact the secret URL in case of errors\n\tif err := json.Unmarshal(data, (*URL)(s)); err != nil {\n\t\tif commoncfg.MarshalSecretValue {\n\t\t\treturn err\n\t\t}\n\t\treturn errors.New(strings.ReplaceAll(err.Error(), string(data), \"[REDACTED]\"))\n\t}\n\n\treturn nil\n}\n\n// containsTemplating checks if the string contains template syntax.\nfunc containsTemplating(s string) (bool, error) {\n\tif !strings.Contains(s, \"{{\") {\n\t\treturn false, nil\n\t}\n\t// If it contains template syntax, validate it's actually a valid templ.\n\t_, err := template.New(\"\").Parse(s)\n\tif err != nil {\n\t\treturn true, err\n\t}\n\treturn true, nil\n}\n\n// SecretTemplateURL is a Secret string that represents a URL which may contain\n// Go template syntax. Unlike SecretURL, it allows templated values and only\n// validates non-templated URLs at unmarshal time.\ntype SecretTemplateURL commoncfg.Secret\n\n// MarshalYAML implements the yaml.Marshaler interface for SecretTemplateURL.\nfunc (s SecretTemplateURL) MarshalYAML() (any, error) {\n\tif s != \"\" {\n\t\tif commoncfg.MarshalSecretValue {\n\t\t\treturn string(s), nil\n\t\t}\n\t\treturn SecretToken, nil\n\t}\n\treturn nil, nil\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for SecretTemplateURL.\nfunc (s *SecretTemplateURL) UnmarshalYAML(unmarshal func(any) error) error {\n\ttype plain commoncfg.Secret\n\tif err := unmarshal((*plain)(s)); err != nil {\n\t\treturn err\n\t}\n\n\turlStr := string(*s)\n\n\t// Skip validation for empty strings or secret token\n\tif urlStr == \"\" || urlStr == SecretToken {\n\t\treturn nil\n\t}\n\n\t// Check if the URL contains template syntax\n\tisTemplated, err := containsTemplating(urlStr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid template syntax: %w\", err)\n\t}\n\n\t// Only validate as URL if it's not templated\n\tif !isTemplated {\n\t\tif _, err := ParseURL(urlStr); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid URL: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// MarshalJSON implements the json.Marshaler interface for SecretTemplateURL.\nfunc (s SecretTemplateURL) MarshalJSON() ([]byte, error) {\n\treturn commoncfg.Secret(s).MarshalJSON()\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface for SecretTemplateURL.\nfunc (s *SecretTemplateURL) UnmarshalJSON(data []byte) error {\n\tif string(data) == SecretToken || string(data) == SecretTokenJSON {\n\t\t*s = \"\"\n\t\treturn nil\n\t}\n\t// Just unmarshal as a string since Secret doesn't have UnmarshalJSON\n\tvar str string\n\tif err := json.Unmarshal(data, &str); err != nil {\n\t\treturn err\n\t}\n\t*s = SecretTemplateURL(str)\n\treturn nil\n}\n\nfunc MustParseURL(s string) *URL {\n\tu, err := ParseURL(s)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn u\n}\n\nfunc ParseURL(s string) (*URL, error) {\n\tu, err := url.Parse(s)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif u.Scheme != \"http\" && u.Scheme != \"https\" {\n\t\treturn nil, fmt.Errorf(\"unsupported scheme %q for URL\", u.Scheme)\n\t}\n\tif u.Host == \"\" {\n\t\treturn nil, errors.New(\"missing host for URL\")\n\t}\n\treturn &URL{u}, nil\n}\n"
  },
  {
    "path": "config/common/url_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage common\n\nimport (\n\t\"encoding/json\"\n\t\"net/url\"\n\t\"testing\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/stretchr/testify/require\"\n\t\"gopkg.in/yaml.v2\"\n)\n\nfunc TestJSONMarshalHideSecretURL(t *testing.T) {\n\turlp, err := url.Parse(\"http://example.com/\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tu := &SecretURL{urlp}\n\n\tc, err := json.Marshal(u)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\t// u003c -> \"<\"\n\t// u003e -> \">\"\n\trequire.Equal(t, \"\\\"\\\\u003csecret\\\\u003e\\\"\", string(c), \"SecretURL not properly elided in JSON.\")\n\t// Check that the marshaled data can be unmarshaled again.\n\tout := &SecretURL{}\n\terr = json.Unmarshal(c, out)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tc, err = yaml.Marshal(u)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\trequire.Equal(t, \"<secret>\\n\", string(c), \"SecretURL not properly elided in YAML.\")\n\t// Check that the marshaled data can be unmarshaled again.\n\tout = &SecretURL{}\n\terr = yaml.Unmarshal(c, &out)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestUnmarshalSecretURL(t *testing.T) {\n\tb := []byte(`\"http://example.com/se cret\"`)\n\tvar u SecretURL\n\n\terr := json.Unmarshal(b, &u)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\trequire.Equal(t, \"http://example.com/se%20cret\", u.String(), \"SecretURL not properly unmarshaled in JSON.\")\n\n\terr = yaml.Unmarshal(b, &u)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\trequire.Equal(t, \"http://example.com/se%20cret\", u.String(), \"SecretURL not properly unmarshaled in YAML.\")\n}\n\nfunc TestHideSecretURL(t *testing.T) {\n\tb := []byte(`\"://wrongurl/\"`)\n\tvar u SecretURL\n\n\terr := json.Unmarshal(b, &u)\n\trequire.Error(t, err)\n\trequire.NotContains(t, err.Error(), \"wrongurl\")\n}\n\nfunc TestShowMarshalSecretURL(t *testing.T) {\n\tcommoncfg.MarshalSecretValue = true\n\tdefer func() { commoncfg.MarshalSecretValue = false }()\n\n\tb := []byte(`\"://wrongurl/\"`)\n\tvar u SecretURL\n\n\terr := json.Unmarshal(b, &u)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"wrongurl\")\n}\n\nfunc TestMarshalURL(t *testing.T) {\n\tfor name, tc := range map[string]struct {\n\t\tinput        *URL\n\t\texpectedJSON string\n\t\texpectedYAML string\n\t}{\n\t\t\"url\": {\n\t\t\tinput:        MustParseURL(\"http://example.com/\"),\n\t\t\texpectedJSON: \"\\\"http://example.com/\\\"\",\n\t\t\texpectedYAML: \"http://example.com/\\n\",\n\t\t},\n\n\t\t\"wrapped nil value\": {\n\t\t\tinput:        &URL{},\n\t\t\texpectedJSON: \"null\",\n\t\t\texpectedYAML: \"null\\n\",\n\t\t},\n\n\t\t\"wrapped empty URL\": {\n\t\t\tinput:        &URL{&url.URL{}},\n\t\t\texpectedJSON: \"\\\"\\\"\",\n\t\t\texpectedYAML: \"\\\"\\\"\\n\",\n\t\t},\n\t} {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tj, err := json.Marshal(tc.input)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.expectedJSON, string(j), \"URL not properly marshaled into JSON.\")\n\n\t\t\ty, err := yaml.Marshal(tc.input)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.expectedYAML, string(y), \"URL not properly marshaled into YAML.\")\n\t\t})\n\t}\n}\n\nfunc TestUnmarshalNilURL(t *testing.T) {\n\tb := []byte(`null`)\n\n\t{\n\t\tvar u URL\n\t\terr := json.Unmarshal(b, &u)\n\t\trequire.Error(t, err, \"unsupported scheme \\\"\\\" for URL\")\n\t}\n\n\t{\n\t\tvar u URL\n\t\terr := yaml.Unmarshal(b, &u)\n\t\trequire.NoError(t, err)\n\t}\n}\n\nfunc TestUnmarshalEmptyURL(t *testing.T) {\n\tb := []byte(`\"\"`)\n\n\t{\n\t\tvar u URL\n\t\terr := json.Unmarshal(b, &u)\n\t\trequire.Error(t, err, \"unsupported scheme \\\"\\\" for URL\")\n\t\trequire.Equal(t, (*url.URL)(nil), u.URL)\n\t}\n\n\t{\n\t\tvar u URL\n\t\terr := yaml.Unmarshal(b, &u)\n\t\trequire.Error(t, err, \"unsupported scheme \\\"\\\" for URL\")\n\t\trequire.Equal(t, (*url.URL)(nil), u.URL)\n\t}\n}\n\nfunc TestUnmarshalURL(t *testing.T) {\n\tb := []byte(`\"http://example.com/a b\"`)\n\tvar u URL\n\n\terr := json.Unmarshal(b, &u)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\trequire.Equal(t, \"http://example.com/a%20b\", u.String(), \"URL not properly unmarshaled in JSON.\")\n\n\terr = yaml.Unmarshal(b, &u)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\trequire.Equal(t, \"http://example.com/a%20b\", u.String(), \"URL not properly unmarshaled in YAML.\")\n}\n\nfunc TestUnmarshalInvalidURL(t *testing.T) {\n\tfor _, b := range [][]byte{\n\t\t[]byte(`\"://example.com\"`),\n\t\t[]byte(`\"http:example.com\"`),\n\t\t[]byte(`\"telnet://example.com\"`),\n\t} {\n\t\tvar u URL\n\n\t\terr := json.Unmarshal(b, &u)\n\t\tif err == nil {\n\t\t\tt.Errorf(\"Expected an error unmarshaling %q from JSON\", string(b))\n\t\t}\n\n\t\terr = yaml.Unmarshal(b, &u)\n\t\tif err == nil {\n\t\t\tt.Errorf(\"Expected an error unmarshaling %q from YAML\", string(b))\n\t\t}\n\t\tt.Logf(\"%s\", err)\n\t}\n}\n\nfunc TestUnmarshalRelativeURL(t *testing.T) {\n\tb := []byte(`\"/home\"`)\n\tvar u URL\n\n\terr := json.Unmarshal(b, &u)\n\tif err == nil {\n\t\tt.Errorf(\"Expected an error parsing URL\")\n\t}\n\n\terr = yaml.Unmarshal(b, &u)\n\tif err == nil {\n\t\tt.Errorf(\"Expected an error parsing URL\")\n\t}\n}\n"
  },
  {
    "path": "config/config.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage config\n\nimport (\n\t\"cmp\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"text/template\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\t\"gopkg.in/yaml.v2\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\t\"github.com/prometheus/alertmanager/matcher/compat\"\n\t\"github.com/prometheus/alertmanager/timeinterval\"\n\t\"github.com/prometheus/alertmanager/tracing\"\n)\n\n// containsTemplating checks if the string contains template syntax.\nfunc containsTemplating(s string) (bool, error) {\n\tif !strings.Contains(s, \"{{\") {\n\t\treturn false, nil\n\t}\n\t// If it contains template syntax, validate it's actually a valid templ.\n\t_, err := template.New(\"\").Parse(s)\n\tif err != nil {\n\t\treturn true, err\n\t}\n\treturn true, nil\n}\n\n// SecretTemplateURL is a Secret string that represents a URL which may contain\n// Go template syntax. Unlike SecretURL, it allows templated values and only\n// validates non-templated URLs at unmarshal time.\ntype SecretTemplateURL commoncfg.Secret\n\n// MarshalYAML implements the yaml.Marshaler interface for SecretTemplateURL.\nfunc (s SecretTemplateURL) MarshalYAML() (any, error) {\n\tif s != \"\" {\n\t\tif commoncfg.MarshalSecretValue {\n\t\t\treturn string(s), nil\n\t\t}\n\t\treturn amcommoncfg.SecretToken, nil\n\t}\n\treturn nil, nil\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for SecretTemplateURL.\nfunc (s *SecretTemplateURL) UnmarshalYAML(unmarshal func(any) error) error {\n\ttype plain commoncfg.Secret\n\tif err := unmarshal((*plain)(s)); err != nil {\n\t\treturn err\n\t}\n\n\turlStr := string(*s)\n\n\t// Skip validation for empty strings or secret token\n\tif urlStr == \"\" || urlStr == amcommoncfg.SecretToken {\n\t\treturn nil\n\t}\n\n\t// Check if the URL contains template syntax\n\tisTemplated, err := containsTemplating(urlStr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid template syntax: %w\", err)\n\t}\n\n\t// Only validate as URL if it's not templated\n\tif !isTemplated {\n\t\tif _, err := amcommoncfg.ParseURL(urlStr); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid URL: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// MarshalJSON implements the json.Marshaler interface for SecretTemplateURL.\nfunc (s SecretTemplateURL) MarshalJSON() ([]byte, error) {\n\treturn commoncfg.Secret(s).MarshalJSON()\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface for SecretTemplateURL.\nfunc (s *SecretTemplateURL) UnmarshalJSON(data []byte) error {\n\tif string(data) == amcommoncfg.SecretToken || string(data) == amcommoncfg.SecretTokenJSON {\n\t\t*s = \"\"\n\t\treturn nil\n\t}\n\t// Just unmarshal as a string since Secret doesn't have UnmarshalJSON\n\tvar str string\n\tif err := json.Unmarshal(data, &str); err != nil {\n\t\treturn err\n\t}\n\t*s = SecretTemplateURL(str)\n\treturn nil\n}\n\n// Load parses the YAML input s into a Config.\nfunc Load(s string) (*Config, error) {\n\tcfg := &Config{}\n\terr := yaml.UnmarshalStrict([]byte(s), cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Check if we have a root route. We cannot check for it in the\n\t// UnmarshalYAML method because it won't be called if the input is empty\n\t// (e.g. the config file is empty or only contains whitespace).\n\tif cfg.Route == nil {\n\t\treturn nil, errors.New(\"no route provided in config\")\n\t}\n\n\t// Check if continue in root route.\n\tif cfg.Route.Continue {\n\t\treturn nil, errors.New(\"cannot have continue in root route\")\n\t}\n\n\tcfg.original = s\n\treturn cfg, nil\n}\n\n// LoadFile parses the given YAML file into a Config.\nfunc LoadFile(filename string) (*Config, error) {\n\tcontent, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcfg, err := Load(string(content))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresolveFilepaths(filepath.Dir(filename), cfg)\n\treturn cfg, nil\n}\n\n// resolveFilepaths joins all relative paths in a configuration\n// with a given base directory.\nfunc resolveFilepaths(baseDir string, cfg *Config) {\n\tjoin := func(fp string) string {\n\t\tif len(fp) > 0 && !filepath.IsAbs(fp) {\n\t\t\tfp = filepath.Join(baseDir, fp)\n\t\t}\n\t\treturn fp\n\t}\n\n\tfor i, tf := range cfg.Templates {\n\t\tcfg.Templates[i] = join(tf)\n\t}\n\n\tcfg.Global.HTTPConfig.SetDirectory(baseDir)\n\tfor _, receiver := range cfg.Receivers {\n\t\tfor _, cfg := range receiver.OpsGenieConfigs {\n\t\t\tcfg.HTTPConfig.SetDirectory(baseDir)\n\t\t}\n\t\tfor _, cfg := range receiver.PagerdutyConfigs {\n\t\t\tcfg.HTTPConfig.SetDirectory(baseDir)\n\t\t}\n\t\tfor _, cfg := range receiver.PushoverConfigs {\n\t\t\tcfg.HTTPConfig.SetDirectory(baseDir)\n\t\t}\n\t\tfor _, cfg := range receiver.SlackConfigs {\n\t\t\tcfg.HTTPConfig.SetDirectory(baseDir)\n\t\t}\n\t\tfor _, cfg := range receiver.VictorOpsConfigs {\n\t\t\tcfg.HTTPConfig.SetDirectory(baseDir)\n\t\t}\n\t\tfor _, cfg := range receiver.WebhookConfigs {\n\t\t\tcfg.HTTPConfig.SetDirectory(baseDir)\n\t\t}\n\t\tfor _, cfg := range receiver.WechatConfigs {\n\t\t\tcfg.HTTPConfig.SetDirectory(baseDir)\n\t\t}\n\t\tfor _, cfg := range receiver.SNSConfigs {\n\t\t\tcfg.HTTPConfig.SetDirectory(baseDir)\n\t\t}\n\t\tfor _, cfg := range receiver.TelegramConfigs {\n\t\t\tcfg.HTTPConfig.SetDirectory(baseDir)\n\t\t}\n\t\tfor _, cfg := range receiver.DiscordConfigs {\n\t\t\tcfg.HTTPConfig.SetDirectory(baseDir)\n\t\t}\n\t\tfor _, cfg := range receiver.WebexConfigs {\n\t\t\tcfg.HTTPConfig.SetDirectory(baseDir)\n\t\t}\n\t\tfor _, cfg := range receiver.MSTeamsConfigs {\n\t\t\tcfg.HTTPConfig.SetDirectory(baseDir)\n\t\t}\n\t\tfor _, cfg := range receiver.MSTeamsV2Configs {\n\t\t\tcfg.HTTPConfig.SetDirectory(baseDir)\n\t\t}\n\t\tfor _, cfg := range receiver.JiraConfigs {\n\t\t\tcfg.HTTPConfig.SetDirectory(baseDir)\n\t\t}\n\t\tfor _, cfg := range receiver.RocketchatConfigs {\n\t\t\tcfg.HTTPConfig.SetDirectory(baseDir)\n\t\t}\n\t\tfor _, cfg := range receiver.MattermostConfigs {\n\t\t\tcfg.HTTPConfig.SetDirectory(baseDir)\n\t\t}\n\t}\n}\n\n// MuteTimeInterval represents a named set of time intervals for which a route should be muted.\ntype MuteTimeInterval struct {\n\tName          string                      `yaml:\"name\" json:\"name\"`\n\tTimeIntervals []timeinterval.TimeInterval `yaml:\"time_intervals\" json:\"time_intervals\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for MuteTimeInterval.\nfunc (mt *MuteTimeInterval) UnmarshalYAML(unmarshal func(any) error) error {\n\ttype plain MuteTimeInterval\n\tif err := unmarshal((*plain)(mt)); err != nil {\n\t\treturn err\n\t}\n\tif mt.Name == \"\" {\n\t\treturn errors.New(\"missing name in mute time interval\")\n\t}\n\treturn nil\n}\n\n// TimeInterval represents a named set of time intervals for which a route should be muted.\ntype TimeInterval struct {\n\tName          string                      `yaml:\"name\" json:\"name\"`\n\tTimeIntervals []timeinterval.TimeInterval `yaml:\"time_intervals\" json:\"time_intervals\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for MuteTimeInterval.\nfunc (ti *TimeInterval) UnmarshalYAML(unmarshal func(any) error) error {\n\ttype plain TimeInterval\n\tif err := unmarshal((*plain)(ti)); err != nil {\n\t\treturn err\n\t}\n\tif ti.Name == \"\" {\n\t\treturn errors.New(\"missing name in time interval\")\n\t}\n\treturn nil\n}\n\n// Config is the top-level configuration for Alertmanager's config files.\ntype Config struct {\n\tGlobal       *GlobalConfig             `yaml:\"global,omitempty\" json:\"global,omitempty\"`\n\tRoute        *Route                    `yaml:\"route,omitempty\" json:\"route,omitempty\"`\n\tInhibitRules []amcommoncfg.InhibitRule `yaml:\"inhibit_rules,omitempty\" json:\"inhibit_rules,omitempty\"`\n\tReceivers    []Receiver                `yaml:\"receivers,omitempty\" json:\"receivers,omitempty\"`\n\tTemplates    []string                  `yaml:\"templates\" json:\"templates\"`\n\t// Deprecated. Remove before v1.0 release.\n\tMuteTimeIntervals []MuteTimeInterval `yaml:\"mute_time_intervals,omitempty\" json:\"mute_time_intervals,omitempty\"`\n\tTimeIntervals     []TimeInterval     `yaml:\"time_intervals,omitempty\" json:\"time_intervals,omitempty\"`\n\n\tTracingConfig tracing.TracingConfig `yaml:\"tracing,omitempty\" json:\"tracing,omitempty\"`\n\n\t// original is the input from which the config was parsed.\n\toriginal string\n}\n\nfunc (c Config) String() string {\n\tb, err := yaml.Marshal(c)\n\tif err != nil {\n\t\treturn fmt.Sprintf(\"<error creating config string: %s>\", err)\n\t}\n\treturn string(b)\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for Config.\nfunc (c *Config) UnmarshalYAML(unmarshal func(any) error) error {\n\t// We want to set c to the defaults and then overwrite it with the input.\n\t// To make unmarshal fill the plain data struct rather than calling UnmarshalYAML\n\t// again, we have to hide it using a type indirection.\n\ttype plain Config\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\n\t// If a global block was open but empty the default global config is overwritten.\n\t// We have to restore it here.\n\tif c.Global == nil {\n\t\tc.Global = &GlobalConfig{}\n\t\t*c.Global = DefaultGlobalConfig()\n\t}\n\n\tif c.Global.SlackAppToken != \"\" && len(c.Global.SlackAppTokenFile) > 0 {\n\t\treturn errors.New(\"at most one of slack_app_token & slack_app_token_file must be configured\")\n\t}\n\n\tif c.Global.SlackAPIURL != nil && len(c.Global.SlackAPIURLFile) > 0 {\n\t\treturn errors.New(\"at most one of slack_api_url & slack_api_url_file must be configured\")\n\t}\n\n\tif (c.Global.SlackAppToken != \"\" || len(c.Global.SlackAppTokenFile) > 0) && (c.Global.SlackAPIURL != nil || len(c.Global.SlackAPIURLFile) > 0) {\n\t\t// Support transition from workaround suggested in https://github.com/prometheus/alertmanager/issues/2513,\n\t\t// where users might set `slack_api_url` at the top level and then have `http_config` with individual\n\t\t// bearer tokens in the receivers.\n\t\tif c.Global.SlackAPIURL.String() != c.Global.SlackAppURL.String() {\n\t\t\treturn errors.New(\"at most one of slack_app_token/slack_app_token_file & slack_api_url/slack_api_url_file must be configured\")\n\t\t}\n\t}\n\n\tif c.Global.OpsGenieAPIKey != \"\" && len(c.Global.OpsGenieAPIKeyFile) > 0 {\n\t\treturn errors.New(\"at most one of opsgenie_api_key & opsgenie_api_key_file must be configured\")\n\t}\n\n\tif c.Global.VictorOpsAPIKey != \"\" && len(c.Global.VictorOpsAPIKeyFile) > 0 {\n\t\treturn errors.New(\"at most one of victorops_api_key & victorops_api_key_file must be configured\")\n\t}\n\n\tif c.Global.TelegramBotToken != \"\" && len(c.Global.TelegramBotTokenFile) > 0 {\n\t\treturn errors.New(\"at most one of telegram_bot_token & telegram_bot_token_file must be configured\")\n\t}\n\n\tif len(c.Global.SMTPAuthPassword) > 0 && len(c.Global.SMTPAuthPasswordFile) > 0 {\n\t\treturn errors.New(\"at most one of smtp_auth_password & smtp_auth_password_file must be configured\")\n\t}\n\n\tif c.Global.RocketchatToken != nil && len(c.Global.RocketchatTokenFile) > 0 {\n\t\treturn errors.New(\"at most one of rocketchat_token & rocketchat_token_file must be configured\")\n\t}\n\n\tif c.Global.RocketchatTokenID != nil && len(c.Global.RocketchatTokenIDFile) > 0 {\n\t\treturn errors.New(\"at most one of rocketchat_token_id & rocketchat_token_id_file must be configured\")\n\t}\n\n\tif len(c.Global.SMTPAuthSecret) > 0 && len(c.Global.SMTPAuthSecretFile) > 0 {\n\t\treturn fmt.Errorf(\"at most one of smtp_auth_secret & smtp_auth_secret_file must be configured\")\n\t}\n\n\tif c.Global.WeChatAPISecret != \"\" && len(c.Global.WeChatAPISecretFile) > 0 {\n\t\treturn errors.New(\"at most one of wechat_api_secret & wechat_api_secret_file must be configured\")\n\t}\n\n\tif c.Global.MattermostWebhookURL != nil && len(c.Global.MattermostWebhookURLFile) > 0 {\n\t\treturn errors.New(\"at most one of mattermost_webhook_url & mattermost_webhook_url_file must be configured\")\n\t}\n\n\tnames := map[string]struct{}{}\n\n\tfor _, rcv := range c.Receivers {\n\t\tif _, ok := names[rcv.Name]; ok {\n\t\t\treturn fmt.Errorf(\"notification config name %q is not unique\", rcv.Name)\n\t\t}\n\t\tfor _, wh := range rcv.WebhookConfigs {\n\t\t\tif wh == nil {\n\t\t\t\treturn errors.New(\"missing webhook config\")\n\t\t\t}\n\t\t\twh.HTTPConfig = cmp.Or(wh.HTTPConfig, c.Global.HTTPConfig)\n\t\t}\n\t\tfor _, ec := range rcv.EmailConfigs {\n\t\t\tif ec == nil {\n\t\t\t\treturn errors.New(\"missing email config\")\n\t\t\t}\n\t\t\tec.TLSConfig = cmp.Or(ec.TLSConfig, c.Global.SMTPTLSConfig)\n\t\t\tec.Smarthost = cmp.Or(ec.Smarthost, c.Global.SMTPSmarthost)\n\t\t\tif ec.Smarthost.String() == \"\" {\n\t\t\t\treturn errors.New(\"no global SMTP smarthost set\")\n\t\t\t}\n\t\t\tec.From = cmp.Or(ec.From, c.Global.SMTPFrom)\n\t\t\tif ec.From == \"\" {\n\t\t\t\treturn errors.New(\"no global SMTP from set\")\n\t\t\t}\n\t\t\tec.Hello = cmp.Or(ec.Hello, c.Global.SMTPHello)\n\t\t\tec.AuthUsername = cmp.Or(ec.AuthUsername, c.Global.SMTPAuthUsername)\n\t\t\tif ec.AuthPassword == \"\" && ec.AuthPasswordFile == \"\" {\n\t\t\t\tec.AuthPassword = c.Global.SMTPAuthPassword\n\t\t\t\tec.AuthPasswordFile = c.Global.SMTPAuthPasswordFile\n\t\t\t}\n\t\t\tec.AuthSecret = cmp.Or(ec.AuthSecret, c.Global.SMTPAuthSecret)\n\t\t\tec.AuthSecretFile = cmp.Or(ec.AuthSecretFile, c.Global.SMTPAuthSecretFile)\n\t\t\tec.AuthIdentity = cmp.Or(ec.AuthIdentity, c.Global.SMTPAuthIdentity)\n\t\t\tif ec.RequireTLS == nil {\n\t\t\t\tec.RequireTLS = new(bool)\n\t\t\t\t*ec.RequireTLS = c.Global.SMTPRequireTLS\n\t\t\t}\n\t\t\tif ec.ForceImplicitTLS == nil {\n\t\t\t\tec.ForceImplicitTLS = c.Global.SMTPForceImplicitTLS\n\t\t\t}\n\t\t}\n\t\tfor _, sc := range rcv.SlackConfigs {\n\t\t\tif sc == nil {\n\t\t\t\tsc = &SlackConfig{}\n\t\t\t}\n\t\t\tsc.AppURL = cmp.Or(sc.AppURL, c.Global.SlackAppURL)\n\t\t\tif sc.AppURL == nil {\n\t\t\t\treturn errors.New(\"no global Slack App URL set\")\n\t\t\t}\n\t\t\t// we only want to set the app token from global if there's no local authorization or webhook url\n\t\t\tif sc.AppToken == \"\" && len(sc.AppTokenFile) == 0 && (sc.HTTPConfig == nil || sc.HTTPConfig.Authorization == nil) && sc.APIURL == nil {\n\t\t\t\tsc.AppToken = c.Global.SlackAppToken\n\t\t\t\tsc.AppTokenFile = c.Global.SlackAppTokenFile\n\t\t\t}\n\t\t\tif sc.APIURL == nil && len(sc.APIURLFile) == 0 {\n\t\t\t\tsc.APIURL = c.Global.SlackAPIURL\n\t\t\t\tsc.APIURLFile = c.Global.SlackAPIURLFile\n\t\t\t}\n\t\t\tif sc.APIURL == nil && len(sc.APIURLFile) == 0 && sc.AppToken == \"\" && len(sc.AppTokenFile) == 0 {\n\t\t\t\treturn errors.New(\"no Slack API URL nor App token set either inline or in a file\")\n\t\t\t}\n\t\t\tif sc.HTTPConfig == nil {\n\t\t\t\t// we don't want to change the global http config when setting the receiver's http config, do we do a copy\n\t\t\t\thttpconfig := *c.Global.HTTPConfig\n\t\t\t\tsc.HTTPConfig = &httpconfig\n\t\t\t}\n\t\t\tif sc.AppToken != \"\" || len(sc.AppTokenFile) != 0 {\n\t\t\t\tif sc.HTTPConfig.Authorization != nil {\n\t\t\t\t\treturn errors.New(\"http authorization can't be set when using Slack App tokens\")\n\t\t\t\t}\n\t\t\t\tsc.HTTPConfig.Authorization = &commoncfg.Authorization{\n\t\t\t\t\tType:            \"Bearer\",\n\t\t\t\t\tCredentials:     commoncfg.Secret(sc.AppToken),\n\t\t\t\t\tCredentialsFile: sc.AppTokenFile,\n\t\t\t\t}\n\t\t\t\tsc.APIURL = (*amcommoncfg.SecretURL)(sc.AppURL)\n\t\t\t}\n\t\t}\n\t\tfor _, poc := range rcv.PushoverConfigs {\n\t\t\tif poc == nil {\n\t\t\t\treturn errors.New(\"missing pushover config\")\n\t\t\t}\n\t\t\tpoc.HTTPConfig = cmp.Or(poc.HTTPConfig, c.Global.HTTPConfig)\n\t\t}\n\t\tfor _, pdc := range rcv.PagerdutyConfigs {\n\t\t\tif pdc == nil {\n\t\t\t\treturn errors.New(\"missing pagerduty config\")\n\t\t\t}\n\t\t\tpdc.HTTPConfig = cmp.Or(pdc.HTTPConfig, c.Global.HTTPConfig)\n\t\t\tpdc.URL = cmp.Or(pdc.URL, c.Global.PagerdutyURL)\n\t\t\tif pdc.URL == nil {\n\t\t\t\treturn errors.New(\"no global PagerDuty URL set\")\n\t\t\t}\n\t\t}\n\t\tfor _, iio := range rcv.IncidentioConfigs {\n\t\t\tif iio == nil {\n\t\t\t\treturn errors.New(\"missing incidentio config\")\n\t\t\t}\n\t\t\tiio.HTTPConfig = cmp.Or(iio.HTTPConfig, c.Global.HTTPConfig)\n\t\t}\n\t\tfor _, ogc := range rcv.OpsGenieConfigs {\n\t\t\tif ogc == nil {\n\t\t\t\togc = &OpsGenieConfig{}\n\t\t\t}\n\t\t\togc.HTTPConfig = cmp.Or(ogc.HTTPConfig, c.Global.HTTPConfig)\n\t\t\togc.APIURL = cmp.Or(ogc.APIURL, c.Global.OpsGenieAPIURL)\n\t\t\tif ogc.APIURL == nil {\n\t\t\t\treturn errors.New(\"no global OpsGenie URL set\")\n\t\t\t}\n\t\t\tif !strings.HasSuffix(ogc.APIURL.Path, \"/\") {\n\t\t\t\togc.APIURL.Path += \"/\"\n\t\t\t}\n\t\t\togc.APIKey = cmp.Or(ogc.APIKey, c.Global.OpsGenieAPIKey)\n\t\t\togc.APIKeyFile = cmp.Or(ogc.APIKeyFile, c.Global.OpsGenieAPIKeyFile)\n\t\t\tif ogc.APIKey == \"\" && len(ogc.APIKeyFile) == 0 {\n\t\t\t\treturn errors.New(\"no global OpsGenie API Key set either inline or in a file\")\n\t\t\t}\n\t\t}\n\t\tfor _, wcc := range rcv.WechatConfigs {\n\t\t\tif wcc == nil {\n\t\t\t\twcc = &WechatConfig{}\n\t\t\t}\n\t\t\twcc.HTTPConfig = cmp.Or(wcc.HTTPConfig, c.Global.HTTPConfig)\n\t\t\twcc.APIURL = cmp.Or(wcc.APIURL, c.Global.WeChatAPIURL)\n\t\t\tif wcc.APIURL == nil {\n\t\t\t\treturn errors.New(\"no global Wechat URL set\")\n\t\t\t}\n\n\t\t\tif wcc.APISecret == \"\" && len(wcc.APISecretFile) == 0 {\n\t\t\t\tif c.Global.WeChatAPISecret == \"\" && len(c.Global.WeChatAPISecretFile) == 0 {\n\t\t\t\t\treturn errors.New(\"no global Wechat Api Secret set either inline or in a file\")\n\t\t\t\t}\n\t\t\t\twcc.APISecret = c.Global.WeChatAPISecret\n\t\t\t\twcc.APISecretFile = c.Global.WeChatAPISecretFile\n\t\t\t}\n\n\t\t\twcc.CorpID = cmp.Or(wcc.CorpID, c.Global.WeChatAPICorpID)\n\t\t\tif wcc.CorpID == \"\" {\n\t\t\t\treturn errors.New(\"no global Wechat CorpID set\")\n\t\t\t}\n\n\t\t\tif !strings.HasSuffix(wcc.APIURL.Path, \"/\") {\n\t\t\t\twcc.APIURL.Path += \"/\"\n\t\t\t}\n\t\t}\n\t\tfor _, voc := range rcv.VictorOpsConfigs {\n\t\t\tif voc == nil {\n\t\t\t\treturn errors.New(\"missing victorops config\")\n\t\t\t}\n\t\t\tvoc.HTTPConfig = cmp.Or(voc.HTTPConfig, c.Global.HTTPConfig)\n\t\t\tvoc.APIURL = cmp.Or(voc.APIURL, c.Global.VictorOpsAPIURL)\n\t\t\tif voc.APIURL == nil {\n\t\t\t\treturn errors.New(\"no global VictorOps URL set\")\n\t\t\t}\n\t\t\tif !strings.HasSuffix(voc.APIURL.Path, \"/\") {\n\t\t\t\tvoc.APIURL.Path += \"/\"\n\t\t\t}\n\t\t\tvoc.APIKey = cmp.Or(voc.APIKey, c.Global.VictorOpsAPIKey)\n\t\t\tvoc.APIKeyFile = cmp.Or(voc.APIKeyFile, c.Global.VictorOpsAPIKeyFile)\n\t\t\tif voc.APIKey == \"\" && len(voc.APIKeyFile) == 0 {\n\t\t\t\treturn errors.New(\"no global VictorOps API Key set\")\n\t\t\t}\n\t\t}\n\t\tfor _, sns := range rcv.SNSConfigs {\n\t\t\tif sns == nil {\n\t\t\t\treturn errors.New(\"missing sns config\")\n\t\t\t}\n\t\t\tsns.HTTPConfig = cmp.Or(sns.HTTPConfig, c.Global.HTTPConfig)\n\t\t}\n\n\t\tfor _, telegram := range rcv.TelegramConfigs {\n\t\t\tif telegram == nil {\n\t\t\t\treturn errors.New(\"missing telegram config\")\n\t\t\t}\n\t\t\ttelegram.HTTPConfig = cmp.Or(telegram.HTTPConfig, c.Global.HTTPConfig)\n\t\t\ttelegram.APIUrl = cmp.Or(telegram.APIUrl, c.Global.TelegramAPIUrl)\n\t\t\tif telegram.BotToken == \"\" && len(telegram.BotTokenFile) == 0 {\n\t\t\t\tif c.Global.TelegramBotToken == \"\" && len(c.Global.TelegramBotTokenFile) == 0 {\n\t\t\t\t\treturn errors.New(\"missing bot_token or bot_token_file on telegram_config\")\n\t\t\t\t}\n\t\t\t\ttelegram.BotToken = c.Global.TelegramBotToken\n\t\t\t\ttelegram.BotTokenFile = c.Global.TelegramBotTokenFile\n\t\t\t}\n\t\t}\n\t\tfor _, discord := range rcv.DiscordConfigs {\n\t\t\tif discord == nil {\n\t\t\t\treturn errors.New(\"missing discord config\")\n\t\t\t}\n\t\t\tdiscord.HTTPConfig = cmp.Or(discord.HTTPConfig, c.Global.HTTPConfig)\n\t\t\tif discord.WebhookURL == nil && len(discord.WebhookURLFile) == 0 {\n\t\t\t\treturn errors.New(\"no discord webhook URL or URLFile provided\")\n\t\t\t}\n\t\t}\n\t\tfor _, webex := range rcv.WebexConfigs {\n\t\t\tif webex == nil {\n\t\t\t\treturn errors.New(\"missing webex config\")\n\t\t\t}\n\t\t\twebex.HTTPConfig = cmp.Or(webex.HTTPConfig, c.Global.HTTPConfig)\n\t\t\twebex.APIURL = cmp.Or(webex.APIURL, c.Global.WebexAPIURL)\n\t\t\tif webex.APIURL == nil {\n\t\t\t\treturn errors.New(\"no global Webex URL set\")\n\t\t\t}\n\t\t}\n\t\tfor _, msteams := range rcv.MSTeamsConfigs {\n\t\t\tif msteams == nil {\n\t\t\t\treturn errors.New(\"missing msteams config\")\n\t\t\t}\n\t\t\tmsteams.HTTPConfig = cmp.Or(msteams.HTTPConfig, c.Global.HTTPConfig)\n\t\t\tif msteams.WebhookURL == nil && len(msteams.WebhookURLFile) == 0 {\n\t\t\t\treturn errors.New(\"no msteams webhook URL or URLFile provided\")\n\t\t\t}\n\t\t}\n\t\tfor _, msteamsv2 := range rcv.MSTeamsV2Configs {\n\t\t\tif msteamsv2 == nil {\n\t\t\t\treturn errors.New(\"missing msteamsv2 config\")\n\t\t\t}\n\t\t\tmsteamsv2.HTTPConfig = cmp.Or(msteamsv2.HTTPConfig, c.Global.HTTPConfig)\n\t\t\tif msteamsv2.WebhookURL == nil && len(msteamsv2.WebhookURLFile) == 0 {\n\t\t\t\treturn errors.New(\"no msteamsv2 webhook URL or URLFile provided\")\n\t\t\t}\n\t\t}\n\t\tfor _, jira := range rcv.JiraConfigs {\n\t\t\tif jira == nil {\n\t\t\t\treturn errors.New(\"missing jira config\")\n\t\t\t}\n\t\t\tjira.HTTPConfig = cmp.Or(jira.HTTPConfig, c.Global.HTTPConfig)\n\t\t\tjira.APIURL = cmp.Or(jira.APIURL, c.Global.JiraAPIURL)\n\t\t\tif jira.APIURL == nil {\n\t\t\t\treturn errors.New(\"no global Jira Cloud URL set\")\n\t\t\t}\n\t\t}\n\t\tfor _, rocketchat := range rcv.RocketchatConfigs {\n\t\t\tif rocketchat == nil {\n\t\t\t\trocketchat = &RocketchatConfig{}\n\t\t\t}\n\t\t\trocketchat.HTTPConfig = cmp.Or(rocketchat.HTTPConfig, c.Global.HTTPConfig)\n\t\t\trocketchat.APIURL = cmp.Or(rocketchat.APIURL, c.Global.RocketchatAPIURL)\n\n\t\t\trocketchat.TokenID = cmp.Or(rocketchat.TokenID, c.Global.RocketchatTokenID)\n\t\t\trocketchat.TokenIDFile = cmp.Or(rocketchat.TokenIDFile, c.Global.RocketchatTokenIDFile)\n\t\t\tif rocketchat.TokenID == nil && len(rocketchat.TokenIDFile) == 0 {\n\t\t\t\treturn errors.New(\"no global Rocketchat TokenID set either inline or in a file\")\n\t\t\t}\n\n\t\t\trocketchat.Token = cmp.Or(rocketchat.Token, c.Global.RocketchatToken)\n\t\t\trocketchat.TokenFile = cmp.Or(rocketchat.TokenFile, c.Global.RocketchatTokenFile)\n\t\t\tif rocketchat.Token == nil && len(rocketchat.TokenFile) == 0 {\n\t\t\t\treturn errors.New(\"no global Rocketchat Token set either inline or in a file\")\n\t\t\t}\n\t\t}\n\t\tfor _, mattermost := range rcv.MattermostConfigs {\n\t\t\tif mattermost == nil {\n\t\t\t\treturn errors.New(\"missing mattermost config\")\n\t\t\t}\n\t\t\tmattermost.HTTPConfig = cmp.Or(mattermost.HTTPConfig, c.Global.HTTPConfig)\n\t\t\tif mattermost.WebhookURL == nil && len(mattermost.WebhookURLFile) == 0 {\n\t\t\t\tif c.Global.MattermostWebhookURL == nil && len(c.Global.MattermostWebhookURLFile) == 0 {\n\t\t\t\t\treturn errors.New(\"missing webhook_url or webhook_url_file on mattermost_config\")\n\t\t\t\t}\n\t\t\t\tmattermost.WebhookURL = c.Global.MattermostWebhookURL\n\t\t\t\tmattermost.WebhookURLFile = c.Global.MattermostWebhookURLFile\n\t\t\t}\n\t\t}\n\n\t\tnames[rcv.Name] = struct{}{}\n\t}\n\n\t// The root route must not have any matchers as it is the fallback node\n\t// for all alerts.\n\tif c.Route == nil {\n\t\treturn errors.New(\"no routes provided\")\n\t}\n\tif len(c.Route.Receiver) == 0 {\n\t\treturn errors.New(\"root route must specify a default receiver\")\n\t}\n\tif len(c.Route.Match) > 0 || len(c.Route.MatchRE) > 0 || len(c.Route.Matchers) > 0 {\n\t\treturn errors.New(\"root route must not have any matchers\")\n\t}\n\tif len(c.Route.MuteTimeIntervals) > 0 {\n\t\treturn errors.New(\"root route must not have any mute time intervals\")\n\t}\n\n\tif len(c.Route.ActiveTimeIntervals) > 0 {\n\t\treturn errors.New(\"root route must not have any active time intervals\")\n\t}\n\n\t// Validate that all receivers used in the routing tree are defined.\n\tif err := checkReceiver(c.Route, names); err != nil {\n\t\treturn err\n\t}\n\n\ttiNames := make(map[string]struct{})\n\n\t// read mute time intervals until deprecated\n\tfor _, mt := range c.MuteTimeIntervals {\n\t\tif _, ok := tiNames[mt.Name]; ok {\n\t\t\treturn fmt.Errorf(\"mute time interval %q is not unique\", mt.Name)\n\t\t}\n\t\ttiNames[mt.Name] = struct{}{}\n\t}\n\n\tfor _, mt := range c.TimeIntervals {\n\t\tif _, ok := tiNames[mt.Name]; ok {\n\t\t\treturn fmt.Errorf(\"time interval %q is not unique\", mt.Name)\n\t\t}\n\t\ttiNames[mt.Name] = struct{}{}\n\t}\n\n\treturn checkTimeInterval(c.Route, tiNames)\n}\n\n// checkReceiver returns an error if a node in the routing tree\n// references a receiver not in the given map.\nfunc checkReceiver(r *Route, receivers map[string]struct{}) error {\n\tfor _, sr := range r.Routes {\n\t\tif err := checkReceiver(sr, receivers); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif r.Receiver == \"\" {\n\t\treturn nil\n\t}\n\tif _, ok := receivers[r.Receiver]; !ok {\n\t\treturn fmt.Errorf(\"undefined receiver %q used in route\", r.Receiver)\n\t}\n\treturn nil\n}\n\nfunc checkTimeInterval(r *Route, timeIntervals map[string]struct{}) error {\n\tfor _, sr := range r.Routes {\n\t\tif err := checkTimeInterval(sr, timeIntervals); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tfor _, ti := range r.ActiveTimeIntervals {\n\t\tif _, ok := timeIntervals[ti]; !ok {\n\t\t\treturn fmt.Errorf(\"undefined time interval %q used in route\", ti)\n\t\t}\n\t}\n\n\tfor _, tm := range r.MuteTimeIntervals {\n\t\tif _, ok := timeIntervals[tm]; !ok {\n\t\t\treturn fmt.Errorf(\"undefined time interval %q used in route\", tm)\n\t\t}\n\t}\n\treturn nil\n}\n\n// DefaultGlobalConfig returns GlobalConfig with default values.\nfunc DefaultGlobalConfig() GlobalConfig {\n\tdefaultHTTPConfig := commoncfg.DefaultHTTPClientConfig\n\tdefaultSMTPTLSConfig := commoncfg.TLSConfig{}\n\n\treturn GlobalConfig{\n\t\tResolveTimeout:   model.Duration(5 * time.Minute),\n\t\tHTTPConfig:       &defaultHTTPConfig,\n\t\tSMTPHello:        \"localhost\",\n\t\tSMTPRequireTLS:   true,\n\t\tSMTPTLSConfig:    &defaultSMTPTLSConfig,\n\t\tPagerdutyURL:     amcommoncfg.MustParseURL(\"https://events.pagerduty.com/v2/enqueue\"),\n\t\tOpsGenieAPIURL:   amcommoncfg.MustParseURL(\"https://api.opsgenie.com/\"),\n\t\tWeChatAPIURL:     amcommoncfg.MustParseURL(\"https://qyapi.weixin.qq.com/cgi-bin/\"),\n\t\tVictorOpsAPIURL:  amcommoncfg.MustParseURL(\"https://alert.victorops.com/integrations/generic/20131114/alert/\"),\n\t\tTelegramAPIUrl:   amcommoncfg.MustParseURL(\"https://api.telegram.org\"),\n\t\tWebexAPIURL:      amcommoncfg.MustParseURL(\"https://webexapis.com/v1/messages\"),\n\t\tRocketchatAPIURL: amcommoncfg.MustParseURL(\"https://open.rocket.chat/\"),\n\t\tSlackAppURL:      amcommoncfg.MustParseURL(\"https://slack.com/api/chat.postMessage\"),\n\t}\n}\n\n// HostPort represents a \"host:port\" network address.\ntype HostPort struct {\n\tHost string\n\tPort string\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for HostPort.\nfunc (hp *HostPort) UnmarshalYAML(unmarshal func(any) error) error {\n\tvar (\n\t\ts   string\n\t\terr error\n\t)\n\tif err = unmarshal(&s); err != nil {\n\t\treturn err\n\t}\n\tif s == \"\" {\n\t\treturn nil\n\t}\n\thp.Host, hp.Port, err = net.SplitHostPort(s)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif hp.Port == \"\" {\n\t\treturn fmt.Errorf(\"address %q: port cannot be empty\", s)\n\t}\n\treturn nil\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface for HostPort.\nfunc (hp *HostPort) UnmarshalJSON(data []byte) error {\n\tvar (\n\t\ts   string\n\t\terr error\n\t)\n\tif err = json.Unmarshal(data, &s); err != nil {\n\t\treturn err\n\t}\n\tif s == \"\" {\n\t\treturn nil\n\t}\n\thp.Host, hp.Port, err = net.SplitHostPort(s)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif hp.Port == \"\" {\n\t\treturn fmt.Errorf(\"address %q: port cannot be empty\", s)\n\t}\n\treturn nil\n}\n\n// MarshalYAML implements the yaml.Marshaler interface for HostPort.\nfunc (hp HostPort) MarshalYAML() (any, error) {\n\treturn hp.String(), nil\n}\n\n// MarshalJSON implements the json.Marshaler interface for HostPort.\nfunc (hp HostPort) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(hp.String())\n}\n\nfunc (hp HostPort) String() string {\n\tif hp.Host == \"\" && hp.Port == \"\" {\n\t\treturn \"\"\n\t}\n\treturn net.JoinHostPort(hp.Host, hp.Port)\n}\n\n// GlobalConfig defines configuration parameters that are valid globally\n// unless overwritten.\ntype GlobalConfig struct {\n\t// ResolveTimeout is the time after which an alert is declared resolved\n\t// if it has not been updated.\n\tResolveTimeout model.Duration `yaml:\"resolve_timeout\" json:\"resolve_timeout\"`\n\n\tHTTPConfig *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\n\tJiraAPIURL               *amcommoncfg.URL       `yaml:\"jira_api_url,omitempty\" json:\"jira_api_url,omitempty\"`\n\tSMTPFrom                 string                 `yaml:\"smtp_from,omitempty\" json:\"smtp_from,omitempty\"`\n\tSMTPHello                string                 `yaml:\"smtp_hello,omitempty\" json:\"smtp_hello,omitempty\"`\n\tSMTPSmarthost            HostPort               `yaml:\"smtp_smarthost,omitempty\" json:\"smtp_smarthost,omitempty\"`\n\tSMTPAuthUsername         string                 `yaml:\"smtp_auth_username,omitempty\" json:\"smtp_auth_username,omitempty\"`\n\tSMTPAuthPassword         commoncfg.Secret       `yaml:\"smtp_auth_password,omitempty\" json:\"smtp_auth_password,omitempty\"`\n\tSMTPAuthPasswordFile     string                 `yaml:\"smtp_auth_password_file,omitempty\" json:\"smtp_auth_password_file,omitempty\"`\n\tSMTPAuthSecret           commoncfg.Secret       `yaml:\"smtp_auth_secret,omitempty\" json:\"smtp_auth_secret,omitempty\"`\n\tSMTPAuthSecretFile       string                 `yaml:\"smtp_auth_secret_file,omitempty\" json:\"smtp_auth_secret_file,omitempty\"`\n\tSMTPAuthIdentity         string                 `yaml:\"smtp_auth_identity,omitempty\" json:\"smtp_auth_identity,omitempty\"`\n\tSMTPRequireTLS           bool                   `yaml:\"smtp_require_tls\" json:\"smtp_require_tls,omitempty\"`\n\tSMTPTLSConfig            *commoncfg.TLSConfig   `yaml:\"smtp_tls_config,omitempty\" json:\"smtp_tls_config,omitempty\"`\n\tSMTPForceImplicitTLS     *bool                  `yaml:\"smtp_force_implicit_tls,omitempty\" json:\"smtp_force_implicit_tls,omitempty\"`\n\tSlackAPIURL              *amcommoncfg.SecretURL `yaml:\"slack_api_url,omitempty\" json:\"slack_api_url,omitempty\"`\n\tSlackAPIURLFile          string                 `yaml:\"slack_api_url_file,omitempty\" json:\"slack_api_url_file,omitempty\"`\n\tSlackAppToken            commoncfg.Secret       `yaml:\"slack_app_token,omitempty\" json:\"slack_app_token,omitempty\"`\n\tSlackAppTokenFile        string                 `yaml:\"slack_app_token_file,omitempty\" json:\"slack_app_token_file,omitempty\"`\n\tSlackAppURL              *amcommoncfg.URL       `yaml:\"slack_app_url,omitempty\" json:\"slack_app_url,omitempty\"`\n\tPagerdutyURL             *amcommoncfg.URL       `yaml:\"pagerduty_url,omitempty\" json:\"pagerduty_url,omitempty\"`\n\tOpsGenieAPIURL           *amcommoncfg.URL       `yaml:\"opsgenie_api_url,omitempty\" json:\"opsgenie_api_url,omitempty\"`\n\tOpsGenieAPIKey           commoncfg.Secret       `yaml:\"opsgenie_api_key,omitempty\" json:\"opsgenie_api_key,omitempty\"`\n\tOpsGenieAPIKeyFile       string                 `yaml:\"opsgenie_api_key_file,omitempty\" json:\"opsgenie_api_key_file,omitempty\"`\n\tWeChatAPIURL             *amcommoncfg.URL       `yaml:\"wechat_api_url,omitempty\" json:\"wechat_api_url,omitempty\"`\n\tWeChatAPISecret          commoncfg.Secret       `yaml:\"wechat_api_secret,omitempty\" json:\"wechat_api_secret,omitempty\"`\n\tWeChatAPISecretFile      string                 `yaml:\"wechat_api_secret_file,omitempty\" json:\"wechat_api_secret_file,omitempty\"`\n\tWeChatAPICorpID          string                 `yaml:\"wechat_api_corp_id,omitempty\" json:\"wechat_api_corp_id,omitempty\"`\n\tVictorOpsAPIURL          *amcommoncfg.URL       `yaml:\"victorops_api_url,omitempty\" json:\"victorops_api_url,omitempty\"`\n\tVictorOpsAPIKey          commoncfg.Secret       `yaml:\"victorops_api_key,omitempty\" json:\"victorops_api_key,omitempty\"`\n\tVictorOpsAPIKeyFile      string                 `yaml:\"victorops_api_key_file,omitempty\" json:\"victorops_api_key_file,omitempty\"`\n\tTelegramAPIUrl           *amcommoncfg.URL       `yaml:\"telegram_api_url,omitempty\" json:\"telegram_api_url,omitempty\"`\n\tTelegramBotToken         commoncfg.Secret       `yaml:\"telegram_bot_token,omitempty\" json:\"telegram_bot_token,omitempty\"`\n\tTelegramBotTokenFile     string                 `yaml:\"telegram_bot_token_file,omitempty\" json:\"telegram_bot_token_file,omitempty\"`\n\tWebexAPIURL              *amcommoncfg.URL       `yaml:\"webex_api_url,omitempty\" json:\"webex_api_url,omitempty\"`\n\tRocketchatAPIURL         *amcommoncfg.URL       `yaml:\"rocketchat_api_url,omitempty\" json:\"rocketchat_api_url,omitempty\"`\n\tRocketchatToken          *commoncfg.Secret      `yaml:\"rocketchat_token,omitempty\" json:\"rocketchat_token,omitempty\"`\n\tRocketchatTokenFile      string                 `yaml:\"rocketchat_token_file,omitempty\" json:\"rocketchat_token_file,omitempty\"`\n\tRocketchatTokenID        *commoncfg.Secret      `yaml:\"rocketchat_token_id,omitempty\" json:\"rocketchat_token_id,omitempty\"`\n\tRocketchatTokenIDFile    string                 `yaml:\"rocketchat_token_id_file,omitempty\" json:\"rocketchat_token_id_file,omitempty\"`\n\tMattermostWebhookURL     *amcommoncfg.SecretURL `yaml:\"mattermost_webhook_url,omitempty\" json:\"mattermost_webhook_url,omitempty\"`\n\tMattermostWebhookURLFile string                 `yaml:\"mattermost_webhook_url_file,omitempty\" json:\"mattermost_webhook_url_file,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for GlobalConfig.\nfunc (c *GlobalConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultGlobalConfig()\n\ttype plain GlobalConfig\n\treturn unmarshal((*plain)(c))\n}\n\n// A Route is a node that contains definitions of how to handle alerts.\ntype Route struct {\n\tReceiver string `yaml:\"receiver,omitempty\" json:\"receiver,omitempty\"`\n\n\tGroupByStr []string          `yaml:\"group_by,omitempty\" json:\"group_by,omitempty\"`\n\tGroupBy    []model.LabelName `yaml:\"-\" json:\"-\"`\n\tGroupByAll bool              `yaml:\"-\" json:\"-\"`\n\t// Deprecated. Remove before v1.0 release.\n\tMatch map[string]string `yaml:\"match,omitempty\" json:\"match,omitempty\"`\n\t// Deprecated. Remove before v1.0 release.\n\tMatchRE             amcommoncfg.MatchRegexps `yaml:\"match_re,omitempty\" json:\"match_re,omitempty\"`\n\tMatchers            amcommoncfg.Matchers     `yaml:\"matchers,omitempty\" json:\"matchers,omitempty\"`\n\tMuteTimeIntervals   []string                 `yaml:\"mute_time_intervals,omitempty\" json:\"mute_time_intervals,omitempty\"`\n\tActiveTimeIntervals []string                 `yaml:\"active_time_intervals,omitempty\" json:\"active_time_intervals,omitempty\"`\n\tContinue            bool                     `yaml:\"continue\" json:\"continue,omitempty\"`\n\tRoutes              []*Route                 `yaml:\"routes,omitempty\" json:\"routes,omitempty\"`\n\n\tGroupWait      *model.Duration `yaml:\"group_wait,omitempty\" json:\"group_wait,omitempty\"`\n\tGroupInterval  *model.Duration `yaml:\"group_interval,omitempty\" json:\"group_interval,omitempty\"`\n\tRepeatInterval *model.Duration `yaml:\"repeat_interval,omitempty\" json:\"repeat_interval,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for Route.\nfunc (r *Route) UnmarshalYAML(unmarshal func(any) error) error {\n\ttype plain Route\n\tif err := unmarshal((*plain)(r)); err != nil {\n\t\treturn err\n\t}\n\n\tfor k := range r.Match {\n\t\tif !model.LabelNameRE.MatchString(k) {\n\t\t\treturn fmt.Errorf(\"invalid label name %q\", k)\n\t\t}\n\t}\n\n\tfor _, l := range r.GroupByStr {\n\t\tif l == \"...\" {\n\t\t\tr.GroupByAll = true\n\t\t} else {\n\t\t\tlabelName := model.LabelName(l)\n\t\t\tif !compat.IsValidLabelName(labelName) {\n\t\t\t\treturn fmt.Errorf(\"invalid label name %q in group_by list\", l)\n\t\t\t}\n\t\t\tr.GroupBy = append(r.GroupBy, labelName)\n\t\t}\n\t}\n\n\tif r.GroupByStr != nil && len(r.GroupByStr) == 0 {\n\t\tr.GroupBy = make([]model.LabelName, 0)\n\t}\n\n\tif len(r.GroupBy) > 0 && r.GroupByAll {\n\t\treturn errors.New(\"cannot have wildcard group_by (`...`) and other labels at the same time\")\n\t}\n\n\tgroupBy := map[model.LabelName]struct{}{}\n\n\tfor _, ln := range r.GroupBy {\n\t\tif _, ok := groupBy[ln]; ok {\n\t\t\treturn fmt.Errorf(\"duplicated label %q in group_by\", ln)\n\t\t}\n\t\tgroupBy[ln] = struct{}{}\n\t}\n\n\tif r.GroupInterval != nil && time.Duration(*r.GroupInterval) == time.Duration(0) {\n\t\treturn errors.New(\"group_interval cannot be zero\")\n\t}\n\tif r.RepeatInterval != nil && time.Duration(*r.RepeatInterval) == time.Duration(0) {\n\t\treturn errors.New(\"repeat_interval cannot be zero\")\n\t}\n\n\treturn nil\n}\n\n// Receiver configuration provides configuration on how to contact a receiver.\ntype Receiver struct {\n\t// A unique identifier for this receiver.\n\tName string `yaml:\"name\" json:\"name\"`\n\n\tDiscordConfigs    []*DiscordConfig    `yaml:\"discord_configs,omitempty\" json:\"discord_configs,omitempty\"`\n\tEmailConfigs      []*EmailConfig      `yaml:\"email_configs,omitempty\" json:\"email_configs,omitempty\"`\n\tIncidentioConfigs []*IncidentioConfig `yaml:\"incidentio_configs,omitempty\" json:\"incidentio_configs,omitempty\"`\n\tPagerdutyConfigs  []*PagerdutyConfig  `yaml:\"pagerduty_configs,omitempty\" json:\"pagerduty_configs,omitempty\"`\n\tSlackConfigs      []*SlackConfig      `yaml:\"slack_configs,omitempty\" json:\"slack_configs,omitempty\"`\n\tWebhookConfigs    []*WebhookConfig    `yaml:\"webhook_configs,omitempty\" json:\"webhook_configs,omitempty\"`\n\tOpsGenieConfigs   []*OpsGenieConfig   `yaml:\"opsgenie_configs,omitempty\" json:\"opsgenie_configs,omitempty\"`\n\tWechatConfigs     []*WechatConfig     `yaml:\"wechat_configs,omitempty\" json:\"wechat_configs,omitempty\"`\n\tPushoverConfigs   []*PushoverConfig   `yaml:\"pushover_configs,omitempty\" json:\"pushover_configs,omitempty\"`\n\tVictorOpsConfigs  []*VictorOpsConfig  `yaml:\"victorops_configs,omitempty\" json:\"victorops_configs,omitempty\"`\n\tSNSConfigs        []*SNSConfig        `yaml:\"sns_configs,omitempty\" json:\"sns_configs,omitempty\"`\n\tTelegramConfigs   []*TelegramConfig   `yaml:\"telegram_configs,omitempty\" json:\"telegram_configs,omitempty\"`\n\tWebexConfigs      []*WebexConfig      `yaml:\"webex_configs,omitempty\" json:\"webex_configs,omitempty\"`\n\tMSTeamsConfigs    []*MSTeamsConfig    `yaml:\"msteams_configs,omitempty\" json:\"msteams_configs,omitempty\"`\n\tMSTeamsV2Configs  []*MSTeamsV2Config  `yaml:\"msteamsv2_configs,omitempty\" json:\"msteamsv2_configs,omitempty\"`\n\tJiraConfigs       []*JiraConfig       `yaml:\"jira_configs,omitempty\" json:\"jira_configs,omitempty\"`\n\tRocketchatConfigs []*RocketchatConfig `yaml:\"rocketchat_configs,omitempty\" json:\"rocketchat_configs,omitempty\"`\n\tMattermostConfigs []*MattermostConfig `yaml:\"mattermost_configs,omitempty\" json:\"mattermost_configs,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for Receiver.\nfunc (c *Receiver) UnmarshalYAML(unmarshal func(any) error) error {\n\ttype plain Receiver\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\tif c.Name == \"\" {\n\t\treturn errors.New(\"missing name in receiver\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "config/config_fuzz_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage config\n\nimport \"testing\"\n\nfunc FuzzLoad(f *testing.F) {\n\tf.Add(`\nglobal:\n  resolve_timeout: 5m\nroute:\n  group_by: ['alertname']\n  group_wait: 10s\n  group_interval: 10s\n  repeat_interval: 1h\n  receiver: 'web.hook'\nreceivers:\n- name: 'web.hook'\n  webhook_configs:\n  - url: 'http://127.0.0.1:5001/'\n`)\n\n\tf.Fuzz(func(t *testing.T, configText string) {\n\t\t_, _ = Load(configText)\n\t})\n}\n"
  },
  {
    "path": "config/config_test.go",
    "content": "// Copyright 2016 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage config\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/stretchr/testify/require\"\n\t\"gopkg.in/yaml.v2\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n)\n\nfunc TestLoadEmptyString(t *testing.T) {\n\tvar in string\n\t_, err := Load(in)\n\n\texpected := \"no route provided in config\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t}\n}\n\nfunc TestDefaultReceiverExists(t *testing.T) {\n\tin := `\nroute:\n   group_wait: 30s\n`\n\t_, err := Load(in)\n\n\texpected := \"root route must specify a default receiver\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t}\n}\n\nfunc TestReceiverNameIsUnique(t *testing.T) {\n\tin := `\nroute:\n    receiver: team-X\n\nreceivers:\n- name: 'team-X'\n- name: 'team-X'\n`\n\t_, err := Load(in)\n\n\texpected := \"notification config name \\\"team-X\\\" is not unique\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t}\n}\n\nfunc TestReceiverExists(t *testing.T) {\n\tin := `\nroute:\n    receiver: team-X\n\nreceivers:\n- name: 'team-Y'\n`\n\t_, err := Load(in)\n\n\texpected := \"undefined receiver \\\"team-X\\\" used in route\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t}\n}\n\nfunc TestReceiverExistsForDeepSubRoute(t *testing.T) {\n\tin := `\nroute:\n    receiver: team-X\n    routes:\n      - match:\n          foo: bar\n        routes:\n        - match:\n            foo: bar\n          receiver: nonexistent\n\nreceivers:\n- name: 'team-X'\n`\n\t_, err := Load(in)\n\n\texpected := \"undefined receiver \\\"nonexistent\\\" used in route\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t}\n}\n\nfunc TestReceiverHasName(t *testing.T) {\n\tin := `\nroute:\n\nreceivers:\n- name: ''\n`\n\t_, err := Load(in)\n\n\texpected := \"missing name in receiver\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t}\n}\n\nfunc TestMuteTimeExists(t *testing.T) {\n\tin := `\nroute:\n    receiver: team-Y\n    routes:\n    -  match:\n        severity: critical\n       mute_time_intervals:\n       - business_hours\n\nreceivers:\n- name: 'team-Y'\n`\n\t_, err := Load(in)\n\n\texpected := \"undefined time interval \\\"business_hours\\\" used in route\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t}\n}\n\nfunc TestActiveTimeExists(t *testing.T) {\n\tin := `\nroute:\n    receiver: team-Y\n    routes:\n    -  match:\n        severity: critical\n       active_time_intervals:\n       - business_hours\n\nreceivers:\n- name: 'team-Y'\n`\n\t_, err := Load(in)\n\n\texpected := \"undefined time interval \\\"business_hours\\\" used in route\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t}\n}\n\nfunc TestTimeIntervalHasName(t *testing.T) {\n\tin := `\ntime_intervals:\n- name:\n  time_intervals:\n  - times:\n     - start_time: '09:00'\n       end_time: '17:00'\n\nreceivers:\n- name: 'team-X-mails'\n\nroute:\n  receiver: 'team-X-mails'\n  routes:\n  -  match:\n      severity: critical\n     mute_time_intervals:\n     - business_hours\n`\n\t_, err := Load(in)\n\n\texpected := \"missing name in time interval\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t}\n}\n\nfunc TestMuteTimeNoDuplicates(t *testing.T) {\n\tin := `\nmute_time_intervals:\n- name: duplicate\n  time_intervals:\n  - times:\n     - start_time: '09:00'\n       end_time: '17:00'\n- name: duplicate\n  time_intervals:\n  - times:\n     - start_time: '10:00'\n       end_time: '14:00'\n\nreceivers:\n- name: 'team-X-mails'\n\nroute:\n  receiver: 'team-X-mails'\n  routes:\n  -  match:\n      severity: critical\n     mute_time_intervals:\n     - business_hours\n`\n\t_, err := Load(in)\n\n\texpected := \"mute time interval \\\"duplicate\\\" is not unique\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t}\n}\n\nfunc TestGroupByHasNoDuplicatedLabels(t *testing.T) {\n\tin := `\nroute:\n  group_by: ['alertname', 'cluster', 'service', 'cluster']\n\nreceivers:\n- name: 'team-X-mails'\n`\n\t_, err := Load(in)\n\n\texpected := \"duplicated label \\\"cluster\\\" in group_by\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t}\n}\n\nfunc TestWildcardGroupByWithOtherGroupByLabels(t *testing.T) {\n\tin := `\nroute:\n  group_by: ['alertname', 'cluster', '...']\n  receiver: team-X-mails\nreceivers:\n- name: 'team-X-mails'\n`\n\t_, err := Load(in)\n\n\texpected := \"cannot have wildcard group_by (`...`) and other labels at the same time\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t}\n}\n\nfunc TestGroupByInvalidLabel(t *testing.T) {\n\tin := `\nroute:\n  group_by: ['-invalid-']\n  receiver: team-X-mails\nreceivers:\n- name: 'team-X-mails'\n`\n\t_, err := Load(in)\n\n\texpected := \"invalid label name \\\"-invalid-\\\" in group_by list\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t}\n}\n\nfunc TestRootRouteExists(t *testing.T) {\n\tin := `\nreceivers:\n- name: 'team-X-mails'\n`\n\t_, err := Load(in)\n\n\texpected := \"no routes provided\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t}\n}\n\nfunc TestRootRouteNoMuteTimes(t *testing.T) {\n\tin := `\nmute_time_intervals:\n- name: my_mute_time\n  time_intervals:\n  - times:\n     - start_time: '09:00'\n       end_time: '17:00'\n\nreceivers:\n- name: 'team-X-mails'\n\nroute:\n  receiver: 'team-X-mails'\n  mute_time_intervals:\n  - my_mute_time\n`\n\t_, err := Load(in)\n\n\texpected := \"root route must not have any mute time intervals\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t}\n}\n\nfunc TestRootRouteNoActiveTimes(t *testing.T) {\n\tin := `\ntime_intervals:\n- name: my_active_time\n  time_intervals:\n  - times:\n     - start_time: '09:00'\n       end_time: '17:00'\n\nreceivers:\n- name: 'team-X-mails'\n\nroute:\n  receiver: 'team-X-mails'\n  active_time_intervals:\n  - my_active_time\n`\n\t_, err := Load(in)\n\n\texpected := \"root route must not have any active time intervals\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t}\n}\n\nfunc TestRootRouteHasNoMatcher(t *testing.T) {\n\ttestCases := []struct {\n\t\tname string\n\t\tin   string\n\t}{\n\t\t{\n\t\t\tname: \"Test deprecated matchers on root route not allowed\",\n\t\t\tin: `\nroute:\n  receiver: 'team-X'\n  match:\n    severity: critical\nreceivers:\n- name: 'team-X'\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"Test matchers not allowed on root route\",\n\t\t\tin: `\nroute:\n  receiver: 'team-X'\n  matchers:\n    - severity=critical\nreceivers:\n- name: 'team-X'\n`,\n\t\t},\n\t}\n\texpected := \"root route must not have any matchers\"\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t_, err := Load(tc.in)\n\n\t\t\tif err == nil {\n\t\t\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t\t\t}\n\t\t\tif err.Error() != expected {\n\t\t\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestContinueErrorInRouteRoot(t *testing.T) {\n\tin := `\nroute:\n    receiver: team-X-mails\n    continue: true\n\nreceivers:\n- name: 'team-X-mails'\n`\n\t_, err := Load(in)\n\n\texpected := \"cannot have continue in root route\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t}\n}\n\nfunc TestGroupIntervalIsGreaterThanZero(t *testing.T) {\n\tin := `\nroute:\n    receiver: team-X-mails\n    group_interval: 0s\n\nreceivers:\n- name: 'team-X-mails'\n`\n\t_, err := Load(in)\n\n\texpected := \"group_interval cannot be zero\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t}\n}\n\nfunc TestRepeatIntervalIsGreaterThanZero(t *testing.T) {\n\tin := `\nroute:\n    receiver: team-X-mails\n    repeat_interval: 0s\n\nreceivers:\n- name: 'team-X-mails'\n`\n\t_, err := Load(in)\n\n\texpected := \"repeat_interval cannot be zero\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%q\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%q\\ngot:\\n%q\", expected, err.Error())\n\t}\n}\n\nfunc TestHideConfigSecrets(t *testing.T) {\n\tc, err := LoadFile(\"testdata/conf.good.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.good.yml\", err)\n\t}\n\n\t// String method must not reveal authentication credentials.\n\ts := c.String()\n\tif strings.Count(s, \"<secret>\") != 13 || strings.Contains(s, \"mysecret\") {\n\t\tt.Fatal(\"config's String method reveals authentication credentials.\")\n\t}\n}\n\nfunc TestJSONMarshal(t *testing.T) {\n\tc, err := LoadFile(\"testdata/conf.good.yml\")\n\tif err != nil {\n\t\tt.Errorf(\"Error parsing %s: %s\", \"testdata/conf.good.yml\", err)\n\t}\n\n\t_, err = json.Marshal(c)\n\tif err != nil {\n\t\tt.Fatal(\"JSON Marshaling failed:\", err)\n\t}\n}\n\nfunc TestJSONUnmarshal(t *testing.T) {\n\tc, err := LoadFile(\"testdata/conf.good.yml\")\n\tif err != nil {\n\t\tt.Errorf(\"Error parsing %s: %s\", \"testdata/conf.good.yml\", err)\n\t}\n\n\t_, err = json.Marshal(c)\n\tif err != nil {\n\t\tt.Fatal(\"JSON Marshaling failed:\", err)\n\t}\n}\n\nfunc TestMarshalIdempotency(t *testing.T) {\n\tc, err := LoadFile(\"testdata/conf.good.yml\")\n\tif err != nil {\n\t\tt.Errorf(\"Error parsing %s: %s\", \"testdata/conf.good.yml\", err)\n\t}\n\n\tmarshaled, err := yaml.Marshal(c)\n\tif err != nil {\n\t\tt.Fatal(\"YAML Marshaling failed:\", err)\n\t}\n\n\tc = new(Config)\n\tif err := yaml.Unmarshal(marshaled, c); err != nil {\n\t\tt.Fatal(\"YAML Unmarshaling failed:\", err)\n\t}\n}\n\nfunc TestGroupByAllNotMarshaled(t *testing.T) {\n\tin := `\nroute:\n    receiver: team-X-mails\n    group_by: [...]\n\nreceivers:\n- name: 'team-X-mails'\n`\n\tc, err := Load(in)\n\tif err != nil {\n\t\tt.Fatal(\"load failed:\", err)\n\t}\n\n\tdat, err := yaml.Marshal(c)\n\tif err != nil {\n\t\tt.Fatal(\"YAML Marshaling failed:\", err)\n\t}\n\n\tif strings.Contains(string(dat), \"groupbyall\") {\n\t\tt.Fatal(\"groupbyall found in config file\")\n\t}\n}\n\nfunc TestEmptyFieldsAndRegex(t *testing.T) {\n\tboolFoo := true\n\tregexpFoo := amcommoncfg.Regexp{\n\t\tRegexp:   regexp.MustCompile(\"^(?:^(foo1|foo2|baz)$)$\"),\n\t\tOriginal: \"^(foo1|foo2|baz)$\",\n\t}\n\n\texpectedConf := Config{\n\t\tGlobal: &GlobalConfig{\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{\n\t\t\t\tFollowRedirects: true,\n\t\t\t\tEnableHTTP2:     true,\n\t\t\t},\n\t\t\tResolveTimeout: model.Duration(5 * time.Minute),\n\t\t\tSMTPSmarthost:  HostPort{Host: \"localhost\", Port: \"25\"},\n\t\t\tSMTPFrom:       \"alertmanager@example.org\",\n\t\t\tSMTPTLSConfig: &commoncfg.TLSConfig{\n\t\t\t\tInsecureSkipVerify: false,\n\t\t\t},\n\t\t\tSlackAPIURL:      (*amcommoncfg.SecretURL)(amcommoncfg.MustParseURL(\"http://slack.example.com/\")),\n\t\t\tSlackAppURL:      amcommoncfg.MustParseURL(\"https://slack.com/api/chat.postMessage\"),\n\t\t\tSMTPRequireTLS:   true,\n\t\t\tPagerdutyURL:     amcommoncfg.MustParseURL(\"https://events.pagerduty.com/v2/enqueue\"),\n\t\t\tOpsGenieAPIURL:   amcommoncfg.MustParseURL(\"https://api.opsgenie.com/\"),\n\t\t\tWeChatAPIURL:     amcommoncfg.MustParseURL(\"https://qyapi.weixin.qq.com/cgi-bin/\"),\n\t\t\tVictorOpsAPIURL:  amcommoncfg.MustParseURL(\"https://alert.victorops.com/integrations/generic/20131114/alert/\"),\n\t\t\tTelegramAPIUrl:   amcommoncfg.MustParseURL(\"https://api.telegram.org\"),\n\t\t\tWebexAPIURL:      amcommoncfg.MustParseURL(\"https://webexapis.com/v1/messages\"),\n\t\t\tRocketchatAPIURL: amcommoncfg.MustParseURL(\"https://open.rocket.chat/\"),\n\t\t},\n\n\t\tTemplates: []string{\n\t\t\t\"/etc/alertmanager/template/*.tmpl\",\n\t\t},\n\t\tRoute: &Route{\n\t\t\tReceiver: \"team-X-mails\",\n\t\t\tGroupBy: []model.LabelName{\n\t\t\t\t\"alertname\",\n\t\t\t\t\"cluster\",\n\t\t\t\t\"service\",\n\t\t\t},\n\t\t\tGroupByStr: []string{\n\t\t\t\t\"alertname\",\n\t\t\t\t\"cluster\",\n\t\t\t\t\"service\",\n\t\t\t},\n\t\t\tGroupByAll: false,\n\t\t\tRoutes: []*Route{\n\t\t\t\t{\n\t\t\t\t\tReceiver: \"team-X-mails\",\n\t\t\t\t\tMatchRE: map[string]amcommoncfg.Regexp{\n\t\t\t\t\t\t\"service\": regexpFoo,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tReceivers: []Receiver{\n\t\t\t{\n\t\t\t\tName: \"team-X-mails\",\n\t\t\t\tEmailConfigs: []*EmailConfig{\n\t\t\t\t\t{\n\t\t\t\t\t\tTo:         \"team-X+alerts@example.org\",\n\t\t\t\t\t\tFrom:       \"alertmanager@example.org\",\n\t\t\t\t\t\tSmarthost:  HostPort{Host: \"localhost\", Port: \"25\"},\n\t\t\t\t\t\tHTML:       \"{{ template \\\"email.default.html\\\" . }}\",\n\t\t\t\t\t\tRequireTLS: &boolFoo,\n\t\t\t\t\t\tTLSConfig: &commoncfg.TLSConfig{\n\t\t\t\t\t\t\tInsecureSkipVerify: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\t// Load a non-empty configuration to ensure that all fields are overwritten.\n\t// See https://github.com/prometheus/alertmanager/issues/1649.\n\t_, err := LoadFile(\"testdata/conf.good.yml\")\n\tif err != nil {\n\t\tt.Errorf(\"Error parsing %s: %s\", \"testdata/conf.good.yml\", err)\n\t}\n\n\tconfig, err := LoadFile(\"testdata/conf.empty-fields.yml\")\n\tif err != nil {\n\t\tt.Errorf(\"Error parsing %s: %s\", \"testdata/conf.empty-fields.yml\", err)\n\t}\n\n\tconfigGot, err := yaml.Marshal(config)\n\tif err != nil {\n\t\tt.Fatal(\"YAML Marshaling failed:\", err)\n\t}\n\n\tconfigExp, err := yaml.Marshal(expectedConf)\n\tif err != nil {\n\t\tt.Fatalf(\"%s\", err)\n\t}\n\n\tif !reflect.DeepEqual(configGot, configExp) {\n\t\tt.Fatalf(\"%s: unexpected config result: \\n\\n%s\\n expected\\n\\n%s\", \"testdata/conf.empty-fields.yml\", configGot, configExp)\n\t}\n}\n\nfunc TestEmptyConfigOfIntegration(t *testing.T) {\n\tbaseConfigTmpl := `\nglobal:\nroute:\n  receiver: 'test-receiver'\nreceivers:\n- name: 'test-receiver'\n  %s:\n  -\n`\n\n\ttests := []struct {\n\t\tintegration string // The key name in YAML (e.g., webhook_configs)\n\t\texpectedErr string // The unique error message expected for this integration\n\t}{\n\t\t{\n\t\t\tintegration: \"discord_configs\",\n\t\t\texpectedErr: \"missing discord config\",\n\t\t},\n\t\t{\n\t\t\tintegration: \"email_configs\",\n\t\t\texpectedErr: \"missing email config\",\n\t\t},\n\t\t{\n\t\t\tintegration: \"incidentio_configs\",\n\t\t\texpectedErr: \"missing incidentio config\",\n\t\t},\n\t\t{\n\t\t\tintegration: \"pagerduty_configs\",\n\t\t\texpectedErr: \"missing pagerduty config\",\n\t\t},\n\t\t{\n\t\t\tintegration: \"webhook_configs\",\n\t\t\texpectedErr: \"missing webhook config\",\n\t\t},\n\t\t{\n\t\t\tintegration: \"pushover_configs\",\n\t\t\texpectedErr: \"missing pushover config\",\n\t\t},\n\t\t{\n\t\t\tintegration: \"victorops_configs\",\n\t\t\texpectedErr: \"missing victorops config\",\n\t\t},\n\t\t{\n\t\t\tintegration: \"sns_configs\",\n\t\t\texpectedErr: \"missing sns config\",\n\t\t},\n\t\t{\n\t\t\tintegration: \"telegram_configs\",\n\t\t\texpectedErr: \"missing telegram config\",\n\t\t},\n\t\t{\n\t\t\tintegration: \"webex_configs\",\n\t\t\texpectedErr: \"missing webex config\",\n\t\t},\n\t\t{\n\t\t\tintegration: \"msteams_configs\",\n\t\t\texpectedErr: \"missing msteams config\",\n\t\t},\n\t\t{\n\t\t\tintegration: \"msteamsv2_configs\",\n\t\t\texpectedErr: \"missing msteamsv2 config\",\n\t\t},\n\t\t{\n\t\t\tintegration: \"jira_configs\",\n\t\t\texpectedErr: \"missing jira config\",\n\t\t},\n\t\t{\n\t\t\tintegration: \"mattermost_configs\",\n\t\t\texpectedErr: \"missing mattermost config\",\n\t\t},\n\t\t{\n\t\t\tintegration: \"slack_configs\",\n\t\t\texpectedErr: \"no Slack API URL nor App token set either inline or in a file\",\n\t\t},\n\t\t{\n\t\t\tintegration: \"opsgenie_configs\",\n\t\t\texpectedErr: \"no global OpsGenie API Key set either inline or in a file\",\n\t\t},\n\t\t{\n\t\t\tintegration: \"wechat_configs\",\n\t\t\texpectedErr: \"no global Wechat Api Secret set either inline or in a file\",\n\t\t},\n\t\t{\n\t\t\tintegration: \"rocketchat_configs\",\n\t\t\texpectedErr: \"no global Rocketchat TokenID set either inline or in a file\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.integration, func(t *testing.T) {\n\t\t\tin := fmt.Sprintf(baseConfigTmpl, tc.integration)\n\t\t\t_, err := Load(in)\n\t\t\trequire.Error(t, err, \"Expected empty configuration to be an error for %s\", tc.integration)\n\t\t\trequire.ErrorContains(t, err, tc.expectedErr)\n\t\t})\n\t}\n}\n\nfunc TestGlobalAndLocalHTTPConfig(t *testing.T) {\n\tconfig, err := LoadFile(\"testdata/conf.http-config.good.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf-http-config.good.yml\", err)\n\t}\n\n\tif config.Global.HTTPConfig.FollowRedirects {\n\t\tt.Fatalf(\"global HTTP config should not follow redirects\")\n\t}\n\n\tif !config.Receivers[0].SlackConfigs[0].HTTPConfig.FollowRedirects {\n\t\tt.Fatalf(\"global HTTP config should follow redirects\")\n\t}\n}\n\nfunc TestSMTPHello(t *testing.T) {\n\tc, err := LoadFile(\"testdata/conf.good.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.good.yml\", err)\n\t}\n\n\tconst refValue = \"host.example.org\"\n\thostName := c.Global.SMTPHello\n\tif hostName != refValue {\n\t\tt.Errorf(\"Invalid SMTP Hello hostname: %s\\nExpected: %s\", hostName, refValue)\n\t}\n}\n\nfunc TestSMTPBothPasswordAndFile(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.smtp-both-password-and-file.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.smtp-both-password-and-file.yml\", err)\n\t}\n\tif err.Error() != \"at most one of smtp_auth_password & smtp_auth_password_file must be configured\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"at most one of auth_password & auth_password_file must be configured\", err.Error())\n\t}\n}\n\nfunc TestSMTPNoUsernameOrPassword(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.smtp-no-username-or-password.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.smtp-no-username-or-password.yml\", err)\n\t}\n}\n\nfunc TestGlobalAndLocalSMTPPassword(t *testing.T) {\n\tconfig, err := LoadFile(\"testdata/conf.smtp-password-global-and-local.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.smtp-password-global-and-local.yml\", err)\n\t}\n\n\trequire.Equal(t, \"/tmp/globaluserpassword\", config.Receivers[0].EmailConfigs[0].AuthPasswordFile, \"first email should use password file /tmp/globaluserpassword\")\n\trequire.Emptyf(t, config.Receivers[0].EmailConfigs[0].AuthPassword, \"password field should be empty when file provided\")\n\n\trequire.Equal(t, \"/tmp/localuser1password\", config.Receivers[0].EmailConfigs[1].AuthPasswordFile, \"second email should use password file /tmp/localuser1password\")\n\trequire.Emptyf(t, config.Receivers[0].EmailConfigs[1].AuthPassword, \"password field should be empty when file provided\")\n\n\trequire.Equal(t, commoncfg.Secret(\"mysecret\"), config.Receivers[0].EmailConfigs[2].AuthPassword, \"third email should use password mysecret\")\n\trequire.Emptyf(t, config.Receivers[0].EmailConfigs[2].AuthPasswordFile, \"file field should be empty when password provided\")\n\n\trequire.Equal(t, commoncfg.Secret(\"myprecious\"), config.Receivers[0].EmailConfigs[3].AuthSecret, \"fourth email should use secret myprecious\")\n\n\trequire.Equal(t, \"/tmp/localuser4secret\", config.Receivers[0].EmailConfigs[4].AuthSecretFile, \"fifth email should use secret file /tmp/localuser4secret\")\n}\n\nfunc TestGroupByAll(t *testing.T) {\n\tc, err := LoadFile(\"testdata/conf.group-by-all.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.group-by-all.yml\", err)\n\t}\n\n\tif !c.Route.GroupByAll {\n\t\tt.Errorf(\"Invalid group by all param: expected to by true\")\n\t}\n}\n\nfunc TestVictorOpsDefaultAPIKey(t *testing.T) {\n\tconf, err := LoadFile(\"testdata/conf.victorops-default-apikey.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.victorops-default-apikey.yml\", err)\n\t}\n\n\tdefaultKey := conf.Global.VictorOpsAPIKey\n\toverrideKey := commoncfg.Secret(\"qwe456\")\n\tif defaultKey != conf.Receivers[0].VictorOpsConfigs[0].APIKey {\n\t\tt.Fatalf(\"Invalid victorops key: %s\\nExpected: %s\", conf.Receivers[0].VictorOpsConfigs[0].APIKey, defaultKey)\n\t}\n\tif overrideKey != conf.Receivers[1].VictorOpsConfigs[0].APIKey {\n\t\tt.Errorf(\"Invalid victorops key: %s\\nExpected: %s\", conf.Receivers[1].VictorOpsConfigs[0].APIKey, string(overrideKey))\n\t}\n}\n\nfunc TestVictorOpsDefaultAPIKeyFile(t *testing.T) {\n\tconf, err := LoadFile(\"testdata/conf.victorops-default-apikey-file.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.victorops-default-apikey-file.yml\", err)\n\t}\n\n\tdefaultKey := conf.Global.VictorOpsAPIKeyFile\n\toverrideKey := \"/override_file\"\n\tif defaultKey != conf.Receivers[0].VictorOpsConfigs[0].APIKeyFile {\n\t\tt.Fatalf(\"Invalid VictorOps key_file: %s\\nExpected: %s\", conf.Receivers[0].VictorOpsConfigs[0].APIKeyFile, defaultKey)\n\t}\n\tif overrideKey != conf.Receivers[1].VictorOpsConfigs[0].APIKeyFile {\n\t\tt.Errorf(\"Invalid VictorOps key_file: %s\\nExpected: %s\", conf.Receivers[1].VictorOpsConfigs[0].APIKeyFile, overrideKey)\n\t}\n}\n\nfunc TestVictorOpsBothAPIKeyAndFile(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.victorops-both-file-and-apikey.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.victorops-both-file-and-apikey.yml\", err)\n\t}\n\tif err.Error() != \"at most one of victorops_api_key & victorops_api_key_file must be configured\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"at most one of victorops_api_key & victorops_api_key_file must be configured\", err.Error())\n\t}\n}\n\nfunc TestVictorOpsNoAPIKey(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.victorops-no-apikey.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.victorops-no-apikey.yml\", err)\n\t}\n\tif err.Error() != \"no global VictorOps API Key set\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"no global VictorOps API Key set\", err.Error())\n\t}\n}\n\nfunc TestTelegramDefaultBotToken(t *testing.T) {\n\tconf, err := LoadFile(\"testdata/conf.telegram-default-bot-token.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.telegram-default-bot-token.yml\", err)\n\t}\n\n\tdefaultBotToken := conf.Global.TelegramBotToken\n\toverrideBotToken := commoncfg.Secret(\"qwe456\")\n\tif defaultBotToken != conf.Receivers[0].TelegramConfigs[0].BotToken {\n\t\tt.Fatalf(\"Invalid telegram bot token: %s\\nExpected: %s\", conf.Receivers[0].TelegramConfigs[0].BotToken, defaultBotToken)\n\t}\n\tif overrideBotToken != conf.Receivers[1].TelegramConfigs[0].BotToken {\n\t\tt.Errorf(\"Invalid telegram bot token: %s\\nExpected: %s\", conf.Receivers[1].TelegramConfigs[0].BotToken, string(overrideBotToken))\n\t}\n}\n\nfunc TestTelegramDefaultBotTokenFile(t *testing.T) {\n\tconf, err := LoadFile(\"testdata/conf.telegram-default-bot-token-file.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.telegram-default-bot-token-file.yml\", err)\n\t}\n\n\tdefaultBotToken := conf.Global.TelegramBotTokenFile\n\toverrideBotToken := \"/override_file\"\n\tif defaultBotToken != conf.Receivers[0].TelegramConfigs[0].BotTokenFile {\n\t\tt.Fatalf(\"Invalid telegram bot token file: %s\\nExpected: %s\", conf.Receivers[0].TelegramConfigs[0].BotTokenFile, defaultBotToken)\n\t}\n\tif overrideBotToken != conf.Receivers[1].TelegramConfigs[0].BotTokenFile {\n\t\tt.Errorf(\"Invalid telegram bot token file: %s\\nExpected: %s\", conf.Receivers[1].TelegramConfigs[0].BotTokenFile, overrideBotToken)\n\t}\n}\n\nfunc TestTelegramBothBotTokenAndFile(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.telegram-both-bot-token-and-file.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.telegram-both-bot-token-and-file.yml\", err)\n\t}\n\tif err.Error() != \"at most one of telegram_bot_token & telegram_bot_token_file must be configured\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"at most one of telegram_bot_token & telegram_bot_token_file must be configured\", err.Error())\n\t}\n}\n\nfunc TestTelegramValidReceiverBothBotTokenAndFile(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.telegram-valid-receiver-both-bot-token-and-file.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.telegram-valid-receiver-both-bot-token-and-file.yml\", err)\n\t}\n\tif err.Error() != \"at most one of telegram_bot_token & telegram_bot_token_file must be configured\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"at most one of telegram_bot_token & telegram_bot_token_file must be configured\", err.Error())\n\t}\n}\n\nfunc TestTelegramNoBotToken(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.telegram-no-bot-token.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.telegram-no-bot-token.yml\", err)\n\t}\n\tif err.Error() != \"missing bot_token or bot_token_file on telegram_config\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"missing bot_token or bot_token_file on telegram_config\", err.Error())\n\t}\n}\n\nfunc TestOpsGenieDefaultAPIKey(t *testing.T) {\n\tconf, err := LoadFile(\"testdata/conf.opsgenie-default-apikey.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.opsgenie-default-apikey.yml\", err)\n\t}\n\n\tdefaultKey := conf.Global.OpsGenieAPIKey\n\tif defaultKey != conf.Receivers[0].OpsGenieConfigs[0].APIKey {\n\t\tt.Fatalf(\"Invalid OpsGenie key: %s\\nExpected: %s\", conf.Receivers[0].OpsGenieConfigs[0].APIKey, defaultKey)\n\t}\n\tif defaultKey == conf.Receivers[1].OpsGenieConfigs[0].APIKey {\n\t\tt.Errorf(\"Invalid OpsGenie key: %s\\nExpected: %s\", conf.Receivers[1].OpsGenieConfigs[0].APIKey, \"qwe456\")\n\t}\n}\n\nfunc TestOpsGenieDefaultAPIKeyFile(t *testing.T) {\n\tconf, err := LoadFile(\"testdata/conf.opsgenie-default-apikey-file.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.opsgenie-default-apikey-file.yml\", err)\n\t}\n\n\tdefaultKey := conf.Global.OpsGenieAPIKeyFile\n\tif defaultKey != conf.Receivers[0].OpsGenieConfigs[0].APIKeyFile {\n\t\tt.Fatalf(\"Invalid OpsGenie key_file: %s\\nExpected: %s\", conf.Receivers[0].OpsGenieConfigs[0].APIKeyFile, defaultKey)\n\t}\n\tif defaultKey == conf.Receivers[1].OpsGenieConfigs[0].APIKeyFile {\n\t\tt.Errorf(\"Invalid OpsGenie key_file: %s\\nExpected: %s\", conf.Receivers[1].OpsGenieConfigs[0].APIKeyFile, \"/override_file\")\n\t}\n}\n\nfunc TestOpsGenieBothAPIKeyAndFile(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.opsgenie-both-file-and-apikey.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.opsgenie-both-file-and-apikey.yml\", err)\n\t}\n\tif err.Error() != \"at most one of opsgenie_api_key & opsgenie_api_key_file must be configured\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"at most one of opsgenie_api_key & opsgenie_api_key_file must be configured\", err.Error())\n\t}\n}\n\nfunc TestOpsGenieNoAPIKey(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.opsgenie-no-apikey.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.opsgenie-no-apikey.yml\", err)\n\t}\n\tif err.Error() != \"no global OpsGenie API Key set either inline or in a file\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"no global OpsGenie API Key set either inline or in a file\", err.Error())\n\t}\n}\n\nfunc TestOpsGenieDeprecatedTeamSpecified(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.opsgenie-default-apikey-old-team.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.opsgenie-default-apikey-old-team.yml\", err)\n\t}\n\n\tconst expectedErr = `yaml: unmarshal errors:\n  line 16: field teams not found in type config.plain`\n\tif err.Error() != expectedErr {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", expectedErr, err.Error())\n\t}\n}\n\nfunc TestSlackBothAPIURLAndFile(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.slack-both-file-and-url.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.slack-both-file-and-url.yml\", err)\n\t}\n\tif err.Error() != \"at most one of slack_api_url & slack_api_url_file must be configured\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"at most one of slack_api_url & slack_api_url_file must be configured\", err.Error())\n\t}\n}\n\nfunc TestSlackBothAppTokenAndFile(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.slack-both-file-and-token.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.slack-both-file-and-token.yml\", err)\n\t}\n\tif err.Error() != \"at most one of slack_app_token & slack_app_token_file must be configured\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"at most one of slack_app_token & slack_app_token_file must be configured\", err.Error())\n\t}\n}\n\nfunc TestSlackBothAppTokenAndAPIURL(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.slack-both-url-and-token.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.slack-both-url-and-token.yml\", err)\n\t}\n\tif err.Error() != \"at most one of slack_app_token/slack_app_token_file & slack_api_url/slack_api_url_file must be configured\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"at most one of slack_app_token/slack_app_token_file & slack_api_url/slack_api_url_file must be configured\", err.Error())\n\t}\n}\n\nfunc TestSlackUpdateMessageWebhookURL(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.slack-update-message-and-webhook.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.slack-update-message-and-webhook\", err)\n\t}\n\tif err.Error() != \"update_message can only be used with bot tokens. api_url must be set to https://slack.com/api/chat.postMessage\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"update_message can only be used with bot tokens. api_url must be set to https://slack.com/api/chat.postMessage\", err.Error())\n\t}\n}\n\nfunc TestSlackGlobalAppToken(t *testing.T) {\n\tconf, err := LoadFile(\"testdata/conf.slack-default-app-token.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.slack-default-app-token.yml\", err)\n\t}\n\n\t// no override\n\tdefaultToken := conf.Global.SlackAppToken\n\tfirstAuth := commoncfg.Authorization{\n\t\tType:        \"Bearer\",\n\t\tCredentials: commoncfg.Secret(defaultToken),\n\t}\n\tfirstConfig := conf.Receivers[0].SlackConfigs[0]\n\tif firstConfig.AppToken != defaultToken {\n\t\tt.Fatalf(\"Invalid Slack App token: %s\\nExpected: %s\", firstConfig.AppToken, defaultToken)\n\t}\n\tif firstConfig.APIURL.String() != conf.Global.SlackAppURL.String() {\n\t\tt.Fatalf(\"Expected API URL: %s\\nGot: %s\", conf.Global.SlackAppURL.String(), firstConfig.APIURL.String())\n\t}\n\tif firstConfig.HTTPConfig == nil || firstConfig.HTTPConfig.Authorization == nil {\n\t\tt.Fatalf(\"Error configuring Slack App authorization: %s\", firstConfig.HTTPConfig)\n\t}\n\tif firstConfig.HTTPConfig.Authorization.Type != firstAuth.Type {\n\t\tt.Fatalf(\"Error configuring Slack App authorization type: %s\\nExpected: %s\", firstConfig.HTTPConfig.Authorization.Type, firstAuth.Type)\n\t}\n\tif firstConfig.HTTPConfig.Authorization.Credentials != firstAuth.Credentials {\n\t\tt.Fatalf(\"Error configuring Slack App authorization credentials: %s\\nExpected: %s\", firstConfig.HTTPConfig.Authorization.Credentials, firstAuth.Credentials)\n\t}\n\n\t// inline override\n\tinlineToken := \"xoxb-1234-xxxxxx\"\n\tsecondAuth := commoncfg.Authorization{\n\t\tType:        \"Bearer\",\n\t\tCredentials: commoncfg.Secret(inlineToken),\n\t}\n\tsecondConfig := conf.Receivers[0].SlackConfigs[1]\n\tif secondConfig.AppToken != commoncfg.Secret(inlineToken) {\n\t\tt.Fatalf(\"Invalid Slack App token: %s\\nExpected: %s\", secondConfig.AppToken, inlineToken)\n\t}\n\tif secondConfig.HTTPConfig == nil || secondConfig.HTTPConfig.Authorization == nil {\n\t\tt.Fatalf(\"Error configuring Slack App authorization: %s\", secondConfig.HTTPConfig)\n\t}\n\tif secondConfig.HTTPConfig.Authorization.Type != secondAuth.Type {\n\t\tt.Fatalf(\"Error configuring Slack App authorization type: %s\\nExpected: %s\", secondConfig.HTTPConfig.Authorization.Type, secondAuth.Type)\n\t}\n\tif secondConfig.HTTPConfig.Authorization.Credentials != secondAuth.Credentials {\n\t\tt.Fatalf(\"Error configuring Slack App authorization credentials: %s\\nExpected: %s\", secondConfig.HTTPConfig.Authorization.Credentials, secondAuth.Credentials)\n\t}\n\n\t// custom app url\n\tthirdConfig := conf.Receivers[0].SlackConfigs[2]\n\tif thirdConfig.AppURL.String() != \"http://api.fakeslack.example/\" {\n\t\tt.Fatalf(\"Invalid Slack URL: %s\\nExpected: %s\", thirdConfig.APIURL.String(), \"http://mysecret.example.com/\")\n\t}\n\n\t// workaround override\n\tworkaroundToken := \"xoxb-my-bot-token\"\n\tfourthAuth := commoncfg.Authorization{\n\t\tType:        \"Bearer\",\n\t\tCredentials: commoncfg.Secret(workaroundToken),\n\t}\n\tfourthConfig := conf.Receivers[0].SlackConfigs[3]\n\tif fourthConfig.AppToken != \"\" {\n\t\tt.Fatalf(\"Invalid Slack App token: %q\\nExpected: %q\", fourthConfig.AppToken, \"\")\n\t}\n\tif fourthConfig.HTTPConfig == nil || fourthConfig.HTTPConfig.Authorization == nil {\n\t\tt.Fatalf(\"Error configuring Slack App authorization: %s\", fourthConfig.HTTPConfig)\n\t}\n\tif fourthConfig.HTTPConfig.Authorization.Type != fourthAuth.Type {\n\t\tt.Fatalf(\"Error configuring Slack App authorization type: %s\\nExpected: %s\", fourthConfig.HTTPConfig.Authorization.Type, fourthAuth.Type)\n\t}\n\tif fourthConfig.HTTPConfig.Authorization.Credentials != fourthAuth.Credentials {\n\t\tt.Fatalf(\"Error configuring Slack App authorization credentials: %s\\nExpected: %s\", fourthConfig.HTTPConfig.Authorization.Credentials, fourthAuth.Credentials)\n\t}\n\n\t// override the global file with an inline webhook URL\n\tapiURL := \"https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX\"\n\tfifthConfig := conf.Receivers[0].SlackConfigs[4]\n\tif fifthConfig.APIURL.String() != apiURL || fifthConfig.APIURLFile != \"\" {\n\t\tt.Fatalf(\"Invalid Slack URL: %s\\nExpected: %s\", fifthConfig.APIURL.String(), apiURL)\n\t}\n}\n\nfunc TestSlackNoAPIURL(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.slack-no-api-url-or-token.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.slack-no-api-url-or-token.yml\", err)\n\t}\n\tif err.Error() != \"no Slack API URL nor App token set either inline or in a file\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"no Slack API URL nor App token set either inline or in a file\", err.Error())\n\t}\n}\n\nfunc TestSlackGlobalAPIURLFile(t *testing.T) {\n\tconf, err := LoadFile(\"testdata/conf.slack-default-api-url-file.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.slack-default-api-url-file.yml\", err)\n\t}\n\n\t// no override\n\tfirstConfig := conf.Receivers[0].SlackConfigs[0]\n\tif firstConfig.APIURLFile != \"/global_file\" || firstConfig.APIURL != nil {\n\t\tt.Fatalf(\"Invalid Slack URL file: %s\\nExpected: %s\", firstConfig.APIURLFile, \"/global_file\")\n\t}\n\n\t// override the file\n\tsecondConfig := conf.Receivers[0].SlackConfigs[1]\n\tif secondConfig.APIURLFile != \"/override_file\" || secondConfig.APIURL != nil {\n\t\tt.Fatalf(\"Invalid Slack URL file: %s\\nExpected: %s\", secondConfig.APIURLFile, \"/override_file\")\n\t}\n\n\t// override the global file with an inline URL\n\tthirdConfig := conf.Receivers[0].SlackConfigs[2]\n\tif thirdConfig.APIURL.String() != \"http://mysecret.example.com/\" || thirdConfig.APIURLFile != \"\" {\n\t\tt.Fatalf(\"Invalid Slack URL: %s\\nExpected: %s\", thirdConfig.APIURL.String(), \"http://mysecret.example.com/\")\n\t}\n}\n\nfunc TestValidSNSConfig(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.sns-topic-arn.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.sns-topic-arn.yml\\\"\", err)\n\t}\n}\n\nfunc TestInvalidSNSConfig(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.sns-invalid.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"expected error with missing fields on SNS config\")\n\t}\n\tconst expectedErr = `must provide either a Target ARN, Topic ARN, or Phone Number for SNS config`\n\tif err.Error() != expectedErr {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", expectedErr, err.Error())\n\t}\n}\n\nfunc TestRocketchatDefaultToken(t *testing.T) {\n\tconf, err := LoadFile(\"testdata/conf.rocketchat-default-token.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.rocketchat-default-token.yml\", err)\n\t}\n\n\tdefaultToken := conf.Global.RocketchatToken\n\toverrideToken := commoncfg.Secret(\"token456\")\n\tif defaultToken != conf.Receivers[0].RocketchatConfigs[0].Token {\n\t\tt.Fatalf(\"Invalid rocketchat key: %s\\nExpected: %s\", string(*conf.Receivers[0].RocketchatConfigs[0].Token), string(*defaultToken))\n\t}\n\tif overrideToken != *conf.Receivers[1].RocketchatConfigs[0].Token {\n\t\tt.Errorf(\"Invalid rocketchat key: %s\\nExpected: %s\", string(*conf.Receivers[1].RocketchatConfigs[0].Token), string(overrideToken))\n\t}\n}\n\nfunc TestRocketchatDefaultTokenID(t *testing.T) {\n\tconf, err := LoadFile(\"testdata/conf.rocketchat-default-token.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.rocketchat-default-token.yml\", err)\n\t}\n\n\tdefaultTokenID := conf.Global.RocketchatTokenID\n\toverrideTokenID := commoncfg.Secret(\"id456\")\n\tif defaultTokenID != conf.Receivers[0].RocketchatConfigs[0].TokenID {\n\t\tt.Fatalf(\"Invalid rocketchat key: %s\\nExpected: %s\", string(*conf.Receivers[0].RocketchatConfigs[0].TokenID), string(*defaultTokenID))\n\t}\n\tif overrideTokenID != *conf.Receivers[1].RocketchatConfigs[0].TokenID {\n\t\tt.Errorf(\"Invalid rocketchat key: %s\\nExpected: %s\", string(*conf.Receivers[1].RocketchatConfigs[0].TokenID), string(overrideTokenID))\n\t}\n}\n\nfunc TestRocketchatDefaultTokenFile(t *testing.T) {\n\tconf, err := LoadFile(\"testdata/conf.rocketchat-default-token-file.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.rocketchat-default-token-file.yml\", err)\n\t}\n\n\tdefaultTokenFile := conf.Global.RocketchatTokenFile\n\toverrideTokenFile := \"/override_file\"\n\tif defaultTokenFile != conf.Receivers[0].RocketchatConfigs[0].TokenFile {\n\t\tt.Fatalf(\"Invalid Rocketchat key_file: %s\\nExpected: %s\", conf.Receivers[0].RocketchatConfigs[0].TokenFile, defaultTokenFile)\n\t}\n\tif overrideTokenFile != conf.Receivers[1].RocketchatConfigs[0].TokenFile {\n\t\tt.Errorf(\"Invalid Rocketchat key_file: %s\\nExpected: %s\", conf.Receivers[1].RocketchatConfigs[0].TokenFile, overrideTokenFile)\n\t}\n}\n\nfunc TestRocketchatDefaultIDTokenFile(t *testing.T) {\n\tconf, err := LoadFile(\"testdata/conf.rocketchat-default-token-file.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.rocketchat-default-token-file.yml\", err)\n\t}\n\n\tdefaultTokenIDFile := conf.Global.RocketchatTokenIDFile\n\toverrideTokenIDFile := \"/override_file\"\n\tif defaultTokenIDFile != conf.Receivers[0].RocketchatConfigs[0].TokenIDFile {\n\t\tt.Fatalf(\"Invalid Rocketchat key_file: %s\\nExpected: %s\", conf.Receivers[0].RocketchatConfigs[0].TokenIDFile, defaultTokenIDFile)\n\t}\n\tif overrideTokenIDFile != conf.Receivers[1].RocketchatConfigs[0].TokenIDFile {\n\t\tt.Errorf(\"Invalid Rocketchat key_file: %s\\nExpected: %s\", conf.Receivers[1].RocketchatConfigs[0].TokenIDFile, overrideTokenIDFile)\n\t}\n}\n\nfunc TestRocketchatBothTokenAndTokenFile(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.rocketchat-both-token-and-tokenfile.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.rocketchat-both-token-and-tokenfile.yml\", err)\n\t}\n\tif err.Error() != \"at most one of rocketchat_token & rocketchat_token_file must be configured\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"at most one of rocketchat_token & rocketchat_token_file must be configured\", err.Error())\n\t}\n}\n\nfunc TestRocketchatBothTokenIDAndTokenIDFile(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.rocketchat-both-tokenid-and-tokenidfile.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.rocketchat-both-tokenid-and-tokenidfile.yml\", err)\n\t}\n\tif err.Error() != \"at most one of rocketchat_token_id & rocketchat_token_id_file must be configured\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"at most one of rocketchat_token_id & rocketchat_token_id_file must be configured\", err.Error())\n\t}\n}\n\nfunc TestRocketchatNoToken(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.rocketchat-no-token.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.rocketchat-no-token.yml\", err)\n\t}\n\tif err.Error() != \"no global Rocketchat Token set either inline or in a file\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"no global Rocketchat Token set either inline or in a file\", err.Error())\n\t}\n}\n\nfunc TestUnmarshalHostPort(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tin string\n\n\t\texp     HostPort\n\t\tjsonOut string\n\t\tyamlOut string\n\t\terr     bool\n\t}{\n\t\t{\n\t\t\tin:  `\"\"`,\n\t\t\texp: HostPort{},\n\t\t\tyamlOut: `\"\"\n`,\n\t\t\tjsonOut: `\"\"`,\n\t\t},\n\t\t{\n\t\t\tin:  `\"localhost:25\"`,\n\t\t\texp: HostPort{Host: \"localhost\", Port: \"25\"},\n\t\t\tyamlOut: `localhost:25\n`,\n\t\t\tjsonOut: `\"localhost:25\"`,\n\t\t},\n\t\t{\n\t\t\tin:  `\":25\"`,\n\t\t\texp: HostPort{Host: \"\", Port: \"25\"},\n\t\t\tyamlOut: `:25\n`,\n\t\t\tjsonOut: `\":25\"`,\n\t\t},\n\t\t{\n\t\t\tin:  `\"localhost\"`,\n\t\t\terr: true,\n\t\t},\n\t\t{\n\t\t\tin:  `\"localhost:\"`,\n\t\t\terr: true,\n\t\t},\n\t\t{\n\t\t\tin:  `\"[fd12:3456:789a::1]:25\"`,\n\t\t\texp: HostPort{Host: \"fd12:3456:789a::1\", Port: \"25\"},\n\t\t\tyamlOut: `'[fd12:3456:789a::1]:25'\n`,\n\t\t\tjsonOut: `\"[fd12:3456:789a::1]:25\"`,\n\t\t},\n\t} {\n\t\tt.Run(tc.in, func(t *testing.T) {\n\t\t\thp := HostPort{}\n\t\t\terr := yaml.Unmarshal([]byte(tc.in), &hp)\n\t\t\tif tc.err {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.exp, hp)\n\n\t\t\tb, err := yaml.Marshal(&hp)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.yamlOut, string(b))\n\n\t\t\tb, err = json.Marshal(&hp)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.jsonOut, string(b))\n\t\t})\n\t}\n}\n\nfunc TestNilRegexp(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tfile   string\n\t\terrMsg string\n\t}{\n\t\t{\n\t\t\tfile:   \"testdata/conf.nil-match_re-route.yml\",\n\t\t\terrMsg: \"invalid_label\",\n\t\t},\n\t\t{\n\t\t\tfile:   \"testdata/conf.nil-source_match_re-inhibition.yml\",\n\t\t\terrMsg: \"invalid_source_label\",\n\t\t},\n\t\t{\n\t\t\tfile:   \"testdata/conf.nil-target_match_re-inhibition.yml\",\n\t\t\terrMsg: \"invalid_target_label\",\n\t\t},\n\t} {\n\t\tt.Run(tc.file, func(t *testing.T) {\n\t\t\t_, err := os.Stat(tc.file)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t_, err = LoadFile(tc.file)\n\t\t\trequire.Error(t, err)\n\t\t\trequire.Contains(t, err.Error(), tc.errMsg)\n\t\t})\n\t}\n}\n\nfunc TestSecretTemplURL(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tinput       string\n\t\texpectError bool\n\t\terrorMsg    string\n\t}{\n\t\t{\n\t\t\tname:        \"valid http URL\",\n\t\t\tinput:       `\"http://example.com/webhook\"`,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid URL missing scheme\",\n\t\t\tinput:       `\"example.com/webhook\"`,\n\t\t\texpectError: true,\n\t\t\terrorMsg:    \"unsupported scheme\",\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid URL unsupported scheme\",\n\t\t\tinput:       `\"ftp://example.com/webhook\"`,\n\t\t\texpectError: true,\n\t\t\terrorMsg:    \"unsupported scheme\",\n\t\t},\n\t\t{\n\t\t\tname:        \"templated URL is not validated\",\n\t\t\tinput:       `\"http://example.com/{{ .GroupLabels.alertname }}\"`,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid URL with template is not validated\",\n\t\t\tinput:       `\"not-a-url-{{ .GroupLabels.alertname }}\"`,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid template syntax\",\n\t\t\tinput:       `\"http://example.com/{{ .Invalid\"`,\n\t\t\texpectError: true,\n\t\t\terrorMsg:    \"invalid template syntax\",\n\t\t},\n\t\t{\n\t\t\tname:        \"empty string\",\n\t\t\tinput:       `\"\"`,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"secret token\",\n\t\t\tinput:       `\"<secret>\"`,\n\t\t\texpectError: false,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar u SecretTemplateURL\n\t\t\terr := yaml.Unmarshal([]byte(tc.input), &u)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif tc.errorMsg != \"\" {\n\t\t\t\t\trequire.Contains(t, err.Error(), tc.errorMsg)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSecretTemplURLMarshaling(t *testing.T) {\n\tt.Run(\"marshals to secret token by default\", func(t *testing.T) {\n\t\tu := SecretTemplateURL(\"http://example.com/secret\")\n\n\t\tyamlOut, err := yaml.Marshal(&u)\n\t\trequire.NoError(t, err)\n\t\trequire.YAMLEq(t, \"<secret>\\n\", string(yamlOut))\n\n\t\tjsonOut, err := json.Marshal(&u)\n\t\trequire.NoError(t, err)\n\t\trequire.JSONEq(t, `\"<secret>\"`, string(jsonOut))\n\t})\n\n\tt.Run(\"marshals actual value when MarshalSecretValue is true\", func(t *testing.T) {\n\t\tcommoncfg.MarshalSecretValue = true\n\t\tdefer func() { commoncfg.MarshalSecretValue = false }()\n\n\t\tu := SecretTemplateURL(\"http://example.com/secret\")\n\n\t\tyamlOut, err := yaml.Marshal(&u)\n\t\trequire.NoError(t, err)\n\t\trequire.YAMLEq(t, \"http://example.com/secret\\n\", string(yamlOut))\n\n\t\tjsonOut, err := json.Marshal(&u)\n\t\trequire.NoError(t, err)\n\t\trequire.JSONEq(t, `\"http://example.com/secret\"`, string(jsonOut))\n\t})\n\n\tt.Run(\"empty URL marshals to empty\", func(t *testing.T) {\n\t\tu := SecretTemplateURL(\"\")\n\n\t\tyamlOut, err := yaml.Marshal(&u)\n\t\trequire.NoError(t, err)\n\t\trequire.YAMLEq(t, \"null\\n\", string(yamlOut))\n\n\t\tjsonOut, err := json.Marshal(&u)\n\t\trequire.NoError(t, err)\n\t\trequire.JSONEq(t, `\"\"`, string(jsonOut))\n\t})\n}\n\nfunc TestGroupByEmptyOverride(t *testing.T) {\n\tin := `\nroute:\n  receiver: 'default'\n  group_by: ['alertname', 'cluster']\n  routes:\n    - group_by: []\n\nreceivers:\n  - name: 'default'\n`\n\tcfg, err := Load(in)\n\trequire.NoError(t, err)\n\trequire.Len(t, cfg.Route.GroupBy, 2)\n\trequire.NotNil(t, cfg.Route.Routes[0].GroupBy)\n\trequire.Empty(t, cfg.Route.Routes[0].GroupBy)\n}\n\nfunc TestWechatNoAPIURL(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.wechat-no-api-secret.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.wechat-no-api-url.yml\", err)\n\t}\n\tif err.Error() != \"no global Wechat Api Secret set either inline or in a file\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"no global Wechat Api Secret set either inline or in a file\", err.Error())\n\t}\n}\n\nfunc TestWechatBothAPIURLAndFile(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.wechat-both-file-and-secret.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.wechat-both-file-and-secret.yml\", err)\n\t}\n\tif err.Error() != \"at most one of wechat_api_secret & wechat_api_secret_file must be configured\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"at most one of wechat_api_secret & wechat_api_secret_file must be configured\", err.Error())\n\t}\n}\n\nfunc TestWechatGlobalAPISecretFile(t *testing.T) {\n\tconf, err := LoadFile(\"testdata/conf.wechat-default-api-secret-file.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.wechat-default-api-secret-file.yml\", err)\n\t}\n\n\t// no override\n\tfirstConfig := conf.Receivers[0].WechatConfigs[0]\n\tif firstConfig.APISecretFile != \"/global_file\" || string(firstConfig.APISecret) != \"\" {\n\t\tt.Fatalf(\"Invalid Wechat API Secret file: %s\\nExpected: %s\", firstConfig.APISecretFile, \"/global_file\")\n\t}\n\n\t// override the file\n\tsecondConfig := conf.Receivers[0].WechatConfigs[1]\n\tif secondConfig.APISecretFile != \"/override_file\" || string(secondConfig.APISecret) != \"\" {\n\t\tt.Fatalf(\"Invalid Wechat API Secret file: %s\\nExpected: %s\", secondConfig.APISecretFile, \"/override_file\")\n\t}\n\n\t// override the global file with an inline URL\n\tthirdConfig := conf.Receivers[0].WechatConfigs[2]\n\tif string(thirdConfig.APISecret) != \"my_inline_secret\" || thirdConfig.APISecretFile != \"\" {\n\t\tt.Fatalf(\"Invalid Wechat API Secret: %s\\nExpected: %s\", string(thirdConfig.APISecret), \"my_inline_secret\")\n\t}\n}\n\nfunc TestMattermostDefaultWebhookURL(t *testing.T) {\n\tconf, err := LoadFile(\"testdata/conf.mattermost-default-webhook-url.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.mattermost-default-webhook-url.yml\", err)\n\t}\n\n\tdefaultWebhookURL := conf.Global.MattermostWebhookURL\n\toverrideWebhookURL := \"https://fakemattermost.example.com/hooks/xxxxxxxxxxxxxxxxxxxxxxxxxx\"\n\tif defaultWebhookURL != conf.Receivers[0].MattermostConfigs[0].WebhookURL {\n\t\tt.Fatalf(\"Invalid mattermost webhook url: %s\\nExpected: %s\", conf.Receivers[0].MattermostConfigs[0].WebhookURL, defaultWebhookURL)\n\t}\n\tif overrideWebhookURL != conf.Receivers[1].MattermostConfigs[0].WebhookURL.String() {\n\t\tt.Errorf(\"Invalid mattermost webhook url: %s\\nExpected: %s\", conf.Receivers[1].MattermostConfigs[0].WebhookURL, overrideWebhookURL)\n\t}\n}\n\nfunc TestMattermostDefaultWebhookURLFile(t *testing.T) {\n\tconf, err := LoadFile(\"testdata/conf.mattermost-default-webhook-url-file.yml\")\n\tif err != nil {\n\t\tt.Fatalf(\"Error parsing %s: %s\", \"testdata/conf.mattermost-default-webhook-url-file.yml\", err)\n\t}\n\n\tdefaultWebhookURLFile := conf.Global.MattermostWebhookURLFile\n\toverrideWebhookURLFile := \"/override_file\"\n\tif defaultWebhookURLFile != conf.Receivers[0].MattermostConfigs[0].WebhookURLFile {\n\t\tt.Fatalf(\"Invalid mattermost webhook url file: %s\\nExpected: %s\", conf.Receivers[0].MattermostConfigs[0].WebhookURLFile, defaultWebhookURLFile)\n\t}\n\tif overrideWebhookURLFile != conf.Receivers[1].MattermostConfigs[0].WebhookURLFile {\n\t\tt.Errorf(\"Invalid mattermost webhook url file: %s\\nExpected: %s\", conf.Receivers[1].MattermostConfigs[0].WebhookURLFile, overrideWebhookURLFile)\n\t}\n}\n\nfunc TestMattermostBothWebhookURLAndFile(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.mattermost-both-webhook-url-and-file.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.mattermost-both-webhook-url-and-file.yml\", err)\n\t}\n\tif err.Error() != \"at most one of mattermost_webhook_url & mattermost_webhook_url_file must be configured\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"at most one of mattermost_webhook_url & mattermost_webhook_url_file must be configured\", err.Error())\n\t}\n}\n\nfunc TestMattermostValidReceiverBothWebhookURLAndFile(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.mattermost-valid-receiver-both-webhook-url-and-file.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.mattermost-valid-receiver-both-webhook-url-and-file.yml\", err)\n\t}\n\tif err.Error() != \"at most one of mattermost_webhook_url & mattermost_webhook_url_file must be configured\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"at most one of mattermost_webhook_url & mattermost_webhook_url_file must be configured\", err.Error())\n\t}\n}\n\nfunc TestMattermostNoWebhookURL(t *testing.T) {\n\t_, err := LoadFile(\"testdata/conf.mattermost-no-webhook-url.yml\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error parsing %s: %s\", \"testdata/conf.mattermost-no-webhook-url.yml\", err)\n\t}\n\tif err.Error() != \"missing webhook_url or webhook_url_file on mattermost_config\" {\n\t\tt.Errorf(\"Expected: %s\\nGot: %s\", \"missing webhook_url or webhook_url_file on mattermost_config\", err.Error())\n\t}\n}\n"
  },
  {
    "path": "config/coordinator.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage config\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/binary\"\n\t\"log/slog\"\n\t\"sync\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n)\n\n// Coordinator coordinates Alertmanager configurations beyond the lifetime of a\n// single configuration.\ntype Coordinator struct {\n\tconfigFilePath string\n\tlogger         *slog.Logger\n\n\t// Protects config and subscribers\n\tmutex       sync.Mutex\n\tconfig      *Config\n\tsubscribers []func(*Config) error\n\n\tconfigHashMetric        prometheus.Gauge\n\tconfigSuccessMetric     prometheus.Gauge\n\tconfigSuccessTimeMetric prometheus.Gauge\n}\n\n// NewCoordinator returns a new coordinator with the given configuration file\n// path. It does not yet load the configuration from file. This is done in\n// `Reload()`.\nfunc NewCoordinator(configFilePath string, r prometheus.Registerer, l *slog.Logger) *Coordinator {\n\tc := &Coordinator{\n\t\tconfigFilePath: configFilePath,\n\t\tlogger:         l,\n\t}\n\n\tc.registerMetrics(r)\n\n\treturn c\n}\n\nfunc (c *Coordinator) registerMetrics(r prometheus.Registerer) {\n\tconfigHash := promauto.With(r).NewGauge(prometheus.GaugeOpts{\n\t\tName: \"alertmanager_config_hash\",\n\t\tHelp: \"Hash of the currently loaded alertmanager configuration. Note that this is not a cryptographically strong hash.\",\n\t})\n\tconfigSuccess := promauto.With(r).NewGauge(prometheus.GaugeOpts{\n\t\tName: \"alertmanager_config_last_reload_successful\",\n\t\tHelp: \"Whether the last configuration reload attempt was successful.\",\n\t})\n\tconfigSuccessTime := promauto.With(r).NewGauge(prometheus.GaugeOpts{\n\t\tName: \"alertmanager_config_last_reload_success_timestamp_seconds\",\n\t\tHelp: \"Timestamp of the last successful configuration reload.\",\n\t})\n\n\tc.configHashMetric = configHash\n\tc.configSuccessMetric = configSuccess\n\tc.configSuccessTimeMetric = configSuccessTime\n}\n\n// Subscribe subscribes the given Subscribers to configuration changes.\nfunc (c *Coordinator) Subscribe(ss ...func(*Config) error) {\n\tc.mutex.Lock()\n\tdefer c.mutex.Unlock()\n\n\tc.subscribers = append(c.subscribers, ss...)\n}\n\nfunc (c *Coordinator) notifySubscribers() error {\n\tfor _, s := range c.subscribers {\n\t\tif err := s(c.config); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// loadFromFile triggers a configuration load, discarding the old configuration.\nfunc (c *Coordinator) loadFromFile() error {\n\tconf, err := LoadFile(c.configFilePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.config = conf\n\n\treturn nil\n}\n\n// Reload triggers a configuration reload from file and notifies all\n// configuration change subscribers.\nfunc (c *Coordinator) Reload() error {\n\tc.mutex.Lock()\n\tdefer c.mutex.Unlock()\n\n\tc.logger.Info(\n\t\t\"Loading configuration file\",\n\t\t\"file\", c.configFilePath,\n\t)\n\tif err := c.loadFromFile(); err != nil {\n\t\tc.logger.Error(\n\t\t\t\"Loading configuration file failed\",\n\t\t\t\"file\", c.configFilePath,\n\t\t\t\"err\", err,\n\t\t)\n\t\tc.configSuccessMetric.Set(0)\n\t\treturn err\n\t}\n\tc.logger.Info(\n\t\t\"Completed loading of configuration file\",\n\t\t\"file\", c.configFilePath,\n\t)\n\n\tif err := c.notifySubscribers(); err != nil {\n\t\tc.logger.Error(\n\t\t\t\"one or more config change subscribers failed to apply new config\",\n\t\t\t\"file\", c.configFilePath,\n\t\t\t\"err\", err,\n\t\t)\n\t\tc.configSuccessMetric.Set(0)\n\t\treturn err\n\t}\n\n\tc.configSuccessMetric.Set(1)\n\tc.configSuccessTimeMetric.SetToCurrentTime()\n\thash := md5HashAsMetricValue([]byte(c.config.original))\n\tc.configHashMetric.Set(hash)\n\n\treturn nil\n}\n\nfunc md5HashAsMetricValue(data []byte) float64 {\n\tsum := md5.Sum(data)\n\t// We only want 48 bits as a float64 only has a 53 bit mantissa.\n\tsmallSum := sum[0:6]\n\tbytes := make([]byte, 8)\n\tcopy(bytes, smallSum)\n\treturn float64(binary.LittleEndian.Uint64(bytes))\n}\n"
  },
  {
    "path": "config/coordinator_test.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage config\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/common/promslog\"\n)\n\ntype fakeRegisterer struct {\n\tregisteredCollectors []prometheus.Collector\n}\n\nfunc (r *fakeRegisterer) Register(prometheus.Collector) error {\n\treturn nil\n}\n\nfunc (r *fakeRegisterer) MustRegister(c ...prometheus.Collector) {\n\tr.registeredCollectors = append(r.registeredCollectors, c...)\n}\n\nfunc (r *fakeRegisterer) Unregister(prometheus.Collector) bool {\n\treturn false\n}\n\nfunc TestCoordinatorRegistersMetrics(t *testing.T) {\n\tfr := fakeRegisterer{}\n\tNewCoordinator(\"testdata/conf.good.yml\", &fr, promslog.NewNopLogger())\n\n\tif len(fr.registeredCollectors) == 0 {\n\t\tt.Error(\"expected NewCoordinator to register metrics on the given registerer\")\n\t}\n}\n\nfunc TestCoordinatorNotifiesSubscribers(t *testing.T) {\n\tcallBackCalled := false\n\tc := NewCoordinator(\"testdata/conf.good.yml\", prometheus.NewRegistry(), promslog.NewNopLogger())\n\tc.Subscribe(func(*Config) error {\n\t\tcallBackCalled = true\n\t\treturn nil\n\t})\n\n\terr := c.Reload()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif !callBackCalled {\n\t\tt.Fatal(\"expected coordinator.Reload() to call subscribers\")\n\t}\n}\n\nfunc TestCoordinatorFailReloadWhenSubscriberFails(t *testing.T) {\n\terrMessage := \"something happened\"\n\tc := NewCoordinator(\"testdata/conf.good.yml\", prometheus.NewRegistry(), promslog.NewNopLogger())\n\n\tc.Subscribe(func(*Config) error {\n\t\treturn errors.New(errMessage)\n\t})\n\n\terr := c.Reload()\n\tif err == nil {\n\t\tt.Fatal(\"expected reload to throw an error\")\n\t}\n\n\tif err.Error() != errMessage {\n\t\tt.Fatalf(\"expected error message %q but got %q\", errMessage, err)\n\t}\n}\n"
  },
  {
    "path": "config/notifiers.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage config\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/textproto\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/sigv4\"\n)\n\nvar (\n\t// DefaultIncidentioConfig defines default values for Incident.io configurations.\n\tDefaultIncidentioConfig = IncidentioConfig{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: true,\n\t\t},\n\t}\n\n\t// DefaultWebhookConfig defines default values for Webhook configurations.\n\tDefaultWebhookConfig = WebhookConfig{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: true,\n\t\t},\n\t}\n\n\t// DefaultWebexConfig defines default values for Webex configurations.\n\tDefaultWebexConfig = WebexConfig{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: true,\n\t\t},\n\t\tMessage: `{{ template \"webex.default.message\" . }}`,\n\t}\n\n\t// DefaultDiscordConfig defines default values for Discord configurations.\n\tDefaultDiscordConfig = DiscordConfig{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: true,\n\t\t},\n\t\tTitle:   `{{ template \"discord.default.title\" . }}`,\n\t\tMessage: `{{ template \"discord.default.message\" . }}`,\n\t}\n\n\t// DefaultEmailConfig defines default values for Email configurations.\n\tDefaultEmailConfig = EmailConfig{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: false,\n\t\t},\n\t\tHTML: `{{ template \"email.default.html\" . }}`,\n\t\tText: ``,\n\t}\n\n\t// DefaultEmailSubject defines the default Subject header of an Email.\n\tDefaultEmailSubject = `{{ template \"email.default.subject\" . }}`\n\n\t// DefaultPagerdutyDetails defines the default values for PagerDuty details.\n\tDefaultPagerdutyDetails = map[string]any{\n\t\t\"firing\":       `{{ .Alerts.Firing | toJson }}`,\n\t\t\"resolved\":     `{{ .Alerts.Resolved | toJson }}`,\n\t\t\"num_firing\":   `{{ .Alerts.Firing | len }}`,\n\t\t\"num_resolved\": `{{ .Alerts.Resolved | len }}`,\n\t}\n\n\t// DefaultPagerdutyConfig defines default values for PagerDuty configurations.\n\tDefaultPagerdutyConfig = PagerdutyConfig{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: true,\n\t\t},\n\t\tDescription: `{{ template \"pagerduty.default.description\" .}}`,\n\t\tClient:      `{{ template \"pagerduty.default.client\" . }}`,\n\t\tClientURL:   `{{ template \"pagerduty.default.clientURL\" . }}`,\n\t}\n\n\t// DefaultSlackConfig defines default values for Slack configurations.\n\tDefaultSlackConfig = SlackConfig{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: false,\n\t\t},\n\t\tColor:      `{{ template \"slack.default.color\" . }}`,\n\t\tUsername:   `{{ template \"slack.default.username\" . }}`,\n\t\tTitle:      `{{ template \"slack.default.title\" . }}`,\n\t\tTitleLink:  `{{ template \"slack.default.titlelink\" . }}`,\n\t\tIconEmoji:  `{{ template \"slack.default.iconemoji\" . }}`,\n\t\tIconURL:    `{{ template \"slack.default.iconurl\" . }}`,\n\t\tPretext:    `{{ template \"slack.default.pretext\" . }}`,\n\t\tText:       `{{ template \"slack.default.text\" . }}`,\n\t\tFallback:   `{{ template \"slack.default.fallback\" . }}`,\n\t\tCallbackID: `{{ template \"slack.default.callbackid\" . }}`,\n\t\tFooter:     `{{ template \"slack.default.footer\" . }}`,\n\t}\n\t// DefaultRocketchatConfig defines default values for Rocketchat configurations.\n\tDefaultRocketchatConfig = RocketchatConfig{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: false,\n\t\t},\n\t\tColor:     `{{ if eq .Status \"firing\" }}red{{ else }}green{{ end }}`,\n\t\tEmoji:     `{{ template \"rocketchat.default.emoji\" . }}`,\n\t\tIconURL:   `{{ template \"rocketchat.default.iconurl\" . }}`,\n\t\tText:      `{{ template \"rocketchat.default.text\" . }}`,\n\t\tTitle:     `{{ template \"rocketchat.default.title\" . }}`,\n\t\tTitleLink: `{{ template \"rocketchat.default.titlelink\" . }}`,\n\t}\n\n\t// DefaultOpsGenieConfig defines default values for OpsGenie configurations.\n\tDefaultOpsGenieConfig = OpsGenieConfig{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: true,\n\t\t},\n\t\tMessage:     `{{ template \"opsgenie.default.message\" . }}`,\n\t\tDescription: `{{ template \"opsgenie.default.description\" . }}`,\n\t\tSource:      `{{ template \"opsgenie.default.source\" . }}`,\n\t\t// TODO: Add a details field with all the alerts.\n\t}\n\n\t// DefaultWechatConfig defines default values for wechat configurations.\n\tDefaultWechatConfig = WechatConfig{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: false,\n\t\t},\n\t\tMessage: `{{ template \"wechat.default.message\" . }}`,\n\t\tToUser:  `{{ template \"wechat.default.to_user\" . }}`,\n\t\tToParty: `{{ template \"wechat.default.to_party\" . }}`,\n\t\tToTag:   `{{ template \"wechat.default.to_tag\" . }}`,\n\t\tAgentID: `{{ template \"wechat.default.agent_id\" . }}`,\n\t}\n\n\t// DefaultVictorOpsConfig defines default values for VictorOps configurations.\n\tDefaultVictorOpsConfig = VictorOpsConfig{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: true,\n\t\t},\n\t\tMessageType:       `CRITICAL`,\n\t\tStateMessage:      `{{ template \"victorops.default.state_message\" . }}`,\n\t\tEntityDisplayName: `{{ template \"victorops.default.entity_display_name\" . }}`,\n\t\tMonitoringTool:    `{{ template \"victorops.default.monitoring_tool\" . }}`,\n\t}\n\n\t// DefaultPushoverConfig defines default values for Pushover configurations.\n\tDefaultPushoverConfig = PushoverConfig{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: true,\n\t\t},\n\t\tTitle:    `{{ template \"pushover.default.title\" . }}`,\n\t\tMessage:  `{{ template \"pushover.default.message\" . }}`,\n\t\tURL:      `{{ template \"pushover.default.url\" . }}`,\n\t\tPriority: `{{ if eq .Status \"firing\" }}2{{ else }}0{{ end }}`, // emergency (firing) or normal\n\t\tRetry:    duration(1 * time.Minute),\n\t\tExpire:   duration(1 * time.Hour),\n\t\tHTML:     false,\n\t}\n\n\t// DefaultSNSConfig defines default values for SNS configurations.\n\tDefaultSNSConfig = SNSConfig{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: true,\n\t\t},\n\t\tSubject: `{{ template \"sns.default.subject\" . }}`,\n\t\tMessage: `{{ template \"sns.default.message\" . }}`,\n\t}\n\n\tDefaultTelegramConfig = TelegramConfig{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: true,\n\t\t},\n\t\tDisableNotifications: false,\n\t\tMessage:              `{{ template \"telegram.default.message\" . }}`,\n\t\tParseMode:            \"HTML\",\n\t}\n\n\tDefaultMSTeamsConfig = MSTeamsConfig{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: true,\n\t\t},\n\t\tTitle:   `{{ template \"msteams.default.title\" . }}`,\n\t\tSummary: `{{ template \"msteams.default.summary\" . }}`,\n\t\tText:    `{{ template \"msteams.default.text\" . }}`,\n\t}\n\n\tDefaultMSTeamsV2Config = MSTeamsV2Config{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: true,\n\t\t},\n\t\tTitle: `{{ template \"msteamsv2.default.title\" . }}`,\n\t\tText:  `{{ template \"msteamsv2.default.text\" . }}`,\n\t}\n\n\tDefaultJiraConfig = JiraConfig{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: true,\n\t\t},\n\t\tAPIType: \"auto\",\n\t\tSummary: JiraFieldConfig{\n\t\t\tTemplate: `{{ template \"jira.default.summary\" . }}`,\n\t\t},\n\t\tDescription: JiraFieldConfig{\n\t\t\tTemplate: `{{ template \"jira.default.description\" . }}`,\n\t\t},\n\t\tPriority: `{{ template \"jira.default.priority\" . }}`,\n\t}\n\n\tDefaultMattermostConfig = MattermostConfig{\n\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\tVSendResolved: true,\n\t\t},\n\t\tUsername:  `{{ template \"mattermost.default.username\" . }}`,\n\t\tColor:     `{{ template \"mattermost.default.color\" . }}`,\n\t\tText:      `{{ template \"mattermost.default.text\" . }}`,\n\t\tTitle:     `{{ template \"mattermost.default.title\" . }}`,\n\t\tTitleLink: `{{ template \"mattermost.default.titlelink\" . }}`,\n\t\tFallback:  `{{ template \"mattermost.default.fallback\" . }}`,\n\t}\n)\n\n// WebexConfig configures notifications via Webex.\ntype WebexConfig struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\tHTTPConfig                 *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\tAPIURL                     *amcommoncfg.URL            `yaml:\"api_url,omitempty\" json:\"api_url,omitempty\"`\n\n\tMessage string `yaml:\"message,omitempty\" json:\"message,omitempty\"`\n\tRoomID  string `yaml:\"room_id\" json:\"room_id\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (c *WebexConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultWebexConfig\n\ttype plain WebexConfig\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\n\tif c.RoomID == \"\" {\n\t\treturn errors.New(\"missing room_id on webex_config\")\n\t}\n\n\tif c.HTTPConfig == nil || c.HTTPConfig.Authorization == nil {\n\t\treturn errors.New(\"missing webex_configs.http_config.authorization\")\n\t}\n\n\treturn nil\n}\n\n// DiscordConfig configures notifications via Discord.\ntype DiscordConfig struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\n\tHTTPConfig     *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\tWebhookURL     *amcommoncfg.SecretURL      `yaml:\"webhook_url,omitempty\" json:\"webhook_url,omitempty\"`\n\tWebhookURLFile string                      `yaml:\"webhook_url_file,omitempty\" json:\"webhook_url_file,omitempty\"`\n\n\tContent   string `yaml:\"content,omitempty\" json:\"content,omitempty\"`\n\tTitle     string `yaml:\"title,omitempty\" json:\"title,omitempty\"`\n\tMessage   string `yaml:\"message,omitempty\" json:\"message,omitempty\"`\n\tUsername  string `yaml:\"username,omitempty\" json:\"username,omitempty\"`\n\tAvatarURL string `yaml:\"avatar_url,omitempty\" json:\"avatar_url,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (c *DiscordConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultDiscordConfig\n\ttype plain DiscordConfig\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\n\tif c.WebhookURL == nil && c.WebhookURLFile == \"\" {\n\t\treturn errors.New(\"one of webhook_url or webhook_url_file must be configured\")\n\t}\n\n\tif c.WebhookURL != nil && len(c.WebhookURLFile) > 0 {\n\t\treturn errors.New(\"at most one of webhook_url & webhook_url_file must be configured\")\n\t}\n\n\treturn nil\n}\n\n// EmailConfig configures notifications via mail.\ntype EmailConfig struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\n\t// Email address to notify.\n\tTo               string               `yaml:\"to,omitempty\" json:\"to,omitempty\"`\n\tFrom             string               `yaml:\"from,omitempty\" json:\"from,omitempty\"`\n\tHello            string               `yaml:\"hello,omitempty\" json:\"hello,omitempty\"`\n\tSmarthost        HostPort             `yaml:\"smarthost,omitempty\" json:\"smarthost,omitempty\"`\n\tAuthUsername     string               `yaml:\"auth_username,omitempty\" json:\"auth_username,omitempty\"`\n\tAuthPassword     commoncfg.Secret     `yaml:\"auth_password,omitempty\" json:\"auth_password,omitempty\"`\n\tAuthPasswordFile string               `yaml:\"auth_password_file,omitempty\" json:\"auth_password_file,omitempty\"`\n\tAuthSecret       commoncfg.Secret     `yaml:\"auth_secret,omitempty\" json:\"auth_secret,omitempty\"`\n\tAuthSecretFile   string               `yaml:\"auth_secret_file,omitempty\" json:\"auth_secret_file,omitempty\"`\n\tAuthIdentity     string               `yaml:\"auth_identity,omitempty\" json:\"auth_identity,omitempty\"`\n\tHeaders          map[string]string    `yaml:\"headers,omitempty\" json:\"headers,omitempty\"`\n\tHTML             string               `yaml:\"html,omitempty\" json:\"html,omitempty\"`\n\tText             string               `yaml:\"text,omitempty\" json:\"text,omitempty\"`\n\tRequireTLS       *bool                `yaml:\"require_tls,omitempty\" json:\"require_tls,omitempty\"`\n\tTLSConfig        *commoncfg.TLSConfig `yaml:\"tls_config,omitempty\" json:\"tls_config,omitempty\"`\n\t// ForceImplicitTLS controls whether to use implicit TLS (direct TLS connection).\n\t// true: force use of implicit TLS (direct TLS connection)\n\t// false: force disable implicit TLS (use explicit TLS/STARTTLS if required)\n\t// nil (default): auto-detect based on port (465=implicit, other=explicit) for backward compatibility\n\tForceImplicitTLS *bool           `yaml:\"force_implicit_tls,omitempty\" json:\"force_implicit_tls,omitempty\"`\n\tThreading        ThreadingConfig `yaml:\"threading,omitempty\" json:\"threading,omitempty\"`\n}\n\n// ThreadingConfig configures mail threading.\ntype ThreadingConfig struct {\n\tEnabled      bool   `yaml:\"enabled,omitempty\" json:\"enabled,omitempty\"`\n\tThreadByDate string `yaml:\"thread_by_date,omitempty\" json:\"thread_by_date,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (c *EmailConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultEmailConfig\n\ttype plain EmailConfig\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\tif c.To == \"\" {\n\t\treturn errors.New(\"missing to address in email config\")\n\t}\n\t// Header names are case-insensitive, check for collisions.\n\tnormalizedHeaders := map[string]string{}\n\tfor h, v := range c.Headers {\n\t\tnormalized := textproto.CanonicalMIMEHeaderKey(h)\n\t\tif _, ok := normalizedHeaders[normalized]; ok {\n\t\t\treturn fmt.Errorf(\"duplicate header %q in email config\", normalized)\n\t\t}\n\t\tnormalizedHeaders[normalized] = v\n\t}\n\tc.Headers = normalizedHeaders\n\n\tif c.Threading.Enabled {\n\t\tif _, ok := normalizedHeaders[\"References\"]; ok {\n\t\t\treturn errors.New(\"conflicting configuration: threading.enabled conflicts with custom References header\")\n\t\t}\n\t\tif _, ok := normalizedHeaders[\"In-Reply-To\"]; ok {\n\t\t\treturn errors.New(\"conflicting configuration: threading.enabled conflicts with custom In-Reply-To header\")\n\t\t}\n\t\tif !slices.Contains([]string{\"none\", \"daily\"}, c.Threading.ThreadByDate) {\n\t\t\treturn errors.New(\"threading.thread_by_date must be either 'none' or 'daily'\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// PagerdutyConfig configures notifications via PagerDuty.\ntype PagerdutyConfig struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\n\tHTTPConfig *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\n\tServiceKey     commoncfg.Secret `yaml:\"service_key,omitempty\" json:\"service_key,omitempty\"`\n\tServiceKeyFile string           `yaml:\"service_key_file,omitempty\" json:\"service_key_file,omitempty\"`\n\tRoutingKey     commoncfg.Secret `yaml:\"routing_key,omitempty\" json:\"routing_key,omitempty\"`\n\tRoutingKeyFile string           `yaml:\"routing_key_file,omitempty\" json:\"routing_key_file,omitempty\"`\n\tURL            *amcommoncfg.URL `yaml:\"url,omitempty\" json:\"url,omitempty\"`\n\tClient         string           `yaml:\"client,omitempty\" json:\"client,omitempty\"`\n\tClientURL      string           `yaml:\"client_url,omitempty\" json:\"client_url,omitempty\"`\n\tDescription    string           `yaml:\"description,omitempty\" json:\"description,omitempty\"`\n\tDetails        map[string]any   `yaml:\"details,omitempty\" json:\"details,omitempty\"`\n\tImages         []PagerdutyImage `yaml:\"images,omitempty\" json:\"images,omitempty\"`\n\tLinks          []PagerdutyLink  `yaml:\"links,omitempty\" json:\"links,omitempty\"`\n\tSource         string           `yaml:\"source,omitempty\" json:\"source,omitempty\"`\n\tSeverity       string           `yaml:\"severity,omitempty\" json:\"severity,omitempty\"`\n\tClass          string           `yaml:\"class,omitempty\" json:\"class,omitempty\"`\n\tComponent      string           `yaml:\"component,omitempty\" json:\"component,omitempty\"`\n\tGroup          string           `yaml:\"group,omitempty\" json:\"group,omitempty\"`\n\t// Timeout is the maximum time allowed to invoke the pagerduty. Setting this to 0\n\t// does not impose a timeout.\n\tTimeout time.Duration `yaml:\"timeout\" json:\"timeout\"`\n}\n\n// PagerdutyLink is a link.\ntype PagerdutyLink struct {\n\tHref string `yaml:\"href,omitempty\" json:\"href,omitempty\"`\n\tText string `yaml:\"text,omitempty\" json:\"text,omitempty\"`\n}\n\n// PagerdutyImage is an image.\ntype PagerdutyImage struct {\n\tSrc  string `yaml:\"src,omitempty\" json:\"src,omitempty\"`\n\tAlt  string `yaml:\"alt,omitempty\" json:\"alt,omitempty\"`\n\tHref string `yaml:\"href,omitempty\" json:\"href,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (c *PagerdutyConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultPagerdutyConfig\n\ttype plain PagerdutyConfig\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\tif c.RoutingKey == \"\" && c.ServiceKey == \"\" && c.RoutingKeyFile == \"\" && c.ServiceKeyFile == \"\" {\n\t\treturn errors.New(\"missing service or routing key in PagerDuty config\")\n\t}\n\tif len(c.RoutingKey) > 0 && len(c.RoutingKeyFile) > 0 {\n\t\treturn errors.New(\"at most one of routing_key & routing_key_file must be configured\")\n\t}\n\tif len(c.ServiceKey) > 0 && len(c.ServiceKeyFile) > 0 {\n\t\treturn errors.New(\"at most one of service_key & service_key_file must be configured\")\n\t}\n\tif c.Details == nil {\n\t\tc.Details = make(map[string]any)\n\t}\n\tif c.Source == \"\" {\n\t\tc.Source = c.Client\n\t}\n\tfor k, v := range DefaultPagerdutyDetails {\n\t\tif _, ok := c.Details[k]; !ok {\n\t\t\tc.Details[k] = v\n\t\t}\n\t}\n\treturn nil\n}\n\n// SlackAction configures a single Slack action that is sent with each notification.\n// See https://api.slack.com/docs/message-attachments#action_fields and https://api.slack.com/docs/message-buttons\n// for more information.\ntype SlackAction struct {\n\tType         string                  `yaml:\"type,omitempty\"  json:\"type,omitempty\"`\n\tText         string                  `yaml:\"text,omitempty\"  json:\"text,omitempty\"`\n\tURL          string                  `yaml:\"url,omitempty\"   json:\"url,omitempty\"`\n\tStyle        string                  `yaml:\"style,omitempty\" json:\"style,omitempty\"`\n\tName         string                  `yaml:\"name,omitempty\"  json:\"name,omitempty\"`\n\tValue        string                  `yaml:\"value,omitempty\"  json:\"value,omitempty\"`\n\tConfirmField *SlackConfirmationField `yaml:\"confirm,omitempty\"  json:\"confirm,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for SlackAction.\nfunc (c *SlackAction) UnmarshalYAML(unmarshal func(any) error) error {\n\ttype plain SlackAction\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\tif c.Type == \"\" {\n\t\treturn errors.New(\"missing type in Slack action configuration\")\n\t}\n\tif c.Text == \"\" {\n\t\treturn errors.New(\"missing text in Slack action configuration\")\n\t}\n\tif c.URL != \"\" {\n\t\t// Clear all message action fields.\n\t\tc.Name = \"\"\n\t\tc.Value = \"\"\n\t\tc.ConfirmField = nil\n\t} else if c.Name != \"\" {\n\t\tc.URL = \"\"\n\t} else {\n\t\treturn errors.New(\"missing name or url in Slack action configuration\")\n\t}\n\treturn nil\n}\n\n// SlackConfirmationField protect users from destructive actions or particularly distinguished decisions\n// by asking them to confirm their button click one more time.\n// See https://api.slack.com/docs/interactive-message-field-guide#confirmation_fields for more information.\ntype SlackConfirmationField struct {\n\tText        string `yaml:\"text,omitempty\"  json:\"text,omitempty\"`\n\tTitle       string `yaml:\"title,omitempty\"  json:\"title,omitempty\"`\n\tOkText      string `yaml:\"ok_text,omitempty\"  json:\"ok_text,omitempty\"`\n\tDismissText string `yaml:\"dismiss_text,omitempty\"  json:\"dismiss_text,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for SlackConfirmationField.\nfunc (c *SlackConfirmationField) UnmarshalYAML(unmarshal func(any) error) error {\n\ttype plain SlackConfirmationField\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\tif c.Text == \"\" {\n\t\treturn errors.New(\"missing text in Slack confirmation configuration\")\n\t}\n\treturn nil\n}\n\n// SlackField configures a single Slack field that is sent with each notification.\n// Each field must contain a title, value, and optionally, a boolean value to indicate if the field\n// is short enough to be displayed next to other fields designated as short.\n// See https://api.slack.com/docs/message-attachments#fields for more information.\ntype SlackField struct {\n\tTitle string `yaml:\"title,omitempty\" json:\"title,omitempty\"`\n\tValue string `yaml:\"value,omitempty\" json:\"value,omitempty\"`\n\tShort *bool  `yaml:\"short,omitempty\" json:\"short,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for SlackField.\nfunc (c *SlackField) UnmarshalYAML(unmarshal func(any) error) error {\n\ttype plain SlackField\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\tif c.Title == \"\" {\n\t\treturn errors.New(\"missing title in Slack field configuration\")\n\t}\n\tif c.Value == \"\" {\n\t\treturn errors.New(\"missing value in Slack field configuration\")\n\t}\n\treturn nil\n}\n\n// SlackConfig configures notifications via Slack.\ntype SlackConfig struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\n\tHTTPConfig *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\n\tAPIURL       *amcommoncfg.SecretURL `yaml:\"api_url,omitempty\" json:\"api_url,omitempty\"`\n\tAPIURLFile   string                 `yaml:\"api_url_file,omitempty\" json:\"api_url_file,omitempty\"`\n\tAppToken     commoncfg.Secret       `yaml:\"app_token,omitempty\" json:\"app_token,omitempty\"`\n\tAppTokenFile string                 `yaml:\"app_token_file,omitempty\" json:\"app_token_file,omitempty\"`\n\tAppURL       *amcommoncfg.URL       `yaml:\"app_url,omitempty\" json:\"app_url,omitempty\"`\n\n\t// Slack channel override, (like #other-channel or @username).\n\tChannel  string `yaml:\"channel,omitempty\" json:\"channel,omitempty\"`\n\tUsername string `yaml:\"username,omitempty\" json:\"username,omitempty\"`\n\tColor    string `yaml:\"color,omitempty\" json:\"color,omitempty\"`\n\n\tTitle       string         `yaml:\"title,omitempty\" json:\"title,omitempty\"`\n\tTitleLink   string         `yaml:\"title_link,omitempty\" json:\"title_link,omitempty\"`\n\tPretext     string         `yaml:\"pretext,omitempty\" json:\"pretext,omitempty\"`\n\tText        string         `yaml:\"text,omitempty\" json:\"text,omitempty\"`\n\tMessageText string         `yaml:\"message_text,omitempty\" json:\"message_text,omitempty\"`\n\tFields      []*SlackField  `yaml:\"fields,omitempty\" json:\"fields,omitempty\"`\n\tShortFields bool           `yaml:\"short_fields\" json:\"short_fields,omitempty\"`\n\tFooter      string         `yaml:\"footer,omitempty\" json:\"footer,omitempty\"`\n\tFallback    string         `yaml:\"fallback,omitempty\" json:\"fallback,omitempty\"`\n\tCallbackID  string         `yaml:\"callback_id,omitempty\" json:\"callback_id,omitempty\"`\n\tIconEmoji   string         `yaml:\"icon_emoji,omitempty\" json:\"icon_emoji,omitempty\"`\n\tIconURL     string         `yaml:\"icon_url,omitempty\" json:\"icon_url,omitempty\"`\n\tImageURL    string         `yaml:\"image_url,omitempty\" json:\"image_url,omitempty\"`\n\tThumbURL    string         `yaml:\"thumb_url,omitempty\" json:\"thumb_url,omitempty\"`\n\tLinkNames   bool           `yaml:\"link_names\" json:\"link_names,omitempty\"`\n\tMrkdwnIn    []string       `yaml:\"mrkdwn_in,omitempty\" json:\"mrkdwn_in,omitempty\"`\n\tActions     []*SlackAction `yaml:\"actions,omitempty\" json:\"actions,omitempty\"`\n\n\t// UpdateMessage enables updating existing Slack messages instead of creating new ones.\n\t// Requires bot token with chat:write scope. Webhook URLs do not support updates.\n\n\tUpdateMessage bool `yaml:\"update_message\" json:\"update_message,omitempty\"`\n\t// Timeout is the maximum time allowed to invoke the slack. Setting this to 0\n\t// does not impose a timeout.\n\tTimeout time.Duration `yaml:\"timeout\" json:\"timeout\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (c *SlackConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultSlackConfig\n\ttype plain SlackConfig\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\n\tif c.APIURL != nil && len(c.APIURLFile) > 0 {\n\t\treturn errors.New(\"at most one of api_url & api_url_file must be configured\")\n\t}\n\tif c.AppToken != \"\" && len(c.AppTokenFile) > 0 {\n\t\treturn errors.New(\"at most one of app_token & app_token_file must be configured\")\n\t}\n\tif (c.APIURL != nil || len(c.APIURLFile) > 0) && (c.AppToken != \"\" || len(c.AppTokenFile) > 0) {\n\t\treturn errors.New(\"at most one of api_url/api_url_file & app_token/app_token_file must be configured\")\n\t}\n\n\tif c.UpdateMessage && c.APIURL.String() != \"https://slack.com/api/chat.postMessage\" {\n\t\treturn errors.New(\"update_message can only be used with bot tokens. api_url must be set to https://slack.com/api/chat.postMessage\")\n\t}\n\n\treturn nil\n}\n\n// IncidentioConfig configures notifications via incident.io.\ntype IncidentioConfig struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\n\tHTTPConfig *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\n\t// URL to send POST request to.\n\tURL     *amcommoncfg.URL `yaml:\"url\" json:\"url\"`\n\tURLFile string           `yaml:\"url_file\" json:\"url_file\"`\n\n\t// AlertSourceToken is the key used to authenticate with the alert source in incident.io.\n\tAlertSourceToken     commoncfg.Secret `yaml:\"alert_source_token,omitempty\" json:\"alert_source_token,omitempty\"`\n\tAlertSourceTokenFile string           `yaml:\"alert_source_token_file,omitempty\" json:\"alert_source_token_file,omitempty\"`\n\n\t// MaxAlerts is the maximum number of alerts to be sent per incident.io message.\n\t// Alerts exceeding this threshold will be truncated. Setting this to 0\n\t// allows an unlimited number of alerts. Note that if the payload exceeds\n\t// incident.io's size limits, you will receive a 429 response and alerts\n\t// will not be ingested.\n\tMaxAlerts uint64 `yaml:\"max_alerts\" json:\"max_alerts\"`\n\n\t// Timeout is the maximum time allowed to invoke incident.io. Setting this to 0\n\t// does not impose a timeout.\n\tTimeout time.Duration `yaml:\"timeout\" json:\"timeout\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (c *IncidentioConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultIncidentioConfig\n\ttype plain IncidentioConfig\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\tif c.URL == nil && c.URLFile == \"\" {\n\t\treturn errors.New(\"one of url or url_file must be configured\")\n\t}\n\tif c.URL != nil && c.URLFile != \"\" {\n\t\treturn errors.New(\"at most one of url & url_file must be configured\")\n\t}\n\tif c.AlertSourceToken != \"\" && c.AlertSourceTokenFile != \"\" {\n\t\treturn errors.New(\"at most one of alert_source_token & alert_source_token_file must be configured\")\n\t}\n\tif c.HTTPConfig != nil && c.HTTPConfig.Authorization != nil && (c.AlertSourceToken != \"\" || c.AlertSourceTokenFile != \"\") {\n\t\treturn errors.New(\"cannot specify alert_source_token or alert_source_token_file when using http_config.authorization\")\n\t}\n\n\tif (c.HTTPConfig != nil && c.HTTPConfig.Authorization == nil) && c.AlertSourceToken == \"\" && c.AlertSourceTokenFile == \"\" {\n\t\treturn errors.New(\"at least one of alert_source_token, alert_source_token_file or http_config.authorization must be configured\")\n\t}\n\treturn nil\n}\n\n// WebhookConfig configures notifications via a generic webhook.\ntype WebhookConfig struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\n\tHTTPConfig *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\n\t// URL to send POST request to.\n\tURL     SecretTemplateURL `yaml:\"url,omitempty\" json:\"url,omitempty\"`\n\tURLFile string            `yaml:\"url_file\" json:\"url_file\"`\n\n\t// MaxAlerts is the maximum number of alerts to be sent per webhook message.\n\t// Alerts exceeding this threshold will be truncated. Setting this to 0\n\t// allows an unlimited number of alerts.\n\tMaxAlerts uint64 `yaml:\"max_alerts\" json:\"max_alerts\"`\n\n\t// Timeout is the maximum time allowed to invoke the webhook. Setting this to 0\n\t// does not impose a timeout.\n\tTimeout time.Duration  `yaml:\"timeout\" json:\"timeout\"`\n\tPayload map[string]any `yaml:\"payload,omitempty\" json:\"payload,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (c *WebhookConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultWebhookConfig\n\ttype plain WebhookConfig\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\tif c.URL == \"\" && c.URLFile == \"\" {\n\t\treturn errors.New(\"one of url or url_file must be configured\")\n\t}\n\tif c.URL != \"\" && c.URLFile != \"\" {\n\t\treturn errors.New(\"at most one of url & url_file must be configured\")\n\t}\n\treturn nil\n}\n\n// WechatConfig configures notifications via Wechat.\ntype WechatConfig struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\n\tHTTPConfig *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\n\tAPISecret     commoncfg.Secret `yaml:\"api_secret,omitempty\" json:\"api_secret,omitempty\"`\n\tAPISecretFile string           `yaml:\"api_secret_file,omitempty\" json:\"api_secret_file,omitempty\"`\n\tCorpID        string           `yaml:\"corp_id,omitempty\" json:\"corp_id,omitempty\"`\n\tMessage       string           `yaml:\"message,omitempty\" json:\"message,omitempty\"`\n\tAPIURL        *amcommoncfg.URL `yaml:\"api_url,omitempty\" json:\"api_url,omitempty\"`\n\tToUser        string           `yaml:\"to_user,omitempty\" json:\"to_user,omitempty\"`\n\tToParty       string           `yaml:\"to_party,omitempty\" json:\"to_party,omitempty\"`\n\tToTag         string           `yaml:\"to_tag,omitempty\" json:\"to_tag,omitempty\"`\n\tAgentID       string           `yaml:\"agent_id,omitempty\" json:\"agent_id,omitempty\"`\n\tMessageType   string           `yaml:\"message_type,omitempty\" json:\"message_type,omitempty\"`\n}\n\nconst wechatValidTypesRe = `^(text|markdown)$`\n\nvar wechatTypeMatcher = regexp.MustCompile(wechatValidTypesRe)\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (c *WechatConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultWechatConfig\n\ttype plain WechatConfig\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\n\tif c.MessageType == \"\" {\n\t\tc.MessageType = \"text\"\n\t}\n\n\tif !wechatTypeMatcher.MatchString(c.MessageType) {\n\t\treturn fmt.Errorf(\"weChat message type %q does not match valid options %s\", c.MessageType, wechatValidTypesRe)\n\t}\n\n\tif c.APISecret != \"\" && len(c.APISecretFile) > 0 {\n\t\treturn errors.New(\"at most one of api_secret & api_secret_file must be configured\")\n\t}\n\n\treturn nil\n}\n\n// OpsGenieConfig configures notifications via OpsGenie.\ntype OpsGenieConfig struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\n\tHTTPConfig *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\n\tAPIKey       commoncfg.Secret          `yaml:\"api_key,omitempty\" json:\"api_key,omitempty\"`\n\tAPIKeyFile   string                    `yaml:\"api_key_file,omitempty\" json:\"api_key_file,omitempty\"`\n\tAPIURL       *amcommoncfg.URL          `yaml:\"api_url,omitempty\" json:\"api_url,omitempty\"`\n\tMessage      string                    `yaml:\"message,omitempty\" json:\"message,omitempty\"`\n\tDescription  string                    `yaml:\"description,omitempty\" json:\"description,omitempty\"`\n\tSource       string                    `yaml:\"source,omitempty\" json:\"source,omitempty\"`\n\tDetails      map[string]string         `yaml:\"details,omitempty\" json:\"details,omitempty\"`\n\tEntity       string                    `yaml:\"entity,omitempty\" json:\"entity,omitempty\"`\n\tResponders   []OpsGenieConfigResponder `yaml:\"responders,omitempty\" json:\"responders,omitempty\"`\n\tActions      string                    `yaml:\"actions,omitempty\" json:\"actions,omitempty\"`\n\tTags         string                    `yaml:\"tags,omitempty\" json:\"tags,omitempty\"`\n\tNote         string                    `yaml:\"note,omitempty\" json:\"note,omitempty\"`\n\tPriority     string                    `yaml:\"priority,omitempty\" json:\"priority,omitempty\"`\n\tUpdateAlerts bool                      `yaml:\"update_alerts,omitempty\" json:\"update_alerts,omitempty\"`\n}\n\nconst opsgenieValidTypesRe = `^(team|teams|user|escalation|schedule)$`\n\nvar opsgenieTypeMatcher = regexp.MustCompile(opsgenieValidTypesRe)\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (c *OpsGenieConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultOpsGenieConfig\n\ttype plain OpsGenieConfig\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\n\tif c.APIKey != \"\" && len(c.APIKeyFile) > 0 {\n\t\treturn errors.New(\"at most one of api_key & api_key_file must be configured\")\n\t}\n\n\tfor _, r := range c.Responders {\n\t\tif r.ID == \"\" && r.Username == \"\" && r.Name == \"\" {\n\t\t\treturn fmt.Errorf(\"opsGenieConfig responder %v has to have at least one of id, username or name specified\", r)\n\t\t}\n\n\t\tisTemplated, err := containsTemplating(r.Type)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"opsGenieConfig responder %v type contains invalid template syntax: %w\", r, err)\n\t\t}\n\t\tif !isTemplated {\n\t\t\tr.Type = strings.ToLower(r.Type)\n\t\t\tif !opsgenieTypeMatcher.MatchString(r.Type) {\n\t\t\t\treturn fmt.Errorf(\"opsGenieConfig responder %v type does not match valid options %s\", r, opsgenieValidTypesRe)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\ntype OpsGenieConfigResponder struct {\n\t// One of those 3 should be filled.\n\tID       string `yaml:\"id,omitempty\" json:\"id,omitempty\"`\n\tName     string `yaml:\"name,omitempty\" json:\"name,omitempty\"`\n\tUsername string `yaml:\"username,omitempty\" json:\"username,omitempty\"`\n\n\t// team, user, escalation, schedule etc.\n\tType string `yaml:\"type,omitempty\" json:\"type,omitempty\"`\n}\n\n// VictorOpsConfig configures notifications via VictorOps.\ntype VictorOpsConfig struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\n\tHTTPConfig *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\n\tAPIKey            commoncfg.Secret  `yaml:\"api_key,omitempty\" json:\"api_key,omitempty\"`\n\tAPIKeyFile        string            `yaml:\"api_key_file,omitempty\" json:\"api_key_file,omitempty\"`\n\tAPIURL            *amcommoncfg.URL  `yaml:\"api_url\" json:\"api_url\"`\n\tRoutingKey        string            `yaml:\"routing_key\" json:\"routing_key\"`\n\tMessageType       string            `yaml:\"message_type\" json:\"message_type\"`\n\tStateMessage      string            `yaml:\"state_message\" json:\"state_message\"`\n\tEntityDisplayName string            `yaml:\"entity_display_name\" json:\"entity_display_name\"`\n\tMonitoringTool    string            `yaml:\"monitoring_tool\" json:\"monitoring_tool\"`\n\tCustomFields      map[string]string `yaml:\"custom_fields,omitempty\" json:\"custom_fields,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (c *VictorOpsConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultVictorOpsConfig\n\ttype plain VictorOpsConfig\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\tif c.RoutingKey == \"\" {\n\t\treturn errors.New(\"missing Routing key in VictorOps config\")\n\t}\n\tif c.APIKey != \"\" && len(c.APIKeyFile) > 0 {\n\t\treturn errors.New(\"at most one of api_key & api_key_file must be configured\")\n\t}\n\n\treservedFields := []string{\"routing_key\", \"message_type\", \"state_message\", \"entity_display_name\", \"monitoring_tool\", \"entity_id\", \"entity_state\"}\n\n\tfor _, v := range reservedFields {\n\t\tif _, ok := c.CustomFields[v]; ok {\n\t\t\treturn fmt.Errorf(\"victorOps config contains custom field %s which cannot be used as it conflicts with the fixed/static fields\", v)\n\t\t}\n\t}\n\n\treturn nil\n}\n\ntype duration time.Duration\n\nfunc (d *duration) UnmarshalText(text []byte) error {\n\tparsed, err := time.ParseDuration(string(text))\n\tif err == nil {\n\t\t*d = duration(parsed)\n\t}\n\treturn err\n}\n\nfunc (d duration) MarshalText() ([]byte, error) {\n\treturn []byte(time.Duration(d).String()), nil\n}\n\ntype PushoverConfig struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\n\tHTTPConfig *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\n\tUserKey     commoncfg.Secret `yaml:\"user_key,omitempty\" json:\"user_key,omitempty\"`\n\tUserKeyFile string           `yaml:\"user_key_file,omitempty\" json:\"user_key_file,omitempty\"`\n\tToken       commoncfg.Secret `yaml:\"token,omitempty\" json:\"token,omitempty\"`\n\tTokenFile   string           `yaml:\"token_file,omitempty\" json:\"token_file,omitempty\"`\n\tTitle       string           `yaml:\"title,omitempty\" json:\"title,omitempty\"`\n\tMessage     string           `yaml:\"message,omitempty\" json:\"message,omitempty\"`\n\tURL         string           `yaml:\"url,omitempty\" json:\"url,omitempty\"`\n\tURLTitle    string           `yaml:\"url_title,omitempty\" json:\"url_title,omitempty\"`\n\tDevice      string           `yaml:\"device,omitempty\" json:\"device,omitempty\"`\n\tSound       string           `yaml:\"sound,omitempty\" json:\"sound,omitempty\"`\n\tPriority    string           `yaml:\"priority,omitempty\" json:\"priority,omitempty\"`\n\tRetry       duration         `yaml:\"retry,omitempty\" json:\"retry,omitempty\"`\n\tExpire      duration         `yaml:\"expire,omitempty\" json:\"expire,omitempty\"`\n\tTTL         duration         `yaml:\"ttl,omitempty\" json:\"ttl,omitempty\"`\n\tHTML        bool             `yaml:\"html,omitempty\" json:\"html,omitempty\"`\n\tMonospace   bool             `yaml:\"monospace,omitempty\" json:\"monospace,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (c *PushoverConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultPushoverConfig\n\ttype plain PushoverConfig\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\tif c.UserKey == \"\" && c.UserKeyFile == \"\" {\n\t\treturn errors.New(\"one of user_key or user_key_file must be configured\")\n\t}\n\tif c.UserKey != \"\" && c.UserKeyFile != \"\" {\n\t\treturn errors.New(\"at most one of user_key & user_key_file must be configured\")\n\t}\n\tif c.Token == \"\" && c.TokenFile == \"\" {\n\t\treturn errors.New(\"one of token or token_file must be configured\")\n\t}\n\tif c.Token != \"\" && c.TokenFile != \"\" {\n\t\treturn errors.New(\"at most one of token & token_file must be configured\")\n\t}\n\tif c.HTML && c.Monospace {\n\t\treturn errors.New(\"at most one of monospace & html must be configured\")\n\t}\n\treturn nil\n}\n\ntype SNSConfig struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\n\tHTTPConfig *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\n\tAPIUrl      string            `yaml:\"api_url,omitempty\" json:\"api_url,omitempty\"`\n\tSigv4       sigv4.SigV4Config `yaml:\"sigv4\" json:\"sigv4\"`\n\tTopicARN    string            `yaml:\"topic_arn,omitempty\" json:\"topic_arn,omitempty\"`\n\tPhoneNumber string            `yaml:\"phone_number,omitempty\" json:\"phone_number,omitempty\"`\n\tTargetARN   string            `yaml:\"target_arn,omitempty\" json:\"target_arn,omitempty\"`\n\tSubject     string            `yaml:\"subject,omitempty\" json:\"subject,omitempty\"`\n\tMessage     string            `yaml:\"message,omitempty\" json:\"message,omitempty\"`\n\tAttributes  map[string]string `yaml:\"attributes,omitempty\" json:\"attributes,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (c *SNSConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultSNSConfig\n\ttype plain SNSConfig\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\tif (c.TargetARN == \"\") != (c.TopicARN == \"\") != (c.PhoneNumber == \"\") {\n\t\treturn errors.New(\"must provide either a Target ARN, Topic ARN, or Phone Number for SNS config\")\n\t}\n\treturn nil\n}\n\n// TelegramConfig configures notifications via Telegram.\ntype TelegramConfig struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\n\tHTTPConfig *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\n\tAPIUrl               *amcommoncfg.URL `yaml:\"api_url\" json:\"api_url,omitempty\"`\n\tBotToken             commoncfg.Secret `yaml:\"bot_token,omitempty\" json:\"token,omitempty\"`\n\tBotTokenFile         string           `yaml:\"bot_token_file,omitempty\" json:\"token_file,omitempty\"`\n\tChatID               int64            `yaml:\"chat_id,omitempty\" json:\"chat,omitempty\"`\n\tChatIDFile           string           `yaml:\"chat_id_file,omitempty\" json:\"chat_file,omitempty\"`\n\tMessageThreadID      int              `yaml:\"message_thread_id,omitempty\" json:\"message_thread_id,omitempty\"`\n\tMessage              string           `yaml:\"message,omitempty\" json:\"message,omitempty\"`\n\tDisableNotifications bool             `yaml:\"disable_notifications,omitempty\" json:\"disable_notifications,omitempty\"`\n\tParseMode            string           `yaml:\"parse_mode,omitempty\" json:\"parse_mode,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (c *TelegramConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultTelegramConfig\n\ttype plain TelegramConfig\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\tif c.BotToken != \"\" && c.BotTokenFile != \"\" {\n\t\treturn errors.New(\"at most one of bot_token & bot_token_file must be configured\")\n\t}\n\tif c.ChatID == 0 && c.ChatIDFile == \"\" {\n\t\treturn errors.New(\"missing chat_id or chat_id_file on telegram_config\")\n\t}\n\tif c.ChatID != 0 && c.ChatIDFile != \"\" {\n\t\treturn errors.New(\"at most one of chat_id & chat_id_file must be configured\")\n\t}\n\tif c.ParseMode != \"\" &&\n\t\tc.ParseMode != \"Markdown\" &&\n\t\tc.ParseMode != \"MarkdownV2\" &&\n\t\tc.ParseMode != \"HTML\" {\n\t\treturn errors.New(\"unknown parse_mode on telegram_config, must be Markdown, MarkdownV2, HTML or empty string\")\n\t}\n\treturn nil\n}\n\ntype MSTeamsConfig struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\tHTTPConfig                 *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\tWebhookURL                 *amcommoncfg.SecretURL      `yaml:\"webhook_url,omitempty\" json:\"webhook_url,omitempty\"`\n\tWebhookURLFile             string                      `yaml:\"webhook_url_file,omitempty\" json:\"webhook_url_file,omitempty\"`\n\n\tTitle   string `yaml:\"title,omitempty\" json:\"title,omitempty\"`\n\tSummary string `yaml:\"summary,omitempty\" json:\"summary,omitempty\"`\n\tText    string `yaml:\"text,omitempty\" json:\"text,omitempty\"`\n}\n\nfunc (c *MSTeamsConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultMSTeamsConfig\n\ttype plain MSTeamsConfig\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\n\tif c.WebhookURL == nil && c.WebhookURLFile == \"\" {\n\t\treturn errors.New(\"one of webhook_url or webhook_url_file must be configured\")\n\t}\n\n\tif c.WebhookURL != nil && len(c.WebhookURLFile) > 0 {\n\t\treturn errors.New(\"at most one of webhook_url & webhook_url_file must be configured\")\n\t}\n\n\treturn nil\n}\n\ntype MSTeamsV2Config struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\tHTTPConfig                 *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\tWebhookURL                 *amcommoncfg.SecretURL      `yaml:\"webhook_url,omitempty\" json:\"webhook_url,omitempty\"`\n\tWebhookURLFile             string                      `yaml:\"webhook_url_file,omitempty\" json:\"webhook_url_file,omitempty\"`\n\n\tTitle string `yaml:\"title,omitempty\" json:\"title,omitempty\"`\n\tText  string `yaml:\"text,omitempty\" json:\"text,omitempty\"`\n}\n\nfunc (c *MSTeamsV2Config) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultMSTeamsV2Config\n\ttype plain MSTeamsV2Config\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\n\tif c.WebhookURL == nil && c.WebhookURLFile == \"\" {\n\t\treturn errors.New(\"one of webhook_url or webhook_url_file must be configured\")\n\t}\n\n\tif c.WebhookURL != nil && len(c.WebhookURLFile) > 0 {\n\t\treturn errors.New(\"at most one of webhook_url & webhook_url_file must be configured\")\n\t}\n\n\treturn nil\n}\n\ntype JiraFieldConfig struct {\n\t// Template is the template string used to render the field.\n\tTemplate string `yaml:\"template,omitempty\" json:\"template,omitempty\"`\n\t// EnableUpdate indicates whether this field should be omitted when updating an existing issue.\n\tEnableUpdate *bool `yaml:\"enable_update,omitempty\" json:\"enable_update,omitempty\"`\n}\n\ntype JiraConfig struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\tHTTPConfig                 *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\n\tAPIURL  *amcommoncfg.URL `yaml:\"api_url,omitempty\" json:\"api_url,omitempty\"`\n\tAPIType string           `yaml:\"api_type,omitempty\" json:\"api_type,omitempty\"`\n\n\tProject     string          `yaml:\"project,omitempty\" json:\"project,omitempty\"`\n\tSummary     JiraFieldConfig `yaml:\"summary,omitempty\" json:\"summary,omitempty\"`\n\tDescription JiraFieldConfig `yaml:\"description,omitempty\" json:\"description,omitempty\"`\n\tLabels      []string        `yaml:\"labels,omitempty\" json:\"labels,omitempty\"`\n\tPriority    string          `yaml:\"priority,omitempty\" json:\"priority,omitempty\"`\n\tIssueType   string          `yaml:\"issue_type,omitempty\" json:\"issue_type,omitempty\"`\n\n\tReopenTransition  string         `yaml:\"reopen_transition,omitempty\" json:\"reopen_transition,omitempty\"`\n\tResolveTransition string         `yaml:\"resolve_transition,omitempty\" json:\"resolve_transition,omitempty\"`\n\tWontFixResolution string         `yaml:\"wont_fix_resolution,omitempty\" json:\"wont_fix_resolution,omitempty\"`\n\tReopenDuration    model.Duration `yaml:\"reopen_duration,omitempty\" json:\"reopen_duration,omitempty\"`\n\n\tFields map[string]any `yaml:\"fields,omitempty\" json:\"custom_fields,omitempty\"`\n}\n\nfunc (f *JiraFieldConfig) EnableUpdateValue() bool {\n\tif f.EnableUpdate == nil {\n\t\treturn true\n\t}\n\treturn *f.EnableUpdate\n}\n\n// Supports both the legacy string and the new object form.\nfunc (f *JiraFieldConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t// Try simple string first (backward compatibility).\n\tvar s string\n\tif err := unmarshal(&s); err == nil {\n\t\tf.Template = s\n\t\t// DisableUpdate stays false by default.\n\t\treturn nil\n\t}\n\n\t// Fallback to full object form.\n\ttype plain JiraFieldConfig\n\treturn unmarshal((*plain)(f))\n}\n\nfunc (c *JiraConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultJiraConfig\n\ttype plain JiraConfig\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\n\tif c.Project == \"\" {\n\t\treturn errors.New(\"missing project in jira_config\")\n\t}\n\tif c.IssueType == \"\" {\n\t\treturn errors.New(\"missing issue_type in jira_config\")\n\t}\n\tif c.APIType != \"auto\" &&\n\t\tc.APIType != \"cloud\" &&\n\t\tc.APIType != \"datacenter\" {\n\t\treturn errors.New(\"unknown api_type on jira_config, must be auto, cloud or datacenter\")\n\t}\n\treturn nil\n}\n\ntype RocketchatAttachmentField struct {\n\tShort *bool  `json:\"short\"`\n\tTitle string `json:\"title,omitempty\"`\n\tValue string `json:\"value,omitempty\"`\n}\n\nconst (\n\tProcessingTypeSendMessage        = \"sendMessage\"\n\tProcessingTypeRespondWithMessage = \"respondWithMessage\"\n)\n\ntype RocketchatAttachmentAction struct {\n\tType               string `json:\"type,omitempty\"`\n\tText               string `json:\"text,omitempty\"`\n\tURL                string `json:\"url,omitempty\"`\n\tImageURL           string `json:\"image_url,omitempty\"`\n\tIsWebView          bool   `json:\"is_webview\"`\n\tWebviewHeightRatio string `json:\"webview_height_ratio,omitempty\"`\n\tMsg                string `json:\"msg,omitempty\"`\n\tMsgInChatWindow    bool   `json:\"msg_in_chat_window\"`\n\tMsgProcessingType  string `json:\"msg_processing_type,omitempty\"`\n}\n\n// RocketchatConfig configures notifications via Rocketchat.\ntype RocketchatConfig struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\n\tHTTPConfig *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\n\tAPIURL      *amcommoncfg.URL  `yaml:\"api_url,omitempty\" json:\"api_url,omitempty\"`\n\tTokenID     *commoncfg.Secret `yaml:\"token_id,omitempty\" json:\"token_id,omitempty\"`\n\tTokenIDFile string            `yaml:\"token_id_file,omitempty\" json:\"token_id_file,omitempty\"`\n\tToken       *commoncfg.Secret `yaml:\"token,omitempty\" json:\"token,omitempty\"`\n\tTokenFile   string            `yaml:\"token_file,omitempty\" json:\"token_file,omitempty\"`\n\n\t// RocketChat channel override, (like #other-channel or @username).\n\tChannel string `yaml:\"channel,omitempty\" json:\"channel,omitempty\"`\n\n\tColor       string                        `yaml:\"color,omitempty\" json:\"color,omitempty\"`\n\tTitle       string                        `yaml:\"title,omitempty\" json:\"title,omitempty\"`\n\tTitleLink   string                        `yaml:\"title_link,omitempty\" json:\"title_link,omitempty\"`\n\tText        string                        `yaml:\"text,omitempty\" json:\"text,omitempty\"`\n\tFields      []*RocketchatAttachmentField  `yaml:\"fields,omitempty\" json:\"fields,omitempty\"`\n\tShortFields bool                          `yaml:\"short_fields\" json:\"short_fields,omitempty\"`\n\tEmoji       string                        `yaml:\"emoji,omitempty\" json:\"emoji,omitempty\"`\n\tIconURL     string                        `yaml:\"icon_url,omitempty\" json:\"icon_url,omitempty\"`\n\tImageURL    string                        `yaml:\"image_url,omitempty\" json:\"image_url,omitempty\"`\n\tThumbURL    string                        `yaml:\"thumb_url,omitempty\" json:\"thumb_url,omitempty\"`\n\tLinkNames   bool                          `yaml:\"link_names\" json:\"link_names,omitempty\"`\n\tActions     []*RocketchatAttachmentAction `yaml:\"actions,omitempty\" json:\"actions,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (c *RocketchatConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultRocketchatConfig\n\ttype plain RocketchatConfig\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\tif c.Token != nil && len(c.TokenFile) > 0 {\n\t\treturn errors.New(\"at most one of token & token_file must be configured\")\n\t}\n\tif c.TokenID != nil && len(c.TokenIDFile) > 0 {\n\t\treturn errors.New(\"at most one of token_id & token_id_file must be configured\")\n\t}\n\treturn nil\n}\n\n// MattermostPriority defines the priority for a mattermost notification.\ntype MattermostPriority struct {\n\tPriority                string `yaml:\"priority,omitempty\" json:\"priority,omitempty\"`\n\tRequestedAck            bool   `yaml:\"requested_ack,omitempty\" json:\"requested_ack,omitempty\"`\n\tPersistentNotifications bool   `yaml:\"persistent_notifications,omitempty\" json:\"persistent_notifications,omitempty\"`\n}\n\n// MattermostProps defines additional properties for a mattermost notification.\n// Only 'card' property takes effect now.\ntype MattermostProps struct {\n\tCard string `yaml:\"card,omitempty\" json:\"card,omitempty\"`\n}\n\n// MattermostField configures a single Mattermost field for Slack compatibility.\n// See https://developers.mattermost.com/integrate/reference/message-attachments/#fields for more information.\ntype MattermostField struct {\n\tTitle string `yaml:\"title,omitempty\" json:\"title,omitempty\"`\n\tValue string `yaml:\"value,omitempty\" json:\"value,omitempty\"`\n\tShort *bool  `yaml:\"short,omitempty\" json:\"short,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface for MattermostField.\nfunc (c *MattermostField) UnmarshalYAML(unmarshal func(any) error) error {\n\ttype plain MattermostField\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\tif c.Title == \"\" {\n\t\treturn errors.New(\"missing title in Mattermost field configuration\")\n\t}\n\tif c.Value == \"\" {\n\t\treturn errors.New(\"missing value in Mattermost field configuration\")\n\t}\n\treturn nil\n}\n\n// MattermostAttachment defines an attachment for a Mattermost notification.\n// See https://developers.mattermost.com/integrate/reference/message-attachments/#fields for more information.\ntype MattermostAttachment struct {\n\tFallback   string             `yaml:\"fallback,omitempty\" json:\"fallback,omitempty\"`\n\tColor      string             `yaml:\"color,omitempty\" json:\"color,omitempty\"`\n\tPretext    string             `yaml:\"pretext,omitempty\" json:\"pretext,omitempty\"`\n\tText       string             `yaml:\"text,omitempty\" json:\"text,omitempty\"`\n\tAuthorName string             `yaml:\"author_name,omitempty\" json:\"author_name,omitempty\"`\n\tAuthorLink string             `yaml:\"author_link,omitempty\" json:\"author_link,omitempty\"`\n\tAuthorIcon string             `yaml:\"author_icon,omitempty\" json:\"author_icon,omitempty\"`\n\tTitle      string             `yaml:\"title,omitempty\" json:\"title,omitempty\"`\n\tTitleLink  string             `yaml:\"title_link,omitempty\" json:\"title_link,omitempty\"`\n\tFields     []*MattermostField `yaml:\"fields,omitempty\" json:\"fields,omitempty\"`\n\tThumbURL   string             `yaml:\"thumb_url,omitempty\" json:\"thumb_url,omitempty\"`\n\tFooter     string             `yaml:\"footer,omitempty\" json:\"footer,omitempty\"`\n\tFooterIcon string             `yaml:\"footer_icon,omitempty\" json:\"footer_icon,omitempty\"`\n\tImageURL   string             `yaml:\"image_url,omitempty\" json:\"image_url,omitempty\"`\n}\n\n// MattermostConfig configures notifications via Mattermost.\n// See https://developers.mattermost.com/integrate/webhooks/incoming/ for more information.\ntype MattermostConfig struct {\n\tamcommoncfg.NotifierConfig `yaml:\",inline\" json:\",inline\"`\n\n\tHTTPConfig     *commoncfg.HTTPClientConfig `yaml:\"http_config,omitempty\" json:\"http_config,omitempty\"`\n\tWebhookURL     *amcommoncfg.SecretURL      `yaml:\"webhook_url,omitempty\" json:\"webhook_url,omitempty\"`\n\tWebhookURLFile string                      `yaml:\"webhook_url_file,omitempty\" json:\"webhook_url_file,omitempty\"`\n\n\tChannel  string `yaml:\"channel,omitempty\" json:\"channel,omitempty\"`\n\tUsername string `yaml:\"username,omitempty\" json:\"username,omitempty\"`\n\n\tText        string                  `yaml:\"text,omitempty\" json:\"text,omitempty\"`\n\tFallback    string                  `yaml:\"fallback,omitempty\" json:\"fallback,omitempty\"`\n\tColor       string                  `yaml:\"color,omitempty\" json:\"color,omitempty\"`\n\tPretext     string                  `yaml:\"pretext,omitempty\" json:\"pretext,omitempty\"`\n\tAuthorName  string                  `yaml:\"author_name,omitempty\" json:\"author_name,omitempty\"`\n\tAuthorLink  string                  `yaml:\"author_link,omitempty\" json:\"author_link,omitempty\"`\n\tAuthorIcon  string                  `yaml:\"author_icon,omitempty\" json:\"author_icon,omitempty\"`\n\tTitle       string                  `yaml:\"title,omitempty\" json:\"title,omitempty\"`\n\tTitleLink   string                  `yaml:\"title_link,omitempty\" json:\"title_link,omitempty\"`\n\tFields      []*MattermostField      `yaml:\"fields,omitempty\" json:\"fields,omitempty\"`\n\tThumbURL    string                  `yaml:\"thumb_url,omitempty\" json:\"thumb_url,omitempty\"`\n\tFooter      string                  `yaml:\"footer,omitempty\" json:\"footer,omitempty\"`\n\tFooterIcon  string                  `yaml:\"footer_icon,omitempty\" json:\"footer_icon,omitempty\"`\n\tImageURL    string                  `yaml:\"image_url,omitempty\" json:\"image_url,omitempty\"`\n\tIconURL     string                  `yaml:\"icon_url,omitempty\" json:\"icon_url,omitempty\"`\n\tIconEmoji   string                  `yaml:\"icon_emoji,omitempty\" json:\"icon_emoji,omitempty\"`\n\tAttachments []*MattermostAttachment `yaml:\"attachments,omitempty\" json:\"attachments,omitempty\"`\n\tType        string                  `yaml:\"type,omitempty\" json:\"type,omitempty\"`\n\tProps       *MattermostProps        `yaml:\"props,omitempty\" json:\"props,omitempty\"`\n\tPriority    *MattermostPriority     `yaml:\"priority,omitempty\" json:\"priority,omitempty\"`\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (c *MattermostConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*c = DefaultMattermostConfig\n\ttype plain MattermostConfig\n\tif err := unmarshal((*plain)(c)); err != nil {\n\t\treturn err\n\t}\n\n\tif c.WebhookURL != nil && len(c.WebhookURLFile) > 0 {\n\t\treturn errors.New(\"at most one of webhook_url & webhook_url_file must be configured\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "config/notifiers_test.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage config\n\nimport (\n\t\"errors\"\n\t\"net/mail\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"gopkg.in/yaml.v2\"\n)\n\nfunc TestEmailToIsPresent(t *testing.T) {\n\tin := `\nto: ''\n`\n\tvar cfg EmailConfig\n\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\texpected := \"missing to address in email config\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t}\n}\n\nfunc TestEmailHeadersCollision(t *testing.T) {\n\tin := `\nto: 'to@email.com'\nheaders:\n  Subject: 'Alert'\n  sUbject: 'New Alert'\n`\n\tvar cfg EmailConfig\n\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\texpected := \"duplicate header \\\"Subject\\\" in email config\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t}\n}\n\nfunc TestEmailToAllowsMultipleAdresses(t *testing.T) {\n\tin := `\nto: 'a@example.com, ,b@example.com,c@example.com'\n`\n\tvar cfg EmailConfig\n\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\texpected := []*mail.Address{\n\t\t{Address: \"a@example.com\"},\n\t\t{Address: \"b@example.com\"},\n\t\t{Address: \"c@example.com\"},\n\t}\n\n\tres, err := mail.ParseAddressList(cfg.To)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif !reflect.DeepEqual(res, expected) {\n\t\tt.Fatalf(\"expected %v, got %v\", expected, res)\n\t}\n}\n\nfunc TestEmailDisallowMalformed(t *testing.T) {\n\tin := `\nto: 'a@'\n`\n\tvar cfg EmailConfig\n\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\t_, err = mail.ParseAddressList(cfg.To)\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%v\", \"mail: no angle-addr\")\n\t}\n}\n\nfunc TestPagerdutyTestRoutingKey(t *testing.T) {\n\tt.Run(\"error if no routing key or key file\", func(t *testing.T) {\n\t\tin := `\nrouting_key: ''\n`\n\t\tvar cfg PagerdutyConfig\n\t\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\t\texpected := \"missing service or routing key in PagerDuty config\"\n\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t\t}\n\t\tif err.Error() != expected {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t\t}\n\t})\n\n\tt.Run(\"error if both routing key and key file\", func(t *testing.T) {\n\t\tin := `\nrouting_key: 'xyz'\nrouting_key_file: 'xyz'\n`\n\t\tvar cfg PagerdutyConfig\n\t\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\t\texpected := \"at most one of routing_key & routing_key_file must be configured\"\n\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t\t}\n\t\tif err.Error() != expected {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t\t}\n\t})\n}\n\nfunc TestPagerdutyServiceKey(t *testing.T) {\n\tt.Run(\"error if no service key or key file\", func(t *testing.T) {\n\t\tin := `\nservice_key: ''\n`\n\t\tvar cfg PagerdutyConfig\n\t\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\t\texpected := \"missing service or routing key in PagerDuty config\"\n\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t\t}\n\t\tif err.Error() != expected {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t\t}\n\t})\n\n\tt.Run(\"error if both service key and key file\", func(t *testing.T) {\n\t\tin := `\nservice_key: 'xyz'\nservice_key_file: 'xyz'\n`\n\t\tvar cfg PagerdutyConfig\n\t\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\t\texpected := \"at most one of service_key & service_key_file must be configured\"\n\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t\t}\n\t\tif err.Error() != expected {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t\t}\n\t})\n}\n\nfunc TestPagerdutyDetails(t *testing.T) {\n\ttests := []struct {\n\t\tin      string\n\t\tcheckFn func(map[string]any)\n\t}{\n\t\t{\n\t\t\tin: `\nrouting_key: 'xyz'\n`,\n\t\t\tcheckFn: func(d map[string]any) {\n\t\t\t\tif len(d) != 4 {\n\t\t\t\t\tt.Errorf(\"expected 4 items, got: %d\", len(d))\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tin: `\nrouting_key: 'xyz'\ndetails:\n  key1: val1\n`,\n\t\t\tcheckFn: func(d map[string]any) {\n\t\t\t\tif len(d) != 5 {\n\t\t\t\t\tt.Errorf(\"expected 5 items, got: %d\", len(d))\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tin: `\nrouting_key: 'xyz'\ndetails:\n  key1: val1\n  key2: val2\n  firing: firing\n`,\n\t\t\tcheckFn: func(d map[string]any) {\n\t\t\t\tif len(d) != 6 {\n\t\t\t\t\tt.Errorf(\"expected 6 items, got: %d\", len(d))\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tc := range tests {\n\t\tvar cfg PagerdutyConfig\n\t\terr := yaml.UnmarshalStrict([]byte(tc.in), &cfg)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"expected no error, got:%v\", err)\n\t\t}\n\n\t\tif tc.checkFn != nil {\n\t\t\ttc.checkFn(cfg.Details)\n\t\t}\n\t}\n}\n\nfunc TestPagerDutySource(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\ttitle string\n\t\tin    string\n\n\t\texpectedSource string\n\t}{\n\t\t{\n\t\t\ttitle: \"check source field is backward compatible\",\n\t\t\tin: `\nrouting_key: 'xyz'\nclient: 'alert-manager-client'\n`,\n\t\t\texpectedSource: \"alert-manager-client\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"check source field is set\",\n\t\t\tin: `\nrouting_key: 'xyz'\nclient: 'alert-manager-client'\nsource: 'alert-manager-source'\n`,\n\t\t\texpectedSource: \"alert-manager-source\",\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\tvar cfg PagerdutyConfig\n\t\t\terr := yaml.UnmarshalStrict([]byte(tc.in), &cfg)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.expectedSource, cfg.Source)\n\t\t})\n\t}\n}\n\nfunc TestWebhookURLIsPresent(t *testing.T) {\n\tin := `{}`\n\tvar cfg WebhookConfig\n\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\texpected := \"one of url or url_file must be configured\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t}\n}\n\nfunc TestWebhookURLOrURLFile(t *testing.T) {\n\tin := `\nurl: 'http://example.com'\nurl_file: 'http://example.com'\n`\n\tvar cfg WebhookConfig\n\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\texpected := \"at most one of url & url_file must be configured\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t}\n}\n\nfunc TestWebhookHttpConfigIsValid(t *testing.T) {\n\tin := `\nurl: 'http://example.com'\nhttp_config:\n  bearer_token: foo\n  bearer_token_file: /tmp/bar\n`\n\tvar cfg WebhookConfig\n\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\texpected := \"at most one of bearer_token & bearer_token_file must be configured\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t}\n}\n\nfunc TestWebhookHttpConfigIsOptional(t *testing.T) {\n\tin := `\nurl: 'http://example.com'\n`\n\tvar cfg WebhookConfig\n\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\tif err != nil {\n\t\tt.Fatalf(\"no error expected, returned:\\n%v\", err.Error())\n\t}\n}\n\nfunc TestWebhookPasswordIsObfuscated(t *testing.T) {\n\tin := `\nurl: 'http://example.com'\nhttp_config:\n  basic_auth:\n    username: foo\n    password: supersecret\n`\n\tvar cfg WebhookConfig\n\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\tif err != nil {\n\t\tt.Fatalf(\"no error expected, returned:\\n%v\", err.Error())\n\t}\n\n\tycfg, err := yaml.Marshal(cfg)\n\tif err != nil {\n\t\tt.Fatalf(\"no error expected, returned:\\n%v\", err.Error())\n\t}\n\tif strings.Contains(string(ycfg), \"supersecret\") {\n\t\tt.Errorf(\"Found password in the YAML cfg: %s\\n\", ycfg)\n\t}\n}\n\nfunc TestVictorOpsConfiguration(t *testing.T) {\n\tt.Run(\"valid configuration\", func(t *testing.T) {\n\t\tin := `\nrouting_key: test\napi_key_file: /global_file\n`\n\t\tvar cfg VictorOpsConfig\n\t\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"no error was expected:\\n%v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"routing key is missing\", func(t *testing.T) {\n\t\tin := `\nrouting_key: ''\n`\n\t\tvar cfg VictorOpsConfig\n\t\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\t\texpected := \"missing Routing key in VictorOps config\"\n\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t\t}\n\t\tif err.Error() != expected {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t\t}\n\t})\n\n\tt.Run(\"api_key and api_key_file both defined\", func(t *testing.T) {\n\t\tin := `\nrouting_key: test\napi_key: xyz\napi_key_file: /global_file\n`\n\t\tvar cfg VictorOpsConfig\n\t\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\t\texpected := \"at most one of api_key & api_key_file must be configured\"\n\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t\t}\n\t\tif err.Error() != expected {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t\t}\n\t})\n}\n\nfunc TestVictorOpsCustomFieldsValidation(t *testing.T) {\n\tin := `\nrouting_key: 'test'\ncustom_fields:\n  entity_state: 'state_message'\n`\n\tvar cfg VictorOpsConfig\n\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\texpected := \"victorOps config contains custom field entity_state which cannot be used as it conflicts with the fixed/static fields\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t}\n\n\tin = `\nrouting_key: 'test'\ncustom_fields:\n  my_special_field: 'special_label'\n`\n\n\terr = yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\texpected = \"special_label\"\n\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected error returned, got:\\n%v\", err.Error())\n\t}\n\n\tval, ok := cfg.CustomFields[\"my_special_field\"]\n\n\tif !ok {\n\t\tt.Fatalf(\"Expected Custom Field to have value %v set, field is empty\", expected)\n\t}\n\tif val != expected {\n\t\tt.Errorf(\"\\nexpected custom field my_special_field value:\\n%v\\ngot:\\n%v\", expected, val)\n\t}\n}\n\nfunc TestPushoverUserKeyIsPresent(t *testing.T) {\n\tin := `\nuser_key: ''\n`\n\tvar cfg PushoverConfig\n\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\texpected := \"one of user_key or user_key_file must be configured\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t}\n}\n\nfunc TestPushoverUserKeyOrUserKeyFile(t *testing.T) {\n\tin := `\nuser_key: 'user key'\nuser_key_file: /pushover/user_key\n`\n\tvar cfg PushoverConfig\n\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\texpected := \"at most one of user_key & user_key_file must be configured\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t}\n}\n\nfunc TestPushoverTokenIsPresent(t *testing.T) {\n\tin := `\nuser_key: '<user_key>'\ntoken: ''\n`\n\tvar cfg PushoverConfig\n\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\texpected := \"one of token or token_file must be configured\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t}\n}\n\nfunc TestPushoverTokenOrTokenFile(t *testing.T) {\n\tin := `\ntoken: 'pushover token'\ntoken_file: /pushover/token\nuser_key: 'user key'\n`\n\tvar cfg PushoverConfig\n\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\texpected := \"at most one of token & token_file must be configured\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t}\n}\n\nfunc TestPushoverHTMLOrMonospace(t *testing.T) {\n\tin := `\ntoken: 'pushover token'\nuser_key: 'user key'\nhtml: true\nmonospace: true\n`\n\tvar cfg PushoverConfig\n\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\n\texpected := \"at most one of monospace & html must be configured\"\n\n\tif err == nil {\n\t\tt.Fatalf(\"no error returned, expected:\\n%v\", expected)\n\t}\n\tif err.Error() != expected {\n\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, err.Error())\n\t}\n}\n\nfunc TestLoadSlackConfiguration(t *testing.T) {\n\ttests := []struct {\n\t\tin       string\n\t\texpected SlackConfig\n\t}{\n\t\t{\n\t\t\tin: `\ncolor: green\nusername: mark\nchannel: engineering\ntitle_link: http://example.com/\nimage_url: https://example.com/logo.png\n`,\n\t\t\texpected: SlackConfig{\n\t\t\t\tColor: \"green\", Username: \"mark\", Channel: \"engineering\",\n\t\t\t\tTitleLink: \"http://example.com/\",\n\t\t\t\tImageURL:  \"https://example.com/logo.png\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tin: `\ncolor: green\nusername: mark\nchannel: alerts\ntitle_link: http://example.com/alert1\nmrkdwn_in:\n- pretext\n- text\n`,\n\t\t\texpected: SlackConfig{\n\t\t\t\tColor: \"green\", Username: \"mark\", Channel: \"alerts\",\n\t\t\t\tMrkdwnIn: []string{\"pretext\", \"text\"}, TitleLink: \"http://example.com/alert1\",\n\t\t\t},\n\t\t},\n\t}\n\tfor _, rt := range tests {\n\t\tvar cfg SlackConfig\n\t\terr := yaml.UnmarshalStrict([]byte(rt.in), &cfg)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"\\nerror returned when none expected, error:\\n%v\", err)\n\t\t}\n\t\tif rt.expected.Color != cfg.Color {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", rt.expected.Color, cfg.Color)\n\t\t}\n\t\tif rt.expected.Username != cfg.Username {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", rt.expected.Username, cfg.Username)\n\t\t}\n\t\tif rt.expected.Channel != cfg.Channel {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", rt.expected.Channel, cfg.Channel)\n\t\t}\n\t\tif rt.expected.ThumbURL != cfg.ThumbURL {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", rt.expected.ThumbURL, cfg.ThumbURL)\n\t\t}\n\t\tif rt.expected.TitleLink != cfg.TitleLink {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", rt.expected.TitleLink, cfg.TitleLink)\n\t\t}\n\t\tif rt.expected.ImageURL != cfg.ImageURL {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", rt.expected.ImageURL, cfg.ImageURL)\n\t\t}\n\t\tif len(rt.expected.MrkdwnIn) != len(cfg.MrkdwnIn) {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", rt.expected.MrkdwnIn, cfg.MrkdwnIn)\n\t\t}\n\t\tfor i := range cfg.MrkdwnIn {\n\t\t\tif rt.expected.MrkdwnIn[i] != cfg.MrkdwnIn[i] {\n\t\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\\nat index %v\", rt.expected.MrkdwnIn[i], cfg.MrkdwnIn[i], i)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestSlackAuthMethodConfigValidation(t *testing.T) {\n\ttests := []struct {\n\t\tin          string\n\t\texpectedErr string\n\t}{\n\t\t{\n\t\t\tin: `\napi_url: 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'\napi_url_file: /slack_url\n`,\n\t\t\texpectedErr: \"at most one of api_url & api_url_file must be configured\",\n\t\t},\n\t\t{\n\t\t\tin: `\napp_token: 'xoxb-1234-abcdefgh'\napp_token_file: /slack_app_token\n`,\n\t\t\texpectedErr: \"at most one of app_token & app_token_file must be configured\",\n\t\t},\n\t\t{\n\t\t\tin: `\napp_token: 'xoxb-1234-abcdefgh'\napi_url: 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'\n`,\n\t\t\texpectedErr: \"at most one of api_url/api_url_file & app_token/app_token_file must be configured\",\n\t\t},\n\t}\n\n\tfor _, rt := range tests {\n\t\tvar cfg SlackConfig\n\t\terr := yaml.UnmarshalStrict([]byte(rt.in), &cfg)\n\n\t\t// Check if an error occurred when it was NOT expected to.\n\t\tif rt.expectedErr == \"\" && err != nil {\n\t\t\tt.Fatalf(\"\\nerror returned when none expected, error:\\n%v\", err)\n\t\t}\n\t\t// Check that an error occurred if one was expected to.\n\t\tif rt.expectedErr != \"\" && err == nil {\n\t\t\tt.Fatalf(\"\\nno error returned, expected:\\n%v\", rt.expectedErr)\n\t\t}\n\t\t// Check that the error that occurred was what was expected.\n\t\tif err != nil && err.Error() != rt.expectedErr {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", rt.expectedErr, err.Error())\n\t\t}\n\t}\n}\n\nfunc TestSlackFieldConfigValidation(t *testing.T) {\n\ttests := []struct {\n\t\tin       string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tin: `\nfields:\n- title: first\n  value: hello\n- title: second\n`,\n\t\t\texpected: \"missing value in Slack field configuration\",\n\t\t},\n\t\t{\n\t\t\tin: `\nfields:\n- title: first\n  value: hello\n  short: true\n- value: world\n  short: true\n`,\n\t\t\texpected: \"missing title in Slack field configuration\",\n\t\t},\n\t\t{\n\t\t\tin: `\nfields:\n- title: first\n  value: hello\n  short: true\n- title: second\n  value: world\n`,\n\t\t\texpected: \"\",\n\t\t},\n\t}\n\n\tfor _, rt := range tests {\n\t\tvar cfg SlackConfig\n\t\terr := yaml.UnmarshalStrict([]byte(rt.in), &cfg)\n\n\t\t// Check if an error occurred when it was NOT expected to.\n\t\tif rt.expected == \"\" && err != nil {\n\t\t\tt.Fatalf(\"\\nerror returned when none expected, error:\\n%v\", err)\n\t\t}\n\t\t// Check that an error occurred if one was expected to.\n\t\tif rt.expected != \"\" && err == nil {\n\t\t\tt.Fatalf(\"\\nno error returned, expected:\\n%v\", rt.expected)\n\t\t}\n\t\t// Check that the error that occurred was what was expected.\n\t\tif err != nil && err.Error() != rt.expected {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", rt.expected, err.Error())\n\t\t}\n\t}\n}\n\nfunc TestSlackFieldConfigUnmarshaling(t *testing.T) {\n\tin := `\nfields:\n- title: first\n  value: hello\n  short: true\n- title: second\n  value: world\n- title: third\n  value: slack field test\n  short: false\n`\n\texpected := []*SlackField{\n\t\t{\n\t\t\tTitle: \"first\",\n\t\t\tValue: \"hello\",\n\t\t\tShort: newBoolPointer(true),\n\t\t},\n\t\t{\n\t\t\tTitle: \"second\",\n\t\t\tValue: \"world\",\n\t\t\tShort: nil,\n\t\t},\n\t\t{\n\t\t\tTitle: \"third\",\n\t\t\tValue: \"slack field test\",\n\t\t\tShort: newBoolPointer(false),\n\t\t},\n\t}\n\n\tvar cfg SlackConfig\n\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\tif err != nil {\n\t\tt.Fatalf(\"\\nerror returned when none expected, error:\\n%v\", err)\n\t}\n\n\tfor index, field := range cfg.Fields {\n\t\texp := expected[index]\n\t\tif field.Title != exp.Title {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", exp.Title, field.Title)\n\t\t}\n\t\tif field.Value != exp.Value {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", exp.Value, field.Value)\n\t\t}\n\t\tif exp.Short == nil && field.Short != nil {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", exp.Short, *field.Short)\n\t\t}\n\t\tif exp.Short != nil && field.Short == nil {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", *exp.Short, field.Short)\n\t\t}\n\t\tif exp.Short != nil && *exp.Short != *field.Short {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", *exp.Short, *field.Short)\n\t\t}\n\t}\n}\n\nfunc TestSlackActionsValidation(t *testing.T) {\n\tin := `\nactions:\n- type: button\n  text: hello\n  url: https://localhost\n  style: danger\n- type: button\n  text: hello\n  name: something\n  style: default\n  confirm:\n    title: please confirm\n    text: are you sure?\n    ok_text: yes\n    dismiss_text: no\n`\n\texpected := []*SlackAction{\n\t\t{\n\t\t\tType:  \"button\",\n\t\t\tText:  \"hello\",\n\t\t\tURL:   \"https://localhost\",\n\t\t\tStyle: \"danger\",\n\t\t},\n\t\t{\n\t\t\tType:  \"button\",\n\t\t\tText:  \"hello\",\n\t\t\tName:  \"something\",\n\t\t\tStyle: \"default\",\n\t\t\tConfirmField: &SlackConfirmationField{\n\t\t\t\tTitle:       \"please confirm\",\n\t\t\t\tText:        \"are you sure?\",\n\t\t\t\tOkText:      \"yes\",\n\t\t\t\tDismissText: \"no\",\n\t\t\t},\n\t\t},\n\t}\n\n\tvar cfg SlackConfig\n\terr := yaml.UnmarshalStrict([]byte(in), &cfg)\n\tif err != nil {\n\t\tt.Fatalf(\"\\nerror returned when none expected, error:\\n%v\", err)\n\t}\n\n\tfor index, action := range cfg.Actions {\n\t\texp := expected[index]\n\t\tif action.Type != exp.Type {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", exp.Type, action.Type)\n\t\t}\n\t\tif action.Text != exp.Text {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", exp.Text, action.Text)\n\t\t}\n\t\tif action.URL != exp.URL {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", exp.URL, action.URL)\n\t\t}\n\t\tif action.Style != exp.Style {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", exp.Style, action.Style)\n\t\t}\n\t\tif action.Name != exp.Name {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", exp.Name, action.Name)\n\t\t}\n\t\tif action.Value != exp.Value {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", exp.Value, action.Value)\n\t\t}\n\t\tif action.ConfirmField != nil && exp.ConfirmField == nil || action.ConfirmField == nil && exp.ConfirmField != nil {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", exp.ConfirmField, action.ConfirmField)\n\t\t} else if action.ConfirmField != nil && exp.ConfirmField != nil {\n\t\t\tif action.ConfirmField.Title != exp.ConfirmField.Title {\n\t\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", exp.ConfirmField.Title, action.ConfirmField.Title)\n\t\t\t}\n\t\t\tif action.ConfirmField.Text != exp.ConfirmField.Text {\n\t\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", exp.ConfirmField.Text, action.ConfirmField.Text)\n\t\t\t}\n\t\t\tif action.ConfirmField.OkText != exp.ConfirmField.OkText {\n\t\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", exp.ConfirmField.OkText, action.ConfirmField.OkText)\n\t\t\t}\n\t\t\tif action.ConfirmField.DismissText != exp.ConfirmField.DismissText {\n\t\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", exp.ConfirmField.DismissText, action.ConfirmField.DismissText)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestOpsgenieTypeMatcher(t *testing.T) {\n\tgood := []string{\"team\", \"user\", \"escalation\", \"schedule\"}\n\tfor _, g := range good {\n\t\tif !opsgenieTypeMatcher.MatchString(g) {\n\t\t\tt.Fatalf(\"failed to match with %s\", g)\n\t\t}\n\t}\n\tbad := []string{\"0user\", \"team1\", \"2escalation3\", \"sche4dule\", \"User\", \"TEAM\"}\n\tfor _, b := range bad {\n\t\tif opsgenieTypeMatcher.MatchString(b) {\n\t\t\tt.Errorf(\"mistakenly match with %s\", b)\n\t\t}\n\t}\n}\n\nfunc TestOpsGenieConfiguration(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tname string\n\t\tin   string\n\n\t\terr bool\n\t}{\n\t\t{\n\t\t\tname: \"valid configuration\",\n\t\t\tin: `api_key: xyz\nresponders:\n- id: foo\n  type: scheDule\n- name: bar\n  type: teams\n- username: fred\n  type: USER\napi_url: http://example.com\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"api_key and api_key_file both defined\",\n\t\t\tin: `api_key: xyz\napi_key_file: xyz\napi_url: http://example.com\n`,\n\t\t\terr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid responder type\",\n\t\t\tin: `api_key: xyz\nresponders:\n- id: foo\n  type: wrong\napi_url: http://example.com\n`,\n\t\t\terr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"missing responder field\",\n\t\t\tin: `api_key: xyz\nresponders:\n- type: schedule\napi_url: http://example.com\n`,\n\t\t\terr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"valid responder type template\",\n\t\t\tin: `api_key: xyz\nresponders:\n- id: foo\n  type: \"{{/* valid comment */}}team\"\napi_url: http://example.com\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid responder type template\",\n\t\t\tin: `api_key: xyz\nresponders:\n- id: foo\n  type: \"{{/* invalid comment }}team\"\napi_url: http://example.com\n`,\n\t\t\terr: true,\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar cfg OpsGenieConfig\n\n\t\t\terr := yaml.UnmarshalStrict([]byte(tc.in), &cfg)\n\t\t\tif tc.err {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Fatalf(\"expected error but got none\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"expected no error, got %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSNS(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tin  string\n\t\terr bool\n\t}{\n\t\t{\n\t\t\t// Valid configuration without sigv4.\n\t\t\tin:  `target_arn: target`,\n\t\t\terr: false,\n\t\t},\n\t\t{\n\t\t\t// Valid configuration without sigv4.\n\t\t\tin:  `topic_arn: topic`,\n\t\t\terr: false,\n\t\t},\n\t\t{\n\t\t\t// Valid configuration with sigv4.\n\t\t\tin: `phone_number: phone\nsigv4:\n    access_key: abc\n    secret_key: abc\n`,\n\t\t\terr: false,\n\t\t},\n\t\t{\n\t\t\t// at most one of 'target_arn', 'topic_arn' or 'phone_number' must be provided without sigv4.\n\t\t\tin: `topic_arn: topic\ntarget_arn: target\n`,\n\t\t\terr: true,\n\t\t},\n\t\t{\n\t\t\t// at most one of 'target_arn', 'topic_arn' or 'phone_number' must be provided without sigv4.\n\t\t\tin: `topic_arn: topic\nphone_number: phone\n`,\n\t\t\terr: true,\n\t\t},\n\t\t{\n\t\t\t// one of 'target_arn', 'topic_arn' or 'phone_number' must be provided without sigv4.\n\t\t\tin:  \"{}\",\n\t\t\terr: true,\n\t\t},\n\t\t{\n\t\t\t// one of 'target_arn', 'topic_arn' or 'phone_number' must be provided with sigv4.\n\t\t\tin: `sigv4:\n    access_key: abc\n    secret_key: abc\n`,\n\t\t\terr: true,\n\t\t},\n\t\t{\n\t\t\t// 'secret_key' must be provided with 'access_key'.\n\t\t\tin: `topic_arn: topic\nsigv4:\n    access_key: abc\n`,\n\t\t\terr: true,\n\t\t},\n\t\t{\n\t\t\t// 'access_key' must be provided with 'secret_key'.\n\t\t\tin: `topic_arn: topic\nsigv4:\n    secret_key: abc\n`,\n\t\t\terr: true,\n\t\t},\n\t} {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tvar cfg SNSConfig\n\t\t\terr := yaml.UnmarshalStrict([]byte(tc.in), &cfg)\n\t\t\tif err != nil {\n\t\t\t\tif !tc.err {\n\t\t\t\t\tt.Errorf(\"expecting no error, got %q\", err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tc.err {\n\t\t\t\tt.Logf(\"%#v\", cfg)\n\t\t\t\tt.Error(\"expecting error, got none\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWeChatTypeMatcher(t *testing.T) {\n\tgood := []string{\"text\", \"markdown\"}\n\tfor _, g := range good {\n\t\tif !wechatTypeMatcher.MatchString(g) {\n\t\t\tt.Fatalf(\"failed to match with %s\", g)\n\t\t}\n\t}\n\tbad := []string{\"TEXT\", \"MarkDOwn\"}\n\tfor _, b := range bad {\n\t\tif wechatTypeMatcher.MatchString(b) {\n\t\t\tt.Errorf(\"mistakenly match with %s\", b)\n\t\t}\n\t}\n}\n\nfunc TestWebexConfiguration(t *testing.T) {\n\ttc := []struct {\n\t\tname string\n\n\t\tin       string\n\t\texpected error\n\t}{\n\t\t{\n\t\t\tname: \"with no room_id - it fails\",\n\t\t\tin: `\nmessage: xyz123\n`,\n\t\t\texpected: errors.New(\"missing room_id on webex_config\"),\n\t\t},\n\t\t{\n\t\t\tname: \"with room_id and http_config.authorization set - it succeeds\",\n\t\t\tin: `\nroom_id: 2\nhttp_config:\n  authorization:\n    credentials: \"xxxyyyzz\"\n`,\n\t\t},\n\t}\n\n\tfor _, tt := range tc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar cfg WebexConfig\n\t\t\terr := yaml.UnmarshalStrict([]byte(tt.in), &cfg)\n\n\t\t\trequire.Equal(t, tt.expected, err)\n\t\t})\n\t}\n}\n\nfunc TestTelegramConfiguration(t *testing.T) {\n\ttc := []struct {\n\t\tname     string\n\t\tin       string\n\t\texpected error\n\t}{\n\t\t{\n\t\t\tname: \"with both bot_token & bot_token_file - it fails\",\n\t\t\tin: `\nbot_token: xyz\nbot_token_file: /file\n`,\n\t\t\texpected: errors.New(\"at most one of bot_token & bot_token_file must be configured\"),\n\t\t},\n\t\t{\n\t\t\tname: \"with bot_token and chat_id set - it succeeds\",\n\t\t\tin: `\nbot_token: xyz\nchat_id: 123\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"with bot_token, chat_id and message_thread_id set - it succeeds\",\n\t\t\tin: `\nbot_token: xyz\nchat_id: 123\nmessage_thread_id: 456\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"with bot_token_file and chat_id set - it succeeds\",\n\t\t\tin: `\nbot_token_file: /file\nchat_id: 123\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"with bot_token_file and chat_id_file set - it succeeds\",\n\t\t\tin: `\nbot_token_file: /file\nchat_id_file: /chat_id_file\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"with no chat_id set - it fails\",\n\t\t\tin: `\nbot_token: xyz\n`,\n\t\t\texpected: errors.New(\"missing chat_id or chat_id_file on telegram_config\"),\n\t\t},\n\t\t{\n\t\t\tname: \"with both chat_id and chat_id_file - it fails\",\n\t\t\tin: `\nbot_token: xyz\nchat_id: 123\nchat_id_file: /file\n`,\n\t\t\texpected: errors.New(\"at most one of chat_id & chat_id_file must be configured\"),\n\t\t},\n\t\t{\n\t\t\tname: \"with unknown parse_mode - it fails\",\n\t\t\tin: `\nbot_token: xyz\nchat_id: 123\nparse_mode: invalid\n`,\n\t\t\texpected: errors.New(\"unknown parse_mode on telegram_config, must be Markdown, MarkdownV2, HTML or empty string\"),\n\t\t},\n\t}\n\n\tfor _, tt := range tc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar cfg TelegramConfig\n\t\t\terr := yaml.UnmarshalStrict([]byte(tt.in), &cfg)\n\n\t\t\trequire.Equal(t, tt.expected, err)\n\t\t})\n\t}\n}\n\nfunc newBoolPointer(b bool) *bool {\n\treturn &b\n}\n\nfunc TestMattermostField_UnmarshalYAML(t *testing.T) {\n\tmf := []struct {\n\t\tname     string\n\t\tin       string\n\t\texpected error\n\t}{\n\t\t{\n\t\t\tname: \"with title, value and short - it succeeds\",\n\t\t\tin: `\ntitle: some title\nvalue: some value\nshort: true\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"with title and value - it succeeds\",\n\t\t\tin: `\ntitle: some title\nvalue: some value\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"with no value - it fails\",\n\t\t\tin: `\ntitle: some title\n`,\n\t\t\texpected: errors.New(\"missing value in Mattermost field configuration\"),\n\t\t},\n\t\t{\n\t\t\tname: \"with no title - it fails\",\n\t\t\tin: `\nvalue: some value\n`,\n\t\t\texpected: errors.New(\"missing title in Mattermost field configuration\"),\n\t\t},\n\t}\n\n\tfor _, tt := range mf {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar cfg MattermostField\n\t\t\terr := yaml.UnmarshalStrict([]byte(tt.in), &cfg)\n\n\t\t\trequire.Equal(t, tt.expected, err)\n\t\t})\n\t}\n}\n\nfunc TestMattermostConfig_UnmarshalYAML(t *testing.T) {\n\tmc := []struct {\n\t\tname     string\n\t\tin       string\n\t\texpected error\n\t}{\n\t\t{\n\t\t\tname: \"with url and text - it succeeds\",\n\t\t\tin: `\nwebhook_url: http://some.url\nchannel: some_channel\nusername: some_username\ntext: some text\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"with url_file, attachments and props - it succeeds\",\n\t\t\tin: `\nwebhook_url_file: /some/url.file\nchannel: some_channel\nusername: some_username\nattachments:\n- text: some text\nprops:\n  card: some text\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"with url and url_file - it fails\",\n\t\t\tin: `\nwebhook_url: http://some.url\nwebhook_url_file: /some/url.file\nchannel: some_channel\nusername: some_username\nattachments:\n- text: some text\n`,\n\t\t\texpected: errors.New(\"at most one of webhook_url & webhook_url_file must be configured\"),\n\t\t},\n\t\t{\n\t\t\tname: \"with text and attachments - it succeeds\",\n\t\t\tin: `\nwebhook_url: http://some.url\nchannel: some_channel\nusername: some_username\ntext: some text\nattachments:\n- text: some text\n`,\n\t\t},\n\t}\n\n\tfor _, tt := range mc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar cfg MattermostConfig\n\t\t\terr := yaml.UnmarshalStrict([]byte(tt.in), &cfg)\n\n\t\t\trequire.Equal(t, tt.expected, err)\n\t\t})\n\t}\n}\n\nfunc TestEmailConfig_UnmarshalYAML(t *testing.T) {\n\ttestConfig := []struct {\n\t\tname     string\n\t\tin       string\n\t\texpected error\n\t}{\n\t\t{\n\t\t\tname: \"with basic config - it succeeds\",\n\t\t\tin: `\nto: foobar@example.com\nheaders: {X-Custom-Header: CustomValue}\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"with empty to address - it fails\",\n\t\t\tin: `\nto: ''`,\n\t\t\texpected: errors.New(\"missing to address in email config\"),\n\t\t},\n\t\t{\n\t\t\tname: \"with correct threading - it succeeds\",\n\t\t\tin: `\nto: foobar@example.com\nthreading:\n  enabled: true\n  thread_by_date: daily\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"with invalid threading - it fails\",\n\t\t\tin: `\nto: foobar@example.com\nthreading:\n  enabled: true\n  thread_by_date: weekly\n`,\n\t\t\texpected: errors.New(\"threading.thread_by_date must be either 'none' or 'daily'\"),\n\t\t},\n\t\t{\n\t\t\tname: \"with duplicate headers - it failes\",\n\t\t\tin: `\nto: foobar@example.com\nheaders: {X-Custom-Header: CustomValue, X-CUSTOM-HEADER: AnotherValue}\n`,\n\t\t\texpected: errors.New(\"duplicate header \\\"X-Custom-Header\\\" in email config\"),\n\t\t},\n\t}\n\n\tfor _, tt := range testConfig {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar cfg EmailConfig\n\t\t\terr := yaml.UnmarshalStrict([]byte(tt.in), &cfg)\n\n\t\t\trequire.Equal(t, tt.expected, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "config/receiver/receiver.go",
    "content": "// Copyright 2023 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage receiver\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/promslog\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/notify/discord\"\n\t\"github.com/prometheus/alertmanager/notify/email\"\n\t\"github.com/prometheus/alertmanager/notify/incidentio\"\n\t\"github.com/prometheus/alertmanager/notify/jira\"\n\t\"github.com/prometheus/alertmanager/notify/mattermost\"\n\t\"github.com/prometheus/alertmanager/notify/msteams\"\n\t\"github.com/prometheus/alertmanager/notify/msteamsv2\"\n\t\"github.com/prometheus/alertmanager/notify/opsgenie\"\n\t\"github.com/prometheus/alertmanager/notify/pagerduty\"\n\t\"github.com/prometheus/alertmanager/notify/pushover\"\n\t\"github.com/prometheus/alertmanager/notify/rocketchat\"\n\t\"github.com/prometheus/alertmanager/notify/slack\"\n\t\"github.com/prometheus/alertmanager/notify/sns\"\n\t\"github.com/prometheus/alertmanager/notify/telegram\"\n\t\"github.com/prometheus/alertmanager/notify/victorops\"\n\t\"github.com/prometheus/alertmanager/notify/webex\"\n\t\"github.com/prometheus/alertmanager/notify/webhook\"\n\t\"github.com/prometheus/alertmanager/notify/wechat\"\n\t\"github.com/prometheus/alertmanager/template\"\n)\n\n// BuildReceiverIntegrations builds a list of integration notifiers off of a\n// receiver config.\nfunc BuildReceiverIntegrations(nc config.Receiver, tmpl *template.Template, logger *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) ([]notify.Integration, error) {\n\tif logger == nil {\n\t\tlogger = promslog.NewNopLogger()\n\t}\n\n\tvar (\n\t\terrs         error\n\t\tintegrations []notify.Integration\n\t\tadd          = func(name string, i int, rs notify.ResolvedSender, f func(l *slog.Logger) (notify.Notifier, error)) {\n\t\t\tn, err := f(logger.With(\"integration\", name))\n\t\t\tif err != nil {\n\t\t\t\terrs = errors.Join(errs, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tintegrations = append(integrations, notify.NewIntegration(n, rs, name, i, nc.Name))\n\t\t}\n\t)\n\n\tfor i, c := range nc.WebhookConfigs {\n\t\tadd(\"webhook\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return webhook.New(c, tmpl, l, httpOpts...) })\n\t}\n\tfor i, c := range nc.EmailConfigs {\n\t\tadd(\"email\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return email.New(c, tmpl, l), nil })\n\t}\n\tfor i, c := range nc.PagerdutyConfigs {\n\t\tadd(\"pagerduty\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return pagerduty.New(c, tmpl, l, httpOpts...) })\n\t}\n\tfor i, c := range nc.OpsGenieConfigs {\n\t\tadd(\"opsgenie\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return opsgenie.New(c, tmpl, l, httpOpts...) })\n\t}\n\tfor i, c := range nc.WechatConfigs {\n\t\tadd(\"wechat\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return wechat.New(c, tmpl, l, httpOpts...) })\n\t}\n\tfor i, c := range nc.SlackConfigs {\n\t\tadd(\"slack\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return slack.New(c, tmpl, l, httpOpts...) })\n\t}\n\tfor i, c := range nc.VictorOpsConfigs {\n\t\tadd(\"victorops\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return victorops.New(c, tmpl, l, httpOpts...) })\n\t}\n\tfor i, c := range nc.PushoverConfigs {\n\t\tadd(\"pushover\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return pushover.New(c, tmpl, l, httpOpts...) })\n\t}\n\tfor i, c := range nc.SNSConfigs {\n\t\tadd(\"sns\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return sns.New(c, tmpl, l, httpOpts...) })\n\t}\n\tfor i, c := range nc.TelegramConfigs {\n\t\tadd(\"telegram\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return telegram.New(c, tmpl, l, httpOpts...) })\n\t}\n\tfor i, c := range nc.DiscordConfigs {\n\t\tadd(\"discord\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return discord.New(c, tmpl, l, httpOpts...) })\n\t}\n\tfor i, c := range nc.WebexConfigs {\n\t\tadd(\"webex\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return webex.New(c, tmpl, l, httpOpts...) })\n\t}\n\tfor i, c := range nc.MSTeamsConfigs {\n\t\tadd(\"msteams\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return msteams.New(c, tmpl, l, httpOpts...) })\n\t}\n\tfor i, c := range nc.MSTeamsV2Configs {\n\t\tadd(\"msteamsv2\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return msteamsv2.New(c, tmpl, l, httpOpts...) })\n\t}\n\tfor i, c := range nc.JiraConfigs {\n\t\tadd(\"jira\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return jira.New(c, tmpl, l, httpOpts...) })\n\t}\n\tfor i, c := range nc.IncidentioConfigs {\n\t\tadd(\"incidentio\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return incidentio.New(c, tmpl, l, httpOpts...) })\n\t}\n\tfor i, c := range nc.RocketchatConfigs {\n\t\tadd(\"rocketchat\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return rocketchat.New(c, tmpl, l, httpOpts...) })\n\t}\n\tfor i, c := range nc.MattermostConfigs {\n\t\tadd(\"mattermost\", i, c, func(l *slog.Logger) (notify.Notifier, error) { return mattermost.New(c, tmpl, l, httpOpts...) })\n\t}\n\n\treturn integrations, errs\n}\n"
  },
  {
    "path": "config/receiver/receiver_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage receiver\n\nimport (\n\t\"testing\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/stretchr/testify/require\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n)\n\ntype sendResolved bool\n\nfunc (s sendResolved) SendResolved() bool { return bool(s) }\n\nfunc TestBuildReceiverIntegrations(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\treceiver config.Receiver\n\t\terr      bool\n\t\texp      []notify.Integration\n\t}{\n\t\t{\n\t\t\treceiver: config.Receiver{\n\t\t\t\tName: \"foo\",\n\t\t\t\tWebhookConfigs: []*config.WebhookConfig{\n\t\t\t\t\t{\n\t\t\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\t\t\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\t\t\t\t\tVSendResolved: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: []notify.Integration{\n\t\t\t\tnotify.NewIntegration(nil, sendResolved(false), \"webhook\", 0, \"foo\"),\n\t\t\t\tnotify.NewIntegration(nil, sendResolved(true), \"webhook\", 1, \"foo\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\treceiver: config.Receiver{\n\t\t\t\tName: \"foo\",\n\t\t\t\tWebhookConfigs: []*config.WebhookConfig{\n\t\t\t\t\t{\n\t\t\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{\n\t\t\t\t\t\t\tTLSConfig: commoncfg.TLSConfig{\n\t\t\t\t\t\t\t\tCAFile: \"not_existing\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: true,\n\t\t},\n\t} {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tintegrations, err := BuildReceiverIntegrations(tc.receiver, nil, nil)\n\t\t\tif tc.err {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, integrations, len(tc.exp))\n\t\t\tfor i := range tc.exp {\n\t\t\t\trequire.Equal(t, tc.exp[i].SendResolved(), integrations[i].SendResolved())\n\t\t\t\trequire.Equal(t, tc.exp[i].Name(), integrations[i].Name())\n\t\t\t\trequire.Equal(t, tc.exp[i].Index(), integrations[i].Index())\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "config/testdata/conf.empty-fields.yml",
    "content": "global:\n  smtp_smarthost: 'localhost:25'\n  smtp_from: 'alertmanager@example.org'\n  smtp_auth_username: ''\n  smtp_auth_password: ''\n  smtp_hello: ''\n  slack_api_url: 'https://slack.com/webhook'\ntemplates:\n  - '/etc/alertmanager/template/*.tmpl'\nroute:\n  group_by: ['alertname', 'cluster', 'service']\n  receiver: team-X-mails\n  routes:\n    - match_re:\n        service: ^(foo1|foo2|baz)$\n      receiver: team-X-mails\nreceivers:\n  - name: 'team-X-mails'\n    email_configs:\n      - to: 'team-X+alerts@example.org'\n"
  },
  {
    "path": "config/testdata/conf.good.yml",
    "content": "global:\n  # The smarthost and SMTP sender used for mail notifications.\n  smtp_smarthost: 'localhost:25'\n  smtp_from: 'alertmanager@example.org'\n  smtp_auth_username: 'alertmanager'\n  smtp_auth_password: \"multiline\\nmysecret\"\n  smtp_hello: \"host.example.org\"\n  slack_api_url: \"http://mysecret.example.com/\"\n  http_config:\n    proxy_url: 'http://127.0.0.1:1025'\n# The directory from which notification templates are read.\ntemplates:\n  - '/etc/alertmanager/template/*.tmpl'\n# The root route on which each incoming alert enters.\nroute:\n  # The labels by which incoming alerts are grouped together. For example,\n  # multiple alerts coming in for cluster=A and alertname=LatencyHigh would\n  # be batched into a single group.\n  group_by: ['alertname', 'cluster', 'service']\n  # When a new group of alerts is created by an incoming alert, wait at\n  # least 'group_wait' to send the initial notification.\n  # This way ensures that you get multiple alerts for the same group that start\n  # firing shortly after another are batched together on the first\n  # notification.\n  group_wait: 30s\n  # When the first notification was sent, wait 'group_interval' to send a batch\n  # of new alerts that started firing for that group.\n  group_interval: 5m\n  # If an alert has successfully been sent, wait 'repeat_interval' to\n  # resend them.\n  repeat_interval: 3h\n  # A default receiver\n  receiver: team-X-mails\n  # All the above attributes are inherited by all child routes and can\n  # overwritten on each.\n\n  # The child route trees.\n  routes:\n    # This routes performs a regular expression match on alert labels to\n    # catch alerts that are related to a list of services.\n    - match_re:\n        service: ^(foo1|foo2|baz)$\n      receiver: team-X-mails\n      # The service has a sub-route for critical alerts, any alerts\n      # that do not match, i.e. severity != critical, fall-back to the\n      # parent node and are sent to 'team-X-mails'\n      routes:\n        - match:\n            severity: critical\n          receiver: team-X-pager\n    - match:\n        service: files\n      receiver: team-Y-mails\n      routes:\n        - match:\n            severity: critical\n          receiver: team-Y-pager\n    # This route handles all alerts coming from a database service. If there's\n    # no team to handle it, it defaults to the DB team.\n    - match:\n        service: database\n      receiver: team-DB-pager\n      # Also group alerts by affected database.\n      group_by: [alertname, cluster, database]\n      routes:\n        - match:\n            owner2: team-X\n          receiver: team-X-pager\n          continue: true\n        - match:\n            owner: team-Y\n          receiver: team-Y-pager\n          # continue: true\n# Inhibition rules allow to mute a set of alerts given that another alert is\n# firing.\n# We use this to mute any warning-level notifications if the same alert is\n# already critical.\ninhibit_rules:\n  - source_match:\n      severity: 'critical'\n    target_match:\n      severity: 'warning'\n    # Apply inhibition if the alertname is the same.\n    equal: ['alertname', 'cluster', 'service']\nreceivers:\n  - name: 'team-X-mails'\n    email_configs:\n      - to: 'team-X+alerts@example.org'\n  - name: 'team-X-pager'\n    email_configs:\n      - to: 'team-X+alerts-critical@example.org'\n    pagerduty_configs:\n      - routing_key: \"mysecret\"\n  - name: 'team-Y-mails'\n    email_configs:\n      - to: 'team-Y+alerts@example.org'\n  - name: 'team-Y-pager'\n    pagerduty_configs:\n      - routing_key: \"mysecret\"\n  - name: 'team-DB-pager'\n    pagerduty_configs:\n      - routing_key: \"mysecret\"\n  - name: victorOps-receiver\n    victorops_configs:\n      - api_key: mysecret\n        routing_key: Sample_route\n  - name: opsGenie-receiver\n    opsgenie_configs:\n      - api_key: mysecret\n  - name: pushover-receiver\n    pushover_configs:\n      - token: mysecret\n        user_key: key\n  - name: slack-receiver\n    slack_configs:\n      - channel: '#my-channel'\n        image_url: 'http://some.img.com/img.png'\n"
  },
  {
    "path": "config/testdata/conf.group-by-all.yml",
    "content": "route:\n  group_by: ['...']\n  group_wait: 30s\n  group_interval: 5m\n  repeat_interval: 3h\n  receiver: team-X\nreceivers:\n  - name: 'team-X'\n"
  },
  {
    "path": "config/testdata/conf.http-config.good.yml",
    "content": "global:\n  slack_api_url: 'https://slack.com/webhook'\n  http_config:\n    follow_redirects: false\nroute:\n  receiver: team-X-slack\nreceivers:\n  - name: 'team-X-slack'\n    slack_configs:\n      - http_config:\n          proxy_url: foo\n"
  },
  {
    "path": "config/testdata/conf.mattermost-both-webhook-url-and-file.yml",
    "content": "global:\n  mattermost_webhook_url: https://mattermost.example.com/hooks/xxxxxxxxxxxxxxxxxxxxxxxxxx\n  mattermost_webhook_url_file: /global_file\nroute:\n  receiver: team-X-mattermost\n\nreceivers:\n  - name: team-X-mattermost\n    mattermost_configs:\n      - channel: team-X\n  - name: team-Y-mattermost\n    mattermost_configs:\n      - channel: team-Y\n        webhook_url: https://fakemattermost.example.com/hooks/xxxxxxxxxxxxxxxxxxxxxxxxxx\n"
  },
  {
    "path": "config/testdata/conf.mattermost-default-webhook-url-file.yml",
    "content": "global:\n  mattermost_webhook_url_file: /global_file\nroute:\n  receiver: team-X-mattermost\n\nreceivers:\n  - name: team-X-mattermost\n    mattermost_configs:\n      - channel: team-X\n  - name: team-Y-mattermost\n    mattermost_configs:\n      - channel: team-Y\n        webhook_url_file: /override_file\n"
  },
  {
    "path": "config/testdata/conf.mattermost-default-webhook-url.yml",
    "content": "global:\n  mattermost_webhook_url: https://mattermost.example.com/hooks/xxxxxxxxxxxxxxxxxxxxxxxxxx\nroute:\n  receiver: team-X-mattermost\n\nreceivers:\n  - name: team-X-mattermost\n    mattermost_configs:\n      - channel: team-X\n  - name: team-Y-mattermost\n    mattermost_configs:\n      - channel: team-Y\n        webhook_url: https://fakemattermost.example.com/hooks/xxxxxxxxxxxxxxxxxxxxxxxxxx\n"
  },
  {
    "path": "config/testdata/conf.mattermost-no-webhook-url.yml",
    "content": "route:\n  receiver: team-X-mattermost\n\nreceivers:\n  - name: team-X-mattermost\n    mattermost_configs:\n      - channel: team-X\n"
  },
  {
    "path": "config/testdata/conf.mattermost-valid-receiver-both-webhook-url-and-file.yml",
    "content": "global:\n  mattermost_webhook_url: https://mattermost.example.com/hooks/xxxxxxxxxxxxxxxxxxxxxxxxxx\n  mattermost_webhook_url_file: /global_file\nroute:\n  receiver: team-X-mattermost\n\nreceivers:\n  - name: team-X-mattermost\n    mattermost_configs:\n      - channel: team-X\n        webhook_url_file: /override_file\n"
  },
  {
    "path": "config/testdata/conf.nil-match_re-route.yml",
    "content": "route:\n  receiver: empty\n  routes:\n    - match_re:\n        invalid_label:\n      receiver: empty\nreceivers:\n  - name: empty\n"
  },
  {
    "path": "config/testdata/conf.nil-source_match_re-inhibition.yml",
    "content": "route:\n  receiver: empty\nreceivers:\n  - name: empty\ninhibit_rules:\n  - source_match_re:\n      invalid_source_label:\n    target_match_re:\n      severity: critical\n"
  },
  {
    "path": "config/testdata/conf.nil-target_match_re-inhibition.yml",
    "content": "route:\n  receiver: empty\nreceivers:\n  - name: empty\ninhibit_rules:\n  - source_match:\n      severity: critical\n    target_match_re:\n      invalid_target_label:\n"
  },
  {
    "path": "config/testdata/conf.opsgenie-both-file-and-apikey.yml",
    "content": "global:\n  opsgenie_api_key: asd132\n  opsgenie_api_key_file: '/global_file'\nroute:\n  group_by: ['alertname', 'cluster', 'service']\n  group_wait: 30s\n  group_interval: 5m\n  repeat_interval: 3h\n  receiver: escalation-Y-opsgenie\n  routes:\n    - match:\n        service: foo\n      receiver: team-X-opsgenie\nreceivers:\n  - name: 'team-X-opsgenie'\n    opsgenie_configs:\n      - responders:\n          - name: 'team-X'\n            type: 'team'\n  - name: 'escalation-Y-opsgenie'\n    opsgenie_configs:\n      - responders:\n          - name: 'escalation-Y'\n            type: 'escalation'\n        api_key: qwe456\n"
  },
  {
    "path": "config/testdata/conf.opsgenie-default-apikey-file.yml",
    "content": "global:\n  opsgenie_api_key_file: '/global_file'\nroute:\n  group_by: ['alertname', 'cluster', 'service']\n  group_wait: 30s\n  group_interval: 5m\n  repeat_interval: 3h\n  receiver: escalation-Y-opsgenie\n  routes:\n    - match:\n        service: foo\n      receiver: team-X-opsgenie\nreceivers:\n  - name: 'team-X-opsgenie'\n    opsgenie_configs:\n      - responders:\n          - name: 'team-X'\n            type: 'team'\n  - name: 'escalation-Y-opsgenie'\n    opsgenie_configs:\n      - responders:\n          - name: 'escalation-Y'\n            type: 'escalation'\n        api_key_file: /override_file\n"
  },
  {
    "path": "config/testdata/conf.opsgenie-default-apikey-old-team.yml",
    "content": "global:\n  opsgenie_api_key: asd132\nroute:\n  group_by: ['alertname', 'cluster', 'service']\n  group_wait: 30s\n  group_interval: 5m\n  repeat_interval: 3h\n  receiver: escalation-Y-opsgenie\n  routes:\n    - match:\n        service: foo\n      receiver: team-X-opsgenie\nreceivers:\n  - name: 'team-X-opsgenie'\n    opsgenie_configs:\n      - teams: 'team-X'\n"
  },
  {
    "path": "config/testdata/conf.opsgenie-default-apikey.yml",
    "content": "global:\n  opsgenie_api_key: asd132\nroute:\n  group_by: ['alertname', 'cluster', 'service']\n  group_wait: 30s\n  group_interval: 5m\n  repeat_interval: 3h\n  receiver: escalation-Y-opsgenie\n  routes:\n    - match:\n        service: foo\n      receiver: team-X-opsgenie\nreceivers:\n  - name: 'team-X-opsgenie'\n    opsgenie_configs:\n      - responders:\n          - name: 'team-X'\n            type: 'team'\n  - name: 'escalation-Y-opsgenie'\n    opsgenie_configs:\n      - responders:\n          - name: 'escalation-Y'\n            type: 'escalation'\n        api_key: qwe456\n"
  },
  {
    "path": "config/testdata/conf.opsgenie-no-apikey.yml",
    "content": "route:\n  group_by: ['alertname', 'cluster', 'service']\n  group_wait: 30s\n  group_interval: 5m\n  repeat_interval: 3h\n  receiver: team-X-opsgenie\n  routes:\n    - match:\n        service: foo\n      receiver: team-X-opsgenie\nreceivers:\n  - name: 'team-X-opsgenie'\n    opsgenie_configs:\n      - responders:\n          - name: 'team-X'\n            type: 'team'\n"
  },
  {
    "path": "config/testdata/conf.rocketchat-both-token-and-tokenfile.yml",
    "content": "global:\n  rocketchat_token_file: /global_file\n  rocketchat_token: token123\nroute:\n  group_by: ['alertname', 'cluster', 'service']\n  group_wait: 30s\n  group_interval: 5m\n  repeat_interval: 3h\n  receiver: team-Y-rocketchat\n  routes:\n    - match:\n        service: foo\n      receiver: team-X-rocketchat\nreceivers:\n  - name: 'team-X-rocketchat'\n    rocketchat_configs:\n      - channel: '#team-X'\n  - name: 'team-Y-rocketchat'\n    rocketchat_configs:\n      - channel: '#team-Y'\n"
  },
  {
    "path": "config/testdata/conf.rocketchat-both-tokenid-and-tokenidfile.yml",
    "content": "global:\n  rocketchat_token_id_file: /global_file\n  rocketchat_token_id: id123\nroute:\n  group_by: ['alertname', 'cluster', 'service']\n  group_wait: 30s\n  group_interval: 5m\n  repeat_interval: 3h\n  receiver: team-Y-rocketchat\n  routes:\n    - match:\n        service: foo\n      receiver: team-X-rocketchat\nreceivers:\n  - name: 'team-X-rocketchat'\n    rocketchat_configs:\n      - channel: '#team-X'\n  - name: 'team-Y-rocketchat'\n    rocketchat_configs:\n      - channel: '#team-Y'\n"
  },
  {
    "path": "config/testdata/conf.rocketchat-default-token-file.yml",
    "content": "global:\n  rocketchat_token_file: /global_file\n  rocketchat_token_id_file: /etc/alertmanager/rocketchat_token_id\nroute:\n  group_by: ['alertname', 'cluster', 'service']\n  group_wait: 30s\n  group_interval: 5m\n  repeat_interval: 3h\n  receiver: team-Y-rocketchat\n  routes:\n    - match:\n        service: foo\n      receiver: team-X-rocketchat\nreceivers:\n  - name: 'team-X-rocketchat'\n    rocketchat_configs:\n      - channel: '#team-X'\n  - name: 'team-Y-rocketchat'\n    rocketchat_configs:\n      - channel: '#team-Y'\n        token_file: /override_file\n        token_id_file: /override_file\n"
  },
  {
    "path": "config/testdata/conf.rocketchat-default-token.yml",
    "content": "global:\n  rocketchat_token: token123\n  rocketchat_token_id: id123\nroute:\n  group_by: ['alertname', 'cluster', 'service']\n  group_wait: 30s\n  group_interval: 5m\n  repeat_interval: 3h\n  receiver: team-Y-rocketchat\n  routes:\n    - match:\n        service: foo\n      receiver: team-X-rocketchat\nreceivers:\n  - name: 'team-X-rocketchat'\n    rocketchat_configs:\n      - channel: '#team-X'\n  - name: 'team-Y-rocketchat'\n    rocketchat_configs:\n      - channel: '#team-Y'\n        token: token456\n        token_id: id456\n"
  },
  {
    "path": "config/testdata/conf.rocketchat-no-token.yml",
    "content": "global:\n  rocketchat_token_id: id123\nroute:\n  group_by: ['alertname', 'cluster', 'service']\n  group_wait: 30s\n  group_interval: 5m\n  repeat_interval: 3h\n  receiver: team-Y-rocketchat\n  routes:\n    - match:\n        service: foo\n      receiver: team-X-rocketchat\nreceivers:\n  - name: 'team-X-rocketchat'\n    rocketchat_configs:\n      - channel: '#team-X'\n  - name: 'team-Y-rocketchat'\n    rocketchat_configs:\n      - channel: '#team-Y'\n"
  },
  {
    "path": "config/testdata/conf.slack-both-file-and-token.yml",
    "content": "global:\n  slack_app_token: 'xoxb-1234-abcdefgh'\n  slack_app_token_file: '/global_file'\nroute:\n  receiver: 'slack-notifications'\n  group_by: [alertname, datacenter, app]\nreceivers:\n  - name: 'slack-notifications'\n    slack_configs:\n      - channel: '#alerts1'\n        text: 'test'\n"
  },
  {
    "path": "config/testdata/conf.slack-both-file-and-url.yml",
    "content": "global:\n  slack_api_url: \"http://mysecret.example.com/\"\n  slack_api_url_file: '/global_file'\nroute:\n  receiver: 'slack-notifications'\n  group_by: [alertname, datacenter, app]\nreceivers:\n  - name: 'slack-notifications'\n    slack_configs:\n      - channel: '#alerts1'\n        text: 'test'\n"
  },
  {
    "path": "config/testdata/conf.slack-both-url-and-token.yml",
    "content": "global:\n  slack_app_token: 'xoxb-1234-abcdefgh'\n  slack_api_url: 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'\nroute:\n  receiver: 'slack-notifications'\n  group_by: [alertname, datacenter, app]\nreceivers:\n  - name: 'slack-notifications'\n    slack_configs:\n      - channel: '#alerts1'\n        text: 'test'\n"
  },
  {
    "path": "config/testdata/conf.slack-default-api-url-file.yml",
    "content": "global:\n  slack_api_url_file: '/global_file'\nroute:\n  receiver: 'slack-notifications'\n  group_by: [alertname, datacenter, app]\nreceivers:\n  - name: 'slack-notifications'\n    slack_configs:\n      # Use global\n      - channel: '#alerts1'\n        text: 'test'\n      # Override global with other file\n      - channel: '#alerts2'\n        text: 'test'\n        api_url_file: '/override_file'\n      # Override global with inline URL\n      - channel: '#alerts3'\n        text: 'test'\n        api_url: 'http://mysecret.example.com/'\n"
  },
  {
    "path": "config/testdata/conf.slack-default-app-token.yml",
    "content": "global:\n  slack_app_token: 'xoxb-1234-abcdefgh'\n  # old workaround to use bot tokens\n  slack_api_url: 'https://slack.com/api/chat.postMessage'\nroute:\n  receiver: 'slack-notifications'\n  group_by: [alertname, datacenter, app]\nreceivers:\n  - name: 'slack-notifications'\n    slack_configs:\n      # use global\n      - channel: '#alerts1'\n        text: 'test'\n      # use override\n      - channel: '#alerts2'\n        text: 'test'\n        app_token: 'xoxb-1234-xxxxxx'\n      # use custom app url\n      - channel: '#alerts3'\n        text: 'test'\n        app_url: http://api.fakeslack.example/\n      # use workaround to configure bot token\n      - channel: '#alerts4'\n        text: 'test'\n        http_config:\n          authorization:\n            credentials: 'xoxb-my-bot-token'\n      - api_url: 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'\n        send_resolved: true\n"
  },
  {
    "path": "config/testdata/conf.slack-no-api-url-or-token.yml",
    "content": "route:\n  receiver: 'slack-notifications'\n  group_by: [alertname, datacenter, app]\nreceivers:\n  - name: 'slack-notifications'\n    slack_configs:\n      - channel: '#alerts'\n        text: 'test'\n"
  },
  {
    "path": "config/testdata/conf.slack-update-message-and-webhook.yml",
    "content": "route:\n  receiver: 'slack-notifications'\n  group_by: [alertname]\nreceivers:\n  - name: 'slack-notifications'\n    slack_configs:\n      # use global\n      - channel: '#alerts1'\n        text: 'test'\n        send_resolved: true\n        # trying to use webhook urls with update_message\n        api_url: 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'\n        update_message: true\n"
  },
  {
    "path": "config/testdata/conf.smtp-both-password-and-file.yml",
    "content": "global:\n  smtp_smarthost: 'localhost:25'\n  smtp_from: 'alertmanager@example.org'\n  smtp_auth_username: 'alertmanager'\n  smtp_auth_password: \"multiline\\nmysecret\"\n  smtp_auth_password_file: \"/tmp/global\"\n  smtp_hello: \"host.example.org\"\nroute:\n  receiver: 'email-notifications'\nreceivers:\n  - name: 'email-notifications'\n    email_configs:\n      - to: 'one@example.org'\n"
  },
  {
    "path": "config/testdata/conf.smtp-no-username-or-password.yml",
    "content": "global:\n  smtp_smarthost: 'localhost:25'\n  smtp_from: 'alertmanager@example.org'\n  smtp_hello: \"host.example.org\"\nroute:\n  receiver: 'email-notifications'\nreceivers:\n  - name: 'email-notifications'\n    email_configs:\n      - to: 'one@example.org'\n"
  },
  {
    "path": "config/testdata/conf.smtp-password-global-and-local.yml",
    "content": "global:\n  smtp_smarthost: 'localhost:25'\n  smtp_from: 'alertmanager@example.org'\n  smtp_auth_username: 'globaluser'\n  smtp_auth_password_file: '/tmp/globaluserpassword'\n  smtp_hello: \"host.example.org\"\nroute:\n  receiver: 'email-notifications'\nreceivers:\n  - name: 'email-notifications'\n    email_configs:\n      # Use global\n      - to: 'one@example.org'\n      # Override global with other file\n      - to: 'two@example.org'\n        auth_username: 'localuser1'\n        auth_password_file: '/tmp/localuser1password'\n      # Override global with inline password\n      - to: 'three@example.org'\n        auth_username: 'localuser2'\n        auth_password: 'mysecret'\n      # Test auth secret\n      - to: 'four@exmaple.org'\n        auth_username: 'localuser3'\n        auth_secret: 'myprecious'\n      # Test auth secret from file\n      - to: 'five@exmaple.org'\n        auth_username: 'localuser4'\n        auth_secret_file: '/tmp/localuser4secret'\n"
  },
  {
    "path": "config/testdata/conf.sns-invalid.yml",
    "content": "route:\n  receiver: 'sns-api-notifications'\n  group_by: [alertname]\nreceivers:\n  - name: 'sns-api-notifications'\n    sns_configs:\n      - api_url: https://sns.us-east-2.amazonaws.com\n        sigv4:\n          region: us-east-2\n          access_key: access_key\n          secret_key: secret_ket\n        attributes:\n          severity: Sev2\n"
  },
  {
    "path": "config/testdata/conf.sns-topic-arn.yml",
    "content": "route:\n  receiver: 'sns-api-notifications'\n  group_by: [alertname]\nreceivers:\n  - name: 'sns-api-notifications'\n    sns_configs:\n      - api_url: https://sns.us-east-2.amazonaws.com\n        topic_arn: arn:aws:sns:us-east-2:123456789012:My-Topic\n        sigv4:\n          region: us-east-2\n          access_key: access_key\n          secret_key: secret_ket\n        attributes:\n          severity: Sev2\n"
  },
  {
    "path": "config/testdata/conf.telegram-both-bot-token-and-file.yml",
    "content": "global:\n  telegram_bot_token: asd132\n  telegram_bot_token_file: /global_file\nroute:\n  receiver: team-X-telegram\n\nreceivers:\n  - name: team-X-telegram\n    telegram_configs:\n      - chat_id: 123\n  - name: team-Y-telegram\n    telegram_configs:\n      - chat_id: 456\n        bot_token: qwe456\n"
  },
  {
    "path": "config/testdata/conf.telegram-default-bot-token-file.yml",
    "content": "global:\n  telegram_bot_token_file: /global_file\nroute:\n  receiver: team-X-telegram\n\nreceivers:\n  - name: team-X-telegram\n    telegram_configs:\n      - chat_id: 123\n  - name: team-Y-telegram\n    telegram_configs:\n      - chat_id: 456\n        bot_token_file: /override_file\n"
  },
  {
    "path": "config/testdata/conf.telegram-default-bot-token.yml",
    "content": "global:\n  telegram_bot_token: asd132\nroute:\n  receiver: team-X-telegram\n\nreceivers:\n  - name: team-X-telegram\n    telegram_configs:\n      - chat_id: 123\n  - name: team-Y-telegram\n    telegram_configs:\n      - chat_id: 456\n        bot_token: qwe456\n"
  },
  {
    "path": "config/testdata/conf.telegram-no-bot-token.yml",
    "content": "route:\n  receiver: team-X-telegram\n\nreceivers:\n  - name: team-X-telegram\n    telegram_configs:\n      - chat_id: 123\n"
  },
  {
    "path": "config/testdata/conf.telegram-valid-receiver-both-bot-token-and-file.yml",
    "content": "global:\n  telegram_bot_token: asd132\n  telegram_bot_token_file: /global_file\nroute:\n  receiver: team-X-telegram\n\nreceivers:\n  - name: team-X-telegram\n    telegram_configs:\n      - chat_id: 123\n        bot_token_file: /override_file\n"
  },
  {
    "path": "config/testdata/conf.victorops-both-file-and-apikey.yml",
    "content": "global:\n  victorops_api_key: asd132\n  victorops_api_key_file: '/global_file'\nroute:\n  group_by: ['alertname', 'cluster', 'service']\n  group_wait: 30s\n  group_interval: 5m\n  repeat_interval: 3h\n  receiver: team-Y-victorops\n  routes:\n    - match:\n        service: foo\n      receiver: team-X-victorops\nreceivers:\n  - name: 'team-X-victorops'\n    victorops_configs:\n      - routing_key: 'team-X'\n  - name: 'team-Y-victorops'\n    victorops_configs:\n      - routing_key: 'team-Y'\n        api_key: qwe456\n"
  },
  {
    "path": "config/testdata/conf.victorops-default-apikey-file.yml",
    "content": "global:\n  victorops_api_key_file: '/global_file'\nroute:\n  group_by: ['alertname', 'cluster', 'service']\n  group_wait: 30s\n  group_interval: 5m\n  repeat_interval: 3h\n  receiver: team-Y-victorops\n  routes:\n    - match:\n        service: foo\n      receiver: team-X-victorops\nreceivers:\n  - name: 'team-X-victorops'\n    victorops_configs:\n      - routing_key: 'team-X'\n  - name: 'team-Y-victorops'\n    victorops_configs:\n      - routing_key: 'team-Y'\n        api_key_file: /override_file\n"
  },
  {
    "path": "config/testdata/conf.victorops-default-apikey.yml",
    "content": "global:\n  victorops_api_key: asd132\nroute:\n  group_by: ['alertname', 'cluster', 'service']\n  group_wait: 30s\n  group_interval: 5m\n  repeat_interval: 3h\n  receiver: team-Y-victorops\n  routes:\n    - match:\n        service: foo\n      receiver: team-X-victorops\nreceivers:\n  - name: 'team-X-victorops'\n    victorops_configs:\n      - routing_key: 'team-X'\n  - name: 'team-Y-victorops'\n    victorops_configs:\n      - routing_key: 'team-Y'\n        api_key: qwe456\n"
  },
  {
    "path": "config/testdata/conf.victorops-no-apikey.yml",
    "content": "route:\n  group_by: ['alertname', 'cluster', 'service']\n  group_wait: 30s\n  group_interval: 5m\n  repeat_interval: 3h\n  receiver: team-X-victorops\n  routes:\n    - match:\n        service: foo\n      receiver: team-X-victorops\nreceivers:\n  - name: 'team-X-victorops'\n    victorops_configs:\n      - routing_key: 'team-X'\n"
  },
  {
    "path": "config/testdata/conf.wechat-both-file-and-secret.yml",
    "content": "global:\n  wechat_api_secret: \"http://mysecret.example.com/\"\n  wechat_api_secret_file: '/global_file'\nroute:\n  receiver: 'wechat-notifications'\n  group_by: [alertname, datacenter, app]\nreceivers:\n  - name: 'wechat-notifications'\n    wechat_configs:\n      - {}\n"
  },
  {
    "path": "config/testdata/conf.wechat-default-api-secret-file.yml",
    "content": "global:\n  wechat_api_secret_file: '/global_file'\n  wechat_api_corp_id: 'my_corp_id'\nroute:\n  receiver: 'wechat-notifications'\n  group_by: [alertname, datacenter, app]\nreceivers:\n  - name: 'wechat-notifications'\n    wechat_configs:\n      # Use global\n      - {}\n      # Override global with other file\n      - api_secret_file: '/override_file'\n      # Override global with inline API secret\n      - api_secret: 'my_inline_secret'\n"
  },
  {
    "path": "config/testdata/conf.wechat-no-api-secret.yml",
    "content": "route:\n  receiver: 'wechat-notifications'\n  group_by: [alertname, datacenter, app]\nreceivers:\n  - name: 'wechat-notifications'\n    wechat_configs:\n      - {}\n"
  },
  {
    "path": "dispatch/dispatch.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage dispatch\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"runtime\"\n\t\"sort\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n\t\"github.com/prometheus/common/model\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/codes\"\n\t\"go.opentelemetry.io/otel/propagation\"\n\t\"go.opentelemetry.io/otel/trace\"\n\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/provider\"\n\t\"github.com/prometheus/alertmanager/store\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nconst (\n\tDispatcherStateUnknown = iota\n\tDispatcherStateWaitingToStart\n\tDispatcherStateRunning\n\tDispatcherStateStopped\n)\n\nvar tracer = otel.Tracer(\"github.com/prometheus/alertmanager/dispatch\")\n\n// DispatcherMetrics represents metrics associated to a dispatcher.\ntype DispatcherMetrics struct {\n\taggrGroups            prometheus.Gauge\n\tprocessingDuration    prometheus.Summary\n\taggrGroupLimitReached prometheus.Counter\n}\n\n// NewDispatcherMetrics returns a new registered DispatchMetrics.\nfunc NewDispatcherMetrics(registerLimitMetrics bool, r prometheus.Registerer) *DispatcherMetrics {\n\tif r == nil {\n\t\treturn nil\n\t}\n\tm := DispatcherMetrics{\n\t\taggrGroups: promauto.With(r).NewGauge(\n\t\t\tprometheus.GaugeOpts{\n\t\t\t\tName: \"alertmanager_dispatcher_aggregation_groups\",\n\t\t\t\tHelp: \"Number of active aggregation groups\",\n\t\t\t},\n\t\t),\n\t\tprocessingDuration: promauto.With(r).NewSummary(\n\t\t\tprometheus.SummaryOpts{\n\t\t\t\tName: \"alertmanager_dispatcher_alert_processing_duration_seconds\",\n\t\t\t\tHelp: \"Summary of latencies for the processing of alerts.\",\n\t\t\t},\n\t\t),\n\t\taggrGroupLimitReached: promauto.With(r).NewCounter(\n\t\t\tprometheus.CounterOpts{\n\t\t\t\tName: \"alertmanager_dispatcher_aggregation_group_limit_reached_total\",\n\t\t\t\tHelp: \"Number of times when dispatcher failed to create new aggregation group due to limit.\",\n\t\t\t},\n\t\t),\n\t}\n\n\treturn &m\n}\n\n// Dispatcher sorts incoming alerts into aggregation groups and\n// assigns the correct notifiers to each.\ntype Dispatcher struct {\n\troute      *Route\n\talerts     provider.Alerts\n\tstage      notify.Stage\n\tmarker     types.GroupMarker\n\tmetrics    *DispatcherMetrics\n\tlimits     Limits\n\tpropagator propagation.TextMapPropagator\n\n\ttimeout func(time.Duration) time.Duration\n\n\tloaded   chan struct{}\n\tfinished sync.WaitGroup\n\tctx      context.Context\n\tcancel   func()\n\n\trouteGroupsSlice []routeAggrGroups\n\taggrGroupsNum    atomic.Int32\n\n\tmaintenanceInterval time.Duration\n\tconcurrency         int // Number of goroutines for alert ingestion\n\n\tlogger *slog.Logger\n\n\tstartTimer *time.Timer\n\tstate      atomic.Int32\n}\n\n// Limits describes limits used by Dispatcher.\ntype Limits interface {\n\t// MaxNumberOfAggregationGroups returns max number of aggregation groups that dispatcher can have.\n\t// 0 or negative value = unlimited.\n\t// If dispatcher hits this limit, it will not create additional groups, but will log an error instead.\n\tMaxNumberOfAggregationGroups() int\n}\n\ntype routeAggrGroups struct {\n\troute     *Route\n\tgroups    sync.Map // map[string]*aggrGroup\n\tgroupsLen atomic.Int64\n}\n\n// NewDispatcher returns a new Dispatcher.\nfunc NewDispatcher(\n\talerts provider.Alerts,\n\troute *Route,\n\tstage notify.Stage,\n\tmarker types.GroupMarker,\n\ttimeout func(time.Duration) time.Duration,\n\tmaintenanceInterval time.Duration,\n\tlimits Limits,\n\tlogger *slog.Logger,\n\tmetrics *DispatcherMetrics,\n) *Dispatcher {\n\tif limits == nil {\n\t\tlimits = nilLimits{}\n\t}\n\n\t// Calculate concurrency for ingestion.\n\tconcurrency := min(max(runtime.GOMAXPROCS(0)/2, 2), 8)\n\n\tdisp := &Dispatcher{\n\t\talerts:              alerts,\n\t\tstage:               stage,\n\t\troute:               route,\n\t\tmarker:              marker,\n\t\ttimeout:             timeout,\n\t\tmaintenanceInterval: maintenanceInterval,\n\t\tconcurrency:         concurrency,\n\t\tlogger:              logger.With(\"component\", \"dispatcher\"),\n\t\tmetrics:             metrics,\n\t\tlimits:              limits,\n\t\tpropagator:          otel.GetTextMapPropagator(),\n\t}\n\tdisp.state.Store(DispatcherStateUnknown)\n\tdisp.loaded = make(chan struct{})\n\tdisp.ctx, disp.cancel = context.WithCancel(context.Background())\n\treturn disp\n}\n\n// Run starts dispatching alerts incoming via the updates channel.\nfunc (d *Dispatcher) Run(dispatchStartTime time.Time) {\n\tif !d.state.CompareAndSwap(DispatcherStateUnknown, DispatcherStateWaitingToStart) {\n\t\treturn\n\t}\n\td.finished.Add(1)\n\tdefer d.finished.Done()\n\n\td.logger.Debug(\"preparing to start\", \"startTime\", dispatchStartTime)\n\td.startTimer = time.NewTimer(time.Until(dispatchStartTime))\n\td.logger.Debug(\"setting state\", \"state\", \"waiting_to_start\")\n\td.routeGroupsSlice = make([]routeAggrGroups, d.route.Idx+1)\n\td.route.Walk(func(r *Route) {\n\t\td.routeGroupsSlice[r.Idx] = routeAggrGroups{\n\t\t\troute: r,\n\t\t}\n\t})\n\n\td.aggrGroupsNum.Store(0)\n\td.metrics.aggrGroups.Set(0)\n\n\tinitalAlerts, it := d.alerts.SlurpAndSubscribe(\"dispatcher\")\n\tfor _, alert := range initalAlerts {\n\t\td.routeAlert(d.ctx, alert)\n\t}\n\tclose(d.loaded)\n\n\td.run(it)\n}\n\nfunc (d *Dispatcher) run(it provider.AlertIterator) {\n\tdefer it.Close()\n\n\t// Start maintenance goroutine\n\td.finished.Go(func() {\n\t\tticker := time.NewTicker(d.maintenanceInterval)\n\t\tdefer ticker.Stop()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ticker.C:\n\t\t\t\td.doMaintenance()\n\t\t\tcase <-d.ctx.Done():\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t})\n\n\t// Start timer goroutine\n\td.finished.Go(func() {\n\t\t<-d.startTimer.C\n\n\t\tif d.state.CompareAndSwap(DispatcherStateWaitingToStart, DispatcherStateRunning) {\n\t\t\td.logger.Debug(\"started\", \"state\", \"running\")\n\t\t\td.logger.Debug(\"Starting all existing aggregation groups\")\n\t\t\tfor rg := range d.routeGroupsSlice {\n\t\t\t\td.routeGroupsSlice[rg].groups.Range(func(_, ag any) bool {\n\t\t\t\t\td.runAG(ag.(*aggrGroup))\n\t\t\t\t\treturn true\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t})\n\n\t// Start multiple alert ingestion goroutines\n\talertCh := it.Next()\n\tfor i := 0; i < d.concurrency; i++ {\n\t\td.finished.Add(1)\n\t\tgo func(workerID int) {\n\t\t\tdefer d.finished.Done()\n\t\t\td.logger.Debug(\"starting alert ingestion worker\", \"workerID\", workerID)\n\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase alert, ok := <-alertCh:\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\t// Iterator exhausted for some reason.\n\t\t\t\t\t\tif err := it.Err(); err != nil {\n\t\t\t\t\t\t\td.logger.Error(\"Error on alert update\", \"err\", err, \"workerID\", workerID)\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\t// Log errors but keep trying.\n\t\t\t\t\tif err := it.Err(); err != nil {\n\t\t\t\t\t\td.logger.Error(\"Error on alert update\", \"err\", err, \"workerID\", workerID)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tctx := d.ctx\n\t\t\t\t\tif alert.Header != nil {\n\t\t\t\t\t\tctx = d.propagator.Extract(ctx, propagation.MapCarrier(alert.Header))\n\t\t\t\t\t}\n\n\t\t\t\t\td.routeAlert(ctx, alert.Data)\n\n\t\t\t\tcase <-d.ctx.Done():\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}(i)\n\t}\n\t<-d.ctx.Done()\n}\n\nfunc (d *Dispatcher) routeAlert(ctx context.Context, alert *types.Alert) {\n\td.logger.Debug(\"Received alert\", \"alert\", alert)\n\n\tctx, span := tracer.Start(ctx, \"dispatch.Dispatcher.routeAlert\",\n\t\ttrace.WithAttributes(\n\t\t\tattribute.String(\"alerting.alert.name\", alert.Name()),\n\t\t\tattribute.String(\"alerting.alert.fingerprint\", alert.Fingerprint().String()),\n\t\t),\n\t\ttrace.WithSpanKind(trace.SpanKindInternal),\n\t)\n\tdefer span.End()\n\n\tnow := time.Now()\n\tfor _, r := range d.route.Match(alert.Labels) {\n\t\tspan.AddEvent(\"dispatching alert to route\",\n\t\t\ttrace.WithAttributes(\n\t\t\t\tattribute.String(\"alerting.route.receiver.name\", r.RouteOpts.Receiver),\n\t\t\t),\n\t\t)\n\t\td.groupAlert(ctx, alert, r)\n\t}\n\td.metrics.processingDuration.Observe(time.Since(now).Seconds())\n}\n\nfunc (d *Dispatcher) doMaintenance() {\n\tfor i := range d.routeGroupsSlice {\n\t\td.routeGroupsSlice[i].groups.Range(func(_, el any) bool {\n\t\t\tag := el.(*aggrGroup)\n\t\t\tif ag.destroyed() {\n\t\t\t\tag.stop()\n\t\t\t\td.marker.DeleteByGroupKey(ag.routeID, ag.GroupKey())\n\t\t\t\tdeleted := d.routeGroupsSlice[i].groups.CompareAndDelete(ag.fingerprint(), ag)\n\t\t\t\tif deleted {\n\t\t\t\t\td.routeGroupsSlice[i].groupsLen.Add(-1)\n\t\t\t\t\td.aggrGroupsNum.Add(-1)\n\t\t\t\t\td.metrics.aggrGroups.Set(float64(d.aggrGroupsNum.Load()))\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\t}\n}\n\nfunc (d *Dispatcher) WaitForLoading() {\n\t<-d.loaded\n}\n\nfunc (d *Dispatcher) LoadingDone() <-chan struct{} {\n\treturn d.loaded\n}\n\n// AlertGroup represents how alerts exist within an aggrGroup.\ntype AlertGroup struct {\n\tAlerts   types.AlertSlice\n\tLabels   model.LabelSet\n\tReceiver string\n\tGroupKey string\n\tRouteID  string\n}\n\ntype AlertGroups []*AlertGroup\n\nfunc (ag AlertGroups) Swap(i, j int) { ag[i], ag[j] = ag[j], ag[i] }\nfunc (ag AlertGroups) Less(i, j int) bool {\n\tif ag[i].Labels.Equal(ag[j].Labels) {\n\t\treturn ag[i].Receiver < ag[j].Receiver\n\t}\n\treturn ag[i].Labels.Before(ag[j].Labels)\n}\nfunc (ag AlertGroups) Len() int { return len(ag) }\n\n// Groups returns a slice of AlertGroups from the dispatcher's internal state.\nfunc (d *Dispatcher) Groups(ctx context.Context, routeFilter func(*Route) bool, alertFilter func(*types.Alert, time.Time) bool) (AlertGroups, map[model.Fingerprint][]string, error) {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn nil, nil, ctx.Err()\n\tcase <-d.LoadingDone():\n\t}\n\tgroups := AlertGroups{}\n\n\t// Keep a list of receivers for an alert to prevent checking each alert\n\t// again against all routes. The alert has already matched against this\n\t// route on ingestion.\n\treceivers := map[model.Fingerprint][]string{}\n\n\tnow := time.Now()\n\tfor i := range d.routeGroupsSlice {\n\t\tif !routeFilter(d.routeGroupsSlice[i].route) {\n\t\t\tcontinue\n\t\t}\n\t\treceiver := d.routeGroupsSlice[i].route.RouteOpts.Receiver\n\n\t\t// Make a snapshot of the aggregation groups in each route to avoid holding\n\t\t// sync.Map locks while we process alerts or acquiring leaf locks in the alert\n\t\t// store.\n\n\t\t// Estimate capacity based on total groups and number of routes.\n\t\t// We overallocate a bit to avoid copying in most cases.\n\t\tsnapshot := make([]*aggrGroup, 0, d.routeGroupsSlice[i].groupsLen.Load()+32)\n\t\td.routeGroupsSlice[i].groups.Range(func(_, el any) bool {\n\t\t\tsnapshot = append(snapshot, el.(*aggrGroup))\n\t\t\treturn true\n\t\t})\n\n\t\t// Process the snapshot without holding sync.Map locks\n\t\tfor _, ag := range snapshot {\n\t\t\talertGroup := &AlertGroup{\n\t\t\t\tLabels:   ag.labels,\n\t\t\t\tReceiver: receiver,\n\t\t\t\tGroupKey: ag.GroupKey(),\n\t\t\t\tRouteID:  ag.routeID,\n\t\t\t}\n\n\t\t\talerts := ag.alerts.List()\n\t\t\tfilteredAlerts := make([]*types.Alert, 0, len(alerts))\n\t\t\tfor _, a := range alerts {\n\t\t\t\tif !alertFilter(a, now) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tfp := a.Fingerprint()\n\t\t\t\tif r, ok := receivers[fp]; ok {\n\t\t\t\t\t// Receivers slice already exists. Add\n\t\t\t\t\t// the current receiver to the slice.\n\t\t\t\t\treceivers[fp] = append(r, receiver)\n\t\t\t\t} else {\n\t\t\t\t\t// First time we've seen this alert fingerprint.\n\t\t\t\t\t// Initialize a new receivers slice.\n\t\t\t\t\treceivers[fp] = []string{receiver}\n\t\t\t\t}\n\n\t\t\t\tfilteredAlerts = append(filteredAlerts, a)\n\t\t\t}\n\t\t\tif len(filteredAlerts) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\talertGroup.Alerts = filteredAlerts\n\n\t\t\tgroups = append(groups, alertGroup)\n\t\t}\n\t}\n\tsort.Sort(groups)\n\tfor i := range groups {\n\t\tsort.Sort(groups[i].Alerts)\n\t}\n\tfor i := range receivers {\n\t\tsort.Strings(receivers[i])\n\t}\n\n\treturn groups, receivers, nil\n}\n\n// Stop the dispatcher.\nfunc (d *Dispatcher) Stop() {\n\tif d == nil {\n\t\treturn\n\t}\n\td.state.Store(DispatcherStateStopped)\n\td.cancel()\n\td.finished.Wait()\n}\n\n// notifyFunc is a function that performs notification for the alert\n// with the given fingerprint. It aborts on context cancelation.\n// Returns false if notifying failed.\ntype notifyFunc func(context.Context, ...*types.Alert) bool\n\n// groupAlert determines in which aggregation group the alert falls\n// and inserts it.\nfunc (d *Dispatcher) groupAlert(ctx context.Context, alert *types.Alert, route *Route) {\n\t_, span := tracer.Start(ctx, \"dispatch.Dispatcher.groupAlert\",\n\t\ttrace.WithAttributes(\n\t\t\tattribute.String(\"alerting.alert.name\", alert.Name()),\n\t\t\tattribute.String(\"alerting.alert.fingerprint\", alert.Fingerprint().String()),\n\t\t\tattribute.String(\"alerting.route.receiver.name\", route.RouteOpts.Receiver),\n\t\t),\n\t\ttrace.WithSpanKind(trace.SpanKindInternal),\n\t)\n\tdefer span.End()\n\n\tnow := time.Now()\n\tgroupLabels := getGroupLabels(alert, route)\n\n\tfp := groupLabels.Fingerprint()\n\n\tel, loaded := d.routeGroupsSlice[route.Idx].groups.Load(fp)\n\tif loaded {\n\t\tag := el.(*aggrGroup)\n\t\t// Try to insert into the aggrgroup.\n\t\t// If it's destroyed insert will return false.\n\t\tif ag.insert(ctx, alert) {\n\t\t\treturn\n\t\t}\n\t}\n\n\t// If we couldn't insert, we need to create a new aggregation group.\n\t// Since multiple goroutines might be trying to create the same group concurrently\n\t// we will use the sync map swap to ensure only one of them creates it.\n\n\t// If the group does not exist, create it. But check the limit first.\n\tlimit := d.limits.MaxNumberOfAggregationGroups()\n\tcurrent := int(d.aggrGroupsNum.Load())\n\tif limit > 0 && current >= limit {\n\t\td.metrics.aggrGroupLimitReached.Inc()\n\t\terr := errors.New(\"too many aggregation groups, cannot create new group for alert\")\n\t\tmessage := \"Failed to create aggregation group\"\n\t\td.logger.Error(message, \"err\", err.Error(), \"groups\", current, \"limit\", limit, \"alert\", alert.Name())\n\t\tspan.SetStatus(codes.Error, message)\n\t\tspan.RecordError(err,\n\t\t\ttrace.WithAttributes(\n\t\t\t\tattribute.Int(\"alerting.aggregation_group.count\", current),\n\t\t\t\tattribute.Int(\"alerting.aggregation_group.limit\", limit),\n\t\t\t),\n\t\t)\n\t\treturn\n\t}\n\n\tag := newAggrGroup(d.ctx, groupLabels, route, d.timeout, d.marker.(types.AlertMarker), d.logger)\n\t// Insert the 1st alert in the group before starting the group's run()\n\t// function, to make sure that when the run() will be executed the 1st\n\t// alert is already there.\n\tag.insert(ctx, alert)\n\n\tretries := 0\n\tfor {\n\t\tif loaded {\n\t\t\t// Try to store the new group in the map. If another goroutine has already created the same group, use the existing one.\n\t\t\tswapped := d.routeGroupsSlice[route.Idx].groups.CompareAndSwap(fp, el, ag)\n\t\t\tif swapped {\n\t\t\t\t// We swapped the new group in, we can break and start it.\n\t\t\t\tbreak\n\t\t\t}\n\t\t} else {\n\t\t\tel, loaded = d.routeGroupsSlice[route.Idx].groups.LoadOrStore(fp, ag)\n\t\t\tif !loaded {\n\t\t\t\td.routeGroupsSlice[route.Idx].groupsLen.Add(1)\n\t\t\t\td.aggrGroupsNum.Add(1)\n\t\t\t\td.metrics.aggrGroups.Set(float64(d.aggrGroupsNum.Load()))\n\t\t\t\t// We stored the new group, we can break and start it.\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif el == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// we found an existing group, try to insert the alert into it. If it's destroyed, we will retry the whole process with the updated el.\n\t\t\tagExisting := el.(*aggrGroup)\n\t\t\tif agExisting.insert(ctx, alert) {\n\t\t\t\treturn // if we inserted we return to avoid incrementing the aggrgroup count and starting the group.\n\t\t\t}\n\t\t}\n\n\t\t// If we failed to swap, it means another goroutine has created/modified the group\n\t\tretries++\n\t\tif retries > 100 {\n\t\t\t// This shouldn't happen - indicates a bug or extreme contention\n\t\t\td.logger.Error(\"excessive retries creating aggregation group\",\n\t\t\t\t\"fingerprint\", fp,\n\t\t\t\t\"route\", route.Key(),\n\t\t\t\t\"alert\", alert.Name(),\n\t\t\t\t\"retries\", retries,\n\t\t\t)\n\t\t\t// Give up and accept potential alert loss rather than infinite loop\n\t\t\treturn\n\t\t}\n\t}\n\n\tspan.AddEvent(\"new AggregationGroup created\",\n\t\ttrace.WithAttributes(\n\t\t\tattribute.String(\"alerting.aggregation_group.key\", ag.GroupKey()),\n\t\t\tattribute.Int(\"alerting.aggregation_group.count\", int(d.aggrGroupsNum.Load())),\n\t\t),\n\t)\n\n\tif alert.StartsAt.Add(ag.opts.GroupWait).Before(now) {\n\t\tmessage := \"Alert is old enough for immediate flush, resetting timer to zero\"\n\t\tag.logger.Debug(message, \"alert\", alert.Name(), \"fingerprint\", alert.Fingerprint(), \"startsAt\", alert.StartsAt)\n\t\tspan.AddEvent(message,\n\t\t\ttrace.WithAttributes(\n\t\t\t\tattribute.String(\"alerting.alert.StartsAt\", alert.StartsAt.Format(time.RFC3339)),\n\t\t\t),\n\t\t)\n\t\tag.resetTimer(0)\n\t}\n\t// Check dispatcher and alert state to determine if we should run the AG now.\n\tswitch d.state.Load() {\n\tcase DispatcherStateWaitingToStart:\n\t\tspan.AddEvent(\"Not starting Aggregation Group, dispatcher is not running\")\n\t\td.logger.Debug(\"Dispatcher still waiting to start\")\n\tcase DispatcherStateRunning:\n\t\tspan.AddEvent(\"Starting Aggregation Group\")\n\t\td.runAG(ag)\n\tdefault:\n\t\td.logger.Warn(\"unknown state detected\", \"state\", \"unknown\")\n\t}\n}\n\nfunc (d *Dispatcher) runAG(ag *aggrGroup) {\n\tif !ag.running.CompareAndSwap(false, true) {\n\t\treturn // already running\n\t}\n\tgo ag.run(func(ctx context.Context, alerts ...*types.Alert) bool {\n\t\t_, _, err := d.stage.Exec(ctx, d.logger, alerts...)\n\t\tif err != nil {\n\t\t\tlogger := d.logger.With(\"aggrGroup\", ag.GroupKey(), \"num_alerts\", len(alerts), \"err\", err)\n\t\t\tif errors.Is(ctx.Err(), context.Canceled) {\n\t\t\t\t// It is expected for the context to be canceled on\n\t\t\t\t// configuration reload or shutdown. In this case, the\n\t\t\t\t// message should only be logged at the debug level.\n\t\t\t\tlogger.Debug(\"Notify for alerts failed\")\n\t\t\t} else {\n\t\t\t\tlogger.Error(\"Notify for alerts failed\")\n\t\t\t}\n\t\t}\n\t\treturn err == nil\n\t})\n}\n\nfunc getGroupLabels(alert *types.Alert, route *Route) model.LabelSet {\n\tcapacity := len(route.RouteOpts.GroupBy)\n\tif route.RouteOpts.GroupByAll {\n\t\tcapacity = len(alert.Labels)\n\t}\n\tgroupLabels := make(model.LabelSet, capacity)\n\tfor ln, lv := range alert.Labels {\n\t\tif _, ok := route.RouteOpts.GroupBy[ln]; ok || route.RouteOpts.GroupByAll {\n\t\t\tgroupLabels[ln] = lv\n\t\t}\n\t}\n\n\treturn groupLabels\n}\n\n// aggrGroup aggregates alert fingerprints into groups to which a\n// common set of routing options applies.\n// It emits notifications in the specified intervals.\ntype aggrGroup struct {\n\tlabels   model.LabelSet\n\topts     *RouteOpts\n\tlogger   *slog.Logger\n\trouteID  string\n\trouteKey string\n\n\talerts  *store.Alerts\n\tmarker  types.AlertMarker\n\tctx     context.Context\n\tcancel  func()\n\tdone    chan struct{}\n\tnext    *time.Timer\n\ttimeout func(time.Duration) time.Duration\n\trunning atomic.Bool\n}\n\n// newAggrGroup returns a new aggregation group.\nfunc newAggrGroup(\n\tctx context.Context,\n\tlabels model.LabelSet,\n\tr *Route,\n\tto func(time.Duration) time.Duration,\n\tmarker types.AlertMarker,\n\tlogger *slog.Logger,\n) *aggrGroup {\n\tif to == nil {\n\t\tto = func(d time.Duration) time.Duration { return d }\n\t}\n\tag := &aggrGroup{\n\t\tlabels:   labels,\n\t\trouteID:  r.ID(),\n\t\trouteKey: r.Key(),\n\t\topts:     &r.RouteOpts,\n\t\ttimeout:  to,\n\t\talerts:   store.NewAlerts(),\n\t\tmarker:   marker,\n\t\tdone:     make(chan struct{}),\n\t}\n\tag.ctx, ag.cancel = context.WithCancel(ctx)\n\n\tag.logger = logger.With(\"aggrGroup\", ag)\n\n\t// Set an initial one-time wait before flushing\n\t// the first batch of notifications.\n\tag.next = time.NewTimer(ag.opts.GroupWait)\n\n\treturn ag\n}\n\nfunc (ag *aggrGroup) fingerprint() model.Fingerprint {\n\treturn ag.labels.Fingerprint()\n}\n\nfunc (ag *aggrGroup) GroupKey() string {\n\treturn fmt.Sprintf(\"%s:%s\", ag.routeKey, ag.labels)\n}\n\nfunc (ag *aggrGroup) String() string {\n\treturn ag.GroupKey()\n}\n\nfunc (ag *aggrGroup) run(nf notifyFunc) {\n\tdefer close(ag.done)\n\tdefer ag.next.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase now := <-ag.next.C:\n\t\t\t// Give the notifications time until the next flush to\n\t\t\t// finish before terminating them.\n\t\t\tctx, cancel := context.WithTimeout(ag.ctx, ag.timeout(ag.opts.GroupInterval))\n\n\t\t\t// The now time we retrieve from the ticker is the only reliable\n\t\t\t// point of time reference for the subsequent notification pipeline.\n\t\t\t// Calculating the current time directly is prone to flaky behavior,\n\t\t\t// which usually only becomes apparent in tests.\n\t\t\tctx = notify.WithNow(ctx, now)\n\n\t\t\t// Populate context with information needed along the pipeline.\n\t\t\tctx = notify.WithGroupKey(ctx, ag.GroupKey())\n\t\t\tctx = notify.WithGroupLabels(ctx, ag.labels)\n\t\t\tctx = notify.WithReceiverName(ctx, ag.opts.Receiver)\n\t\t\tctx = notify.WithRepeatInterval(ctx, ag.opts.RepeatInterval)\n\t\t\tctx = notify.WithMuteTimeIntervals(ctx, ag.opts.MuteTimeIntervals)\n\t\t\tctx = notify.WithActiveTimeIntervals(ctx, ag.opts.ActiveTimeIntervals)\n\t\t\tctx = notify.WithRouteID(ctx, ag.routeID)\n\n\t\t\t// Wait the configured interval before calling flush again.\n\t\t\tag.resetTimer(ag.opts.GroupInterval)\n\n\t\t\tag.flush(func(alerts ...*types.Alert) bool {\n\t\t\t\tctx, span := tracer.Start(ctx, \"dispatch.AggregationGroup.flush\",\n\t\t\t\t\ttrace.WithAttributes(\n\t\t\t\t\t\tattribute.String(\"alerting.aggregation_group.key\", ag.GroupKey()),\n\t\t\t\t\t\tattribute.Int(\"alerting.alerts.count\", len(alerts)),\n\t\t\t\t\t),\n\t\t\t\t\ttrace.WithSpanKind(trace.SpanKindInternal),\n\t\t\t\t)\n\t\t\t\tdefer span.End()\n\n\t\t\t\tsuccess := nf(ctx, alerts...)\n\t\t\t\tif !success {\n\t\t\t\t\tspan.SetStatus(codes.Error, \"notification failed\")\n\t\t\t\t}\n\t\t\t\treturn success\n\t\t\t})\n\n\t\t\tcancel()\n\n\t\tcase <-ag.ctx.Done():\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (ag *aggrGroup) stop() {\n\t// Calling cancel will terminate all in-process notifications\n\t// and the run() loop.\n\tag.cancel()\n\t<-ag.done\n}\n\n// resetTimer resets the timer for the AG.\nfunc (ag *aggrGroup) resetTimer(t time.Duration) {\n\tag.next.Reset(t)\n}\n\n// insert inserts the alert into the aggregation group.\n// Returns false if the aggregation group has been destroyed.\nfunc (ag *aggrGroup) insert(ctx context.Context, alert *types.Alert) bool {\n\t_, span := tracer.Start(ctx, \"dispatch.AggregationGroup.insert\",\n\t\ttrace.WithAttributes(\n\t\t\tattribute.String(\"alerting.alert.name\", alert.Name()),\n\t\t\tattribute.String(\"alerting.alert.fingerprint\", alert.Fingerprint().String()),\n\t\t\tattribute.String(\"alerting.aggregation_group.key\", ag.GroupKey()),\n\t\t),\n\t\ttrace.WithSpanKind(trace.SpanKindInternal),\n\t)\n\tdefer span.End()\n\tif err := ag.alerts.Set(alert); err != nil {\n\t\tif errors.Is(err, store.ErrDestroyed) {\n\t\t\treturn false\n\t\t}\n\t\tmessage := \"error on set alert\"\n\t\tspan.SetStatus(codes.Error, message)\n\t\tspan.RecordError(err)\n\t\tag.logger.Error(message, \"err\", err)\n\t}\n\treturn true\n}\n\nfunc (ag *aggrGroup) empty() bool {\n\treturn ag.alerts.Empty()\n}\n\nfunc (ag *aggrGroup) destroyed() bool {\n\treturn ag.alerts.Destroyed()\n}\n\n// flush sends notifications for all new alerts.\nfunc (ag *aggrGroup) flush(notify func(...*types.Alert) bool) {\n\tif ag.empty() {\n\t\treturn\n\t}\n\n\tvar (\n\t\talerts        = ag.alerts.List()\n\t\talertsSlice   = make(types.AlertSlice, 0, len(alerts))\n\t\tresolvedSlice = make(types.AlertSlice, 0, len(alerts))\n\t\tnow           = time.Now()\n\t)\n\tfor _, alert := range alerts {\n\t\ta := *alert\n\t\t// Ensure that alerts don't resolve as time move forwards.\n\t\tif a.ResolvedAt(now) {\n\t\t\tresolvedSlice = append(resolvedSlice, &a)\n\t\t} else {\n\t\t\ta.EndsAt = time.Time{}\n\t\t}\n\t\talertsSlice = append(alertsSlice, &a)\n\t}\n\tsort.Stable(alertsSlice)\n\n\tag.logger.Debug(\"flushing\", \"alerts\", fmt.Sprintf(\"%v\", alertsSlice))\n\n\tif notify(alertsSlice...) {\n\t\t// Delete all resolved alerts as we just sent a notification for them,\n\t\t// and we don't want to send another one. However, we need to make sure\n\t\t// that each resolved alert has not fired again during the flush as then\n\t\t// we would delete an active alert thinking it was resolved.\n\t\t// Since we are passing DestroyIfEmpty=true the group will be marked as\n\t\t// destroyed if there are no more alerts after the deletion.\n\t\tif err := ag.alerts.DeleteIfNotModified(resolvedSlice, true); err != nil {\n\t\t\tag.logger.Error(\"error on delete alerts\", \"err\", err)\n\t\t} else {\n\t\t\t// Delete markers for resolved alerts that are not in the store.\n\t\t\tfor _, alert := range resolvedSlice {\n\t\t\t\t_, err := ag.alerts.Get(alert.Fingerprint())\n\t\t\t\tif errors.Is(err, store.ErrNotFound) {\n\t\t\t\t\tag.marker.Delete(alert.Fingerprint())\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\ntype nilLimits struct{}\n\nfunc (n nilLimits) MaxNumberOfAggregationGroups() int { return 0 }\n"
  },
  {
    "path": "dispatch/dispatch_bench_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage dispatch\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/provider/mem\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// buildDeepRouteTree creates a multi-level hierarchical route tree:\n// - numTeams routes at top level\n// - Each team has numClusters cluster sub-routes\n// - Each cluster has numPriorities priority sub-routes\n// Total: numTeams * numClusters * numPriorities leaf routes with 3 levels of depth.\nfunc buildDeepRouteTree(numTeams, numClusters, numPriorities int) *Route {\n\tgroupWait := model.Duration(30 * time.Second)\n\tgroupInterval := model.Duration(5 * time.Minute)\n\trepeatInterval := model.Duration(4 * time.Hour)\n\n\troot := &config.Route{\n\t\tReceiver:       \"default\",\n\t\tGroupBy:        []model.LabelName{\"alertname\"},\n\t\tGroupWait:      &groupWait,\n\t\tGroupInterval:  &groupInterval,\n\t\tRepeatInterval: &repeatInterval,\n\t}\n\n\t// Create team routes, each with cluster sub-routes, each with priority sub-routes\n\troot.Routes = make([]*config.Route, 0, numTeams)\n\tfor i := range numTeams {\n\t\tteamRoute := &config.Route{\n\t\t\tReceiver:       fmt.Sprintf(\"team-%d-default\", i),\n\t\t\tMatch:          map[string]string{\"team\": fmt.Sprintf(\"team-%d\", i)},\n\t\t\tGroupBy:        []model.LabelName{\"alertname\"},\n\t\t\tGroupWait:      &groupWait,\n\t\t\tGroupInterval:  &groupInterval,\n\t\t\tRepeatInterval: &repeatInterval,\n\t\t}\n\n\t\t// Add cluster sub-routes\n\t\tteamRoute.Routes = make([]*config.Route, 0, numClusters)\n\t\tfor j := range numClusters {\n\t\t\tclusterRoute := &config.Route{\n\t\t\t\tReceiver:       fmt.Sprintf(\"team-%d-cluster-%d-default\", i, j),\n\t\t\t\tMatch:          map[string]string{\"cluster\": fmt.Sprintf(\"cluster-%d\", j)},\n\t\t\t\tGroupBy:        []model.LabelName{\"alertname\"},\n\t\t\t\tGroupWait:      &groupWait,\n\t\t\t\tGroupInterval:  &groupInterval,\n\t\t\t\tRepeatInterval: &repeatInterval,\n\t\t\t}\n\n\t\t\t// Add priority sub-routes\n\t\t\tclusterRoute.Routes = make([]*config.Route, 0, numPriorities)\n\t\t\tfor k := range numPriorities {\n\t\t\t\tsevRoute := &config.Route{\n\t\t\t\t\tReceiver:       fmt.Sprintf(\"team-%d-cluster-%d-p%d\", i, j, k),\n\t\t\t\t\tMatch:          map[string]string{\"priority\": fmt.Sprintf(\"p%d\", k)},\n\t\t\t\t\tGroupBy:        []model.LabelName{\"alertname\"},\n\t\t\t\t\tGroupWait:      &groupWait,\n\t\t\t\t\tGroupInterval:  &groupInterval,\n\t\t\t\t\tRepeatInterval: &repeatInterval,\n\t\t\t\t}\n\t\t\t\tclusterRoute.Routes = append(clusterRoute.Routes, sevRoute)\n\t\t\t}\n\n\t\t\tteamRoute.Routes = append(teamRoute.Routes, clusterRoute)\n\t\t}\n\n\t\troot.Routes = append(root.Routes, teamRoute)\n\t}\n\n\treturn NewRoute(root, nil)\n}\n\n// newBenchAlert creates a simple alert with given labels for benchmarking.\nfunc newBenchAlert(labels model.LabelSet) *types.Alert {\n\tnow := time.Now()\n\treturn &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels:       labels,\n\t\t\tAnnotations:  model.LabelSet{\"description\": \"benchmark alert\"},\n\t\t\tStartsAt:     now,\n\t\t\tEndsAt:       now.Add(time.Hour),\n\t\t\tGeneratorURL: \"http://localhost\",\n\t\t},\n\t\tUpdatedAt: now,\n\t}\n}\n\n// makeBenchAlertBatch creates a batch of alerts distributed across route tree dimensions:\n//   - offset is added to the index to create unique alerts across multiple batches,\n//     exercising the whole route tree.\n//   - numTeams, numClusters, numPriorities define the route tree structure.\nfunc makeBenchAlertBatch(size, offset, numTeams, numClusters, numPriorities int) []*types.Alert {\n\tbatch := make([]*types.Alert, size)\n\tfor i := range size {\n\t\tidx := offset + i\n\t\tlabels := model.LabelSet{\n\t\t\t\"alertname\": model.LabelValue(fmt.Sprintf(\"alert-%d\", idx)),\n\t\t\t\"instance\":  model.LabelValue(fmt.Sprintf(\"instance-%d\", idx)),\n\t\t}\n\n\t\t// Distribute alerts across teams, clusters, priorities using simple modulo\n\t\t// This ensures each batch hits all dimensions evenly\n\t\tif numTeams > 0 {\n\t\t\tlabels[\"team\"] = model.LabelValue(fmt.Sprintf(\"team-%d\", idx%numTeams))\n\t\t\tlabels[\"cluster\"] = model.LabelValue(fmt.Sprintf(\"cluster-%d\", idx%numClusters))\n\t\t\tlabels[\"priority\"] = model.LabelValue(fmt.Sprintf(\"p%d\", idx%numPriorities))\n\t\t}\n\n\t\tbatch[i] = newBenchAlert(labels)\n\t}\n\treturn batch\n}\n\n// setupDispatcher creates a dispatcher with the given route for benchmarking.\nfunc setupDispatcher(b *testing.B, route *Route) (*Dispatcher, *mem.Alerts, *recordStage) {\n\tlogger := promslog.NewNopLogger()\n\treg := prometheus.NewRegistry()\n\tmarker := types.NewMarker(reg)\n\n\talerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, reg, nil)\n\trequire.NoError(b, err)\n\tb.Cleanup(func() { alerts.Close() })\n\n\trecorder := &recordStage{alerts: make(map[string]map[model.Fingerprint]*types.Alert)}\n\ttimeout := func(d time.Duration) time.Duration { return time.Duration(0) }\n\tmetrics := NewDispatcherMetrics(false, reg)\n\n\tdispatcher := NewDispatcher(alerts, route, recorder, marker, timeout, 30*time.Second, nil, logger, metrics)\n\n\treturn dispatcher, alerts, recorder\n}\n\n// populateGroups pre-populates the dispatcher with aggregation groups.\n// It puts a total of numGroups groups, each with alertsPerGroup alerts, spread across\n// numTeams, numClusters, numPriorities route dimensions.\nfunc populateGroups(b *testing.B, d *Dispatcher, alerts *mem.Alerts, numGroups, alertsPerGroup, numTeams, numClusters, numPriorities, expectedMinGroups int) {\n\tctx := context.Background()\n\n\tfor i := range numGroups {\n\t\tgroupAlerts := make([]*types.Alert, 0, alertsPerGroup)\n\t\tfor j := range alertsPerGroup {\n\t\t\tlabels := model.LabelSet{\n\t\t\t\t\"alertname\": model.LabelValue(fmt.Sprintf(\"alert-%d\", i)),\n\t\t\t\t\"instance\":  model.LabelValue(fmt.Sprintf(\"instance-%d\", j)),\n\t\t\t}\n\t\t\t// Distribute alerts across teams, clusters, priorities (for deep route tree)\n\t\t\tif numTeams > 0 {\n\t\t\t\tlabels[\"team\"] = model.LabelValue(fmt.Sprintf(\"team-%d\", i%numTeams))\n\t\t\t\tlabels[\"cluster\"] = model.LabelValue(fmt.Sprintf(\"cluster-%d\", (i/numTeams)%numClusters))\n\t\t\t\tlabels[\"priority\"] = model.LabelValue(fmt.Sprintf(\"p%d\", (i/(numTeams*numClusters))%numPriorities))\n\t\t\t}\n\t\t\tgroupAlerts = append(groupAlerts, newBenchAlert(labels))\n\t\t}\n\t\trequire.NoError(b, alerts.Put(ctx, groupAlerts...))\n\t}\n\n\t// Wait for dispatcher to create all expected groups\n\trequire.Eventually(b, func() bool {\n\t\tgroups, _, _ := d.Groups(ctx,\n\t\t\tfunc(*Route) bool { return true },\n\t\t\tfunc(*types.Alert, time.Time) bool { return true },\n\t\t)\n\t\treturn len(groups) >= expectedMinGroups\n\t}, 30*time.Second, 10*time.Millisecond, \"expected %d groups to be created\", expectedMinGroups)\n}\n\n// BenchmarkGroups simulates a realistic production scenario:\n// - 500 leaf routes in a deep hierarchy (25 teams × 4 clusters × 5 priorities)\n// - 5000 stable aggregation groups (average ~10 per leaf route)\n// - Measures Groups() API latency (simulates GET /api/v2/alerts/groups)\n//\n// This benchmark demonstrates the concurrent dispatcher's benefit:\n// - main branch: Global lock blocks all Groups() calls during any alert processing\n// - concurrent dispatcher: Per-route locks allow Groups() to run mostly lock-free.\nfunc BenchmarkGroups(b *testing.B) {\n\tb.Run(\"500 routes, 5000 groups\", func(b *testing.B) {\n\t\tbenchmarkGroups(b, 5000, 3, 25, 4, 5)\n\t})\n\tb.Run(\"400 routes, 10000 groups\", func(b *testing.B) {\n\t\tbenchmarkGroups(b, 10000, 3, 20, 4, 5)\n\t})\n}\n\nfunc benchmarkGroups(b *testing.B, numGroups, alertsPerGroup, numTeams, numClusters, numPriorities int) {\n\troute := buildDeepRouteTree(numTeams, numClusters, numPriorities)\n\n\tb.ReportAllocs()\n\n\tdispatcher, alerts, _ := setupDispatcher(b, route)\n\tgo dispatcher.Run(time.Now())\n\tdefer dispatcher.Stop()\n\n\t// Pre-populate with stable groups (uses existing helper)\n\tpopulateGroups(b, dispatcher, alerts, numGroups, alertsPerGroup, numTeams, numClusters, numPriorities, numGroups)\n\n\tctx := context.Background()\n\n\trouteFilter := func(*Route) bool { return true }\n\talertFilter := func(*types.Alert, time.Time) bool { return true }\n\n\tb.ResetTimer()\n\n\t// Measure Groups() API latency\n\tfor b.Loop() {\n\t\tgroups, _, _ := dispatcher.Groups(ctx, routeFilter, alertFilter)\n\t\tif len(groups) != numGroups {\n\t\t\tb.Fatalf(\"unexpected group count: %d (expected %d)\", len(groups), numGroups)\n\t\t}\n\t}\n\n\tb.StopTimer()\n}\n\n// BenchmarkIngestionUnderGroupsLoad measures alert ingestion latency\n// while concurrent Groups() API calls are happening.\n//\n// This demonstrates the key benefit of the concurrent dispatcher:\n// - Main branch: Groups() holds global RLock, blocks ingestion (needs WLock)\n// - Concurrent dispatcher: Groups() iterates sync.Maps, minimal blocking\n//\n// We measure ingestion latency (time for alerts.Put to complete) as we\n// increase the number of concurrent Groups() callers from 0 to 100.\nfunc BenchmarkIngestionUnderGroupsLoad(b *testing.B) {\n\tb.Run(\"500 routes, 0/s Groups() callers\", func(b *testing.B) {\n\t\tbenchmarkIngestionUnderGroupsLoad(b, 0, 0*time.Millisecond)\n\t})\n\tb.Run(\"500 routes, 1 10/s Groups() callers\", func(b *testing.B) {\n\t\tbenchmarkIngestionUnderGroupsLoad(b, 1, 100*time.Millisecond)\n\t})\n\tb.Run(\"500 routes, 10 10/s Groups() callers\", func(b *testing.B) {\n\t\tbenchmarkIngestionUnderGroupsLoad(b, 10, 100*time.Millisecond)\n\t})\n\tb.Run(\"500 routes, 25 10/s Groups() callers\", func(b *testing.B) {\n\t\tbenchmarkIngestionUnderGroupsLoad(b, 25, 100*time.Millisecond)\n\t})\n\tb.Run(\"500 routes, 25 20/s Groups() callers\", func(b *testing.B) {\n\t\tbenchmarkIngestionUnderGroupsLoad(b, 25, 50*time.Millisecond)\n\t})\n}\n\nfunc benchmarkIngestionUnderGroupsLoad(b *testing.B, numGroupsCallers int, groupsTick time.Duration) {\n\troute := buildDeepRouteTree(25, 4, 5)\n\n\tb.ReportAllocs()\n\n\tdispatcher, alerts, _ := setupDispatcher(b, route)\n\tgo dispatcher.Run(time.Now())\n\tdefer dispatcher.Stop()\n\n\t// Pre-populate 5000 stable groups across routes\n\tpopulateGroups(b, dispatcher, alerts, 5000, 3, 25, 4, 5, 5000)\n\n\tctx := context.Background()\n\tstopCh := make(chan struct{})\n\tdefer close(stopCh)\n\n\t// Start concurrent Groups() callers (simulating dashboard queries)\n\tfor range numGroupsCallers {\n\t\tgo func() {\n\t\t\tticker := time.NewTicker(groupsTick)\n\t\t\tdefer ticker.Stop()\n\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-ticker.C:\n\t\t\t\t\tdispatcher.Groups(ctx,\n\t\t\t\t\t\tfunc(*Route) bool { return true },\n\t\t\t\t\t\tfunc(*types.Alert, time.Time) bool { return true })\n\t\t\t\tcase <-stopCh:\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\n\t// Let Groups() callers stabilize\n\ttime.Sleep(500 * time.Millisecond)\n\n\tb.ResetTimer()\n\n\tcounter := 0\n\tfor b.Loop() {\n\t\tbatch := makeBenchAlertBatch(100, counter*100, 25, 4, 5)\n\n\t\t// Put alerts into provider\n\t\terr := alerts.Put(ctx, batch...)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t\tcounter++\n\t}\n}\n"
  },
  {
    "path": "dispatch/dispatch_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage dispatch\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"reflect\"\n\t\"sort\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/testutil\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/provider/mem\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nconst testMaintenanceInterval = 30 * time.Second\n\nfunc TestAggrGroup(t *testing.T) {\n\tlset := model.LabelSet{\n\t\t\"a\": \"v1\",\n\t\t\"b\": \"v2\",\n\t}\n\topts := &RouteOpts{\n\t\tReceiver: \"n1\",\n\t\tGroupBy: map[model.LabelName]struct{}{\n\t\t\t\"a\": {},\n\t\t\t\"b\": {},\n\t\t},\n\t\tGroupWait:      1 * time.Second,\n\t\tGroupInterval:  300 * time.Millisecond,\n\t\tRepeatInterval: 1 * time.Hour,\n\t}\n\troute := &Route{\n\t\tRouteOpts: *opts,\n\t}\n\n\tvar (\n\t\ta1 = &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"a\": \"v1\",\n\t\t\t\t\t\"b\": \"v2\",\n\t\t\t\t\t\"c\": \"v3\",\n\t\t\t\t},\n\t\t\t\tStartsAt: time.Now().Add(time.Minute),\n\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t},\n\t\t\tUpdatedAt: time.Now(),\n\t\t}\n\t\ta2 = &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"a\": \"v1\",\n\t\t\t\t\t\"b\": \"v2\",\n\t\t\t\t\t\"c\": \"v4\",\n\t\t\t\t},\n\t\t\t\tStartsAt: time.Now().Add(-time.Hour),\n\t\t\t\tEndsAt:   time.Now().Add(2 * time.Hour),\n\t\t\t},\n\t\t\tUpdatedAt: time.Now(),\n\t\t}\n\t\ta3 = &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"a\": \"v1\",\n\t\t\t\t\t\"b\": \"v2\",\n\t\t\t\t\t\"c\": \"v5\",\n\t\t\t\t},\n\t\t\t\tStartsAt: time.Now().Add(time.Minute),\n\t\t\t\tEndsAt:   time.Now().Add(5 * time.Minute),\n\t\t\t},\n\t\t\tUpdatedAt: time.Now(),\n\t\t}\n\t)\n\n\tvar (\n\t\tlast       = time.Now()\n\t\tcurrent    = time.Now()\n\t\tlastCurMtx = &sync.Mutex{}\n\t\talertsCh   = make(chan types.AlertSlice)\n\t)\n\n\tntfy := func(ctx context.Context, alerts ...*types.Alert) bool {\n\t\t// Validate that the context is properly populated.\n\t\tif _, ok := notify.Now(ctx); !ok {\n\t\t\tt.Errorf(\"now missing\")\n\t\t}\n\t\tif _, ok := notify.GroupKey(ctx); !ok {\n\t\t\tt.Errorf(\"group key missing\")\n\t\t}\n\t\tif lbls, ok := notify.GroupLabels(ctx); !ok || !reflect.DeepEqual(lbls, lset) {\n\t\t\tt.Errorf(\"wrong group labels: %q\", lbls)\n\t\t}\n\t\tif rcv, ok := notify.ReceiverName(ctx); !ok || rcv != opts.Receiver {\n\t\t\tt.Errorf(\"wrong receiver: %q\", rcv)\n\t\t}\n\t\tif ri, ok := notify.RepeatInterval(ctx); !ok || ri != opts.RepeatInterval {\n\t\t\tt.Errorf(\"wrong repeat interval: %q\", ri)\n\t\t}\n\n\t\tlastCurMtx.Lock()\n\t\tlast = current\n\t\t// Subtract a millisecond to allow for races.\n\t\tcurrent = time.Now().Add(-time.Millisecond)\n\t\tlastCurMtx.Unlock()\n\n\t\talertsCh <- types.AlertSlice(alerts)\n\n\t\treturn true\n\t}\n\n\tremoveEndsAt := func(as types.AlertSlice) types.AlertSlice {\n\t\tfor i, a := range as {\n\t\t\tac := *a\n\t\t\tac.EndsAt = time.Time{}\n\t\t\tas[i] = &ac\n\t\t}\n\t\treturn as\n\t}\n\n\t// Test regular situation where we wait for group_wait to send out alerts.\n\tag := newAggrGroup(context.Background(), lset, route, nil, types.NewMarker(prometheus.NewRegistry()), promslog.NewNopLogger())\n\tgo ag.run(ntfy)\n\n\tctx := context.Background()\n\tag.insert(ctx, a1)\n\n\tselect {\n\tcase <-time.After(2 * opts.GroupWait):\n\t\tt.Fatalf(\"expected initial batch after group_wait\")\n\n\tcase batch := <-alertsCh:\n\t\tlastCurMtx.Lock()\n\t\ts := time.Since(last)\n\t\tlastCurMtx.Unlock()\n\t\tif s < opts.GroupWait {\n\t\t\tt.Fatalf(\"received batch too early after %v\", s)\n\t\t}\n\t\texp := removeEndsAt(types.AlertSlice{a1})\n\t\tsort.Sort(batch)\n\n\t\tif !reflect.DeepEqual(batch, exp) {\n\t\t\tt.Fatalf(\"expected alerts %v but got %v\", exp, batch)\n\t\t}\n\t}\n\n\tfor range 3 {\n\t\t// New alert should come in after group interval.\n\t\tag.insert(ctx, a3)\n\n\t\tselect {\n\t\tcase <-time.After(2 * opts.GroupInterval):\n\t\t\tt.Fatalf(\"expected new batch after group interval but received none\")\n\n\t\tcase batch := <-alertsCh:\n\t\t\tlastCurMtx.Lock()\n\t\t\ts := time.Since(last)\n\t\t\tlastCurMtx.Unlock()\n\t\t\tif s < opts.GroupInterval {\n\t\t\t\tt.Fatalf(\"received batch too early after %v\", s)\n\t\t\t}\n\t\t\texp := removeEndsAt(types.AlertSlice{a1, a3})\n\t\t\tsort.Sort(batch)\n\n\t\t\tif !reflect.DeepEqual(batch, exp) {\n\t\t\t\tt.Fatalf(\"expected alerts %v but got %v\", exp, batch)\n\t\t\t}\n\t\t}\n\t}\n\n\tag.stop()\n\n\t// Finally, set all alerts to be resolved. After successful notify the aggregation group\n\t// should empty itself.\n\tag = newAggrGroup(context.Background(), lset, route, nil, types.NewMarker(prometheus.NewRegistry()), promslog.NewNopLogger())\n\tgo ag.run(ntfy)\n\n\tag.insert(ctx, a1)\n\tag.insert(ctx, a2)\n\n\tbatch := <-alertsCh\n\texp := removeEndsAt(types.AlertSlice{a1, a2})\n\tsort.Sort(batch)\n\n\tif !reflect.DeepEqual(batch, exp) {\n\t\tt.Fatalf(\"expected alerts %v but got %v\", exp, batch)\n\t}\n\n\tfor range 3 {\n\t\t// New alert should come in after group interval.\n\t\tag.insert(ctx, a3)\n\n\t\tselect {\n\t\tcase <-time.After(2 * opts.GroupInterval):\n\t\t\tt.Fatalf(\"expected new batch after group interval but received none\")\n\n\t\tcase batch := <-alertsCh:\n\t\t\tlastCurMtx.Lock()\n\t\t\ts := time.Since(last)\n\t\t\tlastCurMtx.Unlock()\n\t\t\tif s < opts.GroupInterval {\n\t\t\t\tt.Fatalf(\"received batch too early after %v\", s)\n\t\t\t}\n\t\t\texp := removeEndsAt(types.AlertSlice{a1, a2, a3})\n\t\t\tsort.Sort(batch)\n\n\t\t\tif !reflect.DeepEqual(batch, exp) {\n\t\t\t\tt.Fatalf(\"expected alerts %v but got %v\", exp, batch)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Resolve an alert, and it should be removed after the next batch was sent.\n\ta1r := *a1\n\ta1r.EndsAt = time.Now()\n\tag.insert(ctx, &a1r)\n\texp = append(types.AlertSlice{&a1r}, removeEndsAt(types.AlertSlice{a2, a3})...)\n\n\tselect {\n\tcase <-time.After(2 * opts.GroupInterval):\n\t\tt.Fatalf(\"expected new batch after group interval but received none\")\n\tcase batch := <-alertsCh:\n\t\tlastCurMtx.Lock()\n\t\ts := time.Since(last)\n\t\tlastCurMtx.Unlock()\n\t\tif s < opts.GroupInterval {\n\t\t\tt.Fatalf(\"received batch too early after %v\", s)\n\t\t}\n\t\tsort.Sort(batch)\n\n\t\tif !reflect.DeepEqual(batch, exp) {\n\t\t\tt.Fatalf(\"expected alerts %v but got %v\", exp, batch)\n\t\t}\n\t}\n\n\t// Resolve all remaining alerts, they should be removed after the next batch was sent.\n\t// Do not add a1r as it should have been deleted following the previous batch.\n\ta2r, a3r := *a2, *a3\n\tresolved := types.AlertSlice{&a2r, &a3r}\n\tfor _, a := range resolved {\n\t\ta.EndsAt = time.Now()\n\t\tag.insert(ctx, a)\n\t}\n\n\tselect {\n\tcase <-time.After(2 * opts.GroupInterval):\n\t\tt.Fatalf(\"expected new batch after group interval but received none\")\n\n\tcase batch := <-alertsCh:\n\t\tlastCurMtx.Lock()\n\t\ts := time.Since(last)\n\t\tlastCurMtx.Unlock()\n\t\tif s < opts.GroupInterval {\n\t\t\tt.Fatalf(\"received batch too early after %v\", s)\n\t\t}\n\t\tsort.Sort(batch)\n\n\t\tif !reflect.DeepEqual(batch, resolved) {\n\t\t\tt.Fatalf(\"expected alerts %v but got %v\", resolved, batch)\n\t\t}\n\n\t\tif !ag.empty() {\n\t\t\tt.Fatalf(\"Expected aggregation group to be empty after resolving alerts: %v\", ag)\n\t\t}\n\t}\n\n\tag.stop()\n}\n\nfunc TestGroupLabels(t *testing.T) {\n\ta := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels: model.LabelSet{\n\t\t\t\t\"a\": \"v1\",\n\t\t\t\t\"b\": \"v2\",\n\t\t\t\t\"c\": \"v3\",\n\t\t\t},\n\t\t},\n\t}\n\n\troute := &Route{\n\t\tRouteOpts: RouteOpts{\n\t\t\tGroupBy: map[model.LabelName]struct{}{\n\t\t\t\t\"a\": {},\n\t\t\t\t\"b\": {},\n\t\t\t},\n\t\t\tGroupByAll: false,\n\t\t},\n\t}\n\n\texpLs := model.LabelSet{\n\t\t\"a\": \"v1\",\n\t\t\"b\": \"v2\",\n\t}\n\n\tls := getGroupLabels(a, route)\n\n\tif !reflect.DeepEqual(ls, expLs) {\n\t\tt.Fatalf(\"expected labels are %v, but got %v\", expLs, ls)\n\t}\n}\n\nfunc TestGroupByAllLabels(t *testing.T) {\n\ta := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels: model.LabelSet{\n\t\t\t\t\"a\": \"v1\",\n\t\t\t\t\"b\": \"v2\",\n\t\t\t\t\"c\": \"v3\",\n\t\t\t},\n\t\t},\n\t}\n\n\troute := &Route{\n\t\tRouteOpts: RouteOpts{\n\t\t\tGroupBy:    map[model.LabelName]struct{}{},\n\t\t\tGroupByAll: true,\n\t\t},\n\t}\n\n\texpLs := model.LabelSet{\n\t\t\"a\": \"v1\",\n\t\t\"b\": \"v2\",\n\t\t\"c\": \"v3\",\n\t}\n\n\tls := getGroupLabels(a, route)\n\n\tif !reflect.DeepEqual(ls, expLs) {\n\t\tt.Fatalf(\"expected labels are %v, but got %v\", expLs, ls)\n\t}\n}\n\nfunc TestGroups(t *testing.T) {\n\tconfData := `receivers:\n- name: 'kafka'\n- name: 'prod'\n- name: 'testing'\n\nroute:\n  group_by: ['alertname']\n  group_wait: 10ms\n  group_interval: 10ms\n  receiver: 'prod'\n  routes:\n  - match:\n      env: 'testing'\n    receiver: 'testing'\n    group_by: ['alertname', 'service']\n  - match:\n      env: 'prod'\n    receiver: 'prod'\n    group_by: ['alertname', 'service', 'cluster']\n    continue: true\n  - match:\n      kafka: 'yes'\n    receiver: 'kafka'\n    group_by: ['alertname', 'service', 'cluster']`\n\tconf, err := config.Load(confData)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tlogger := promslog.NewNopLogger()\n\troute := NewRoute(conf.Route, nil)\n\treg := prometheus.NewRegistry()\n\tmarker := types.NewMarker(reg)\n\talerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, reg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer alerts.Close()\n\n\ttimeout := func(d time.Duration) time.Duration { return time.Duration(0) }\n\trecorder := &recordStage{alerts: make(map[string]map[model.Fingerprint]*types.Alert)}\n\tdispatcher := NewDispatcher(alerts, route, recorder, marker, timeout, testMaintenanceInterval, nil, logger, NewDispatcherMetrics(false, reg))\n\tgo dispatcher.Run(time.Now())\n\tdefer dispatcher.Stop()\n\n\t// Create alerts. the dispatcher will automatically create the groups.\n\tinputAlerts := []*types.Alert{\n\t\t// Matches the parent route.\n\t\tnewAlert(model.LabelSet{\"alertname\": \"OtherAlert\", \"cluster\": \"cc\", \"service\": \"dd\"}),\n\t\t// Matches the first sub-route.\n\t\tnewAlert(model.LabelSet{\"env\": \"testing\", \"alertname\": \"TestingAlert\", \"service\": \"api\", \"instance\": \"inst1\"}),\n\t\t// Matches the second sub-route.\n\t\tnewAlert(model.LabelSet{\"env\": \"prod\", \"alertname\": \"HighErrorRate\", \"cluster\": \"aa\", \"service\": \"api\", \"instance\": \"inst1\"}),\n\t\tnewAlert(model.LabelSet{\"env\": \"prod\", \"alertname\": \"HighErrorRate\", \"cluster\": \"aa\", \"service\": \"api\", \"instance\": \"inst2\"}),\n\t\t// Matches the second sub-route.\n\t\tnewAlert(model.LabelSet{\"env\": \"prod\", \"alertname\": \"HighErrorRate\", \"cluster\": \"bb\", \"service\": \"api\", \"instance\": \"inst1\"}),\n\t\t// Matches the second and third sub-route.\n\t\tnewAlert(model.LabelSet{\"env\": \"prod\", \"alertname\": \"HighLatency\", \"cluster\": \"bb\", \"service\": \"db\", \"kafka\": \"yes\", \"instance\": \"inst3\"}),\n\t}\n\talerts.Put(context.Background(), inputAlerts...)\n\n\t// Let alerts get processed.\n\tfor i := 0; len(recorder.Alerts()) != 7 && i < 10; i++ {\n\t\ttime.Sleep(200 * time.Millisecond)\n\t}\n\trequire.Len(t, recorder.Alerts(), 7)\n\n\talertGroups, receivers, _ := dispatcher.Groups(context.Background(),\n\t\tfunc(*Route) bool {\n\t\t\treturn true\n\t\t}, func(*types.Alert, time.Time) bool {\n\t\t\treturn true\n\t\t},\n\t)\n\n\trequire.Equal(t, AlertGroups{\n\t\t&AlertGroup{\n\t\t\tAlerts: []*types.Alert{inputAlerts[0]},\n\t\t\tLabels: model.LabelSet{\n\t\t\t\t\"alertname\": \"OtherAlert\",\n\t\t\t},\n\t\t\tReceiver: \"prod\",\n\t\t\tGroupKey: \"{}:{alertname=\\\"OtherAlert\\\"}\",\n\t\t\tRouteID:  \"{}\",\n\t\t},\n\t\t&AlertGroup{\n\t\t\tAlerts: []*types.Alert{inputAlerts[1]},\n\t\t\tLabels: model.LabelSet{\n\t\t\t\t\"alertname\": \"TestingAlert\",\n\t\t\t\t\"service\":   \"api\",\n\t\t\t},\n\t\t\tReceiver: \"testing\",\n\t\t\tGroupKey: \"{}/{env=\\\"testing\\\"}:{alertname=\\\"TestingAlert\\\", service=\\\"api\\\"}\",\n\t\t\tRouteID:  \"{}/{env=\\\"testing\\\"}/0\",\n\t\t},\n\t\t&AlertGroup{\n\t\t\tAlerts: []*types.Alert{inputAlerts[2], inputAlerts[3]},\n\t\t\tLabels: model.LabelSet{\n\t\t\t\t\"alertname\": \"HighErrorRate\",\n\t\t\t\t\"service\":   \"api\",\n\t\t\t\t\"cluster\":   \"aa\",\n\t\t\t},\n\t\t\tReceiver: \"prod\",\n\t\t\tGroupKey: \"{}/{env=\\\"prod\\\"}:{alertname=\\\"HighErrorRate\\\", cluster=\\\"aa\\\", service=\\\"api\\\"}\",\n\t\t\tRouteID:  \"{}/{env=\\\"prod\\\"}/1\",\n\t\t},\n\t\t&AlertGroup{\n\t\t\tAlerts: []*types.Alert{inputAlerts[4]},\n\t\t\tLabels: model.LabelSet{\n\t\t\t\t\"alertname\": \"HighErrorRate\",\n\t\t\t\t\"service\":   \"api\",\n\t\t\t\t\"cluster\":   \"bb\",\n\t\t\t},\n\t\t\tReceiver: \"prod\",\n\t\t\tGroupKey: \"{}/{env=\\\"prod\\\"}:{alertname=\\\"HighErrorRate\\\", cluster=\\\"bb\\\", service=\\\"api\\\"}\",\n\t\t\tRouteID:  \"{}/{env=\\\"prod\\\"}/1\",\n\t\t},\n\t\t&AlertGroup{\n\t\t\tAlerts: []*types.Alert{inputAlerts[5]},\n\t\t\tLabels: model.LabelSet{\n\t\t\t\t\"alertname\": \"HighLatency\",\n\t\t\t\t\"service\":   \"db\",\n\t\t\t\t\"cluster\":   \"bb\",\n\t\t\t},\n\t\t\tReceiver: \"kafka\",\n\t\t\tGroupKey: \"{}/{kafka=\\\"yes\\\"}:{alertname=\\\"HighLatency\\\", cluster=\\\"bb\\\", service=\\\"db\\\"}\",\n\t\t\tRouteID:  \"{}/{kafka=\\\"yes\\\"}/2\",\n\t\t},\n\t\t&AlertGroup{\n\t\t\tAlerts: []*types.Alert{inputAlerts[5]},\n\t\t\tLabels: model.LabelSet{\n\t\t\t\t\"alertname\": \"HighLatency\",\n\t\t\t\t\"service\":   \"db\",\n\t\t\t\t\"cluster\":   \"bb\",\n\t\t\t},\n\t\t\tReceiver: \"prod\",\n\t\t\tGroupKey: \"{}/{env=\\\"prod\\\"}:{alertname=\\\"HighLatency\\\", cluster=\\\"bb\\\", service=\\\"db\\\"}\",\n\t\t\tRouteID:  \"{}/{env=\\\"prod\\\"}/1\",\n\t\t},\n\t}, alertGroups)\n\trequire.Equal(t, map[model.Fingerprint][]string{\n\t\tinputAlerts[0].Fingerprint(): {\"prod\"},\n\t\tinputAlerts[1].Fingerprint(): {\"testing\"},\n\t\tinputAlerts[2].Fingerprint(): {\"prod\"},\n\t\tinputAlerts[3].Fingerprint(): {\"prod\"},\n\t\tinputAlerts[4].Fingerprint(): {\"prod\"},\n\t\tinputAlerts[5].Fingerprint(): {\"kafka\", \"prod\"},\n\t}, receivers)\n}\n\nfunc TestGroupsWithLimits(t *testing.T) {\n\tconfData := `receivers:\n- name: 'kafka'\n- name: 'prod'\n- name: 'testing'\n\nroute:\n  group_by: ['alertname']\n  group_wait: 10ms\n  group_interval: 10ms\n  receiver: 'prod'\n  routes:\n  - match:\n      env: 'testing'\n    receiver: 'testing'\n    group_by: ['alertname', 'service']\n  - match:\n      env: 'prod'\n    receiver: 'prod'\n    group_by: ['alertname', 'service', 'cluster']\n    continue: true\n  - match:\n      kafka: 'yes'\n    receiver: 'kafka'\n    group_by: ['alertname', 'service', 'cluster']`\n\tconf, err := config.Load(confData)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tlogger := promslog.NewNopLogger()\n\troute := NewRoute(conf.Route, nil)\n\treg := prometheus.NewRegistry()\n\tmarker := types.NewMarker(reg)\n\talerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, reg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer alerts.Close()\n\n\ttimeout := func(d time.Duration) time.Duration { return time.Duration(0) }\n\trecorder := &recordStage{alerts: make(map[string]map[model.Fingerprint]*types.Alert)}\n\tlim := limits{groups: 6}\n\tm := NewDispatcherMetrics(true, reg)\n\tdispatcher := NewDispatcher(alerts, route, recorder, marker, timeout, testMaintenanceInterval, lim, logger, m)\n\tgo dispatcher.Run(time.Now())\n\tdefer dispatcher.Stop()\n\n\t// Create alerts. the dispatcher will automatically create the groups.\n\tinputAlerts := []*types.Alert{\n\t\t// Matches the parent route.\n\t\tnewAlert(model.LabelSet{\"alertname\": \"OtherAlert\", \"cluster\": \"cc\", \"service\": \"dd\"}),\n\t\t// Matches the first sub-route.\n\t\tnewAlert(model.LabelSet{\"env\": \"testing\", \"alertname\": \"TestingAlert\", \"service\": \"api\", \"instance\": \"inst1\"}),\n\t\t// Matches the second sub-route.\n\t\tnewAlert(model.LabelSet{\"env\": \"prod\", \"alertname\": \"HighErrorRate\", \"cluster\": \"aa\", \"service\": \"api\", \"instance\": \"inst1\"}),\n\t\tnewAlert(model.LabelSet{\"env\": \"prod\", \"alertname\": \"HighErrorRate\", \"cluster\": \"aa\", \"service\": \"api\", \"instance\": \"inst2\"}),\n\t\t// Matches the second sub-route.\n\t\tnewAlert(model.LabelSet{\"env\": \"prod\", \"alertname\": \"HighErrorRate\", \"cluster\": \"bb\", \"service\": \"api\", \"instance\": \"inst1\"}),\n\t\t// Matches the second and third sub-route.\n\t\tnewAlert(model.LabelSet{\"env\": \"prod\", \"alertname\": \"HighLatency\", \"cluster\": \"bb\", \"service\": \"db\", \"kafka\": \"yes\", \"instance\": \"inst3\"}),\n\t}\n\terr = alerts.Put(context.Background(), inputAlerts...)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Let alerts get processed.\n\tfor i := 0; len(recorder.Alerts()) != 7 && i < 10; i++ {\n\t\ttime.Sleep(200 * time.Millisecond)\n\t}\n\trequire.Len(t, recorder.Alerts(), 7)\n\n\trouteFilter := func(*Route) bool { return true }\n\talertFilter := func(*types.Alert, time.Time) bool { return true }\n\n\talertGroups, _, _ := dispatcher.Groups(context.Background(), routeFilter, alertFilter)\n\trequire.Len(t, alertGroups, 6)\n\n\trequire.Equal(t, 0.0, testutil.ToFloat64(m.aggrGroupLimitReached))\n\n\t// Try to store new alert. This time, we will hit limit for number of groups.\n\terr = alerts.Put(context.Background(), newAlert(model.LabelSet{\"env\": \"prod\", \"alertname\": \"NewAlert\", \"cluster\": \"new-cluster\", \"service\": \"db\"}))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Let alert get processed.\n\tfor i := 0; testutil.ToFloat64(m.aggrGroupLimitReached) == 0 && i < 10; i++ {\n\t\ttime.Sleep(200 * time.Millisecond)\n\t}\n\trequire.Equal(t, 1.0, testutil.ToFloat64(m.aggrGroupLimitReached))\n\n\t// Verify there are still only 6 groups.\n\talertGroups, _, _ = dispatcher.Groups(context.Background(), routeFilter, alertFilter)\n\trequire.Len(t, alertGroups, 6)\n}\n\ntype recordStage struct {\n\tmtx    sync.RWMutex\n\talerts map[string]map[model.Fingerprint]*types.Alert\n}\n\nfunc (r *recordStage) Alerts() []*types.Alert {\n\tr.mtx.RLock()\n\tdefer r.mtx.RUnlock()\n\talerts := make([]*types.Alert, 0)\n\tfor k := range r.alerts {\n\t\tfor _, a := range r.alerts[k] {\n\t\t\talerts = append(alerts, a)\n\t\t}\n\t}\n\treturn alerts\n}\n\nfunc (r *recordStage) Exec(ctx context.Context, l *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {\n\tr.mtx.Lock()\n\tdefer r.mtx.Unlock()\n\tgk, ok := notify.GroupKey(ctx)\n\tif !ok {\n\t\tpanic(\"GroupKey not present!\")\n\t}\n\tif _, ok := r.alerts[gk]; !ok {\n\t\tr.alerts[gk] = make(map[model.Fingerprint]*types.Alert)\n\t}\n\tfor _, a := range alerts {\n\t\tr.alerts[gk][a.Fingerprint()] = a\n\t}\n\treturn ctx, nil, nil\n}\n\nvar (\n\t// Set the start time in the past to trigger a flush immediately.\n\tt0 = time.Now().Add(-time.Minute)\n\t// Set the end time in the future to avoid deleting the alert.\n\tt1 = t0.Add(2 * time.Minute)\n)\n\nfunc newAlert(labels model.LabelSet) *types.Alert {\n\treturn &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels:       labels,\n\t\t\tAnnotations:  model.LabelSet{\"foo\": \"bar\"},\n\t\t\tStartsAt:     t0,\n\t\t\tEndsAt:       t1,\n\t\t\tGeneratorURL: \"http://example.com/prometheus\",\n\t\t},\n\t\tUpdatedAt: t0,\n\t\tTimeout:   false,\n\t}\n}\n\nfunc TestDispatcherRace(t *testing.T) {\n\tlogger := promslog.NewNopLogger()\n\treg := prometheus.NewRegistry()\n\tmarker := types.NewMarker(reg)\n\talerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, reg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer alerts.Close()\n\n\ttimeout := func(d time.Duration) time.Duration { return time.Duration(0) }\n\troute := &Route{}\n\tdispatcher := NewDispatcher(alerts, route, nil, marker, timeout, testMaintenanceInterval, nil, logger, NewDispatcherMetrics(false, reg))\n\tgo dispatcher.Run(time.Now())\n\tdispatcher.Stop()\n}\n\nfunc TestDispatcherRaceOnFirstAlertNotDeliveredWhenGroupWaitIsZero(t *testing.T) {\n\tconst numAlerts = 5000\n\n\tlogger := promslog.NewNopLogger()\n\treg := prometheus.NewRegistry()\n\tmarker := types.NewMarker(reg)\n\talerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, reg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer alerts.Close()\n\n\troute := &Route{\n\t\tRouteOpts: RouteOpts{\n\t\t\tReceiver:       \"default\",\n\t\t\tGroupBy:        map[model.LabelName]struct{}{\"alertname\": {}},\n\t\t\tGroupWait:      0,\n\t\t\tGroupInterval:  1 * time.Hour, // Should never hit in this test.\n\t\t\tRepeatInterval: 1 * time.Hour, // Should never hit in this test.\n\t\t},\n\t}\n\n\ttimeout := func(d time.Duration) time.Duration { return d }\n\trecorder := &recordStage{alerts: make(map[string]map[model.Fingerprint]*types.Alert)}\n\tdispatcher := NewDispatcher(alerts, route, recorder, marker, timeout, testMaintenanceInterval, nil, logger, NewDispatcherMetrics(false, reg))\n\tgo dispatcher.Run(time.Now())\n\tdefer dispatcher.Stop()\n\n\t// Push all alerts.\n\tfor i := range numAlerts {\n\t\talert := newAlert(model.LabelSet{\"alertname\": model.LabelValue(fmt.Sprintf(\"Alert_%d\", i))})\n\t\trequire.NoError(t, alerts.Put(context.Background(), alert))\n\t}\n\n\t// Wait until the alerts have been notified or the waiting timeout expires.\n\tfor deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); {\n\t\tif len(recorder.Alerts()) >= numAlerts {\n\t\t\tbreak\n\t\t}\n\n\t\t// Throttle.\n\t\ttime.Sleep(10 * time.Millisecond)\n\t}\n\n\t// We expect all alerts to be notified immediately, since they all belong to different groups.\n\trequire.Len(t, recorder.Alerts(), numAlerts)\n}\n\ntype limits struct {\n\tgroups int\n}\n\nfunc (l limits) MaxNumberOfAggregationGroups() int {\n\treturn l.groups\n}\n\nfunc TestDispatcher_DoMaintenance(t *testing.T) {\n\tr := prometheus.NewRegistry()\n\tmarker := types.NewMarker(r)\n\n\talerts, err := mem.NewAlerts(context.Background(), marker, time.Minute, 0, nil, promslog.NewNopLogger(), r, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\troute := &Route{\n\t\tRouteOpts: RouteOpts{\n\t\t\tGroupBy:       map[model.LabelName]struct{}{\"alertname\": {}},\n\t\t\tGroupWait:     0,\n\t\t\tGroupInterval: 5 * time.Minute, // Should never hit in this test.\n\t\t},\n\t\tIdx: 0,\n\t}\n\ttimeout := func(d time.Duration) time.Duration { return d }\n\trecorder := &recordStage{alerts: make(map[string]map[model.Fingerprint]*types.Alert)}\n\n\tctx := context.Background()\n\tdispatcher := NewDispatcher(alerts, route, recorder, marker, timeout, testMaintenanceInterval, nil, promslog.NewNopLogger(), NewDispatcherMetrics(false, r))\n\t// Manually create the routeAggrGroups structure since we are not calling Run().\n\tdispatcher.routeGroupsSlice = make([]routeAggrGroups, route.Idx+1)\n\tdispatcher.routeGroupsSlice[route.Idx] = routeAggrGroups{\n\t\troute: route,\n\t}\n\n\t// Insert an aggregation group with one resolved alert.\n\tlabels := model.LabelSet{\"alertname\": \"1\"}\n\taggrGroup1 := newAggrGroup(ctx, labels, route, timeout, types.NewMarker(prometheus.NewRegistry()), promslog.NewNopLogger())\n\tdispatcher.routeGroupsSlice[route.Idx].groups.Store(aggrGroup1.fingerprint(), aggrGroup1)\n\n\t// Add a resolved alert\n\tresolvedAlert := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels:   labels,\n\t\t\tStartsAt: time.Now().Add(-2 * time.Hour),\n\t\t\tEndsAt:   time.Now().Add(-1 * time.Hour), // Already resolved\n\t\t},\n\t\tUpdatedAt: time.Now().Add(-1 * time.Hour),\n\t}\n\taggrGroup1.alerts.Set(resolvedAlert)\n\n\t// Flush will detect the resolved alert and delete it via DeleteIfNotModified\n\t// This is the actual production code path\n\tnotified := false\n\taggrGroup1.flush(func(alerts ...*types.Alert) bool {\n\t\trequire.Len(t, alerts, 1)\n\t\trequire.Equal(t, labels, alerts[0].Labels)\n\t\tnotified = true\n\t\treturn true // Simulate successful notification\n\t})\n\trequire.True(t, notified, \"flush should have called notify function\")\n\n\t// Must run otherwise doMaintenance blocks on aggrGroup1.stop().\n\tgo aggrGroup1.run(func(context.Context, ...*types.Alert) bool { return true })\n\n\t// Insert a marker for the aggregation group's group key.\n\tmarker.SetMuted(route.ID(), aggrGroup1.GroupKey(), []string{\"weekends\"})\n\tmutedBy, isMuted := marker.Muted(route.ID(), aggrGroup1.GroupKey())\n\trequire.True(t, isMuted)\n\trequire.Equal(t, []string{\"weekends\"}, mutedBy)\n\n\t// Run the maintenance and the marker should be removed.\n\tdispatcher.doMaintenance()\n\tmutedBy, isMuted = marker.Muted(route.ID(), aggrGroup1.GroupKey())\n\trequire.False(t, isMuted)\n\trequire.Empty(t, mutedBy)\n}\n\nfunc TestDispatcher_DeleteResolvedAlertsFromMarker(t *testing.T) {\n\tt.Run(\"successful flush deletes markers for resolved alerts\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tmarker := types.NewMarker(prometheus.NewRegistry())\n\t\tlabels := model.LabelSet{\"alertname\": \"TestAlert\"}\n\t\troute := &Route{\n\t\t\tRouteOpts: RouteOpts{\n\t\t\t\tReceiver:       \"test\",\n\t\t\t\tGroupBy:        map[model.LabelName]struct{}{\"alertname\": {}},\n\t\t\t\tGroupWait:      0,\n\t\t\t\tGroupInterval:  time.Minute,\n\t\t\t\tRepeatInterval: time.Hour,\n\t\t\t},\n\t\t}\n\t\ttimeout := func(d time.Duration) time.Duration { return d }\n\t\tlogger := promslog.NewNopLogger()\n\n\t\t// Create an aggregation group\n\t\tag := newAggrGroup(ctx, labels, route, timeout, marker, logger)\n\n\t\t// Create test alerts: one active and one resolved\n\t\tnow := time.Now()\n\t\tactiveAlert := &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"alertname\": \"TestAlert\",\n\t\t\t\t\t\"instance\":  \"1\",\n\t\t\t\t},\n\t\t\t\tStartsAt: now.Add(-time.Hour),\n\t\t\t\tEndsAt:   now.Add(time.Hour), // Active alert\n\t\t\t},\n\t\t\tUpdatedAt: now,\n\t\t}\n\t\tresolvedAlert := &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"alertname\": \"TestAlert\",\n\t\t\t\t\t\"instance\":  \"2\",\n\t\t\t\t},\n\t\t\t\tStartsAt: now.Add(-time.Hour),\n\t\t\t\tEndsAt:   now.Add(-time.Minute), // Resolved alert\n\t\t\t},\n\t\t\tUpdatedAt: now,\n\t\t}\n\n\t\t// Insert alerts into the aggregation group\n\t\tag.insert(ctx, activeAlert)\n\t\tag.insert(ctx, resolvedAlert)\n\n\t\t// Set markers for both alerts\n\t\tmarker.SetActiveOrSilenced(activeAlert.Fingerprint(), nil)\n\t\tmarker.SetActiveOrSilenced(resolvedAlert.Fingerprint(), nil)\n\n\t\t// Verify markers exist before flush\n\t\trequire.True(t, marker.Active(activeAlert.Fingerprint()))\n\t\trequire.True(t, marker.Active(resolvedAlert.Fingerprint()))\n\n\t\t// Create a notify function that succeeds\n\t\tnotifyFunc := func(alerts ...*types.Alert) bool {\n\t\t\treturn true\n\t\t}\n\n\t\t// Flush the alerts\n\t\tag.flush(notifyFunc)\n\n\t\t// Verify that the resolved alert's marker was deleted\n\t\trequire.True(t, marker.Active(activeAlert.Fingerprint()), \"active alert marker should still exist\")\n\t\trequire.False(t, marker.Active(resolvedAlert.Fingerprint()), \"resolved alert marker should be deleted\")\n\t})\n\n\tt.Run(\"failed flush does not delete markers\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tmarker := types.NewMarker(prometheus.NewRegistry())\n\t\tlabels := model.LabelSet{\"alertname\": \"TestAlert\"}\n\t\troute := &Route{\n\t\t\tRouteOpts: RouteOpts{\n\t\t\t\tReceiver:       \"test\",\n\t\t\t\tGroupBy:        map[model.LabelName]struct{}{\"alertname\": {}},\n\t\t\t\tGroupWait:      0,\n\t\t\t\tGroupInterval:  time.Minute,\n\t\t\t\tRepeatInterval: time.Hour,\n\t\t\t},\n\t\t}\n\t\ttimeout := func(d time.Duration) time.Duration { return d }\n\t\tlogger := promslog.NewNopLogger()\n\n\t\t// Create an aggregation group\n\t\tag := newAggrGroup(ctx, labels, route, timeout, marker, logger)\n\n\t\t// Create a resolved alert\n\t\tnow := time.Now()\n\t\tresolvedAlert := &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"alertname\": \"TestAlert\",\n\t\t\t\t\t\"instance\":  \"1\",\n\t\t\t\t},\n\t\t\t\tStartsAt: now.Add(-time.Hour),\n\t\t\t\tEndsAt:   now.Add(-time.Minute), // Resolved alert\n\t\t\t},\n\t\t\tUpdatedAt: now,\n\t\t}\n\n\t\t// Insert alert into the aggregation group\n\t\tag.insert(ctx, resolvedAlert)\n\n\t\t// Set marker for the alert\n\t\tmarker.SetActiveOrSilenced(resolvedAlert.Fingerprint(), nil)\n\n\t\t// Verify marker exists before flush\n\t\trequire.True(t, marker.Active(resolvedAlert.Fingerprint()))\n\n\t\t// Create a notify function that fails\n\t\tnotifyFunc := func(alerts ...*types.Alert) bool {\n\t\t\treturn false\n\t\t}\n\n\t\t// Flush the alerts (notify will fail)\n\t\tag.flush(notifyFunc)\n\n\t\t// Verify that the marker was NOT deleted due to failed notification\n\t\trequire.True(t, marker.Active(resolvedAlert.Fingerprint()), \"marker should not be deleted when notify fails\")\n\t})\n\n\tt.Run(\"markers not deleted when alert is modified during flush\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tmarker := types.NewMarker(prometheus.NewRegistry())\n\t\tlabels := model.LabelSet{\"alertname\": \"TestAlert\"}\n\t\troute := &Route{\n\t\t\tRouteOpts: RouteOpts{\n\t\t\t\tReceiver:       \"test\",\n\t\t\t\tGroupBy:        map[model.LabelName]struct{}{\"alertname\": {}},\n\t\t\t\tGroupWait:      0,\n\t\t\t\tGroupInterval:  time.Minute,\n\t\t\t\tRepeatInterval: time.Hour,\n\t\t\t},\n\t\t}\n\t\ttimeout := func(d time.Duration) time.Duration { return d }\n\t\tlogger := promslog.NewNopLogger()\n\n\t\t// Create an aggregation group\n\t\tag := newAggrGroup(ctx, labels, route, timeout, marker, logger)\n\n\t\t// Create a resolved alert\n\t\tnow := time.Now()\n\t\tresolvedAlert := &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"alertname\": \"TestAlert\",\n\t\t\t\t\t\"instance\":  \"1\",\n\t\t\t\t},\n\t\t\t\tStartsAt: now.Add(-time.Hour),\n\t\t\t\tEndsAt:   now.Add(-time.Minute), // Resolved alert\n\t\t\t},\n\t\t\tUpdatedAt: now,\n\t\t}\n\n\t\t// Insert alert into the aggregation group\n\t\tag.insert(ctx, resolvedAlert)\n\n\t\t// Set marker for the alert\n\t\tmarker.SetActiveOrSilenced(resolvedAlert.Fingerprint(), nil)\n\n\t\t// Verify marker exists before flush\n\t\trequire.True(t, marker.Active(resolvedAlert.Fingerprint()))\n\n\t\t// Create a notify function that modifies the alert before returning\n\t\tnotifyFunc := func(alerts ...*types.Alert) bool {\n\t\t\t// Simulate the alert being modified (e.g., firing again) during flush\n\t\t\tmodifiedAlert := &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\"alertname\": \"TestAlert\",\n\t\t\t\t\t\t\"instance\":  \"1\",\n\t\t\t\t\t},\n\t\t\t\t\tStartsAt: now.Add(-time.Hour),\n\t\t\t\t\tEndsAt:   now.Add(time.Hour), // Active again\n\t\t\t\t},\n\t\t\t\tUpdatedAt: now.Add(time.Second), // More recent update\n\t\t\t}\n\t\t\t// Update the alert in the store\n\t\t\tag.alerts.Set(modifiedAlert)\n\t\t\treturn true\n\t\t}\n\n\t\t// Flush the alerts\n\t\tag.flush(notifyFunc)\n\n\t\t// Verify that the marker was NOT deleted because the alert was modified\n\t\t// during the flush (DeleteIfNotModified should have failed)\n\t\trequire.True(t, marker.Active(resolvedAlert.Fingerprint()), \"marker should not be deleted when alert is modified during flush\")\n\t})\n}\n\nfunc TestDispatchOnStartup(t *testing.T) {\n\tlogger := promslog.NewNopLogger()\n\treg := prometheus.NewRegistry()\n\tmarker := types.NewMarker(reg)\n\talerts, err := mem.NewAlerts(context.Background(), marker, time.Hour, 0, nil, logger, reg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer alerts.Close()\n\n\t// Set up a route with GroupBy to separate alerts into different aggregation groups.\n\troute := &Route{\n\t\tRouteOpts: RouteOpts{\n\t\t\tReceiver:       \"default\",\n\t\t\tGroupBy:        map[model.LabelName]struct{}{\"instance\": {}},\n\t\t\tGroupWait:      1 * time.Second,\n\t\t\tGroupInterval:  3 * time.Minute,\n\t\t\tRepeatInterval: 1 * time.Hour,\n\t\t},\n\t}\n\n\trecorder := &recordStage{alerts: make(map[string]map[model.Fingerprint]*types.Alert)}\n\ttimeout := func(d time.Duration) time.Duration { return d }\n\n\t// Set start time to 3 seconds in the future\n\tnow := time.Now()\n\tstartDelay := 2 * time.Second\n\tstartTime := time.Now().Add(startDelay)\n\tdispatcher := NewDispatcher(alerts, route, recorder, marker, timeout, testMaintenanceInterval, nil, logger, NewDispatcherMetrics(false, reg))\n\tgo dispatcher.Run(startTime)\n\tdefer dispatcher.Stop()\n\n\t// Create 2 similar alerts with start times in the past\n\talert1 := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels:       model.LabelSet{\"alertname\": \"TestAlert1\", \"instance\": \"1\"},\n\t\t\tAnnotations:  model.LabelSet{\"foo\": \"bar\"},\n\t\t\tStartsAt:     now.Add(-1 * time.Hour),\n\t\t\tEndsAt:       now.Add(time.Hour),\n\t\t\tGeneratorURL: \"http://example.com/prometheus\",\n\t\t},\n\t\tUpdatedAt: now,\n\t\tTimeout:   false,\n\t}\n\n\talert2 := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels:       model.LabelSet{\"alertname\": \"TestAlert2\", \"instance\": \"2\"},\n\t\t\tAnnotations:  model.LabelSet{\"foo\": \"bar\"},\n\t\t\tStartsAt:     now.Add(-1 * time.Hour),\n\t\t\tEndsAt:       now.Add(time.Hour),\n\t\t\tGeneratorURL: \"http://example.com/prometheus\",\n\t\t},\n\t\tUpdatedAt: now,\n\t\tTimeout:   false,\n\t}\n\n\t// Send alert1\n\trequire.NoError(t, alerts.Put(context.Background(), alert1))\n\n\tvar recordedAlerts []*types.Alert\n\t// Expect a recorded alert after startTime + GroupWait which is in future\n\trequire.Eventually(t, func() bool {\n\t\trecordedAlerts = recorder.Alerts()\n\t\treturn len(recordedAlerts) == 1\n\t}, startDelay+route.RouteOpts.GroupWait, 500*time.Millisecond)\n\n\trequire.Equal(t, alert1.Fingerprint(), recordedAlerts[0].Fingerprint(), \"expected alert1 to be dispatched after GroupWait\")\n\n\t// Send alert2\n\trequire.NoError(t, alerts.Put(context.Background(), alert2))\n\n\t// Expect a recorded alert after GroupInterval\n\trequire.Eventually(t, func() bool {\n\t\trecordedAlerts = recorder.Alerts()\n\t\treturn len(recordedAlerts) == 2\n\t}, route.RouteOpts.GroupInterval, 100*time.Millisecond)\n\n\t// Sort alerts by fingerprint for deterministic ordering\n\tsort.Slice(recordedAlerts, func(i, j int) bool {\n\t\treturn recordedAlerts[i].Fingerprint() < recordedAlerts[j].Fingerprint()\n\t})\n\trequire.Equal(t, alert2.Fingerprint(), recordedAlerts[1].Fingerprint(), \"expected alert2 to be dispatched after GroupInterval\")\n\n\t// Verify both alerts are present\n\tfingerprints := make(map[model.Fingerprint]bool)\n\tfor _, a := range recordedAlerts {\n\t\tfingerprints[a.Fingerprint()] = true\n\t}\n\trequire.True(t, fingerprints[alert1.Fingerprint()], \"expected alert1 to be present\")\n\trequire.True(t, fingerprints[alert2.Fingerprint()], \"expected alert2 to be present\")\n}\n\nfunc TestGetGroupLabels(t *testing.T) {\n\talert := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels: model.LabelSet{\n\t\t\t\t\"alertname\": \"TestAlert\",\n\t\t\t\t\"job\":       \"prometheus\",\n\t\t\t\t\"instance\":  \"localhost:9090\",\n\t\t\t\t\"severity\":  \"critical\",\n\t\t\t},\n\t\t},\n\t}\n\n\tt.Run(\"specific labels\", func(t *testing.T) {\n\t\troute := &Route{\n\t\t\tRouteOpts: RouteOpts{\n\t\t\t\tGroupBy: map[model.LabelName]struct{}{\n\t\t\t\t\t\"alertname\": {},\n\t\t\t\t\t\"job\":       {},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\tlabels := getGroupLabels(alert, route)\n\t\trequire.Len(t, labels, 2)\n\t\trequire.Equal(t, model.LabelValue(\"TestAlert\"), labels[\"alertname\"])\n\t\trequire.Equal(t, model.LabelValue(\"prometheus\"), labels[\"job\"])\n\t})\n\n\tt.Run(\"group by all\", func(t *testing.T) {\n\t\troute := &Route{\n\t\t\tRouteOpts: RouteOpts{\n\t\t\t\tGroupByAll: true,\n\t\t\t},\n\t\t}\n\t\tlabels := getGroupLabels(alert, route)\n\t\trequire.Len(t, labels, 4)\n\t\trequire.Equal(t, alert.Labels, labels)\n\t})\n}\n\nfunc BenchmarkGetGroupLabels(b *testing.B) {\n\tnow := time.Now()\n\n\t// Alert with many labels (typical production alert)\n\talert := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels: model.LabelSet{\n\t\t\t\t\"alertname\":  \"TestAlert\",\n\t\t\t\t\"severity\":   \"critical\",\n\t\t\t\t\"job\":        \"prometheus\",\n\t\t\t\t\"instance\":   \"localhost:9090\",\n\t\t\t\t\"namespace\":  \"monitoring\",\n\t\t\t\t\"cluster\":    \"prod-us-east-1\",\n\t\t\t\t\"datacenter\": \"dc1\",\n\t\t\t\t\"env\":        \"production\",\n\t\t\t\t\"team\":       \"platform\",\n\t\t\t\t\"service\":    \"alertmanager\",\n\t\t\t},\n\t\t\tStartsAt: now.Add(-time.Hour),\n\t\t\tEndsAt:   now.Add(time.Hour),\n\t\t},\n\t}\n\n\tb.Run(\"specific_labels\", func(b *testing.B) {\n\t\troute := &Route{\n\t\t\tRouteOpts: RouteOpts{\n\t\t\t\tGroupBy: map[model.LabelName]struct{}{\n\t\t\t\t\t\"alertname\": {},\n\t\t\t\t\t\"job\":       {},\n\t\t\t\t\t\"severity\":  {},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tb.ResetTimer()\n\t\tb.ReportAllocs()\n\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\t_ = getGroupLabels(alert, route)\n\t\t}\n\t})\n\n\tb.Run(\"group_by_all\", func(b *testing.B) {\n\t\troute := &Route{\n\t\t\tRouteOpts: RouteOpts{\n\t\t\t\tGroupByAll: true,\n\t\t\t},\n\t\t}\n\n\t\tb.ResetTimer()\n\t\tb.ReportAllocs()\n\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\t_ = getGroupLabels(alert, route)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "dispatch/route.go",
    "content": "// Copyright 2015 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage dispatch\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/prometheus/common/model\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n)\n\n// DefaultRouteOpts are the defaulting routing options which apply\n// to the root route of a routing tree.\nvar DefaultRouteOpts = RouteOpts{\n\tGroupWait:         30 * time.Second,\n\tGroupInterval:     5 * time.Minute,\n\tRepeatInterval:    4 * time.Hour,\n\tGroupBy:           map[model.LabelName]struct{}{},\n\tGroupByAll:        false,\n\tMuteTimeIntervals: []string{},\n}\n\n// A Route is a node that contains definitions of how to handle alerts.\ntype Route struct {\n\tparent *Route\n\n\t// The configuration parameters for matches of this route.\n\tRouteOpts RouteOpts\n\n\t// Matchers an alert has to fulfill to match\n\t// this route.\n\tMatchers labels.Matchers\n\n\t// If true, an alert matches further routes on the same level.\n\tContinue bool\n\n\t// Children routes of this route.\n\tRoutes []*Route\n\n\t// Idx contains the index of this route in the config\n\tIdx int\n}\n\n// NewRoute returns a new route.\nfunc NewRoute(cr *config.Route, parent *Route) *Route {\n\tcounter := 0\n\treturn newRoute(cr, parent, &counter)\n}\n\nfunc newRoute(cr *config.Route, parent *Route, counter *int) *Route {\n\t// Create default and overwrite with configured settings.\n\topts := DefaultRouteOpts\n\tif parent != nil {\n\t\topts = parent.RouteOpts\n\t}\n\n\tif cr.Receiver != \"\" {\n\t\topts.Receiver = cr.Receiver\n\t}\n\n\tif cr.GroupBy != nil {\n\t\topts.GroupBy = map[model.LabelName]struct{}{}\n\t\tfor _, ln := range cr.GroupBy {\n\t\t\topts.GroupBy[ln] = struct{}{}\n\t\t}\n\t\topts.GroupByAll = false\n\t} else {\n\t\tif cr.GroupByAll {\n\t\t\topts.GroupByAll = cr.GroupByAll\n\t\t}\n\t}\n\n\tif cr.GroupWait != nil {\n\t\topts.GroupWait = time.Duration(*cr.GroupWait)\n\t}\n\tif cr.GroupInterval != nil {\n\t\topts.GroupInterval = time.Duration(*cr.GroupInterval)\n\t}\n\tif cr.RepeatInterval != nil {\n\t\topts.RepeatInterval = time.Duration(*cr.RepeatInterval)\n\t}\n\n\t// Build matchers.\n\tvar matchers labels.Matchers\n\n\t// cr.Match will be deprecated. This for loop appends matchers.\n\tfor ln, lv := range cr.Match {\n\t\tmatcher, err := labels.NewMatcher(labels.MatchEqual, ln, lv)\n\t\tif err != nil {\n\t\t\t// This error must not happen because the config already validates the yaml.\n\t\t\tpanic(err)\n\t\t}\n\t\tmatchers = append(matchers, matcher)\n\t}\n\n\t// cr.MatchRE will be deprecated. This for loop appends regex matchers.\n\tfor ln, lv := range cr.MatchRE {\n\t\tmatcher, err := labels.NewMatcher(labels.MatchRegexp, ln, lv.String())\n\t\tif err != nil {\n\t\t\t// This error must not happen because the config already validates the yaml.\n\t\t\tpanic(err)\n\t\t}\n\t\tmatchers = append(matchers, matcher)\n\t}\n\n\t// We append the new-style matchers. This can be simplified once the deprecated matcher syntax is removed.\n\tmatchers = append(matchers, cr.Matchers...)\n\n\tsort.Sort(matchers)\n\n\topts.MuteTimeIntervals = cr.MuteTimeIntervals\n\topts.ActiveTimeIntervals = cr.ActiveTimeIntervals\n\n\troute := &Route{\n\t\tparent:    parent,\n\t\tRouteOpts: opts,\n\t\tMatchers:  matchers,\n\t\tContinue:  cr.Continue,\n\t}\n\n\t// Create child routes first (they get lower indices)\n\troute.Routes = newRoutes(cr.Routes, route, counter)\n\n\t// Assign index to this route after all children have been indexed\n\troute.Idx = *counter\n\t*counter++\n\n\treturn route\n}\n\n// newRoutes returns a slice of routes.\nfunc newRoutes(croutes []*config.Route, parent *Route, counter *int) []*Route {\n\tres := []*Route{}\n\tfor _, cr := range croutes {\n\t\tres = append(res, newRoute(cr, parent, counter))\n\t}\n\treturn res\n}\n\n// Match does a depth-first left-to-right search through the route tree\n// and returns the matching routing nodes.\nfunc (r *Route) Match(lset model.LabelSet) []*Route {\n\tif !r.Matchers.Matches(lset) {\n\t\treturn nil\n\t}\n\n\tvar all []*Route\n\n\tfor _, cr := range r.Routes {\n\t\tmatches := cr.Match(lset)\n\n\t\tall = append(all, matches...)\n\n\t\tif matches != nil && !cr.Continue {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// If no child nodes were matches, the current node itself is a match.\n\tif len(all) == 0 {\n\t\tall = append(all, r)\n\t}\n\n\treturn all\n}\n\n// Key returns a key for the route. It does not uniquely identify the route in general.\nfunc (r *Route) Key() string {\n\tb := strings.Builder{}\n\n\tif r.parent != nil {\n\t\tb.WriteString(r.parent.Key())\n\t\tb.WriteRune('/')\n\t}\n\tb.WriteString(r.Matchers.String())\n\treturn b.String()\n}\n\n// ID returns a unique identifier for the route.\nfunc (r *Route) ID() string {\n\tb := strings.Builder{}\n\n\tif r.parent != nil {\n\t\tb.WriteString(r.parent.ID())\n\t\tb.WriteRune('/')\n\t}\n\n\tb.WriteString(r.Matchers.String())\n\n\tif r.parent != nil {\n\t\tfor i := range r.parent.Routes {\n\t\t\tif r == r.parent.Routes[i] {\n\t\t\t\tb.WriteRune('/')\n\t\t\t\tb.WriteString(strconv.Itoa(i))\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn b.String()\n}\n\n// Walk traverses the route tree in depth-first order.\nfunc (r *Route) Walk(visit func(*Route)) {\n\tvisit(r)\n\tfor i := range r.Routes {\n\t\tr.Routes[i].Walk(visit)\n\t}\n}\n\n// RouteOpts holds various routing options necessary for processing alerts\n// that match a given route.\ntype RouteOpts struct {\n\t// The identifier of the associated notification configuration.\n\tReceiver string\n\n\t// What labels to group alerts by for notifications.\n\tGroupBy map[model.LabelName]struct{}\n\n\t// Use all alert labels to group.\n\tGroupByAll bool\n\n\t// How long to wait to group matching alerts before sending\n\t// a notification.\n\tGroupWait      time.Duration\n\tGroupInterval  time.Duration\n\tRepeatInterval time.Duration\n\n\t// A list of time intervals for which the route is muted.\n\tMuteTimeIntervals []string\n\n\t// A list of time intervals for which the route is active.\n\tActiveTimeIntervals []string\n}\n\nfunc (ro *RouteOpts) String() string {\n\tvar labels []model.LabelName\n\tfor ln := range ro.GroupBy {\n\t\tlabels = append(labels, ln)\n\t}\n\treturn fmt.Sprintf(\"<RouteOpts send_to:%q group_by:%q group_by_all:%t timers:%q|%q>\",\n\t\tro.Receiver, labels, ro.GroupByAll, ro.GroupWait, ro.GroupInterval)\n}\n\n// MarshalJSON returns a JSON representation of the routing options.\nfunc (ro *RouteOpts) MarshalJSON() ([]byte, error) {\n\tv := struct {\n\t\tReceiver       string           `json:\"receiver\"`\n\t\tGroupBy        model.LabelNames `json:\"groupBy\"`\n\t\tGroupByAll     bool             `json:\"groupByAll\"`\n\t\tGroupWait      time.Duration    `json:\"groupWait\"`\n\t\tGroupInterval  time.Duration    `json:\"groupInterval\"`\n\t\tRepeatInterval time.Duration    `json:\"repeatInterval\"`\n\t}{\n\t\tReceiver:       ro.Receiver,\n\t\tGroupByAll:     ro.GroupByAll,\n\t\tGroupWait:      ro.GroupWait,\n\t\tGroupInterval:  ro.GroupInterval,\n\t\tRepeatInterval: ro.RepeatInterval,\n\t}\n\tfor ln := range ro.GroupBy {\n\t\tv.GroupBy = append(v.GroupBy, ln)\n\t}\n\n\treturn json.Marshal(&v)\n}\n"
  },
  {
    "path": "dispatch/route_test.go",
    "content": "// Copyright 2015 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage dispatch\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/stretchr/testify/require\"\n\t\"gopkg.in/yaml.v2\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n)\n\nfunc TestRouteMatch(t *testing.T) {\n\tin := `\nreceiver: 'notify-def'\n\nroutes:\n- match:\n    owner: 'team-A'\n\n  receiver: 'notify-A'\n\n  routes:\n  - match:\n      env: 'testing'\n\n    receiver: 'notify-testing'\n    group_by: [...]\n\n  - match:\n      env: \"production\"\n\n    receiver: 'notify-productionA'\n    group_wait: 1m\n\n    continue: true\n\n  - match_re:\n      env: \"produ.*\"\n      job: \".*\"\n\n    receiver: 'notify-productionB'\n    group_wait: 30s\n    group_interval: 5m\n    repeat_interval: 1h\n    group_by: ['job']\n\n- match_re:\n    owner: 'team-(B|C)'\n\n  group_by: ['foo', 'bar']\n  group_wait: 2m\n  receiver: 'notify-BC'\n\n- match:\n    group_by: 'role'\n  group_by: ['role']\n\n  routes:\n  - match:\n      env: 'testing'\n    receiver: 'notify-testing'\n    routes:\n    - match:\n        wait: 'long'\n      group_wait: 2m\n`\n\n\tvar ctree config.Route\n\tif err := yaml.UnmarshalStrict([]byte(in), &ctree); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tvar (\n\t\tdef  = DefaultRouteOpts\n\t\ttree = NewRoute(&ctree, nil)\n\t)\n\tlset := func(labels ...string) map[model.LabelName]struct{} {\n\t\ts := map[model.LabelName]struct{}{}\n\t\tfor _, ls := range labels {\n\t\t\ts[model.LabelName(ls)] = struct{}{}\n\t\t}\n\t\treturn s\n\t}\n\n\ttests := []struct {\n\t\tinput  model.LabelSet\n\t\tresult []*RouteOpts\n\t\tkeys   []string\n\t}{\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"owner\": \"team-A\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-A\",\n\t\t\t\t\tGroupBy:        def.GroupBy,\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      def.GroupWait,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{owner=\\\"team-A\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"owner\": \"team-A\",\n\t\t\t\t\"env\":   \"unset\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-A\",\n\t\t\t\t\tGroupBy:        def.GroupBy,\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      def.GroupWait,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{owner=\\\"team-A\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"owner\": \"team-C\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-BC\",\n\t\t\t\t\tGroupBy:        lset(\"foo\", \"bar\"),\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      2 * time.Minute,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{owner=~\\\"^(?:team-(B|C))$\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"owner\": \"team-A\",\n\t\t\t\t\"env\":   \"testing\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-testing\",\n\t\t\t\t\tGroupBy:        lset(),\n\t\t\t\t\tGroupByAll:     true,\n\t\t\t\t\tGroupWait:      def.GroupWait,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{owner=\\\"team-A\\\"}/{env=\\\"testing\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"owner\": \"team-A\",\n\t\t\t\t\"env\":   \"production\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-productionA\",\n\t\t\t\t\tGroupBy:        def.GroupBy,\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      1 * time.Minute,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-productionB\",\n\t\t\t\t\tGroupBy:        lset(\"job\"),\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      30 * time.Second,\n\t\t\t\t\tGroupInterval:  5 * time.Minute,\n\t\t\t\t\tRepeatInterval: 1 * time.Hour,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\n\t\t\t\t\"{}/{owner=\\\"team-A\\\"}/{env=\\\"production\\\"}\",\n\t\t\t\t\"{}/{owner=\\\"team-A\\\"}/{env=~\\\"^(?:produ.*)$\\\",job=~\\\"^(?:.*)$\\\"}\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"group_by\": \"role\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-def\",\n\t\t\t\t\tGroupBy:        lset(\"role\"),\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      def.GroupWait,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{group_by=\\\"role\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"env\":      \"testing\",\n\t\t\t\t\"group_by\": \"role\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-testing\",\n\t\t\t\t\tGroupBy:        lset(\"role\"),\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      def.GroupWait,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{group_by=\\\"role\\\"}/{env=\\\"testing\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"env\":      \"testing\",\n\t\t\t\t\"group_by\": \"role\",\n\t\t\t\t\"wait\":     \"long\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-testing\",\n\t\t\t\t\tGroupBy:        lset(\"role\"),\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      2 * time.Minute,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{group_by=\\\"role\\\"}/{env=\\\"testing\\\"}/{wait=\\\"long\\\"}\"},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tvar matches []*RouteOpts\n\t\tvar keys []string\n\n\t\tfor _, r := range tree.Match(test.input) {\n\t\t\tmatches = append(matches, &r.RouteOpts)\n\t\t\tkeys = append(keys, r.Key())\n\t\t}\n\n\t\tif !reflect.DeepEqual(matches, test.result) {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", test.result, matches)\n\t\t}\n\n\t\tif !reflect.DeepEqual(keys, test.keys) {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", test.keys, keys)\n\t\t}\n\t}\n}\n\nfunc TestRouteWalk(t *testing.T) {\n\tin := `\nreceiver: 'notify-def'\n\nroutes:\n- match:\n    owner: 'team-A'\n\n  receiver: 'notify-A'\n\n  routes:\n  - match:\n      env: 'testing'\n\n    receiver: 'notify-testing'\n    group_by: [...]\n\n  - match:\n      env: \"production\"\n\n    receiver: 'notify-productionA'\n    group_wait: 1m\n\n    continue: true\n\n  - match_re:\n      env: \"produ.*\"\n      job: \".*\"\n\n    receiver: 'notify-productionB'\n    group_wait: 30s\n    group_interval: 5m\n    repeat_interval: 1h\n    group_by: ['job']\n\n\n- match_re:\n    owner: 'team-(B|C)'\n\n  group_by: ['foo', 'bar']\n  group_wait: 2m\n  receiver: 'notify-BC'\n\n- match:\n    group_by: 'role'\n  group_by: ['role']\n\n  routes:\n  - match:\n      env: 'testing'\n    receiver: 'notify-testing'\n    routes:\n    - match:\n        wait: 'long'\n      group_wait: 2m\n`\n\n\tvar ctree config.Route\n\tif err := yaml.UnmarshalStrict([]byte(in), &ctree); err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttree := NewRoute(&ctree, nil)\n\n\texpected := []string{\n\t\t\"notify-def\",\n\t\t\"notify-A\",\n\t\t\"notify-testing\",\n\t\t\"notify-productionA\",\n\t\t\"notify-productionB\",\n\t\t\"notify-BC\",\n\t\t\"notify-def\",\n\t\t\"notify-testing\",\n\t\t\"notify-testing\",\n\t}\n\n\tvar got []string\n\ttree.Walk(func(r *Route) {\n\t\tgot = append(got, r.RouteOpts.Receiver)\n\t})\n\n\tif !reflect.DeepEqual(got, expected) {\n\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", expected, got)\n\t}\n}\n\nfunc TestInheritParentGroupByAll(t *testing.T) {\n\tin := `\nroutes:\n- match:\n    env: 'parent'\n  group_by: ['...']\n\n  routes:\n  - match:\n      env: 'child1'\n\n  - match:\n      env: 'child2'\n    group_by: ['foo']\n`\n\n\tvar ctree config.Route\n\tif err := yaml.UnmarshalStrict([]byte(in), &ctree); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttree := NewRoute(&ctree, nil)\n\tparent := tree.Routes[0]\n\tchild1 := parent.Routes[0]\n\tchild2 := parent.Routes[1]\n\trequire.True(t, parent.RouteOpts.GroupByAll)\n\trequire.True(t, child1.RouteOpts.GroupByAll)\n\trequire.False(t, child2.RouteOpts.GroupByAll)\n}\n\nfunc TestRouteMatchers(t *testing.T) {\n\tin := `\nreceiver: 'notify-def'\n\nroutes:\n- matchers: ['{owner=\"team-A\"}', '{level!=\"critical\"}']\n\n  receiver: 'notify-A'\n\n  routes:\n  - matchers: ['{env=\"testing\"}', '{baz!~\".*quux\"}']\n\n    receiver: 'notify-testing'\n    group_by: [...]\n\n  - matchers: ['{env=\"production\"}']\n\n    receiver: 'notify-productionA'\n    group_wait: 1m\n\n    continue: true\n\n  - matchers: [ env=~\"produ.*\", job=~\".*\"]\n\n    receiver: 'notify-productionB'\n    group_wait: 30s\n    group_interval: 5m\n    repeat_interval: 1h\n    group_by: ['job']\n\n\n- matchers: [owner=~\"team-(B|C)\"]\n\n  group_by: ['foo', 'bar']\n  group_wait: 2m\n  receiver: 'notify-BC'\n\n- matchers: [group_by=\"role\"]\n  group_by: ['role']\n\n  routes:\n  - matchers: ['{env=\"testing\"}']\n    receiver: 'notify-testing'\n    routes:\n    - matchers: [wait=\"long\"]\n      group_wait: 2m\n`\n\n\tvar ctree config.Route\n\tif err := yaml.UnmarshalStrict([]byte(in), &ctree); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tvar (\n\t\tdef  = DefaultRouteOpts\n\t\ttree = NewRoute(&ctree, nil)\n\t)\n\tlset := func(labels ...string) map[model.LabelName]struct{} {\n\t\ts := map[model.LabelName]struct{}{}\n\t\tfor _, ls := range labels {\n\t\t\ts[model.LabelName(ls)] = struct{}{}\n\t\t}\n\t\treturn s\n\t}\n\n\ttests := []struct {\n\t\tinput  model.LabelSet\n\t\tresult []*RouteOpts\n\t\tkeys   []string\n\t}{\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"owner\": \"team-A\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-A\",\n\t\t\t\t\tGroupBy:        def.GroupBy,\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      def.GroupWait,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{level!=\\\"critical\\\",owner=\\\"team-A\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"owner\": \"team-A\",\n\t\t\t\t\"env\":   \"unset\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-A\",\n\t\t\t\t\tGroupBy:        def.GroupBy,\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      def.GroupWait,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{level!=\\\"critical\\\",owner=\\\"team-A\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"owner\": \"team-C\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-BC\",\n\t\t\t\t\tGroupBy:        lset(\"foo\", \"bar\"),\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      2 * time.Minute,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{owner=~\\\"team-(B|C)\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"owner\": \"team-A\",\n\t\t\t\t\"env\":   \"testing\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-testing\",\n\t\t\t\t\tGroupBy:        lset(),\n\t\t\t\t\tGroupByAll:     true,\n\t\t\t\t\tGroupWait:      def.GroupWait,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{level!=\\\"critical\\\",owner=\\\"team-A\\\"}/{baz!~\\\".*quux\\\",env=\\\"testing\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"owner\": \"team-A\",\n\t\t\t\t\"env\":   \"production\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-productionA\",\n\t\t\t\t\tGroupBy:        def.GroupBy,\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      1 * time.Minute,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-productionB\",\n\t\t\t\t\tGroupBy:        lset(\"job\"),\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      30 * time.Second,\n\t\t\t\t\tGroupInterval:  5 * time.Minute,\n\t\t\t\t\tRepeatInterval: 1 * time.Hour,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\n\t\t\t\t\"{}/{level!=\\\"critical\\\",owner=\\\"team-A\\\"}/{env=\\\"production\\\"}\",\n\t\t\t\t\"{}/{level!=\\\"critical\\\",owner=\\\"team-A\\\"}/{env=~\\\"produ.*\\\",job=~\\\".*\\\"}\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"group_by\": \"role\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-def\",\n\t\t\t\t\tGroupBy:        lset(\"role\"),\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      def.GroupWait,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{group_by=\\\"role\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"env\":      \"testing\",\n\t\t\t\t\"group_by\": \"role\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-testing\",\n\t\t\t\t\tGroupBy:        lset(\"role\"),\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      def.GroupWait,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{group_by=\\\"role\\\"}/{env=\\\"testing\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"env\":      \"testing\",\n\t\t\t\t\"group_by\": \"role\",\n\t\t\t\t\"wait\":     \"long\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-testing\",\n\t\t\t\t\tGroupBy:        lset(\"role\"),\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      2 * time.Minute,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{group_by=\\\"role\\\"}/{env=\\\"testing\\\"}/{wait=\\\"long\\\"}\"},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tvar matches []*RouteOpts\n\t\tvar keys []string\n\n\t\tfor _, r := range tree.Match(test.input) {\n\t\t\tmatches = append(matches, &r.RouteOpts)\n\t\t\tkeys = append(keys, r.Key())\n\t\t}\n\n\t\tif !reflect.DeepEqual(matches, test.result) {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", test.result, matches)\n\t\t}\n\n\t\tif !reflect.DeepEqual(keys, test.keys) {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", test.keys, keys)\n\t\t}\n\t}\n}\n\nfunc TestRouteMatchersAndMatch(t *testing.T) {\n\tin := `\nreceiver: 'notify-def'\n\nroutes:\n- matchers: ['{owner=\"team-A\"}', '{level!=\"critical\"}']\n\n  receiver: 'notify-A'\n\n  routes:\n  - matchers: ['{env=\"testing\"}', '{baz!~\".*quux\"}']\n\n    receiver: 'notify-testing'\n    group_by: [...]\n\n  - match:\n      env: \"production\"\n\n    receiver: 'notify-productionA'\n    group_wait: 1m\n\n    continue: true\n\n  - matchers: [ env=~\"produ.*\", job=~\".*\"]\n\n    receiver: 'notify-productionB'\n    group_wait: 30s\n    group_interval: 5m\n    repeat_interval: 1h\n    group_by: ['job']\n\n- match_re:\n    owner: 'team-(B|C)'\n\n  group_by: ['foo', 'bar']\n  group_wait: 2m\n  receiver: 'notify-BC'\n\n- matchers: [group_by=\"role\"]\n  group_by: ['role']\n\n  routes:\n  - matchers: ['{env=\"testing\"}']\n    receiver: 'notify-testing'\n    routes:\n    - matchers: [wait=\"long\"]\n      group_wait: 2m\n`\n\n\tvar ctree config.Route\n\tif err := yaml.UnmarshalStrict([]byte(in), &ctree); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tvar (\n\t\tdef  = DefaultRouteOpts\n\t\ttree = NewRoute(&ctree, nil)\n\t)\n\tlset := func(labels ...string) map[model.LabelName]struct{} {\n\t\ts := map[model.LabelName]struct{}{}\n\t\tfor _, ls := range labels {\n\t\t\ts[model.LabelName(ls)] = struct{}{}\n\t\t}\n\t\treturn s\n\t}\n\n\ttests := []struct {\n\t\tinput  model.LabelSet\n\t\tresult []*RouteOpts\n\t\tkeys   []string\n\t}{\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"owner\": \"team-A\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-A\",\n\t\t\t\t\tGroupBy:        def.GroupBy,\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      def.GroupWait,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{level!=\\\"critical\\\",owner=\\\"team-A\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"owner\": \"team-A\",\n\t\t\t\t\"env\":   \"unset\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-A\",\n\t\t\t\t\tGroupBy:        def.GroupBy,\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      def.GroupWait,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{level!=\\\"critical\\\",owner=\\\"team-A\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"owner\": \"team-C\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-BC\",\n\t\t\t\t\tGroupBy:        lset(\"foo\", \"bar\"),\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      2 * time.Minute,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{owner=~\\\"^(?:team-(B|C))$\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"owner\": \"team-A\",\n\t\t\t\t\"env\":   \"testing\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-testing\",\n\t\t\t\t\tGroupBy:        lset(),\n\t\t\t\t\tGroupByAll:     true,\n\t\t\t\t\tGroupWait:      def.GroupWait,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{level!=\\\"critical\\\",owner=\\\"team-A\\\"}/{baz!~\\\".*quux\\\",env=\\\"testing\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"owner\": \"team-A\",\n\t\t\t\t\"env\":   \"production\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-productionA\",\n\t\t\t\t\tGroupBy:        def.GroupBy,\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      1 * time.Minute,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-productionB\",\n\t\t\t\t\tGroupBy:        lset(\"job\"),\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      30 * time.Second,\n\t\t\t\t\tGroupInterval:  5 * time.Minute,\n\t\t\t\t\tRepeatInterval: 1 * time.Hour,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\n\t\t\t\t\"{}/{level!=\\\"critical\\\",owner=\\\"team-A\\\"}/{env=\\\"production\\\"}\",\n\t\t\t\t\"{}/{level!=\\\"critical\\\",owner=\\\"team-A\\\"}/{env=~\\\"produ.*\\\",job=~\\\".*\\\"}\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"group_by\": \"role\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-def\",\n\t\t\t\t\tGroupBy:        lset(\"role\"),\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      def.GroupWait,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{group_by=\\\"role\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"env\":      \"testing\",\n\t\t\t\t\"group_by\": \"role\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-testing\",\n\t\t\t\t\tGroupBy:        lset(\"role\"),\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      def.GroupWait,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{group_by=\\\"role\\\"}/{env=\\\"testing\\\"}\"},\n\t\t},\n\t\t{\n\t\t\tinput: model.LabelSet{\n\t\t\t\t\"env\":      \"testing\",\n\t\t\t\t\"group_by\": \"role\",\n\t\t\t\t\"wait\":     \"long\",\n\t\t\t},\n\t\t\tresult: []*RouteOpts{\n\t\t\t\t{\n\t\t\t\t\tReceiver:       \"notify-testing\",\n\t\t\t\t\tGroupBy:        lset(\"role\"),\n\t\t\t\t\tGroupByAll:     false,\n\t\t\t\t\tGroupWait:      2 * time.Minute,\n\t\t\t\t\tGroupInterval:  def.GroupInterval,\n\t\t\t\t\tRepeatInterval: def.RepeatInterval,\n\t\t\t\t},\n\t\t\t},\n\t\t\tkeys: []string{\"{}/{group_by=\\\"role\\\"}/{env=\\\"testing\\\"}/{wait=\\\"long\\\"}\"},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tvar matches []*RouteOpts\n\t\tvar keys []string\n\n\t\tfor _, r := range tree.Match(test.input) {\n\t\t\tmatches = append(matches, &r.RouteOpts)\n\t\t\tkeys = append(keys, r.Key())\n\t\t}\n\n\t\tif !reflect.DeepEqual(matches, test.result) {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", test.result, matches)\n\t\t}\n\n\t\tif !reflect.DeepEqual(keys, test.keys) {\n\t\t\tt.Errorf(\"\\nexpected:\\n%v\\ngot:\\n%v\", test.keys, keys)\n\t\t}\n\t}\n}\n\nfunc TestRouteID(t *testing.T) {\n\tin := `\nreceiver: default\nroutes:\n- continue: true\n  matchers:\n  - foo=bar\n  receiver: test1\n  routes:\n  - matchers:\n    - bar=baz\n- continue: true\n  matchers:\n  - foo=bar\n  receiver: test1\n  routes:\n  - matchers:\n    - bar=baz\n- continue: true\n  matchers:\n  - foo=bar\n  receiver: test2\n  routes:\n  - matchers:\n    - bar=baz\n- continue: true\n  matchers:\n  - bar=baz\n  receiver: test3\n  routes:\n  - matchers:\n    - baz=qux\n  - matchers:\n    - qux=corge\n- continue: true\n  matchers:\n  - qux=~\"[a-zA-Z0-9]+\"\n- continue: true\n  matchers:\n  - corge!~\"[0-9]+\"\n`\n\tcr := config.Route{}\n\trequire.NoError(t, yaml.Unmarshal([]byte(in), &cr))\n\tr := NewRoute(&cr, nil)\n\n\texpected := []string{\n\t\t\"{}\",\n\t\t\"{}/{foo=\\\"bar\\\"}/0\",\n\t\t\"{}/{foo=\\\"bar\\\"}/0/{bar=\\\"baz\\\"}/0\",\n\t\t\"{}/{foo=\\\"bar\\\"}/1\",\n\t\t\"{}/{foo=\\\"bar\\\"}/1/{bar=\\\"baz\\\"}/0\",\n\t\t\"{}/{foo=\\\"bar\\\"}/2\",\n\t\t\"{}/{foo=\\\"bar\\\"}/2/{bar=\\\"baz\\\"}/0\",\n\t\t\"{}/{bar=\\\"baz\\\"}/3\",\n\t\t\"{}/{bar=\\\"baz\\\"}/3/{baz=\\\"qux\\\"}/0\",\n\t\t\"{}/{bar=\\\"baz\\\"}/3/{qux=\\\"corge\\\"}/1\",\n\t\t\"{}/{qux=~\\\"[a-zA-Z0-9]+\\\"}/4\",\n\t\t\"{}/{corge!~\\\"[0-9]+\\\"}/5\",\n\t}\n\n\tvar actual []string\n\tr.Walk(func(r *Route) {\n\t\tactual = append(actual, r.ID())\n\t})\n\trequire.ElementsMatch(t, actual, expected)\n}\n\nfunc TestRouteIndices(t *testing.T) {\n\tin := `\nreceiver: 'notify-def'\n\nroutes:\n- match:\n    owner: 'team-A'\n\n  receiver: 'notify-A'\n\n  routes:\n  - match:\n      env: 'testing'\n\n    receiver: 'notify-testing'\n    group_by: [...]\n\n  - match:\n      env: \"production\"\n\n    receiver: 'notify-productionA'\n    group_wait: 1m\n\n    continue: true\n\n  - match_re:\n      env: \"produ.*\"\n      job: \".*\"\n\n    receiver: 'notify-productionB'\n    group_wait: 30s\n    group_interval: 5m\n    repeat_interval: 1h\n    group_by: ['job']\n\n- match_re:\n    owner: 'team-(B|C)'\n\n  group_by: ['foo', 'bar']\n  group_wait: 2m\n  receiver: 'notify-BC'\n\n- match:\n    group_by: 'role'\n  group_by: ['role']\n\n  routes:\n  - match:\n      env: 'testing'\n    receiver: 'notify-testing'\n    routes:\n    - match:\n        wait: 'long'\n      group_wait: 2m\n`\n\n\tvar ctree config.Route\n\tif err := yaml.UnmarshalStrict([]byte(in), &ctree); err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttree := NewRoute(&ctree, nil)\n\n\t// Collect all indices\n\tvar indices []int\n\tvar totalNodes int\n\ttree.Walk(func(r *Route) {\n\t\tindices = append(indices, r.Idx)\n\t\ttotalNodes++\n\t})\n\n\t// All indices are unique\n\tseenIndices := make(map[int]bool)\n\tfor _, idx := range indices {\n\t\trequire.False(t, seenIndices[idx], \"Index %d appears more than once\", idx)\n\t\tseenIndices[idx] = true\n\t}\n\n\t// Root index equals total nodes - 1\n\trequire.Equal(t, totalNodes-1, tree.Idx, \"Root index should equal total nodes - 1\")\n\n\t// All indices are in range [0, totalNodes)\n\tfor _, idx := range indices {\n\t\trequire.GreaterOrEqual(t, idx, 0, \"Index should be >= 0\")\n\t\trequire.Less(t, idx, totalNodes, \"Index should be < total nodes\")\n\t}\n}\n"
  },
  {
    "path": "doc/alertmanager-mixin/.gitignore",
    "content": "vendor\ndashboards_out\n"
  },
  {
    "path": "doc/alertmanager-mixin/.lint",
    "content": "exclusions:\n  target-instance-rule:\n    reason: no need to have every query contains two matchers within every selector - `{job=~\"$job\", instance=~\"$instance\"}`\n  template-job-rule:\n    entries:\n    - dashboard: Alertmanager / Overview \n      reason: multi-select is not always required\n  template-instance-rule:\n    entries:\n    - dashboard: Alertmanager / Overview\n      reason: multi-select is not always required\n  panel-units-rule:\n    entries:\n    - dashboard: Alertmanager / Overview\n      reason: Dashboard does not benefit from specific unit specification."
  },
  {
    "path": "doc/alertmanager-mixin/Makefile",
    "content": "JSONNET_FMT := jsonnetfmt -n 2 --max-blank-lines 1 --string-style s --comment-style s\nALERTMANAGER_ALERTS := alertmanager_alerts.yaml\n\ndefault: vendor build dashboards_out\n\nall: fmt build\n\nvendor: \n\tjb install\n\nfmt:\n\tfind . -name 'vendor' -prune -o -name '*.libsonnet' -print -o -name '*.jsonnet' -print | \\\n\t\txargs -n 1 -- $(JSONNET_FMT) -i\n\nlint: build\n\tfind . -name 'vendor' -prune -o -name '*.libsonnet' -print -o -name '*.jsonnet' -print | \\\n\t\twhile read f; do \\\n\t\t\t$(JSONNET_FMT) \"$$f\" | diff -u \"$$f\" -; \\\n\t\tdone\n\n\tmixtool lint mixin.libsonnet\n\ndashboards_out: mixin.libsonnet config.libsonnet $(wildcard dashboards/*)\n\t@mkdir -p dashboards_out\n\tjsonnet -J vendor -m dashboards_out dashboards.jsonnet\n\nbuild: vendor\n\tmixtool generate alerts mixin.libsonnet > $(ALERTMANAGER_ALERTS)\n\nclean:\n\trm -rf $(ALERTMANAGER_ALERTS)\n"
  },
  {
    "path": "doc/alertmanager-mixin/README.md",
    "content": "# Alertmanager Mixin\n\nThe Alertmanager Mixin is a set of configurable, reusable, and extensible\nalerts (and eventually dashboards) for Alertmanager.\n\nThe alerts are designed to monitor a cluster of Alertmanager instances. To make\nthem work as expected, the Prometheus server the alerts are evaluated on has to\nscrape all Alertmanager instances of the cluster, even if those instances are\ndistributed over different locations. All Alertmanager instances in the same\nAlertmanager cluster must have the same `job` label. In turn, if monitoring\nmultiple different Alertmanager clusters, instances from different clusters\nmust have a different `job` label.\n\nThe most basic use of the Alertmanager Mixin is to create a YAML file with the\nalerts from it. To do so, you need to have `jsonnetfmt` and `mixtool` installed. If you have a working Go development environment, it's\neasiest to run the following:\n\n```bash\n$ go get github.com/monitoring-mixins/mixtool/cmd/mixtool\n$ go get github.com/google/go-jsonnet/cmd/jsonnetfmt\n```\n\nEdit `config.libsonnet` to match your environment and then build\n`alertmanager_alerts.yaml` with the alerts by running:\n\n```bash\n$ make build\n```\n\nFor instructions on more advanced uses of mixins, see https://github.com/monitoring-mixins/docs.\n"
  },
  {
    "path": "doc/alertmanager-mixin/alerts.jsonnet",
    "content": "std.manifestYamlDoc((import 'mixin.libsonnet').prometheusAlerts)\n"
  },
  {
    "path": "doc/alertmanager-mixin/alerts.libsonnet",
    "content": "{\n  prometheusAlerts+:: {\n    groups+: [\n      {\n        name: 'alertmanager.rules',\n        rules: [\n          {\n            alert: 'AlertmanagerFailedReload',\n            expr: |||\n              # Without max_over_time, failed scrapes could create false negatives, see\n              # https://www.robustperception.io/alerting-on-gauges-in-prometheus-2-0 for details.\n              max_over_time(alertmanager_config_last_reload_successful{%(alertmanagerSelector)s}[5m]) == 0\n            ||| % $._config,\n            'for': '10m',\n            labels: {\n              severity: 'critical',\n            },\n            annotations: {\n              summary: 'Reloading an Alertmanager configuration has failed.',\n              description: 'Configuration has failed to load for %(alertmanagerName)s.' % $._config,\n            },\n          },\n          {\n            alert: 'AlertmanagerMembersInconsistent',\n            expr: |||\n              # Without max_over_time, failed scrapes could create false negatives, see\n              # https://www.robustperception.io/alerting-on-gauges-in-prometheus-2-0 for details.\n                max_over_time(alertmanager_cluster_members{%(alertmanagerSelector)s}[5m])\n              < on (%(alertmanagerClusterLabels)s) group_left\n                count by (%(alertmanagerClusterLabels)s) (max_over_time(alertmanager_cluster_members{%(alertmanagerSelector)s}[5m]))\n            ||| % $._config,\n            'for': '15m',\n            labels: {\n              severity: 'critical',\n            },\n            annotations: {\n              summary: 'A member of an Alertmanager cluster has not found all other cluster members.',\n              description: 'Alertmanager %(alertmanagerName)s has only found {{ $value }} members of the %(alertmanagerClusterName)s cluster.' % $._config,\n            },\n          },\n          {\n            alert: 'AlertmanagerFailedToSendAlerts',\n            expr: |||\n              (\n                rate(alertmanager_notifications_failed_total{%(alertmanagerSelector)s}[15m])\n              /\n                ignoring (reason) group_left rate(alertmanager_notifications_total{%(alertmanagerSelector)s}[15m])\n              )\n              > 0.01\n            ||| % $._config,\n            'for': '5m',\n            labels: {\n              severity: 'warning',\n            },\n            annotations: {\n              summary: 'An Alertmanager instance failed to send notifications.',\n              description: 'Alertmanager %(alertmanagerName)s failed to send {{ $value | humanizePercentage }} of notifications to {{ $labels.integration }}.' % $._config,\n            },\n          },\n          {\n            alert: 'AlertmanagerClusterFailedToSendAlerts',\n            expr: |||\n              min by (%(alertmanagerClusterLabels)s, integration) (\n                rate(alertmanager_notifications_failed_total{%(alertmanagerSelector)s, integration=~`%(alertmanagerCriticalIntegrationsRegEx)s`}[15m])\n              /\n                ignoring (reason) group_left rate(alertmanager_notifications_total{%(alertmanagerSelector)s, integration=~`%(alertmanagerCriticalIntegrationsRegEx)s`}[15m]) > 0\n              )\n              > 0.01\n            ||| % $._config,\n            'for': '5m',\n            labels: {\n              severity: 'critical',\n            },\n            annotations: {\n              summary: 'All Alertmanager instances in a cluster failed to send notifications to a critical integration.',\n              description: 'The minimum notification failure rate to {{ $labels.integration }} sent from any instance in the %(alertmanagerClusterName)s cluster is {{ $value | humanizePercentage }}.' % $._config,\n            },\n          },\n          {\n            alert: 'AlertmanagerClusterFailedToSendAlerts',\n            expr: |||\n              min by (%(alertmanagerClusterLabels)s, integration) (\n                rate(alertmanager_notifications_failed_total{%(alertmanagerSelector)s, integration!~`%(alertmanagerCriticalIntegrationsRegEx)s`}[15m])\n              /\n                ignoring (reason) group_left rate(alertmanager_notifications_total{%(alertmanagerSelector)s, integration!~`%(alertmanagerCriticalIntegrationsRegEx)s`}[15m]) > 0\n              )\n              > 0.01\n            ||| % $._config,\n            'for': '5m',\n            labels: {\n              severity: 'warning',\n            },\n            annotations: {\n              summary: 'All Alertmanager instances in a cluster failed to send notifications to a non-critical integration.',\n              description: 'The minimum notification failure rate to {{ $labels.integration }} sent from any instance in the %(alertmanagerClusterName)s cluster is {{ $value | humanizePercentage }}.' % $._config,\n            },\n          },\n          {\n            alert: 'AlertmanagerConfigInconsistent',\n            expr: |||\n              count by (%(alertmanagerClusterLabels)s) (\n                count_values by (%(alertmanagerClusterLabels)s) (\"config_hash\", alertmanager_config_hash{%(alertmanagerSelector)s})\n              )\n              != 1\n            ||| % $._config,\n            'for': '20m',  // A config change across an Alertmanager cluster can take its time. But it's really bad if it persists for too long.\n            labels: {\n              severity: 'critical',\n            },\n            annotations: {\n              summary: 'Alertmanager instances within the same cluster have different configurations.',\n              description: 'Alertmanager instances within the %(alertmanagerClusterName)s cluster have different configurations.' % $._config,\n            },\n          },\n          // Both the following critical alerts, AlertmanagerClusterDown and\n          // AlertmanagerClusterCrashlooping, fire if a whole cluster is\n          // unhealthy. It is implied that a generic warning alert is in place\n          // for individual instances being down or crashlooping.\n          {\n            alert: 'AlertmanagerClusterDown',\n            expr: |||\n              (\n                count by (%(alertmanagerClusterLabels)s) (\n                  avg_over_time(up{%(alertmanagerSelector)s}[5m]) < 0.5\n                )\n              /\n                count by (%(alertmanagerClusterLabels)s) (\n                  up{%(alertmanagerSelector)s}\n                )\n              )\n              >= 0.5\n            ||| % $._config,\n            'for': '5m',\n            labels: {\n              severity: 'critical',\n            },\n            annotations: {\n              summary: 'Half or more of the Alertmanager instances within the same cluster are down.',\n              description: '{{ $value | humanizePercentage }} of Alertmanager instances within the %(alertmanagerClusterName)s cluster have been up for less than half of the last 5m.' % $._config,\n            },\n          },\n          {\n            alert: 'AlertmanagerClusterCrashlooping',\n            expr: |||\n              (\n                count by (%(alertmanagerClusterLabels)s) (\n                  changes(process_start_time_seconds{%(alertmanagerSelector)s}[10m]) > 4\n                )\n              /\n                count by (%(alertmanagerClusterLabels)s) (\n                  up{%(alertmanagerSelector)s}\n                )\n              )\n              >= 0.5\n            ||| % $._config,\n            'for': '5m',\n            labels: {\n              severity: 'critical',\n            },\n            annotations: {\n              summary: 'Half or more of the Alertmanager instances within the same cluster are crashlooping.',\n              description: '{{ $value | humanizePercentage }} of Alertmanager instances within the %(alertmanagerClusterName)s cluster have restarted at least 5 times in the last 10m.' % $._config,\n            },\n          },\n        ],\n      },\n    ],\n  },\n}\n"
  },
  {
    "path": "doc/alertmanager-mixin/config.libsonnet",
    "content": "{\n  _config+:: {\n    local c = self,\n    // alertmanagerSelector is inserted as part of the label selector in\n    // PromQL queries to identify metrics collected from Alertmanager\n    // servers.\n    alertmanagerSelector: 'job=\"alertmanager\"',\n\n    // alertmanagerClusterLabels is a string with comma-separated\n    // labels that are common labels of instances belonging to the\n    // same Alertmanager cluster. Include not only enough labels to\n    // identify cluster members, but also all common labels you want\n    // to keep for resulting cluster-level alerts.\n    alertmanagerClusterLabels: 'job',\n\n    // alertmanagerNameLabels is a string with comma-separated\n    // labels used to identify different alertmanagers within the same\n    // Alertmanager HA cluster.\n    // If you run Alertmanager on Kubernetes with the Prometheus\n    // Operator, you can make use of the configured target labels for\n    // nicer naming:\n    // alertmanagerNameLabels: 'namespace,pod'\n    alertmanagerNameLabels: 'instance',\n\n    // alertmanagerName is an identifier for alerts. By default, it is built from 'alertmanagerNameLabels'.\n    alertmanagerName: std.join('/', ['{{$labels.%s}}' % [label] for label in std.split(c.alertmanagerNameLabels, ',')]),\n\n    // alertmanagerClusterName is inserted into annotations to name an\n    // Alertmanager cluster. All labels used here must also be present\n    // in alertmanagerClusterLabels above.\n    alertmanagerClusterName: '{{$labels.job}}',\n\n    // alertmanagerCriticalIntegrationsRegEx is matched against the\n    // value of the `integration` label to determine if the\n    // AlertmanagerClusterFailedToSendAlerts is critical or merely a\n    // warning. This can be used to avoid paging about a failed\n    // integration that is itself not used for critical alerts.\n    // Example: @'pagerduty|webhook'\n    alertmanagerCriticalIntegrationsRegEx: @'.*',\n\n    dashboardNamePrefix: 'Alertmanager / ',\n    dashboardTags: ['alertmanager-mixin'],\n  },\n}\n"
  },
  {
    "path": "doc/alertmanager-mixin/dashboards/overview.libsonnet",
    "content": "local grafana = import 'github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet';\nlocal dashboard = grafana.dashboard;\nlocal prometheus = grafana.query.prometheus;\nlocal variable = dashboard.variable;\nlocal panel = grafana.panel;\nlocal row = panel.row;\n\n{\n  grafanaDashboards+:: {\n    local amQuerySelector = std.join(',', ['%s=~\"$%s\"' % [label, label] for label in std.split($._config.alertmanagerClusterLabels, ',')]),\n    local amNameDashboardLegend = std.join('/', ['{{%s}}' % [label] for label in std.split($._config.alertmanagerNameLabels, ',')]),\n\n    local datasource =\n      variable.datasource.new('datasource', 'prometheus')\n      + variable.datasource.generalOptions.withLabel('Data Source')\n      + variable.datasource.generalOptions.withCurrent('Prometheus')\n      + variable.datasource.generalOptions.showOnDashboard.withLabelAndValue(),\n\n    local alertmanagerClusterSelectorVariables =\n      [\n        variable.query.new(label)\n        + variable.query.generalOptions.withLabel(label)\n        + variable.query.withDatasourceFromVariable(datasource)\n        + variable.query.queryTypes.withLabelValues(label, metric='alertmanager_alerts')\n        + variable.query.generalOptions.withCurrent('')\n        + variable.query.refresh.onTime()\n        + variable.query.selectionOptions.withIncludeAll(false)\n        + variable.query.withSort(type='alphabetical')\n        for label in std.split($._config.alertmanagerClusterLabels, ',')\n      ],\n\n    local integrationVariable =\n      variable.query.new('integration')\n      + variable.query.withDatasourceFromVariable(datasource)\n      + variable.query.queryTypes.withLabelValues('integration', metric='alertmanager_notifications_total{integration=~\"%s\"}' % $._config.alertmanagerCriticalIntegrationsRegEx)\n      + variable.query.generalOptions.withCurrent('$__all')\n      + variable.datasource.generalOptions.showOnDashboard.withNothing()\n      + variable.query.refresh.onTime()\n      + variable.query.selectionOptions.withIncludeAll(true)\n      + variable.query.withSort(type='alphabetical'),\n\n    local panelTimeSeriesStdOptions =\n      {}\n      + panel.timeSeries.fieldConfig.defaults.custom.stacking.withMode('normal')\n      + panel.timeSeries.fieldConfig.defaults.custom.withFillOpacity(10)\n      + panel.timeSeries.fieldConfig.defaults.custom.withShowPoints('never')\n      + panel.timeSeries.options.legend.withShowLegend(false)\n      + panel.timeSeries.options.tooltip.withMode('multi')\n      + panel.timeSeries.queryOptions.withDatasource('prometheus', '$datasource'),\n\n    'alertmanager-overview.json':\n      local alerts =\n        panel.timeSeries.new('Alerts')\n        + panel.timeSeries.panelOptions.withDescription('current set of alerts stored in the Alertmanager')\n        + panel.timeSeries.standardOptions.withUnit('none')\n        + panelTimeSeriesStdOptions\n        + panel.timeSeries.queryOptions.withTargets([\n            prometheus.new(\n              '$datasource',\n              'sum(alertmanager_alerts{%(amQuerySelector)s}) by (%(alertmanagerClusterLabels)s,%(alertmanagerNameLabels)s)' % $._config { amQuerySelector: amQuerySelector },\n            )\n            + prometheus.withIntervalFactor(2)\n            + prometheus.withLegendFormat('%(amNameDashboardLegend)s' % $._config { amNameDashboardLegend: amNameDashboardLegend }),\n          ]);\n\n      local alertsRate =\n        panel.timeSeries.new('Alerts receive rate')\n        + panel.timeSeries.panelOptions.withDescription('rate of successful and invalid alerts received by the Alertmanager')\n        + panel.timeSeries.standardOptions.withUnit('ops')\n        + panelTimeSeriesStdOptions\n        + panel.timeSeries.queryOptions.withTargets([\n            prometheus.new(\n              '$datasource',\n              'sum(rate(alertmanager_alerts_received_total{%(amQuerySelector)s}[$__rate_interval])) by (%(alertmanagerClusterLabels)s,%(alertmanagerNameLabels)s)' % $._config { amQuerySelector: amQuerySelector },\n            )\n            + prometheus.withIntervalFactor(2)\n            + prometheus.withLegendFormat('%(amNameDashboardLegend)s Received' % $._config { amNameDashboardLegend: amNameDashboardLegend }),\n            prometheus.new(\n              '$datasource',\n              'sum(rate(alertmanager_alerts_invalid_total{%(amQuerySelector)s}[$__rate_interval])) by (%(alertmanagerClusterLabels)s,%(alertmanagerNameLabels)s)' % $._config { amQuerySelector: amQuerySelector },\n            )\n            + prometheus.withIntervalFactor(2)\n            + prometheus.withLegendFormat('%(amNameDashboardLegend)s Invalid' % $._config { amNameDashboardLegend: amNameDashboardLegend }),\n          ]);\n\n      local notifications =\n        panel.timeSeries.new('$integration: Notifications Send Rate')\n        + panel.timeSeries.panelOptions.withDescription('rate of successful and invalid notifications sent by the Alertmanager')\n        + panel.timeSeries.standardOptions.withUnit('ops')\n        + panelTimeSeriesStdOptions\n        + panel.timeSeries.panelOptions.withRepeat('integration')\n        + panel.timeSeries.queryOptions.withTargets([\n            prometheus.new(\n              '$datasource',\n              'sum(rate(alertmanager_notifications_total{%(amQuerySelector)s, integration=\"$integration\"}[$__rate_interval])) by (integration,%(alertmanagerClusterLabels)s,%(alertmanagerNameLabels)s)' % $._config { amQuerySelector: amQuerySelector },\n            )\n            + prometheus.withIntervalFactor(2)\n            + prometheus.withLegendFormat('%(amNameDashboardLegend)s Total' % $._config { amNameDashboardLegend: amNameDashboardLegend }),\n            prometheus.new(\n              '$datasource',\n              'sum(rate(alertmanager_notifications_failed_total{%(amQuerySelector)s, integration=\"$integration\"}[$__rate_interval])) by (integration,%(alertmanagerClusterLabels)s,%(alertmanagerNameLabels)s)' % $._config { amQuerySelector: amQuerySelector },\n            )\n            + prometheus.withIntervalFactor(2)\n            + prometheus.withLegendFormat('%(amNameDashboardLegend)s Failed' % $._config { amNameDashboardLegend: amNameDashboardLegend }),\n          ]);\n\n      local notificationDuration =\n        panel.timeSeries.new('$integration: Notification Duration')\n        + panel.timeSeries.panelOptions.withDescription('latency of notifications sent by the Alertmanager')\n        + panel.timeSeries.standardOptions.withUnit('s')\n        + panelTimeSeriesStdOptions\n        + panel.timeSeries.panelOptions.withRepeat('integration')\n        + panel.timeSeries.queryOptions.withTargets([\n            prometheus.new(\n              '$datasource',\n              |||\n                histogram_quantile(0.99,\n                  sum(rate(alertmanager_notification_latency_seconds_bucket{%(amQuerySelector)s, integration=\"$integration\"}[$__rate_interval])) by (le,%(alertmanagerClusterLabels)s,%(alertmanagerNameLabels)s)\n                )\n              ||| % $._config { amQuerySelector: amQuerySelector },\n            )\n            + prometheus.withIntervalFactor(2)\n            + prometheus.withLegendFormat('%(amNameDashboardLegend)s 99th Percentile' % $._config { amNameDashboardLegend: amNameDashboardLegend }),\n            prometheus.new(\n              '$datasource',\n            |||\n              histogram_quantile(0.50,\n                sum(rate(alertmanager_notification_latency_seconds_bucket{%(amQuerySelector)s, integration=\"$integration\"}[$__rate_interval])) by (le,%(alertmanagerClusterLabels)s,%(alertmanagerNameLabels)s)\n              )\n            ||| % $._config { amQuerySelector: amQuerySelector },\n            )\n            + prometheus.withIntervalFactor(2)\n            + prometheus.withLegendFormat('%(amNameDashboardLegend)s Median' % $._config { amNameDashboardLegend: amNameDashboardLegend }),\n            prometheus.new(\n              '$datasource',\n              |||\n                sum(rate(alertmanager_notification_latency_seconds_sum{%(amQuerySelector)s, integration=\"$integration\"}[$__rate_interval])) by (%(alertmanagerClusterLabels)s,%(alertmanagerNameLabels)s)\n                /\n                sum(rate(alertmanager_notification_latency_seconds_count{%(amQuerySelector)s, integration=\"$integration\"}[$__rate_interval])) by (%(alertmanagerClusterLabels)s,%(alertmanagerNameLabels)s)\n              ||| % $._config { amQuerySelector: amQuerySelector },\n            )\n            + prometheus.withIntervalFactor(2)\n            + prometheus.withLegendFormat('%(amNameDashboardLegend)s Average' % $._config { amNameDashboardLegend: amNameDashboardLegend }),\n          ]);\n\n      dashboard.new('%sOverview' % $._config.dashboardNamePrefix)\n      + dashboard.time.withFrom('now-1h')\n      + dashboard.withTags($._config.dashboardTags)\n      + dashboard.withTimezone('utc')\n      + dashboard.timepicker.withRefreshIntervals('30s')\n      + dashboard.graphTooltip.withSharedCrosshair()\n      + dashboard.withUid('alertmanager-overview')\n      + dashboard.withVariables(\n        [datasource]\n        + alertmanagerClusterSelectorVariables\n        + [integrationVariable]\n        )\n      + dashboard.withPanels(\n          grafana.util.grid.makeGrid([\n            row.new('Alerts')\n            + row.withPanels([\n              alerts,\n              alertsRate\n              ]),\n            row.new('Notifications')\n            + row.withPanels([\n              notifications,\n              notificationDuration\n              ])\n          ], panelWidth=12,  panelHeight=7)\n        )\n  },\n}\n"
  },
  {
    "path": "doc/alertmanager-mixin/dashboards.jsonnet",
    "content": "local dashboards = (import 'mixin.libsonnet').grafanaDashboards;\n\n{\n  [name]: dashboards[name]\n  for name in std.objectFields(dashboards)\n}\n"
  },
  {
    "path": "doc/alertmanager-mixin/dashboards.libsonnet",
    "content": "(import './dashboards/overview.libsonnet')\n"
  },
  {
    "path": "doc/alertmanager-mixin/jsonnetfile.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": [\n    {\n      \"source\": {\n        \"git\": {\n          \"remote\": \"https://github.com/grafana/grafonnet.git\",\n          \"subdir\": \"gen/grafonnet-latest\"\n        }\n      },\n      \"version\": \"main\"\n    }\n  ],\n  \"legacyImports\": false\n}\n"
  },
  {
    "path": "doc/alertmanager-mixin/jsonnetfile.lock.json",
    "content": "{\n  \"version\": 1,\n  \"dependencies\": [\n    {\n      \"source\": {\n        \"git\": {\n          \"remote\": \"https://github.com/grafana/grafonnet.git\",\n          \"subdir\": \"gen/grafonnet-latest\"\n        }\n      },\n      \"version\": \"1ce5aec95ce32336fe47c8881361847c475b5254\",\n      \"sum\": \"64fMUPI3frXGj4X1FqFd1t7r04w3CUSmXaDcJ23EYbQ=\"\n    },\n    {\n      \"source\": {\n        \"git\": {\n          \"remote\": \"https://github.com/grafana/grafonnet.git\",\n          \"subdir\": \"gen/grafonnet-v11.1.0\"\n        }\n      },\n      \"version\": \"1ce5aec95ce32336fe47c8881361847c475b5254\",\n      \"sum\": \"41w7p/rwrNsITqNHMXtGSJAfAyKmnflg6rFhKBduUxM=\"\n    },\n    {\n      \"source\": {\n        \"git\": {\n          \"remote\": \"https://github.com/jsonnet-libs/docsonnet.git\",\n          \"subdir\": \"doc-util\"\n        }\n      },\n      \"version\": \"6ac6c69685b8c29c54515448eaca583da2d88150\",\n      \"sum\": \"BrAL/k23jq+xy9oA7TWIhUx07dsA/QLm3g7ktCwe//U=\"\n    },\n    {\n      \"source\": {\n        \"git\": {\n          \"remote\": \"https://github.com/jsonnet-libs/xtd.git\",\n          \"subdir\": \"\"\n        }\n      },\n      \"version\": \"63d430b69a95741061c2f7fc9d84b1a778511d9c\",\n      \"sum\": \"qiZi3axUSXCVzKUF83zSAxklwrnitMmrDK4XAfjPMdE=\"\n    }\n  ],\n  \"legacyImports\": false\n}\n"
  },
  {
    "path": "doc/alertmanager-mixin/mixin.libsonnet",
    "content": "(import 'config.libsonnet') +\n(import 'alerts.libsonnet') +\n(import 'dashboards.libsonnet')\n"
  },
  {
    "path": "doc/arch.xml",
    "content": "<mxfile userAgent=\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36\" version=\"8.6.9\" editor=\"www.draw.io\" type=\"google\"><diagram id=\"eda46daa-11a9-bbae-2e7a-d514aa78bb29\" name=\"Page-1\">7V1bk9o6Ev41VO0+QPlu8ziXkHOqkq2pzFZlz6OwBWjjQaxtMpPz61eyLWOrZTAgG5hM5iEgy7Ksr7vVVzGyH17ePidos/pKIxyPLCN6G9mPI8syHctj//GWX0WLbxlFwzIhUdlp1/BM/sZlo+i2JRFOGx0zSuOMbJqNIV2vcZg12lCS0NdmtwWNm0/doCUGDc8himHrdxJlq6I1cI1d+x+YLFfiyaZRXnlBonPZkK5QRF9rTfankf2QUJoVn17eHnDMF0+sS3HfrOVqNbEEr7MuNzg4jEIrChfRFOEpDsemNS3G+Inibfm6/6IZWZAQZYSu2ZUnssExWePyDbJfYlleVyTDzxsU8u+vDPqRfb/KXmL2zWQfaYLWfF3vFySOH2hMk/w2ezb79PDAJni/TFBE2MTFtTVd8+7VGhm8T4zStPycZgn9gWsjBQb/Y1cilK5wVD73J04yNvv4LibLNWvLKJ/Ygq6z53LuvNeKJuRv1obEdPMOBemZdvl9hl5IzIn2LiG84325VuwJ+K0VArMClnEEpi84S36xLuUNjiCbkhksv/j6uqMsyy67rGpU5UztkqJLal5WQ+8AZx9KzFvw9xbIwf4cG35gRNF8bHsA/pHlxeyZ93P2Yck/fMMhJuydxQX2kOpa1TmRWyLyUzR9Gn9FJK7dXrt2LFUVRCDY0DyKXu7zv1YSSeh2HeVkxO9G5bWQ4crevT+CcCWCcCwHUISvIghTAz0AeeC5gCCeccYaCqnAxPAHYDJg/rQTYIKr9TIwxEvBgSfxtMyoYNTveL6i9Ee7DLgRVu9N1AtmOpazdRDKIkBTy4yiaYiCIPDHJtzoASBwxetwrNCG93t5W3I1b7KI6Wu4Qkk22SQ0xByLexWifa2uZR/aR01TxYX++YtrgaV8JOkGZeFqx1F1btClOM3sT94exUlmAiP/dxwnddOeesN0elA5UoJquhpYxjQBrJ9pmpINhJRtiVl8tEZ8jkCz7el0NlMA9EKiiD9/MJnmNRAKzG4izdWAD9zq7p7+PB+ENjYbiuidpiATLFBf0UBB8pYOkg/gksbsXVjTZ7zGCcpoktbJv7bQ3v+23GzNl2Wc5pbTHetgWpu3QgUor8tKAe9/1kD/eEr4m67wNv1nTdMoxm0RucPtbHU70tJDIHZTKNr2cDud0BpqS4mjJRZsQZNsRZd0jeJPu1ZJEtUWGq+jO+6PYV/nMQ1/FE0zEjeZs8aMziP/y/uxmf+HDzlxxde/yifgN5IVlxy//PqXeCL7/IQTwl6c2wSHRCN/t/0gsaWg2yQUvUr+yVCyxNVepAYzwTHKmM7dGEyFTXnrEyU5o5RE4PsNIjBla6KYQnlT3QUkjRM05bcvDVO8HRgmp5PqZU60LS3obHgmMV6z1bQMxtQ/ScT1J5l1S+6MUIZSJo9wCzfW6CxGcxw/0ZTkLqyaSSg2zy9Sh2oTBUakvN3OaZbRlx53A9NvcrvpuxMXMLyn4HcdOhAETdAZBA1CdTmDbih1xxTsM4StD7GAGtA3us1+SyQsu5vm2RMS0Jr+jkh2NA7H6/+dlI6DYqzACpJBc+vWAlvTB2IFAzrLAGwOtNxb9xvykoeF6nCpV7FtU6m2inw7ukfhj2W+vDVUF/k/1iV/2F26KcJXuTtTfFmQNw7IfTmfx1WW8bjXHV8GaxZGa2tCQrpeEAZcMgnZE60Z3yvZf7ydqe+zIgg2RutovF1zt1+K4jEK+TzTMe80TrmPd2a5Hl9TtseyzTqYbNbLo2i08jccpFGzRqMxXmQ55WWoXDgz0ER8nt1UmlS2laWwrUTbOcRnL4w5ngZGEGKEkeOMXftDZJzkX7+syHB9gNA5FlBlqriSpZIyBT4T1lHl0WNtpXlkdLGf/ouz7FeJKtpmNPcmigl+oRzBdleGzKD7TK7RSeZSu3ZZN6Da9B7tJpUj2UKW8M8fsKm0GEMeFAc6Ce36yEkXybgKm1vRa3omyZwuMXwHIHsXJxhFv6AjOc3ffdYb8gLXOvSSJ6Ykjh3yxm0j79t7dxdjYjWYvgqMnSpMRBe6WORalGY54UJL41tFo9dh8gHVIWWzIOvll1yzy4e8TMDVHjIyDyW8/yHhT+JzrxufG2paGEDCezBs8oij7eaKePJKcmNsu1vKQz8c6EPf5Yfs7IrcRWWnDx01N5KGNlgy0WXxgdZLjgyf8EckR8g+iaMU6V/DxXEMiJgI9X8AVl6VQm8i7WIAvIC30BnA7WRcpRa5czs1nE75zHVpmIIZGhqmnDHZRi1nep3kHLdqMge8TgwC9KvWbcM7pHueYzSf400lgiwGPNVShRQLleLn7TwNEzKHmXKMSbMmiSY4JX+jed6BI1++Huvt3o/cxy4CohIlHQIuMtUdJ1PKSqNyuqOqvqdOiC0svc8xYYs1rHwTo7NcE+XI46bHw2ver8VvAaMdBsBcg/wy69Jr55VukRbXJ9t0yS+B4QG3+VAWsh+EbhRMLR8ZwXQauWMRHteVNtaOcVd4DuLcTmIa88XgHmNBIM1+9h3HkPcdKdqhL/UL0IOoVeqXHnxXJ0MPQhCuOzcs17RCFM3nxjwcuxckCNMdjiCc/tXbQzHMfkW/LlEvROmBcFdwKVEvMvs/kGwi2Zr/sR9I73JAekMBeX1g9RA/VstcV5K5Tn8pBxDggTJZbgPgs8NHLQAHLTWfQwAMLfA/1ysyJxmFvr33nyFsCq3zInXZMPj7EG/T7KoKRKsMwZspEDVdeyLF+I2poj5CHJZRx1kETPTiDHnuCeMExqfYOGST4tGu3CyM6ZYn9mpL+NxT73tURm7lYNMBmOfJgNmKchYFXI7XB1x6HROtbvU2bfWUBK3r9Uu1Cb39Km7hvBoicUOen9OLU9IYyQ6CCn2zjr3CKXnT6LcGhRvwq2nkIujrdUIdRv8Q778rl7TI2DgtOXMA9LX6Ka4PRz1h0/3oGeeip7aZTHFLW0S0xREJB5KLpOUzEjQm9AP6UhQ35ykTMtX9BqHPAyqhqMKr/M7GZM/xIC0xzaatN5aITk8yNsA40LqDvNPQZc/aX5sMkYrGDGkIjQcbyGQhYjYfrH8060+tG2F9RfVmeQwCNPVPgpnxZ1oiMwLFsS21mEWq8L9zRh47Z+N7xDldZnPHtkSVyoFUObMPV5sL662aR9jK/rYvdNkraIdKaC8Hm9vML7JFls4lPKTuFQQOb3mbbZxDpNh6W8IDl7C93MFCi+8f6taoz5XkgkHwB3C5/i7gtx8JdAD9waqhZfQ9vS7Xd4pxHb2WxJEeUhEkHc6T4mM9WkyeVlfs7SKvH1TPOAlUMJDXdgjLEGdjuLdWXztYQfuQ5bSAaaGd9Qd7JGu5+4lIjOYkJhmf/AuNcIcsh90ScmT0H3t0RVFwkQQkkoICGAI3fa8nHNV1Tc1KwOUywUuUQZdVk8478VsNi2n+T1pk5WlTvRVhGs3TlV14onjgw4WXs7b0LDz0E35m6wvFWg+LXj8FoHQqNSvNrT5BkCphFcewqbxFOo5hgyCofvFGWv+PIwAvfQTgeGpoIj3Hl/KPTFchfFUHrGugPlDeYWtVfAfIwQUIPzzsQ/hyiq/ZjCyAvNuuii8YyOhN8YXUAUXR7dZSHCkbulLOoRS0Tv6QIlllAH8IhNj+EAD9CIBpM/fjdAEgD9RRABxbn9/2HF0F+pD03lPN3YWkS6e6a+/c40pPli4uNO1Uyu1Omat+ORO41OrLW/rUpHID1+B/oxPOfKnUYrU2TbcZ/8HRh+rnXI0R/JEWuliQEDNldx3iTZZOwpKcoGNGZXeWGifQsHuzgLxm5HXqKYoWTIUHwNSQBd9aZPzbG6KmBX9Lpy9LFKLQ4QTgD2a9AmY1LVWJ0XDcqvqJT3kj3+2rIbfPSXiANxvbZX3Rm5Dsy1E8c6Pdf/ZSi4JeW39Xwaei7Uxd0jabYiKwTlMlbUcSNyZId9OkTMozFod79KZMuh3caDdOlC3b5rUQZfUr8+dTZbcS5bNpsiwP648muxy4fGs0eTXkJutAnclN1rm8gURg+Rxd5GZ6vmNbC8+NHMPDEVL9ovJkMgEUdzix9Ng00irjG2ak1rU+ZdV32dghfK0q6Rb07za99fqc9SZw1kMF3VYp6McHStnXhPIfFt3RA9dVv9II8x7/Bw==</diagram></mxfile>"
  },
  {
    "path": "doc/design/secure-cluster-traffic.md",
    "content": "# Secure Alertmanager cluster traffic\n\nType: Design document\n\nDate: 2019-02-21\n\nAuthor: Max Inden <IndenML@gmail.com>\n\n\n## Status Quo\n\nAlertmanager supports [high\navailability](https://github.com/prometheus/alertmanager/blob/master/README.md#high-availability)\nby interconnecting multiple Alertmanager instances building an Alertmanager\ncluster. Instances of a cluster communicate on top of a gossip protocol managed\nvia Hashicorps [_Memberlist_](https://github.com/hashicorp/memberlist) library.\n_Memberlist_ uses two channels to communicate: TCP for reliable and UDP for\nbest-effort communication.\n\nAlertmanager instances use the gossip layer to:\n\n- Keep track of membership\n- Replicate silence creation, update and deletion\n- Replicate notification log\n\nAs of today the communication between Alertmanager instances in a cluster is\nsent in clear-text.\n\n\n## Goal\n\nInstances in a cluster should communicate among each other in a secure fashion.\nAlertmanager should guarantee confidentiality, integrity and client authenticity\nfor each message touching the wire. While this would improve the security of\nsingle datacenter deployments, one could see this as a necessity for\nwide-area-network deployments.\n\n\n## Non-Goal\n\nEven though solutions might also be applicable to the API endpoints exposed by\nAlertmanager, it is not the goal of this design document to secure the API\nendpoints.\n\n\n## Proposed Solution - TLS Memberlist\n\n_Memberlist_ enables users to implement their own [transport\nlayer](https://godoc.org/github.com/hashicorp/memberlist#Transport) without the\nneed of forking the library itself. That transport layer needs to support\nreliable as well as best-effort communication. Instead of using TCP and UDP like\nthe default transport layer of _Memberlist_, the suggestion is to only use TCP\nfor both reliable as well as best-effort communication. On top of that TCP\nlayer, one can use mutual TLS to secure all communication. A proof-of-concept\nimplementation can be found here:\nhttps://github.com/mxinden/memberlist-tls-transport.\n\nThe data gossiped between instances does not have a low-latency requirement that\nTCP could not fulfill, same would apply for the relatively low data throughput\nrequirements of Alertmanager.\n\nTCP connections could be kept alive beyond a single message to reduce latency as\nwell as handshake overhead costs. While this is feasible in a 3-instance\nAlertmanager cluster, the discussed custom implementation would need to limit\nthe amount of open connections for clusters with many instances (#connections =\nn*(n-1)/2).\n\nAs of today, Alertmanager already forces _Memberlist_ to use the reliable TCP\ninstead of the best-effort UDP connection to gossip large notification logs and\nsilences between instances. The reason is, that those packets would otherwise\nexceed the [MTU](https://en.wikipedia.org/wiki/Maximum_transmission_unit) of\nmost UDP setups. Splitting packets is not supported by _Memberlist_ and was not\nconsidered worth the effort to be implemented in Alertmanager either. For more\ninfo see this [Github\nissue](https://github.com/prometheus/alertmanager/issues/1412).\n\nWith the last [Prometheus developer\nsummit](https://docs.google.com/document/d/1-C5PycocOZEVIPrmM1hn8fBelShqtqiAmFptoG4yK70/edit)\nin mind, the Prometheus projects preferred security mechanism seems to be mutual\nTLS. Having Alertmanager use the same mechanism would ease deployment with the\nrest of the Prometheus stack.\n\nAs a side effect (benefit) Alertmanager would only need a single open port (TCP\ntraffic) instead of two open ports (TCP and UDP traffic) for cluster\ncommunication. This does not affect the API endpoint which remains a separate\nTCP port.\n\n\n## Alternative Solutions\n\n### Symmetric Memberlist\n\n_Memberlist_ supports [symmetric key\nencryption](https://godoc.org/github.com/hashicorp/memberlist#Keyring) via\nAES-128, AES-192 or AES-256 ciphers. One can specify multiple keys for rolling\nupdates. Securing the cluster traffic via symmetric encryption would just\ninvolve small configuration changes in the Alertmanager code base.\n\n\n### Replace Memberlist\n\nCoordinating membership might not be required by the Alertmanager cluster\ncomponent. Instead this could be bound to static configuration or e.g. DNS\nservice discovery. On the other hand, gossiping silences and notifications is\nideally done in an eventual consistent gossip fashion, given that Alertmanager\nis supposed to scale beyond a 3-instance cluster and beyond local-area-network\ndeployments. With these requirements in mind, replacing _Memberlist_ with an\nentirely self-built communication layer is a great undertaking.\n\n\n### TLS Memberlist with DTLS\n\nInstead of redirecting all best-effort traffic via the reliable channel as\nproposed above, one could also secure the best-effort channel itself using UDP\nand [DTLS](https://en.wikipedia.org/wiki/Datagram_Transport_Layer_Security) in\naddition to securing the reliable traffic via TCP and TLS. DTLS is not supported\nby the Golang standard library.\n"
  },
  {
    "path": "doc/examples/simple.yml",
    "content": "global:\n  # The smarthost and SMTP sender used for mail notifications.\n  smtp_smarthost: 'localhost:25'\n  smtp_from: 'alertmanager@example.org'\n  smtp_auth_username: 'alertmanager'\n  smtp_auth_password: 'password'\n\n# The directory from which notification templates are read.\ntemplates:\n  - '/etc/alertmanager/template/*.tmpl'\n\n# The root route on which each incoming alert enters.\nroute:\n  # The labels by which incoming alerts are grouped together. For example,\n  # multiple alerts coming in for cluster=A and alertname=LatencyHigh would\n  # be batched into a single group.\n  #\n  # To aggregate by all possible labels use '...' as the sole label name.\n  # This effectively disables aggregation entirely, passing through all\n  # alerts as-is. This is unlikely to be what you want, unless you have\n  # a very low alert volume or your upstream notification system performs\n  # its own grouping. Example: group_by: [...]\n  group_by: ['alertname', 'cluster', 'service']\n\n  # When a new group of alerts is created by an incoming alert, wait at\n  # least 'group_wait' to send the initial notification.\n  # This way ensures that you get multiple alerts for the same group that start\n  # firing shortly after another are batched together on the first\n  # notification.\n  group_wait: 30s\n\n  # When the first notification was sent, wait 'group_interval' to send a batch\n  # of new alerts that started firing for that group.\n  group_interval: 5m\n\n  # If an alert has successfully been sent, wait 'repeat_interval' to\n  # resend them.\n  repeat_interval: 3h\n\n  # A default receiver\n  receiver: team-X-mails\n\n  # All the above attributes are inherited by all child routes and can\n  # overwritten on each.\n\n  # The child route trees.\n  routes:\n    # This routes performs a regular expression match on alert labels to\n    # catch alerts that are related to a list of services.\n    - matchers:\n        - service=~\"foo1|foo2|baz\"\n      receiver: team-X-mails\n      # The service has a sub-route for critical alerts, any alerts\n      # that do not match, i.e. severity != critical, fall-back to the\n      # parent node and are sent to 'team-X-mails'\n      routes:\n        - matchers:\n            - severity=\"critical\"\n          receiver: team-X-pager\n    - matchers:\n        - service=\"files\"\n      receiver: team-Y-mails\n\n      routes:\n        - matchers:\n            - severity=\"critical\"\n          receiver: team-Y-pager\n\n    # This route handles all alerts coming from a database service. If there's\n    # no team to handle it, it defaults to the DB team.\n    - matchers:\n        - service=\"database\"\n      receiver: team-DB-pager\n      # Also group alerts by affected database.\n      group_by: [alertname, cluster, database]\n      routes:\n        - matchers:\n            - owner=\"team-X\"\n          receiver: team-X-pager\n          continue: true\n        - matchers:\n            - owner=\"team-Y\"\n          receiver: team-Y-pager\n\n\n# Inhibition rules allow to mute a set of alerts given that another alert is\n# firing.\n# We use this to mute any warning-level notifications if the same alert is\n# already critical.\ninhibit_rules:\n  - source_matchers: [severity=\"critical\"]\n    target_matchers: [severity=\"warning\"]\n    # Apply inhibition if the alertname is the same.\n    # CAUTION:\n    #   If all label names listed in `equal` are missing\n    #   from both the source and target alerts,\n    #   the inhibition rule will apply!\n    equal: [alertname, cluster, service]\n\n\nreceivers:\n  - name: 'team-X-mails'\n    email_configs:\n      - to: 'team-X+alerts@example.org'\n\n  - name: 'team-X-pager'\n    email_configs:\n      - to: 'team-X+alerts-critical@example.org'\n    pagerduty_configs:\n      - service_key: <team-X-key>\n\n  - name: 'team-Y-mails'\n    email_configs:\n      - to: 'team-Y+alerts@example.org'\n\n  - name: 'team-Y-pager'\n    pagerduty_configs:\n      - service_key: <team-Y-key>\n\n  - name: 'team-DB-pager'\n    pagerduty_configs:\n      - service_key: <team-DB-key>\n\ntracing:\n  endpoint: localhost:4317\n  insecure: true\n  sampling_fraction: 1.0\n"
  },
  {
    "path": "docs/alertmanager.md",
    "content": "---\ntitle: Alertmanager\nsort_rank: 2\nnav_icon: sliders\n---\n\nThe [Alertmanager](https://github.com/prometheus/alertmanager) handles alerts\nsent by client applications such as the Prometheus server.\nIt takes care of deduplicating, grouping, and routing\nthem to the correct receiver integration such as email, PagerDuty, or OpsGenie.\nIt also takes care of silencing and inhibition of alerts.\n\nThe following describes the core concepts the Alertmanager implements. Consult\nthe [configuration documentation](configuration.md) to learn how to use them\nin more detail.\n\n## Grouping\n\nGrouping categorizes alerts of similar nature into a single notification. This\nis especially useful during larger outages when many systems fail at once and\nhundreds to thousands of alerts may be firing simultaneously.\n\n**Example:** Dozens or hundreds of instances of a service are running in your\ncluster when a network partition occurs. Half of your service instances\ncan no longer reach the database.\nAlerting rules in Prometheus were configured to send an alert for each service\ninstance if it cannot communicate with the database. As a result hundreds of\nalerts are sent to Alertmanager.\n\nAs a user, one only wants to get a single page while still being able to see\nexactly which service instances were affected. Thus one can configure\nAlertmanager to group alerts by their cluster and alertname so it sends a\nsingle compact notification.\n\nGrouping of alerts, timing for the grouped notifications, and the receivers\nof those notifications are configured by a routing tree in the configuration\nfile.\n\n## Inhibition\n\nInhibition is a concept of suppressing notifications for certain alerts if\ncertain other alerts are already firing.\n\n**Example:** An alert is firing that informs that an entire cluster is not\nreachable. Alertmanager can be configured to mute all other alerts concerning\nthis cluster if that particular alert is firing.\nThis prevents notifications for hundreds or thousands of firing alerts that\nare unrelated to the actual issue.\n\nInhibitions are configured through the Alertmanager's configuration file.\n\n## Silences\n\nSilences are a straightforward way to simply mute alerts for a given time.\nA silence is configured based on matchers, just like the routing tree. Incoming\nalerts are checked whether they match all the equality or regular expression\nmatchers of an active silence.\nIf they do, no notifications will be sent out for that alert.\n\nSilences are configured in the web interface of the Alertmanager.\n\n\n## Client behavior\n\nThe Alertmanager has [special requirements](alerts_api.md) for behavior of its\nclient. Those are only relevant for advanced use cases where Prometheus\nis not used to send alerts.\n\n## High Availability\n\nAlertmanager supports configuration to create a cluster for high availability.\nThis can be configured using the [--cluster-*](https://github.com/prometheus/alertmanager#high-availability) flags.\n\nIt's important not to load balance traffic between Prometheus and its Alertmanagers, but instead, point Prometheus to a list of all Alertmanagers.\n\n## Alert limits (optional)\n\nAlertmanager supports configuration to limit the number of active alerts per alertname.\nThis can be configured using the [--alerts.per-alertname-limit] flag.\n\nWhen the limit is reached any new alerts are dropped, heartbeats from already know alerts are processed.\nThe known alert (fingerprint) automatically expire to make room for new alerts.\n\nThis feature is useful when an unexpected high number of instances of the same alert are sent to Alertmanager.\nLimiting the number of alerts per alertname can prevent reliability issues and avoid alert receivers from being flooded.\n\nThe `alertmanager_alerts_limited_total` metric shows the total number of alerts that were dropped due to per alert name limit.\nEnabling the `alert-names-in-metrics` feature flag will add the `alertname` label to the metric."
  },
  {
    "path": "docs/alerts_api.md",
    "content": "---\ntitle: Alerts API\nsort_rank: 6\nnav_icon: sliders\n---\n\n**Important**: Prometheus takes care of sending alerts to the Alertmanager.\nIt is recommended to configure alerting rules in Prometheus based on time\nseries data instead of sending alerts to the Alerts API, as Prometheus supports\na number of special cases to make sure alerts are delivered even if Alertmanager\ncrashes or restarts.\n\nYou send alerts to Alertmanager via APIv2. The APIv2 is specified as an\nOpenAPI specification that can be found [here](https://github.com/prometheus/alertmanager/blob/master/api/v2/openapi.yaml).\n\nAPIv1 was deprecated in Alertmanager version 0.16.0 and removed in Alertmanager\nversion 0.27.0.\n\nTo send alerts to APIv2 make a POST request to `api/v2/alerts`. You must set\nthe `Content-Type` header to `application/json`, and send JSON data containing\nan array of alerts.\n\nHere is an example:\n\n```json\n[\n  {\n    \"labels\": {\n      \"alertname\": \"<required_value>\",\n      \"<name>\": \"<value>\",\n      ...\n    },\n    \"annotations\": {\n      \"<name>\": \"<value>\",\n    },\n    \"startsAt\": \"<RFC3339>\",\n    \"endsAt\": \"<RFC3339>\",\n    \"generatorURL\": \"<value>\"\n  },\n  ...\n]\n```\n\nAll alerts have labels, annotations, an optional `startsAt` timestamp and an\noptional `endsAt` timestamp. All timestamps are expected in the RFC3339 format.\n\nLabels are used to deduplicate identical instances of the same alert, while\nannotations are used to include other information about the alert, such as a\nsummary, description or a URL to a runbook.\n\nThe `startsAt` timestamp is the time the alert fired. If omitted, Alertmanager\nsets `startsAt` to the current time.\n\nThe `endsAt` timestamp is the time the alert should be resolved. If omitted,\nAlertmanager sets `endsAt` to the current time + `resolve_timeout`.\n\nThe `generatorURL` is a unique URL which links to the source of the alert. For\nexample, it might link to the firing rule in Prometheus.\n\n## Expectations from clients\n\nClients are expected to re-send firing alerts to the Alertmanager at regular\nintervals until the alert is resolved.\n\nThe exact interval depends on a number of variables such as the `endsAt`\ntimestamp, or if omitted the value of `resolve_timeout`. If the `endsAt`\ntimestamp is omitted, the Alertmanager will update the existing `endsAt`\ntimestamp for the alert to the current time + `resolve_timeout`.\n\nFiring alerts are resolved once their `endsAt` timestamp has elapsed.\n\nTo ensure resolved notifications are sent for resolved alerts, clients are also\nexpected to re-send resolved alerts to the Alertmanager for up to 5 minutes\nafter the alert has resolved. As the Alertmanager is stateless, this ensures\nthat a resolved notification is sent even if the Alertmanager crashes or is\nrestarted.\n"
  },
  {
    "path": "docs/configuration.md",
    "content": "---\ntitle: Configuration\nsort_rank: 3\nnav_icon: sliders\n---\n\n[Alertmanager](https://github.com/prometheus/alertmanager) is configured via\ncommand-line flags and a configuration file.\nWhile the command-line flags configure immutable system parameters, the\nconfiguration file defines inhibition rules, notification routing and\nnotification receivers.\n\nThe [visual editor](https://www.prometheus.io/webtools/alerting/routing-tree-editor)\ncan assist in building routing trees.\n\nTo view all available command-line flags, run `alertmanager -h`.\n\nAlertmanager can reload its configuration at runtime. If the new configuration\nis not well-formed, the changes will not be applied and an error is logged.\nA configuration reload is triggered by sending a `SIGHUP` to the process or\nsending an HTTP POST request to the `/-/reload` endpoint.\n\n## Limits\n\nAlertmanager supports a number of configurable limits via command-line flags.\n\nTo limit the maximum number of silences, including expired ones,\nuse the `--silences.max-silences` flag.\nYou can limit the maximum size of individual silences with `--silences.max-silence-size-bytes`,\nwhere the unit is in bytes.\n\nBoth limits are disabled by default.\n\n## Configuration file introduction\n\nTo specify which configuration file to load, use the `--config.file` flag.\n\n```bash\n./alertmanager --config.file=alertmanager.yml\n```\n\nThe file is written in the [YAML format](http://en.wikipedia.org/wiki/YAML),\ndefined by the scheme described below.\nBrackets indicate that a parameter is optional. For non-list parameters the\nvalue is set to the specified default.\n\nGeneric placeholders are defined as follows:\n\n* `<duration>`: a duration matching the regular expression `((([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?|0)`, e.g. `1d`, `1h30m`, `5m`, `10s`\n* `<labelname>`: a string matching the regular expression `[a-zA-Z_][a-zA-Z0-9_]*`\n* `<labelvalue>`: a string of unicode characters\n* `<filepath>`: a valid path in the current working directory\n* `<boolean>`: a boolean that can take the values `true` or `false`\n* `<string>`: a regular string\n* `<secret>`: a regular string that is a secret, such as a password\n* `<tmpl_string>`: a string which is template-expanded before usage\n* `<tmpl_secret>`: a string which is template-expanded before usage that is a secret\n* `<int>`: an integer value\n* `<regex>`: any valid [RE2 regular expression](https://github.com/google/re2/wiki/Syntax) (The regex is anchored on both ends. To un-anchor the regex, use `.*<regex>.*`.)\n\nThe other placeholders are specified separately.\n\nA provided [valid example file](https://github.com/prometheus/alertmanager/blob/main/doc/examples/simple.yml)\nshows usage in context.\n\n## File layout and global settings\n\nThe global configuration specifies parameters that are valid in all other\nconfiguration contexts. They also serve as defaults for other configuration\nsections. The other top-level sections are documented below on this page.\n\n```yaml\nglobal:\n  # The default SMTP From header field.\n  [ smtp_from: <tmpl_string> ]\n  # The default SMTP smarthost used for sending emails, including port number.\n  # Port number usually is 25, or 587 for SMTP over TLS (sometimes referred to as STARTTLS).\n  # Example: smtp.example.org:587\n  [ smtp_smarthost: <string> ]\n  # The default hostname to identify to the SMTP server.\n  [ smtp_hello: <string> | default = \"localhost\" ]\n  # SMTP Auth using CRAM-MD5, LOGIN and PLAIN. If empty, Alertmanager doesn't authenticate to the SMTP server.\n  # PLAIN is only supported when using TLS.\n  [ smtp_auth_username: <string> ]\n  # SMTP Auth using LOGIN and PLAIN.\n  [ smtp_auth_password: <secret> ]\n  # SMTP Auth using LOGIN and PLAIN.\n  [ smtp_auth_password_file: <string> ]\n  # SMTP Auth using PLAIN.\n  [ smtp_auth_identity: <string> ]\n  # SMTP Auth using CRAM-MD5.\n  [ smtp_auth_secret: <secret> ]\n  # SMTP Auth using CRAM-MD5.\n  [ smtp_auth_secret_file: <string> ]\n  # The default SMTP TLS requirement.\n  # Note that Go does not support unencrypted connections to remote SMTP endpoints.\n  [ smtp_require_tls: <bool> | default = true ]\n  # The default TLS configuration for SMTP receivers\n  [ smtp_tls_config: <tls_config> ]\n  # Force implicit TLS regardless of SMTP port\n  [ smtp_force_implicit_tls: <bool>]\n\n  # Default settings for the JIRA integration.\n  [ jira_api_url: <string> ]\n\n  # The API URL to use for Slack notifications.\n  [ slack_api_url: <secret> ]\n  [ slack_api_url_file: <filepath> ]\n  [ slack_app_token: <secret> ]\n  [ slack_app_token_file: <filepath> ]\n  [ slack_app_url: <string> ]\n\n  [ victorops_api_key: <secret> ]\n  [ victorops_api_key_file: <filepath> ]\n  [ victorops_api_url: <string> | default = \"https://alert.victorops.com/integrations/generic/20131114/alert/\" ]\n  [ pagerduty_url: <string> | default = \"https://events.pagerduty.com/v2/enqueue\" ]\n  [ opsgenie_api_key: <secret> ]\n  [ opsgenie_api_key_file: <filepath> ]\n  [ opsgenie_api_url: <string> | default = \"https://api.opsgenie.com/\" ]\n  [ rocketchat_api_url: <string> | default = \"https://open.rocket.chat/\" ]\n  [ rocketchat_token: <secret> ]\n  [ rocketchat_token_file: <filepath> ]\n  [ rocketchat_token_id: <secret> ]\n  [ rocketchat_token_id_file: <filepath> ]\n  [ wechat_api_url: <string> | default = \"https://qyapi.weixin.qq.com/cgi-bin/\" ]\n  [ wechat_api_secret: <secret> ]\n  [ wechat_api_secret_file: <string> ]\n  [ wechat_api_corp_id: <string> ]\n  [ telegram_api_url: <string> | default = \"https://api.telegram.org\" ]\n  [ telegram_bot_token: <secret> ]\n  [ telegram_bot_token_file: <string> ]\n  [ webex_api_url: <string> | default = \"https://webexapis.com/v1/messages\" ]\n  [ mattermost_webhook_url: <secret> ]\n  [ mattermost_webhook_url_file: <string> ]\n  # The default HTTP client configuration\n  [ http_config: <http_config> ]\n\n  # ResolveTimeout is the default value used by alertmanager if the alert does\n  # not include EndsAt, after this time passes it can declare the alert as resolved if it has not been updated.\n  # This has no impact on alerts from Prometheus, as they always include EndsAt.\n  [ resolve_timeout: <duration> | default = 5m ]\n\n# Files from which custom notification template definitions are read.\n# The last component may use a wildcard matcher, e.g. 'templates/*.tmpl'.\ntemplates:\n  [ - <filepath> ... ]\n\n# The root node of the routing tree.\nroute: <route>\n\n# A list of notification receivers.\nreceivers:\n  - <receiver> ...\n\n# A list of inhibition rules.\ninhibit_rules:\n  [ - <inhibit_rule> ... ]\n\n# DEPRECATED: use time_intervals below.\n# A list of mute time intervals for muting routes.\nmute_time_intervals:\n  [ - <time_interval> ... ]\n\n# A list of time intervals for muting/activating routes.\ntime_intervals:\n  [ - <time_interval> ... ]\n```\n\n## Route-related settings\n\nRouting-related settings allow configuring how alerts are routed, aggregated, throttled, and muted based on time.\n\n### `<route>`\n\nA route block defines a node in a routing tree and its children. Its optional\nconfiguration parameters are inherited from its parent node if not set.\n\nEvery alert enters the routing tree at the configured top-level route, which\nmust match all alerts (i.e. not have any configured matchers).\nIt then traverses the child nodes. If `continue` is set to false, it stops\nafter the first matching child. If `continue` is true on a matching node, the\nalert will continue matching against subsequent siblings.\nIf an alert does not match any children of a node (no matching child nodes, or\nnone exist), the alert is handled based on the configuration parameters of the\ncurrent node.\n\nSee [Alertmanager concepts](https://prometheus.io/docs/alerting/alertmanager/#grouping) for more information on grouping.\n\n```yaml\n[ receiver: <string> ]\n# The labels by which incoming alerts are grouped together. For example,\n# multiple alerts coming in for cluster=A and alertname=LatencyHigh would\n# be batched into a single group.\n#\n# To aggregate by all possible labels use the special value '...' as the sole label name, for example:\n# group_by: ['...']\n# This effectively disables aggregation entirely, passing through all\n# alerts as-is. This is unlikely to be what you want, unless you have\n# a very low alert volume or your upstream notification system performs\n# its own grouping.\n[ group_by: '[' <labelname>, ... ']' ]\n\n# Whether an alert should continue matching subsequent sibling nodes.\n[ continue: <boolean> | default = false ]\n\n# DEPRECATED: Use matchers below.\n# A set of equality matchers an alert has to fulfill to match the node.\nmatch:\n  [ <labelname>: <labelvalue>, ... ]\n\n# DEPRECATED: Use matchers below.\n# A set of regex-matchers an alert has to fulfill to match the node.\nmatch_re:\n  [ <labelname>: <regex>, ... ]\n\n# A list of matchers that an alert has to fulfill to match the node.\nmatchers:\n  [ - <matcher> ... ]\n\n# How long to wait before sending the first notification for a new group of\n# alerts. Allows to wait for alerts to arrive from other rule groups or\n# Prometheus servers, and for one or more inhibiting alerts to arrive and mute\n# any target alerts before the first notification.\n#\n# A short group_wait will reduce the time to wait before sending the first\n# notification for a new group of alerts. However, if group_wait is too short\n# then the first notification might not contain the complete set of expected\n# alerts, and alerts that should be inhibited might not be inhibited if the\n# inhibiting alerts have not arrived in time.\n#\n# A long group_wait will increase the time to wait before sending the first\n# notification for a new group of alerts. However, if group_wait is too long\n# then notifications for firing alerts might not be sent within a reasonable\n# time.\n#\n# If an alert is resolved before group_wait has elapsed, no notification will\n# be sent for that alert. This reduces noise of flapping alerts.\n\n# A notification for any alerts that missed the initial group_wait will be\n# sent at the next group_interval instead.\n#\n# If omitted, child routes inherit the group_wait of the parent route.\n[ group_wait: <duration> | default = 30s ]\n\n# How long to wait before sending subsequent notifications for an existing\n# group of alerts after group_wait.\n#\n# The group_interval is a recurring timer that starts as soon as group_wait\n# has elapsed. At each group_interval, Alertmanager checks if any new alerts\n# have fired or any firing alerts have resolved since the last group_interval,\n# and if they have a notification is sent. If they haven't, Alertmanager checks\n# if the repeat_interval has elapsed instead.\n#\n# Note: group_interval also sets the context timeout for the notification\n# pipeline for each send. So if sending a notification takes longer than the\n# group_interval, the notification will get canceled. This can happen with\n# small group_interval values and slow notification receivers.\n#\n# If omitted, child routes inherit the group_interval of the parent route.\n[ group_interval: <duration> | default = 5m ]\n\n# How long to wait before repeating the last notification. Notifications are\n# not repeated if any new alerts have fired or any firing alerts have resolved\n# since the last group_interval.\n#\n# Since the repeat_interval is checked after each group_interval, it should\n# be a multiple of the group_interval. If it's not, the repeat_interval\n# is rounded up to the next multiple of the group_interval.\n#\n# In addition, if repeat_interval is longer then `--data.retention`, the\n# notification will be repeated at the end of the data retention period\n# instead.\n#\n# If omitted, child routes inherit the repeat_interval of the parent route.\n[ repeat_interval: <duration> | default = 4h ]\n\n# Times when the route should be muted. These must match the name of a\n# time interval defined in the time_intervals section.\n# Additionally, the root node cannot have any mute times.\n# When a route is muted it will not send any notifications, but\n# otherwise acts normally (including ending the route-matching process\n# if the `continue` option is not set.)\nmute_time_intervals:\n  [ - <string> ...]\n\n# Times when the route should be active. These must match the name of a\n# time interval defined in the time_intervals section. An empty value\n# means that the route is always active.\n# Additionally, the root node cannot have any active times.\n# The route will send notifications only when active, but otherwise\n# acts normally (including ending the route-matching process\n# if the `continue` option is not set).\nactive_time_intervals:\n  [ - <string> ...]\n\n# Zero or more child routes.\nroutes:\n  [ - <route> ... ]\n```\n\n#### Example\n\n```yaml\n# The root route with all parameters, which are inherited by the child\n# routes if they are not overwritten.\nroute:\n  receiver: 'default-receiver'\n  group_wait: 30s\n  group_interval: 5m\n  repeat_interval: 4h\n  group_by: [cluster, alertname]\n  # All alerts that do not match the following child routes\n  # will remain at the root node and be dispatched to 'default-receiver'.\n  routes:\n  # All alerts with service=mysql or service=cassandra\n  # are dispatched to the database pager.\n  - receiver: 'database-pager'\n    group_wait: 10s\n    matchers:\n    - service=~\"mysql|cassandra\"\n  # All alerts with the team=frontend label match this sub-route.\n  # They are grouped by product and environment rather than cluster\n  # and alertname.\n  - receiver: 'frontend-pager'\n    group_by: [product, environment]\n    matchers:\n    - team=\"frontend\"\n\n  # All alerts with the service=inhouse-service label match this sub-route.\n  # the route will be muted during offhours and holidays time intervals.\n  # even if it matches, it will continue to the next sub-route\n  - receiver: 'dev-pager'\n    matchers:\n      - service=\"inhouse-service\"\n    mute_time_intervals:\n      - offhours\n      - holidays\n    continue: true\n\n    # All alerts with the service=inhouse-service label match this sub-route\n    # the route will be active only during offhours and holidays time intervals.\n  - receiver: 'on-call-pager'\n    matchers:\n      - service=\"inhouse-service\"\n    active_time_intervals:\n      - offhours\n      - holidays\n```\n\n### `<time_interval>`\n\nA `time_interval` specifies a named interval of time that may be referenced\nin the routing tree to mute/activate particular routes for particular times of the day.\n\n```yaml\nname: <string>\ntime_intervals:\n  [ - <time_interval_spec> ... ]\n```\n\n#### `<time_interval_spec>`\n\nA `time_interval_spec` contains the actual definition for an interval of time. The syntax\nsupports the following fields:\n\n```yaml\n- times:\n  [ - <time_range> ...]\n  weekdays:\n  [ - <weekday_range> ...]\n  days_of_month:\n  [ - <days_of_month_range> ...]\n  months:\n  [ - <month_range> ...]\n  years:\n  [ - <year_range> ...]\n  location: <string>\n```\n\nAll fields are lists. Within each non-empty list, at least one element must be satisfied to match\nthe field. If a field is left unspecified, any value will match the field. For an instant of time\nto match a complete time interval, all fields must match.\nSome fields support ranges and negative indices, and are detailed below. If a time zone is not\nspecified, then the times are taken to be in UTC.\n\n`time_range`: Ranges inclusive of the starting time and exclusive of the end time to\nmake it easy to represent times that start/end on hour boundaries.\nFor example, `start_time: '17:00'` and `end_time: '24:00'` will begin at 17:00 and finish\nimmediately before 24:00. They are specified like so:\n\n```yaml\ntimes:\n- start_time: HH:MM\n  end_time: HH:MM\n```\n\n`weekday_range`: A list of days of the week, where the week begins on Sunday and ends on Saturday.\nDays should be specified by name (e.g. 'Sunday'). For convenience, ranges are also accepted\nof the form `<start_day>:<end_day>` and are inclusive on both ends. For example:\n`['monday:wednesday','saturday', 'sunday']`\n\n`days_of_month_range`: A list of numerical days in the month. Days begin at 1.\nNegative values are also accepted which begin at the end of the month,\ne.g. -1 during January would represent January 31. For example: `['1:5', '-3:-1']`.\nExtending past the start or end of the month will cause it to be clamped. E.g. specifying\n`['1:31']` during February will clamp the actual end date to 28 or 29 depending on leap years.\nInclusive on both ends.\n\n`month_range`: A list of calendar months identified by a case-insensitive name (e.g. 'January') or by number,\nwhere January = 1. Ranges are also accepted. For example, `['1:3', 'may:august', 'december']`.\nInclusive on both ends.\n\n`year_range`: A numerical list of years. Ranges are accepted. For example, `['2020:2022', '2030']`.\nInclusive on both ends.\n\n`location`: A string that matches a location in the IANA time zone database. For\nexample, `'Australia/Sydney'`. The location provides the time zone for the time\ninterval. For example, a time interval with a location of `'Australia/Sydney'` that\ncontained something like:\n\n```yaml\ntimes:\n- start_time: 09:00\n  end_time: 17:00\nweekdays: ['monday:friday']\n```\n\nwould include any time that fell between the hours of 9:00AM and 5:00PM, between Monday\nand Friday, using the local time in Sydney, Australia.\n\nYou may also use `'Local'` as a location to use the local time of the machine where\nAlertmanager is running, or `'UTC'` for UTC time. If no timezone is provided, the time\ninterval is taken to be in UTC time.**Note:** On Windows, only `Local` or `UTC` are\nsupported unless you provide a custom time zone database using the `ZONEINFO`\nenvironment variable.\n\n## Inhibition-related settings\n\nInhibition allows muting a set of alerts based on the presence of another set of\nalerts. This allows establishing dependencies between systems or services such that\nonly the most relevant of a set of interconnected alerts are sent out during an outage.\n\nSee [Alertmanager concepts](https://prometheus.io/docs/alerting/alertmanager/#inhibition) for more information on inhibition.\n\n### `<inhibit_rule>`\n\nAn inhibition rule mutes an alert (target) matching a set of matchers\nwhen an alert (source) exists that matches another set of matchers.\nBoth target and source alerts must have the same label values\nfor the label names in the `equal` list.\n\nSemantically, a missing label and a label with an empty value are the same\nthing. Therefore, if all the label names listed in `equal` are missing from\nboth the source and target alerts, the inhibition rule will apply.\n\nTo prevent an alert from inhibiting itself, an alert that matches _both_ the\ntarget and the source side of a rule cannot be inhibited by alerts for which\nthe same is true (including itself). However, we recommend to choose target and\nsource matchers in a way that alerts never match both sides. It is much easier\nto reason about and does not trigger this special case.\n\n```yaml\n# Optional name of the inhibition rule.\n# Duplicate names are allowed but will affect the per-rule metrics.\nname: <string>\n\n# DEPRECATED: Use target_matchers below.\n# Matchers that have to be fulfilled in the alerts to be muted.\ntarget_match:\n  [ <labelname>: <labelvalue>, ... ]\n# DEPRECATED: Use target_matchers below.\ntarget_match_re:\n  [ <labelname>: <regex>, ... ]\n\n# A list of matchers that have to be fulfilled by the target\n# alerts to be muted.\ntarget_matchers:\n  [ - <matcher> ... ]\n\n# DEPRECATED: Use source_matchers below.\n# Matchers for which one or more alerts have to exist for the\n# inhibition to take effect.\nsource_match:\n  [ <labelname>: <labelvalue>, ... ]\n# DEPRECATED: Use source_matchers below.\nsource_match_re:\n  [ <labelname>: <regex>, ... ]\n\n# A list of matchers for which one or more alerts have\n# to exist for the inhibition to take effect.\nsource_matchers:\n  [ - <matcher> ... ]\n\n# Labels that must have an equal value in the source and target\n# alert for the inhibition to take effect.\n[ equal: '[' <labelname>, ... ']' ]\n```\n\n## Label matchers\n\nLabel matchers match alerts to routes, silences, and inhibition rules.\n\n**Important**: Prometheus is adding support for UTF-8 in labels and metrics. In order to also support UTF-8 in the Alertmanager, Alertmanager versions 0.27 and later have a new parser for matchers that has a number of backwards incompatible changes. While most matchers will be forward-compatible, some will not. Alertmanager is operating a transition period where it supports both UTF-8 and classic matchers, and has provided a number of tools to help you prepare for the transition.\n\nIf this is a new Alertmanager installation, we recommend enabling UTF-8 strict mode before creating an Alertmanager configuration file. You can find instructions on how to enable UTF-8 strict mode [here](#utf-8-strict-mode).\n\nIf this is an existing Alertmanager installation, we recommend running the Alertmanager in the default mode called fallback mode before enabling UTF-8 strict mode. In this mode, Alertmanager will log a warning if you need to make any changes to your configuration file before UTF-8 strict mode can be enabled. Alertmanager will make UTF-8 strict mode the default in the next two versions, so it's important to transition as soon as possible.\n\nIrrespective of whether an Alertmanager installation is a new or existing installation, you can also use `amtool` to validate that an Alertmanager configuration file is compatible with UTF-8 strict mode before enabling it in Alertmanager server. You do not need a running Alertmanager server to do this. You can find instructions on how to validate an Alertmanager configuration file using `amtool` [here](#verification).\n\n### Alertmanager server operational modes\n\nDuring the transition period, Alertmanager supports three modes of operation. These are known as fallback mode, UTF-8 strict mode and classic mode. Fallback mode is the default mode.\n\nOperators of Alertmanager servers should transition to UTF-8 strict mode before the end of the transition period. Alertmanager will make UTF-8 strict mode the default in the next two versions, so it's important to transition as soon as possible.\n\n#### Fallback mode\n\nAlertmanager runs in a special mode called fallback mode as its default mode. As operators, you should not experience any difference in how your routes, silences or inhibition rules work.\n\nIn fallback mode, configurations are first parsed as UTF-8 matchers, and if incompatible with the UTF-8 parser, are then parsed as classic matchers. If your Alertmanager configuration contains matchers that are incompatible with the UTF-8 parser, Alertmanager will parse them as classic matchers and log a warning. This warning also includes a suggestion on how to change the matchers from classic matchers to UTF-8 matchers. For example:\n\n```\nts=2024-02-11T10:00:00Z caller=parse.go:176 level=warn msg=\"Alertmanager is moving to a new parser for labels and matchers, and this input is incompatible. Alertmanager has instead parsed the input using the classic matchers parser as a fallback. To make this input compatible with the UTF-8 matchers parser please make sure all regular expressions and values are double-quoted and backslashes are escaped. If you are still seeing this message please open an issue.\" input=\"foo=\" origin=config err=\"end of input: expected label value\" suggestion=\"foo=\\\"\\\"\"\n```\n\nHere the matcher `foo=` can be made into a valid UTF-8 matcher by double quoting the right hand side of the expression to give `foo=\"\"`. These two matchers are equivalent, however with UTF-8 matchers the right hand side of the matcher is a required field.\n\nIn rare cases, a configuration can cause disagreement between the UTF-8 and classic parser. This happens when a matcher is valid in both parsers, but due to added support for UTF-8, results in different parsings depending on which parser is used. If your Alertmanager configuration has disagreement, Alertmanager will use the classic parser and log a warning. For example:\n\n```\nts=2024-02-11T10:00:00Z caller=parse.go:183 level=warn msg=\"Matchers input has disagreement\" input=\"qux=\\\"\\\\xf0\\\\x9f\\\\x99\\\\x82\\\"\\n\" origin=config\n```\n\nAny occurrences of disagreement should be looked at on a case by case basis as depending on the nature of the disagreement, the configuration might not need updating before enabling UTF-8 strict mode. For example `\\xf0\\x9f\\x99\\x82` is the byte sequence for the 🙂 emoji. If the intention is to match a literal 🙂 emoji then no change is required. However, if the intention is to match the literal `\\xf0\\x9f\\x99\\x82` then the matcher should be changed to `qux=\"\\\\xf0\\\\x9f\\\\x99\\\\x82\"`.\n\n#### UTF-8 strict mode\n\nIn UTF-8 strict mode, Alertmanager disables support for classic matchers:\n\n```bash\nalertmanager --config.file=config.yml --enable-feature=\"utf8-strict-mode\"\n```\n\nThis mode should be enabled for new Alertmanager installations, and existing Alertmanager installations once all warnings of incompatible matchers have been resolved. Alertmanager will not start in UTF-8 strict mode until all the warnings of incompatible matchers have been resolved:\n\n```\nts=2024-02-11T10:00:00Z caller=coordinator.go:118 level=error component=configuration msg=\"Loading configuration file failed\" file=config.yml err=\"end of input: expected label value\"\n```\n\nUTF-8 strict mode will be the default mode of Alertmanager at the end of the transition period.\n\n#### Classic mode\n\nClassic mode is equivalent to Alertmanager versions 0.26.0 and older:\n\n```bash\nalertmanager --config.file=config.yml --enable-feature=\"classic-mode\"\n```\n\nYou can use this mode if you suspect there is an issue with fallback mode or UTF-8 strict mode. In such cases, please open an issue on GitHub with as much information as possible.\n\n### Verification\n\nYou can use `amtool` to validate that an Alertmanager configuration file is compatible with UTF-8 strict mode before enabling it in Alertmanager server. You do not need a running Alertmanager server to do this.\n\nJust like Alertmanager server, `amtool` will log a warning if the configuration is incompatible or contains disagreement:\n\n```\namtool check-config config.yml\nChecking 'config.yml'\nlevel=warn msg=\"Alertmanager is moving to a new parser for labels and matchers, and this input is incompatible. Alertmanager has instead parsed the input using the classic matchers parser as a fallback. To make this input compatible with the UTF-8 matchers parser please make sure all regular expressions and values are double-quoted and backslashes are escaped. If you are still seeing this message please open an issue.\" input=\"foo=\" origin=config err=\"end of input: expected label value\" suggestion=\"foo=\\\"\\\"\"\nlevel=warn msg=\"Matchers input has disagreement\" input=\"qux=\\\"\\\\xf0\\\\x9f\\\\x99\\\\x82\\\"\\n\" origin=config\n  SUCCESS\nFound:\n - global config\n - route\n - 2 inhibit rules\n - 2 receivers\n - 0 templates\n```\n\nYou will know if a configuration is compatible with UTF-8 strict mode when no warnings are logged in `amtool`:\n\n```\namtool check-config config.yml\nChecking 'config.yml'  SUCCESS\nFound:\n - global config\n - route\n - 2 inhibit rules\n - 2 receivers\n - 0 templates\n```\n\nYou can also use `amtool` in UTF-8 strict mode as an additional level of verification. You will know that a configuration is invalid because the command will fail:\n\n```\namtool check-config config.yml --enable-feature=\"utf8-strict-mode\"\nlevel=warn msg=\"UTF-8 mode enabled\"\nChecking 'config.yml'  FAILED: end of input: expected label value\n\namtool: error: failed to validate 1 file(s)\n```\n\nYou will know that a configuration is valid because the command will succeed:\n\n```\namtool check-config config.yml --enable-feature=\"utf8-strict-mode\"\nlevel=warn msg=\"UTF-8 mode enabled\"\nChecking 'config.yml'  SUCCESS\nFound:\n - global config\n - route\n - 2 inhibit rules\n - 2 receivers\n - 0 templates\n```\n\n### `<matcher>` (Shared)\n\n#### UTF-8 matchers\n\nA UTF-8 matcher consists of three tokens:\n\n- An unquoted literal or a double-quoted string for the label name.\n- One of `=`, `!=`, `=~`, or `!~`. `=` means equals, `!=` means not equal, `=~` means matches the regular expression and `!~` means doesn't match the regular expression.\n- An unquoted literal or a double-quoted string for the regular expression or label value.\n\nUnquoted literals can contain all UTF-8 characters other than the reserved characters. The reserved characters include whitespace and all characters in ``` { } ! = ~ , \\ \" ' ` ```. For example, `foo`, `[a-zA-Z]+`, and `Προμηθεύς` (Prometheus in Greek) are all examples of valid unquoted literals. However, `foo!` is not a valid literal as `!` is a reserved character.\n\nDouble-quoted strings can contain all UTF-8 characters. Unlike unquoted literals, there are no reserved characters. However, literal double quotes and backslashes must be escaped with a single backslash. For example, to match the regular expression `\\d+` the backslash must be escaped `\"\\\\d+\"`. This is because double-quoted strings follow the same rules as Go's [string literals](https://go.dev/ref/spec#String_literals). Double-quoted strings also support UTF-8 code points. For example, `\"foo!\"`, `\"bar,baz\"`, `\"\\\"baz qux\\\"\"` and `\"\\xf0\\x9f\\x99\\x82\"`.\n\n#### Classic matchers\n\nA classic matcher is a string with a syntax inspired by PromQL and OpenMetrics. The syntax of a classic matcher consists of three tokens:\n\n- A valid Prometheus label name.\n- One of `=`, `!=`, `=~`, or `!~`. `=` means equals, `!=` means that the strings are not equal, `=~` is used for equality of regex expressions and `!~` is used for un-equality of regex expressions. They have the same meaning as known from PromQL selectors.\n- A UTF-8 string, which may be enclosed in double quotes. Before or after each token, there may be any amount of whitespace.\n\nThe 3rd token may be the empty string. Within the 3rd token, OpenMetrics escaping rules apply: `\\\"` for a double-quote, `\\n` for a line feed, `\\\\` for a literal backslash. Unescaped `\"` must not occur inside the 3rd token (only as the 1st or last character). However, literal line feed characters are tolerated, as are single `\\` characters not followed by `\\`, `n`, or `\"`. They act as a literal backslash in that case.\n\n#### Composition of matchers\n\nYou can compose matchers to create complex match expressions. When composed, all matchers must match for the entire expression to match. For example, the expression `{alertname=\"Watchdog\", severity=~\"warning|critical\"}` will match an alert with labels `alertname=Watchdog, severity=critical` but not an alert with labels `alertname=Watchdog, severity=none` as while the alertname is Watchdog the severity is neither warning nor critical.\n\nYou can compose matchers into expressions with a YAML list:\n\n```yaml\nmatchers:\n  - alertname = Watchdog\n  - severity =~ \"warning|critical\"\n```\n\nor as a PromQL inspired expression where each matcher is comma separated:\n\n```\n{alertname=\"Watchdog\", severity=~\"warning|critical\"}\n```\n\nA single trailing comma is permitted:\n\n```\n{alertname=\"Watchdog\", severity=~\"warning|critical\",}\n```\n\nThe open `{` and close `}` brace are optional:\n\n```\nalertname=\"Watchdog\", severity=~\"warning|critical\"\n```\n\nHowever, both must be either present or omitted. You cannot have incomplete open or close braces:\n\n```\n{alertname=\"Watchdog\", severity=~\"warning|critical\"\n```\n\n```\nalertname=\"Watchdog\", severity=~\"warning|critical\"}\n```\n\nYou cannot have duplicate open or close braces either:\n\n```\n{{alertname=\"Watchdog\", severity=~\"warning|critical\",}}\n```\n\nWhitespace (spaces, tabs and newlines) is permitted outside double quotes and has no effect on the matchers themselves. For example:\n\n```\n{\n   alertname = \"Watchdog\",\n   severity =~ \"warning|critical\",\n}\n```\n\nis equivalent to:\n\n```\n{alertname=\"Watchdog\",severity=~\"warning|critical\"}\n```\n\n#### More examples\n\nHere are some more examples:\n\n1. Two equals matchers composed as a YAML list:\n\n    ```yaml\n    matchers:\n      - foo = bar\n      - dings != bums\n    ```\n\n2. Two matchers combined composed as a short-form YAML list:\n\n    ```yaml\n    matchers: [ foo = bar, dings != bums ]\n    ```\n\n   As shown below, in the short-form, it's better to use double quotes to avoid problems with special characters like commas:\n\n   ```yaml\n   matchers: [ \"foo = \\\"bar,baz\\\"\", \"dings != bums\" ]\n   ```\n\n3. You can also put both matchers into one PromQL-like string. Single quotes work best here:\n\n    ```yaml\n    matchers: [ '{foo=\"bar\", dings!=\"bums\"}' ]\n    ```\n\n4. To avoid issues with escaping and quoting rules in YAML, you can also use a YAML block:\n\n    ```yaml\n    matchers:\n      - |\n          {quote=~\"She said: \\\"Hi, all!( How're you…)?\\\"\"}\n    ```\n\n## General receiver-related settings\n\nThese receiver settings allow configuring notification destinations (receivers) and HTTP client options for HTTP-based receivers.\n\n### `<receiver>`\n\nReceiver is a named configuration of one or more notification integrations.\n\nNote: As part of lifting the past moratorium on new receivers it was agreed that, in addition to the existing requirements, new notification integrations will be required to have a committed maintainer with push access.\n\n```yaml\n# The unique name of the receiver.\nname: <string>\n\n# Configurations for several notification integrations.\ndiscord_configs:\n  [ - <discord_config>, ... ]\nemail_configs:\n  [ - <email_config>, ... ]\nmattermost_configs:\n  [ - <mattermost_config>, ... ]\nmsteams_configs:\n  [ - <msteams_config>, ... ]\nmsteamsv2_configs:\n  [ - <msteamsv2_config>, ... ]\njira_configs:\n  [ - <jira_config>, ... ]\nopsgenie_configs:\n  [ - <opsgenie_config>, ... ]\npagerduty_configs:\n  [ - <pagerduty_config>, ... ]\nincidentio_configs:\n  [ - <incidentio_config>, ... ]\npushover_configs:\n  [ - <pushover_config>, ... ]\nrocketchat_configs:\n  [ - <rocketchat_config>, ... ]\nslack_configs:\n  [ - <slack_config>, ... ]\nsns_configs:\n  [ - <sns_config>, ... ]\ntelegram_configs:\n  [ - <telegram_config>, ... ]\nvictorops_configs:\n  [ - <victorops_config>, ... ]\nwebex_configs:\n  [ - <webex_config>, ... ]\nwebhook_configs:\n  [ - <webhook_config>, ... ]\nwechat_configs:\n  [ - <wechat_config>, ... ]\n```\n\n### `<http_config>` (Shared) \n\nAn `http_config` allows configuring the HTTP client that the receiver uses to\ncommunicate with HTTP-based API services.\n\n```yaml\n# Note that `basic_auth` and `authorization` options are mutually exclusive.\n\n# Sets the `Authorization` header with the configured username and password.\n# password and password_file are mutually exclusive.\nbasic_auth:\n  [ username: <string> ]\n  [ password: <secret> ]\n  [ password_file: <string> ]\n\n# Optional the `Authorization` header configuration.\nauthorization:\n  # Sets the authentication type.\n  [ type: <string> | default: Bearer ]\n  # Sets the credentials. It is mutually exclusive with\n  # `credentials_file`.\n  [ credentials: <secret> ]\n  # Sets the credentials with the credentials read from the configured file.\n  # It is mutually exclusive with `credentials`.\n  [ credentials_file: <filename> ]\n\n# Optional OAuth 2.0 configuration.\n# Cannot be used at the same time as basic_auth or authorization.\noauth2:\n  [ <oauth2> ]\n\n# Whether to enable HTTP2.\n[ enable_http2: <bool> | default: true ]\n\n# Optional proxy URL.\n[ proxy_url: <string> ]\n# Comma-separated string that can contain IPs, CIDR notation, domain names\n# that should be excluded from proxying. IP and domain names can\n# contain port numbers.\n[ no_proxy: <string> ]\n# Use proxy URL indicated by environment variables (HTTP_PROXY, http_proxy, HTTPS_PROXY, https_proxy, NO_PROXY, and no_proxy)\n[ proxy_from_environment: <boolean> | default: false ]\n# Specifies headers to send to proxies during CONNECT requests.\n[ proxy_connect_header:\n  [ <string>: [<secret>, ...] ] ]\n\n# Configure whether HTTP requests follow HTTP 3xx redirects.\n[ follow_redirects: <bool> | default = true ]\n\n# Configures the TLS settings.\ntls_config:\n  [ <tls_config> ]\n\n# Custom HTTP headers to be sent along with each request.\n# Headers that are set by Prometheus itself can't be overwritten.\nhttp_headers:\n  [ <http_header> ]\n```\n\n#### `<http_header>` (Shared)\n\n```yaml\n# Header name.\n<string>:\n    # Header values.\n    [ values: [<string>, ...] ]\n    # Headers values. Hidden in configuration page.\n    [ secrets: [<secret>, ...] ]\n    # Files to read header values from.\n    [ files: [<string>, ...] ]\n```\n\n#### `<oauth2>` (Shared)\n\nOAuth 2.0 authentication using the client credentials grant type.\nAlertmanager fetches an access token from the specified endpoint with\nthe given client access and secret keys.\n\n```yaml\nclient_id: <string>\n[ client_secret: <secret> ]\n\n# Read the client secret from a file.\n# It is mutually exclusive with `client_secret`.\n[ client_secret_file: <filename> ]\n\n# Scopes for the token request.\nscopes:\n  [ - <string> ... ]\n\n# The URL to fetch the token from.\ntoken_url: <string>\n\n# Optional parameters to append to the token URL.\nendpoint_params:\n  [ <string>: <string> ... ]\n\n# Configures the token request's TLS settings.\ntls_config:\n  [ <tls_config> ]\n\n# Optional proxy URL.\n[ proxy_url: <string> ]\n# Comma-separated string that can contain IPs, CIDR notation, domain names\n# that should be excluded from proxying. IP and domain names can\n# contain port numbers.\n[ no_proxy: <string> ]\n# Use proxy URL indicated by environment variables (HTTP_PROXY, https_proxy, HTTPs_PROXY, https_proxy, and no_proxy)\n[ proxy_from_environment: <boolean> | default: false ]\n# Specifies headers to send to proxies during CONNECT requests.\n[ proxy_connect_header:\n  [ <string>: [<secret>, ...] ] ]\n```\n\n#### `<tls_config>` (Shared)\n\nA `tls_config` allows configuring TLS connections.\n\n```yaml\n# CA certificate to validate the server certificate with.\n[ ca_file: <filepath> ]\n\n# Certificate and key files for client cert authentication to the server.\n[ cert_file: <filepath> ]\n[ key_file: <filepath> ]\n\n# ServerName extension to indicate the name of the server.\n# http://tools.ietf.org/html/rfc4366#section-3.1\n[ server_name: <string> ]\n\n# Disable validation of the server certificate.\n[ insecure_skip_verify: <boolean> | default = false]\n\n# Minimum acceptable TLS version. Accepted values: TLS10 (TLS 1.0), TLS11 (TLS\n# 1.1), TLS12 (TLS 1.2), TLS13 (TLS 1.3).\n# If unset, Prometheus will use Go default minimum version, which is TLS 1.2.\n# See MinVersion in https://pkg.go.dev/crypto/tls#Config.\n[ min_version: <string> ]\n# Maximum acceptable TLS version. Accepted values: TLS10 (TLS 1.0), TLS11 (TLS\n# 1.1), TLS12 (TLS 1.2), TLS13 (TLS 1.3).\n# If unset, Prometheus will use Go default maximum version, which is TLS 1.3.\n# See MaxVersion in https://pkg.go.dev/crypto/tls#Config.\n[ max_version: <string> ]\n```\n\n## Receiver integration settings\n\nThese settings allow configuring specific receiver integrations.\n\n### `<discord_config>`\n\nDiscord notifications are sent via the [Discord webhook API](https://discord.com/developers/docs/resources/webhook). See Discord's [\"Intro to Webhooks\" article](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) to learn how to configure a webhook integration for a channel.\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = true ]\n\n# The Discord webhook URL.\n# webhook_url and webhook_url_file are mutually exclusive.\nwebhook_url: <secret>\nwebhook_url_file: <filepath>\n\n# Message title template.\n[ title: <tmpl_string> | default = '{{ template \"discord.default.title\" . }}' ]\n\n# Message body template.\n[ message: <tmpl_string> | default = '{{ template \"discord.default.message\" . }}' ]\n\n# Message content template. Limited to 2000 characters.\n[ content: <tmpl_string> | default = '{{ template \"discord.default.content\" . }}' ]\n\n# Message username.\n[ username: <string> | default = '' ]\n\n# Message avatar URL.\n[ avatar_url: <string> | default = '' ]\n\n# The HTTP client's configuration.\n[ http_config: <http_config> | default = global.http_config ]\n```\n\n### `<email_config>`\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = false ]\n\n# The email address to send notifications to.\n# Allows a comma separated list of rfc5322 compliant email addresses.\nto: <tmpl_string>\n\n# The sender's address.\n[ from: <tmpl_string> | default = global.smtp_from ]\n\n# The SMTP host through which emails are sent.\n[ smarthost: <string> | default = global.smtp_smarthost ]\n\n# The hostname to identify to the SMTP server.\n[ hello: <string> | default = global.smtp_hello ]\n\n# SMTP authentication information.\n# auth_password and auth_password_file are mutually exclusive.\n# auth_secret and auth_secret_file are mutually exclusive.\n[ auth_username: <string> | default = global.smtp_auth_username ]\n[ auth_password: <secret> | default = global.smtp_auth_password ]\n[ auth_password_file: <string> | default = global.smtp_auth_password_file ]\n[ auth_secret: <secret> | default = global.smtp_auth_secret ]\n[ auth_secret_file: <secret> | default = global.smtp_auth_secret_file ]\n[ auth_identity: <string> | default = global.smtp_auth_identity ]\n\n# The SMTP TLS requirement.\n# Note that Go does not support unencrypted connections to remote SMTP endpoints.\n[ require_tls: <bool> | default = global.smtp_require_tls ]\n\n# Force use of implicit TLS (direct TLS connection) for better security.\n# true: force use of implicit TLS (direct TLS connection on any port)\n# nil (default): auto-detect based on port (465=implicit, other=explicit) for backward compatibility\n[ force_implicit_tls: <bool> | default = nil ]\n\n# TLS configuration.\ntls_config:\n  [ <tls_config> | default = global.smtp_tls_config ]\n\n# The HTML body of the email notification.\n[ html: <tmpl_string> | default = '{{ template \"email.default.html\" . }}' ]\n# The text body of the email notification.\n[ text: <tmpl_string> ]\n\n# Further headers email header key/value pairs. Overrides any headers\n# previously set by the notification implementation.\n[ headers: { <string>: <tmpl_string>, ... } ]\n\n# Email threading configuration.\nthreading:\n  # Whether to enable threading, which makes alert notifications in the same\n  # alert group show up in the same email thread.\n  [ enabled: <boolean> | default = false ]\n  # What granularity of current date to thread by. Accepted values: daily, none.\n  # (none means group by alert group key, no date).\n  [ thread_by_date: <string> | default = daily ]\n```\n\n#### Email TLS Configuration Examples\n\n```yaml\n# Example 1: Force implicit TLS on any port (recommended for security)\nreceivers:\n  - name: email-implicit-tls\n    email_configs:\n      - to: alerts@example.com\n        smarthost: smtp.example.com:8465\n        force_implicit_tls: true  # Use direct TLS connection on port 8465\n\n# Example 2: Backward compatible (no force_implicit_tls specified)\nreceivers:\n  - name: email-default\n    email_configs:\n      - to: alerts@example.com\n        smarthost: smtp.example.com:465  # Auto-detects implicit TLS\n      - to: alerts@example.com\n        smarthost: smtp.example.com:587  # Auto-detects explicit TLS\n```\n\n### `<mattermost_config>`\n\nMattermost notifications are sent via the [Mattermost webhook API](https://developers.mattermost.com/integrate/webhooks/incoming/).\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = true ]\n\n# The Mattermost webhook URL.\n# webhook_url and webhook_url_file are mutually exclusive.\nwebhook_url: <secret>\nwebhook_url_file: <filepath>\n\n# Overrides the channel the message posts in. Use the channel’s name and not the display name, e.g. use town-square, not Town Square.\n[ channel: <string> | default = '' ]\n\n# Overrides the username the message posts as.\n# Defaults to the username set during webhook creation; if no username was set during creation, webhook is used.\n[ username: <string> | default = '' ]\n\n# Overrides the profile picture the message posts with.\n[ icon_url: <string> | default = '' ]\n\n# Overrides the profile picture and icon_url parameter.\n[ icon_emoji: <string> | default = '' ]\n\n# Message attachments used for richer formatting options.\n# It is for compatibility with Slack.\n[ fallback: <tmpl_string> | default = '{{ template \"mattermost.default.fallback\" . }}' ]\n[ color: <tmpl_string> | default = '{{ template \"mattermost.default.color\" . }}' ]\n[ title: <tmpl_string> | default = '{{ template \"mattermost.default.title\" . }}' ]\n[ title_link: <tmpl_string> | default = '{{ template \"mattermost.default.titlelink\" . }}' ]\n[ text: <tmpl_string> | default = '{{ template \"mattermost.default.text\" . }}' ]\n[ pretext: <string> | default = '' ]\n[ author_name: <string> | default = '' ]\n[ author_link: <string> | default = '' ]\n[ author_icon: <string> | default = '' ]\n[ fields: <string> | default = '' ]\n  [ <field_config> ... ]\n[ thumb_url: <string> | default = '' ]\n[ footer: <string> | default = '' ]\n[ footer_icon: <string> | default = '' ]\n[ image_url: <string> | default = '' ]\n# Deprecated: use top-level fields instead; `attachments` will be removed in a future.\n[ attachments: ]\n  [ <attachment_config> ... ]\n\n[ props: <prop_config> ]\n\n[ priority: <priority_config> ]\n\n# The HTTP client's configuration.\n[ http_config: <http_config> | default = global.http_config ]\n```\n\n#### `<attachment_config>`\n\nSee [Mattermost documentation](https://developers.mattermost.com/integrate/reference/message-attachments/) for more info.\n```yaml\n[ fallback: <string> | default = '' ]\n[ color: <string> | default = '' ]\n[ pretext: <string> | default = '' ]\n[ text: <string> | default = '' ]\n[ author_name: <string> | default = '' ]\n[ author_link: <string> | default = '' ]\n[ author_icon: <string> | default = '' ]\n[ title: <string> | default = '' ]\n[ title_link: <string> | default = '' ]\n# Same as Slack fields.\n[ fields: <string> | default = '' ]\n  [ <field_config> ... ]\n[ thumb_url: <string> | default = '' ]\n[ footer: <string> | default = '' ]\n[ footer_icon: <string> | default = '' ]\n[ image_url: <string> | default = '' ]\n```\n\n#### `<prop_config>`\n\n```yaml\n# Props card allows for extra information (Markdown-formatted text) to be sent to Mattermost that will only be displayed in the RHS panel after a user selects the info icon displayed alongside the post.\n[ card: <string> | default = '' ]\n```\n\n#### `<priority_config>`\n\n```yaml\n# priority adds label to the message. Possible values are \"urgent\", \"important\" and \"standard\".\n[ priority: <string> | default = '' ]\n\n# If set to true, the message will be marked as requiring an acknowledgment from the users by displaying a checkmark icon next to the message. Keep in mind that this requires the message priority to be set to Important or Urgent.\n# Only for enterprise version of Mattermost.\n[ requested_ack: <bool> | default = false ]\n\n# Only for Urgent messages. If set to true recipients will receive a persistent notification every five minutes until they acknowledge the message.\n# Only for enterprise version of Mattermost.\n[ persistent_notifications: <bool> | default = false ]\n```\n\n### `<msteams_config>`\n\nMicrosoft Teams notifications are sent via the [Incoming Webhooks](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/what-are-webhooks-and-connectors) API endpoint.\n\nDEPRECATION NOTICE: Microsoft is deprecating the creation and usage of [Microsoft 365 connectors via Microsoft Teams](https://devblogs.microsoft.com/microsoft365dev/retirement-of-office-365-connectors-within-microsoft-teams/). Consider migrating to using [Workflows](https://learn.microsoft.com/en-us/power-automate/teams/send-a-message-in-teams) with the msteamsv2 config.\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = true ]\n\n# The incoming webhook URL.\n# webhook_url and webhook_url_file are mutually exclusive.\n[ webhook_url: <secret> ]\n[ webhook_url_file: <filepath> ]\n\n# Message title template.\n[ title: <tmpl_string> | default = '{{ template \"msteams.default.title\" . }}' ]\n\n# Message summary template.\n[ summary: <tmpl_string> | default = '{{ template \"msteams.default.summary\" . }}' ]\n\n# Message body template.\n[ text: <tmpl_string> | default = '{{ template \"msteams.default.text\" . }}' ]\n\n# The HTTP client's configuration.\n[ http_config: <http_config> | default = global.http_config ]\n```\n\n### `<msteamsv2_config>`\n\nMicrosoft Teams v2 notifications using the new message format with adaptive cards as required by [flows](https://learn.microsoft.com/en-us/power-automate/teams/overview). Please follow [the documentation](https://support.microsoft.com/en-gb/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498) for more information on how to set up this integration.\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = true ]\n\n# The incoming webhook URL.\n# webhook_url and webhook_url_file are mutually exclusive.\n[ webhook_url: <secret> ]\n[ webhook_url_file: <filepath> ]\n\n# Message title template.\n[ title: <tmpl_string> | default = '{{ template \"msteamsv2.default.title\" . }}' ]\n\n# Message body template.\n[ text: <tmpl_string> | default = '{{ template \"msteamsv2.default.text\" . }}' ]\n\n# The HTTP client's configuration.\n[ http_config: <http_config> | default = global.http_config ]\n```\n\n### `<jira_config>`\n\nJIRA notifications are sent via [JIRA Rest API v2](https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/)\nor [JIRA REST API v3](https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/#version).\n\nNote: This integration is only tested against a Jira Cloud instance.\nJira Data Center (on premise instance) can work, but it's not guaranteed.\n\nBoth APIs have the same feature set. The difference is that V2 supports [Wiki Markup](https://jira.atlassian.com/secure/WikiRendererHelpAction.jspa?section=all)\nfor the issue description and V3 supports [Atlassian Document Format (ADF)](https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/).\nThe default `jira.default.description` template only works with V2.\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = true ]\n\n# The URL to send API requests to. The full API path must be included.\n# Example: https://company.atlassian.net/rest/api/2/\n[ api_url: <string> | default = global.jira_api_url ]\n\n# The API Type to use for search requests, can be either auto, cloud or datacenter\n# Example: cloud\n[ api_type: <string> | default = auto ]\n\n# The project key where issues are created.\nproject: <string>\n\n# Issue summary configuration.\n[ summary:\n    # Template for the issue summary.\n    [ template: <tmpl_string> | default = '{{ template \"jira.default.summary\" . }}' ]\n\n    # If set to false, the summary will not be updated when updating an existing issue.\n    [ enable_update: <boolean> | default = true ]\n]\n\n# Issue description configuration.\n[ description:\n    # Template for the issue description.\n    [ template: <tmpl_string> | default = '{{ template \"jira.default.description\" . }}' ]\n\n    # If set to false, the description will not be updated when updating an existing issue.\n    [ enable_update: <boolean> | default = true ]\n]\n\n# Labels to be added to the issue.\nlabels:\n  [ - <tmpl_string> ... ]\n\n# Priority of the issue.\n[ priority: <tmpl_string> | default = '{{ template \"jira.default.priority\" . }}' ]\n\n# Type of the issue (e.g. Bug).\n[ issue_type: <string> ]\n\n# Name of the workflow transition to resolve an issue. The target status must have the category \"done\".\n# NOTE: The name of the transition can be localized and depends on the language setting of the service account.\n[ resolve_transition: <string> ]\n\n# Name of the workflow transition to reopen an issue. The target status should not have the category \"done\".\n# NOTE: The name of the transition can be localized and depends on the language setting of the service account.\n[ reopen_transition: <string> ]\n\n# If reopen_transition is defined, ignore issues with that resolution.\n[ wont_fix_resolution: <string> ]\n\n# If reopen_transition is defined, reopen the issue when it is not older than this value (rounded down to the nearest minute).\n# The resolutiondate field is used to determine the age of the issue.\n[ reopen_duration: <duration> ]\n\n# Other issue and custom fields.\nfields:\n  [ <string>: <jira_field> ... ]\n\n\n# The HTTP client's configuration. You must use this configuration to supply the personal access token (PAT) as part of the HTTP `Authorization` header.\n# For Jira Cloud, use basic_auth with the email address as the username and the PAT as the password.\n# For Jira Data Center, use the 'authorization' field with 'credentials: <PAT value>'.\n[ http_config: <http_config> | default = global.http_config ]\n```\n\nThe `labels` field is a list of labels added to the issue. Template expressions are supported. For example:\n\n```yaml\nlabels:\n  - 'alertmanager'\n  - '{{ .CommonLabels.severity }}'\n```\n\n#### `<jira_field>`\n\nJira issue field can have multiple types.\nDepends on the field type, the values must be provided differently.\nSee https://developer.atlassian.com/server/jira/platform/jira-rest-api-examples/#setting-custom-field-data-for-other-field-types for further examples.\n\n```yaml\nfields:\n    # Components\n    components: { name: \"Monitoring\" }\n    # Custom Field TextField\n    customfield_10001: \"Random text\"\n    # Custom Field SelectList\n    customfield_10002: {\"value\": \"red\"}\n    # Custom Field MultiSelect\n    customfield_10003: [{\"value\": \"red\"}, {\"value\": \"blue\"}, {\"value\": \"green\"}]\n```\n\n### `<opsgenie_config>`\n\nOpsGenie notifications are sent via the [OpsGenie API](https://docs.opsgenie.com/docs/alert-api).\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = true ]\n\n# The API key to use when talking to the OpsGenie API.\n[ api_key: <secret> | default = global.opsgenie_api_key ]\n\n# The filepath to API key to use when talking to the OpsGenie API. Conflicts with api_key.\n[ api_key_file: <filepath> | default = global.opsgenie_api_key_file ]\n\n# The base URL for OpsGenie API requests.\n[ api_url: <string> | default = global.opsgenie_api_url ]\n\n# Alert text limited to 130 characters.\n[ message: <tmpl_string> | default = '{{ template \"opsgenie.default.message\" . }}' ]\n\n# A description of the alert.\n[ description: <tmpl_string> | default = '{{ template \"opsgenie.default.description\" . }}' ]\n\n# A backlink to the sender of the notification.\n[ source: <tmpl_string> | default = '{{ template \"opsgenie.default.source\" . }}' ]\n\n# A set of arbitrary key/value pairs that provide further detail\n# about the alert.\n# All common labels are included as details by default.\n[ details: { <string>: <tmpl_string>, ... } ]\n\n# List of responders responsible for notifications.\nresponders:\n  [ - <responder> ... ]\n\n# Comma separated list of tags attached to the notifications.\n[ tags: <tmpl_string> ]\n\n# Additional alert note.\n[ note: <tmpl_string> ]\n\n# Priority level of alert. Possible values are P1, P2, P3, P4, and P5.\n[ priority: <tmpl_string> ]\n\n# Whether to update message and description of the alert in OpsGenie if it already exists\n# By default, the alert is never updated in OpsGenie, the new message only appears in activity log.\n[ update_alerts: <boolean> | default = false ]\n\n# Optional field that can be used to specify which domain alert is related to.\n[ entity: <tmpl_string> ]\n\n# Comma separated list of actions that will be available for the alert.\n[ actions: <tmpl_string> ]\n\n# The HTTP client's configuration.\n[ http_config: <http_config> | default = global.http_config ]\n```\n\n#### `<responder>`\n\n```yaml\n# Exactly one of these fields should be defined.\n[ id: <tmpl_string> ]\n[ name: <tmpl_string> ]\n[ username: <tmpl_string> ]\n\n# One of `team`, `teams`, `user`, `escalation` or `schedule`.\n#\n# The `teams` responder is configured using the `name` field above.\n# This field can contain a comma-separated list of team names.\n# If the list is empty, no responders are configured.\ntype: <tmpl_string>\n```\n\n### `<pagerduty_config>`\n\nPagerDuty notifications are sent via the [PagerDuty API](https://developer.pagerduty.com/documentation/integration/events).\nPagerDuty provides [documentation](https://www.pagerduty.com/docs/guides/prometheus-integration-guide/) on how to integrate. There are important differences with Alertmanager's v0.11 and greater support of PagerDuty's Events API v2.\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = true ]\n\n# The routing and service keys are mutually exclusive.\n# The PagerDuty integration key (when using PagerDuty integration type `Events API v2`).\n# It is mutually exclusive with `routing_key_file`.\nrouting_key: <tmpl_secret>\n# Read the Pager Duty routing key from a file.\n# It is mutually exclusive with `routing_key`.\nrouting_key_file: <filepath>\n# The PagerDuty integration key (when using PagerDuty integration type `Prometheus`).\n# It is mutually exclusive with `service_key_file`.\nservice_key: <tmpl_secret>\n# Read the Pager Duty service key from a file.\n# It is mutually exclusive with `service_key`.\nservice_key_file: <filepath>\n\n# The URL to send API requests to\n[ url: <string> | default = global.pagerduty_url ]\n\n# The client identification of the Alertmanager.\n[ client:  <tmpl_string> | default = '{{ template \"pagerduty.default.client\" . }}' ]\n# A backlink to the sender of the notification.\n[ client_url:  <tmpl_string> | default = '{{ template \"pagerduty.default.clientURL\" . }}' ]\n\n# A description of the incident.\n[ description: <tmpl_string> | default = '{{ template \"pagerduty.default.description\" .}}' ]\n\n# Severity of the incident.\n[ severity: <tmpl_string> | default = 'error' ]\n\n# Unique location of the affected system.\n[ source: <tmpl_string> | default = client ]\n\n# A set of arbitrary key/value pairs that provide further detail about the incident.\n# Nested key/value pairs are accepted when using PagerDuty integration type `Events API v2`.\n[ details: { <string>: <tmpl_string>, ... } | default = {\n  firing:       '{{ .Alerts.Firing | toJSON }}'\n  resolved:     '{{ .Alerts.Resolved | toJSON }}'\n  num_firing:   '{{ .Alerts.Firing | len }}'\n  num_resolved: '{{ .Alerts.Resolved | len }}'\n} ]\n\n# Images to attach to the incident.\nimages:\n  [ <image_config> ... ]\n\n# Links to attach to the incident.\nlinks:\n  [ <link_config> ... ]\n\n# The part or component of the affected system that is broken.\n[ component: <tmpl_string> ]\n\n# A cluster or grouping of sources.\n[ group: <tmpl_string> ]\n\n# The class/type of the event.\n[ class: <tmpl_string> ]\n\n# The HTTP client's configuration.\n[ http_config: <http_config> | default = global.http_config ]\n\n# The maximum time to wait for a pagerduty request to complete, before failing the\n# request and allowing it to be retried. The default value of 0s indicates that\n# no timeout should be applied.\n# NOTE: This will have no effect if set higher than the group_interval.\n[ timeout: <duration> | default = 0s ]\n```\n\n#### `<image_config>` (PagerDuty)\n\nThe fields are documented in the [PagerDuty API documentation](https://developer.pagerduty.com/docs/events-api-v2/trigger-events/#the-images-property).\n\n```yaml\nhref: <tmpl_string>\nsrc: <tmpl_string>\nalt: <tmpl_string>\n```\n\n#### `<link_config>` (PagerDuty)\n\nThe fields are documented in the [PagerDuty API documentation](https://developer.pagerduty.com/docs/events-api-v2/trigger-events/#the-links-property).\n\n```yaml\nhref: <tmpl_string>\ntext: <tmpl_string>\n```\n\n### `<pushover_config>`\n\nPushover notifications are sent via the [Pushover API](https://pushover.net/api).\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = true ]\n\n# The recipient user's key.\n# user_key and user_key_file are mutually exclusive.\nuser_key: <secret>\nuser_key_file: <filepath>\n\n# Your registered application's API token, see https://pushover.net/apps\n# You can also register a token by cloning this Prometheus app:\n# https://pushover.net/apps/clone/prometheus\n# token and token_file are mutually exclusive.\ntoken: <secret>\ntoken_file: <filepath>\n\n# Notification title.\n[ title: <tmpl_string> | default = '{{ template \"pushover.default.title\" . }}' ]\n\n# Notification message.\n[ message: <tmpl_string> | default = '{{ template \"pushover.default.message\" . }}' ]\n\n# A supplementary URL shown alongside the message.\n[ url: <tmpl_string> | default = '{{ template \"pushover.default.url\" . }}' ]\n\n# Optional device to send notification to, see https://pushover.net/api#device\n[ device: <string> ]\n\n# Optional sound to use for notification, see https://pushover.net/api#sound\n[ sound: <string> ]\n\n# Priority, see https://pushover.net/api#priority\n[ priority: <tmpl_string> | default = '{{ if eq .Status \"firing\" }}2{{ else }}0{{ end }}' ]\n\n# How often the Pushover servers will send the same notification to the user.\n# Must be at least 30 seconds.\n[ retry: <duration> | default = 1m ]\n\n# How long your notification will continue to be retried for, unless the user\n# acknowledges the notification.\n[ expire: <duration> | default = 1h ]\n\n# Optional time to live (TTL) to use for notification, see https://pushover.net/api#ttl\n[ ttl: <duration> ]\n\n# Optional HTML/monospace formatting for the message, see https://pushover.net/api#html\n# html and monospace formatting are mutually exclusive.\n[ html: <boolean> | default = false ]\n[ monospace: <boolean> | default = false ]\n\n# The HTTP client's configuration.\n[ http_config: <http_config> | default = global.http_config ]\n```\n\n### `<rocketchat_config>`\n\nRocketchat notifications are sent via the [Rocketchat REST API](https://developer.rocket.chat/reference/api/rest-api/endpoints/messaging/chat-endpoints/postmessage).\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = true ]\n[ api_url: <string> | default = global.rocketchat_api_url ]\n[ channel: <tmpl_string> | default = global.rocketchat_api_url ]\n\n# The sender token and token_id\n# See https://docs.rocket.chat/use-rocket.chat/user-guides/user-panel/my-account#personal-access-tokens\n# token and token_file are mutually exclusive.\n# token_id and token_id_file are mutually exclusive.\ntoken: <secret>\ntoken_file: <filepath>\ntoken_id: <secret>\ntoken_id_file: <filepath>\n\n\n[ color: <tmpl_string | default '{{ if eq .Status \"firing\" }}red{{ else }}green{{ end }}' ]\n[ emoji <tmpl_string | default = '{{ template \"rocketchat.default.emoji\" . }}'\n[ icon_url <tmpl_string | default = '{{ template \"rocketchat.default.iconurl\" . }}'\n[ text <tmpl_string | default = '{{ template \"rocketchat.default.text\" . }}'\n[ title <tmpl_string | default = '{{ template \"rocketchat.default.title\" . }}'\n[ title_link <tmpl_string | default = '{{ template \"rocketchat.default.titlelink\" . }}'\nfields:\n  [ <rocketchat_field_config> ... ]\n[ image_url <tmpl_string> ]\n[ thumb_url <tmpl_string> ]\n[ link_names <tmpl_string> ]\n[ short_fields: <boolean> | default = false ]\nactions:\n  [ <rocketchat_action_config> ... ]\n```\n\n#### `<rocketchat_field_config>`\n\nThe fields are documented in the [Rocketchat API documentation](https://developer.rocket.chat/reference/api/rest-api/endpoints/messaging/chat-endpoints/postmessage#attachment-field-objects).\n\n```yaml\n[ title: <tmpl_string> ]\n[ value: <tmpl_string> ]\n[ short: <boolean> | default = rocketchat_config.short_fields ]\n```\n\n#### `<rocketchat_action_config>`\n\nThe fields are documented in the [Rocketchat API api models](https://github.com/RocketChat/Rocket.Chat.Go.SDK/blob/master/models/message.go).\n\n```yaml\n[ type: <tmpl_string> | ignored, only \"button\" is supported ]\n[ text: <tmpl_string> ]\n[ url: <tmpl_string> ]\n[ msg: <tmpl_string> ]\n```\n\n### `<slack_config>`\n\nSlack notifications can be sent via [Incoming webhooks](https://api.slack.com/messaging/webhooks) or [Bot tokens](https://api.slack.com/authentication/token-types).\n\nIf using an incoming webhook then `api_url` must be set to the URL of the incoming webhook, or written to the file referenced in `api_url_file`.\n\nIf using Bot tokens then `api_url` must be set to [`https://slack.com/api/chat.postMessage`](https://api.slack.com/methods/chat.postMessage), the bot token must be set as the authorization credentials in `http_config`, and `channel` must contain either the name of the channel or Channel ID to send notifications to. If using the name of the channel the # is optional.\n\nThe notification contains an [attachment](https://docs.slack.dev/legacy/legacy-messaging/legacy-secondary-message-attachments/).\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = false ]\n# The Slack webhook URL. Either api_url/api_url_file OR app_token/app_token_file should be set, but not both.\n# Defaults to global settings if none are set here.\n[ api_url: <secret> | default = global.slack_api_url ]\n[ api_url_file: <filepath> | default = global.slack_api_url_file ]\n\n# Slack App token for OAuth authentication. Mutually exclusive with api_url/api_url_file.\n# Defaults to global settings if no local authorization or webhook URL is configured.\n[ app_token: <secret> | default = global.slack_app_token ]\n[ app_token_file: <filepath> | default = global.slack_app_token_file ]\n\n# The Slack App URL. Required when using app_token authentication.\n[ app_url: <string> | default = global.slack_app_url ]\n\n# The channel or user to send notifications to.\nchannel: <tmpl_string>\n\n# API request data as defined by the Slack webhook API.\n[ icon_emoji: <tmpl_string> | default = '{{ template \"slack.default.iconemoji\" . }}' ]\n[ icon_url: <tmpl_string> | default = '{{ template \"slack.default.iconurl\" . }}' ]\n[ link_names: <boolean> | default = false ]\n# The text content of the Slack message.\n# If set, this is sent as the top-level 'text' field in the Slack payload.\n# This is useful for simple notifications or compatibility with Slack Workflow Webhooks.\n[ message_text: <tmpl_string> ]\n[ username: <tmpl_string> | default = '{{ template \"slack.default.username\" . }}' ]\n# The following parameters define the attachment.\nactions:\n  [ <action_config> ... ]\n[ callback_id: <tmpl_string> | default = '{{ template \"slack.default.callbackid\" . }}' ]\n[ color: <tmpl_string> | default = '{{ template \"slack.default.color\" . }}' ]\n[ fallback: <tmpl_string> | default = '{{ template \"slack.default.fallback\" . }}' ]\nfields:\n  [ <field_config> ... ]\n[ footer: <tmpl_string> | default = '{{ template \"slack.default.footer\" . }}' ]\n[ mrkdwn_in: [ <string>, ... ] | default = [\"fallback\", \"pretext\", \"text\"] ]\n[ pretext: <tmpl_string> | default = '{{ template \"slack.default.pretext\" . }}' ]\n[ short_fields: <boolean> | default = false ]\n[ text: <tmpl_string> | default = '{{ template \"slack.default.text\" . }}' ]\n[ title: <tmpl_string> | default = '{{ template \"slack.default.title\" . }}' ]\n[ title_link: <tmpl_string> | default = '{{ template \"slack.default.titlelink\" . }}' ]\n[ image_url: <tmpl_string> ]\n[ thumb_url: <tmpl_string> ]\n\n# The HTTP client's configuration.\n[ http_config: <http_config> | default = global.http_config ]\n\n# The maximum time to wait for a slack request to complete, before failing the\n# request and allowing it to be retried. The default value of 0s indicates that\n# no timeout should be applied.\n# NOTE: This will have no effect if set higher than the group_interval.\n[ timeout: <duration> | default = 0s ]\n\n# Enables updating existing Slack messages instead of creating new ones on alert state change.\n# Webhook URLs do not support updates.\n[ update_message: <boolean> | default = false ]\n```\n\n#### `<action_config>` (Slack)\n\nThe fields are documented in the Slack API documentation for [message attachments](https://docs.slack.dev/legacy/legacy-messaging/legacy-secondary-message-attachments/) and [interactive messages](https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#action_fields).\n\n```yaml\ntext: <tmpl_string>\ntype: <tmpl_string>\n# Either url or name and value are mandatory.\n[ url: <tmpl_string> ]\n[ name: <tmpl_string> ]\n[ value: <tmpl_string> ]\n\n[ confirm: <action_confirm_field_config> ]\n[ style: <tmpl_string> | default = '' ]\n```\n\n##### `<action_confirm_field_config>` (Slack)\n\nThe fields are documented in the [Slack API documentation](https://api.slack.com/legacy/interactive-message-field-guide#confirmation_fields).\n\n```yaml\ntext: <tmpl_string>\n[ dismiss_text: <tmpl_string> | default '' ]\n[ ok_text: <tmpl_string> | default '' ]\n[ title: <tmpl_string> | default '' ]\n```\n\n#### `<field_config>` (Slack)\n\nThe fields are documented in the [Slack API documentation](https://docs.slack.dev/legacy/legacy-messaging/legacy-secondary-message-attachments/).\n\n```yaml\ntitle: <tmpl_string>\nvalue: <tmpl_string>\n[ short: <boolean> | default = slack_config.short_fields ]\n```\n\n### `<sns_config>`\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = true ]\n\n# The SNS API URL i.e. https://sns.us-east-2.amazonaws.com.\n#  If not specified, the SNS API URL from the SNS SDK will be used.\n[ api_url: <tmpl_string> ]\n\n# Configures AWS's Signature Verification 4 signing process to sign requests.\nsigv4:\n  [ <sigv4_config> ]\n\n# SNS topic ARN, i.e. arn:aws:sns:us-east-2:698519295917:My-Topic\n# If you don't specify this value, you must specify a value for the phone_number or target_arn.\n# If you are using a FIFO SNS topic you should set a message group interval longer than 5 minutes\n# to prevent messages with the same group key being deduplicated by the SNS default deduplication window\n[ topic_arn: <tmpl_string> ]\n\n# Subject line when the message is delivered to email endpoints.\n[ subject: <tmpl_string> | default = '{{ template \"sns.default.subject\" .}}' ]\n\n# Phone number if message is delivered via SMS in E.164 format.\n# If you don't specify this value, you must specify a value for the topic_arn or target_arn.\n[ phone_number: <tmpl_string> ]\n\n# The  mobile platform endpoint ARN if message is delivered via mobile notifications.\n# If you don't specify this value, you must specify a value for the topic_arn or phone_number.\n[ target_arn: <tmpl_string> ]\n\n# The message content of the SNS notification.\n[ message: <tmpl_string> | default = '{{ template \"sns.default.message\" .}}' ]\n\n# SNS message attributes.\nattributes:\n  [ <string>: <string> ... ]\n\n# The HTTP client's configuration.\n[ http_config: <http_config> | default = global.http_config ]\n```\n\n#### `<sigv4_config>` (SNS)\n\n```yaml\n# The AWS region. If blank, the region from the default credentials chain is used.\n[ region: <string> ]\n\n# The AWS API keys. Both access_key and secret_key must be supplied or both must be blank.\n# If blank the environment variables `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are used.\n[ access_key: <string> ]\n[ secret_key: <secret> ]\n\n# Named AWS profile used to authenticate.\n[ profile: <string> ]\n\n# AWS Role ARN, an alternative to using AWS API keys.\n[ role_arn: <string> ]\n```\n\n### `<telegram_config>`\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = true ]\n\n# The Telegram API URL i.e. https://api.telegram.org.\n# If not specified, default API URL will be used.\n[ api_url: <string> | default = global.telegram_api_url ]\n\n# Telegram bot token. It is mutually exclusive with `bot_token_file`.\n[ bot_token: <secret> ]\n\n# Read the Telegram bot token from a file. It is mutually exclusive with `bot_token`.\n[ bot_token_file: <filepath> ]\n\n# ID of the chat where to send the messages. It is mutually exclusive with `chat_id_file`.\n[ chat_id: <int> ]\n\n# Read the chat ID from a file. It is mutually exclusive with `chat_id`.\n[ chat_id_file: <filepath> ]\n\n# Optional ID of the message thread where to send the messages.\n[ message_thread_id: <int> ]\n\n# Message template.\n[ message: <tmpl_string> default = '{{ template \"telegram.default.message\" .}}' ]\n\n# Disable telegram notifications\n[ disable_notifications: <boolean> | default = false ]\n\n# Parse mode for telegram message, supported values are MarkdownV2, Markdown, HTML and empty string for plain text.\n# If the message exceeds Telegram's character limit, it will be truncated or replaced with a fallback message if parse_mode is set to HTML.\n[ parse_mode: <string> | default = \"HTML\" ]\n\n# The HTTP client's configuration.\n[ http_config: <http_config> | default = global.http_config ]\n```\n\n### `<victorops_config>`\n\nVictorOps notifications are sent out via the [VictorOps API](https://help.victorops.com/knowledge-base/rest-endpoint-integration-guide/)\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = true ]\n\n# The API key to use when talking to the VictorOps API.\n# It is mutually exclusive with `api_key_file`.\n[ api_key: <secret> | default = global.victorops_api_key ]\n\n# Reads the API key to use when talking to the VictorOps API from a file.\n# It is mutually exclusive with `api_key`.\n[ api_key_file: <filepath> | default = global.victorops_api_key_file ]\n\n# The VictorOps API URL.\n[ api_url: <string> | default = global.victorops_api_url ]\n\n# A key used to map the alert to a team.\nrouting_key: <tmpl_string>\n\n# Describes the behavior of the alert (CRITICAL, WARNING, INFO).\n[ message_type: <tmpl_string> | default = 'CRITICAL' ]\n\n# Contains summary of the alerted problem.\n[ entity_display_name: <tmpl_string> | default = '{{ template \"victorops.default.entity_display_name\" . }}' ]\n\n# Contains long explanation of the alerted problem.\n[ state_message: <tmpl_string> | default = '{{ template \"victorops.default.state_message\" . }}' ]\n\n# The monitoring tool the state message is from.\n[ monitoring_tool: <tmpl_string> | default = '{{ template \"victorops.default.monitoring_tool\" . }}' ]\n\n# The HTTP client's configuration.\n[ http_config: <http_config> | default = global.http_config ]\n```\n\n### `<webhook_config>`\n\nThe webhook receiver allows configuring a generic receiver.\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = true ]\n\n# The endpoint to send HTTP POST requests to.\n# url and url_file are mutually exclusive.\nurl: <secret>\nurl_file: <filepath>\n\n# The HTTP client's configuration.\n[ http_config: <http_config> | default = global.http_config ]\n\n# The maximum number of alerts to include in a single webhook message. Alerts\n# above this threshold are truncated. When leaving this at its default value of\n# 0, all alerts are included.\n[ max_alerts: <int> | default = 0 ]\n\n# The maximum time to wait for a webhook request to complete, before failing the\n# request and allowing it to be retried. The default value of 0s indicates that\n# no timeout should be applied.\n# NOTE: This will have no effect if set higher than the group_interval.\n[ timeout: <duration> | default = 0s ]\n\n# Define custom payload to be sent to the webhook endpoint.\n# USE AT YOUR OWN RISK: This is an advanced configuration option that allows you\n# to define a custom payload using Go templates. Be aware that the Alertmanager does not\n# perform any validation on the resulting payload, and it is your responsibility to\n# ensure that the generated payload is in the desired format expected by the receiving endpoint.\n# The payload has to be valid JSON. You can use the `toJson` function to help with this.\n# THE ALERTMANAGER TEAM WILL NOT PROVIDE ANY SUPPORT FOR ISSUES ARISING FROM THE USE OF THIS OPTION.\n[ payload: { <string>: <tmpl_string>, ... } ]\n```\n\nThe Alertmanager\nwill send HTTP POST requests in the following JSON format to the configured\nendpoint:\n\n```\n{\n  \"version\": \"4\",\n  \"groupKey\": <string>,              // key identifying the group of alerts (e.g. to deduplicate)\n  \"truncatedAlerts\": <int>,          // how many alerts have been truncated due to \"max_alerts\"\n  \"status\": \"<resolved|firing>\",\n  \"receiver\": <string>,\n  \"groupLabels\": <object>,\n  \"commonLabels\": <object>,\n  \"commonAnnotations\": <object>,\n  \"externalURL\": <string>,           // backlink to the Alertmanager.\n  \"alerts\": [\n    {\n      \"status\": \"<resolved|firing>\",\n      \"labels\": <object>,\n      \"annotations\": <object>,\n      \"startsAt\": \"<rfc3339>\",\n      \"endsAt\": \"<rfc3339>\",\n      \"generatorURL\": <string>,      // identifies the entity that caused the alert\n      \"fingerprint\": <string>        // fingerprint to identify the alert\n    },\n    ...\n  ]\n}\n```\n\nThere is a list of\n[integrations](https://prometheus.io/docs/operating/integrations/#alertmanager-webhook-receiver) with\nthis feature.\n\n### `<incidentio_config>`\n\nincident.io notifications are sent via the [incident.io Alert Sources API](https://api-docs.incident.io/tag/Alert-Sources-V2#operation/Alert%20Sources%20V2_Create).\n\nWhen configuring this integration, you can do so via the `http_config` by setting the `authorization` directly or using one of `alert_source_token` or `alert_source_token_file`. The configuration of `alert_source_token` or `alert_source_token_file` takes precedence over `http_config`.\n\nPlease be aware that if the payload exceeds incident.io's API limits (512KB), the integration will automatically truncate all alerts except the first one.\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = true ]\n\n# The HTTP client's configuration.\n[ http_config: <http_config> | default = global.http_config ]\n\n# The URL to send the incident.io alert. This would typically be provided by the\n# incident.io team when setting up an alert source.\n# URL and URL_file are mutually exclusive.\nurl: <string>\nurl_file: <filepath>\n\n# The alert source token is used to authenticate with incident.io.\n# alert_source_token and alert_source_token_file are mutually exclusive.\n[ alert_source_token: <secret> ]\n[ alert_source_token_file: <filepath> ]\n\n# The maximum number of alerts to be sent per incident.io message.\n# Alerts exceeding this threshold will be truncated. Setting this to 0\n# allows an unlimited number of alerts. Note that if the payload exceeds\n# incident.io's size limits (512KB), the notifier will automatically drop\n# all alerts except the first one. If the payload is still too\n# large after this truncation, you will receive a 429 response and alerts\n# will not be ingested.\n[ max_alerts: <int> | default = 0 ]\n\n# Timeout is the maximum time allowed to invoke incident.io. Setting this to 0\n# does not impose a timeout.\n[ timeout: <duration> | default = 0s ]\n```\n\n### `<wechat_config>`\n\nWeChat notifications are sent via the [WeChat\nAPI](https://developers.weixin.qq.com/doc/offiaccount/en/Message_Management/Service_Center_messages.html).\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = false ]\n\n# The API key to use when talking to the WeChat API. Either api_secret or api_secret_file should be set.\n[ api_secret: <secret> | default = global.wechat_api_secret ]\n[ api_secret_file: <string> | default = global.wechat_api_secret_file ]\n\n# The WeChat API URL.\n[ api_url: <string> | default = global.wechat_api_url ]\n\n# The corp id for authentication.\n[ corp_id: <string> | default = global.wechat_api_corp_id ]\n\n# API request data as defined by the WeChat API.\n[ message: <tmpl_string> | default = '{{ template \"wechat.default.message\" . }}' ]\n# Type of the message type, supported values are `text` and `markdown`.\n[ message_type: <string> | default = 'text' ]\n[ agent_id: <string> | default = '{{ template \"wechat.default.agent_id\" . }}' ]\n[ to_user: <string> | default = '{{ template \"wechat.default.to_user\" . }}' ]\n[ to_party: <string> | default = '{{ template \"wechat.default.to_party\" . }}' ]\n[ to_tag: <string> | default = '{{ template \"wechat.default.to_tag\" . }}' ]\n```\n\n### `<webex_config>`\n\n```yaml\n# Whether to notify about resolved alerts.\n[ send_resolved: <boolean> | default = true ]\n\n# The Webex Teams API URL i.e. https://webexapis.com/v1/messages\n# If not specified, default API URL will be used.\n[ api_url: <string> | default = global.webex_api_url ]\n\n# ID of the Webex Teams room where to send the messages.\nroom_id: <tmpl_string>\n\n# Message template.\n[ message: <tmpl_string> default = '{{ template \"webex.default.message\" .}}' ]\n\n# The HTTP client's configuration. You must use this configuration to supply the bot token as part of the HTTP `Authorization` header.\n[ http_config: <http_config> | default = global.http_config ]\n```\n\n## Tracing Configuration\n### `<tracing_config>`\n\n```yaml\n# The tracing client type, supported values are `http` and `grpc`.\n[ client_type: <tracing_client_type> | default = \"grpc\" ]\n\n# The tracing endpoint.\n[ endpoint: <string> | default = \"\" ]\n\n# The sampling fraction.\n[ sampling_fraction: <float> | default = 0.0 ]\n\n# Whether to disable TLS.\n[ insecure: <boolean> | default = false ]\n\n# The HTTP client's configuration.\n[ tls_config: <tls_config> ]\n\n# Custom HTTP headers.\n[ http_headers:\n  [ <http_header> ] ]\n\n# The tracing compression.\n[ compression: <string> | default = \"gzip\" ]\n\n# The tracing timeout.\n[ timeout: <duration> | default = 0s ]\n```\n"
  },
  {
    "path": "docs/high_availability.md",
    "content": "---\ntitle: High Availability\nsort_rank: 4\nnav_icon: network\n---\n\nAlertmanager supports configuration to create a cluster for high availability. This document describes how the HA mechanism works, its design goals, and operational considerations.\n\n## Design Goals\n\nThe Alertmanager HA implementation is designed around three core principles:\n\n1. **Single pane view and management** - Silences and alerts can be viewed and managed from any cluster member, providing a unified operational experience\n2. **Survive cluster split-brain with \"fail open\"** - During network partitions, Alertmanager prefers to send duplicate notifications rather than miss critical alerts\n3. **At-least-once delivery** - The system guarantees that notifications are delivered at least once, in line with the fail-open philosophy\n\nThese goals prioritize operational reliability and alert delivery over strict exactly-once semantics.\n\n## Architecture Overview\n\nAn Alertmanager cluster consists of multiple Alertmanager instances that communicate using a gossip protocol. Each instance:\n\n- Receives alerts independently from Prometheus servers\n- Participates in a peer-to-peer gossip mesh\n- Replicates state (silences and notification log) to other cluster members\n- Processes and sends notifications independently\n\n```\n┌──────────────┐    ┌──────────────┐    ┌──────────────┐\n│ Prometheus 1 │    │ Prometheus 2 │    │ Prometheus N │\n└──────┬───────┘    └──────┬───────┘    └──────┬───────┘\n       │                   │                   │\n       │ alerts            │ alerts            │ alerts\n       │                   │                   │\n       ▼                   ▼                   ▼\n    ┌────────────────────────────────────────────┐\n    │  ┌──────────┐  ┌──────────┐  ┌──────────┐  │\n    │  │  AM-1    │  │  AM-2    │  │  AM-3    │  │\n    │  │ (pos: 0) ├──┤ (pos: 1) ├──┤ (pos: 2) │  │\n    │  └──────────┘  └──────────┘  └──────────┘  │\n    │          Gossip Protocol (Memberlist)      │\n    └────────────────────────────────────────────┘\n              │           │           │\n              ▼           ▼           ▼\n         Receivers   Receivers   Receivers\n```\n\n## Gossip Protocol\n\nAlertmanager uses [Hashicorp's Memberlist](https://github.com/hashicorp/memberlist) library to implement gossip-based communication. The gossip protocol handles:\n\n### Membership Management\n\n- **Automatic peer discovery** - Instances can be configured with a list of known peers and will automatically discover other cluster members\n- **Health checking** - Regular probes detect failed members (default: every 1 second)\n- **Failure detection** - Failed members are marked and can attempt to rejoin\n\n### State Replication\n\nThe gossip layer replicates three types of state:\n\n1. **Silences** - Create, update, and delete operations are broadcast to all peers\n2. **Notification log** - Records of which notifications were sent to prevent duplicates\n3. **Membership changes** - Join, leave, and failure events\n\nState is eventually consistent - all cluster members will converge to the same state given sufficient time and network connectivity.\n\n### Gossip Settling\n\nWhen an Alertmanager starts or rejoins the cluster, it waits for gossip to \"settle\" before processing notifications. This prevents sending notifications based on incomplete state.\n\nThe settling algorithm waits until:\n- The number of peers remains stable for 3 consecutive checks (default interval: push-pull interval)\n- Or a timeout occurs (configurable via context)\n\nDuring this time, the instance already receives and stores alerts but defers notification processing.\n\n## Notification Pipeline in HA Mode\n\nThe notification pipeline operates differently in a clustered environment to ensure deduplication while maintaining at-least-once delivery:\n\n```\n┌────────────────────────────────────────────────┐\n│              DISPATCHER STAGE                  │\n├────────────────────────────────────────────────┤\n│ 1. Find matching route(s)                      │\n│ 2. Find/create aggregation group within route  │\n│ 3. Throttle by group wait or group interval    │\n└───────────────────┬────────────────────────────┘\n                    │\n                    ▼\n┌────────────────────────────────────────────────┐\n│               NOTIFIER STAGE                   │\n├────────────────────────────────────────────────┤\n│ 1. Wait for HA gossip to settle                │◄─── Ensures complete state\n│ 2. Filter inhibited alerts                     │\n│ 3. Filter non-time-active alerts               │\n│ 4. Filter time-muted alerts                    │\n│ 5. Filter silenced alerts                      │◄─── Uses replicated silences\n│ 6. Wait according to HA cluster peer index     │◄─── Staggered notifications\n│ 7. Dedupe by repeat interval/HA state          │◄─── Uses notification log\n│ 8. Notify & retry intermittent failures        │\n│ 9. Update notification log                     │◄─── Replicated to peers\n└────────────────────────────────────────────────┘\n```\n\n### HA-Specific Stages\n\n#### 1. Gossip Settling Wait\n\nBefore the first notification from a group, the instance waits for gossip to settle. This ensures:\n- Silences are fully replicated\n- The notification log contains recent send records from other instances\n- The cluster membership is stable\n\n**Implementation**: `peer.WaitReady(ctx)`\n\n#### 2. Peer Position-Based Wait\n\nTo prevent all cluster members from sending notifications simultaneously, each instance waits based on its position in the sorted peer list:\n\n```\nwait_time = peer_position × peer_timeout\n```\n\nFor example, with 3 instances and a 15-second peer timeout:\n- Instance `am-1` (position 0): waits 0 seconds\n- Instance `am-2` (position 1): waits 15 seconds\n- Instance `am-3` (position 2): waits 30 seconds\n\nThis staggered timing allows:\n- The first instance to send the notification\n- Subsequent instances to see the notification log entry\n- Deduplication to prevent duplicate sends\n\n**Implementation**: `clusterWait()` in `cmd/alertmanager/main.go:594`\n\nPosition is determined by sorting all peer names alphabetically:\n\n```go\nfunc (p *Peer) Position() int {\n    all := p.mlist.Members()\n    sort.Slice(all, func(i, j int) bool {\n        return all[i].Name < all[j].Name\n    })\n    // Find position of self in sorted list\n}\n```\n\n#### 3. Deduplication via Notification Log\n\nThe `DedupStage` queries the notification log to determine if a notification should be sent:\n\n```go\n// Check notification log for recent sends\nentry := nflog.Query(receiver, groupKey)\nif entry.exists && !shouldNotify(entry, alerts, repeatInterval) {\n    // Skip: already notified recently\n    return nil\n}\n```\n\nDeduplication checks:\n- **Firing alerts changed?** If yes, notify\n- **Resolved alerts changed?** If yes and `send_resolved: true`, notify\n- **Repeat interval elapsed?** If yes, notify\n- **Otherwise**: Skip notification (deduplicated)\n\nThe notification log is replicated via gossip, so all cluster members share the same send history.\n\n## Split-Brain Handling (Fail Open)\n\nDuring a network partition, the cluster may split into multiple groups that cannot communicate. Alertmanager's \"fail open\" design ensures alerts are still delivered:\n\n### Scenario: Network Partition\n\n```\nBefore partition:\n┌────────┬────────┬────────┐\n│  AM-1  │  AM-2  │  AM-3  │\n└────────┴────────┴────────┘\n    Unified cluster\n\nAfter partition:\n┌────────┐       │       ┌────────┬────────┐\n│  AM-1  │       │       │  AM-2  │  AM-3  │\n└────────┘       │       └────────┴────────┘\n Partition A     │        Partition B\n```\n\n### Behavior During Partition\n\n**In Partition A** (AM-1 alone):\n- AM-1 sees itself as position 0\n- Waits 0 × timeout = 0 seconds\n- Sends notifications (no dedup from AM-2/AM-3)\n\n**In Partition B** (AM-2, AM-3):\n- AM-2 is position 0, AM-3 is position 1\n- AM-2 waits 0 seconds, sends notification\n- AM-3 sees AM-2's notification log entry, deduplicates\n\n**Result**: Duplicate notifications sent (one from Partition A, one from Partition B)\n\nThis is **intentional** - Alertmanager prefers duplicate notifications over missed alerts.\n\n### After Partition Heals\n\nWhen the network partition heals:\n1. Gossip protocol detects all peers again\n2. Notification logs are merged (via CRDT-like merge with timestamp)\n3. Future notifications are deduplicated correctly across all instances\n4. Silences created in either partition are replicated to all peers\n\n## Silence Management in HA\n\nSilences are first-class replicated state in the cluster.\n\n### Silence Creation and Updates\n\nWhen a silence is created or updated on any instance:\n\n1. **Local storage** - Silence is stored in the local state map\n2. **Broadcast** - Silence is serialized (protobuf) and broadcast via gossip\n3. **Merge on receive** - Other instances receive and merge the silence:\n   ```go\n   // Merge logic: last-write-wins based on UpdatedAt timestamp\n   if !exists || incoming.UpdatedAt > existing.UpdatedAt {\n       accept_update()\n   }\n   ```\n4. **Indexing** - The silence matcher cache is updated for fast alert matching\n\n### Silence Expiry\n\nSilences have:\n- `StartsAt`, `EndsAt` - The active time range\n- `ExpiresAt` - When to garbage collect (EndsAt + retention period)\n- `UpdatedAt` - For conflict resolution during merge\n\nEach instance independently:\n- Evaluates silence state (pending/active/expired) based on current time\n- Garbage collects expired silences past their retention period\n- The GC is local only (no gossip) since all instances converge to the same decision\n\n### Single Pane of Glass\n\nUsers can interact with any Alertmanager instance in the cluster:\n- **View silences** - All instances have the same silence state (eventually consistent)\n- **Create/update silences** - Changes made on any instance propagate to all peers\n- **Delete silences** - Implemented as \"expire immediately\" + gossip\n\nThis provides a unified operational experience regardless of which instance you access.\n\n## Operational Considerations\n\n### Configuration\n\nTo configure a cluster, each Alertmanager instance needs:\n\n```yaml\n# alertmanager.yml\nglobal:\n  # ... other config ...\n\n# No cluster config in YAML - use CLI flags\n```\n\nCommand-line flags:\n\n```bash\nalertmanager \\\n  --cluster.listen-address=0.0.0.0:9094 \\\n  --cluster.peer=am-1.example.com:9094 \\\n  --cluster.peer=am-2.example.com:9094 \\\n  --cluster.peer=am-3.example.com:9094 \\\n  --cluster.advertise-address=$(hostname):9094 \\\n  --cluster.peer-timeout=15s \\\n  --cluster.gossip-interval=200ms \\\n  --cluster.pushpull-interval=60s\n```\n\nKey flags:\n- `--cluster.listen-address` - Bind address for cluster communication (default: `0.0.0.0:9094`)\n- `--cluster.peer` - List of peer addresses (can be repeated)\n- `--cluster.advertise-address` - Address advertised to peers (auto-detected if omitted)\n- `--cluster.peer-timeout` - Wait time per peer position for deduplication (default: `15s`)\n- `--cluster.gossip-interval` - How often to gossip (default: `200ms`)\n- `--cluster.pushpull-interval` - Full state sync interval (default: `60s`)\n- `--cluster.probe-interval` - Peer health check interval (default: `1s`)\n- `--cluster.settle-timeout` - Max time to wait for gossip settling (default: context timeout)\n\n### Prometheus Configuration\n\n**Important**: Configure Prometheus to send alerts to **all** Alertmanager instances, not via a load balancer.\n\n```yaml\n# prometheus.yml\nalerting:\n  alertmanagers:\n    - static_configs:\n        - targets:\n            - am-1.example.com:9093\n            - am-2.example.com:9093\n            - am-3.example.com:9093\n```\n\nThis ensures:\n- **Redundancy** - If one Alertmanager is down, others still receive alerts\n- **Independent processing** - Each instance independently evaluates routing, grouping, and deduplication\n- **No single point of failure** - Load balancers introduce a single point of failure\n\n### Cluster Size Considerations\n\nSince Alertmanager uses gossip without quorum or voting, **any N instances tolerate up to N-1 failures** - as long as one instance is alive, notifications will be sent.\n\nHowever, cluster size involves tradeoffs:\n\n**Benefits of more instances:**\n- Greater resilience to simultaneous failures (hardware, network, datacenter outages)\n- Continued operation even during maintenance windows\n\n**Costs of more instances:**\n- In case of partitions there will be an increase in duplicate notifications\n- More gossip traffic\n\n**Typical deployments:**\n- **2-3 instances** - Common for single-datacenter production deployments\n- **4-5 instances** - Multi-datacenter or highly critical environments\n\n**Note**: Unlike consensus-based systems (etcd, Raft), odd vs. even cluster sizes make no difference - there is no voting or quorum.\n\n### Monitoring Cluster Health\n\nKey metrics to monitor:\n\n```\n# Cluster size\nalertmanager_cluster_members\n\n# Peer health\nalertmanager_cluster_peer_info\n\n# Peer position (affects notification timing)\nalertmanager_peer_position\n\n# Failed peers\nalertmanager_cluster_failed_peers\n\n# State replication\nalertmanager_nflog_gossip_messages_propagated_total\nalertmanager_silences_gossip_messages_propagated_total\n```\n\n### Security\n\nBy default, cluster communication is unencrypted. For production deployments, especially across WANs, use mutual TLS:\n\n```bash\nalertmanager \\\n  --cluster.tls-config=/etc/alertmanager/cluster-tls.yml\n```\n\nSee [Secure Cluster Traffic](../doc/design/secure-cluster-traffic.md) for details.\n\n### Persistence\n\nEach Alertmanager instance persists:\n- **Silences** - Stored in a snapshot file (default: `data/silences`)\n- **Notification log** - Stored in a snapshot file (default: `data/nflog`)\n\nOn restart:\n1. Instance loads silences and notification log from disk\n2. Joins the cluster and gossips with peers\n3. Merges state received from peers (newer timestamps win)\n4. Begins processing notifications after gossip settling\n\n**Note**: Alerts themselves are **not** persisted - Prometheus re-sends firing alerts regularly.\n\n### Common Pitfalls\n\n1. **Load balancing Prometheus → Alertmanager**\n   - ❌ Don't use a load balancer\n   - ✅ Configure all instances in Prometheus\n\n2. **Not waiting for gossip to settle**\n   - Can lead to missed silences or duplicate notifications on startup\n   - The `--cluster.settle-timeout` flag controls this\n\n3. **Network ACLs blocking cluster port**\n   - Ensure port 9094 (or your `--cluster.listen-address` port) is open between all instances\n   - Both TCP and UDP are used by default (TCP only if using TLS transport)\n\n4. **Unroutable advertise addresses**\n   - If `--cluster.advertise-address` is not set, Alertmanager tries to auto-detect\n   - For cloud/NAT environments, explicitly set a routable address\n\n5. **Mismatched cluster configurations**\n   - All instances should have the same `--cluster.peer-timeout` and gossip settings\n   - Mismatches can cause unnecessary duplicates or missed notifications\n\n## How It Works: End-to-End Example\n\n### Scenario: 3-instance cluster, new alert group\n\n1. **Alert arrives** at all 3 instances from Prometheus\n2. **Dispatcher** creates aggregation group, waits `group_wait` (e.g., 30s)\n3. **After group_wait**:\n   - Each instance prepares to notify\n4. **Notifier stage**:\n   - All instances wait for gossip to settle (if just started)\n   - **AM-1** (position 0): waits 0s, checks notification log (empty), sends notification, logs to nflog\n   - **AM-2** (position 1): waits 15s, checks notification log (sees AM-1's entry), **skips** notification\n   - **AM-3** (position 2): waits 30s, checks notification log (sees AM-1's entry), **skips** notification\n5. **Result**: Exactly one notification sent (by AM-1)\n\n### Scenario: AM-1 fails\n\n1. **Alert arrives** at AM-2 and AM-3 only\n2. **Dispatcher** creates group, waits `group_wait`\n3. **Notifier stage**:\n   - AM-1 is not in cluster (failed probe)\n   - **AM-2** is now position 0: waits 0s, sends notification\n   - **AM-3** is now position 1: waits 15s, sees AM-2's entry, skips\n4. **Result**: Notification still sent (fail-open)\n\n### Scenario: Network partition during notification\n\n1. **Alert arrives** at all instances\n2. **Network partition** splits AM-1 from AM-2/AM-3\n3. **In partition A** (AM-1):\n   - Position 0, waits 0s, sends notification\n4. **In partition B** (AM-2, AM-3):\n   - AM-2 is position 0, waits 0s, sends notification\n   - AM-3 is position 1, waits 15s, deduplicates\n5. **Result**: Two notifications sent (one per partition) - fail-open behavior\n\n## Troubleshooting\n\n### Check cluster status\n\n```bash\n# View cluster members via API\ncurl http://am-1:9093/api/v2/status\n\n# Check metrics\ncurl http://am-1:9093/metrics | grep cluster\n```\n\n### Diagnose split-brain\n\nIf you suspect split-brain:\n\n1. Check `alertmanager_cluster_members` on each instance\n   - Should match total cluster size\n2. Check `alertmanager_cluster_peer_info{state=\"alive\"}`\n   - Should show all peers as alive\n3. Review network connectivity between instances\n\n### Debug duplicate notifications\n\nDuplicate notifications can occur due to:\n\n1. **Network partitions** (expected, fail-open)\n2. **Gossip not settled** - Check `--cluster.settle-timeout`\n3. **Clock skew** - Ensure NTP is configured on all instances\n4. **Notification log not replicating** - Check gossip metrics\n\nEnable debug logging:\n\n```bash\nalertmanager --log.level=debug\n```\n\nLook for:\n- `\"Waiting for gossip to settle...\"`\n- `\"gossip settled; proceeding\"`\n- Deduplication decisions in notification pipeline\n\n## Further Reading\n\n- [Alertmanager Configuration](configuration.md)\n- [Secure Cluster Traffic Design](../doc/design/secure-cluster-traffic.md)\n- [Hashicorp Memberlist Documentation](https://github.com/hashicorp/memberlist)\n"
  },
  {
    "path": "docs/https.md",
    "content": "---\ntitle: HTTPS and authentication\nsort_rank: 11\n---\n\nAlertmanager supports basic authentication and TLS.\nThis is **experimental** and might change in the future.\n\nCurrently TLS is supported for the HTTP traffic and gossip traffic.\n\n## HTTP Traffic\n\nTo specify which web configuration file to load, use the `--web.config.file` flag.\n\nThe file is written in [YAML format](https://en.wikipedia.org/wiki/YAML),\ndefined by the scheme described below.\nBrackets indicate that a parameter is optional. For non-list parameters the\nvalue is set to the specified default.\n\nThe file is read upon every http request, such as any change in the\nconfiguration and the certificates is picked up immediately.\n\nGeneric placeholders are defined as follows:\n\n* `<boolean>`: a boolean that can take the values `true` or `false`\n* `<filename>`: a valid path in the current working directory\n* `<secret>`: a regular string that is a secret, such as a password\n* `<string>`: a regular string\n\n```\ntls_server_config:\n  # Certificate and key files for server to use to authenticate to client.\n  cert_file: <filename>\n  key_file: <filename>\n\n  # Server policy for client authentication. Maps to ClientAuth Policies.\n  # For more detail on clientAuth options:\n  # https://golang.org/pkg/crypto/tls/#ClientAuthType\n  #\n  # NOTE: If you want to enable client authentication, you need to use\n  # RequireAndVerifyClientCert. Other values are insecure.\n  [ client_auth_type: <string> | default = \"NoClientCert\" ]\n\n  # CA certificate for client certificate authentication to the server.\n  [ client_ca_file: <filename> ]\n\n  # Verify that the client certificate has a Subject Alternate Name (SAN)\n  # which is an exact match to an entry in this list, else terminate the\n  # connection. SAN match can be one or multiple of the following: DNS,\n  # IP, e-mail, or URI address from https://pkg.go.dev/crypto/x509#Certificate.\n  [ client_allowed_sans:\n    [ - <string> ] ]\n\n  # Minimum TLS version that is acceptable.\n  [ min_version: <string> | default = \"TLS12\" ]\n\n  # Maximum TLS version that is acceptable.\n  [ max_version: <string> | default = \"TLS13\" ]\n\n  # List of supported cipher suites for TLS versions up to TLS 1.2. If empty,\n  # Go default cipher suites are used. Available cipher suites are documented\n  # in the go documentation:\n  # https://golang.org/pkg/crypto/tls/#pkg-constants\n  #\n  # Note that only the cipher returned by the following function are supported:\n  # https://pkg.go.dev/crypto/tls#CipherSuites\n  [ cipher_suites:\n    [ - <string> ] ]\n\n  # prefer_server_cipher_suites controls whether the server selects the\n  # client's most preferred ciphersuite, or the server's most preferred\n  # ciphersuite. If true then the server's preference, as expressed in\n  # the order of elements in cipher_suites, is used.\n  [ prefer_server_cipher_suites: <bool> | default = true ]\n\n  # Elliptic curves that will be used in an ECDHE handshake, in preference\n  # order. Available curves are documented in the go documentation:\n  # https://golang.org/pkg/crypto/tls/#CurveID\n  [ curve_preferences:\n    [ - <string> ] ]\n\nhttp_server_config:\n  # Enable HTTP/2 support. Note that HTTP/2 is only supported with TLS.\n  # This can not be changed on the fly.\n  [ http2: <boolean> | default = true ]\n  # List of headers that can be added to HTTP responses.\n  [ headers:\n    # Set the Content-Security-Policy header to HTTP responses.\n    # Unset if blank.\n    [ Content-Security-Policy: <string> ]\n    # Set the X-Frame-Options header to HTTP responses.\n    # Unset if blank. Accepted values are deny and sameorigin.\n    # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options\n    [ X-Frame-Options: <string> ]\n    # Set the X-Content-Type-Options header to HTTP responses.\n    # Unset if blank. Accepted value is nosniff.\n    # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options\n    [ X-Content-Type-Options: <string> ]\n    # Set the X-XSS-Protection header to all responses.\n    # Unset if blank.\n    # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection\n    [ X-XSS-Protection: <string> ]\n    # Set the Strict-Transport-Security header to HTTP responses.\n    # Unset if blank.\n    # Please make sure that you use this with care as this header might force\n    # browsers to load Prometheus and the other applications hosted on the same\n    # domain and subdomains over HTTPS.\n    # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security\n    [ Strict-Transport-Security: <string> ] ]\n\n# Usernames and hashed passwords that have full access to the web\n# server via basic authentication. If empty, no basic authentication is\n# required. Passwords are hashed with bcrypt.\nbasic_auth_users:\n  [ <string>: <secret> ... ]\n```\n\n## Gossip Traffic\n\nTo specify whether to use mutual TLS for gossip, use the `--cluster.tls-config` flag.\n\nThe server and client sides of the gossip are configurable.\n\n```\ntls_server_config:\n  # Certificate and key files for server to use to authenticate to client.\n  cert_file: <filename>\n  key_file: <filename>\n\n  # Server policy for client authentication. Maps to ClientAuth Policies.\n  # For more detail on clientAuth options:\n  # https://golang.org/pkg/crypto/tls/#ClientAuthType\n  [ client_auth_type: <string> | default = \"NoClientCert\" ]\n\n  # CA certificate for client certificate authentication to the server.\n  [ client_ca_file: <filename> ]\n\n  # Minimum TLS version that is acceptable.\n  [ min_version: <string> | default = \"TLS12\" ]\n\n  # Maximum TLS version that is acceptable.\n  [ max_version: <string> | default = \"TLS13\" ]\n\n  # List of supported cipher suites for TLS versions up to TLS 1.2. If empty,\n  # Go default cipher suites are used. Available cipher suites are documented\n  # in the go documentation:\n  # https://golang.org/pkg/crypto/tls/#pkg-constants\n  [ cipher_suites:\n    [ - <string> ] ]\n\n  # prefer_server_cipher_suites controls whether the server selects the\n  # client's most preferred ciphersuite, or the server's most preferred\n  # ciphersuite. If true then the server's preference, as expressed in\n  # the order of elements in cipher_suites, is used.\n  [ prefer_server_cipher_suites: <bool> | default = true ]\n\n  # Elliptic curves that will be used in an ECDHE handshake, in preference\n  # order. Available curves are documented in the go documentation:\n  # https://golang.org/pkg/crypto/tls/#CurveID\n  [ curve_preferences:\n    [ - <string> ] ]\n\ntls_client_config:\n  # Path to the CA certificate with which to validate the server certificate.\n  [ ca_file: <filepath> ]\n\n  # Certificate and key files for client cert authentication to the server.\n  [ cert_file: <filepath> ]\n  [ key_file: <filepath> ]\n\n  # Server name extension to indicate the name of the server.\n  # http://tools.ietf.org/html/rfc4366#section-3.1\n  [ server_name: <string> ]\n\n  # Disable validation of the server certificate.\n  [ insecure_skip_verify: <boolean> | default = false]\n```\n"
  },
  {
    "path": "docs/index.md",
    "content": "---\ntitle: Alerting\nsort_rank: 7\nnav_icon: bell-o\n---\n"
  },
  {
    "path": "docs/integrations.md",
    "content": "---\ntitle: Notification Integrations\nsort_rank: 4\n---\n\nAlertmanager supports a number of notification integrations via the [configuration file](configuration.md).\n\n## Available Integrations\n\n| Name | Configuration | External Configuration | API Reference |\n|------|---------------|-------------------------------------|---------------|\n| [Amazon SNS](https://aws.amazon.com/sns/) | [sns_config](configuration.md#sns_config) | [Amazon SNS Documentation](https://docs.aws.amazon.com/sns/) | [SNS API Reference](https://docs.aws.amazon.com/sns/latest/api/welcome.html) |\n| [Discord](https://discord.com/) | [discord_config](configuration.md#discord_config) | [Intro to Webhooks](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) | [Discord Webhook API](https://discord.com/developers/docs/resources/webhook) |\n| [Email](https://en.wikipedia.org/wiki/Email) | [email_config](configuration.md#email_config) | - | [SMTP](https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol) |\n| [incident.io](https://incident.io/) | [incidentio_config](configuration.md#incidentio_config) | [Alert Sources Documentation](https://api-docs.incident.io/tag/Alert-Sources-V2) | [Alert Sources V2 API](https://api-docs.incident.io/tag/Alert-Sources-V2#operation/Alert%20Sources%20V2_Create) |\n| [Jira](https://www.atlassian.com/software/jira) | [jira_config](configuration.md#jira_config) | [Jira Cloud Platform](https://developer.atlassian.com/cloud/jira/platform/) | [REST API v2](https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/) / [REST API v3](https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/) |\n| [Mattermost](https://mattermost.com/) | [mattermost_config](configuration.md#mattermost_config) | [Incoming Webhooks](https://developers.mattermost.com/integrate/webhooks/incoming/) | [Mattermost Webhook API](https://developers.mattermost.com/integrate/webhooks/incoming/) |\n| [Microsoft Teams](https://www.microsoft.com/en-us/microsoft-teams/) | [msteams_config](configuration.md#msteams_config) | [Incoming Webhooks (Deprecated)](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/what-are-webhooks-and-connectors) | [Microsoft Teams Connectors](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/what-are-webhooks-and-connectors) |\n| [Microsoft Teams v2](https://www.microsoft.com/en-us/microsoft-teams/) | [msteamsv2_config](configuration.md#msteamsv2_config) | [Workflows for Teams](https://support.microsoft.com/en-gb/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498) | [Power Automate Flows](https://learn.microsoft.com/en-us/power-automate/teams/overview) |\n| [OpsGenie](https://www.atlassian.com/software/opsgenie) | [opsgenie_config](configuration.md#opsgenie_config) | [OpsGenie Documentation](https://docs.opsgenie.com/) | [OpsGenie Alert API](https://docs.opsgenie.com/docs/alert-api) |\n| [PagerDuty](https://www.pagerduty.com/) | [pagerduty_config](configuration.md#pagerduty_config) | [Prometheus Integration Guide](https://www.pagerduty.com/docs/guides/prometheus-integration-guide/) | [PagerDuty Events API](https://developer.pagerduty.com/documentation/integration/events) |\n| [Pushover](https://pushover.net/) | [pushover_config](configuration.md#pushover_config) | [Pushover Documentation](https://pushover.net/api) | [Pushover API](https://pushover.net/api) |\n| [Rocket.Chat](https://rocket.chat/) | [rocketchat_config](configuration.md#rocketchat_config) | [Personal Access Tokens](https://docs.rocket.chat/use-rocket.chat/user-guides/user-panel/my-account#personal-access-tokens) | [Rocket.Chat REST API](https://developer.rocket.chat/reference/api/rest-api/endpoints/messaging/chat-endpoints/postmessage) |\n| [Slack](https://slack.com/) | [slack_config](configuration.md#slack_config) | [Incoming Webhooks](https://api.slack.com/messaging/webhooks) / [Bot Tokens](https://api.slack.com/authentication/token-types) | [Slack API](https://api.slack.com/methods/chat.postMessage) |\n| [Telegram](https://telegram.org/) | [telegram_config](configuration.md#telegram_config) | [Telegram Bots](https://core.telegram.org/bots) | [Telegram Bot API](https://core.telegram.org/bots/api) |\n| [VictorOps](https://victorops.com/) | [victorops_config](configuration.md#victorops_config) | [REST Endpoint Integration Guide](https://help.victorops.com/knowledge-base/rest-endpoint-integration-guide/) | [VictorOps REST API](https://help.victorops.com/knowledge-base/rest-endpoint-integration-guide/) |\n| [Webex](https://www.webex.com/) | [webex_config](configuration.md#webex_config) | [Webex for Developers](https://developer.webex.com/) | [Webex Messages API](https://developer.webex.com/docs/api/v1/messages) |\n| [Webhook](https://en.wikipedia.org/wiki/Webhook) | [webhook_config](configuration.md#webhook_config) | [Webhook Integrations](https://prometheus.io/docs/operating/integrations/#alertmanager-webhook-receiver) | - |\n| [WeChat](https://www.wechat.com/) | [wechat_config](configuration.md#wechat_config) | [WeChat Work Documentation](https://developers.weixin.qq.com/doc/offiaccount/en/Message_Management/Service_Center_messages.html) | [WeChat Work API](https://developers.weixin.qq.com/doc/offiaccount/en/Message_Management/Service_Center_messages.html) |\n\nFor notification mechanisms not natively supported by the Alertmanager, the webhook receiver allows for integration. An incomplete list can be found [here](https://prometheus.io/docs/operating/integrations/#alertmanager-webhook-receiver).\n"
  },
  {
    "path": "docs/management_api.md",
    "content": "---\ntitle: Management API\nsort_rank: 9\n---\n\nAlertmanager provides a set of management API to ease automation and integrations.\n\n\n### Health check\n\n```\nGET /-/healthy\nHEAD /-/healthy\n```\n\nThis endpoint always returns 200 and should be used to check Alertmanager health.\n\n\n### Readiness check\n\n```\nGET /-/ready\nHEAD /-/ready\n```\n\nThis endpoint returns 200 when Alertmanager is ready to serve traffic (i.e. respond to queries).\n\n\n### Reload\n\n```\nPOST /-/reload\n```\n\nThis endpoint triggers a reload of the Alertmanager configuration file.\n\nAn alternative way to trigger a configuration reload is by sending a `SIGHUP` to the Alertmanager process.\n"
  },
  {
    "path": "docs/notification_examples.md",
    "content": "---\ntitle: Notification template examples\nsort_rank: 8\n---\n\nThe following are all different examples of alerts and corresponding Alertmanager configuration file setups (alertmanager.yml).\nEach use the [Go templating](http://golang.org/pkg/text/template/) system.\n\n## Customizing Slack notifications\n\nIn this example we've customised our Slack notification to send a URL to our organisation's wiki on how to deal with the particular alert that's been sent.\n\n```\nglobal:\n  # Also possible to place this URL in a file.\n  # Ex: `slack_api_url_file: '/etc/alertmanager/slack_url'`\n  slack_api_url: '<slack_webhook_url>'\n\nroute:\n  receiver: 'slack-notifications'\n  group_by: [alertname, datacenter, app]\n\nreceivers:\n- name: 'slack-notifications'\n  slack_configs:\n  - channel: '#alerts'\n    text: 'https://internal.myorg.net/wiki/alerts/{{ .GroupLabels.app }}/{{ .GroupLabels.alertname }}'\n```\n\n## Accessing annotations in CommonAnnotations\n\nIn this example we again customize the text sent to our Slack receiver accessing the `summary` and `description` stored in the `CommonAnnotations` of the data sent by the Alertmanager.\n\nAlert\n\n```\ngroups:\n- name: Instances\n  rules:\n  - alert: InstanceDown\n    expr: up == 0\n    for: 5m\n    labels:\n      severity: page\n    # Prometheus templates apply here in the annotation and label fields of the alert.\n    annotations:\n      description: '{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 5 minutes.'\n      summary: 'Instance {{ $labels.instance }} down'\n```\n\nReceiver\n\n```\n- name: 'team-x'\n  slack_configs:\n  - channel: '#alerts'\n    # Alertmanager templates apply here.\n    text: \"<!channel> \\nsummary: {{ .CommonAnnotations.summary }}\\ndescription: {{ .CommonAnnotations.description }}\"\n```\n\n## Ranging over all received Alerts\n\nFinally, assuming the same alert as the previous example, we customize our receiver to range over all of the alerts received from the Alertmanager, printing their respective annotation summaries and descriptions on new lines.\n\nReceiver\n\n```\n- name: 'default-receiver'\n  slack_configs:\n  - channel: '#alerts'\n    title: \"{{ range .Alerts }}{{ .Annotations.summary }}\\n{{ end }}\"\n    text: \"{{ range .Alerts }}{{ .Annotations.description }}\\n{{ end }}\"\n```\n\n## Defining reusable templates\n\nGoing back to our first example, we can also provide a file containing named templates which are then loaded by Alertmanager in order to avoid complex templates that span many lines.\nCreate a file under `/alertmanager/template/myorg.tmpl` and create a template in it named \"slack.myorg.text\":\n\n```\n{{ define \"slack.myorg.text\" }}https://internal.myorg.net/wiki/alerts/{{ .GroupLabels.app }}/{{ .GroupLabels.alertname }}{{ end}}\n```\n\nThe configuration now loads the template with the given name for the \"text\" field and we provide a path to our custom template file:\n\n```\nglobal:\n  slack_api_url: '<slack_webhook_url>'\n\nroute:\n  receiver: 'slack-notifications'\n  group_by: [alertname, datacenter, app]\n\nreceivers:\n- name: 'slack-notifications'\n  slack_configs:\n  - channel: '#alerts'\n    text: '{{ template \"slack.myorg.text\" . }}'\n\ntemplates:\n- '/etc/alertmanager/templates/myorg.tmpl'\n```\n\nThis example is explained in further detail in this [blogpost](https://prometheus.io/blog/2016/03/03/custom-alertmanager-templates/).\n"
  },
  {
    "path": "docs/notifications.md",
    "content": "---\ntitle: Notification template reference\nsort_rank: 7\n---\n\nPrometheus creates and sends alerts to the Alertmanager which then sends notifications out to different receivers based on their labels.\nA receiver can be one of many integrations including: Slack, PagerDuty, email, or a custom integration via the generic webhook interface.\n\nThe notifications sent to receivers are constructed via templates. The Alertmanager comes with default templates but they can also be customized.\nTo avoid confusion it's important to note that the Alertmanager templates differ from [templating in Prometheus](https://prometheus.io/docs/visualization/template_reference/), however Prometheus templating also includes the templating in alert rule labels/annotations.\n\n\nThe Alertmanager's notification templates are based on the [Go templating](http://golang.org/pkg/text/template) system.\nNote that some fields are evaluated as text, and others as HTML which will affect escaping.\n\n# Data Structures\n\n## Data\n\n`Data` is the structure passed to notification templates and webhook pushes.\n\n| Name          | Type     | Notes    |\n| ------------- | ------------- | -------- |\n| Receiver | string | Defines the receiver's name that the notification will be sent to (slack, email etc.). |\n| Status | string | Defined as firing if at least one alert is firing, otherwise resolved. |\n| Alerts | [Alert](#alert) | List of all alert objects in this group ([see below](#alert)). |\n| GroupLabels | [KV](#kv) | The labels these alerts were grouped by. |\n| CommonLabels | [KV](#kv) | The labels common to all of the alerts. |\n| CommonAnnotations | [KV](#kv) | Set of common annotations to all of the alerts. Used for longer additional strings of information about the alert. |\n| ExternalURL | string | Backlink to the Alertmanager that sent the notification. |\n\nThe `Alerts` type exposes functions for filtering alerts:\n\n - `Alerts.Firing` returns a list of currently firing alert objects in this group\n - `Alerts.Resolved` returns a list of resolved alert objects in this group\n\n## Alert\n\n`Alert` holds one alert for notification templates.\n\n| Name          | Type     | Notes    |\n| ------------- | ------------- | -------- |\n| Status | string | Defines whether or not the alert is resolved or currently firing. |\n| Labels | [KV](#kv) | A set of labels to be attached to the alert. |\n| Annotations | [KV](#kv) | A set of annotations for the alert. |\n| StartsAt | time.Time | The time the alert started firing. If omitted, the current time is assigned by the Alertmanager. |\n| EndsAt | time.Time | Only set if the end time of an alert is known. Otherwise set to a configurable timeout period from the time since the last alert was received. |\n| GeneratorURL | string | A backlink which identifies the causing entity of this alert. |\n| Fingerprint | string | Fingerprint that can be used to identify the alert. |\n\n## KV\n\n`KV` is a set of key/value string pairs used to represent labels and annotations.\n\n```\ntype KV map[string]string\n```\n\nAnnotation example containing two annotations:\n\n```\n{\n  summary: \"alert summary\",\n  description: \"alert description\",\n}\n```\n\nIn addition to direct access of data (labels and annotations) stored as KV, there are also methods for sorting, removing, and viewing the LabelSets:\n\n### KV methods\n| Name          | Arguments     | Returns  | Notes    |\n| ------------- | ------------- | -------- | -------- |\n| SortedPairs | - | Pairs (list of key/value string pairs.) | Returns a sorted list of key/value pairs. |\n| Remove | []string | KV | Returns a copy of the key/value map without the given keys. |\n| Names | - | []string | Returns the names of the label names in the LabelSet. |\n| Values | - | []string | Returns a list of the values in the LabelSet. |\n\n# Functions\n\nNote the [default\nfunctions](http://golang.org/pkg/text/template/#hdr-Functions) also provided by Go\ntemplating.\n\n## Strings\n\n| Name             | Arguments                  | Description |\n| ---------------- | -------------------------- | ----------- |\n| date             | string, time.Time          | Returns the text representation of the time in the specified format. For documentation on formats refer to [pkg.go.dev/time](https://pkg.go.dev/time#pkg-constants). |\n| humanizeDuration | number or string           | Returns a human-readable string representing the duration, and the error if it happened. |\n| join             | sep string, s []string     | [strings.Join](http://golang.org/pkg/strings/#Join), concatenates the elements of s to create a single string. The separator string sep is placed between elements in the resulting string. (note: argument order inverted for easier pipelining in templates.) |\n| match            | pattern, string            | [Regexp.MatchString](https://golang.org/pkg/regexp/#MatchString). Match a string using Regexp. |\n| reReplaceAll     | pattern, replacement, text | [Regexp.ReplaceAllString](http://golang.org/pkg/regexp/#Regexp.ReplaceAllString) Regexp substitution, unanchored. |\n| safeHtml         | text string                | [html/template.HTML](https://golang.org/pkg/html/template/#HTML), Marks string as HTML not requiring auto-escaping. |\n| safeUrl          | text string                | [html/template.URL](https://golang.org/pkg/html/template/#URL), Marks string as URL not requiring auto-escaping. |\n| since            | time.Time                  | [time.Since](https://pkg.go.dev/time#Since), returns the duration of how much time passed from the provided time till the current system time. |\n| stringSlice      | ...string                  | Returns the passed strings as a slice of strings. |\n| title            | string                     | [strings.Title](http://golang.org/pkg/strings/#Title), capitalises first character of each word. |\n| toJson           | any                        | [json.Marshal](https://pkg.go.dev/encoding/json#Marshal), returns the JSON encoding of the value. |\n| toLower          | string                     | [strings.ToLower](http://golang.org/pkg/strings/#ToLower), converts all characters to lower case. |\n| toUpper          | string                     | [strings.ToUpper](http://golang.org/pkg/strings/#ToUpper), converts all characters to upper case. |\n| trimSpace        | string                     | [strings.TrimSpace](https://pkg.go.dev/strings#TrimSpace), removes leading and trailing white spaces. |\n| tz               | string, time.Time          | Returns the time in the timezone. For example, Europe/Paris. |\n| urlUnescape      | text string                | [url.QueryUnescape](https://pkg.go.dev/net/url#QueryUnescape), unescapes a URL with % encoding |\n"
  },
  {
    "path": "docs/overview.md",
    "content": "---\ntitle: Alerting overview\nsort_rank: 1\nnav_icon: sliders\n---\n\nAlerting with Prometheus is separated into two parts. Alerting rules in\nPrometheus servers send alerts to an Alertmanager. The [Alertmanager](alertmanager.md)\nthen manages those alerts, including silencing, inhibition, aggregation and\nsending out notifications via methods such as email, on-call notification systems, and chat platforms.\n\nThe main steps to setting up alerting and notifications are:\n\n* Setup and [configure](configuration.md) the Alertmanager\n* [Configure Prometheus](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#alertmanager_config) to talk to the Alertmanager\n* Create [alerting rules](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/) in Prometheus\n"
  },
  {
    "path": "examples/ha/send_alerts.sh",
    "content": "#!/usr/bin/env bash\nalerts1='[\n  {\n    \"labels\": {\n       \"alertname\": \"DiskRunningFull\",\n       \"dev\": \"sda1\",\n       \"instance\": \"example1\"\n     },\n     \"annotations\": {\n        \"info\": \"The disk sda1 is running full\",\n        \"summary\": \"please check the instance example1\"\n      }\n  },\n  {\n    \"labels\": {\n       \"alertname\": \"DiskRunningFull\",\n       \"dev\": \"sda2\",\n       \"instance\": \"example1\"\n     },\n     \"annotations\": {\n        \"info\": \"The disk sda2 is running full\",\n        \"summary\": \"please check the instance example1\",\n        \"runbook\": \"the following link http://test-url should be clickable\"\n      }\n  },\n  {\n    \"labels\": {\n       \"alertname\": \"DiskRunningFull\",\n       \"dev\": \"sda1\",\n       \"instance\": \"example2\"\n     },\n     \"annotations\": {\n        \"info\": \"The disk sda1 is running full\",\n        \"summary\": \"please check the instance example2\"\n      }\n  },\n  {\n    \"labels\": {\n       \"alertname\": \"DiskRunningFull\",\n       \"dev\": \"sdb2\",\n       \"instance\": \"example2\"\n     },\n     \"annotations\": {\n        \"info\": \"The disk sdb2 is running full\",\n        \"summary\": \"please check the instance example2\"\n      }\n  },\n  {\n    \"labels\": {\n       \"alertname\": \"DiskRunningFull\",\n       \"dev\": \"sda1\",\n       \"instance\": \"example3\",\n       \"severity\": \"critical\"\n     }\n  },\n  {\n    \"labels\": {\n       \"alertname\": \"DiskRunningFull\",\n       \"dev\": \"sda1\",\n       \"instance\": \"example3\",\n       \"severity\": \"warning\"\n     }\n  }\n]'\ncurl -XPOST -d\"$alerts1\" http://localhost:9093/api/v1/alerts\ncurl -XPOST -d\"$alerts1\" http://localhost:9094/api/v1/alerts\ncurl -XPOST -d\"$alerts1\" http://localhost:9095/api/v1/alerts\n"
  },
  {
    "path": "examples/ha/tls/Makefile",
    "content": "# Based on https://github.com/wolfeidau/golang-massl/\n\n.PHONY: start\nstart:\n\tgoreman start\n\n.PHONY: gen-certs\ngen-certs: certs/ca.pem certs/node1.pem certs/node1-key.pem certs/node2.pem certs/node2-key.pem\n\ncerts/ca.pem certs/ca-key.pem: certs/ca-csr.json\n\tcd certs; cfssl gencert -initca ca-csr.json | cfssljson -bare ca\n\ncerts/node1.pem certs/node1-key.pem: certs/ca-config.json certs/ca.pem certs/ca-key.pem certs/node1-csr.json\n\tcd certs; cfssl gencert  \\\n    -ca=ca.pem \\\n    -ca-key=ca-key.pem \\\n    -config=ca-config.json \\\n    -hostname=localhost,127.0.0.1 \\\n    -profile=massl node1-csr.json | cfssljson -bare node1\n\ncerts/node2.pem certs/node2-key.pem: certs/ca-config.json certs/ca.pem certs/ca-key.pem certs/node2-csr.json\n\tcd certs; cfssl gencert  \\\n    -ca=ca.pem \\\n    -ca-key=ca-key.pem \\\n    -config=ca-config.json \\\n    -hostname=localhost,127.0.0.1 \\\n    -profile=massl node2-csr.json | cfssljson -bare node2\n"
  },
  {
    "path": "examples/ha/tls/Procfile",
    "content": "a1: ./../../../alertmanager --log.level=debug --storage.path=$TMPDIR/a1 --web.listen-address=:9093  --cluster.listen-address=127.0.0.1:8001 --config.file=../alertmanager.yml --cluster.tls-config=./tls_config_node1.yml\na2: ./../../../alertmanager --log.level=debug --storage.path=$TMPDIR/a2 --web.listen-address=:9094  --cluster.listen-address=127.0.0.1:8002 --cluster.peer=127.0.0.1:8001 --config.file=../alertmanager.yml --cluster.tls-config=./tls_config_node2.yml\na3: ./../../../alertmanager --log.level=debug --storage.path=$TMPDIR/a3 --web.listen-address=:9095  --cluster.listen-address=127.0.0.1:8003 --cluster.peer=127.0.0.1:8001 --config.file=../alertmanager.yml --cluster.tls-config=./tls_config_node1.yml\na4: ./../../../alertmanager --log.level=debug --storage.path=$TMPDIR/a4 --web.listen-address=:9096  --cluster.listen-address=127.0.0.1:8004 --cluster.peer=127.0.0.1:8001 --config.file=../alertmanager.yml --cluster.tls-config=./tls_config_node2.yml\na5: ./../../../alertmanager --log.level=debug --storage.path=$TMPDIR/a5 --web.listen-address=:9097  --cluster.listen-address=127.0.0.1:8005 --cluster.peer=127.0.0.1:8001 --config.file=../alertmanager.yml --cluster.tls-config=./tls_config_node1.yml\na6: ./../../../alertmanager --log.level=debug --storage.path=$TMPDIR/a6 --web.listen-address=:9098  --cluster.listen-address=127.0.0.1:8006 --cluster.peer=127.0.0.1:8001 --config.file=../alertmanager.yml --cluster.tls-config=./tls_config_node2.yml\nwh: go run ../../webhook/echo.go\n"
  },
  {
    "path": "examples/ha/tls/README.md",
    "content": "# TLS Transport Config Example\n\n## Usage\n1. Install dependencies:\n   1. `go install github.com/cloudflare/cfssl/cmd/cfssl`\n   2. `go install github.com/mattn/goreman`\n2. Build Alertmanager (root of repository):\n   1. `go mod download`\n   1. `make build`.\n2. `make start` (inside this directory).\n\n## Testing\n1. Start the cluster (as explained above)\n2. Navigate to one of the Alertmanager instances at `localhost:9093`.\n3. Create a silence.\n4. Navigate to the other Alertmanager instance at `localhost:9094`.\n5. Observe that the silence created in the other Alertmanager instance has been synchronized over to this instance.\n6. Repeat.\n"
  },
  {
    "path": "examples/ha/tls/certs/ca-config.json",
    "content": "{\n  \"signing\": {\n    \"default\": {\n      \"expiry\": \"876000h\"\n    },\n    \"profiles\": {\n      \"massl\": {\n        \"usages\": [\"signing\", \"key encipherment\", \"server auth\", \"client auth\"],\n        \"expiry\": \"876000h\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "examples/ha/tls/certs/ca-csr.json",
    "content": "{\n  \"CN\": \"massl\",\n  \"key\": {\n    \"algo\": \"rsa\",\n    \"size\": 2048\n  },\n  \"names\": [\n    {\n      \"C\": \"AU\",\n      \"L\": \"Melbourne\",\n      \"O\": \"massl\",\n      \"OU\": \"VIC\",\n      \"ST\": \"Victoria\"\n    }\n  ]\n}"
  },
  {
    "path": "examples/ha/tls/certs/ca-key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAuljDjKVGwlyiuKTSHc1QpoZPX9dbgwU/9113ctI8U/ZwMWLp\nnZ4f/zVpf4LW5foM9zSEUGPiyJe/NaTZUOXkRBSIQ13QroK4OJ1XGacQKpTxewCb\nChESZEfKWEhnP/Y7BYc4z1Li6Dkxh4TIElHwOVe62jbhNnzYlr4evmSuiuItAc8u\nhEYxncThPzmHEWPXKw8CFNhxCSYsjbb72UAIht0knMHQ7VXBX1VuuL0rolJBiToC\nva+I6CjG0c6qfi9/BcPsuW6cNjmQnwTg6SaSoGO/5zgbxBgy9MZQEot88d1T2XH6\nrBANYsfojvyCXuytWnj04mvdAWwmFh0hhq+nxQIDAQABAoIBAQCwcL1vXUq7W4UD\nOaRtbWrQ0dk0ETBnxT/E0y33fRJ8GZovWM2EXSVEuukSP+uEQ5elNYeWqo0fi3cT\nruvJSnMw9xPyXVDq+4C8slW3R1TqTK683VzvUizM4KC5qIyCpn1KBbgHrh6E7Sp1\ne4cIuaawVN3qIg5qThmx2YA4nBIcEt68q9cpy3NgEe+EQf44zM/St+y8kSkDUOVw\nfNKX0WfZ/hPL1TAYpWiIgSf+m/V3d/1l/scvMYONcuSjXSORCyoeAWYtOQgf78wW\n9j3kiBTaqDYCUZFnY/ltlZrm8ltAaKVJ0MmPKjVh8GJBXZp9fSVU8Y4ZIZRSeuBA\nOoStHGAdAoGBAMluMIE33hGny2V0dNzW23D84eXQK38AsdP632jQeuzxBknItg45\nqAfzh8F8W10DQnSv5tj0bmOHfo0mG09bu9eo5nLLINOE7Ju/7ly/76RNJNJ4ADjx\nJKZi/PpvfP+s/fzel0X3OPgA+CJKzUHuqlU4V9BLc7focZAYtaM2w7rHAoGBAOzU\neXpapkqYhbYRcsrVV57nZV0rLzsLVJBpJg2zC8un95ALrr0rlZfuPJfOCY/uuS1w\nf8ixRz2MkRWGreLHy35NB4GV0sF9VPn1jMp9SuBNvO0JRUMWuDAdVe8SCjXadrOh\n+m3yKJSkFKDchglUYnZKV1skgA/b9jjjnu2fvd0TAoGAVUTnFZxvzmuIp78fxWjS\n5ka23hE8iHvjy4e00WsHzovNjKiBoQ35Orx16ItbJcm+dSUNhSQcItf104yhHPwJ\nTab7PvcMQ15OxzP9lJfPu27Iuqv/9Bro1+Kpkt5lPNqffk9AHGcmX54RbHrb3yBI\nTOEYE14Nc3nbsRM0uQ3y13sCgYB5Om4QZpSWvKo9P4M+NqTKb3JglblwhOU9osVa\n39ra3dkIgCJrLQM/KTEVF9+nMLDThLG0fqKT6/9cQHuECXet6Co+d/3RE6HK7Zmr\nESWh2ckqoMM2i0uvPWT+ooJdfL2kR/bUDtAc/jyc9yUZY3ufR4Cd4/o1pAfOqR1y\nT4G1xwKBgQChE4VWawCVg2qanRjvZcdNk0zpZx4dxqqKYq/VHuSfjNLQixIZsgXT\nxx9BHuORn6c/nurqEStLwN3BzbpPU/j6YjMUmTslSH2sKhHwWNYGBZC52aJiOOda\nBz6nAkihG0n2PjYt2T84w6FWHgLJuSsmiEVJcb+AOdyKh1MlzJiwMQ==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "examples/ha/tls/certs/ca.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIICpzCCAY8CAQAwYjELMAkGA1UEBhMCQVUxETAPBgNVBAgTCFZpY3RvcmlhMRIw\nEAYDVQQHEwlNZWxib3VybmUxDjAMBgNVBAoTBW1hc3NsMQwwCgYDVQQLEwNWSUMx\nDjAMBgNVBAMTBW1hc3NsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\nuljDjKVGwlyiuKTSHc1QpoZPX9dbgwU/9113ctI8U/ZwMWLpnZ4f/zVpf4LW5foM\n9zSEUGPiyJe/NaTZUOXkRBSIQ13QroK4OJ1XGacQKpTxewCbChESZEfKWEhnP/Y7\nBYc4z1Li6Dkxh4TIElHwOVe62jbhNnzYlr4evmSuiuItAc8uhEYxncThPzmHEWPX\nKw8CFNhxCSYsjbb72UAIht0knMHQ7VXBX1VuuL0rolJBiToCva+I6CjG0c6qfi9/\nBcPsuW6cNjmQnwTg6SaSoGO/5zgbxBgy9MZQEot88d1T2XH6rBANYsfojvyCXuyt\nWnj04mvdAWwmFh0hhq+nxQIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAJFmooMt\nTocElxCb3DGJTRUXxr4DqcATASIX35a2wV3MmPqUHHXr6BQkO/FRho66EsZf3DE/\nmumou01K+KByxgsmw04CACjSeZ2t/g6pAsDCKrx/BwL3tAo09lG2Y2Ah0BND2Cta\nEZpTliU2MimZlk7UZb8VIXh2Tx56fZRoHLzO4U4+FY8ZR+tspxPRM7hLg/aUqA5D\nzGj6kByX8aYjxsmQokP4rx/w2mz6vwt4cZ1pXwr0RderkMIh9Har/0k9X1WIAP61\nPNQx74qnaq+icjtN2+8gvJE/CJL/wfcwW6kQwEtX1xsTpnzyFaRoYpSPQrvkCtiW\n+WzgnOh7RvKyAYI=\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "examples/ha/tls/certs/ca.pem",
    "content": "Certificate:\n    Data:\n        Version: 3 (0x2)\n        Serial Number:\n            7a:d7:1c:f3:22:da:b1:20:31:bf:25:16:b6:04:d5:29:1e:a3:7c:12\n        Signature Algorithm: sha256WithRSAEncryption\n        Issuer: C=AU, ST=Victoria, L=Melbourne, O=massl, OU=VIC, CN=massl\n        Validity\n            Not Before: Nov  6 22:02:17 2024 GMT\n            Not After : Nov  1 22:02:17 2044 GMT\n        Subject: C=AU, ST=Victoria, L=Melbourne, O=massl, OU=VIC, CN=massl\n        Subject Public Key Info:\n            Public Key Algorithm: rsaEncryption\n                Public-Key: (2048 bit)\n                Modulus:\n                    00:ba:58:c3:8c:a5:46:c2:5c:a2:b8:a4:d2:1d:cd:\n                    50:a6:86:4f:5f:d7:5b:83:05:3f:f7:5d:77:72:d2:\n                    3c:53:f6:70:31:62:e9:9d:9e:1f:ff:35:69:7f:82:\n                    d6:e5:fa:0c:f7:34:84:50:63:e2:c8:97:bf:35:a4:\n                    d9:50:e5:e4:44:14:88:43:5d:d0:ae:82:b8:38:9d:\n                    57:19:a7:10:2a:94:f1:7b:00:9b:0a:11:12:64:47:\n                    ca:58:48:67:3f:f6:3b:05:87:38:cf:52:e2:e8:39:\n                    31:87:84:c8:12:51:f0:39:57:ba:da:36:e1:36:7c:\n                    d8:96:be:1e:be:64:ae:8a:e2:2d:01:cf:2e:84:46:\n                    31:9d:c4:e1:3f:39:87:11:63:d7:2b:0f:02:14:d8:\n                    71:09:26:2c:8d:b6:fb:d9:40:08:86:dd:24:9c:c1:\n                    d0:ed:55:c1:5f:55:6e:b8:bd:2b:a2:52:41:89:3a:\n                    02:bd:af:88:e8:28:c6:d1:ce:aa:7e:2f:7f:05:c3:\n                    ec:b9:6e:9c:36:39:90:9f:04:e0:e9:26:92:a0:63:\n                    bf:e7:38:1b:c4:18:32:f4:c6:50:12:8b:7c:f1:dd:\n                    53:d9:71:fa:ac:10:0d:62:c7:e8:8e:fc:82:5e:ec:\n                    ad:5a:78:f4:e2:6b:dd:01:6c:26:16:1d:21:86:af:\n                    a7:c5\n                Exponent: 65537 (0x10001)\n        X509v3 extensions:\n            X509v3 Key Usage: critical\n                Certificate Sign, CRL Sign\n            X509v3 Basic Constraints: critical\n                CA:TRUE\n            X509v3 Subject Key Identifier:\n                77:80:D3:12:52:AA:EA:09:C6:60:32:59:80:9B:C2:FB:87:E5:AD:90\n    Signature Algorithm: sha256WithRSAEncryption\n    Signature Value:\n        92:f2:a4:8f:7d:04:f1:7e:08:b0:6b:3e:0c:b9:88:29:18:b6:\n        ce:88:4e:84:b0:10:8b:ca:b5:d6:6a:fb:12:52:14:f2:4e:01:\n        bb:b3:8b:a0:b4:65:d9:fd:d4:c7:6b:44:54:3a:e5:5b:c9:0e:\n        bd:3c:3b:f7:41:0a:67:1d:5a:21:32:7c:42:3b:b1:37:b4:c0:\n        78:07:4b:ae:e2:18:77:90:85:33:70:46:20:61:1a:7a:67:38:\n        0a:cf:fc:1c:bd:d2:c6:1a:0e:09:5a:d5:36:74:8a:8e:66:0f:\n        1f:47:69:7a:17:a7:d3:bf:74:40:85:3f:80:a2:53:00:2a:65:\n        3c:3f:ca:44:d9:ec:71:cf:17:4e:3d:b0:1e:5e:e8:73:ab:0a:\n        27:95:02:88:2b:b0:46:9a:4d:a4:7d:05:ba:df:4c:e5:65:d3:\n        2b:12:fd:17:74:51:f2:bb:d1:0e:32:8c:e9:ee:42:5c:d7:3c:\n        85:60:f0:1a:52:fc:11:31:e1:12:8c:c9:a0:1f:1f:52:7e:d9:\n        1e:a0:c7:f7:48:05:9d:dc:f5:c1:59:5a:9b:e7:bd:a3:37:54:\n        8a:42:c7:10:d7:51:19:99:e2:e7:d3:56:66:18:4a:d0:d1:f6:\n        25:1d:c9:f9:48:60:43:cc:6f:9c:ba:95:03:3e:a0:5a:ad:26:\n        d8:ce:4c:4a\n-----BEGIN CERTIFICATE-----\nMIIDlDCCAnygAwIBAgIUetcc8yLasSAxvyUWtgTVKR6jfBIwDQYJKoZIhvcNAQEL\nBQAwYjELMAkGA1UEBhMCQVUxETAPBgNVBAgTCFZpY3RvcmlhMRIwEAYDVQQHEwlN\nZWxib3VybmUxDjAMBgNVBAoTBW1hc3NsMQwwCgYDVQQLEwNWSUMxDjAMBgNVBAMT\nBW1hc3NsMB4XDTI0MTEwNjIyMDIxN1oXDTQ0MTEwMTIyMDIxN1owYjELMAkGA1UE\nBhMCQVUxETAPBgNVBAgTCFZpY3RvcmlhMRIwEAYDVQQHEwlNZWxib3VybmUxDjAM\nBgNVBAoTBW1hc3NsMQwwCgYDVQQLEwNWSUMxDjAMBgNVBAMTBW1hc3NsMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuljDjKVGwlyiuKTSHc1QpoZPX9db\ngwU/9113ctI8U/ZwMWLpnZ4f/zVpf4LW5foM9zSEUGPiyJe/NaTZUOXkRBSIQ13Q\nroK4OJ1XGacQKpTxewCbChESZEfKWEhnP/Y7BYc4z1Li6Dkxh4TIElHwOVe62jbh\nNnzYlr4evmSuiuItAc8uhEYxncThPzmHEWPXKw8CFNhxCSYsjbb72UAIht0knMHQ\n7VXBX1VuuL0rolJBiToCva+I6CjG0c6qfi9/BcPsuW6cNjmQnwTg6SaSoGO/5zgb\nxBgy9MZQEot88d1T2XH6rBANYsfojvyCXuytWnj04mvdAWwmFh0hhq+nxQIDAQAB\no0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU\nd4DTElKq6gnGYDJZgJvC+4flrZAwDQYJKoZIhvcNAQELBQADggEBAJLypI99BPF+\nCLBrPgy5iCkYts6IToSwEIvKtdZq+xJSFPJOAbuzi6C0Zdn91MdrRFQ65VvJDr08\nO/dBCmcdWiEyfEI7sTe0wHgHS67iGHeQhTNwRiBhGnpnOArP/By90sYaDgla1TZ0\nio5mDx9HaXoXp9O/dECFP4CiUwAqZTw/ykTZ7HHPF049sB5e6HOrCieVAogrsEaa\nTaR9BbrfTOVl0ysS/Rd0UfK70Q4yjOnuQlzXPIVg8BpS/BEx4RKMyaAfH1J+2R6g\nx/dIBZ3c9cFZWpvnvaM3VIpCxxDXURmZ4ufTVmYYStDR9iUdyflIYEPMb5y6lQM+\noFqtJtjOTEo=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "examples/ha/tls/certs/node1-csr.json",
    "content": "{\n  \"CN\": \"system:server\",\n  \"key\": {\n    \"algo\": \"rsa\",\n    \"size\": 2048\n  },\n  \"names\": [\n    {\n      \"C\": \"AU\",\n      \"L\": \"Melbourne\",\n      \"O\": \"system:node1\",\n      \"OU\": \"massl\",\n      \"ST\": \"Victoria\"\n    }\n  ]\n}\n"
  },
  {
    "path": "examples/ha/tls/certs/node1-key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA1b9bm4rvDtpYsqgtCC52+L535d4/Q2O10fWD2i2CfRXXfYJQ\n5cr4AV2iqScFsJSs7KwyQde/c4VWj/vEA2/SJHZFBlknKdCcrgHVebrvnzm6Guze\nICutZSKocFXy9Kw+YmWuA64nHVfmSCKG07GhXhEsLsSCn4PTDYOiGAUm1GdSDxUp\n8yUXec13Eb20mld0xE9kQnCnEWRnxMXtQJXoz9lpLc7DgXtN6nCXSG/CqdDPOduU\nnmseaxyAGpAFnUmxcqUuYAJUQ1hUOJhk0RVSsLTmu+FGdOxk79AxmmKQ2z9l/GuA\nVikVJGTxY4jRPezxHQ3bdqzzCIdJxTxLinftZQIDAQABAoIBADpxQtvphemau8vF\nfeKRycfDVEcOmF+VoL4SkgWSke4fjbbsbbAW6e59qp7zY3PfgtSHVIp6Mgek+oEN\nxo9mAKAlkkPlFncxadWN/M921FPF1ePMxgMnzhYr/sAQUAikG76NrKGm+VzljrpE\nbnbtR4DP0zPKWSjCQ2+bgTNuHSrPwUtEngVT6ugjfWU1RitlvjTsZ9hSuOSBlS7P\nrjbQGaEh53PraDut8PIlF4wIF+nLeERFP/a6DC8Btpbv9P50YRosag6yU/G+OYX9\nspvBPvRJGrubslKnNRz9AcjbVd3QhL+Tm7mV7iakK918jLWb95Ro4WW+9lT6IAi6\nxRSOr9UCgYEA5wI3JhKkYa4PST7ALqmJSDkPH8+tctiEx+ovmnqBufFuLWFoO/rc\nEOYslnaZM3UVCnhrFv7+LxezSI5DyQu8dBEzf0RMICvXUNBkGC7ZJQL428fjXPhX\n8mZIoJ0ol4hbamr8yTYlK0vGTwqN1bDj71w6NszuN4ecN1cKNWsMbnMCgYEA7N8Y\nMzHWNijMr7xZ1lXl4Ye9+kp3aTUjUYBBaxFr4bQ8y0fH48lzq3qOso5wgvp0DKYo\nuemD5QKbo81LKoTRLa+nxPq0UqKm9FiSWmnrcxMuph989oZ1ZFHA2B+nvbuMTF8J\n8sESclTSbgkG87DpycJOUwG3XAcXM+80pXuzJscCgYB+Dzxu/09KqoRW8PJIxGVQ\nzypMrrS07iiPO2FcyCtQf8oi43vQ91Ttt91vAisZ5HNl8k5mDyJAKovANToSVOAy\n6kwSz/9GswXdaMqmU7JVOyj4Lj0JN9AuS9ioJPrIrjVMfjORzYU8+i2uZlD94niP\n3uE5lF0OWmdJ36qHefIftwKBgQDcPQZcO19H1iGS2FbTYeSvEK5ENM7YRG8FTXIF\n4hnjrtjDzYb+tYVWEErznFrifYo/ZJMDYSqgWQ9reusDqqBvkR41mUDmgJMpJ91U\nMZ2YzmIWVbqz4QrvbtAWY0Bsuh/VtpwiWQAUy+coJj6PgJOvY3m91h+tcm5RfHz/\nzIcjawKBgA6kDcOLOnWcvhP3XwtW5dcWlNuBBNEKna+kIT/5Vlrh91y2w7F54DNK\ni0w5CZCpbTugJmZ67XLHnfongC7e2vAQ3atoT96RU4mf9614qs9LMtGAbnuCLB8+\nsT2rnaZKtzr83ensbYkbBxP/zmPBfFQ9FKcIYIA7En8zAIr2T3vJ\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "examples/ha/tls/certs/node1.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIC5TCCAc0CAQAwczELMAkGA1UEBhMCQVUxETAPBgNVBAgTCFZpY3RvcmlhMRIw\nEAYDVQQHEwlNZWxib3VybmUxFTATBgNVBAoTDHN5c3RlbTpub2RlMTEOMAwGA1UE\nCxMFbWFzc2wxFjAUBgNVBAMTDXN5c3RlbTpzZXJ2ZXIwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQDVv1ubiu8O2liyqC0ILnb4vnfl3j9DY7XR9YPaLYJ9\nFdd9glDlyvgBXaKpJwWwlKzsrDJB179zhVaP+8QDb9IkdkUGWScp0JyuAdV5uu+f\nOboa7N4gK61lIqhwVfL0rD5iZa4DricdV+ZIIobTsaFeESwuxIKfg9MNg6IYBSbU\nZ1IPFSnzJRd5zXcRvbSaV3TET2RCcKcRZGfExe1AlejP2WktzsOBe03qcJdIb8Kp\n0M8525Seax5rHIAakAWdSbFypS5gAlRDWFQ4mGTRFVKwtOa74UZ07GTv0DGaYpDb\nP2X8a4BWKRUkZPFjiNE97PEdDdt2rPMIh0nFPEuKd+1lAgMBAAGgLTArBgkqhkiG\n9w0BCQ4xHjAcMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0B\nAQsFAAOCAQEAW/tTyJaBfWtbC9hYUmhh8lxUztv2+WT4xaR/jdQ46sk/87vKuwI6\n4AkkGfiPLLqgW3xbQOwk5/ynRabttbsgTUHt744RtRFLzfcQKEBZoNPvrfHvmDil\nYqHIOx2SJ5hzIBwVlVSBn50hdSSED1Ip22DaU8GukzuacB8+2rhg3MOWJbKVt5aR\n03H4XkAynLS1FHNOraDIv1eT58D3l4hanrNOZIa0xAuChd25qLO/JHvU/3wccGUA\nKNg3vGOy2Q8qVBrTFLn+yQHuOr/wSupXESO1jiI/h+txsBQnZ6oYfZnVJ+7o3Oln\n3Hguw77aYeTAeZQPPbmJbDLegLG0ZC6RmA==\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "examples/ha/tls/certs/node1.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEAjCCAuqgAwIBAgIUbYMGwSgQF8iRZ5xmhflInj8VZ0owDQYJKoZIhvcNAQEL\nBQAwYjELMAkGA1UEBhMCQVUxETAPBgNVBAgTCFZpY3RvcmlhMRIwEAYDVQQHEwlN\nZWxib3VybmUxDjAMBgNVBAoTBW1hc3NsMQwwCgYDVQQLEwNWSUMxDjAMBgNVBAMT\nBW1hc3NsMCAXDTIxMDUwNTE2MTYwMFoYDzIxMjEwNDExMTYxNjAwWjBzMQswCQYD\nVQQGEwJBVTERMA8GA1UECBMIVmljdG9yaWExEjAQBgNVBAcTCU1lbGJvdXJuZTEV\nMBMGA1UEChMMc3lzdGVtOm5vZGUxMQ4wDAYDVQQLEwVtYXNzbDEWMBQGA1UEAxMN\nc3lzdGVtOnNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANW/\nW5uK7w7aWLKoLQgudvi+d+XeP0NjtdH1g9otgn0V132CUOXK+AFdoqknBbCUrOys\nMkHXv3OFVo/7xANv0iR2RQZZJynQnK4B1Xm67585uhrs3iArrWUiqHBV8vSsPmJl\nrgOuJx1X5kgihtOxoV4RLC7Egp+D0w2DohgFJtRnUg8VKfMlF3nNdxG9tJpXdMRP\nZEJwpxFkZ8TF7UCV6M/ZaS3Ow4F7Tepwl0hvwqnQzznblJ5rHmscgBqQBZ1JsXKl\nLmACVENYVDiYZNEVUrC05rvhRnTsZO/QMZpikNs/ZfxrgFYpFSRk8WOI0T3s8R0N\n23as8wiHScU8S4p37WUCAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0l\nBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYE\nFGprx5v+KrO4DeOtA6kps4BL/zKyMB8GA1UdIwQYMBaAFHeA0xJSquoJxmAyWYCb\nwvuH5a2QMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0BAQsF\nAAOCAQEAmWTdMLyWOrNAS0uY+u3FUV3Hm50xF1PfxbT6wK1hu6vH6B63E0o9K2/1\nU25Ie8Y2IzFocKMvbqC+mrY56G0bWoUlMONhthYqm8uTKtjlFO33A9I7WIT9Tw+B\nnnwZZO7+Ljkd30qSzBinCjrIEx31Vq2pr54ungd8+wK8nfz/zdZnJcqxcN9zvCXB\nGTE8yCuqGWKk/oDuIzVjr73U0QaWi+vThqJtBjhOIWQHHVJwbIyhuYzUaivgZPYB\n8eKXWk4JH3eAcq5z5koNGyCcZd/k4WnvxZYxNBAkoQ6AWVfEMGOCaRjD1FTnMbpG\nBW79ndJqLmn8OH+DeCnSWhTWxAgg+Q==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "examples/ha/tls/certs/node2-csr.json",
    "content": "{\n  \"CN\": \"system:server\",\n  \"key\": {\n    \"algo\": \"rsa\",\n    \"size\": 2048\n  },\n  \"names\": [\n    {\n      \"C\": \"AU\",\n      \"L\": \"Melbourne\",\n      \"O\": \"system:node2\",\n      \"OU\": \"massl\",\n      \"ST\": \"Victoria\"\n    }\n  ]\n}\n"
  },
  {
    "path": "examples/ha/tls/certs/node2-key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAtCtzT9vhRMTbhAg/pm8eBn+4IvVQeVqnHoEon9IKIx5fyvqS\nQ6Ui3xSik9kJq5FSAa1mScajJwfB1o6ycaSP6n+Q88Py4v7q65n0stCHoJCH0uPw\nMQyEhwX7nNilV9C4UZTyZ2StDdAjmMBHiN81EJAqH2d4Xtgrd/IIWhljSXm+aPbu\nQjSz8BtR/7+MswrCdlJ8y6gWi020kt6GSHjmaxI1jStGvBxxksK86v3J97wfNwWY\n7GJi70uBrvO0pk5bYckDzUTKeN1QGvBnZ8uDXs7pPysvftJr85GzX0iE9YLMDxO3\nqc/PlwCdxM8H6gHTTkLPizGZtpMF9Z497pW9YQIDAQABAoIBAFfQwdCPxHmnVbNB\n7fwqNsFGKTLozMOJeuE0ZN+ZGZXKbTha70WHTLrcrO1RIRR9rTHiGXQmHEmez0zL\nmpAnfHn4mWcm/9DCHTCehpVNbH3HVFxm+yB9EG9bbCsjsVtfASfKaGgauvp7k44V\nUgiVeqDLE6zg2tunk3BQCOAZdbpOiXrdvoZiGx2Q4SMLPfzmfIyH4BUT836pLTmp\no6/yNiFqQWfCgjeEAOQor4TcdzYIT+3wP51HfAjhZKMIvmjwL16ov1/QpmWRD4ni\n4svzYpeMYpl5OrZkKeDS4ZIQBGjxk+fzPmfFUbfVRSI2gDORsah8HoRVI4LnwKWn\n7kQDv0ECgYEA6V+KVb8bPzCZNbroEZFdug6YtT4yv5Mj3/kpMTIvA3vtu02v8e7F\nO56yT43QfUZA0Ar37O0HQ6mbpPsRE5RSr70i40RR+slMZVHX/AQViG7oQJGBijPt\n1tFdLnb+1wSON3jYt2975Kw2IfgOXprWtEmL5zGuplEUjx9Lbdf1HjkCgYEAxaNe\nXgXdAiWFoY4Qq6xBRO/WNZCdn3Ysqx6snCtDRilxeNyDoE/6x2Ma9/NRBtIiulAb\ns09vDRfJKLbzocUhIn8BQ+GkbAS/A6+x2vcuGhK3F84xqZdbrCqvqdJS8K824jug\nvUCfCBJlyNRDz8kEsN5odLM1xkij93Jv23HvGGkCgYEAptcz6ctfalSPI9eEs5KO\nREbNK73UwBssaaISreYnsED4G5EVuUuvW8k/xxomtHj2OwWsa4ilSd1GtbL8aVf/\nqT35ZCrixP0GjeTuGXC+CDTp+8dKqggoAAzbpi1SUVwjZEsT/EhKdZgcdzqE42Ol\nHWz7BQUCzEpo/U0tOtFKnxkCgYEAi05Vy8wyNbsg7/jlAzyNXPv4bxUaJTX00kDy\nxbkw2BmKI/i6xprZVwUiEzdsG3SuicjBXahVzFLBtXMPUy1R57DBwYkgjgriYMTM\nhlzIIBSk/aCXHMTVFwuXegoH8CJwexIwgHU2I0hkeiQ0EBfOuKRr2CYhdzvoZxhA\ng9tQ/lECgYAjPYoXfNI3rHCWUmaD5eDJZpE0xuJeiiy5auojykdAc7vVapNaIyMK\nG3EaU44RtXcSwH19TlH9UCm3MH1QiIwaBOzGcKj3Ut6ZyFKuWDUk4yqvps3uZU/h\nh16Tp49Ja7/4LY1uuEngg1KMEiWgk5jiU7G0H9zrtEiTj9c3FDKDvg==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "examples/ha/tls/certs/node2.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIC5TCCAc0CAQAwczELMAkGA1UEBhMCQVUxETAPBgNVBAgTCFZpY3RvcmlhMRIw\nEAYDVQQHEwlNZWxib3VybmUxFTATBgNVBAoTDHN5c3RlbTpub2RlMjEOMAwGA1UE\nCxMFbWFzc2wxFjAUBgNVBAMTDXN5c3RlbTpzZXJ2ZXIwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQC0K3NP2+FExNuECD+mbx4Gf7gi9VB5WqcegSif0goj\nHl/K+pJDpSLfFKKT2QmrkVIBrWZJxqMnB8HWjrJxpI/qf5Dzw/Li/urrmfSy0Ieg\nkIfS4/AxDISHBfuc2KVX0LhRlPJnZK0N0COYwEeI3zUQkCofZ3he2Ct38ghaGWNJ\neb5o9u5CNLPwG1H/v4yzCsJ2UnzLqBaLTbSS3oZIeOZrEjWNK0a8HHGSwrzq/cn3\nvB83BZjsYmLvS4Gu87SmTlthyQPNRMp43VAa8Gdny4Nezuk/Ky9+0mvzkbNfSIT1\ngswPE7epz8+XAJ3EzwfqAdNOQs+LMZm2kwX1nj3ulb1hAgMBAAGgLTArBgkqhkiG\n9w0BCQ4xHjAcMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0B\nAQsFAAOCAQEARh0Pi36mNmyprU4j25GWNqQYCJ6cBGnaPeiwr8/F3rsGsF4LTQdP\nxW2oBrEWyYRidNCkSMrPkcSiXu1Loy9APwSAXgJZWMYy0Ccdbd3P7dtGNOZkKaLA\nQKntGA5E1YAbzNhlt7NviGpqZ49K2aOgcGBTnDZ7xDzmg4uo3tcHgzOCwarYZT8l\nqVpc3jAyxRBOrxVKPZNFb4hAFvUm8k6/Etn5n4otN0JT3KGewbfQY50CxW5ShK52\nQCs2PmFMYHHmG11FD3W755MxzhL6UmMy20GUgWWthGmR1LugcBgDtWO/7bqqC9tT\nXYDTDJ1j0g3Y0cvy2+kltrams4lGE3xs6g==\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "examples/ha/tls/certs/node2.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEAjCCAuqgAwIBAgIUex5xEYsDJPUg8idU0Sql2ixGdTwwDQYJKoZIhvcNAQEL\nBQAwYjELMAkGA1UEBhMCQVUxETAPBgNVBAgTCFZpY3RvcmlhMRIwEAYDVQQHEwlN\nZWxib3VybmUxDjAMBgNVBAoTBW1hc3NsMQwwCgYDVQQLEwNWSUMxDjAMBgNVBAMT\nBW1hc3NsMCAXDTIxMDUwNTE2MTYwMFoYDzIxMjEwNDExMTYxNjAwWjBzMQswCQYD\nVQQGEwJBVTERMA8GA1UECBMIVmljdG9yaWExEjAQBgNVBAcTCU1lbGJvdXJuZTEV\nMBMGA1UEChMMc3lzdGVtOm5vZGUyMQ4wDAYDVQQLEwVtYXNzbDEWMBQGA1UEAxMN\nc3lzdGVtOnNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALQr\nc0/b4UTE24QIP6ZvHgZ/uCL1UHlapx6BKJ/SCiMeX8r6kkOlIt8UopPZCauRUgGt\nZknGoycHwdaOsnGkj+p/kPPD8uL+6uuZ9LLQh6CQh9Lj8DEMhIcF+5zYpVfQuFGU\n8mdkrQ3QI5jAR4jfNRCQKh9neF7YK3fyCFoZY0l5vmj27kI0s/AbUf+/jLMKwnZS\nfMuoFotNtJLehkh45msSNY0rRrwccZLCvOr9yfe8HzcFmOxiYu9Lga7ztKZOW2HJ\nA81EynjdUBrwZ2fLg17O6T8rL37Sa/ORs19IhPWCzA8Tt6nPz5cAncTPB+oB005C\nz4sxmbaTBfWePe6VvWECAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0l\nBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYE\nFDNgivphLRqKzV8n29GJq6S2I+CQMB8GA1UdIwQYMBaAFHeA0xJSquoJxmAyWYCb\nwvuH5a2QMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0BAQsF\nAAOCAQEAnNG3nzycALGf+N8PuG4sUIkD+SYA1nOEgfD2KiGNyuTYHhGgFXTw8KzB\nolH05VidldBvC0+pl5EqZAp9qdzpw6Z5Mb0gdoZY6TeKDUo022G3BHLMUGLp8y+i\nKE6+awwgdJZ6vPbdnWAh7VM/HCUrGIIPmLFan13j/2RiMfaDxdMAowPmbVc8MLgA\nJHI6pPo8D1DacEvMM09qGtwQEUoREOWJ/SzTWl1nc/IAS1yOL1LCyKLcoj/HWqjG\n3LXficQ7rf+Cpn1GnrKwMziT0OLDLxOs/+5d3nFSLxqF1lpykhPPkmHOHnuY8sMX\nQdndn9QILdp5GNvqiVNQYcQa/gOb6g==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "examples/ha/tls/tls_config_node1.yml",
    "content": "tls_server_config:\n  cert_file: \"certs/node1.pem\"\n  key_file: \"certs/node1-key.pem\"\n  client_ca_file: \"certs/ca.pem\"\n  client_auth_type: \"VerifyClientCertIfGiven\"\ntls_client_config:\n  cert_file: \"certs/node1.pem\"\n  key_file: \"certs/node1-key.pem\"\n  ca_file: \"certs/ca.pem\"\n"
  },
  {
    "path": "examples/ha/tls/tls_config_node2.yml",
    "content": "tls_server_config:\n  cert_file: \"certs/node2.pem\"\n  key_file: \"certs/node2-key.pem\"\n  client_ca_file: \"certs/ca.pem\"\n  client_auth_type: \"VerifyClientCertIfGiven\"\ntls_client_config:\n  cert_file: \"certs/node2.pem\"\n  key_file: \"certs/node2-key.pem\"\n  ca_file: \"certs/ca.pem\"\n"
  },
  {
    "path": "examples/webhook/echo.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage main\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n)\n\nfunc main() {\n\tlog.Fatal(http.ListenAndServe(\":5001\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tb, err := io.ReadAll(r.Body)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tdefer r.Body.Close()\n\t\tvar buf bytes.Buffer\n\t\tif err := json.Indent(&buf, b, \" >\", \"  \"); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tlog.Println(buf.String())\n\t})))\n}\n"
  },
  {
    "path": "examples/webhook/teams.tmpl",
    "content": "{{/*\nCopyright The Prometheus Authors\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/}}\n\n{{/*\nThis an example how to render custom templates for Microsoft Teams using the Alertmanager webhook receiver.\nReceiver is configured as:\n```yaml\n- name: local_test\n  webhook_configs:\n  - url: 'http://localhost:8080'\n    send_resolved: true\n    payload:\n      type: \"message\"\n      attachments:\n        - contentType: \"application/vnd.microsoft.card.adaptive\"\n          contentUrl: null\n          content:\n            \"$schema\": \"http://adaptivecards.io/schemas/adaptive-card.json\"\n            type: \"AdaptiveCard\"\n            body:\n              - type: \"ColumnSet\"\n                columns:\n                  - type: \"Column\"\n                    items:\n                      - type: \"Icon\"\n                        name: '{{ template \"wh_teams_icon\" . }}'\n                        size: \"xSmall\"\n                    width: \"25px\"\n                  - type: \"Column\"\n                    items:\n                      - type: \"TextBlock\"\n                        text: '{{ .CommonLabels.alertname }}'\n                        weight: \"bolder\"\n                        color: '{{ template \"wh_teams_color\" . }}'\n                    width: \"stretch\"\n              - type: \"ColumnSet\"\n                columns: '{{ template \"wh_teams_alertlist\" . }}'\n              - type: ActionSet\n                actions: '{{ template \"wh_teams_actions\" . }}'\n\n```\n*/}}\n\n\n{{- define \"wh_teams_icon\" }}\n{{- if eq .Status \"firing\" -}}AlertOn{{- else -}}Checkmark{{- end -}}\n{{- end -}}\n\n{{- define \"wh_teams_color\" }}\n{{- if eq .Status \"firing\" -}}Attention{{- else -}}Good{{- end -}}\n{{- end }}\n\n{{- define \"wh_teams_alertlist_icon_column\" }}\n{{- $length := len ( .Alerts ) }}\n[\n{{- range $index, $element := .Alerts }}\n{{- if $index -}}, {{ end -}}\n{{- if gt 5 $index }}\n  {\n    \"type\": \"Icon\",\n    \"size\": \"xSmall\",\n    \"name\": \"{{ template \"wh_teams_icon\" $element }}\"\n  }\n{{ else if eq 5 $index }}\n  {\n    \"type\": \"Icon\",\n    \"size\": \"xSmall\",\n    \"name\": \"CommentNote\"\n  }\n{{- end }}\n{{- end }}\n]\n{{- end }}\n\n\n{{- define \"wh_teams_alertlist_instance_column\" -}}\n{{- $length := len ( .Alerts ) -}}\n[\n{{- range $index, $element := .Alerts -}}\n{{- if $index -}}, {{ end -}}\n{{- if gt 5 $index -}}\n  {\n    \"type\": \"TextBlock\",\n    \"size\": \"Medium\",\n    \"text\": \"{{ .Labels.instance }}\"\n  }\n{{- else if eq 5 $index -}}\n  {\n   \"type\": \"TextBlock\",\n   \"size\": \"Medium\",\n   \"text\": \"Only 5 out of {{ $length }} displayed!\",\n  }\n{{- end -}}\n{{- end }}\n]\n{{- end }}\n\n{{ define \"wh_teams_alertlist\" }}\n[\n  {\n    \"type\": \"Column\",\n    \"width\": \"25px\",\n    \"items\": {{ template \"wh_teams_alertlist_icon_column\" . }}\n  },\n  {\n    \"type\": \"Column\",\n    \"width\": \"stretch\",\n    \"items\": {{ template \"wh_teams_alertlist_instance_column\" . }}\n  }\n]\n{{- end }}\n\n{{ define \"wh_teams_actions\" }}\n[\n  {{- if ((index .Alerts 0).Annotations).dashboard_url }}\n  {\n    \"type\": \"Action.OpenUrl\",\n    \"title\": \"Dashboard\",\n    \"url\": \"{{ ((index .Alerts 0).Annotations).dashboard_url }}\",\n    \"iconUrl\": \"icon:ArrowTrendingLines\"\n  }\n  {{- end }}\n  {{- if ((index .Alerts 0).Annotations).runbook_url }}\n  ,{\n    \"type\": \"Action.OpenUrl\",\n    \"title\": \"Runbook\",\n    \"url\": \"{{ ((index .Alerts 0).Annotations).runbook_url }}\",\n    \"iconUrl\": \"icon:PersonRunning\"\n  }\n  {{- end }}\n]\n{{ end }}\n"
  },
  {
    "path": "featurecontrol/featurecontrol.go",
    "content": "// Copyright 2023 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage featurecontrol\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n)\n\nconst (\n\tFeatureAlertNamesInMetrics   = \"alert-names-in-metrics\"\n\tFeatureReceiverNameInMetrics = \"receiver-name-in-metrics\"\n\tFeatureClassicMode           = \"classic-mode\"\n\tFeatureUTF8StrictMode        = \"utf8-strict-mode\"\n\tFeatureAutoGOMEMLIMIT        = \"auto-gomemlimit\"\n\tFeatureAutoGOMAXPROCS        = \"auto-gomaxprocs\"\n)\n\nvar AllowedFlags = []string{\n\tFeatureAlertNamesInMetrics,\n\tFeatureReceiverNameInMetrics,\n\tFeatureClassicMode,\n\tFeatureUTF8StrictMode,\n\tFeatureAutoGOMEMLIMIT,\n\tFeatureAutoGOMAXPROCS,\n}\n\ntype Flagger interface {\n\tEnableAlertNamesInMetrics() bool\n\tEnableReceiverNamesInMetrics() bool\n\tClassicMode() bool\n\tUTF8StrictMode() bool\n\tEnableAutoGOMEMLIMIT() bool\n\tEnableAutoGOMAXPROCS() bool\n}\n\ntype Flags struct {\n\tlogger                       *slog.Logger\n\tenableAlertNamesInMetrics    bool\n\tenableReceiverNamesInMetrics bool\n\tclassicMode                  bool\n\tutf8StrictMode               bool\n\tenableAutoGOMEMLIMIT         bool\n\tenableAutoGOMAXPROCS         bool\n}\n\nfunc (f *Flags) EnableAlertNamesInMetrics() bool {\n\treturn f.enableAlertNamesInMetrics\n}\n\nfunc (f *Flags) EnableReceiverNamesInMetrics() bool {\n\treturn f.enableReceiverNamesInMetrics\n}\n\nfunc (f *Flags) ClassicMode() bool {\n\treturn f.classicMode\n}\n\nfunc (f *Flags) UTF8StrictMode() bool {\n\treturn f.utf8StrictMode\n}\n\nfunc (f *Flags) EnableAutoGOMEMLIMIT() bool {\n\treturn f.enableAutoGOMEMLIMIT\n}\n\nfunc (f *Flags) EnableAutoGOMAXPROCS() bool {\n\treturn f.enableAutoGOMAXPROCS\n}\n\ntype flagOption func(flags *Flags)\n\nfunc enableReceiverNameInMetrics() flagOption {\n\treturn func(configs *Flags) {\n\t\tconfigs.enableReceiverNamesInMetrics = true\n\t}\n}\n\nfunc enableClassicMode() flagOption {\n\treturn func(configs *Flags) {\n\t\tconfigs.classicMode = true\n\t}\n}\n\nfunc enableUTF8StrictMode() flagOption {\n\treturn func(configs *Flags) {\n\t\tconfigs.utf8StrictMode = true\n\t}\n}\n\nfunc enableAutoGOMEMLIMIT() flagOption {\n\treturn func(configs *Flags) {\n\t\tconfigs.enableAutoGOMEMLIMIT = true\n\t}\n}\n\nfunc enableAutoGOMAXPROCS() flagOption {\n\treturn func(configs *Flags) {\n\t\tconfigs.enableAutoGOMAXPROCS = true\n\t}\n}\n\nfunc enableAlertNamesInMetrics() flagOption {\n\treturn func(configs *Flags) {\n\t\tconfigs.enableAlertNamesInMetrics = true\n\t}\n}\n\nfunc NewFlags(logger *slog.Logger, features string) (Flagger, error) {\n\tfc := &Flags{logger: logger}\n\topts := []flagOption{}\n\n\tif len(features) == 0 {\n\t\treturn NoopFlags{}, nil\n\t}\n\n\tfor feature := range strings.SplitSeq(features, \",\") {\n\t\tswitch feature {\n\t\tcase FeatureAlertNamesInMetrics:\n\t\t\topts = append(opts, enableAlertNamesInMetrics())\n\t\t\tlogger.Warn(\"Alert names in metrics enabled\")\n\t\tcase FeatureReceiverNameInMetrics:\n\t\t\topts = append(opts, enableReceiverNameInMetrics())\n\t\t\tlogger.Warn(\"Experimental receiver name in metrics enabled\")\n\t\tcase FeatureClassicMode:\n\t\t\topts = append(opts, enableClassicMode())\n\t\t\tlogger.Warn(\"Classic mode enabled\")\n\t\tcase FeatureUTF8StrictMode:\n\t\t\topts = append(opts, enableUTF8StrictMode())\n\t\t\tlogger.Warn(\"UTF-8 strict mode enabled\")\n\t\tcase FeatureAutoGOMEMLIMIT:\n\t\t\topts = append(opts, enableAutoGOMEMLIMIT())\n\t\t\tlogger.Warn(\"Automatically set GOMEMLIMIT to match the Linux container or system memory limit.\")\n\t\tcase FeatureAutoGOMAXPROCS:\n\t\t\topts = append(opts, enableAutoGOMAXPROCS())\n\t\t\tlogger.Error(\"Deprecated: auto-gomaxprocs will be removed in v0.33. Removing this flag does not affect behavior, as Go 1.25+ natively handles container CPU quotas.\")\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"unknown option '%s' for --enable-feature\", feature)\n\t\t}\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(fc)\n\t}\n\n\tif fc.classicMode && fc.utf8StrictMode {\n\t\treturn nil, errors.New(\"cannot have both classic and UTF-8 modes enabled\")\n\t}\n\n\treturn fc, nil\n}\n\ntype NoopFlags struct{}\n\nfunc (n NoopFlags) EnableAlertNamesInMetrics() bool { return false }\n\nfunc (n NoopFlags) EnableReceiverNamesInMetrics() bool { return false }\n\nfunc (n NoopFlags) ClassicMode() bool { return false }\n\nfunc (n NoopFlags) UTF8StrictMode() bool { return false }\n\nfunc (n NoopFlags) EnableAutoGOMEMLIMIT() bool { return false }\n\nfunc (n NoopFlags) EnableAutoGOMAXPROCS() bool { return false }\n"
  },
  {
    "path": "featurecontrol/featurecontrol_test.go",
    "content": "// Copyright 2023 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage featurecontrol\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFlags(t *testing.T) {\n\ttc := []struct {\n\t\tname         string\n\t\tfeatureFlags string\n\t\terr          error\n\t}{\n\t\t{\n\t\t\tname:         \"with only valid feature flags\",\n\t\t\tfeatureFlags: FeatureReceiverNameInMetrics,\n\t\t},\n\t\t{\n\t\t\tname:         \"with only invalid feature flags\",\n\t\t\tfeatureFlags: \"somethingsomething\",\n\t\t\terr:          errors.New(\"unknown option 'somethingsomething' for --enable-feature\"),\n\t\t},\n\t\t{\n\t\t\tname:         \"with both, valid and invalid feature flags\",\n\t\t\tfeatureFlags: strings.Join([]string{FeatureReceiverNameInMetrics, \"somethingbad\"}, \",\"),\n\t\t\terr:          errors.New(\"unknown option 'somethingbad' for --enable-feature\"),\n\t\t},\n\t}\n\n\tfor _, tt := range tc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfc, err := NewFlags(promslog.NewNopLogger(), tt.featureFlags)\n\t\t\tif tt.err != nil {\n\t\t\t\trequire.EqualError(t, err, tt.err.Error())\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, fc)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/prometheus/alertmanager\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/KimMachineGun/automemlimit v0.7.5\n\tgithub.com/alecthomas/kingpin/v2 v2.4.0\n\tgithub.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b\n\tgithub.com/aws/aws-sdk-go-v2 v1.41.3\n\tgithub.com/aws/aws-sdk-go-v2/config v1.32.11\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.19.11\n\tgithub.com/aws/aws-sdk-go-v2/service/sns v1.39.13\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.41.8\n\tgithub.com/aws/smithy-go v1.24.2\n\tgithub.com/cenkalti/backoff/v4 v4.3.0\n\tgithub.com/cespare/xxhash/v2 v2.3.0\n\tgithub.com/coder/quartz v0.3.0\n\tgithub.com/emersion/go-smtp v0.24.0\n\tgithub.com/go-openapi/analysis v0.24.3\n\tgithub.com/go-openapi/errors v0.22.7\n\tgithub.com/go-openapi/loads v0.23.3\n\tgithub.com/go-openapi/runtime v0.29.3\n\tgithub.com/go-openapi/spec v0.22.4\n\tgithub.com/go-openapi/strfmt v0.26.0\n\tgithub.com/go-openapi/swag v0.25.5\n\tgithub.com/go-openapi/validate v0.25.2\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/hashicorp/go-sockaddr v1.0.7\n\tgithub.com/hashicorp/golang-lru/v2 v2.0.7\n\tgithub.com/hashicorp/memberlist v0.5.4\n\tgithub.com/jessevdk/go-flags v1.6.1\n\tgithub.com/oklog/run v1.2.0\n\tgithub.com/oklog/ulid/v2 v2.1.1\n\tgithub.com/prometheus/client_golang v1.23.2\n\tgithub.com/prometheus/common v0.67.5\n\tgithub.com/prometheus/exporter-toolkit v0.15.1\n\tgithub.com/prometheus/sigv4 v0.4.1\n\tgithub.com/rs/cors v1.11.1\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/xlab/treeprint v1.2.0\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.66.0\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0\n\tgo.opentelemetry.io/otel v1.41.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0\n\tgo.opentelemetry.io/otel/sdk v1.41.0\n\tgo.opentelemetry.io/otel/trace v1.41.0\n\tgolang.org/x/mod v0.33.0\n\tgolang.org/x/net v0.51.0\n\tgolang.org/x/text v0.34.0\n\tgoogle.golang.org/grpc v1.79.2\n\tgoogle.golang.org/protobuf v1.36.11\n\tgopkg.in/telebot.v3 v3.3.8\n\tgopkg.in/yaml.v2 v2.4.0\n)\n\nrequire (\n\tgithub.com/armon/go-metrics v0.4.1 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/coreos/go-systemd/v22 v22.6.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.22.5 // indirect\n\tgithub.com/go-openapi/jsonreference v0.21.5 // indirect\n\tgithub.com/go-openapi/swag/cmdutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/conv v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/fileutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/jsonname v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/jsonutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/loading v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/mangling v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/netutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/stringutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/typeutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/yamlutils v0.25.5 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.5.0 // indirect\n\tgithub.com/golang-jwt/jwt/v5 v5.3.0 // indirect\n\tgithub.com/google/btree v1.1.3 // indirect\n\tgithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-immutable-radix v1.3.1 // indirect\n\tgithub.com/hashicorp/go-metrics v0.5.4 // indirect\n\tgithub.com/hashicorp/go-msgpack/v2 v2.1.5 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/golang-lru v0.5.4 // indirect\n\tgithub.com/jpillora/backoff v1.0.0 // indirect\n\tgithub.com/julienschmidt/httprouter v1.3.0 // indirect\n\tgithub.com/kylelemons/godebug v1.1.0 // indirect\n\tgithub.com/mdlayher/socket v0.4.1 // indirect\n\tgithub.com/mdlayher/vsock v1.2.1 // indirect\n\tgithub.com/miekg/dns v1.1.68 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect\n\tgithub.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/prometheus/client_model v0.6.2 // indirect\n\tgithub.com/prometheus/procfs v0.16.1 // indirect\n\tgithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect\n\tgithub.com/xhit/go-str2duration/v2 v2.1.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/otel/metric v1.41.0 // indirect\n\tgo.opentelemetry.io/proto/otlp v1.9.0 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.3 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/crypto v0.48.0 // indirect\n\tgolang.org/x/oauth2 v0.35.0 // indirect\n\tgolang.org/x/sync v0.19.0 // indirect\n\tgolang.org/x/sys v0.41.0 // indirect\n\tgolang.org/x/time v0.14.0 // indirect\n\tgolang.org/x/tools v0.41.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=\ncloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=\ncloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=\ncloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=\ncloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=\ncloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=\ncloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=\ncloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=\ncloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=\ncloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=\ncloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=\ncloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=\ncloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=\ncloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=\ncloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=\ncloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=\ncloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=\ncloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=\ncloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=\ngithub.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=\ngithub.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=\ngithub.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=\ngithub.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=\ngithub.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=\ngithub.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=\ngithub.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=\ngithub.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=\ngithub.com/aws/aws-sdk-go-v2/service/sns v1.39.13 h1:8xP94tDzFpgwIOsusGiEFHPaqrpckDojoErk/ZFZTio=\ngithub.com/aws/aws-sdk-go-v2/service/sns v1.39.13/go.mod h1:RwF6Xnba8PlINxJUQq1IAWeon6IglvqsnhNqV8QsQjk=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI=\ngithub.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=\ngithub.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=\ngithub.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=\ngithub.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/coder/quartz v0.3.0 h1:bUoSEJ77NBfKtUqv6CPSC0AS8dsjqAqqAv7bN02m1mg=\ngithub.com/coder/quartz v0.3.0/go.mod h1:BgE7DOj/8NfvRgvKw0jPLDQH/2Lya2kxcTaNJ8X0rZk=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=\ngithub.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=\ngithub.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=\ngithub.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=\ngithub.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=\ngithub.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=\ngithub.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=\ngithub.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=\ngithub.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-openapi/analysis v0.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk=\ngithub.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw=\ngithub.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA=\ngithub.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w=\ngithub.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=\ngithub.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=\ngithub.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=\ngithub.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=\ngithub.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ=\ngithub.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA=\ngithub.com/go-openapi/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y=\ngithub.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI=\ngithub.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=\ngithub.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=\ngithub.com/go-openapi/strfmt v0.26.0 h1:SDdQLyOEqu8W96rO1FRG1fuCtVyzmukky0zcD6gMGLU=\ngithub.com/go-openapi/strfmt v0.26.0/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y=\ngithub.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU=\ngithub.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA=\ngithub.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c=\ngithub.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=\ngithub.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=\ngithub.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=\ngithub.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk=\ngithub.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc=\ngithub.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=\ngithub.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=\ngithub.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=\ngithub.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=\ngithub.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=\ngithub.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=\ngithub.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=\ngithub.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=\ngithub.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw=\ngithub.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY=\ngithub.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU=\ngithub.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14=\ngithub.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=\ngithub.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=\ngithub.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=\ngithub.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=\ngithub.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=\ngithub.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=\ngithub.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ=\ngithub.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q=\ngithub.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw=\ngithub.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=\ngithub.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0=\ngithub.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY=\ngithub.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=\ngithub.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=\ngithub.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=\ngithub.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=\ngithub.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=\ngithub.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=\ngithub.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=\ngithub.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=\ngithub.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=\ngithub.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=\ngithub.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=\ngithub.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=\ngithub.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=\ngithub.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=\ngithub.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=\ngithub.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY=\ngithub.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-msgpack/v2 v2.1.5 h1:Ue879bPnutj/hXfmUk6s/jtIK90XxgiUIcXRl656T44=\ngithub.com/hashicorp/go-msgpack/v2 v2.1.5/go.mod h1:bjCsRXpZ7NsJdk45PoCQnzRGDaK8TKm5ZnDI/9y3J4M=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=\ngithub.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=\ngithub.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=\ngithub.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=\ngithub.com/hashicorp/memberlist v0.5.4 h1:40YY+3qq2tAUhZIMEK8kqusKZBBjdwJ3NUjvYkcxh74=\ngithub.com/hashicorp/memberlist v0.5.4/go.mod h1:OgN6xiIo6RlHUWk+ALjP9e32xWCoQrsOCmHrWCm2MWA=\ngithub.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=\ngithub.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=\ngithub.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=\ngithub.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=\ngithub.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=\ngithub.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=\ngithub.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=\ngithub.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=\ngithub.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ=\ngithub.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE=\ngithub.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=\ngithub.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=\ngithub.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=\ngithub.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=\ngithub.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=\ngithub.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=\ngithub.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=\ngithub.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=\ngithub.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=\ngithub.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=\ngithub.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=\ngithub.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=\ngithub.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=\ngithub.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=\ngithub.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=\ngithub.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=\ngithub.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=\ngithub.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=\ngithub.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=\ngithub.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=\ngithub.com/prometheus/exporter-toolkit v0.15.1 h1:XrGGr/qWl8Gd+pqJqTkNLww9eG8vR/CoRk0FubOKfLE=\ngithub.com/prometheus/exporter-toolkit v0.15.1/go.mod h1:P/NR9qFRGbCFgpklyhix9F6v6fFr/VQB/CVsrMDGKo4=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=\ngithub.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=\ngithub.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=\ngithub.com/prometheus/sigv4 v0.4.1 h1:EIc3j+8NBea9u1iV6O5ZAN8uvPq2xOIUPcqCTivHuXs=\ngithub.com/prometheus/sigv4 v0.4.1/go.mod h1:eu+ZbRvsc5TPiHwqh77OWuCnWK73IdkETYY46P4dXOU=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=\ngithub.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=\ngithub.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=\ngithub.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=\ngithub.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=\ngithub.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=\ngithub.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=\ngithub.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=\ngithub.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngo.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=\ngo.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU=\ngo.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.66.0 h1:U++6AfUpXXSILim4iH6Jb2oeK/mp7J4lNzzyO8Cx4Zw=\ngo.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.66.0/go.mod h1:HVNUDNMGMeykut/2GZ++AZjglCqew/+Hf4lxRVqFFxQ=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 h1:PnV4kVnw0zOmwwFkAzCN5O07fw1YOIQor120zrh0AVo=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0/go.mod h1:ofAwF4uinaf8SXdVzzbL4OsxJ3VfeEg3f/F6CeF49/Y=\ngo.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=\ngo.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 h1:ao6Oe+wSebTlQ1OEht7jlYTzQKE+pnx/iNywFvTbuuI=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0/go.mod h1:u3T6vz0gh/NVzgDgiwkgLxpsSF6PaPmo2il0apGJbls=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 h1:mq/Qcf28TWz719lE3/hMB4KkyDuLJIvgJnFGcd0kEUI=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0/go.mod h1:yk5LXEYhsL2htyDNJbEq7fWzNEigeEdV5xBF/Y+kAv0=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY=\ngo.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=\ngo.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=\ngo.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8=\ngo.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90=\ngo.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8=\ngo.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y=\ngo.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=\ngo.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=\ngo.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=\ngo.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=\ngo.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngo.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=\ngo.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=\ngolang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=\ngolang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=\ngolang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=\ngolang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=\ngolang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=\ngoogle.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=\ngoogle.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=\ngoogle.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=\ngoogle.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=\ngoogle.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=\ngoogle.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=\ngoogle.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=\ngoogle.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=\ngoogle.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=\ngoogle.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=\ngoogle.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=\ngoogle.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=\ngoogle.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=\ngoogle.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=\ngoogle.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=\ngoogle.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=\ngoogle.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=\ngoogle.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=\ngoogle.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=\ngoogle.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=\ngoogle.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=\ngoogle.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=\ngoogle.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=\ngoogle.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=\ngoogle.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=\ngoogle.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=\ngoogle.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/telebot.v3 v3.3.8 h1:uVDGjak9l824FN9YARWUHMsiNZnlohAVwUycw21k6t8=\ngopkg.in/telebot.v3 v3.3.8/go.mod h1:1mlbqcLTVSfK9dx7fdp+Nb5HZsy4LLPtpZTKmwhwtzM=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=\n"
  },
  {
    "path": "inhibit/index.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage inhibit\n\nimport (\n\t\"sync\"\n\n\t\"github.com/prometheus/common/model\"\n)\n\n// index contains map of fingerprints to fingerprints.\n// The keys are fingerprints of the equal labels of source alerts.\n// The values are fingerprints of the source alerts.\n// For more info see comments on inhibitor and InhibitRule.\ntype index struct {\n\tmtx   sync.RWMutex\n\titems map[model.Fingerprint]model.Fingerprint\n}\n\nfunc newIndex() *index {\n\treturn &index{\n\t\titems: make(map[model.Fingerprint]model.Fingerprint),\n\t}\n}\n\nfunc (c *index) Get(key model.Fingerprint) (model.Fingerprint, bool) {\n\tc.mtx.RLock()\n\tdefer c.mtx.RUnlock()\n\n\tfp, ok := c.items[key]\n\treturn fp, ok\n}\n\nfunc (c *index) Set(key, value model.Fingerprint) {\n\tc.mtx.Lock()\n\tdefer c.mtx.Unlock()\n\n\tc.items[key] = value\n}\n\nfunc (c *index) Delete(key model.Fingerprint) {\n\tc.mtx.Lock()\n\tdefer c.mtx.Unlock()\n\n\tdelete(c.items, key)\n}\n\nfunc (c *index) Len() int {\n\tc.mtx.RLock()\n\tdefer c.mtx.RUnlock()\n\n\treturn len(c.items)\n}\n"
  },
  {
    "path": "inhibit/inhibit.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage inhibit\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/oklog/run\"\n\t\"github.com/prometheus/common/model\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/codes\"\n\t\"go.opentelemetry.io/otel/propagation\"\n\t\"go.opentelemetry.io/otel/trace\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n\t\"github.com/prometheus/alertmanager/provider\"\n\t\"github.com/prometheus/alertmanager/store\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nvar tracer = otel.Tracer(\"github.com/prometheus/alertmanager/inhibit\")\n\n// An Inhibitor determines whether a given label set is muted based on the\n// currently active alerts and a set of inhibition rules. It implements the\n// Muter interface.\ntype Inhibitor struct {\n\talerts     provider.Alerts\n\trules      []*InhibitRule\n\tmarker     types.AlertMarker\n\tlogger     *slog.Logger\n\tpropagator propagation.TextMapPropagator\n\n\tmtx             sync.RWMutex\n\tloadingFinished sync.WaitGroup\n\tcancel          func()\n}\n\n// NewInhibitor returns a new Inhibitor.\nfunc NewInhibitor(ap provider.Alerts, rs []amcommoncfg.InhibitRule, mk types.AlertMarker, logger *slog.Logger) *Inhibitor {\n\tih := &Inhibitor{\n\t\talerts:     ap,\n\t\tmarker:     mk,\n\t\tlogger:     logger,\n\t\tpropagator: otel.GetTextMapPropagator(),\n\t}\n\n\tih.loadingFinished.Add(1)\n\truleNames := make(map[string]struct{})\n\tfor i, cr := range rs {\n\t\tif _, ok := ruleNames[cr.Name]; ok {\n\t\t\tih.logger.Debug(\"duplicate inhibition rule name\", \"index\", i, \"name\", cr.Name)\n\t\t}\n\n\t\tr := NewInhibitRule(cr)\n\t\tih.rules = append(ih.rules, r)\n\n\t\tif cr.Name != \"\" {\n\t\t\truleNames[cr.Name] = struct{}{}\n\t\t}\n\t}\n\treturn ih\n}\n\nfunc (ih *Inhibitor) run(ctx context.Context) {\n\tinitalAlerts, it := ih.alerts.SlurpAndSubscribe(\"inhibitor\")\n\tdefer it.Close()\n\n\tfor _, a := range initalAlerts {\n\t\tih.processAlert(ctx, a)\n\t}\n\n\tih.loadingFinished.Done()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase a := <-it.Next():\n\t\t\tif err := it.Err(); err != nil {\n\t\t\t\tih.logger.Error(\"Error iterating alerts\", \"err\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttraceCtx := context.Background()\n\t\t\tif a.Header != nil {\n\t\t\t\ttraceCtx = ih.propagator.Extract(traceCtx, propagation.MapCarrier(a.Header))\n\t\t\t}\n\t\t\tih.processAlert(traceCtx, a.Data)\n\t\t}\n\t}\n}\n\nfunc (ih *Inhibitor) processAlert(ctx context.Context, a *types.Alert) {\n\t_, span := tracer.Start(ctx, \"inhibit.Inhibitor.processAlert\",\n\t\ttrace.WithAttributes(\n\t\t\tattribute.String(\"alerting.alert.name\", a.Name()),\n\t\t\tattribute.String(\"alerting.alert.fingerprint\", a.Fingerprint().String()),\n\t\t),\n\t\ttrace.WithSpanKind(trace.SpanKindInternal),\n\t)\n\tdefer span.End()\n\n\t// Update the inhibition rules' cache.\n\tfor _, r := range ih.rules {\n\t\tif r.SourceMatchers.Matches(a.Labels) {\n\t\t\tattr := attribute.String(\"alerting.inhibit_rule.name\", r.Name)\n\t\t\tspan.AddEvent(\"alert matched rule source\", trace.WithAttributes(attr))\n\t\t\tif err := r.scache.Set(a); err != nil {\n\t\t\t\tmessage := \"error on set alert\"\n\t\t\t\tih.logger.Error(message, \"err\", err)\n\t\t\t\tspan.SetStatus(codes.Error, message)\n\t\t\t\tspan.RecordError(err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tspan.SetAttributes(attr)\n\t\t\tr.updateIndex(a)\n\t\t}\n\t}\n}\n\nfunc (ih *Inhibitor) WaitForLoading() {\n\tih.loadingFinished.Wait()\n}\n\n// Run the Inhibitor's background processing.\nfunc (ih *Inhibitor) Run() {\n\tvar (\n\t\tg   run.Group\n\t\tctx context.Context\n\t)\n\n\tih.mtx.Lock()\n\tctx, ih.cancel = context.WithCancel(context.Background())\n\tih.mtx.Unlock()\n\trunCtx, runCancel := context.WithCancel(ctx)\n\n\tfor _, rule := range ih.rules {\n\t\tgo rule.scache.Run(runCtx, 15*time.Minute)\n\t}\n\n\tg.Add(func() error {\n\t\tih.run(runCtx)\n\t\treturn nil\n\t}, func(err error) {\n\t\trunCancel()\n\t})\n\n\tif err := g.Run(); err != nil {\n\t\tih.logger.Warn(\"error running inhibitor\", \"err\", err)\n\t}\n}\n\n// Stop the Inhibitor's background processing.\nfunc (ih *Inhibitor) Stop() {\n\tif ih == nil {\n\t\treturn\n\t}\n\n\tih.mtx.RLock()\n\tdefer ih.mtx.RUnlock()\n\tif ih.cancel != nil {\n\t\tih.cancel()\n\t}\n}\n\n// Mutes returns true iff the given label set is muted. It implements the Muter\n// interface.\nfunc (ih *Inhibitor) Mutes(ctx context.Context, lset model.LabelSet) bool {\n\tfp := lset.Fingerprint()\n\n\t_, span := tracer.Start(ctx, \"inhibit.Inhibitor.Mutes\",\n\t\ttrace.WithAttributes(attribute.String(\"alerting.alert.fingerprint\", fp.String())),\n\t\ttrace.WithSpanKind(trace.SpanKindInternal),\n\t)\n\tdefer span.End()\n\n\tnow := time.Now()\n\tfor _, r := range ih.rules {\n\t\tif !r.TargetMatchers.Matches(lset) {\n\t\t\t// If target side of rule doesn't match, we don't need to look any further.\n\t\t\tcontinue\n\t\t}\n\t\tspan.AddEvent(\"alert matched rule target\",\n\t\t\ttrace.WithAttributes(\n\t\t\t\tattribute.String(\"alerting.inhibit_rule.name\", r.Name),\n\t\t\t),\n\t\t)\n\t\t// If we are here, the target side matches. If the source side matches, too, we\n\t\t// need to exclude inhibiting alerts for which the same is true.\n\t\tif inhibitedByFP, eq := r.hasEqual(lset, r.SourceMatchers.Matches(lset), now); eq {\n\t\t\tih.marker.SetInhibited(fp, inhibitedByFP.String())\n\t\t\tspan.AddEvent(\"alert inhibited\",\n\t\t\t\ttrace.WithAttributes(\n\t\t\t\t\tattribute.String(\"alerting.inhibit_rule.source.fingerprint\", inhibitedByFP.String()),\n\t\t\t\t),\n\t\t\t)\n\t\t\treturn true\n\t\t}\n\t}\n\tih.marker.SetInhibited(fp)\n\tspan.AddEvent(\"alert not inhibited\")\n\n\treturn false\n}\n\n// An InhibitRule specifies that a class of (source) alerts should inhibit\n// notifications for another class of (target) alerts if all specified matching\n// labels are equal between the two alerts. This may be used to inhibit alerts\n// from sending notifications if their meaning is logically a subset of a\n// higher-level alert.\ntype InhibitRule struct {\n\t// Name is an optional name for the inhibition rule.\n\tName string\n\t// The set of Filters which define the group of source alerts (which inhibit\n\t// the target alerts).\n\tSourceMatchers labels.Matchers\n\t// The set of Filters which define the group of target alerts (which are\n\t// inhibited by the source alerts).\n\tTargetMatchers labels.Matchers\n\t// A set of label names whose label values need to be identical in source and\n\t// target alerts in order for the inhibition to take effect.\n\tEqual map[model.LabelName]struct{}\n\n\t// Cache of alerts matching source labels.\n\tscache *store.Alerts\n\n\t// Index of fingerprints of source alert equal labels to fingerprint of source alert.\n\t// The index helps speed up source alert lookups from scache significantely in scenarios with 100s of source alerts cached.\n\t// The index items might overwrite eachother if multiple source alerts have exact equal labels.\n\t// Overwrites only happen if the new source alert has bigger EndsAt value.\n\tsindex *index\n}\n\n// NewInhibitRule returns a new InhibitRule based on a configuration definition.\nfunc NewInhibitRule(cr amcommoncfg.InhibitRule) *InhibitRule {\n\tvar (\n\t\tsourcem labels.Matchers\n\t\ttargetm labels.Matchers\n\t)\n\n\t// cr.SourceMatch will be deprecated. This for loop appends regex matchers.\n\tfor ln, lv := range cr.SourceMatch {\n\t\tmatcher, err := labels.NewMatcher(labels.MatchEqual, ln, lv)\n\t\tif err != nil {\n\t\t\t// This error must not happen because the config already validates the yaml.\n\t\t\tpanic(err)\n\t\t}\n\t\tsourcem = append(sourcem, matcher)\n\t}\n\t// cr.SourceMatchRE will be deprecated. This for loop appends regex matchers.\n\tfor ln, lv := range cr.SourceMatchRE {\n\t\tmatcher, err := labels.NewMatcher(labels.MatchRegexp, ln, lv.String())\n\t\tif err != nil {\n\t\t\t// This error must not happen because the config already validates the yaml.\n\t\t\tpanic(err)\n\t\t}\n\t\tsourcem = append(sourcem, matcher)\n\t}\n\t// We append the new-style matchers. This can be simplified once the deprecated matcher syntax is removed.\n\tsourcem = append(sourcem, cr.SourceMatchers...)\n\n\t// cr.TargetMatch will be deprecated. This for loop appends regex matchers.\n\tfor ln, lv := range cr.TargetMatch {\n\t\tmatcher, err := labels.NewMatcher(labels.MatchEqual, ln, lv)\n\t\tif err != nil {\n\t\t\t// This error must not happen because the config already validates the yaml.\n\t\t\tpanic(err)\n\t\t}\n\t\ttargetm = append(targetm, matcher)\n\t}\n\t// cr.TargetMatchRE will be deprecated. This for loop appends regex matchers.\n\tfor ln, lv := range cr.TargetMatchRE {\n\t\tmatcher, err := labels.NewMatcher(labels.MatchRegexp, ln, lv.String())\n\t\tif err != nil {\n\t\t\t// This error must not happen because the config already validates the yaml.\n\t\t\tpanic(err)\n\t\t}\n\t\ttargetm = append(targetm, matcher)\n\t}\n\t// We append the new-style matchers. This can be simplified once the deprecated matcher syntax is removed.\n\ttargetm = append(targetm, cr.TargetMatchers...)\n\n\tequal := map[model.LabelName]struct{}{}\n\tfor _, ln := range cr.Equal {\n\t\tequal[model.LabelName(ln)] = struct{}{}\n\t}\n\n\trule := &InhibitRule{\n\t\tName:           cr.Name,\n\t\tSourceMatchers: sourcem,\n\t\tTargetMatchers: targetm,\n\t\tEqual:          equal,\n\t\tscache:         store.NewAlerts(),\n\t\tsindex:         newIndex(),\n\t}\n\n\trule.scache.SetGCCallback(rule.gcCallback)\n\n\treturn rule\n}\n\n// fingerprintEquals returns the fingerprint of the equal labels of the given label set.\nfunc (r *InhibitRule) fingerprintEquals(lset model.LabelSet) model.Fingerprint {\n\tequalSet := make(model.LabelSet, len(r.Equal))\n\tfor n := range r.Equal {\n\t\tequalSet[n] = lset[n]\n\t}\n\treturn equalSet.Fingerprint()\n}\n\n// updateIndex updates the source alert index if necessary.\nfunc (r *InhibitRule) updateIndex(alert *types.Alert) {\n\tfp := alert.Fingerprint()\n\t// Calculate source labelset subset which is in equals.\n\teq := r.fingerprintEquals(alert.Labels)\n\n\t// Check if the equal labelset is already in the index.\n\tindexed, ok := r.sindex.Get(eq)\n\tif !ok {\n\t\t// If not, add it.\n\t\tr.sindex.Set(eq, fp)\n\t\treturn\n\t}\n\t// If the indexed fingerprint is the same as the new fingerprint, do nothing.\n\tif indexed == fp {\n\t\treturn\n\t}\n\n\t// New alert and existing index are not the same, compare them.\n\texisting, err := r.scache.Get(indexed)\n\tif err != nil {\n\t\t// failed to get the existing alert, overwrite the index.\n\t\tr.sindex.Set(eq, fp)\n\t\treturn\n\t}\n\n\t// If the new alert resolves after the existing alert, replace the index.\n\tif existing.ResolvedAt(alert.EndsAt) {\n\t\tr.sindex.Set(eq, fp)\n\t\treturn\n\t}\n\t// If the existing alert resolves after the new alert, do nothing.\n}\n\n// findEqualSourceAlert returns the source alert that matches the equal labels of the given label set.\nfunc (r *InhibitRule) findEqualSourceAlert(lset model.LabelSet, now time.Time) (*types.Alert, bool) {\n\tequalsFP := r.fingerprintEquals(lset)\n\tsourceFP, ok := r.sindex.Get(equalsFP)\n\tif ok {\n\t\talert, err := r.scache.Get(sourceFP)\n\t\tif err != nil {\n\t\t\treturn nil, false\n\t\t}\n\n\t\tif alert.ResolvedAt(now) {\n\t\t\treturn nil, false\n\t\t}\n\n\t\treturn alert, true\n\t}\n\n\treturn nil, false\n}\n\nfunc (r *InhibitRule) gcCallback(alerts []*types.Alert) {\n\tfor _, a := range alerts {\n\t\tfp := r.fingerprintEquals(a.Labels)\n\t\tr.sindex.Delete(fp)\n\t}\n}\n\n// hasEqual checks whether the source cache contains alerts matching the equal\n// labels for the given label set. If so, the fingerprint of one of those alerts\n// is returned. If excludeTwoSidedMatch is true, alerts that match both the\n// source and the target side of the rule are disregarded.\nfunc (r *InhibitRule) hasEqual(lset model.LabelSet, excludeTwoSidedMatch bool, now time.Time) (model.Fingerprint, bool) {\n\tequal, found := r.findEqualSourceAlert(lset, now)\n\tif found {\n\t\tif excludeTwoSidedMatch && r.TargetMatchers.Matches(equal.Labels) {\n\t\t\treturn model.Fingerprint(0), false\n\t\t}\n\t\treturn equal.Fingerprint(), found\n\t}\n\n\treturn model.Fingerprint(0), false\n}\n"
  },
  {
    "path": "inhibit/inhibit_bench_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage inhibit\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n\t\"github.com/prometheus/alertmanager/provider/mem\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// BenchmarkMutes benchmarks the Mutes method for the Muter interface\n// for different numbers of inhibition rules.\nfunc BenchmarkMutes(b *testing.B) {\n\tb.Run(\"1 inhibition rule, 1 inhibiting alert\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, allRulesMatchBenchmark(b, 1, 1))\n\t})\n\tb.Run(\"10 inhibition rules, 1 inhibiting alert\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, allRulesMatchBenchmark(b, 10, 1))\n\t})\n\tb.Run(\"100 inhibition rules, 1 inhibiting alert\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, allRulesMatchBenchmark(b, 100, 1))\n\t})\n\tb.Run(\"1000 inhibition rules, 1 inhibiting alert\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, allRulesMatchBenchmark(b, 1000, 1))\n\t})\n\tb.Run(\"10000 inhibition rules, 1 inhibiting alert\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, allRulesMatchBenchmark(b, 10000, 1))\n\t})\n\tb.Run(\"1 inhibition rule, 10 inhibiting alerts\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, allRulesMatchBenchmark(b, 1, 10))\n\t})\n\tb.Run(\"1 inhibition rule, 100 inhibiting alerts\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, allRulesMatchBenchmark(b, 1, 100))\n\t})\n\tb.Run(\"1 inhibition rule, 1000 inhibiting alerts\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, allRulesMatchBenchmark(b, 1, 1000))\n\t})\n\tb.Run(\"1 inhibition rule, 10000 inhibiting alerts\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, allRulesMatchBenchmark(b, 1, 10000))\n\t})\n\tb.Run(\"100 inhibition rules, 1000 inhibiting alerts\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, allRulesMatchBenchmark(b, 100, 1000))\n\t})\n\tb.Run(\"10 inhibition rules, last rule matches\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, lastRuleMatchesBenchmark(b, 10))\n\t})\n\tb.Run(\"100 inhibition rules, last rule matches\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, lastRuleMatchesBenchmark(b, 100))\n\t})\n\tb.Run(\"1000 inhibition rules, last rule matches\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, lastRuleMatchesBenchmark(b, 1000))\n\t})\n\tb.Run(\"10000 inhibition rules, last rule matches\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, lastRuleMatchesBenchmark(b, 10000))\n\t})\n}\n\n// benchmarkOptions allows the declaration of a wide range of benchmarks.\ntype benchmarkOptions struct {\n\t// n is the total number of inhibition rules.\n\tn int\n\t// newRuleFunc creates the next inhibition rule. It is called n times.\n\tnewRuleFunc func(idx int) amcommoncfg.InhibitRule\n\t// newAlertsFunc creates the inhibiting alerts for each inhibition rule.\n\t// It is called n times.\n\tnewAlertsFunc func(idx int, r amcommoncfg.InhibitRule) []types.Alert\n\t// benchFunc runs the benchmark.\n\tbenchFunc func(mutesFunc func(context.Context, model.LabelSet) bool) error\n}\n\n// allRulesMatchBenchmark returns a new benchmark where all inhibition rules\n// inhibit the label dst=0. It supports a number of variations, including\n// customization of the number of inhibition rules, and the number of\n// inhibiting alerts per inhibition rule.\n//\n// The source matchers are suffixed with the position of the inhibition rule\n// in the list (e.g. src=1, src=2, etc...). The target matchers are the same\n// across all inhibition rules (dst=0).\n//\n// Each inhibition rule can have zero or more alerts that match the source\n// matchers, and is determined with numInhibitingAlerts.\n//\n// It expects dst=0 to be muted and will fail if not.\nfunc allRulesMatchBenchmark(b *testing.B, numInhibitionRules, numInhibitingAlerts int) benchmarkOptions {\n\treturn benchmarkOptions{\n\t\tn: numInhibitionRules,\n\t\tnewRuleFunc: func(idx int) amcommoncfg.InhibitRule {\n\t\t\treturn amcommoncfg.InhibitRule{\n\t\t\t\tSourceMatchers: amcommoncfg.Matchers{\n\t\t\t\t\tmustNewMatcher(b, labels.MatchEqual, \"src\", strconv.Itoa(idx)),\n\t\t\t\t},\n\t\t\t\tTargetMatchers: amcommoncfg.Matchers{\n\t\t\t\t\tmustNewMatcher(b, labels.MatchEqual, \"dst\", \"0\"),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\tnewAlertsFunc: func(idx int, _ amcommoncfg.InhibitRule) []types.Alert {\n\t\t\tvar alerts []types.Alert\n\t\t\tfor i := range numInhibitingAlerts {\n\t\t\t\talerts = append(alerts, types.Alert{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"src\": model.LabelValue(strconv.Itoa(idx)),\n\t\t\t\t\t\t\t\"idx\": model.LabelValue(strconv.Itoa(i)),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn alerts\n\t\t}, benchFunc: func(mutesFunc func(context.Context, model.LabelSet) bool) error {\n\t\t\tif ok := mutesFunc(context.Background(), model.LabelSet{\"dst\": \"0\"}); !ok {\n\t\t\t\treturn errors.New(\"expected dst=0 to be muted\")\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n}\n\n// lastRuleMatchesBenchmark returns a new benchmark where the last inhibition\n// rule inhibits the label dst=0. All other inhibition rules are no-ops.\n//\n// The source matchers are suffixed with the position of the inhibition rule\n// in the list (e.g. src=1, src=2, etc...). The target matchers are the same\n// across all inhibition rules (dst=0).\n//\n// It expects dst=0 to be muted and will fail if not.\nfunc lastRuleMatchesBenchmark(b *testing.B, n int) benchmarkOptions {\n\treturn benchmarkOptions{\n\t\tn: n,\n\t\tnewRuleFunc: func(idx int) amcommoncfg.InhibitRule {\n\t\t\treturn amcommoncfg.InhibitRule{\n\t\t\t\tSourceMatchers: amcommoncfg.Matchers{\n\t\t\t\t\tmustNewMatcher(b, labels.MatchEqual, \"src\", strconv.Itoa(idx)),\n\t\t\t\t},\n\t\t\t\tTargetMatchers: amcommoncfg.Matchers{\n\t\t\t\t\tmustNewMatcher(b, labels.MatchEqual, \"dst\", \"0\"),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\tnewAlertsFunc: func(idx int, _ amcommoncfg.InhibitRule) []types.Alert {\n\t\t\t// Do not create an alert unless it is the last inhibition rule.\n\t\t\tif idx < n-1 {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn []types.Alert{{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\"src\": model.LabelValue(strconv.Itoa(idx)),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}}\n\t\t}, benchFunc: func(mutesFunc func(context.Context, model.LabelSet) bool) error {\n\t\t\tif ok := mutesFunc(context.Background(), model.LabelSet{\"dst\": \"0\"}); !ok {\n\t\t\t\treturn errors.New(\"expected dst=0 to be muted\")\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n}\n\nfunc benchmarkMutes(b *testing.B, opts benchmarkOptions) {\n\tr := prometheus.NewRegistry()\n\tm := types.NewMarker(r)\n\ts, err := mem.NewAlerts(context.TODO(), m, time.Minute, 0, nil, promslog.NewNopLogger(), r, nil)\n\tif err != nil {\n\t\tb.Fatal(err)\n\t}\n\tdefer s.Close()\n\n\talerts, rules := benchmarkFromOptions(opts)\n\tfor _, a := range alerts {\n\t\ttmp := a\n\t\tif err = s.Put(context.Background(), &tmp); err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n\n\tih := NewInhibitor(s, rules, m, promslog.NewNopLogger())\n\tdefer ih.Stop()\n\tgo ih.Run()\n\n\t// Wait some time for the inhibitor to seed its cache.\n\t<-time.After(time.Second)\n\n\tfor b.Loop() {\n\t\trequire.NoError(b, opts.benchFunc(ih.Mutes))\n\t}\n}\n\nfunc benchmarkFromOptions(opts benchmarkOptions) ([]types.Alert, []amcommoncfg.InhibitRule) {\n\tvar (\n\t\talerts = make([]types.Alert, 0, opts.n)\n\t\trules  = make([]amcommoncfg.InhibitRule, 0, opts.n)\n\t)\n\tfor i := 0; i < opts.n; i++ {\n\t\tr := opts.newRuleFunc(i)\n\t\talerts = append(alerts, opts.newAlertsFunc(i, r)...)\n\t\trules = append(rules, r)\n\t}\n\treturn alerts, rules\n}\n\nfunc mustNewMatcher(b *testing.B, op labels.MatchType, name, value string) *labels.Matcher {\n\tm, err := labels.NewMatcher(op, name, value)\n\trequire.NoError(b, err)\n\treturn m\n}\n"
  },
  {
    "path": "inhibit/inhibit_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage inhibit\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n\t\"github.com/prometheus/alertmanager/provider\"\n\t\"github.com/prometheus/alertmanager/store\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nvar nopLogger = promslog.NewNopLogger()\n\nfunc TestInhibitRuleHasEqual(t *testing.T) {\n\tt.Parallel()\n\n\tnow := time.Now()\n\tcases := []struct {\n\t\tname    string\n\t\tinitial map[model.Fingerprint]*types.Alert\n\t\tequal   model.LabelNames\n\t\tinput   model.LabelSet\n\t\tresult  bool\n\t}{\n\t\t{\n\t\t\tname:    \"no source alerts\",\n\t\t\tinitial: map[model.Fingerprint]*types.Alert{},\n\t\t\tinput:   model.LabelSet{\"a\": \"b\"},\n\t\t\tresult:  false,\n\t\t},\n\t\t{\n\t\t\tname:    \"no equal labels, any source alerts satisfies the requirement\",\n\t\t\tinitial: map[model.Fingerprint]*types.Alert{1: {}},\n\t\t\tinput:   model.LabelSet{\"a\": \"b\"},\n\t\t\tresult:  true,\n\t\t},\n\t\t{\n\t\t\tname: \"matching but already resolved\",\n\t\t\tinitial: map[model.Fingerprint]*types.Alert{\n\t\t\t\t1: {\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels:   model.LabelSet{\"a\": \"b\", \"b\": \"f\"},\n\t\t\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\t\t\tEndsAt:   now.Add(-time.Second),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t2: {\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels:   model.LabelSet{\"a\": \"b\", \"b\": \"c\"},\n\t\t\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\t\t\tEndsAt:   now.Add(-time.Second),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tequal:  model.LabelNames{\"a\", \"b\"},\n\t\t\tinput:  model.LabelSet{\"a\": \"b\", \"b\": \"c\"},\n\t\t\tresult: false,\n\t\t},\n\t\t{\n\t\t\tname: \"matching and unresolved\",\n\t\t\tinitial: map[model.Fingerprint]*types.Alert{\n\t\t\t\t1: {\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels:   model.LabelSet{\"a\": \"b\", \"c\": \"d\"},\n\t\t\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\t\t\tEndsAt:   now.Add(-time.Second),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t2: {\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels:   model.LabelSet{\"a\": \"b\", \"c\": \"f\"},\n\t\t\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\t\t\tEndsAt:   now.Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tequal:  model.LabelNames{\"a\"},\n\t\t\tinput:  model.LabelSet{\"a\": \"b\"},\n\t\t\tresult: true,\n\t\t},\n\t\t{\n\t\t\tname: \"equal label does not match\",\n\t\t\tinitial: map[model.Fingerprint]*types.Alert{\n\t\t\t\t1: {\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels:   model.LabelSet{\"a\": \"c\", \"c\": \"d\"},\n\t\t\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\t\t\tEndsAt:   now.Add(-time.Second),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t2: {\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels:   model.LabelSet{\"a\": \"c\", \"c\": \"f\"},\n\t\t\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\t\t\tEndsAt:   now.Add(-time.Second),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tequal:  model.LabelNames{\"a\"},\n\t\t\tinput:  model.LabelSet{\"a\": \"b\"},\n\t\t\tresult: false,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.name, func(t *testing.T) {\n\t\t\tr := &InhibitRule{\n\t\t\t\tEqual:  map[model.LabelName]struct{}{},\n\t\t\t\tscache: store.NewAlerts(),\n\t\t\t\tsindex: newIndex(),\n\t\t\t}\n\t\t\tfor _, ln := range c.equal {\n\t\t\t\tr.Equal[ln] = struct{}{}\n\t\t\t}\n\t\t\tfor _, v := range c.initial {\n\t\t\t\tr.scache.Set(v)\n\t\t\t\tr.updateIndex(v)\n\t\t\t}\n\n\t\t\tif _, have := r.hasEqual(c.input, false, time.Now()); have != c.result {\n\t\t\t\tt.Errorf(\"Unexpected result %t, expected %t\", have, c.result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestInhibitRuleMatches(t *testing.T) {\n\tt.Parallel()\n\n\trule1 := amcommoncfg.InhibitRule{\n\t\tSourceMatch: map[string]string{\"s1\": \"1\"},\n\t\tTargetMatch: map[string]string{\"t1\": \"1\"},\n\t\tEqual:       []string{\"e\"},\n\t}\n\trule2 := amcommoncfg.InhibitRule{\n\t\tSourceMatch: map[string]string{\"s2\": \"1\"},\n\t\tTargetMatch: map[string]string{\"t2\": \"1\"},\n\t\tEqual:       []string{\"e\"},\n\t}\n\n\tm := types.NewMarker(prometheus.NewRegistry())\n\tih := NewInhibitor(nil, []amcommoncfg.InhibitRule{rule1, rule2}, m, nopLogger)\n\tnow := time.Now()\n\t// Active alert that matches the source filter of rule1.\n\tsourceAlert1 := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels:   model.LabelSet{\"s1\": \"1\", \"t1\": \"2\", \"e\": \"1\"},\n\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\tEndsAt:   now.Add(time.Hour),\n\t\t},\n\t}\n\t// Active alert that matches the source filter _and_ the target filter of rule2.\n\tsourceAlert2 := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels:   model.LabelSet{\"s2\": \"1\", \"t2\": \"1\", \"e\": \"1\"},\n\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\tEndsAt:   now.Add(time.Hour),\n\t\t},\n\t}\n\n\tih.rules[0].scache = store.NewAlerts()\n\tih.rules[0].scache.Set(sourceAlert1)\n\tih.rules[0].sindex = newIndex()\n\tih.rules[0].updateIndex(sourceAlert1)\n\n\tih.rules[1].scache = store.NewAlerts()\n\tih.rules[1].scache.Set(sourceAlert2)\n\tih.rules[1].sindex = newIndex()\n\tih.rules[1].updateIndex(sourceAlert2)\n\n\tcases := []struct {\n\t\ttarget   model.LabelSet\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\t// Matches target filter of rule1, inhibited.\n\t\t\ttarget:   model.LabelSet{\"t1\": \"1\", \"e\": \"1\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\t// Matches target filter of rule2, inhibited.\n\t\t\ttarget:   model.LabelSet{\"t2\": \"1\", \"e\": \"1\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\t// Matches target filter of rule1 (plus noise), inhibited.\n\t\t\ttarget:   model.LabelSet{\"t1\": \"1\", \"t3\": \"1\", \"e\": \"1\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\t// Matches target filter of rule1 plus rule2, inhibited.\n\t\t\ttarget:   model.LabelSet{\"t1\": \"1\", \"t2\": \"1\", \"e\": \"1\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\t// Doesn't match target filter, not inhibited.\n\t\t\ttarget:   model.LabelSet{\"t1\": \"0\", \"e\": \"1\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\t// Matches both source and target filters of rule1,\n\t\t\t// inhibited because sourceAlert1 matches only the\n\t\t\t// source filter of rule1.\n\t\t\ttarget:   model.LabelSet{\"s1\": \"1\", \"t1\": \"1\", \"e\": \"1\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\t// Matches both source and target filters of rule2,\n\t\t\t// not inhibited because sourceAlert2 matches also both the\n\t\t\t// source and target filter of rule2.\n\t\t\ttarget:   model.LabelSet{\"s2\": \"1\", \"t2\": \"1\", \"e\": \"1\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\t// Matches target filter, equal label doesn't match, not inhibited\n\t\t\ttarget:   model.LabelSet{\"t1\": \"1\", \"e\": \"0\"},\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tif actual := ih.Mutes(context.Background(), c.target); actual != c.expected {\n\t\t\tt.Errorf(\"Expected (*Inhibitor).Mutes(%v) to return %t but got %t\", c.target, c.expected, actual)\n\t\t}\n\t}\n}\n\nfunc TestInhibitRuleMatchers(t *testing.T) {\n\tt.Parallel()\n\n\trule1 := amcommoncfg.InhibitRule{\n\t\tSourceMatchers: amcommoncfg.Matchers{&labels.Matcher{Type: labels.MatchEqual, Name: \"s1\", Value: \"1\"}},\n\t\tTargetMatchers: amcommoncfg.Matchers{&labels.Matcher{Type: labels.MatchNotEqual, Name: \"t1\", Value: \"1\"}},\n\t\tEqual:          []string{\"e\"},\n\t}\n\trule2 := amcommoncfg.InhibitRule{\n\t\tSourceMatchers: amcommoncfg.Matchers{&labels.Matcher{Type: labels.MatchEqual, Name: \"s2\", Value: \"1\"}},\n\t\tTargetMatchers: amcommoncfg.Matchers{&labels.Matcher{Type: labels.MatchEqual, Name: \"t2\", Value: \"1\"}},\n\t\tEqual:          []string{\"e\"},\n\t}\n\n\tm := types.NewMarker(prometheus.NewRegistry())\n\tih := NewInhibitor(nil, []amcommoncfg.InhibitRule{rule1, rule2}, m, nopLogger)\n\tnow := time.Now()\n\t// Active alert that matches the source filter of rule1.\n\tsourceAlert1 := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels:   model.LabelSet{\"s1\": \"1\", \"t1\": \"2\", \"e\": \"1\"},\n\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\tEndsAt:   now.Add(time.Hour),\n\t\t},\n\t}\n\t// Active alert that matches the source filter _and_ the target filter of rule2.\n\tsourceAlert2 := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels:   model.LabelSet{\"s2\": \"1\", \"t2\": \"1\", \"e\": \"1\"},\n\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\tEndsAt:   now.Add(time.Hour),\n\t\t},\n\t}\n\n\tih.rules[0].scache = store.NewAlerts()\n\tih.rules[0].scache.Set(sourceAlert1)\n\tih.rules[0].sindex = newIndex()\n\tih.rules[0].updateIndex(sourceAlert1)\n\n\tih.rules[1].scache = store.NewAlerts()\n\tih.rules[1].scache.Set(sourceAlert2)\n\tih.rules[1].sindex = newIndex()\n\tih.rules[1].updateIndex(sourceAlert2)\n\n\tcases := []struct {\n\t\ttarget   model.LabelSet\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\t// Matches target filter of rule1, inhibited.\n\t\t\ttarget:   model.LabelSet{\"t1\": \"1\", \"e\": \"1\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\t// Matches target filter of rule2, inhibited.\n\t\t\ttarget:   model.LabelSet{\"t2\": \"1\", \"e\": \"1\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\t// Matches target filter of rule1 (plus noise), inhibited.\n\t\t\ttarget:   model.LabelSet{\"t1\": \"1\", \"t3\": \"1\", \"e\": \"1\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\t// Matches target filter of rule1 plus rule2, inhibited.\n\t\t\ttarget:   model.LabelSet{\"t1\": \"1\", \"t2\": \"1\", \"e\": \"1\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\t// Doesn't match target filter, not inhibited.\n\t\t\ttarget:   model.LabelSet{\"t1\": \"0\", \"e\": \"1\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\t// Matches both source and target filters of rule1,\n\t\t\t// inhibited because sourceAlert1 matches only the\n\t\t\t// source filter of rule1.\n\t\t\ttarget:   model.LabelSet{\"s1\": \"1\", \"t1\": \"1\", \"e\": \"1\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\t// Matches both source and target filters of rule2,\n\t\t\t// not inhibited because sourceAlert2 matches also both the\n\t\t\t// source and target filter of rule2.\n\t\t\ttarget:   model.LabelSet{\"s2\": \"1\", \"t2\": \"1\", \"e\": \"1\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\t// Matches target filter, equal label doesn't match, not inhibited\n\t\t\ttarget:   model.LabelSet{\"t1\": \"1\", \"e\": \"0\"},\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tif actual := ih.Mutes(context.Background(), c.target); actual != c.expected {\n\t\t\tt.Errorf(\"Expected (*Inhibitor).Mutes(%v) to return %t but got %t\", c.target, c.expected, actual)\n\t\t}\n\t}\n}\n\nfunc TestInhibitRuleName(t *testing.T) {\n\tt.Parallel()\n\n\tconfig1 := amcommoncfg.InhibitRule{\n\t\tName: \"test-rule\",\n\t\tSourceMatchers: []*labels.Matcher{\n\t\t\t{Type: labels.MatchEqual, Name: \"severity\", Value: \"critical\"},\n\t\t},\n\t\tTargetMatchers: []*labels.Matcher{\n\t\t\t{Type: labels.MatchEqual, Name: \"severity\", Value: \"warning\"},\n\t\t},\n\t\tEqual: []string{\"instance\"},\n\t}\n\tconfig2 := amcommoncfg.InhibitRule{\n\t\tSourceMatchers: []*labels.Matcher{\n\t\t\t{Type: labels.MatchEqual, Name: \"severity\", Value: \"critical\"},\n\t\t},\n\t\tTargetMatchers: []*labels.Matcher{\n\t\t\t{Type: labels.MatchEqual, Name: \"severity\", Value: \"warning\"},\n\t\t},\n\t\tEqual: []string{\"instance\"},\n\t}\n\n\trule1 := NewInhibitRule(config1)\n\trule2 := NewInhibitRule(config2)\n\n\trequire.Equal(t, \"test-rule\", rule1.Name, \"Expected named rule to have adopt name from config\")\n\trequire.Empty(t, rule2.Name, \"Expected unnamed rule to have empty name\")\n}\n\ntype fakeAlerts struct {\n\talerts   []*types.Alert\n\tfinished chan struct{}\n}\n\nfunc newFakeAlerts(alerts []*types.Alert) *fakeAlerts {\n\treturn &fakeAlerts{\n\t\talerts:   alerts,\n\t\tfinished: make(chan struct{}),\n\t}\n}\n\nfunc (f *fakeAlerts) GetPending() provider.AlertIterator          { return nil }\nfunc (f *fakeAlerts) Get(model.Fingerprint) (*types.Alert, error) { return nil, nil }\nfunc (f *fakeAlerts) Put(context.Context, ...*types.Alert) error  { return nil }\nfunc (f *fakeAlerts) Subscribe(name string) provider.AlertIterator {\n\tch := make(chan *provider.Alert)\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tfor _, a := range f.alerts {\n\t\t\tch <- &provider.Alert{\n\t\t\t\tData:   a,\n\t\t\t\tHeader: map[string]string{},\n\t\t\t}\n\t\t}\n\t\t// Send another (meaningless) alert to make sure that the inhibitor has\n\t\t// processed everything.\n\t\tch <- &provider.Alert{\n\t\t\tData: &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tLabels:   model.LabelSet{},\n\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t},\n\t\t\t},\n\t\t\tHeader: map[string]string{},\n\t\t}\n\t\tclose(f.finished)\n\t\t<-done\n\t}()\n\treturn provider.NewAlertIterator(ch, done, nil)\n}\n\nfunc (f *fakeAlerts) SlurpAndSubscribe(name string) ([]*types.Alert, provider.AlertIterator) {\n\tch := make(chan *provider.Alert)\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tfor _, a := range f.alerts {\n\t\t\tch <- &provider.Alert{\n\t\t\t\tData:   a,\n\t\t\t\tHeader: map[string]string{},\n\t\t\t}\n\t\t}\n\t\t// Send another (meaningless) alert to make sure that the inhibitor has\n\t\t// processed everything.\n\t\tch <- &provider.Alert{\n\t\t\tData: &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tLabels:   model.LabelSet{},\n\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t},\n\t\t\t},\n\t\t\tHeader: map[string]string{},\n\t\t}\n\t\tclose(f.finished)\n\t\t<-done\n\t}()\n\treturn []*types.Alert{}, provider.NewAlertIterator(ch, done, nil)\n}\n\nfunc TestInhibit(t *testing.T) {\n\tt.Parallel()\n\n\tnow := time.Now()\n\tinhibitRule := func() amcommoncfg.InhibitRule {\n\t\treturn amcommoncfg.InhibitRule{\n\t\t\tSourceMatch: map[string]string{\"s\": \"1\"},\n\t\t\tTargetMatch: map[string]string{\"t\": \"1\"},\n\t\t\tEqual:       []string{\"e\"},\n\t\t}\n\t}\n\t// alertOne is muted by alertTwo when it is active.\n\talertOne := func() *types.Alert {\n\t\treturn &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels:   model.LabelSet{\"t\": \"1\", \"e\": \"f\"},\n\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\tEndsAt:   now.Add(time.Hour),\n\t\t\t},\n\t\t}\n\t}\n\talertTwo := func(resolved bool) *types.Alert {\n\t\tvar end time.Time\n\t\tif resolved {\n\t\t\tend = now.Add(-time.Second)\n\t\t} else {\n\t\t\tend = now.Add(time.Hour)\n\t\t}\n\t\treturn &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels:   model.LabelSet{\"s\": \"1\", \"e\": \"f\"},\n\t\t\t\tStartsAt: now.Add(-time.Minute),\n\t\t\t\tEndsAt:   end,\n\t\t\t},\n\t\t}\n\t}\n\n\ttype exp struct {\n\t\tlbls  model.LabelSet\n\t\tmuted bool\n\t}\n\tfor i, tc := range []struct {\n\t\talerts   []*types.Alert\n\t\texpected []exp\n\t}{\n\t\t{\n\t\t\t// alertOne shouldn't be muted since alertTwo hasn't fired.\n\t\t\talerts: []*types.Alert{alertOne()},\n\t\t\texpected: []exp{\n\t\t\t\t{\n\t\t\t\t\tlbls:  model.LabelSet{\"t\": \"1\", \"e\": \"f\"},\n\t\t\t\t\tmuted: false,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// alertOne should be muted by alertTwo which is active.\n\t\t\talerts: []*types.Alert{alertOne(), alertTwo(false)},\n\t\t\texpected: []exp{\n\t\t\t\t{\n\t\t\t\t\tlbls:  model.LabelSet{\"t\": \"1\", \"e\": \"f\"},\n\t\t\t\t\tmuted: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlbls:  model.LabelSet{\"s\": \"1\", \"e\": \"f\"},\n\t\t\t\t\tmuted: false,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// alertOne shouldn't be muted since alertTwo is resolved.\n\t\t\talerts: []*types.Alert{alertOne(), alertTwo(false), alertTwo(true)},\n\t\t\texpected: []exp{\n\t\t\t\t{\n\t\t\t\t\tlbls:  model.LabelSet{\"t\": \"1\", \"e\": \"f\"},\n\t\t\t\t\tmuted: false,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlbls:  model.LabelSet{\"s\": \"1\", \"e\": \"f\"},\n\t\t\t\t\tmuted: false,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t} {\n\t\tap := newFakeAlerts(tc.alerts)\n\t\tmk := types.NewMarker(prometheus.NewRegistry())\n\t\tinhibitor := NewInhibitor(ap, []amcommoncfg.InhibitRule{inhibitRule()}, mk, nopLogger)\n\n\t\tgo func() {\n\t\t\tfor ap.finished != nil {\n\t\t\t\tselect {\n\t\t\t\tcase <-ap.finished:\n\t\t\t\t\tap.finished = nil\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t}\n\t\t\tinhibitor.Stop()\n\t\t}()\n\t\tinhibitor.Run()\n\n\t\tfor _, expected := range tc.expected {\n\t\t\tif inhibitor.Mutes(context.Background(), expected.lbls) != expected.muted {\n\t\t\t\tmute := \"unmuted\"\n\t\t\t\tif expected.muted {\n\t\t\t\t\tmute = \"muted\"\n\t\t\t\t}\n\t\t\t\tt.Errorf(\"tc: %d, expected alert with labels %q to be %s\", i, expected.lbls, mute)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestInhibitRule_fingerprintEquals(t *testing.T) {\n\trule := &InhibitRule{\n\t\tEqual: map[model.LabelName]struct{}{\n\t\t\t\"cluster\": {},\n\t\t\t\"service\": {},\n\t\t},\n\t}\n\n\tlset := model.LabelSet{\n\t\t\"cluster\":  \"prod\",\n\t\t\"service\":  \"api\",\n\t\t\"instance\": \"host1\",\n\t}\n\n\tfp := rule.fingerprintEquals(lset)\n\n\t// Same equal labels should produce same fingerprint\n\tlset2 := model.LabelSet{\n\t\t\"cluster\":  \"prod\",\n\t\t\"service\":  \"api\",\n\t\t\"instance\": \"host2\", // different non-equal label\n\t}\n\trequire.Equal(t, fp, rule.fingerprintEquals(lset2))\n\n\t// Different equal label value should produce different fingerprint\n\tlset3 := model.LabelSet{\n\t\t\"cluster\": \"staging\",\n\t\t\"service\": \"api\",\n\t}\n\trequire.NotEqual(t, fp, rule.fingerprintEquals(lset3))\n}\n\nfunc BenchmarkFingerprintEquals(b *testing.B) {\n\t// Test fingerprintEquals with varying number of equal labels\n\tfor _, numLabels := range []int{1, 3, 5, 10} {\n\t\tb.Run(fmt.Sprintf(\"%d_equal_labels\", numLabels), func(b *testing.B) {\n\t\t\tequalLabels := make(map[model.LabelName]struct{}, numLabels)\n\t\t\tfor i := range numLabels {\n\t\t\t\tequalLabels[model.LabelName(fmt.Sprintf(\"label_%d\", i))] = struct{}{}\n\t\t\t}\n\n\t\t\trule := &InhibitRule{Equal: equalLabels}\n\n\t\t\t// Create a label set with matching values\n\t\t\tlset := make(model.LabelSet, numLabels+2)\n\t\t\tlset[\"source\"] = \"true\"\n\t\t\tlset[\"target\"] = \"true\"\n\t\t\tfor i := range numLabels {\n\t\t\t\tlset[model.LabelName(fmt.Sprintf(\"label_%d\", i))] = model.LabelValue(fmt.Sprintf(\"value_%d\", i))\n\t\t\t}\n\n\t\t\tb.ResetTimer()\n\t\t\tb.ReportAllocs()\n\n\t\t\tfor b.Loop() {\n\t\t\t\t_ = rule.fingerprintEquals(lset)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/tools/go.mod",
    "content": "module github.com/prometheus/prometheus/internal/tools\n\ngo 1.25.0\n\ntool (\n\tgithub.com/bufbuild/buf/cmd/buf\n\tgithub.com/go-swagger/go-swagger/cmd/swagger\n\tgoogle.golang.org/protobuf/cmd/protoc-gen-go\n)\n\nrequire (\n\tbuf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1 // indirect\n\tbuf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 // indirect\n\tbuf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 // indirect\n\tbuf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-819582968857.2 // indirect\n\tbuf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260126144947-819582968857.1 // indirect\n\tbuf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 // indirect\n\tbuf.build/go/app v0.2.0 // indirect\n\tbuf.build/go/bufplugin v0.9.0 // indirect\n\tbuf.build/go/bufprivateusage v0.1.0 // indirect\n\tbuf.build/go/interrupt v1.1.0 // indirect\n\tbuf.build/go/protovalidate v1.1.0 // indirect\n\tbuf.build/go/protoyaml v0.6.0 // indirect\n\tbuf.build/go/spdx v0.2.0 // indirect\n\tbuf.build/go/standard v0.1.0 // indirect\n\tcel.dev/expr v0.25.1 // indirect\n\tconnectrpc.com/connect v1.19.1 // indirect\n\tconnectrpc.com/otelconnect v0.9.0 // indirect\n\tdario.cat/mergo v1.0.2 // indirect\n\tgithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect\n\tgithub.com/Masterminds/goutils v1.1.1 // indirect\n\tgithub.com/Masterminds/semver/v3 v3.4.0 // indirect\n\tgithub.com/Masterminds/sprig/v3 v3.3.0 // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/antlr4-go/antlr/v4 v4.13.1 // indirect\n\tgithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect\n\tgithub.com/bufbuild/buf v1.65.0 // indirect\n\tgithub.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e // indirect\n\tgithub.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/cli/browser v1.3.0 // indirect\n\tgithub.com/containerd/errdefs v1.0.0 // indirect\n\tgithub.com/containerd/errdefs/pkg v0.3.0 // indirect\n\tgithub.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/docker/cli v29.2.0+incompatible // indirect\n\tgithub.com/docker/distribution v2.8.3+incompatible // indirect\n\tgithub.com/docker/docker v28.5.2+incompatible // indirect\n\tgithub.com/docker/docker-credential-helpers v0.9.5 // indirect\n\tgithub.com/docker/go-connections v0.6.0 // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/go-chi/chi/v5 v5.2.4 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-openapi/analysis v0.23.0 // indirect\n\tgithub.com/go-openapi/errors v0.22.2 // indirect\n\tgithub.com/go-openapi/inflect v0.21.3 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.21.2 // indirect\n\tgithub.com/go-openapi/jsonreference v0.21.0 // indirect\n\tgithub.com/go-openapi/loads v0.22.0 // indirect\n\tgithub.com/go-openapi/runtime v0.28.0 // indirect\n\tgithub.com/go-openapi/spec v0.21.0 // indirect\n\tgithub.com/go-openapi/strfmt v0.23.0 // indirect\n\tgithub.com/go-openapi/swag v0.23.1 // indirect\n\tgithub.com/go-openapi/validate v0.24.0 // indirect\n\tgithub.com/go-swagger/go-swagger v0.33.1 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.4.0 // indirect\n\tgithub.com/gofrs/flock v0.13.0 // indirect\n\tgithub.com/google/cel-go v0.27.0 // indirect\n\tgithub.com/google/go-containerregistry v0.20.7 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/gorilla/handlers v1.5.2 // indirect\n\tgithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect\n\tgithub.com/huandu/xstrings v1.5.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/jdx/go-netrc v1.0.0 // indirect\n\tgithub.com/jessevdk/go-flags v1.6.1 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/klauspost/compress v1.18.3 // indirect\n\tgithub.com/klauspost/pgzip v1.2.6 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/kr/text v0.2.0 // indirect\n\tgithub.com/mailru/easyjson v0.9.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mitchellh/copystructure v1.2.0 // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/mitchellh/reflectwalk v1.0.2 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/term v0.5.2 // indirect\n\tgithub.com/morikuni/aec v1.1.0 // indirect\n\tgithub.com/oklog/ulid v1.3.1 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/quic-go/qpack v0.6.0 // indirect\n\tgithub.com/quic-go/quic-go v0.59.0 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/rs/cors v1.11.1 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/sagikazarmark/locafero v0.10.0 // indirect\n\tgithub.com/segmentio/asm v1.2.1 // indirect\n\tgithub.com/segmentio/encoding v0.5.3 // indirect\n\tgithub.com/shopspring/decimal v1.4.0 // indirect\n\tgithub.com/sirupsen/logrus v1.9.4 // indirect\n\tgithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect\n\tgithub.com/spf13/afero v1.14.0 // indirect\n\tgithub.com/spf13/cast v1.9.2 // indirect\n\tgithub.com/spf13/cobra v1.10.2 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/spf13/viper v1.20.1 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/tetratelabs/wazero v1.11.0 // indirect\n\tgithub.com/tidwall/btree v1.8.1 // indirect\n\tgithub.com/toqueteos/webbrowser v1.2.1 // indirect\n\tgithub.com/vbatts/tar-split v0.12.2 // indirect\n\tgo.lsp.dev/jsonrpc2 v0.10.0 // indirect\n\tgo.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 // indirect\n\tgo.lsp.dev/protocol v0.12.0 // indirect\n\tgo.lsp.dev/uri v0.3.0 // indirect\n\tgo.mongodb.org/mongo-driver v1.17.4 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect\n\tgo.opentelemetry.io/otel v1.39.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.39.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.39.0 // indirect\n\tgo.uber.org/multierr v1.11.0 // indirect\n\tgo.uber.org/zap v1.27.1 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/crypto v0.47.0 // indirect\n\tgolang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect\n\tgolang.org/x/mod v0.32.0 // indirect\n\tgolang.org/x/net v0.49.0 // indirect\n\tgolang.org/x/sync v0.19.0 // indirect\n\tgolang.org/x/sys v0.40.0 // indirect\n\tgolang.org/x/term v0.39.0 // indirect\n\tgolang.org/x/text v0.34.0 // indirect\n\tgolang.org/x/tools v0.41.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect\n\tgoogle.golang.org/grpc v1.79.1 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tmvdan.cc/xurls/v2 v2.6.0 // indirect\n\tpluginrpc.com/pluginrpc v0.5.0 // indirect\n)\n"
  },
  {
    "path": "internal/tools/go.sum",
    "content": "buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1 h1:zQ9C3e6FtwSZUFuKAQfpIKGFk5ZuRoGt5g35Bix55sI=\nbuf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1/go.mod h1:1Znr6gmYBhbxWUPRrrVnSLXQsz8bvFVw1HHJq2bI3VQ=\nbuf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 h1:HwzzCRS4ZrEm1++rzSDxHnO0DOjiT1b8I/24e8a4exY=\nbuf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1/go.mod h1:8PRKXhgNes29Tjrnv8KdZzg3I1QceOkzibW1QK7EXv0=\nbuf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 h1:j9yeqTWEFrtimt8Nng2MIeRrpoCvQzM9/g25XTvqUGg=\nbuf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=\nbuf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-819582968857.2 h1:XPrWCd9ydEo5Ofv1aNJVJaxndMXLQjRO9vVzsJG3jL8=\nbuf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-819582968857.2/go.mod h1:mpsjeEaxOYPIJV2cz4IagLghZufRvx+NPVtInjEeoQ8=\nbuf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260126144947-819582968857.1 h1:Yreby6Ypa58wdQUEm9Fnc5g8n/jP487Dq3aK5yBYwfk=\nbuf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260126144947-819582968857.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40=\nbuf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 h1:iGPvEJltOXUMANWf0zajcRcbiOXLD90ZwPUFvbcuv6Q=\nbuf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1/go.mod h1:nWVKKRA29zdt4uvkjka3i/y4mkrswyWwiu0TbdX0zts=\nbuf.build/go/app v0.2.0 h1:NYaH13A+RzPb7M5vO8uZYZ2maBZI5+MS9A9tQm66fy8=\nbuf.build/go/app v0.2.0/go.mod h1:0XVOYemubVbxNXVY0DnsVgWeGkcbbAvjDa1fmhBC+Wo=\nbuf.build/go/bufplugin v0.9.0 h1:ktZJNP3If7ldcWVqh46XKeiYJVPxHQxCfjzVQDzZ/lo=\nbuf.build/go/bufplugin v0.9.0/go.mod h1:Z0CxA3sKQ6EPz/Os4kJJneeRO6CjPeidtP1ABh5jPPY=\nbuf.build/go/bufprivateusage v0.1.0 h1:SzCoCcmzS3zyXHEXHeSQhGI7OTkgtljoknLzsUz9Gg4=\nbuf.build/go/bufprivateusage v0.1.0/go.mod h1:GlCCJ3VVF7EqqU0CoRmo1FzAwwaKymEWSr+ty69xU5w=\nbuf.build/go/interrupt v1.1.0 h1:olBuhgv9Sav4/9pkSLoxgiOsZDgM5VhRhvRpn3DL0lE=\nbuf.build/go/interrupt v1.1.0/go.mod h1:ql56nXPG1oHlvZa6efNC7SKAQ/tUjS6z0mhJl0gyeRM=\nbuf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY=\nbuf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss=\nbuf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w=\nbuf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q=\nbuf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=\nbuf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8=\nbuf.build/go/standard v0.1.0 h1:g98T9IyvAl0vS3Pq8iVk6Cvj2ZiFvoUJRtfyGa0120U=\nbuf.build/go/standard v0.1.0/go.mod h1:PiqpHz/7ZFq+kqvYhc/SK3lxFIB9N/aiH2CFC2JHIQg=\ncel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=\ncel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=\nconnectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=\nconnectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=\nconnectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA=\nconnectrpc.com/otelconnect v0.9.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc=\ndario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=\ngithub.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=\ngithub.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=\ngithub.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=\ngithub.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=\ngithub.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=\ngithub.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=\ngithub.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=\ngithub.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=\ngithub.com/bufbuild/buf v1.65.0 h1:f2BzeCY9rRh9P5KD340ZoPAaFLTkssoUTHx7lpqozgg=\ngithub.com/bufbuild/buf v1.65.0/go.mod h1:7SAs2YqGpPXHqBBXBeYQbCzY0OQq4Jbg6XCqirEiYvQ=\ngithub.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e h1:emH16Bf1w4C0cJ3ge4QtBAl4sIYJe23EfpWH0SpA9co=\ngithub.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE=\ngithub.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU=\ngithub.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=\ngithub.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=\ngithub.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=\ngithub.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=\ngithub.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=\ngithub.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw=\ngithub.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=\ngithub.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=\ngithub.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=\ngithub.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=\ngithub.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=\ngithub.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=\ngithub.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=\ngithub.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=\ngithub.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=\ngithub.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=\ngithub.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=\ngithub.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=\ngithub.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrYnZg=\ngithub.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=\ngithub.com/go-openapi/inflect v0.21.3 h1:TmQvw+9eLrsNp4X0BBQacEZZtAnzk2z1FaLdQQJsDiU=\ngithub.com/go-openapi/inflect v0.21.3/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw=\ngithub.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA=\ngithub.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=\ngithub.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=\ngithub.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=\ngithub.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=\ngithub.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs=\ngithub.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ=\ngithub.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc=\ngithub.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=\ngithub.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=\ngithub.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=\ngithub.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=\ngithub.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=\ngithub.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=\ngithub.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=\ngithub.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=\ngithub.com/go-swagger/go-swagger v0.33.1 h1:BdtmxCvMxkrYIGAyn/qXPi4j85mFTwG4c9ED/27jtq4=\ngithub.com/go-swagger/go-swagger v0.33.1/go.mod h1:wJK762cSroJbM7hgJtKtbZxfevB3RSF3sC8/hK+kRwc=\ngithub.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=\ngithub.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=\ngithub.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=\ngithub.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=\ngithub.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=\ngithub.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=\ngithub.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=\ngithub.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=\ngithub.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ=\ngithub.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8=\ngithub.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=\ngithub.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=\ngithub.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88=\ngithub.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=\ngithub.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=\ngithub.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=\ngithub.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=\ngithub.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=\ngithub.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=\ngithub.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=\ngithub.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=\ngithub.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=\ngithub.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=\ngithub.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=\ngithub.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=\ngithub.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=\ngithub.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=\ngithub.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14=\ngithub.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9 h1:arwj11zP0yJIxIRiDn22E0H8PxfF7TsTrc2wIPFIsf4=\ngithub.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9/go.mod h1:SKZx6stCn03JN3BOWTwvVIO2ajMkb/zQdTceXYhKw/4=\ngithub.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=\ngithub.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=\ngithub.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=\ngithub.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8=\ngithub.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=\ngithub.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.10.0 h1:FM8Cv6j2KqIhM2ZK7HZjm4mpj9NBktLgowT1aN9q5Cc=\ngithub.com/sagikazarmark/locafero v0.10.0/go.mod h1:Ieo3EUsjifvQu4NZwV5sPd4dwvu0OCgEQV7vjc9yDjw=\ngithub.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=\ngithub.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=\ngithub.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=\ngithub.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=\ngithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=\ngithub.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=\ngithub.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=\ngithub.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=\ngithub.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=\ngithub.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=\ngithub.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=\ngithub.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA=\ngithub.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A=\ngithub.com/toqueteos/webbrowser v1.2.1 h1:O7IsnnU7XQyJ1nHMRfAktUUJOAZD3aQyUVnxzhWphCg=\ngithub.com/toqueteos/webbrowser v1.2.1/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=\ngithub.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=\ngithub.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=\ngo.lsp.dev/jsonrpc2 v0.10.0 h1:Pr/YcXJoEOTMc/b6OTmcR1DPJ3mSWl/SWiU1Cct6VmI=\ngo.lsp.dev/jsonrpc2 v0.10.0/go.mod h1:fmEzIdXPi/rf6d4uFcayi8HpFP1nBF99ERP1htC72Ac=\ngo.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 h1:hCzQgh6UcwbKgNSRurYWSqh8MufqRRPODRBblutn4TE=\ngo.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2/go.mod h1:gtSHRuYfbCT0qnbLnovpie/WEmqyJ7T4n6VXiFMBtcw=\ngo.lsp.dev/protocol v0.12.0 h1:tNprUI9klQW5FAFVM4Sa+AbPFuVQByWhP1ttNUAjIWg=\ngo.lsp.dev/protocol v0.12.0/go.mod h1:Qb11/HgZQ72qQbeyPfJbu3hZBH23s1sr4st8czGeDMQ=\ngo.lsp.dev/uri v0.3.0 h1:KcZJmh6nFIBeJzTugn5JTU6OOyG0lDOo3R9KwTxTYbo=\ngo.lsp.dev/uri v0.3.0/go.mod h1:P5sbO1IQR+qySTWOCnhnK7phBx+W3zbLqSMDJNTw88I=\ngo.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=\ngo.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=\ngo.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=\ngo.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=\ngo.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=\ngo.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=\ngo.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=\ngo.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=\ngo.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=\ngo.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=\ngo.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=\ngo.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=\ngo.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=\ngo.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=\ngolang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=\ngolang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=\ngolang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=\ngolang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=\ngolang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=\ngolang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=\ngolang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=\ngolang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=\ngolang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngolang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=\ngolang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=\ngolang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=\ngoogle.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=\ngoogle.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=\ngotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=\nmvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=\nmvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=\npluginrpc.com/pluginrpc v0.5.0 h1:tOQj2D35hOmvHyPu8e7ohW2/QvAnEtKscy2IJYWQ2yo=\npluginrpc.com/pluginrpc v0.5.0/go.mod h1:UNWZ941hcVAoOZUn8YZsMmOZBzbUjQa3XMns8RQLp9o=\n"
  },
  {
    "path": "limit/bucket.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage limit\n\nimport (\n\t\"container/heap\"\n\t\"sync\"\n\t\"time\"\n)\n\n// item represents a value and its priority based on time.\ntype item[V any] struct {\n\tvalue    V\n\tpriority time.Time\n\tindex    int\n}\n\n// expired returns true if the item is expired (priority is before the given time).\nfunc (i *item[V]) expired(at time.Time) bool {\n\treturn i.priority.Before(at)\n}\n\n// sortedItems is a heap of items.\ntype sortedItems[V any] []*item[V]\n\n// Len returns the number of items in the heap.\nfunc (s sortedItems[V]) Len() int { return len(s) }\n\n// Less reports whether the element with index i should sort before the element with index j.\nfunc (s sortedItems[V]) Less(i, j int) bool { return s[i].priority.Before(s[j].priority) }\n\n// Swap swaps the elements with indexes i and j.\nfunc (s sortedItems[V]) Swap(i, j int) {\n\ts[i], s[j] = s[j], s[i]\n\ts[i].index = i\n\ts[j].index = j\n}\n\n// Push adds an item to the heap.\nfunc (s *sortedItems[V]) Push(x any) {\n\tn := len(*s)\n\titem := x.(*item[V])\n\titem.index = n\n\t*s = append(*s, item)\n}\n\n// Pop removes and returns the minimum element (according to Less).\nfunc (s *sortedItems[V]) Pop() any {\n\told := *s\n\tn := len(old)\n\titem := old[n-1]\n\told[n-1] = nil  // don't stop the GC from reclaiming the item eventually\n\titem.index = -1 // for safety\n\t*s = old[0 : n-1]\n\treturn item\n}\n\n// update modifies the priority and value of an item in the heap.\nfunc (s *sortedItems[V]) update(item *item[V], priority time.Time) {\n\titem.priority = priority\n\theap.Fix(s, item.index)\n}\n\n// Bucket is a simple cache for values with priority(expiry).\n// It has:\n// - configurable capacity.\n// - a mutex for thread safety.\n// - a sorted heap of items for priority/expiry based eviction.\n// - an index of items for fast updates.\ntype Bucket[V comparable] struct {\n\tmtx      sync.Mutex\n\tindex    map[V]*item[V]\n\titems    sortedItems[V]\n\tcapacity int\n}\n\n// NewBucket creates a new bucket with the given capacity.\n// All internal data structures are initialized to the given capacity to avoid allocations during runtime.\nfunc NewBucket[V comparable](capacity int) *Bucket[V] {\n\titems := make(sortedItems[V], 0, capacity)\n\theap.Init(&items)\n\treturn &Bucket[V]{\n\t\tindex:    make(map[V]*item[V], capacity),\n\t\titems:    items,\n\t\tcapacity: capacity,\n\t}\n}\n\n// IsStale returns true if the latest item in the bucket is expired.\nfunc (b *Bucket[V]) IsStale() (stale bool) {\n\tb.mtx.Lock()\n\tdefer b.mtx.Unlock()\n\tif b.items.Len() == 0 {\n\t\treturn true\n\t}\n\n\tlatest := b.items[b.items.Len()-1]\n\treturn latest.expired(time.Now())\n}\n\n// Upsert tries to add a new value and its priority to the bucket.\n// If the value is already in the bucket, its priority is updated.\n// If the bucket is not full, the new value is added.\n// If the bucket is full, oldest expired item is evicted based on priority and the new value is added.\n// Otherwise the new value is ignored and the method returns false.\nfunc (b *Bucket[V]) Upsert(value V, priority time.Time) (ok bool) {\n\tif b.capacity < 1 {\n\t\treturn false\n\t}\n\n\tb.mtx.Lock()\n\tdefer b.mtx.Unlock()\n\n\t// If the value is already in the index, update it.\n\tif item, exists := b.index[value]; exists {\n\t\tb.items.update(item, priority)\n\t\treturn true\n\t}\n\n\t// If the bucket is not full, add the new value to the heap and index.\n\tif b.items.Len() < b.capacity {\n\t\titem := &item[V]{\n\t\t\tvalue:    value,\n\t\t\tpriority: priority,\n\t\t}\n\t\tb.index[value] = item\n\t\theap.Push(&b.items, item)\n\t\treturn true\n\t}\n\n\t// If the bucket is full, check the oldest item (at heap root) and evict it if expired\n\toldest := b.items[0]\n\tif oldest.expired(time.Now()) {\n\t\t// Remove the expired item from both the heap and the index\n\t\theap.Pop(&b.items)\n\t\tdelete(b.index, oldest.value)\n\n\t\t// Add the new item\n\t\titem := &item[V]{\n\t\t\tvalue:    value,\n\t\t\tpriority: priority,\n\t\t}\n\t\tb.index[value] = item\n\t\theap.Push(&b.items, item)\n\t\treturn true\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "limit/bucket_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage limit\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestBucketUpsert(t *testing.T) {\n\ttestCases := []struct {\n\t\tname           string\n\t\tbucketCapacity int\n\t\talerts         []model.Alert\n\t\talertTimings   []time.Time // When each alert is added relative to now\n\t\texpectedResult []bool      // Expected return value for each Add() call\n\t\tdescription    string\n\t}{\n\t\t{\n\t\t\tname:           \"Bucket with zero capacity should reject all alerts\",\n\t\t\tbucketCapacity: 0,\n\t\t\talerts: []model.Alert{\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert1\", \"instance\": \"server1\"}, EndsAt: time.Now().Add(1 * time.Hour)},\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert2\", \"instance\": \"server2\"}, EndsAt: time.Now().Add(1 * time.Hour)},\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert3\", \"instance\": \"server3\"}, EndsAt: time.Now().Add(1 * time.Hour)},\n\t\t\t},\n\t\t\talertTimings:   []time.Time{time.Now(), time.Now(), time.Now()},\n\t\t\texpectedResult: []bool{false, false, false}, // All should be rejected\n\t\t\tdescription:    \"Adding 3 alerts to a bucket with capacity 0 should fail\",\n\t\t},\n\t\t{\n\t\t\tname:           \"Empty bucket should add items while not full\",\n\t\t\tbucketCapacity: 3,\n\t\t\talerts: []model.Alert{\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert1\", \"instance\": \"server1\"}, EndsAt: time.Now().Add(1 * time.Hour)},\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert2\", \"instance\": \"server2\"}, EndsAt: time.Now().Add(1 * time.Hour)},\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert3\", \"instance\": \"server3\"}, EndsAt: time.Now().Add(1 * time.Hour)},\n\t\t\t},\n\t\t\talertTimings:   []time.Time{time.Now(), time.Now(), time.Now()},\n\t\t\texpectedResult: []bool{true, true, true}, // All should be added successfully\n\t\t\tdescription:    \"Adding 3 alerts to a bucket with capacity 3 should succeed\",\n\t\t},\n\t\t{\n\t\t\tname:           \"Full bucket must not add items if old items are not expired yet\",\n\t\t\tbucketCapacity: 2,\n\t\t\talerts: []model.Alert{\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert1\", \"instance\": \"server1\"}, EndsAt: time.Now().Add(1 * time.Hour)},\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert2\", \"instance\": \"server2\"}, EndsAt: time.Now().Add(1 * time.Hour)},\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert3\", \"instance\": \"server3\"}, EndsAt: time.Now().Add(1 * time.Hour)},\n\t\t\t},\n\t\t\talertTimings:   []time.Time{time.Now(), time.Now(), time.Now()},\n\t\t\texpectedResult: []bool{true, true, false}, // First two succeed, third fails\n\t\t\tdescription:    \"Adding third alert to full bucket with non-expired items should fail\",\n\t\t},\n\t\t{\n\t\t\tname:           \"Full bucket must add items if old items are expired\",\n\t\t\tbucketCapacity: 2,\n\t\t\talerts: []model.Alert{\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert1\", \"instance\": \"server1\"}, EndsAt: time.Now().Add(-1 * time.Hour)},    // Expired 1 hour ago\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert2\", \"instance\": \"server2\"}, EndsAt: time.Now().Add(-30 * time.Minute)}, // Expired 30 minutes ago\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert3\", \"instance\": \"server3\"}, EndsAt: time.Now().Add(1 * time.Hour)},     // Will expire in 1 hour\n\t\t\t},\n\t\t\talertTimings:   []time.Time{time.Now(), time.Now(), time.Now()},\n\t\t\texpectedResult: []bool{true, true, true}, // All should succeed because older items get evicted\n\t\t\tdescription:    \"Adding new alerts when bucket is full but oldest items are expired should succeed\",\n\t\t},\n\t\t{\n\t\t\tname:           \"Update existing alert in bucket should not increase size\",\n\t\t\tbucketCapacity: 2,\n\t\t\talerts: []model.Alert{\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert1\", \"instance\": \"server1\"}, EndsAt: time.Now().Add(1 * time.Hour)},\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert1\", \"instance\": \"server1\"}, EndsAt: time.Now().Add(2 * time.Hour)}, // Same fingerprint, different EndsAt\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert2\", \"instance\": \"server2\"}, EndsAt: time.Now().Add(1 * time.Hour)},\n\t\t\t},\n\t\t\talertTimings:   []time.Time{time.Now(), time.Now(), time.Now()},\n\t\t\texpectedResult: []bool{true, true, true}, // All should succeed - second is an update, not a new entry\n\t\t\tdescription:    \"Updating existing alert should not consume additional bucket space\",\n\t\t},\n\t\t{\n\t\t\tname:           \"Mixed scenario with expiration and updates\",\n\t\t\tbucketCapacity: 2,\n\t\t\talerts: []model.Alert{\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert1\", \"instance\": \"server1\"}, EndsAt: time.Now().Add(-1 * time.Hour)}, // Expired\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert2\", \"instance\": \"server2\"}, EndsAt: time.Now().Add(1 * time.Hour)},  // Active\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert1\", \"instance\": \"server1\"}, EndsAt: time.Now().Add(2 * time.Hour)},  // Update of first alert\n\t\t\t\t{Labels: model.LabelSet{\"alertname\": \"Alert3\", \"instance\": \"server3\"}, EndsAt: time.Now().Add(1 * time.Hour)},  // New alert, bucket full but Alert2 not expired\n\t\t\t},\n\t\t\talertTimings:   []time.Time{time.Now(), time.Now(), time.Now(), time.Now()},\n\t\t\texpectedResult: []bool{true, true, true, false}, // Last one should fail because bucket is full with non-expired items\n\t\t\tdescription:    \"Complex scenario with expiration, updates, and eviction should work correctly\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tbucket := NewBucket[model.Fingerprint](tc.bucketCapacity)\n\n\t\t\tfor i, alert := range tc.alerts {\n\t\t\t\tresult := bucket.Upsert(alert.Fingerprint(), alert.EndsAt)\n\t\t\t\trequire.Equal(t, tc.expectedResult[i], result,\n\t\t\t\t\t\"Alert %d: expected %v, got %v. %s\", i+1, tc.expectedResult[i], result, tc.description)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBucketAddConcurrency(t *testing.T) {\n\tbucket := NewBucket[model.Fingerprint](2)\n\n\t// Test that concurrent access to bucket is safe\n\talert1 := model.Alert{Labels: model.LabelSet{\"alertname\": \"Alert1\", \"instance\": \"server1\"}, EndsAt: time.Now().Add(1 * time.Hour)}\n\talert2 := model.Alert{Labels: model.LabelSet{\"alertname\": \"Alert2\", \"instance\": \"server2\"}, EndsAt: time.Now().Add(1 * time.Hour)}\n\n\tdone := make(chan bool, 2)\n\n\t// Add alerts concurrently\n\tgo func() {\n\t\tbucket.Upsert(alert1.Fingerprint(), alert1.EndsAt)\n\t\tdone <- true\n\t}()\n\n\tgo func() {\n\t\tbucket.Upsert(alert2.Fingerprint(), alert2.EndsAt)\n\t\tdone <- true\n\t}()\n\n\t// Wait for both goroutines to complete\n\t<-done\n\t<-done\n\n\t// Verify that both alerts were added (bucket should contain 2 items)\n\trequire.Len(t, bucket.index, 2, \"Expected 2 alerts in bucket after concurrent adds\")\n\trequire.Len(t, bucket.items, 2, \"Expected 2 items in bucket map after concurrent adds\")\n}\n\nfunc TestBucketAddExpiredEviction(t *testing.T) {\n\tbucket := NewBucket[model.Fingerprint](2)\n\n\t// Add two alerts that are already expired\n\texpiredAlert1 := model.Alert{\n\t\tLabels: model.LabelSet{\"alertname\": \"ExpiredAlert1\", \"instance\": \"server1\"},\n\t\tEndsAt: time.Now().Add(-1 * time.Hour),\n\t}\n\texpiredFingerprint1 := expiredAlert1.Fingerprint()\n\texpiredAlert2 := model.Alert{\n\t\tLabels: model.LabelSet{\"alertname\": \"ExpiredAlert2\", \"instance\": \"server2\"},\n\t\tEndsAt: time.Now().Add(-30 * time.Minute),\n\t}\n\texpiredFingerprint2 := expiredAlert2.Fingerprint()\n\n\t// Fill the bucket with expired alerts\n\tresult1 := bucket.Upsert(expiredFingerprint1, expiredAlert1.EndsAt)\n\trequire.True(t, result1, \"First expired alert should be added successfully\")\n\n\tresult2 := bucket.Upsert(expiredFingerprint2, expiredAlert2.EndsAt)\n\trequire.True(t, result2, \"Second expired alert should be added successfully\")\n\n\t// Now add a fresh alert - it should evict the first expired alert\n\tfreshAlert := model.Alert{\n\t\tLabels: model.LabelSet{\"alertname\": \"FreshAlert\", \"instance\": \"server3\"},\n\t\tEndsAt: time.Now().Add(1 * time.Hour),\n\t}\n\tfreshFingerprint := freshAlert.Fingerprint()\n\n\tresult3 := bucket.Upsert(freshFingerprint, freshAlert.EndsAt)\n\trequire.True(t, result3, \"Fresh alert should be added successfully, evicting expired alert\")\n\n\t// Verify the bucket state\n\trequire.Len(t, bucket.index, 2, \"Bucket should still contain 2 items after eviction\")\n\trequire.Len(t, bucket.items, 2, \"Bucket map should still contain 2 items after eviction\")\n\n\t// The fresh alert should be in the bucket\n\t_, exists := bucket.index[freshFingerprint]\n\trequire.True(t, exists, \"Fresh alert should exist in bucket after eviction\")\n\n\t// The first expired alert should have been evicted\n\t_, exists = bucket.index[expiredFingerprint1]\n\trequire.False(t, exists, \"First expired alert should have been evicted from bucket, fingerprint: %d\", expiredFingerprint1)\n}\n\nfunc TestBucketAddEdgeCases(t *testing.T) {\n\tt.Run(\"Single capacity bucket with replacement\", func(t *testing.T) {\n\t\tbucket := NewBucket[model.Fingerprint](1)\n\n\t\t// Add expired alert\n\t\texpiredAlert := model.Alert{Labels: model.LabelSet{\"alertname\": \"Expired\"}, EndsAt: time.Now().Add(-1 * time.Hour)}\n\t\tresult1 := bucket.Upsert(expiredAlert.Fingerprint(), expiredAlert.EndsAt)\n\t\trequire.True(t, result1, \"Adding expired alert to single-capacity bucket should succeed\")\n\n\t\t// Add fresh alert (should replace expired one)\n\t\tfreshAlert := model.Alert{Labels: model.LabelSet{\"alertname\": \"Fresh\"}, EndsAt: time.Now().Add(1 * time.Hour)}\n\t\tresult2 := bucket.Upsert(freshAlert.Fingerprint(), freshAlert.EndsAt)\n\t\trequire.True(t, result2, \"Adding fresh alert should succeed by replacing expired one\")\n\n\t\t// Verify only the fresh alert remains\n\t\trequire.Len(t, bucket.index, 1, \"Bucket should contain exactly 1 item\")\n\t\tfreshFingerprint := freshAlert.Fingerprint()\n\t\t_, exists := bucket.index[freshFingerprint]\n\t\trequire.True(t, exists, \"Fresh alert should exist in bucket\")\n\t})\n\n\tt.Run(\"Alert with same fingerprint but different EndsAt\", func(t *testing.T) {\n\t\tbucket := NewBucket[model.Fingerprint](2)\n\n\t\t// Add initial alert\n\t\toriginalTime := time.Now().Add(1 * time.Hour)\n\t\talert1 := model.Alert{Labels: model.LabelSet{\"alertname\": \"Test\"}, EndsAt: originalTime}\n\t\tresult1 := bucket.Upsert(alert1.Fingerprint(), alert1.EndsAt)\n\t\trequire.True(t, result1, \"Initial alert should be added successfully\")\n\n\t\t// Add same alert with different EndsAt (should update, not add new)\n\t\tupdatedTime := time.Now().Add(2 * time.Hour)\n\t\talert2 := model.Alert{Labels: model.LabelSet{\"alertname\": \"Test\"}, EndsAt: updatedTime}\n\t\tresult2 := bucket.Upsert(alert2.Fingerprint(), alert2.EndsAt)\n\t\trequire.True(t, result2, \"Updated alert should not fill bucket\")\n\n\t\t// Verify bucket still has only one entry with updated time\n\t\trequire.Len(t, bucket.index, 1, \"Bucket should contain exactly 1 item after update\")\n\t\tfingerprint := alert1.Fingerprint()\n\t\tstoredTime, exists := bucket.index[fingerprint]\n\t\trequire.True(t, exists, \"Alert should exist in bucket\")\n\t\trequire.Equal(t, updatedTime, storedTime.priority, \"Alert should have updated EndsAt time\")\n\t})\n}\n\n// Benchmark tests for Bucket.Upsert() performance.\nfunc BenchmarkBucketUpsert(b *testing.B) {\n\tb.Run(\"EmptyBucket\", func(b *testing.B) {\n\t\tbucket := NewBucket[model.Fingerprint](1000)\n\t\talert := model.Alert{\n\t\t\tLabels: model.LabelSet{\"alertname\": \"TestAlert\", \"instance\": \"server1\"},\n\t\t\tEndsAt: time.Now().Add(1 * time.Hour),\n\t\t}\n\n\t\tb.ResetTimer()\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tbucket.Upsert(alert.Fingerprint(), alert.EndsAt)\n\t\t}\n\t})\n\n\tb.Run(\"AddToFullBucketWithExpiredItems\", func(b *testing.B) {\n\t\tbucketSize := 100\n\t\tbucket := NewBucket[model.Fingerprint](bucketSize)\n\n\t\t// Fill bucket with expired alerts\n\t\tfor i := range bucketSize {\n\t\t\texpiredAlert := model.Alert{\n\t\t\t\tLabels: model.LabelSet{\"alertname\": model.LabelValue(\"ExpiredAlert\" + string(rune(i))), \"instance\": \"server1\"},\n\t\t\t\tEndsAt: time.Now().Add(-1 * time.Hour), // Expired 1 hour ago\n\t\t\t}\n\t\t\tbucket.Upsert(expiredAlert.Fingerprint(), expiredAlert.EndsAt)\n\t\t}\n\n\t\tnewAlert := model.Alert{\n\t\t\tLabels: model.LabelSet{\"alertname\": \"NewAlert\", \"instance\": \"server2\"},\n\t\t\tEndsAt: time.Now().Add(1 * time.Hour),\n\t\t}\n\n\t\tb.ResetTimer()\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tbucket.Upsert(newAlert.Fingerprint(), newAlert.EndsAt)\n\t\t}\n\t})\n\n\tb.Run(\"AddToFullBucketWithActiveItems\", func(b *testing.B) {\n\t\tbucketSize := 100\n\t\tbucket := NewBucket[model.Fingerprint](bucketSize)\n\n\t\t// Fill bucket with active alerts\n\t\tfor i := range bucketSize {\n\t\t\tactiveAlert := model.Alert{\n\t\t\t\tLabels: model.LabelSet{\"alertname\": model.LabelValue(\"ActiveAlert\" + string(rune(i))), \"instance\": \"server1\"},\n\t\t\t\tEndsAt: time.Now().Add(1 * time.Hour), // Active for 1 hour\n\t\t\t}\n\t\t\tbucket.Upsert(activeAlert.Fingerprint(), activeAlert.EndsAt)\n\t\t}\n\n\t\tnewAlert := model.Alert{\n\t\t\tLabels: model.LabelSet{\"alertname\": \"NewAlert\", \"instance\": \"server2\"},\n\t\t\tEndsAt: time.Now().Add(1 * time.Hour),\n\t\t}\n\n\t\tb.ResetTimer()\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tbucket.Upsert(newAlert.Fingerprint(), newAlert.EndsAt)\n\t\t}\n\t})\n\n\tb.Run(\"UpdateExistingItem\", func(b *testing.B) {\n\t\tbucket := NewBucket[model.Fingerprint](100)\n\n\t\t// Add initial alert\n\t\talert := model.Alert{\n\t\t\tLabels: model.LabelSet{\"alertname\": \"TestAlert\", \"instance\": \"server1\"},\n\t\t\tEndsAt: time.Now().Add(1 * time.Hour),\n\t\t}\n\t\tbucket.Upsert(alert.Fingerprint(), alert.EndsAt)\n\n\t\t// Create update with same fingerprint but different EndsAt\n\t\tupdatedAlert := model.Alert{\n\t\t\tLabels: model.LabelSet{\"alertname\": \"TestAlert\", \"instance\": \"server1\"},\n\t\t\tEndsAt: time.Now().Add(2 * time.Hour),\n\t\t}\n\n\t\tb.ResetTimer()\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tbucket.Upsert(updatedAlert.Fingerprint(), updatedAlert.EndsAt)\n\t\t}\n\t})\n\n\tb.Run(\"MixedWorkload\", func(b *testing.B) {\n\t\tbucketSize := 50\n\t\tbucket := NewBucket[model.Fingerprint](bucketSize)\n\n\t\t// Pre-populate with mix of expired and active alerts\n\t\tfor i := 0; i < bucketSize/2; i++ {\n\t\t\texpiredAlert := model.Alert{\n\t\t\t\tLabels: model.LabelSet{\"alertname\": model.LabelValue(\"ExpiredAlert\" + string(rune(i))), \"instance\": \"server1\"},\n\t\t\t\tEndsAt: time.Now().Add(-1 * time.Hour),\n\t\t\t}\n\t\t\tbucket.Upsert(expiredAlert.Fingerprint(), expiredAlert.EndsAt)\n\t\t}\n\t\tfor i := 0; i < bucketSize/2; i++ {\n\t\t\tactiveAlert := model.Alert{\n\t\t\t\tLabels: model.LabelSet{\"alertname\": model.LabelValue(\"ActiveAlert\" + string(rune(i))), \"instance\": \"server1\"},\n\t\t\t\tEndsAt: time.Now().Add(1 * time.Hour),\n\t\t\t}\n\t\t\tbucket.Upsert(activeAlert.Fingerprint(), activeAlert.EndsAt)\n\t\t}\n\n\t\t// Create different types of alerts for the benchmark\n\t\talerts := []*model.Alert{\n\t\t\t{Labels: model.LabelSet{\"alertname\": \"NewAlert1\", \"instance\": \"server2\"}, EndsAt: time.Now().Add(1 * time.Hour)},\n\t\t\t{Labels: model.LabelSet{\"alertname\": \"ExpiredAlert0\", \"instance\": \"server1\"}, EndsAt: time.Now().Add(2 * time.Hour)}, // Update existing\n\t\t\t{Labels: model.LabelSet{\"alertname\": \"NewAlert2\", \"instance\": \"server3\"}, EndsAt: time.Now().Add(1 * time.Hour)},\n\t\t}\n\n\t\tb.ResetTimer()\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\talertIndex := i % len(alerts)\n\t\t\tbucket.Upsert(alerts[alertIndex].Fingerprint(), alerts[alertIndex].EndsAt)\n\t\t}\n\t})\n}\n\n// Benchmark different bucket sizes to understand scaling behavior.\nfunc BenchmarkBucketUpsertScaling(b *testing.B) {\n\tsizes := []int{10, 50, 100, 500, 1000}\n\n\tfor _, size := range sizes {\n\t\tb.Run(fmt.Sprintf(\"BucketSize_%d\", size), func(b *testing.B) {\n\t\t\tbucket := NewBucket[model.Fingerprint](size)\n\n\t\t\t// Fill bucket to capacity with expired items\n\t\t\tfor i := range size {\n\t\t\t\talert := model.Alert{\n\t\t\t\t\tLabels: model.LabelSet{\"alertname\": model.LabelValue(fmt.Sprintf(\"Alert%d\", i)), \"instance\": \"server1\"},\n\t\t\t\t\tEndsAt: time.Now().Add(-1 * time.Hour),\n\t\t\t\t}\n\t\t\t\tbucket.Upsert(alert.Fingerprint(), alert.EndsAt)\n\t\t\t}\n\n\t\t\tnewAlert := model.Alert{\n\t\t\t\tLabels: model.LabelSet{\"alertname\": \"NewAlert\", \"instance\": \"server2\"},\n\t\t\t\tEndsAt: time.Now().Add(1 * time.Hour),\n\t\t\t}\n\n\t\t\tb.ResetTimer()\n\t\t\tfor range b.N {\n\t\t\t\tbucket.Upsert(newAlert.Fingerprint(), newAlert.EndsAt)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBucketIsStale(t *testing.T) {\n\tt.Run(\"IsStale on empty bucket should return true\", func(t *testing.T) {\n\t\tbucket := NewBucket[model.Fingerprint](5)\n\n\t\t// Should not panic when bucket is empty and return true\n\t\trequire.NotPanics(t, func() {\n\t\t\tstale := bucket.IsStale()\n\t\t\trequire.True(t, stale, \"IsStale on empty bucket should return true\")\n\t\t}, \"IsStale on empty bucket should not panic\")\n\t})\n\n\tt.Run(\"IsStale returns true when latest item is expired\", func(t *testing.T) {\n\t\tbucket := NewBucket[model.Fingerprint](3)\n\n\t\t// Add three alerts that are all expired\n\t\texpiredTime := time.Now().Add(-1 * time.Hour)\n\t\talert1 := model.Alert{Labels: model.LabelSet{\"alertname\": \"Alert1\"}, EndsAt: expiredTime}\n\t\talert2 := model.Alert{Labels: model.LabelSet{\"alertname\": \"Alert2\"}, EndsAt: expiredTime.Add(-10 * time.Minute)}\n\t\talert3 := model.Alert{Labels: model.LabelSet{\"alertname\": \"Alert3\"}, EndsAt: expiredTime.Add(-20 * time.Minute)}\n\n\t\tbucket.Upsert(alert1.Fingerprint(), alert1.EndsAt)\n\t\tbucket.Upsert(alert2.Fingerprint(), alert2.EndsAt)\n\t\tbucket.Upsert(alert3.Fingerprint(), alert3.EndsAt)\n\n\t\trequire.Len(t, bucket.items, 3, \"Bucket should have 3 items before IsStale check\")\n\t\trequire.Len(t, bucket.index, 3, \"Bucket index should have 3 items before IsStale check\")\n\n\t\t// IsStale should return true when all items are expired\n\t\tstale := bucket.IsStale()\n\n\t\trequire.True(t, stale, \"IsStale should return true when all items are expired\")\n\t\t// IsStale doesn't remove items, so bucket should still contain them\n\t\trequire.Len(t, bucket.items, 3, \"Bucket should still have 3 items after IsStale check\")\n\t\trequire.Len(t, bucket.index, 3, \"Bucket index should still have 3 items after IsStale check\")\n\t})\n\n\tt.Run(\"IsStale returns false when latest item is not expired\", func(t *testing.T) {\n\t\tbucket := NewBucket[model.Fingerprint](3)\n\n\t\t// Add mix of expired and non-expired alerts\n\t\texpiredTime := time.Now().Add(-1 * time.Hour)\n\t\tfutureTime := time.Now().Add(1 * time.Hour)\n\n\t\talert1 := model.Alert{Labels: model.LabelSet{\"alertname\": \"Expired1\"}, EndsAt: expiredTime}\n\t\talert2 := model.Alert{Labels: model.LabelSet{\"alertname\": \"Expired2\"}, EndsAt: expiredTime.Add(-10 * time.Minute)}\n\t\talert3 := model.Alert{Labels: model.LabelSet{\"alertname\": \"Active\"}, EndsAt: futureTime}\n\n\t\tbucket.Upsert(alert1.Fingerprint(), alert1.EndsAt)\n\t\tbucket.Upsert(alert2.Fingerprint(), alert2.EndsAt)\n\t\tbucket.Upsert(alert3.Fingerprint(), alert3.EndsAt)\n\n\t\trequire.Len(t, bucket.items, 3, \"Bucket should have 3 items before IsStale check\")\n\n\t\t// IsStale should return false since the latest item (alert3) is not expired\n\t\tstale := bucket.IsStale()\n\n\t\trequire.False(t, stale, \"IsStale should return false when latest item is not expired\")\n\t\trequire.Len(t, bucket.items, 3, \"Bucket should still have 3 items after IsStale check\")\n\t\trequire.Len(t, bucket.index, 3, \"Bucket index should still have 3 items after IsStale check\")\n\t})\n}\n\n// Benchmark concurrent access to Bucket.Upsert().\nfunc BenchmarkBucketUpsertConcurrent(b *testing.B) {\n\tbucket := NewBucket[model.Fingerprint](100)\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\talertCounter := 0\n\t\tfor pb.Next() {\n\t\t\talert := model.Alert{\n\t\t\t\tLabels: model.LabelSet{\"alertname\": model.LabelValue(\"Alert\" + string(rune(alertCounter))), \"instance\": \"server1\"},\n\t\t\t\tEndsAt: time.Now().Add(1 * time.Hour),\n\t\t\t}\n\t\t\tbucket.Upsert(alert.Fingerprint(), alert.EndsAt)\n\t\t\talertCounter++\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "matcher/compat/parse.go",
    "content": "// Copyright 2023 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage compat\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"reflect\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\n\t\"github.com/prometheus/alertmanager/featurecontrol\"\n\t\"github.com/prometheus/alertmanager/matcher/parse\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n)\n\nvar (\n\tisValidLabelName = isValidClassicLabelName(promslog.NewNopLogger())\n\tparseMatcher     = ClassicMatcherParser(promslog.NewNopLogger())\n\tparseMatchers    = ClassicMatchersParser(promslog.NewNopLogger())\n)\n\n// IsValidLabelName returns true if the string is a valid label name.\nfunc IsValidLabelName(name model.LabelName) bool {\n\treturn isValidLabelName(name)\n}\n\ntype ParseMatcher func(input, origin string) (*labels.Matcher, error)\n\ntype ParseMatchers func(input, origin string) (labels.Matchers, error)\n\n// Matcher parses the matcher in the input string. It returns an error\n// if the input is invalid or contains two or more matchers.\nfunc Matcher(input, origin string) (*labels.Matcher, error) {\n\treturn parseMatcher(input, origin)\n}\n\n// Matchers parses one or more matchers in the input string. It returns\n// an error if the input is invalid.\nfunc Matchers(input, origin string) (labels.Matchers, error) {\n\treturn parseMatchers(input, origin)\n}\n\n// InitFromFlags initializes the compat package from the flagger.\nfunc InitFromFlags(l *slog.Logger, f featurecontrol.Flagger) {\n\tif f.ClassicMode() {\n\t\tisValidLabelName = isValidClassicLabelName(l)\n\t\tparseMatcher = ClassicMatcherParser(l)\n\t\tparseMatchers = ClassicMatchersParser(l)\n\t} else if f.UTF8StrictMode() {\n\t\tisValidLabelName = isValidUTF8LabelName(l)\n\t\tparseMatcher = UTF8MatcherParser(l)\n\t\tparseMatchers = UTF8MatchersParser(l)\n\t} else {\n\t\tisValidLabelName = isValidUTF8LabelName(l)\n\t\tparseMatcher = FallbackMatcherParser(l)\n\t\tparseMatchers = FallbackMatchersParser(l)\n\t}\n}\n\n// ClassicMatcherParser uses the pkg/labels parser to parse the matcher in\n// the input string.\nfunc ClassicMatcherParser(l *slog.Logger) ParseMatcher {\n\treturn func(input, origin string) (matcher *labels.Matcher, err error) {\n\t\tl.Debug(\"Parsing with classic matchers parser\", \"input\", input, \"origin\", origin)\n\t\treturn labels.ParseMatcher(input)\n\t}\n}\n\n// ClassicMatchersParser uses the pkg/labels parser to parse zero or more\n// matchers in the input string. It returns an error if the input is invalid.\nfunc ClassicMatchersParser(l *slog.Logger) ParseMatchers {\n\treturn func(input, origin string) (matchers labels.Matchers, err error) {\n\t\tl.Debug(\"Parsing with classic matchers parser\", \"input\", input, \"origin\", origin)\n\t\treturn labels.ParseMatchers(input)\n\t}\n}\n\n// UTF8MatcherParser uses the new matcher/parse parser to parse the matcher\n// in the input string. If this fails it does not revert to the pkg/labels parser.\nfunc UTF8MatcherParser(l *slog.Logger) ParseMatcher {\n\treturn func(input, origin string) (matcher *labels.Matcher, err error) {\n\t\tl.Debug(\"Parsing with UTF-8 matchers parser\", \"input\", input, \"origin\", origin)\n\t\tif strings.HasPrefix(input, \"{\") || strings.HasSuffix(input, \"}\") {\n\t\t\treturn nil, fmt.Errorf(\"unexpected open or close brace: %s\", input)\n\t\t}\n\t\treturn parse.Matcher(input)\n\t}\n}\n\n// UTF8MatchersParser uses the new matcher/parse parser to parse zero or more\n// matchers in the input string. If this fails it does not revert to the\n// pkg/labels parser.\nfunc UTF8MatchersParser(l *slog.Logger) ParseMatchers {\n\treturn func(input, origin string) (matchers labels.Matchers, err error) {\n\t\tl.Debug(\"Parsing with UTF-8 matchers parser\", \"input\", input, \"origin\", origin)\n\t\treturn parse.Matchers(input)\n\t}\n}\n\n// FallbackMatcherParser uses the new matcher/parse parser to parse zero or more\n// matchers in the string. If this fails it reverts to the pkg/labels parser and\n// emits a warning log line.\nfunc FallbackMatcherParser(l *slog.Logger) ParseMatcher {\n\treturn func(input, origin string) (matcher *labels.Matcher, err error) {\n\t\tl.Debug(\"Parsing with UTF-8 matchers parser, with fallback to classic matchers parser\", \"input\", input, \"origin\", origin)\n\t\tif strings.HasPrefix(input, \"{\") || strings.HasSuffix(input, \"}\") {\n\t\t\treturn nil, fmt.Errorf(\"unexpected open or close brace: %s\", input)\n\t\t}\n\t\t// Parse the input in both parsers to look for disagreement and incompatible\n\t\t// inputs.\n\t\tnMatcher, nErr := parse.Matcher(input)\n\t\tcMatcher, cErr := labels.ParseMatcher(input)\n\t\tif nErr != nil {\n\t\t\t// If the input is invalid in both parsers, return the error.\n\t\t\tif cErr != nil {\n\t\t\t\treturn nil, cErr\n\t\t\t}\n\t\t\t// The input is valid in the pkg/labels parser, but not the matcher/parse\n\t\t\t// parser. This means the input is not forwards compatible.\n\t\t\tsuggestion := cMatcher.String()\n\t\t\tl.Warn(\"Alertmanager is moving to a new parser for labels and matchers, and this input is incompatible. Alertmanager has instead parsed the input using the classic matchers parser as a fallback. To make this input compatible with the UTF-8 matchers parser please make sure all regular expressions and values are double-quoted and backslashes are escaped. If you are still seeing this message please open an issue.\", \"input\", input, \"origin\", origin, \"err\", nErr, \"suggestion\", suggestion)\n\t\t\treturn cMatcher, nil\n\t\t}\n\t\t// If the input is valid in both parsers, but produces different results,\n\t\t// then there is disagreement.\n\t\tif cErr == nil && !reflect.DeepEqual(nMatcher, cMatcher) {\n\t\t\tl.Warn(\"Matchers input has disagreement\", \"input\", input, \"origin\", origin)\n\t\t\treturn cMatcher, nil\n\t\t}\n\t\treturn nMatcher, nil\n\t}\n}\n\n// FallbackMatchersParser uses the new matcher/parse parser to parse the\n// matcher in the input string. If this fails it falls back to the pkg/labels\n// parser and emits a warning log line.\nfunc FallbackMatchersParser(l *slog.Logger) ParseMatchers {\n\treturn func(input, origin string) (matchers labels.Matchers, err error) {\n\t\tl.Debug(\"Parsing with UTF-8 matchers parser, with fallback to classic matchers parser\", \"input\", input, \"origin\", origin)\n\t\t// Parse the input in both parsers to look for disagreement and incompatible\n\t\t// inputs.\n\t\tnMatchers, nErr := parse.Matchers(input)\n\t\tcMatchers, cErr := labels.ParseMatchers(input)\n\t\tif nErr != nil {\n\t\t\t// If the input is invalid in both parsers, return the error.\n\t\t\tif cErr != nil {\n\t\t\t\treturn nil, cErr\n\t\t\t}\n\t\t\t// The input is valid in the pkg/labels parser, but not the matcher/parse\n\t\t\t// parser. This means the input is not forwards compatible.\n\t\t\tvar sb strings.Builder\n\t\t\tfor i, n := range cMatchers {\n\t\t\t\tsb.WriteString(n.String())\n\t\t\t\tif i < len(cMatchers)-1 {\n\t\t\t\t\tsb.WriteRune(',')\n\t\t\t\t}\n\t\t\t}\n\t\t\tsuggestion := sb.String()\n\t\t\t// The input is valid in the pkg/labels parser, but not the\n\t\t\t// new matcher/parse parser.\n\t\t\tl.Warn(\"Alertmanager is moving to a new parser for labels and matchers, and this input is incompatible. Alertmanager has instead parsed the input using the classic matchers parser as a fallback. To make this input compatible with the UTF-8 matchers parser please make sure all regular expressions and values are double-quoted and backslashes are escaped. If you are still seeing this message please open an issue.\", \"input\", input, \"origin\", origin, \"err\", nErr, \"suggestion\", suggestion)\n\t\t\treturn cMatchers, nil\n\t\t}\n\t\t// If the input is valid in both parsers, but produces different results,\n\t\t// then there is disagreement. We need to compare to labels.Matchers(cMatchers)\n\t\t// as cMatchers is a []*labels.Matcher not labels.Matchers.\n\t\tif cErr == nil && !reflect.DeepEqual(nMatchers, labels.Matchers(cMatchers)) {\n\t\t\tl.Warn(\"Matchers input has disagreement\", \"input\", input, \"origin\", origin)\n\t\t\treturn cMatchers, nil\n\t\t}\n\t\treturn nMatchers, nil\n\t}\n}\n\n// isValidClassicLabelName returns true if the string is a valid classic label name.\nfunc isValidClassicLabelName(_ *slog.Logger) func(model.LabelName) bool {\n\treturn func(name model.LabelName) bool {\n\t\treturn model.LegacyValidation.IsValidLabelName(string(name))\n\t}\n}\n\n// isValidUTF8LabelName returns true if the string is a valid UTF-8 label name.\nfunc isValidUTF8LabelName(_ *slog.Logger) func(model.LabelName) bool {\n\treturn func(name model.LabelName) bool {\n\t\tif len(name) == 0 {\n\t\t\treturn false\n\t\t}\n\t\treturn utf8.ValidString(string(name))\n\t}\n}\n"
  },
  {
    "path": "matcher/compat/parse_test.go",
    "content": "// Copyright 2023 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage compat\n\nimport (\n\t\"testing\"\n\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n)\n\nfunc TestFallbackMatcherParser(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected *labels.Matcher\n\t\terr      string\n\t}{{\n\t\tname:     \"input is accepted\",\n\t\tinput:    \"foo=bar\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\"),\n\t}, {\n\t\tname:  \"input is accepted in neither\",\n\t\tinput: \"foo!bar\",\n\t\terr:   \"bad matcher format: foo!bar\",\n\t}, {\n\t\tname:     \"input is accepted in matchers/parse but not pkg/labels\",\n\t\tinput:    \"foo🙂=bar\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo🙂\", \"bar\"),\n\t}, {\n\t\tname:     \"input is accepted in pkg/labels but not matchers/parse\",\n\t\tinput:    \"foo=!bar\\\\n\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"!bar\\n\"),\n\t}, {\n\t\t// This input causes disagreement because \\xf0\\x9f\\x99\\x82 is the byte sequence for 🙂,\n\t\t// which is not understood by pkg/labels but is understood by matchers/parse. In such cases,\n\t\t// the fallback parser returns the result from pkg/labels.\n\t\tname:     \"input causes disagreement\",\n\t\tinput:    \"foo=\\\"\\\\xf0\\\\x9f\\\\x99\\\\x82\\\"\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"\\\\xf0\\\\x9f\\\\x99\\\\x82\"),\n\t}}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tf := FallbackMatcherParser(promslog.NewNopLogger())\n\t\t\tmatcher, err := f(test.input, \"test\")\n\t\t\tif test.err != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, test.expected, matcher)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFallbackMatchersParser(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected labels.Matchers\n\t\terr      string\n\t}{{\n\t\tname:  \"input is accepted\",\n\t\tinput: \"{foo=bar,bar=baz}\",\n\t\texpected: labels.Matchers{\n\t\t\tmustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\"),\n\t\t\tmustNewMatcher(t, labels.MatchEqual, \"bar\", \"baz\"),\n\t\t},\n\t}, {\n\t\tname:  \"input is accepted in neither\",\n\t\tinput: \"{foo!bar}\",\n\t\terr:   \"bad matcher format: foo!bar\",\n\t}, {\n\t\tname:  \"input is accepted in matchers/parse but not pkg/labels\",\n\t\tinput: \"{foo🙂=bar,bar=baz🙂}\",\n\t\texpected: labels.Matchers{\n\t\t\tmustNewMatcher(t, labels.MatchEqual, \"foo🙂\", \"bar\"),\n\t\t\tmustNewMatcher(t, labels.MatchEqual, \"bar\", \"baz🙂\"),\n\t\t},\n\t}, {\n\t\tname:  \"is accepted in pkg/labels but not matchers/parse\",\n\t\tinput: \"{foo=!bar,bar=$baz\\\\n}\",\n\t\texpected: labels.Matchers{\n\t\t\tmustNewMatcher(t, labels.MatchEqual, \"foo\", \"!bar\"),\n\t\t\tmustNewMatcher(t, labels.MatchEqual, \"bar\", \"$baz\\n\"),\n\t\t},\n\t}, {\n\t\t// This input causes disagreement because \\xf0\\x9f\\x99\\x82 is the byte sequence for 🙂,\n\t\t// which is not understood by pkg/labels but is understood by matchers/parse. In such cases,\n\t\t// the fallback parser returns the result from pkg/labels.\n\t\tname:  \"input causes disagreement\",\n\t\tinput: \"{foo=\\\"\\\\xf0\\\\x9f\\\\x99\\\\x82\\\"}\",\n\t\texpected: labels.Matchers{\n\t\t\tmustNewMatcher(t, labels.MatchEqual, \"foo\", \"\\\\xf0\\\\x9f\\\\x99\\\\x82\"),\n\t\t},\n\t}}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tf := FallbackMatchersParser(promslog.NewNopLogger())\n\t\t\tmatchers, err := f(test.input, \"test\")\n\t\t\tif test.err != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, test.expected, matchers)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc mustNewMatcher(t *testing.T, op labels.MatchType, name, value string) *labels.Matcher {\n\tm, err := labels.NewMatcher(op, name, value)\n\trequire.NoError(t, err)\n\treturn m\n}\n\nfunc TestIsValidClassicLabelName(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    model.LabelName\n\t\texpected bool\n\t}{{\n\t\tname:     \"foo is accepted\",\n\t\tinput:    \"foo\",\n\t\texpected: true,\n\t}, {\n\t\tname:     \"starts with underscore and ends with number is accepted\",\n\t\tinput:    \"_foo1\",\n\t\texpected: true,\n\t}, {\n\t\tname:     \"empty is not accepted\",\n\t\tinput:    \"\",\n\t\texpected: false,\n\t}, {\n\t\tname:     \"starts with number is not accepted\",\n\t\tinput:    \"0foo\",\n\t\texpected: false,\n\t}, {\n\t\tname:     \"contains emoji is not accepted\",\n\t\tinput:    \"foo🙂\",\n\t\texpected: false,\n\t}}\n\n\tfor _, test := range tests {\n\t\tfn := isValidClassicLabelName(promslog.NewNopLogger())\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\trequire.Equal(t, test.expected, fn(test.input))\n\t\t})\n\t}\n}\n\nfunc TestIsValidUTF8LabelName(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    model.LabelName\n\t\texpected bool\n\t}{{\n\t\tname:     \"foo is accepted\",\n\t\tinput:    \"foo\",\n\t\texpected: true,\n\t}, {\n\t\tname:     \"starts with underscore and ends with number is accepted\",\n\t\tinput:    \"_foo1\",\n\t\texpected: true,\n\t}, {\n\t\tname:     \"starts with number is accepted\",\n\t\tinput:    \"0foo\",\n\t\texpected: true,\n\t}, {\n\t\tname:     \"contains emoji is accepted\",\n\t\tinput:    \"foo🙂\",\n\t\texpected: true,\n\t}, {\n\t\tname:     \"empty is not accepted\",\n\t\tinput:    \"\",\n\t\texpected: false,\n\t}}\n\n\tfor _, test := range tests {\n\t\tfn := isValidUTF8LabelName(promslog.NewNopLogger())\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\trequire.Equal(t, test.expected, fn(test.input))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "matcher/compliance/compliance_test.go",
    "content": "// Copyright 2023 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage compliance\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/prometheus/alertmanager/matcher/parse\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n)\n\nfunc TestCompliance(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tinput string\n\t\twant  labels.Matchers\n\t\terr   string\n\t\tskip  bool\n\t}{\n\t\t{\n\t\t\tinput: `{}`,\n\t\t\twant:  labels.Matchers{},\n\t\t\tskip:  true,\n\t\t},\n\t\t{\n\t\t\tinput: `{foo='}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"'\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t\t{\n\t\t\tinput: \"{foo=`}\",\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"`\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\\n}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"\\n\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=bar\\n}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar\\n\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\\t}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"\\\\t\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=bar\\t}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar\\\\t\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=bar\\}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar\\\\\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=bar\\\\}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar\\\\\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\\\"}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"\\\"\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=bar\\\"}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar\\\"\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=bar}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"bar\"}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=~bar.*}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchRegexp, \"foo\", \"bar.*\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=~\"bar.*\"}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchRegexp, \"foo\", \"bar.*\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo!=bar}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchNotEqual, \"foo\", \"bar\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo!=\"bar\"}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchNotEqual, \"foo\", \"bar\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo!~bar.*}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchNotRegexp, \"foo\", \"bar.*\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo!~\"bar.*\"}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchNotRegexp, \"foo\", \"bar.*\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"bar\", baz!=\"quux\"}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar\")\n\t\t\t\tm2, _ := labels.NewMatcher(labels.MatchNotEqual, \"baz\", \"quux\")\n\t\t\t\treturn append(ms, m, m2)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"bar\", baz!~\"quux.*\"}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar\")\n\t\t\t\tm2, _ := labels.NewMatcher(labels.MatchNotRegexp, \"baz\", \"quux.*\")\n\t\t\t\treturn append(ms, m, m2)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"bar\",baz!~\".*quux\", derp=\"wat\"}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar\")\n\t\t\t\tm2, _ := labels.NewMatcher(labels.MatchNotRegexp, \"baz\", \".*quux\")\n\t\t\t\tm3, _ := labels.NewMatcher(labels.MatchEqual, \"derp\", \"wat\")\n\t\t\t\treturn append(ms, m, m2, m3)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"bar\", baz!=\"quux\", derp=\"wat\"}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar\")\n\t\t\t\tm2, _ := labels.NewMatcher(labels.MatchNotEqual, \"baz\", \"quux\")\n\t\t\t\tm3, _ := labels.NewMatcher(labels.MatchEqual, \"derp\", \"wat\")\n\t\t\t\treturn append(ms, m, m2, m3)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"bar\", baz!~\".*quux.*\", derp=\"wat\"}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar\")\n\t\t\t\tm2, _ := labels.NewMatcher(labels.MatchNotRegexp, \"baz\", \".*quux.*\")\n\t\t\t\tm3, _ := labels.NewMatcher(labels.MatchEqual, \"derp\", \"wat\")\n\t\t\t\treturn append(ms, m, m2, m3)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"bar\", instance=~\"some-api.*\"}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar\")\n\t\t\t\tm2, _ := labels.NewMatcher(labels.MatchRegexp, \"instance\", \"some-api.*\")\n\t\t\t\treturn append(ms, m, m2)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"\"}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"bar,quux\", job=\"job1\"}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar,quux\")\n\t\t\t\tm2, _ := labels.NewMatcher(labels.MatchEqual, \"job\", \"job1\")\n\t\t\t\treturn append(ms, m, m2)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo = \"bar\", dings != \"bums\", }`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar\")\n\t\t\t\tm2, _ := labels.NewMatcher(labels.MatchNotEqual, \"dings\", \"bums\")\n\t\t\t\treturn append(ms, m, m2)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `foo=bar,dings!=bums`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar\")\n\t\t\t\tm2, _ := labels.NewMatcher(labels.MatchNotEqual, \"dings\", \"bums\")\n\t\t\t\treturn append(ms, m, m2)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{quote=\"She said: \\\"Hi, ladies! That's gender-neutral…\\\"\"}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"quote\", `She said: \"Hi, ladies! That's gender-neutral…\"`)\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `statuscode=~\"5..\"`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchRegexp, \"statuscode\", \"5..\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `tricky=~~~`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchRegexp, \"tricky\", \"~~\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t\t{\n\t\t\tinput: `trickier==\\\\=\\=\\\"`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"trickier\", `=\\=\\=\"`)\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t\t{\n\t\t\tinput: `contains_quote != \"\\\"\" , contains_comma !~ \"foo,bar\" , `,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchNotEqual, \"contains_quote\", `\"`)\n\t\t\t\tm2, _ := labels.NewMatcher(labels.MatchNotRegexp, \"contains_comma\", \"foo,bar\")\n\t\t\t\treturn append(ms, m, m2)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=bar}}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar}\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=bar}},}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"bar}}\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=,bar=}}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm1, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"\")\n\t\t\t\tm2, _ := labels.NewMatcher(labels.MatchEqual, \"bar\", \"}\")\n\t\t\t\treturn append(ms, m1, m2)\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t\t{\n\t\t\tinput: `job=`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"job\", \"\")\n\t\t\t\treturn labels.Matchers{m}\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t\t{\n\t\t\tinput: `{name-with-dashes = \"bar\"}`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"name-with-dashes\", \"bar\")\n\t\t\t\treturn labels.Matchers{m}\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{,}`,\n\t\t\terr:   \"bad matcher format: \",\n\t\t},\n\t\t{\n\t\t\tinput: `job=\"value`,\n\t\t\terr:   `matcher value contains unescaped double quote: \"value`,\n\t\t},\n\t\t{\n\t\t\tinput: `job=value\"`,\n\t\t\terr:   `matcher value contains unescaped double quote: value\"`,\n\t\t},\n\t\t{\n\t\t\tinput: `trickier==\\\\=\\=\\\"\"`,\n\t\t\terr:   `matcher value contains unescaped double quote: =\\\\=\\=\\\"\"`,\n\t\t},\n\t\t{\n\t\t\tinput: `contains_unescaped_quote = foo\"bar`,\n\t\t\terr:   `matcher value contains unescaped double quote: foo\"bar`,\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=~\"invalid[regexp\"}`,\n\t\t\terr:   \"error parsing regexp: missing closing ]: `[regexp)$`\",\n\t\t},\n\t\t// Double escaped strings.\n\t\t{\n\t\t\tinput: `\"{foo=\\\"bar\"}`,\n\t\t\terr:   `bad matcher format: \"{foo=\\\"bar\"`,\n\t\t},\n\t\t{\n\t\t\tinput: `\"foo=\\\"bar\"`,\n\t\t\terr:   `bad matcher format: \"foo=\\\"bar\"`,\n\t\t},\n\t\t{\n\t\t\tinput: `\"foo=\\\"bar\\\"\"`,\n\t\t\terr:   `bad matcher format: \"foo=\\\"bar\\\"\"`,\n\t\t},\n\t\t{\n\t\t\tinput: `\"foo=\\\"bar\\\"`,\n\t\t\terr:   `bad matcher format: \"foo=\\\"bar\\\"`,\n\t\t},\n\t\t{\n\t\t\tinput: `\"{foo=\\\"bar\\\"}\"`,\n\t\t\terr:   `bad matcher format: \"{foo=\\\"bar\\\"}\"`,\n\t\t},\n\t\t{\n\t\t\tinput: `\"foo=\"bar\"\"`,\n\t\t\terr:   `bad matcher format: \"foo=\"bar\"\"`,\n\t\t},\n\t\t{\n\t\t\tinput: `{{foo=`,\n\t\t\terr:   `bad matcher format: {foo=`,\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=}b`,\n\t\t\twant: func() labels.Matchers {\n\t\t\t\tms := labels.Matchers{}\n\t\t\t\tm, _ := labels.NewMatcher(labels.MatchEqual, \"foo\", \"}b\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t\tskip: true,\n\t\t},\n\t} {\n\t\tt.Run(tc.input, func(t *testing.T) {\n\t\t\tif tc.skip {\n\t\t\t\tt.Skip()\n\t\t\t}\n\t\t\tgot, err := parse.Matchers(tc.input)\n\t\t\tif err != nil && tc.err == \"\" {\n\t\t\t\tt.Fatalf(\"got error where none expected: %v\", err)\n\t\t\t}\n\t\t\tif err == nil && tc.err != \"\" {\n\t\t\t\tt.Fatalf(\"expected error but got none: %v\", tc.err)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tc.want) {\n\t\t\t\tt.Fatalf(\"matchers not equal:\\ngot %s\\nwant %s\", got, tc.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "matcher/parse/bench_test.go",
    "content": "// Copyright 2023 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage parse\n\nimport (\n\t\"testing\"\n)\n\nconst (\n\tsimpleExample  = \"{foo=\\\"bar\\\"}\"\n\tcomplexExample = \"{foo=\\\"bar\\\",bar=~\\\"[a-zA-Z0-9+]\\\"}\"\n)\n\nfunc BenchmarkParseSimple(b *testing.B) {\n\tfor b.Loop() {\n\t\tif _, err := Matchers(simpleExample); err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n\nfunc BenchmarkParseComplex(b *testing.B) {\n\tfor b.Loop() {\n\t\tif _, err := Matchers(complexExample); err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "matcher/parse/fuzz_test.go",
    "content": "// Copyright 2023 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage parse\n\nimport (\n\t\"testing\"\n)\n\n// FuzzParse fuzz tests the parser to see if we can make it panic.\nfunc FuzzParse(f *testing.F) {\n\tf.Add(\"{foo=bar,bar=~[a-zA-Z]+,baz!=qux,qux!~[0-9]+\")\n\tf.Fuzz(func(t *testing.T, s string) {\n\t\tmatchers, err := Matchers(s)\n\t\tif matchers != nil && err != nil {\n\t\t\tt.Errorf(\"Unexpected matchers and err: %v %s\", matchers, err)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "matcher/parse/lexer.go",
    "content": "// Copyright 2023 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage parse\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n)\n\nconst (\n\teof rune = -1\n)\n\nfunc isReserved(r rune) bool {\n\treturn unicode.IsSpace(r) || strings.ContainsRune(\"{}!=~,\\\\\\\"'`\", r)\n}\n\n// expectedError is returned when the next rune does not match what is expected.\ntype expectedError struct {\n\tposition\n\tinput    string\n\texpected string\n}\n\nfunc (e expectedError) Error() string {\n\tif e.offsetEnd >= len(e.input) {\n\t\treturn fmt.Sprintf(\"%d:%d: unexpected end of input, expected one of '%s'\",\n\t\t\te.columnStart,\n\t\t\te.columnEnd,\n\t\t\te.expected,\n\t\t)\n\t}\n\treturn fmt.Sprintf(\"%d:%d: %s: expected one of '%s'\",\n\t\te.columnStart,\n\t\te.columnEnd,\n\t\te.input[e.offsetStart:e.offsetEnd],\n\t\te.expected,\n\t)\n}\n\n// invalidInputError is returned when the next rune in the input does not match\n// the grammar of Prometheus-like matchers.\ntype invalidInputError struct {\n\tposition\n\tinput string\n}\n\nfunc (e invalidInputError) Error() string {\n\treturn fmt.Sprintf(\"%d:%d: %s: invalid input\",\n\t\te.columnStart,\n\t\te.columnEnd,\n\t\te.input[e.offsetStart:e.offsetEnd],\n\t)\n}\n\n// unterminatedError is returned when text in quotes does not have a closing quote.\ntype unterminatedError struct {\n\tposition\n\tinput string\n\tquote rune\n}\n\nfunc (e unterminatedError) Error() string {\n\treturn fmt.Sprintf(\"%d:%d: %s: missing end %c\",\n\t\te.columnStart,\n\t\te.columnEnd,\n\t\te.input[e.offsetStart:e.offsetEnd],\n\t\te.quote,\n\t)\n}\n\n// lexer scans a sequence of tokens that match the grammar of Prometheus-like\n// matchers. A token is emitted for each call to scan() which returns the\n// next token in the input or an error if the input does not conform to the\n// grammar. A token can be one of a number of kinds and corresponds to a\n// subslice of the input. Once the input has been consumed successive calls to\n// scan() return a tokenEOF token.\ntype lexer struct {\n\tinput  string\n\terr    error\n\tstart  int // The offset of the current token.\n\tpos    int // The position of the cursor in the input.\n\twidth  int // The width of the last rune.\n\tcolumn int // The column offset of the current token.\n\tcols   int // The number of columns (runes) decoded from the input.\n}\n\n// Scans the next token in the input or an error if the input does not\n// conform to the grammar. Once the input has been consumed successive\n// calls scan() return a tokenEOF token.\nfunc (l *lexer) scan() (token, error) {\n\tt := token{}\n\t// Do not attempt to emit more tokens if the input is invalid.\n\tif l.err != nil {\n\t\treturn t, l.err\n\t}\n\t// Iterate over each rune in the input and either emit a token or an error.\n\tfor r := l.next(); r != eof; r = l.next() {\n\t\tswitch {\n\t\tcase r == '{':\n\t\t\tt = l.emit(tokenOpenBrace)\n\t\t\treturn t, l.err\n\t\tcase r == '}':\n\t\t\tt = l.emit(tokenCloseBrace)\n\t\t\treturn t, l.err\n\t\tcase r == ',':\n\t\t\tt = l.emit(tokenComma)\n\t\t\treturn t, l.err\n\t\tcase r == '=' || r == '!':\n\t\t\tl.rewind()\n\t\t\tt, l.err = l.scanOperator()\n\t\t\treturn t, l.err\n\t\tcase r == '\"':\n\t\t\tl.rewind()\n\t\t\tt, l.err = l.scanQuoted()\n\t\t\treturn t, l.err\n\t\tcase !isReserved(r):\n\t\t\tl.rewind()\n\t\t\tt, l.err = l.scanUnquoted()\n\t\t\treturn t, l.err\n\t\tcase unicode.IsSpace(r):\n\t\t\tl.skip()\n\t\tdefault:\n\t\t\tl.err = invalidInputError{\n\t\t\t\tposition: l.position(),\n\t\t\t\tinput:    l.input,\n\t\t\t}\n\t\t\treturn t, l.err\n\t\t}\n\t}\n\treturn t, l.err\n}\n\nfunc (l *lexer) scanOperator() (token, error) {\n\t// If the first rune is an '!' then it must be followed with either an\n\t// '=' or '~' to not match a string or regex.\n\tif l.accept(\"!\") {\n\t\tif l.accept(\"=\") {\n\t\t\treturn l.emit(tokenNotEquals), nil\n\t\t}\n\t\tif l.accept(\"~\") {\n\t\t\treturn l.emit(tokenNotMatches), nil\n\t\t}\n\t\treturn token{}, expectedError{\n\t\t\tposition: l.position(),\n\t\t\tinput:    l.input,\n\t\t\texpected: \"=~\",\n\t\t}\n\t}\n\t// If the first rune is an '=' then it can be followed with an optional\n\t// '~' to match a regex.\n\tif l.accept(\"=\") {\n\t\tif l.accept(\"~\") {\n\t\t\treturn l.emit(tokenMatches), nil\n\t\t}\n\t\treturn l.emit(tokenEquals), nil\n\t}\n\treturn token{}, expectedError{\n\t\tposition: l.position(),\n\t\tinput:    l.input,\n\t\texpected: \"!=\",\n\t}\n}\n\nfunc (l *lexer) scanQuoted() (token, error) {\n\tif err := l.expect(\"\\\"\"); err != nil {\n\t\treturn token{}, err\n\t}\n\tvar isEscaped bool\n\tfor r := l.next(); r != eof; r = l.next() {\n\t\tif isEscaped {\n\t\t\tisEscaped = false\n\t\t} else if r == '\\\\' {\n\t\t\tisEscaped = true\n\t\t} else if r == '\"' {\n\t\t\tl.rewind()\n\t\t\tbreak\n\t\t}\n\t}\n\tif err := l.expect(\"\\\"\"); err != nil {\n\t\treturn token{}, unterminatedError{\n\t\t\tposition: l.position(),\n\t\t\tinput:    l.input,\n\t\t\tquote:    '\"',\n\t\t}\n\t}\n\treturn l.emit(tokenQuoted), nil\n}\n\nfunc (l *lexer) scanUnquoted() (token, error) {\n\tfor r := l.next(); r != eof; r = l.next() {\n\t\tif isReserved(r) {\n\t\t\tl.rewind()\n\t\t\tbreak\n\t\t}\n\t}\n\treturn l.emit(tokenUnquoted), nil\n}\n\n// peek the next token in the input or an error if the input does not\n// conform to the grammar. Once the input has been consumed successive\n// calls peek() return a tokenEOF token.\nfunc (l *lexer) peek() (token, error) {\n\tstart := l.start\n\tpos := l.pos\n\twidth := l.width\n\tcolumn := l.column\n\tcols := l.cols\n\t// Do not reset l.err because we can return it on the next call to scan().\n\tdefer func() {\n\t\tl.start = start\n\t\tl.pos = pos\n\t\tl.width = width\n\t\tl.column = column\n\t\tl.cols = cols\n\t}()\n\treturn l.scan()\n}\n\n// position returns the position of the last emitted token.\nfunc (l *lexer) position() position {\n\treturn position{\n\t\toffsetStart: l.start,\n\t\toffsetEnd:   l.pos,\n\t\tcolumnStart: l.column,\n\t\tcolumnEnd:   l.cols,\n\t}\n}\n\n// accept consumes the next if its one of the valid runes.\n// It returns true if the next rune was accepted, otherwise false.\nfunc (l *lexer) accept(valid string) bool {\n\tif strings.ContainsRune(valid, l.next()) {\n\t\treturn true\n\t}\n\tl.rewind()\n\treturn false\n}\n\n// expect consumes the next rune if its one of the valid runes.\n// It returns nil if the next rune is valid, otherwise an expectedError\n// error.\nfunc (l *lexer) expect(valid string) error {\n\tif strings.ContainsRune(valid, l.next()) {\n\t\treturn nil\n\t}\n\tl.rewind()\n\treturn expectedError{\n\t\tposition: l.position(),\n\t\tinput:    l.input,\n\t\texpected: valid,\n\t}\n}\n\n// emits returns the scanned input as a token.\nfunc (l *lexer) emit(kind tokenKind) token {\n\tt := token{\n\t\tkind:     kind,\n\t\tvalue:    l.input[l.start:l.pos],\n\t\tposition: l.position(),\n\t}\n\tl.start = l.pos\n\tl.column = l.cols\n\treturn t\n}\n\n// next returns the next rune in the input or eof.\nfunc (l *lexer) next() rune {\n\tif l.pos >= len(l.input) {\n\t\tl.width = 0\n\t\treturn eof\n\t}\n\tr, width := utf8.DecodeRuneInString(l.input[l.pos:])\n\tl.width = width\n\tl.pos += width\n\tl.cols++\n\treturn r\n}\n\n// rewind the last rune in the input. It should not be called more than once\n// between consecutive calls of next.\nfunc (l *lexer) rewind() {\n\tl.pos -= l.width\n\t// When the next rune in the input is eof the width is zero. This check\n\t// prevents cols from being decremented when the next rune being accepted\n\t// is instead eof.\n\tif l.width > 0 {\n\t\tl.cols--\n\t}\n}\n\n// skip the scanned input between start and pos.\nfunc (l *lexer) skip() {\n\tl.start = l.pos\n\tl.column = l.cols\n}\n"
  },
  {
    "path": "matcher/parse/lexer_test.go",
    "content": "// Copyright 2023 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage parse\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestLexer_Scan(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected []token\n\t\terr      string\n\t}{{\n\t\tname:  \"no input\",\n\t\tinput: \"\",\n\t}, {\n\t\tname:  \"open brace\",\n\t\tinput: \"{\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenOpenBrace,\n\t\t\tvalue: \"{\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   1,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   1,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"open brace with leading space\",\n\t\tinput: \" {\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenOpenBrace,\n\t\t\tvalue: \"{\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 1,\n\t\t\t\toffsetEnd:   2,\n\t\t\t\tcolumnStart: 1,\n\t\t\t\tcolumnEnd:   2,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"close brace\",\n\t\tinput: \"}\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenCloseBrace,\n\t\t\tvalue: \"}\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   1,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   1,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"close brace with leading space\",\n\t\tinput: \" }\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenCloseBrace,\n\t\t\tvalue: \"}\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 1,\n\t\t\t\toffsetEnd:   2,\n\t\t\t\tcolumnStart: 1,\n\t\t\t\tcolumnEnd:   2,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"open and closing braces\",\n\t\tinput: \"{}\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenOpenBrace,\n\t\t\tvalue: \"{\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   1,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   1,\n\t\t\t},\n\t\t}, {\n\t\t\tkind:  tokenCloseBrace,\n\t\t\tvalue: \"}\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 1,\n\t\t\t\toffsetEnd:   2,\n\t\t\t\tcolumnStart: 1,\n\t\t\t\tcolumnEnd:   2,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"open and closing braces with space\",\n\t\tinput: \"{ }\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenOpenBrace,\n\t\t\tvalue: \"{\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   1,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   1,\n\t\t\t},\n\t\t}, {\n\t\t\tkind:  tokenCloseBrace,\n\t\t\tvalue: \"}\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 2,\n\t\t\t\toffsetEnd:   3,\n\t\t\t\tcolumnStart: 2,\n\t\t\t\tcolumnEnd:   3,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"unquoted\",\n\t\tinput: \"hello\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"hello\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   5,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   5,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"unquoted with underscore\",\n\t\tinput: \"hello_world\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"hello_world\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   11,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   11,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"unquoted with colon\",\n\t\tinput: \"hello:world\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"hello:world\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   11,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   11,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"unquoted with numbers\",\n\t\tinput: \"hello0123456789\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"hello0123456789\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   15,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   15,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"unquoted can start with underscore\",\n\t\tinput: \"_hello\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"_hello\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   6,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   6,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"unquoted separated with space\",\n\t\tinput: \"hello world\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"hello\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   5,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   5,\n\t\t\t},\n\t\t}, {\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"world\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 6,\n\t\t\t\toffsetEnd:   11,\n\t\t\t\tcolumnStart: 6,\n\t\t\t\tcolumnEnd:   11,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"newline before unquoted is skipped\",\n\t\tinput: \"\\nhello\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"hello\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 1,\n\t\t\t\toffsetEnd:   6,\n\t\t\t\tcolumnStart: 1,\n\t\t\t\tcolumnEnd:   6,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"newline after unquoted is skipped\",\n\t\tinput: \"hello\\n\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"hello\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   5,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   5,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"carriage return before unquoted is skipped\",\n\t\tinput: \"\\rhello\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"hello\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 1,\n\t\t\t\toffsetEnd:   6,\n\t\t\t\tcolumnStart: 1,\n\t\t\t\tcolumnEnd:   6,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"space before unquoted is skipped\",\n\t\tinput: \" hello\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"hello\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 1,\n\t\t\t\toffsetEnd:   6,\n\t\t\t\tcolumnStart: 1,\n\t\t\t\tcolumnEnd:   6,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"space after unquoted is skipped\",\n\t\tinput: \"hello \",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"hello\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   5,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   5,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"newline between two unquoted is skipped\",\n\t\tinput: \"hello\\nworld\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"hello\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   5,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   5,\n\t\t\t},\n\t\t}, {\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"world\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 6,\n\t\t\t\toffsetEnd:   11,\n\t\t\t\tcolumnStart: 6,\n\t\t\t\tcolumnEnd:   11,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"unquoted $\",\n\t\tinput: \"$\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"$\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   1,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   1,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"unquoted emoji\",\n\t\tinput: \"🙂\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"🙂\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   4,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   1,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"unquoted unicode\",\n\t\tinput: \"Σ\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"Σ\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   2,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   1,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"unquoted unicode sentence\",\n\t\tinput: \"hello🙂Σ world\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"hello🙂Σ\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   11,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   7,\n\t\t\t},\n\t\t}, {\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"world\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 12,\n\t\t\t\toffsetEnd:   17,\n\t\t\t\tcolumnStart: 8,\n\t\t\t\tcolumnEnd:   13,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"unquoted unicode sentence with unicode space\",\n\t\tinput: \"hello🙂Σ\\u202fworld\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"hello🙂Σ\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   11,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   7,\n\t\t\t},\n\t\t}, {\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"world\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 14,\n\t\t\t\toffsetEnd:   19,\n\t\t\t\tcolumnStart: 8,\n\t\t\t\tcolumnEnd:   13,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"quoted\",\n\t\tinput: \"\\\"hello\\\"\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenQuoted,\n\t\t\tvalue: \"\\\"hello\\\"\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   7,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   7,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"quoted with unicode\",\n\t\tinput: \"\\\"hello 🙂\\\"\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenQuoted,\n\t\t\tvalue: \"\\\"hello 🙂\\\"\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   12,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   9,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"quoted with space\",\n\t\tinput: \"\\\"hello world\\\"\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenQuoted,\n\t\t\tvalue: \"\\\"hello world\\\"\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   13,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   13,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"quoted with unicode space\",\n\t\tinput: \"\\\"hello\\u202fworld\\\"\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenQuoted,\n\t\t\tvalue: \"\\\"hello\\u202fworld\\\"\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   15,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   13,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"quoted with newline\",\n\t\tinput: \"\\\"hello\\nworld\\\"\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenQuoted,\n\t\t\tvalue: \"\\\"hello\\nworld\\\"\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   13,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   13,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"quoted with tab\",\n\t\tinput: \"\\\"hello\\tworld\\\"\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenQuoted,\n\t\t\tvalue: \"\\\"hello\\tworld\\\"\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   13,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   13,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"quoted with regex digit character class\",\n\t\tinput: \"\\\"\\\\d+\\\"\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenQuoted,\n\t\t\tvalue: \"\\\"\\\\d+\\\"\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   5,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   5,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"quoted with escaped regex digit character class\",\n\t\tinput: \"\\\"\\\\\\\\d+\\\"\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenQuoted,\n\t\t\tvalue: \"\\\"\\\\\\\\d+\\\"\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   6,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   6,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"quoted with escaped quotes\",\n\t\tinput: \"\\\"hello \\\\\\\"world\\\\\\\"\\\"\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenQuoted,\n\t\t\tvalue: \"\\\"hello \\\\\\\"world\\\\\\\"\\\"\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   17,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   17,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"quoted with escaped backslash\",\n\t\tinput: \"\\\"hello world\\\\\\\\\\\"\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenQuoted,\n\t\t\tvalue: \"\\\"hello world\\\\\\\\\\\"\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   15,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   15,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"quoted escape sequence\",\n\t\tinput: \"\\\"\\\\n\\\"\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenQuoted,\n\t\t\tvalue: \"\\\"\\\\n\\\"\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   4,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   4,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"equals operator\",\n\t\tinput: \"=\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenEquals,\n\t\t\tvalue: \"=\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   1,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   1,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"not equals operator\",\n\t\tinput: \"!=\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenNotEquals,\n\t\t\tvalue: \"!=\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   2,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   2,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"matches regex operator\",\n\t\tinput: \"=~\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenMatches,\n\t\t\tvalue: \"=~\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   2,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   2,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"not matches regex operator\",\n\t\tinput: \"!~\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenNotMatches,\n\t\t\tvalue: \"!~\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   2,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   2,\n\t\t\t},\n\t\t}},\n\t}, {\n\t\tname:  \"invalid operator\",\n\t\tinput: \"!\",\n\t\terr:   \"0:1: unexpected end of input, expected one of '=~'\",\n\t}, {\n\t\tname:  \"another invalid operator\",\n\t\tinput: \"~\",\n\t\terr:   \"0:1: ~: invalid input\",\n\t}, {\n\t\tname:  \"unexpected ! after operator\",\n\t\tinput: \"=!\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenEquals,\n\t\t\tvalue: \"=\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   1,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   1,\n\t\t\t},\n\t\t}},\n\t\terr: \"1:2: unexpected end of input, expected one of '=~'\",\n\t}, {\n\t\tname:  \"unexpected !! after operator\",\n\t\tinput: \"!=!!\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenNotEquals,\n\t\t\tvalue: \"!=\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   2,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   2,\n\t\t\t},\n\t\t}},\n\t\terr: \"2:3: !: expected one of '=~'\",\n\t}, {\n\t\tname:  \"unexpected ! after unquoted\",\n\t\tinput: \"hello!\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"hello\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   5,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   5,\n\t\t\t},\n\t\t}},\n\t\terr: \"5:6: unexpected end of input, expected one of '=~'\",\n\t}, {\n\t\tname:  \"invalid escape sequence\",\n\t\tinput: \"\\\\n\",\n\t\terr:   \"0:1: \\\\: invalid input\",\n\t}, {\n\t\tname:  \"invalid escape sequence before unquoted\",\n\t\tinput: \"\\\\nhello\",\n\t\terr:   \"0:1: \\\\: invalid input\",\n\t}, {\n\t\tname:  \"invalid escape sequence after unquoted\",\n\t\tinput: \"hello\\\\n\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"hello\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   5,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   5,\n\t\t\t},\n\t\t}},\n\t\terr: \"5:6: \\\\: invalid input\",\n\t}, {\n\t\tname:  \"another invalid escape sequence after unquoted\",\n\t\tinput: \"hello\\\\r\",\n\t\texpected: []token{{\n\t\t\tkind:  tokenUnquoted,\n\t\t\tvalue: \"hello\",\n\t\t\tposition: position{\n\t\t\t\toffsetStart: 0,\n\t\t\t\toffsetEnd:   5,\n\t\t\t\tcolumnStart: 0,\n\t\t\t\tcolumnEnd:   5,\n\t\t\t},\n\t\t}},\n\t\terr: \"5:6: \\\\: invalid input\",\n\t}, {\n\t\tname:  \"unterminated quoted\",\n\t\tinput: \"\\\"hello\",\n\t\terr:   \"0:6: \\\"hello: missing end \\\"\",\n\t}, {\n\t\tname:  \"unterminated quoted with escaped quote\",\n\t\tinput: \"\\\"hello\\\\\\\"\",\n\t\terr:   \"0:8: \\\"hello\\\\\\\": missing end \\\"\",\n\t}}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tl := lexer{input: test.input}\n\t\t\t// scan all expected tokens.\n\t\t\tfor i := 0; i < len(test.expected); i++ {\n\t\t\t\ttok, err := l.scan()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, test.expected[i], tok)\n\t\t\t}\n\t\t\tif test.err == \"\" {\n\t\t\t\t// Check there are no more tokens.\n\t\t\t\ttok, err := l.scan()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, token{}, tok)\n\t\t\t} else {\n\t\t\t\t// Check if expected error is returned.\n\t\t\t\ttok, err := l.scan()\n\t\t\t\trequire.Equal(t, token{}, tok)\n\t\t\t\trequire.EqualError(t, err, test.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// This test asserts that the lexer does not emit more tokens after an\n// error has occurred.\nfunc TestLexer_ScanError(t *testing.T) {\n\tl := lexer{input: \"\\\"hello\"}\n\tfor range 10 {\n\t\ttok, err := l.scan()\n\t\trequire.Equal(t, token{}, tok)\n\t\trequire.EqualError(t, err, \"0:6: \\\"hello: missing end \\\"\")\n\t}\n}\n\nfunc TestLexer_Peek(t *testing.T) {\n\tl := lexer{input: \"hello world\"}\n\texpected1 := token{\n\t\tkind:  tokenUnquoted,\n\t\tvalue: \"hello\",\n\t\tposition: position{\n\t\t\toffsetStart: 0,\n\t\t\toffsetEnd:   5,\n\t\t\tcolumnStart: 0,\n\t\t\tcolumnEnd:   5,\n\t\t},\n\t}\n\texpected2 := token{\n\t\tkind:  tokenUnquoted,\n\t\tvalue: \"world\",\n\t\tposition: position{\n\t\t\toffsetStart: 6,\n\t\t\toffsetEnd:   11,\n\t\t\tcolumnStart: 6,\n\t\t\tcolumnEnd:   11,\n\t\t},\n\t}\n\t// Check that peek() returns the first token.\n\ttok, err := l.peek()\n\trequire.NoError(t, err)\n\trequire.Equal(t, expected1, tok)\n\t// Check that scan() returns the peeked token.\n\ttok, err = l.scan()\n\trequire.NoError(t, err)\n\trequire.Equal(t, expected1, tok)\n\t// Check that peek() returns the second token until the next scan().\n\tfor range 10 {\n\t\ttok, err = l.peek()\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, expected2, tok)\n\t}\n\t// Check that scan() returns the last token.\n\ttok, err = l.scan()\n\trequire.NoError(t, err)\n\trequire.Equal(t, expected2, tok)\n\t// Should not be able to peek() further tokens.\n\tfor range 10 {\n\t\ttok, err = l.peek()\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, token{}, tok)\n\t}\n}\n\n// This test asserts that the lexer does not emit more tokens after an\n// error has occurred.\nfunc TestLexer_PeekError(t *testing.T) {\n\tl := lexer{input: \"\\\"hello\"}\n\tfor range 10 {\n\t\ttok, err := l.peek()\n\t\trequire.Equal(t, token{}, tok)\n\t\trequire.EqualError(t, err, \"0:6: \\\"hello: missing end \\\"\")\n\t}\n}\n\nfunc TestLexer_Pos(t *testing.T) {\n\tl := lexer{input: \"hello🙂\"}\n\t// The start position should be the zero-value.\n\trequire.Equal(t, position{}, l.position())\n\t_, err := l.scan()\n\trequire.NoError(t, err)\n\t// The position should contain the offset and column of the end.\n\texpected := position{\n\t\toffsetStart: 9,\n\t\toffsetEnd:   9,\n\t\tcolumnStart: 6,\n\t\tcolumnEnd:   6,\n\t}\n\trequire.Equal(t, expected, l.position())\n\t// The position should not change once the input has been consumed.\n\ttok, err := l.scan()\n\trequire.NoError(t, err)\n\trequire.True(t, tok.isEOF())\n\trequire.Equal(t, expected, l.position())\n}\n"
  },
  {
    "path": "matcher/parse/parse.go",
    "content": "// Copyright 2023 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage parse\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"runtime/debug\"\n\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n)\n\nvar (\n\terrEOF                         = errors.New(\"end of input\")\n\terrExpectedEOF                 = errors.New(\"expected end of input\")\n\terrNoOpenBrace                 = errors.New(\"expected opening brace\")\n\terrNoCloseBrace                = errors.New(\"expected close brace\")\n\terrNoLabelName                 = errors.New(\"expected label name\")\n\terrNoLabelValue                = errors.New(\"expected label value\")\n\terrNoOperator                  = errors.New(\"expected an operator such as '=', '!=', '=~' or '!~'\")\n\terrExpectedComma               = errors.New(\"expected a comma\")\n\terrExpectedCommaOrCloseBrace   = errors.New(\"expected a comma or close brace\")\n\terrExpectedMatcherOrCloseBrace = errors.New(\"expected a matcher or close brace after comma\")\n)\n\n// Matchers parses one or more matchers in the input string. It returns an error\n// if the input is invalid.\nfunc Matchers(input string) (matchers labels.Matchers, err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"parser panic: %s, %s\", r, debug.Stack())\n\t\t\terr = errors.New(\"parser panic: this should never happen, check stderr for the stack trace\")\n\t\t}\n\t}()\n\tp := parser{lexer: lexer{input: input}}\n\treturn p.parse()\n}\n\n// Matcher parses the matcher in the input string. It returns an error\n// if the input is invalid or contains two or more matchers.\nfunc Matcher(input string) (*labels.Matcher, error) {\n\tm, err := Matchers(input)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tswitch len(m) {\n\tcase 1:\n\t\treturn m[0], nil\n\tcase 0:\n\t\treturn nil, fmt.Errorf(\"no matchers\")\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"expected 1 matcher, found %d\", len(m))\n\t}\n}\n\n// parseFunc is state in the finite state automata.\ntype parseFunc func(l *lexer) (parseFunc, error)\n\n// parser reads the sequence of tokens from the lexer and returns either a\n// series of matchers or an error. It works as a finite state automata, where\n// each state in the automata is a parseFunc. The finite state automata can move\n// from one state to another by returning the next parseFunc. It terminates when\n// a parseFunc returns nil as the next parseFunc, if the lexer attempts to scan\n// input that does not match the expected grammar, or if the tokens returned from\n// the lexer cannot be parsed into a complete series of matchers.\ntype parser struct {\n\tmatchers labels.Matchers\n\t// Tracks if the input starts with an open brace and if we should expect to\n\t// parse a close brace at the end of the input.\n\thasOpenBrace bool\n\tlexer        lexer\n}\n\nfunc (p *parser) parse() (labels.Matchers, error) {\n\tvar (\n\t\terr error\n\t\tfn  = p.parseOpenBrace\n\t\tl   = &p.lexer\n\t)\n\tfor {\n\t\tif fn, err = fn(l); err != nil {\n\t\t\treturn nil, err\n\t\t} else if fn == nil {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn p.matchers, nil\n}\n\nfunc (p *parser) parseOpenBrace(l *lexer) (parseFunc, error) {\n\tvar (\n\t\thasCloseBrace bool\n\t\terr           error\n\t)\n\t// Can start with an optional open brace.\n\tp.hasOpenBrace, err = p.accept(l, tokenOpenBrace)\n\tif err != nil {\n\t\tif errors.Is(err, errEOF) {\n\t\t\treturn p.parseEOF, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\t// If the next token is a close brace there are no matchers in the input.\n\thasCloseBrace, err = p.acceptPeek(l, tokenCloseBrace)\n\tif err != nil {\n\t\t// If there is no more input after the open brace then parse the close brace\n\t\t// so the error message contains ErrNoCloseBrace.\n\t\tif errors.Is(err, errEOF) {\n\t\t\treturn p.parseCloseBrace, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\tif hasCloseBrace {\n\t\treturn p.parseCloseBrace, nil\n\t}\n\treturn p.parseMatcher, nil\n}\n\nfunc (p *parser) parseCloseBrace(l *lexer) (parseFunc, error) {\n\tif p.hasOpenBrace {\n\t\t// If there was an open brace there must be a matching close brace.\n\t\tif _, err := p.expect(l, tokenCloseBrace); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"0:%d: %w: %w\", l.position().columnEnd, err, errNoCloseBrace)\n\t\t}\n\t} else {\n\t\t// If there was no open brace there must not be a close brace either.\n\t\tif _, err := p.expect(l, tokenCloseBrace); err == nil {\n\t\t\treturn nil, fmt.Errorf(\"0:%d: }: %w\", l.position().columnEnd, errNoOpenBrace)\n\t\t}\n\t}\n\treturn p.parseEOF, nil\n}\n\nfunc (p *parser) parseMatcher(l *lexer) (parseFunc, error) {\n\tvar (\n\t\terr                   error\n\t\tt                     token\n\t\tmatchName, matchValue string\n\t\tmatchTy               labels.MatchType\n\t)\n\t// The first token should be the label name.\n\tif t, err = p.expect(l, tokenQuoted, tokenUnquoted); err != nil {\n\t\treturn nil, fmt.Errorf(\"%w: %w\", err, errNoLabelName)\n\t}\n\tmatchName, err = t.unquote()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%d:%d: %s: invalid input\", t.columnStart, t.columnEnd, t.value)\n\t}\n\t// The next token should be the operator.\n\tif t, err = p.expect(l, tokenEquals, tokenNotEquals, tokenMatches, tokenNotMatches); err != nil {\n\t\treturn nil, fmt.Errorf(\"%w: %w\", err, errNoOperator)\n\t}\n\tswitch t.kind {\n\tcase tokenEquals:\n\t\tmatchTy = labels.MatchEqual\n\tcase tokenNotEquals:\n\t\tmatchTy = labels.MatchNotEqual\n\tcase tokenMatches:\n\t\tmatchTy = labels.MatchRegexp\n\tcase tokenNotMatches:\n\t\tmatchTy = labels.MatchNotRegexp\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"bad operator %s\", t))\n\t}\n\t// The next token should be the match value. Like the match name, this too\n\t// can be either double-quoted UTF-8 or unquoted UTF-8 without reserved characters.\n\tif t, err = p.expect(l, tokenUnquoted, tokenQuoted); err != nil {\n\t\treturn nil, fmt.Errorf(\"%w: %w\", err, errNoLabelValue)\n\t}\n\tmatchValue, err = t.unquote()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%d:%d: %s: invalid input\", t.columnStart, t.columnEnd, t.value)\n\t}\n\tm, err := labels.NewMatcher(matchTy, matchName, matchValue)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create matcher: %w\", err)\n\t}\n\tp.matchers = append(p.matchers, m)\n\treturn p.parseEndOfMatcher, nil\n}\n\nfunc (p *parser) parseEndOfMatcher(l *lexer) (parseFunc, error) {\n\tt, err := p.expectPeek(l, tokenComma, tokenCloseBrace)\n\tif err != nil {\n\t\tif errors.Is(err, errEOF) {\n\t\t\t// If this is the end of input we still need to check if the optional\n\t\t\t// open brace has a matching close brace.\n\t\t\treturn p.parseCloseBrace, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"%w: %w\", err, errExpectedCommaOrCloseBrace)\n\t}\n\tswitch t.kind {\n\tcase tokenComma:\n\t\treturn p.parseComma, nil\n\tcase tokenCloseBrace:\n\t\treturn p.parseCloseBrace, nil\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"bad token %s\", t))\n\t}\n}\n\nfunc (p *parser) parseComma(l *lexer) (parseFunc, error) {\n\tif _, err := p.expect(l, tokenComma); err != nil {\n\t\treturn nil, fmt.Errorf(\"%w: %w\", err, errExpectedComma)\n\t}\n\t// The token after the comma can be another matcher, a close brace or end of input.\n\tt, err := p.expectPeek(l, tokenCloseBrace, tokenUnquoted, tokenQuoted)\n\tif err != nil {\n\t\tif errors.Is(err, errEOF) {\n\t\t\t// If this is the end of input we still need to check if the optional\n\t\t\t// open brace has a matching close brace.\n\t\t\treturn p.parseCloseBrace, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"%w: %w\", err, errExpectedMatcherOrCloseBrace)\n\t}\n\tif t.kind == tokenCloseBrace {\n\t\treturn p.parseCloseBrace, nil\n\t}\n\treturn p.parseMatcher, nil\n}\n\nfunc (p *parser) parseEOF(l *lexer) (parseFunc, error) {\n\tt, err := l.scan()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%w: %w\", err, errExpectedEOF)\n\t}\n\tif !t.isEOF() {\n\t\treturn nil, fmt.Errorf(\"%d:%d: %s: %w\", t.columnStart, t.columnEnd, t.value, errExpectedEOF)\n\t}\n\treturn nil, nil\n}\n\n// nolint:godot\n// accept returns true if the next token is one of the specified kinds,\n// otherwise false. If the token is accepted it is consumed. tokenEOF is\n// not an accepted kind  and instead accept returns ErrEOF if there is no\n// more input.\nfunc (p *parser) accept(l *lexer, kinds ...tokenKind) (ok bool, err error) {\n\tok, err = p.acceptPeek(l, kinds...)\n\tif ok {\n\t\tif _, err = l.scan(); err != nil {\n\t\t\tpanic(\"failed to scan peeked token\")\n\t\t}\n\t}\n\treturn ok, err\n}\n\n// nolint:godot\n// acceptPeek returns true if the next token is one of the specified kinds,\n// otherwise false. However, unlike accept, acceptPeek does not consume accepted\n// tokens. tokenEOF is not an accepted kind and instead accept returns ErrEOF\n// if there is no more input.\nfunc (p *parser) acceptPeek(l *lexer, kinds ...tokenKind) (bool, error) {\n\tt, err := l.peek()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif t.isEOF() {\n\t\treturn false, errEOF\n\t}\n\treturn t.isOneOf(kinds...), nil\n}\n\n// nolint:godot\n// expect returns the next token if it is one of the specified kinds, otherwise\n// it returns an error. If the token is expected it is consumed. tokenEOF is not\n// an accepted kind and instead expect returns ErrEOF if there is no more input.\nfunc (p *parser) expect(l *lexer, kind ...tokenKind) (token, error) {\n\tt, err := p.expectPeek(l, kind...)\n\tif err != nil {\n\t\treturn t, err\n\t}\n\tif _, err = l.scan(); err != nil {\n\t\tpanic(\"failed to scan peeked token\")\n\t}\n\treturn t, nil\n}\n\n// nolint:godot\n// expect returns the next token if it is one of the specified kinds, otherwise\n// it returns an error. However, unlike expect, expectPeek does not consume tokens.\n// tokenEOF is not an accepted kind and instead expect returns ErrEOF if there is no\n// more input.\nfunc (p *parser) expectPeek(l *lexer, kind ...tokenKind) (token, error) {\n\tt, err := l.peek()\n\tif err != nil {\n\t\treturn t, err\n\t}\n\tif t.isEOF() {\n\t\treturn t, errEOF\n\t}\n\tif !t.isOneOf(kind...) {\n\t\treturn t, fmt.Errorf(\"%d:%d: unexpected %s\", t.columnStart, t.columnEnd, t.value)\n\t}\n\treturn t, nil\n}\n"
  },
  {
    "path": "matcher/parse/parse_test.go",
    "content": "// Copyright 2023 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage parse\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n)\n\nfunc TestMatchers(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected labels.Matchers\n\t\terror    string\n\t}{{\n\t\tname:     \"no braces\",\n\t\tinput:    \"\",\n\t\texpected: nil,\n\t}, {\n\t\tname:     \"open and closing braces\",\n\t\tinput:    \"{}\",\n\t\texpected: nil,\n\t}, {\n\t\tname:     \"equals\",\n\t\tinput:    \"{foo=bar}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\")},\n\t}, {\n\t\tname:     \"equals with trailing comma\",\n\t\tinput:    \"{foo=bar,}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\")},\n\t}, {\n\t\tname:     \"not equals\",\n\t\tinput:    \"{foo!=bar}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchNotEqual, \"foo\", \"bar\")},\n\t}, {\n\t\tname:     \"match regex\",\n\t\tinput:    \"{foo=~[a-z]+}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchRegexp, \"foo\", \"[a-z]+\")},\n\t}, {\n\t\tname:     \"doesn't match regex\",\n\t\tinput:    \"{foo!~[a-z]+}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchNotRegexp, \"foo\", \"[a-z]+\")},\n\t}, {\n\t\tname:     \"equals unicode emoji\",\n\t\tinput:    \"{foo=🙂}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"🙂\")},\n\t}, {\n\t\tname:     \"equals unicode sentence\",\n\t\tinput:    \"{foo=🙂bar}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"🙂bar\")},\n\t}, {\n\t\tname:     \"equals without braces\",\n\t\tinput:    \"foo=bar\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\")},\n\t}, {\n\t\tname:     \"equals without braces but with trailing comma\",\n\t\tinput:    \"foo=bar,\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\")},\n\t}, {\n\t\tname:     \"not equals without braces\",\n\t\tinput:    \"foo!=bar\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchNotEqual, \"foo\", \"bar\")},\n\t}, {\n\t\tname:     \"match regex without braces\",\n\t\tinput:    \"foo=~[a-z]+\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchRegexp, \"foo\", \"[a-z]+\")},\n\t}, {\n\t\tname:     \"doesn't match regex without braces\",\n\t\tinput:    \"foo!~[a-z]+\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchNotRegexp, \"foo\", \"[a-z]+\")},\n\t}, {\n\t\tname:     \"equals in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"bar\\\"}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\")},\n\t}, {\n\t\tname:     \"equals in quotes and with trailing comma\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"bar\\\",}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\")},\n\t}, {\n\t\tname:     \"not equals in quotes\",\n\t\tinput:    \"{\\\"foo\\\"!=\\\"bar\\\"}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchNotEqual, \"foo\", \"bar\")},\n\t}, {\n\t\tname:     \"match regex in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=~\\\"[a-z]+\\\"}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchRegexp, \"foo\", \"[a-z]+\")},\n\t}, {\n\t\tname:     \"match regex digit in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=~\\\"\\\\\\\\d+\\\"}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchRegexp, \"foo\", \"\\\\d+\")},\n\t}, {\n\t\tname:     \"doesn't match regex in quotes\",\n\t\tinput:    \"{\\\"foo\\\"!~\\\"[a-z]+\\\"}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchNotRegexp, \"foo\", \"[a-z]+\")},\n\t}, {\n\t\tname:     \"equals unicode emoji in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"🙂\\\"}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"🙂\")},\n\t}, {\n\t\tname:     \"equals unicode emoji as bytes in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"\\\\xf0\\\\x9f\\\\x99\\\\x82\\\"}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"🙂\")},\n\t}, {\n\t\tname:     \"equals unicode emoji as code points in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"\\\\U0001f642\\\"}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"🙂\")},\n\t}, {\n\t\tname:     \"equals unicode sentence in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"🙂bar\\\"}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"🙂bar\")},\n\t}, {\n\t\tname:     \"equals with newline in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"bar\\\\n\\\"}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\\n\")},\n\t}, {\n\t\tname:     \"equals with tab in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"bar\\\\t\\\"}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\\t\")},\n\t}, {\n\t\tname:     \"equals with escaped quotes in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"\\\\\\\"bar\\\\\\\"\\\"}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"\\\"bar\\\"\")},\n\t}, {\n\t\tname:     \"equals with escaped backslash in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"bar\\\\\\\\\\\"}\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\\\\\")},\n\t}, {\n\t\tname:     \"equals without braces in quotes\",\n\t\tinput:    \"\\\"foo\\\"=\\\"bar\\\"\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\")},\n\t}, {\n\t\tname:     \"equals without braces in quotes with trailing comma\",\n\t\tinput:    \"\\\"foo\\\"=\\\"bar\\\",\",\n\t\texpected: labels.Matchers{mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\")},\n\t}, {\n\t\tname:  \"complex\",\n\t\tinput: \"{foo=bar,bar!=baz}\",\n\t\texpected: labels.Matchers{\n\t\t\tmustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\"),\n\t\t\tmustNewMatcher(t, labels.MatchNotEqual, \"bar\", \"baz\"),\n\t\t},\n\t}, {\n\t\tname:  \"complex in quotes\",\n\t\tinput: \"{foo=\\\"bar\\\",bar!=\\\"baz\\\"}\",\n\t\texpected: labels.Matchers{\n\t\t\tmustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\"),\n\t\t\tmustNewMatcher(t, labels.MatchNotEqual, \"bar\", \"baz\"),\n\t\t},\n\t}, {\n\t\tname:  \"complex without braces\",\n\t\tinput: \"foo=bar,bar!=baz\",\n\t\texpected: labels.Matchers{\n\t\t\tmustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\"),\n\t\t\tmustNewMatcher(t, labels.MatchNotEqual, \"bar\", \"baz\"),\n\t\t},\n\t}, {\n\t\tname:  \"complex without braces in quotes\",\n\t\tinput: \"foo=\\\"bar\\\",bar!=\\\"baz\\\"\",\n\t\texpected: labels.Matchers{\n\t\t\tmustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\"),\n\t\t\tmustNewMatcher(t, labels.MatchNotEqual, \"bar\", \"baz\"),\n\t\t},\n\t}, {\n\t\tname:  \"comma\",\n\t\tinput: \",\",\n\t\terror: \"0:1: unexpected ,: expected label name\",\n\t}, {\n\t\tname:  \"comma in braces\",\n\t\tinput: \"{,}\",\n\t\terror: \"1:2: unexpected ,: expected label name\",\n\t}, {\n\t\tname:  \"open brace\",\n\t\tinput: \"{\",\n\t\terror: \"0:1: end of input: expected close brace\",\n\t}, {\n\t\tname:  \"close brace\",\n\t\tinput: \"}\",\n\t\terror: \"0:1: }: expected opening brace\",\n\t}, {\n\t\tname:  \"no open brace\",\n\t\tinput: \"foo=bar}\",\n\t\terror: \"0:8: }: expected opening brace\",\n\t}, {\n\t\tname:  \"no close brace\",\n\t\tinput: \"{foo=bar\",\n\t\terror: \"0:8: end of input: expected close brace\",\n\t}, {\n\t\tname:  \"invalid input after operator and before quotes\",\n\t\tinput: \"{foo=:\\\"bar\\\"}\",\n\t\terror: \"6:11: unexpected \\\"bar\\\": expected a comma or close brace\",\n\t}, {\n\t\tname:  \"invalid escape sequence\",\n\t\tinput: \"{foo=\\\"bar\\\\w\\\"}\",\n\t\terror: \"5:12: \\\"bar\\\\w\\\": invalid input\",\n\t}, {\n\t\tname:  \"invalid escape sequence regex digits\",\n\t\tinput: \"{\\\"foo\\\"=~\\\"\\\\d+\\\"}\",\n\t\terror: \"8:13: \\\"\\\\d+\\\": invalid input\",\n\t}, {\n\t\tname:  \"no unquoted escape sequences\",\n\t\tinput: \"{foo=bar\\\\n}\",\n\t\terror: \"8:9: \\\\: invalid input: expected a comma or close brace\",\n\t}, {\n\t\tname:  \"invalid unicode\",\n\t\tinput: \"{\\\"foo\\\"=\\\"\\\\xf0\\\\x9f\\\"}\",\n\t\terror: \"7:17: \\\"\\\\xf0\\\\x9f\\\": invalid input\",\n\t}}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tmatchers, err := Matchers(test.input)\n\t\t\tif test.error != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.error)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, test.expected, matchers)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMatcher(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected *labels.Matcher\n\t\terror    string\n\t}{{\n\t\tname:     \"equals\",\n\t\tinput:    \"{foo=bar}\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\"),\n\t}, {\n\t\tname:     \"equals with trailing comma\",\n\t\tinput:    \"{foo=bar,}\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\"),\n\t}, {\n\t\tname:     \"not equals\",\n\t\tinput:    \"{foo!=bar}\",\n\t\texpected: mustNewMatcher(t, labels.MatchNotEqual, \"foo\", \"bar\"),\n\t}, {\n\t\tname:     \"match regex\",\n\t\tinput:    \"{foo=~[a-z]+}\",\n\t\texpected: mustNewMatcher(t, labels.MatchRegexp, \"foo\", \"[a-z]+\"),\n\t}, {\n\t\tname:     \"doesn't match regex\",\n\t\tinput:    \"{foo!~[a-z]+}\",\n\t\texpected: mustNewMatcher(t, labels.MatchNotRegexp, \"foo\", \"[a-z]+\"),\n\t}, {\n\t\tname:     \"equals unicode emoji\",\n\t\tinput:    \"{foo=🙂}\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"🙂\"),\n\t}, {\n\t\tname:     \"equals unicode emoji as bytes in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"\\\\xf0\\\\x9f\\\\x99\\\\x82\\\"}\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"🙂\"),\n\t}, {\n\t\tname:     \"equals unicode emoji as code points in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"\\\\U0001f642\\\"}\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"🙂\"),\n\t}, {\n\t\tname:     \"equals unicode sentence\",\n\t\tinput:    \"{foo=🙂bar}\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"🙂bar\"),\n\t}, {\n\t\tname:     \"equals without braces\",\n\t\tinput:    \"foo=bar\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\"),\n\t}, {\n\t\tname:     \"equals without braces but with trailing comma\",\n\t\tinput:    \"foo=bar,\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\"),\n\t}, {\n\t\tname:     \"not equals without braces\",\n\t\tinput:    \"foo!=bar\",\n\t\texpected: mustNewMatcher(t, labels.MatchNotEqual, \"foo\", \"bar\"),\n\t}, {\n\t\tname:     \"match regex without braces\",\n\t\tinput:    \"foo=~[a-z]+\",\n\t\texpected: mustNewMatcher(t, labels.MatchRegexp, \"foo\", \"[a-z]+\"),\n\t}, {\n\t\tname:     \"doesn't match regex without braces\",\n\t\tinput:    \"foo!~[a-z]+\",\n\t\texpected: mustNewMatcher(t, labels.MatchNotRegexp, \"foo\", \"[a-z]+\"),\n\t}, {\n\t\tname:     \"equals in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"bar\\\"}\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\"),\n\t}, {\n\t\tname:     \"equals in quotes and with trailing comma\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"bar\\\",}\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\"),\n\t}, {\n\t\tname:     \"not equals in quotes\",\n\t\tinput:    \"{\\\"foo\\\"!=\\\"bar\\\"}\",\n\t\texpected: mustNewMatcher(t, labels.MatchNotEqual, \"foo\", \"bar\"),\n\t}, {\n\t\tname:     \"match regex in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=~\\\"[a-z]+\\\"}\",\n\t\texpected: mustNewMatcher(t, labels.MatchRegexp, \"foo\", \"[a-z]+\"),\n\t}, {\n\t\tname:     \"doesn't match regex in quotes\",\n\t\tinput:    \"{\\\"foo\\\"!~\\\"[a-z]+\\\"}\",\n\t\texpected: mustNewMatcher(t, labels.MatchNotRegexp, \"foo\", \"[a-z]+\"),\n\t}, {\n\t\tname:     \"equals unicode emoji in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"🙂\\\"}\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"🙂\"),\n\t}, {\n\t\tname:     \"equals unicode sentence in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"🙂bar\\\"}\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"🙂bar\"),\n\t}, {\n\t\tname:     \"equals with newline in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"bar\\\\n\\\"}\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\\n\"),\n\t}, {\n\t\tname:     \"equals with tab in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"bar\\\\t\\\"}\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\\t\"),\n\t}, {\n\t\tname:     \"equals with escaped quotes in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"\\\\\\\"bar\\\\\\\"\\\"}\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"\\\"bar\\\"\"),\n\t}, {\n\t\tname:     \"equals with escaped backslash in quotes\",\n\t\tinput:    \"{\\\"foo\\\"=\\\"bar\\\\\\\\\\\"}\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\\\\\"),\n\t}, {\n\t\tname:     \"equals without braces in quotes\",\n\t\tinput:    \"\\\"foo\\\"=\\\"bar\\\"\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\"),\n\t}, {\n\t\tname:     \"equals without braces in quotes with trailing comma\",\n\t\tinput:    \"\\\"foo\\\"=\\\"bar\\\",\",\n\t\texpected: mustNewMatcher(t, labels.MatchEqual, \"foo\", \"bar\"),\n\t}, {\n\t\tname:  \"no input\",\n\t\terror: \"no matchers\",\n\t}, {\n\t\tname:  \"open and closing braces\",\n\t\tinput: \"{}\",\n\t\terror: \"no matchers\",\n\t}, {\n\t\tname:  \"two or more returns error\",\n\t\tinput: \"foo=bar,bar=baz\",\n\t\terror: \"expected 1 matcher, found 2\",\n\t}, {\n\t\tname:  \"invalid unicode\",\n\t\tinput: \"foo=\\\"\\\\xf0\\\\x9f\\\"\",\n\t\terror: \"4:14: \\\"\\\\xf0\\\\x9f\\\": invalid input\",\n\t}}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tmatcher, err := Matcher(test.input)\n\t\t\tif test.error != \"\" {\n\t\t\t\trequire.EqualError(t, err, test.error)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, test.expected, matcher)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc mustNewMatcher(t *testing.T, op labels.MatchType, name, value string) *labels.Matcher {\n\tm, err := labels.NewMatcher(op, name, value)\n\trequire.NoError(t, err)\n\treturn m\n}\n"
  },
  {
    "path": "matcher/parse/token.go",
    "content": "// Copyright 2023 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage parse\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strconv\"\n\t\"unicode/utf8\"\n)\n\ntype tokenKind int\n\nconst (\n\ttokenEOF tokenKind = iota\n\ttokenOpenBrace\n\ttokenCloseBrace\n\ttokenComma\n\ttokenEquals\n\ttokenNotEquals\n\ttokenMatches\n\ttokenNotMatches\n\ttokenQuoted\n\ttokenUnquoted\n)\n\nfunc (k tokenKind) String() string {\n\tswitch k {\n\tcase tokenOpenBrace:\n\t\treturn \"OpenBrace\"\n\tcase tokenCloseBrace:\n\t\treturn \"CloseBrace\"\n\tcase tokenComma:\n\t\treturn \"Comma\"\n\tcase tokenEquals:\n\t\treturn \"Equals\"\n\tcase tokenNotEquals:\n\t\treturn \"NotEquals\"\n\tcase tokenMatches:\n\t\treturn \"Matches\"\n\tcase tokenNotMatches:\n\t\treturn \"NotMatches\"\n\tcase tokenQuoted:\n\t\treturn \"Quoted\"\n\tcase tokenUnquoted:\n\t\treturn \"Unquoted\"\n\tdefault:\n\t\treturn \"EOF\"\n\t}\n}\n\ntype token struct {\n\tkind  tokenKind\n\tvalue string\n\tposition\n}\n\n// isEOF returns true if the token is an end of file token.\nfunc (t token) isEOF() bool {\n\treturn t.kind == tokenEOF\n}\n\n// isOneOf returns true if the token is one of the specified kinds.\nfunc (t token) isOneOf(kinds ...tokenKind) bool {\n\treturn slices.Contains(kinds, t.kind)\n}\n\n// unquote the value in token. If unquoted returns it unmodified.\nfunc (t token) unquote() (string, error) {\n\tif t.kind == tokenQuoted {\n\t\tunquoted, err := strconv.Unquote(t.value)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif !utf8.ValidString(unquoted) {\n\t\t\treturn \"\", errors.New(\"quoted string contains invalid UTF-8 code points\")\n\t\t}\n\t\treturn unquoted, nil\n\t}\n\treturn t.value, nil\n}\n\nfunc (t token) String() string {\n\treturn fmt.Sprintf(\"(%s) '%s'\", t.kind, t.value)\n}\n\ntype position struct {\n\toffsetStart int // The start position in the input.\n\toffsetEnd   int // The end position in the input.\n\tcolumnStart int // The column number.\n\tcolumnEnd   int // The end of the column.\n}\n"
  },
  {
    "path": "nflog/nflog.go",
    "content": "// Copyright 2016 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package nflog implements a garbage-collected and snapshottable append-only log of\n// active/resolved notifications. Each log entry stores the active/resolved state,\n// the notified receiver, and a hash digest of the notification's identifying contents.\n// The log can be queried along different parameters.\npackage nflog\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"maps\"\n\t\"math/rand\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/coder/quartz\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"google.golang.org/protobuf/encoding/protodelim\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/prometheus/alertmanager/cluster\"\n\tpb \"github.com/prometheus/alertmanager/nflog/nflogpb\"\n)\n\n// ErrNotFound is returned for empty query results.\nvar ErrNotFound = errors.New(\"not found\")\n\n// ErrInvalidState is returned if the state isn't valid.\nvar ErrInvalidState = errors.New(\"invalid state\")\n\n// query currently allows filtering by and/or receiver group key.\n// It is configured via QueryParameter functions.\n//\n// TODO(fabxc): Future versions could allow querying a certain receiver,\n// group or a given time interval.\ntype query struct {\n\trecv     *pb.Receiver\n\tgroupKey string\n}\n\n// QueryParam is a function that modifies a query to incorporate\n// a set of parameters. Returns an error for invalid or conflicting\n// parameters.\ntype QueryParam func(*query) error\n\n// QReceiver adds a receiver parameter to a query.\nfunc QReceiver(r *pb.Receiver) QueryParam {\n\treturn func(q *query) error {\n\t\tq.recv = r\n\t\treturn nil\n\t}\n}\n\n// QGroupKey adds a group key as querying argument.\nfunc QGroupKey(gk string) QueryParam {\n\treturn func(q *query) error {\n\t\tq.groupKey = gk\n\t\treturn nil\n\t}\n}\n\n// Store abstracts the NFLog's receiver data storage as a mutable key/value store. A store\n// can be generated from a nflogpb.Entry and then written via the call to Log.\n//\n// Every key in the Store is associated with either an int, float, or string value.\ntype Store struct {\n\tdata map[string]*pb.ReceiverDataValue\n}\n\n// NewStore creates a Store from the entry's receiver data. If entry is nil, the resulting\n// Store is empty.\nfunc NewStore(entry *pb.Entry) *Store {\n\tvar receiverData map[string]*pb.ReceiverDataValue\n\tif entry != nil {\n\t\treceiverData = maps.Clone(entry.ReceiverData)\n\t}\n\tif receiverData == nil {\n\t\treceiverData = make(map[string]*pb.ReceiverDataValue)\n\t}\n\treturn &Store{\n\t\tdata: receiverData,\n\t}\n}\n\n// GetInt finds the integer value associated with the key, if any, and returns it.\nfunc (s *Store) GetInt(key string) (int64, bool) {\n\tdataValue, ok := s.data[key]\n\tif !ok {\n\t\treturn 0, false\n\t}\n\tintVal, ok := dataValue.Value.(*pb.ReceiverDataValue_IntVal)\n\tif !ok {\n\t\treturn 0, false\n\t}\n\treturn intVal.IntVal, true\n}\n\n// GetFloat finds the float value associated with the key, if any, and returns it.\nfunc (s *Store) GetFloat(key string) (float64, bool) {\n\tdataValue, ok := s.data[key]\n\tif !ok {\n\t\treturn 0, false\n\t}\n\tfloatVal, ok := dataValue.Value.(*pb.ReceiverDataValue_DoubleVal)\n\tif !ok {\n\t\treturn 0, false\n\t}\n\treturn floatVal.DoubleVal, true\n}\n\n// GetFloat finds the string value associated with the key, if any, and returns it.\nfunc (s *Store) GetStr(key string) (string, bool) {\n\tdataValue, ok := s.data[key]\n\tif !ok {\n\t\treturn \"\", false\n\t}\n\tstrVal, ok := dataValue.Value.(*pb.ReceiverDataValue_StrVal)\n\tif !ok {\n\t\treturn \"\", false\n\t}\n\treturn strVal.StrVal, true\n}\n\n// SetInt associates an integer value with the provided key, overwriting any existing value.\nfunc (s *Store) SetInt(key string, v int64) {\n\ts.data[key] = &pb.ReceiverDataValue{\n\t\tValue: &pb.ReceiverDataValue_IntVal{\n\t\t\tIntVal: v,\n\t\t},\n\t}\n}\n\n// SetFloat associates a float value with the provided key, overwriting any existing value.\nfunc (s *Store) SetFloat(key string, v float64) {\n\ts.data[key] = &pb.ReceiverDataValue{\n\t\tValue: &pb.ReceiverDataValue_DoubleVal{\n\t\t\tDoubleVal: v,\n\t\t},\n\t}\n}\n\n// SetStr associates a string value with the provided key, overwriting any existing value.\nfunc (s *Store) SetStr(key, v string) {\n\ts.data[key] = &pb.ReceiverDataValue{\n\t\tValue: &pb.ReceiverDataValue_StrVal{\n\t\t\tStrVal: v,\n\t\t},\n\t}\n}\n\n// Delete deletes any value associated with the key.\nfunc (s *Store) Delete(key string) {\n\tdelete(s.data, key)\n}\n\n// Log holds the notification log state for alerts that have been notified.\ntype Log struct {\n\tclock quartz.Clock\n\n\tlogger    *slog.Logger\n\tmetrics   *metrics\n\tretention time.Duration\n\n\t// For now we only store the most recently added log entry.\n\t// The key is a serialized concatenation of group key and receiver.\n\tmtx       sync.RWMutex\n\tst        state\n\tbroadcast func([]byte)\n}\n\n// MaintenanceFunc represents the function to run as part of the periodic maintenance for the nflog.\n// It returns the size of the snapshot taken or an error if it failed.\ntype MaintenanceFunc func() (int64, error)\n\ntype metrics struct {\n\tgcDuration              prometheus.Summary\n\tsnapshotDuration        prometheus.Summary\n\tsnapshotSize            prometheus.Gauge\n\tqueriesTotal            prometheus.Counter\n\tqueryErrorsTotal        prometheus.Counter\n\tqueryDuration           prometheus.Histogram\n\tpropagatedMessagesTotal prometheus.Counter\n\tmaintenanceTotal        prometheus.Counter\n\tmaintenanceErrorsTotal  prometheus.Counter\n}\n\nfunc newMetrics(r prometheus.Registerer) *metrics {\n\tm := &metrics{}\n\n\tm.gcDuration = promauto.With(r).NewSummary(prometheus.SummaryOpts{\n\t\tName:       \"alertmanager_nflog_gc_duration_seconds\",\n\t\tHelp:       \"Duration of the last notification log garbage collection cycle.\",\n\t\tObjectives: map[float64]float64{},\n\t})\n\tm.snapshotDuration = promauto.With(r).NewSummary(prometheus.SummaryOpts{\n\t\tName:       \"alertmanager_nflog_snapshot_duration_seconds\",\n\t\tHelp:       \"Duration of the last notification log snapshot.\",\n\t\tObjectives: map[float64]float64{},\n\t})\n\tm.snapshotSize = promauto.With(r).NewGauge(prometheus.GaugeOpts{\n\t\tName: \"alertmanager_nflog_snapshot_size_bytes\",\n\t\tHelp: \"Size of the last notification log snapshot in bytes.\",\n\t})\n\tm.maintenanceTotal = promauto.With(r).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_nflog_maintenance_total\",\n\t\tHelp: \"How many maintenances were executed for the notification log.\",\n\t})\n\tm.maintenanceErrorsTotal = promauto.With(r).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_nflog_maintenance_errors_total\",\n\t\tHelp: \"How many maintenances were executed for the notification log that failed.\",\n\t})\n\tm.queriesTotal = promauto.With(r).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_nflog_queries_total\",\n\t\tHelp: \"Number of notification log queries were received.\",\n\t})\n\tm.queryErrorsTotal = promauto.With(r).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_nflog_query_errors_total\",\n\t\tHelp: \"Number notification log received queries that failed.\",\n\t})\n\tm.queryDuration = promauto.With(r).NewHistogram(prometheus.HistogramOpts{\n\t\tName:                            \"alertmanager_nflog_query_duration_seconds\",\n\t\tHelp:                            \"Duration of notification log query evaluation.\",\n\t\tBuckets:                         prometheus.DefBuckets,\n\t\tNativeHistogramBucketFactor:     1.1,\n\t\tNativeHistogramMaxBucketNumber:  100,\n\t\tNativeHistogramMinResetDuration: 1 * time.Hour,\n\t})\n\tm.propagatedMessagesTotal = promauto.With(r).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_nflog_gossip_messages_propagated_total\",\n\t\tHelp: \"Number of received gossip messages that have been further gossiped.\",\n\t})\n\n\treturn m\n}\n\ntype state map[string]*pb.MeshEntry\n\nfunc (s state) clone() state {\n\tc := make(state, len(s))\n\tmaps.Copy(c, s)\n\treturn c\n}\n\n// merge returns true or false whether the MeshEntry was merged or\n// not. This information is used to decide to gossip the message further.\nfunc (s state) merge(e *pb.MeshEntry, now time.Time) bool {\n\tif e.ExpiresAt.AsTime().Before(now) {\n\t\treturn false\n\t}\n\tk := stateKey(string(e.Entry.GroupKey), e.Entry.Receiver)\n\n\tprev, ok := s[k]\n\tif !ok || prev.Entry.Timestamp.AsTime().Before(e.Entry.Timestamp.AsTime()) {\n\t\ts[k] = e\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (s state) MarshalBinary() ([]byte, error) {\n\tvar buf bytes.Buffer\n\n\tfor _, e := range s {\n\t\tif _, err := protodelim.MarshalTo(&buf, e); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn buf.Bytes(), nil\n}\n\nfunc decodeState(r io.Reader) (state, error) {\n\tst := state{}\n\tbr := bufio.NewReader(r)\n\tfor {\n\t\tvar e pb.MeshEntry\n\t\terr := protodelim.UnmarshalFrom(br, &e)\n\t\tif err == nil {\n\t\t\tif e.Entry == nil || e.Entry.Receiver == nil {\n\t\t\t\treturn nil, ErrInvalidState\n\t\t\t}\n\t\t\tst[stateKey(string(e.Entry.GroupKey), e.Entry.Receiver)] = &e\n\t\t\tcontinue\n\t\t}\n\t\tif errors.Is(err, io.EOF) {\n\t\t\tbreak\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn st, nil\n}\n\nfunc marshalMeshEntry(e *pb.MeshEntry) ([]byte, error) {\n\tvar buf bytes.Buffer\n\tif _, err := protodelim.MarshalTo(&buf, e); err != nil {\n\t\treturn nil, err\n\t}\n\treturn buf.Bytes(), nil\n}\n\n// Options configures a new Log implementation.\ntype Options struct {\n\tSnapshotReader io.Reader\n\tSnapshotFile   string\n\n\tRetention time.Duration\n\n\tLogger  *slog.Logger\n\tMetrics prometheus.Registerer\n}\n\nfunc (o *Options) validate() error {\n\tif o.SnapshotFile != \"\" && o.SnapshotReader != nil {\n\t\treturn errors.New(\"only one of SnapshotFile and SnapshotReader must be set\")\n\t}\n\n\tif o.Metrics == nil {\n\t\treturn errors.New(\"missing prometheus.Registerer\")\n\t}\n\n\treturn nil\n}\n\n// New creates a new notification log based on the provided options.\n// The snapshot is loaded into the Log if it is set.\nfunc New(o Options) (*Log, error) {\n\tif err := o.validate(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tl := &Log{\n\t\tclock:     quartz.NewReal(),\n\t\tretention: o.Retention,\n\t\tlogger:    promslog.NewNopLogger(),\n\t\tst:        state{},\n\t\tbroadcast: func([]byte) {},\n\t\tmetrics:   newMetrics(o.Metrics),\n\t}\n\n\tif o.Logger != nil {\n\t\tl.logger = o.Logger\n\t}\n\n\tif o.SnapshotFile != \"\" {\n\t\tif r, err := os.Open(o.SnapshotFile); err != nil {\n\t\t\tif !os.IsNotExist(err) {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tl.logger.Debug(\"notification log snapshot file doesn't exist\", \"err\", err)\n\t\t} else {\n\t\t\to.SnapshotReader = r\n\t\t\tdefer r.Close()\n\t\t}\n\t}\n\n\tif o.SnapshotReader != nil {\n\t\tif err := l.loadSnapshot(o.SnapshotReader); err != nil {\n\t\t\treturn l, err\n\t\t}\n\t}\n\n\treturn l, nil\n}\n\nfunc (l *Log) now() time.Time {\n\treturn l.clock.Now()\n}\n\n// Maintenance garbage collects the notification log state at the given interval. If the snapshot\n// file is set, a snapshot is written to it afterwards.\n// Terminates on receiving from stopc.\n// If not nil, the last argument is an override for what to do as part of the maintenance - for advanced usage.\nfunc (l *Log) Maintenance(interval time.Duration, snapf string, stopc <-chan struct{}, override MaintenanceFunc) {\n\tif interval == 0 || stopc == nil {\n\t\tl.logger.Error(\"interval or stop signal are missing - not running maintenance\")\n\t\treturn\n\t}\n\tt := l.clock.NewTicker(interval)\n\tdefer t.Stop()\n\n\tvar doMaintenance MaintenanceFunc\n\tdoMaintenance = func() (int64, error) {\n\t\tvar size int64\n\t\tif _, err := l.GC(); err != nil {\n\t\t\treturn size, err\n\t\t}\n\t\tif snapf == \"\" {\n\t\t\treturn size, nil\n\t\t}\n\t\tf, err := openReplace(snapf)\n\t\tif err != nil {\n\t\t\treturn size, err\n\t\t}\n\t\tif size, err = l.Snapshot(f); err != nil {\n\t\t\tf.Close()\n\t\t\treturn size, err\n\t\t}\n\t\treturn size, f.Close()\n\t}\n\n\tif override != nil {\n\t\tdoMaintenance = override\n\t}\n\n\trunMaintenance := func(do func() (int64, error)) error {\n\t\tl.metrics.maintenanceTotal.Inc()\n\t\tstart := l.now().UTC()\n\t\tl.logger.Debug(\"Running maintenance\")\n\t\tsize, err := do()\n\t\tl.metrics.snapshotSize.Set(float64(size))\n\t\tif err != nil {\n\t\t\tl.metrics.maintenanceErrorsTotal.Inc()\n\t\t\treturn err\n\t\t}\n\t\tl.logger.Debug(\"Maintenance done\", \"duration\", l.now().Sub(start), \"size\", size)\n\t\treturn nil\n\t}\n\nLoop:\n\tfor {\n\t\tselect {\n\t\tcase <-stopc:\n\t\t\tbreak Loop\n\t\tcase <-t.C:\n\t\t\tif err := runMaintenance(doMaintenance); err != nil {\n\t\t\t\tl.logger.Error(\"Running maintenance failed\", \"err\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\t// No need to run final maintenance if we don't want to snapshot.\n\tif snapf == \"\" {\n\t\treturn\n\t}\n\tif err := runMaintenance(doMaintenance); err != nil {\n\t\tl.logger.Error(\"Creating shutdown snapshot failed\", \"err\", err)\n\t}\n}\n\nfunc receiverKey(r *pb.Receiver) string {\n\treturn fmt.Sprintf(\"%s/%s/%d\", r.GroupName, r.Integration, r.Idx)\n}\n\n// stateKey returns a string key for a log entry consisting of the group key\n// and receiver.\nfunc stateKey(k string, r *pb.Receiver) string {\n\treturn fmt.Sprintf(\"%s:%s\", k, receiverKey(r))\n}\n\nfunc (l *Log) Log(r *pb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64, store *Store, expiry time.Duration) error {\n\t// Write all st with the same timestamp.\n\tnow := l.now()\n\tkey := stateKey(gkey, r)\n\n\tl.mtx.Lock()\n\tdefer l.mtx.Unlock()\n\n\tif prevle, ok := l.st[key]; ok {\n\t\t// Entry already exists, only overwrite if timestamp is newer.\n\t\t// This may happen with raciness or clock-drift across AM nodes.\n\t\tif prevle.Entry.Timestamp.AsTime().After(now) {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\texpiresAt := now.Add(l.retention)\n\tif expiry > 0 && l.retention > expiry {\n\t\texpiresAt = now.Add(expiry)\n\t}\n\n\tvar receiverData map[string]*pb.ReceiverDataValue\n\tif store != nil {\n\t\treceiverData = store.data\n\t}\n\n\te := &pb.MeshEntry{\n\t\tEntry: &pb.Entry{\n\t\t\tReceiver:       r,\n\t\t\tGroupKey:       []byte(gkey),\n\t\t\tTimestamp:      timestamppb.New(now),\n\t\t\tFiringAlerts:   firingAlerts,\n\t\t\tResolvedAlerts: resolvedAlerts,\n\t\t\tReceiverData:   receiverData,\n\t\t},\n\t\tExpiresAt: timestamppb.New(expiresAt),\n\t}\n\n\tb, err := marshalMeshEntry(e)\n\tif err != nil {\n\t\treturn err\n\t}\n\tl.st.merge(e, l.now())\n\tl.broadcast(b)\n\n\treturn nil\n}\n\n// GC implements the Log interface.\nfunc (l *Log) GC() (int, error) {\n\tstart := time.Now()\n\tdefer func() { l.metrics.gcDuration.Observe(time.Since(start).Seconds()) }()\n\n\tnow := l.now()\n\tvar n int\n\n\tl.mtx.Lock()\n\tdefer l.mtx.Unlock()\n\n\tfor k, le := range l.st {\n\t\tif le.ExpiresAt.AsTime().IsZero() {\n\t\t\treturn n, errors.New(\"unexpected zero expiration timestamp\")\n\t\t}\n\t\tif !le.ExpiresAt.AsTime().After(now) {\n\t\t\tdelete(l.st, k)\n\t\t\tn++\n\t\t}\n\t}\n\n\treturn n, nil\n}\n\n// Query implements the Log interface.\nfunc (l *Log) Query(params ...QueryParam) ([]*pb.Entry, error) {\n\tstart := time.Now()\n\tl.metrics.queriesTotal.Inc()\n\n\tentries, err := func() ([]*pb.Entry, error) {\n\t\tq := &query{}\n\t\tfor _, p := range params {\n\t\t\tif err := p(q); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\t// TODO(fabxc): For now our only query mode is the most recent entry for a\n\t\t// receiver/group_key combination.\n\t\tif q.recv == nil || q.groupKey == \"\" {\n\t\t\t// TODO(fabxc): allow more complex queries in the future.\n\t\t\t// How to enable pagination?\n\t\t\treturn nil, errors.New(\"no query parameters specified\")\n\t\t}\n\n\t\tl.mtx.RLock()\n\t\tdefer l.mtx.RUnlock()\n\n\t\tif le, ok := l.st[stateKey(q.groupKey, q.recv)]; ok {\n\t\t\treturn []*pb.Entry{le.Entry}, nil\n\t\t}\n\t\treturn nil, ErrNotFound\n\t}()\n\tif err != nil {\n\t\tl.metrics.queryErrorsTotal.Inc()\n\t}\n\tl.metrics.queryDuration.Observe(time.Since(start).Seconds())\n\treturn entries, err\n}\n\n// loadSnapshot loads a snapshot generated by Snapshot() into the state.\nfunc (l *Log) loadSnapshot(r io.Reader) error {\n\tst, err := decodeState(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tl.mtx.Lock()\n\tl.st = st\n\tl.mtx.Unlock()\n\n\treturn nil\n}\n\n// Snapshot implements the Log interface.\nfunc (l *Log) Snapshot(w io.Writer) (int64, error) {\n\tstart := time.Now()\n\tdefer func() { l.metrics.snapshotDuration.Observe(time.Since(start).Seconds()) }()\n\n\tl.mtx.RLock()\n\tdefer l.mtx.RUnlock()\n\n\tb, err := l.st.MarshalBinary()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn io.Copy(w, bytes.NewReader(b))\n}\n\n// MarshalBinary serializes all contents of the notification log.\nfunc (l *Log) MarshalBinary() ([]byte, error) {\n\tl.mtx.Lock()\n\tdefer l.mtx.Unlock()\n\n\treturn l.st.MarshalBinary()\n}\n\n// Merge merges notification log state received from the cluster with the local state.\nfunc (l *Log) Merge(b []byte) error {\n\tst, err := decodeState(bytes.NewReader(b))\n\tif err != nil {\n\t\treturn err\n\t}\n\tl.mtx.Lock()\n\tdefer l.mtx.Unlock()\n\tnow := l.now()\n\n\tfor _, e := range st {\n\t\tif merged := l.st.merge(e, now); merged && !cluster.OversizedMessage(b) {\n\t\t\t// If this is the first we've seen the message and it's\n\t\t\t// not oversized, gossip it to other nodes. We don't\n\t\t\t// propagate oversized messages because they're sent to\n\t\t\t// all nodes already.\n\t\t\tl.broadcast(b)\n\t\t\tl.metrics.propagatedMessagesTotal.Inc()\n\t\t\tl.logger.Debug(\"gossiping new entry\", \"entry\", e)\n\t\t}\n\t}\n\treturn nil\n}\n\n// SetBroadcast sets a broadcast callback that will be invoked with serialized state\n// on updates.\nfunc (l *Log) SetBroadcast(f func([]byte)) {\n\tl.mtx.Lock()\n\tl.broadcast = f\n\tl.mtx.Unlock()\n}\n\n// replaceFile wraps a file that is moved to another filename on closing.\ntype replaceFile struct {\n\t*os.File\n\tfilename string\n}\n\nfunc (f *replaceFile) Close() error {\n\tif err := f.Sync(); err != nil {\n\t\treturn err\n\t}\n\tif err := f.File.Close(); err != nil {\n\t\treturn err\n\t}\n\treturn os.Rename(f.Name(), f.filename)\n}\n\n// openReplace opens a new temporary file that is moved to filename on closing.\nfunc openReplace(filename string) (*replaceFile, error) {\n\ttmpFilename := fmt.Sprintf(\"%s.%x\", filename, uint64(rand.Int63()))\n\n\tf, err := os.Create(tmpFilename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trf := &replaceFile{\n\t\tFile:     f,\n\t\tfilename: filename,\n\t}\n\treturn rf, nil\n}\n"
  },
  {
    "path": "nflog/nflog_test.go",
    "content": "// Copyright 2016 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage nflog\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\tpb \"github.com/prometheus/alertmanager/nflog/nflogpb\"\n\n\t\"github.com/coder/quartz\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/testutil\"\n\t\"github.com/stretchr/testify/require\"\n\t\"google.golang.org/protobuf/proto\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n)\n\nfunc TestLogGC(t *testing.T) {\n\tmockClock := quartz.NewMock(t)\n\tnow := mockClock.Now()\n\t// We only care about key names and expiration timestamps.\n\tnewEntry := func(ts time.Time) *pb.MeshEntry {\n\t\treturn &pb.MeshEntry{\n\t\t\tExpiresAt: timestamppb.New(ts),\n\t\t}\n\t}\n\n\tl := &Log{\n\t\tst: state{\n\t\t\t\"a1\": newEntry(now),\n\t\t\t\"a2\": newEntry(now.Add(time.Second)),\n\t\t\t\"a3\": newEntry(now.Add(-time.Second)),\n\t\t},\n\t\tclock:   mockClock,\n\t\tmetrics: newMetrics(prometheus.NewRegistry()),\n\t}\n\tn, err := l.GC()\n\trequire.NoError(t, err, \"unexpected error in garbage collection\")\n\trequire.Equal(t, 2, n, \"unexpected number of removed entries\")\n\n\texpected := state{\n\t\t\"a2\": newEntry(now.Add(time.Second)),\n\t}\n\trequire.Equal(t, expected, l.st, \"unexpected state after garbage collection\")\n}\n\nfunc TestLogSnapshot(t *testing.T) {\n\t// Check whether storing and loading the snapshot is symmetric.\n\tmockClock := quartz.NewMock(t)\n\tnow := mockClock.Now().UTC()\n\n\tcases := []struct {\n\t\tentries []*pb.MeshEntry\n\t}{\n\t\t{\n\t\t\tentries: []*pb.MeshEntry{\n\t\t\t\t{\n\t\t\t\t\tEntry: &pb.Entry{\n\t\t\t\t\t\tGroupKey:  []byte(\"d8e8fca2dc0f896fd7cb4cb0031ba249\"),\n\t\t\t\t\t\tReceiver:  &pb.Receiver{GroupName: \"abc\", Integration: \"test1\", Idx: 1},\n\t\t\t\t\t\tGroupHash: []byte(\"126a8a51b9d1bbd07fddc65819a542c3\"),\n\t\t\t\t\t\tResolved:  false,\n\t\t\t\t\t\tTimestamp: timestamppb.New(now),\n\t\t\t\t\t},\n\t\t\t\t\tExpiresAt: timestamppb.New(now),\n\t\t\t\t}, {\n\t\t\t\t\tEntry: &pb.Entry{\n\t\t\t\t\t\tGroupKey:  []byte(\"d8e8fca2dc0f8abce7cb4cb0031ba249\"),\n\t\t\t\t\t\tReceiver:  &pb.Receiver{GroupName: \"def\", Integration: \"test2\", Idx: 29},\n\t\t\t\t\t\tGroupHash: []byte(\"122c2331b9d1bbd07fddc65819a542c3\"),\n\t\t\t\t\t\tResolved:  true,\n\t\t\t\t\t\tTimestamp: timestamppb.New(now),\n\t\t\t\t\t},\n\t\t\t\t\tExpiresAt: timestamppb.New(now),\n\t\t\t\t}, {\n\t\t\t\t\tEntry: &pb.Entry{\n\t\t\t\t\t\tGroupKey:  []byte(\"aaaaaca2dc0f896fd7cb4cb0031ba249\"),\n\t\t\t\t\t\tReceiver:  &pb.Receiver{GroupName: \"ghi\", Integration: \"test3\", Idx: 0},\n\t\t\t\t\t\tGroupHash: []byte(\"126a8a51b9d1bbd07fddc6e3e3e542c3\"),\n\t\t\t\t\t\tResolved:  false,\n\t\t\t\t\t\tTimestamp: timestamppb.New(now),\n\t\t\t\t\t},\n\t\t\t\t\tExpiresAt: timestamppb.New(now),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tf, err := os.CreateTemp(t.TempDir(), \"snapshot\")\n\t\trequire.NoError(t, err, \"creating temp file failed\")\n\n\t\tl1 := &Log{\n\t\t\tst:      state{},\n\t\t\tmetrics: newMetrics(nil),\n\t\t}\n\t\t// Setup internal state manually.\n\t\tfor _, e := range c.entries {\n\t\t\tl1.st[stateKey(string(e.Entry.GroupKey), e.Entry.Receiver)] = e\n\t\t}\n\t\t_, err = l1.Snapshot(f)\n\t\trequire.NoError(t, err, \"creating snapshot failed\")\n\t\trequire.NoError(t, f.Close(), \"closing snapshot file failed\")\n\n\t\tf, err = os.Open(f.Name())\n\t\trequire.NoError(t, err, \"opening snapshot file failed\")\n\n\t\t// Check again against new nlog instance.\n\t\tl2 := &Log{}\n\t\terr = l2.loadSnapshot(f)\n\t\trequire.NoError(t, err, \"error loading snapshot\")\n\n\t\tfor id, expected := range l1.st {\n\t\t\tactual, ok := l2.st[id]\n\t\t\trequire.True(t, ok, \"silence %s missing from decoded state\", id)\n\t\t\trequire.True(t, proto.Equal(expected, actual), \"silence %s mismatch after decoding\", id)\n\t\t}\n\n\t\trequire.NoError(t, f.Close(), \"closing snapshot file failed\")\n\t}\n}\n\nfunc TestWithMaintenance_SupportsCustomCallback(t *testing.T) {\n\tf, err := os.CreateTemp(t.TempDir(), \"snapshot\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\tstopc := make(chan struct{})\n\treg := prometheus.NewPedanticRegistry()\n\topts := Options{\n\t\tMetrics:      reg,\n\t\tSnapshotFile: f.Name(),\n\t}\n\n\tl, err := New(opts)\n\tclock := quartz.NewMock(t)\n\tl.clock = clock\n\trequire.NoError(t, err)\n\n\tvar calls atomic.Int32\n\tvar wg sync.WaitGroup\n\n\twg.Go(func() {\n\t\tl.Maintenance(100*time.Millisecond, f.Name(), stopc, func() (int64, error) {\n\t\t\tcalls.Add(1)\n\t\t\treturn 0, nil\n\t\t})\n\t})\n\tgosched()\n\n\t// Before the first tick, no maintenance executed.\n\tclock.Advance(99 * time.Millisecond)\n\trequire.EqualValues(t, 0, calls.Load())\n\n\t// Tick once.\n\tclock.Advance(1 * time.Millisecond)\n\trequire.Eventually(t, func() bool { return calls.Load() == 1 }, 5*time.Second, time.Second)\n\n\t// Stop the maintenance loop. We should get exactly one more execution of the maintenance func.\n\tclose(stopc)\n\twg.Wait()\n\n\trequire.EqualValues(t, 2, calls.Load())\n\t// Check the maintenance metrics.\n\trequire.NoError(t, testutil.GatherAndCompare(reg, bytes.NewBufferString(`\n# HELP alertmanager_nflog_maintenance_errors_total How many maintenances were executed for the notification log that failed.\n# TYPE alertmanager_nflog_maintenance_errors_total counter\nalertmanager_nflog_maintenance_errors_total 0\n# HELP alertmanager_nflog_maintenance_total How many maintenances were executed for the notification log.\n# TYPE alertmanager_nflog_maintenance_total counter\nalertmanager_nflog_maintenance_total 2\n`), \"alertmanager_nflog_maintenance_total\", \"alertmanager_nflog_maintenance_errors_total\"))\n}\n\nfunc TestReplaceFile(t *testing.T) {\n\tdir, err := os.MkdirTemp(\"\", \"replace_file\")\n\trequire.NoError(t, err, \"creating temp dir failed\")\n\n\torigFilename := filepath.Join(dir, \"testfile\")\n\n\tof, err := os.Create(origFilename)\n\trequire.NoError(t, err, \"creating file failed\")\n\n\tnf, err := openReplace(origFilename)\n\trequire.NoError(t, err, \"opening replacement file failed\")\n\n\t_, err = nf.Write([]byte(\"test\"))\n\trequire.NoError(t, err, \"writing replace file failed\")\n\n\trequire.NotEqual(t, nf.Name(), of.Name(), \"replacement file must have different name while editing\")\n\trequire.NoError(t, nf.Close(), \"closing replacement file failed\")\n\trequire.NoError(t, of.Close(), \"closing original file failed\")\n\n\tofr, err := os.Open(origFilename)\n\trequire.NoError(t, err, \"opening original file failed\")\n\tdefer ofr.Close()\n\n\tres, err := io.ReadAll(ofr)\n\trequire.NoError(t, err, \"reading original file failed\")\n\trequire.Equal(t, \"test\", string(res), \"unexpected file contents\")\n}\n\nfunc TestStateMerge(t *testing.T) {\n\tmockClock := quartz.NewMock(t)\n\tnow := mockClock.Now()\n\n\t// We only care about key names and timestamps for the\n\t// merging logic.\n\tnewEntry := func(name string, ts, exp time.Time) *pb.MeshEntry {\n\t\treturn &pb.MeshEntry{\n\t\t\tEntry: &pb.Entry{\n\t\t\t\tTimestamp: timestamppb.New(ts),\n\t\t\t\tGroupKey:  []byte(\"key\"),\n\t\t\t\tReceiver: &pb.Receiver{\n\t\t\t\t\tGroupName:   name,\n\t\t\t\t\tIdx:         1,\n\t\t\t\t\tIntegration: \"integr\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tExpiresAt: timestamppb.New(exp),\n\t\t}\n\t}\n\n\texp := now.Add(time.Minute)\n\n\tcases := []struct {\n\t\ta, b  state\n\t\tfinal state\n\t}{\n\t\t{\n\t\t\ta: state{\n\t\t\t\t\"key:a1/integr/1\": newEntry(\"a1\", now, exp),\n\t\t\t\t\"key:a2/integr/1\": newEntry(\"a2\", now, exp),\n\t\t\t\t\"key:a3/integr/1\": newEntry(\"a3\", now, exp),\n\t\t\t},\n\t\t\tb: state{\n\t\t\t\t\"key:b1/integr/1\": newEntry(\"b1\", now, exp),                                          // new key, should be added\n\t\t\t\t\"key:b2/integr/1\": newEntry(\"b2\", now.Add(-time.Minute), now.Add(-time.Millisecond)), // new key, expired, should not be added\n\t\t\t\t\"key:a2/integr/1\": newEntry(\"a2\", now.Add(-time.Minute), exp),                        // older timestamp, should be dropped\n\t\t\t\t\"key:a3/integr/1\": newEntry(\"a3\", now.Add(time.Minute), exp),                         // newer timestamp, should overwrite\n\t\t\t},\n\t\t\tfinal: state{\n\t\t\t\t\"key:a1/integr/1\": newEntry(\"a1\", now, exp),\n\t\t\t\t\"key:a2/integr/1\": newEntry(\"a2\", now, exp),\n\t\t\t\t\"key:a3/integr/1\": newEntry(\"a3\", now.Add(time.Minute), exp),\n\t\t\t\t\"key:b1/integr/1\": newEntry(\"b1\", now, exp),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tca, cb := c.a.clone(), c.b.clone()\n\n\t\tres := c.a.clone()\n\t\tfor _, e := range cb {\n\t\t\tres.merge(e, now)\n\t\t}\n\t\trequire.Equal(t, c.final, res, \"Merge result should match expectation\")\n\t\trequire.Equal(t, c.b, cb, \"Merged state should remain unmodified\")\n\t\trequire.NotEqual(t, c.final, ca, \"Merge should not change original state\")\n\t}\n}\n\nfunc TestStateDataCoding(t *testing.T) {\n\t// Check whether encoding and decoding the data is symmetric.\n\tmockClock := quartz.NewMock(t)\n\tnow := mockClock.Now().UTC()\n\n\tcases := []struct {\n\t\tentries []*pb.MeshEntry\n\t}{\n\t\t{\n\t\t\tentries: []*pb.MeshEntry{\n\t\t\t\t{\n\t\t\t\t\tEntry: &pb.Entry{\n\t\t\t\t\t\tGroupKey:  []byte(\"d8e8fca2dc0f896fd7cb4cb0031ba249\"),\n\t\t\t\t\t\tReceiver:  &pb.Receiver{GroupName: \"abc\", Integration: \"test1\", Idx: 1},\n\t\t\t\t\t\tGroupHash: []byte(\"126a8a51b9d1bbd07fddc65819a542c3\"),\n\t\t\t\t\t\tResolved:  false,\n\t\t\t\t\t\tTimestamp: timestamppb.New(now),\n\t\t\t\t\t},\n\t\t\t\t\tExpiresAt: timestamppb.New(now),\n\t\t\t\t}, {\n\t\t\t\t\tEntry: &pb.Entry{\n\t\t\t\t\t\tGroupKey:  []byte(\"d8e8fca2dc0f8abce7cb4cb0031ba249\"),\n\t\t\t\t\t\tReceiver:  &pb.Receiver{GroupName: \"def\", Integration: \"test2\", Idx: 29},\n\t\t\t\t\t\tGroupHash: []byte(\"122c2331b9d1bbd07fddc65819a542c3\"),\n\t\t\t\t\t\tResolved:  true,\n\t\t\t\t\t\tTimestamp: timestamppb.New(now),\n\t\t\t\t\t},\n\t\t\t\t\tExpiresAt: timestamppb.New(now),\n\t\t\t\t}, {\n\t\t\t\t\tEntry: &pb.Entry{\n\t\t\t\t\t\tGroupKey:  []byte(\"aaaaaca2dc0f896fd7cb4cb0031ba249\"),\n\t\t\t\t\t\tReceiver:  &pb.Receiver{GroupName: \"ghi\", Integration: \"test3\", Idx: 0},\n\t\t\t\t\t\tGroupHash: []byte(\"126a8a51b9d1bbd07fddc6e3e3e542c3\"),\n\t\t\t\t\t\tResolved:  false,\n\t\t\t\t\t\tTimestamp: timestamppb.New(now),\n\t\t\t\t\t},\n\t\t\t\t\tExpiresAt: timestamppb.New(now),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\t// Create gossip data from input.\n\t\tin := state{}\n\t\tfor _, e := range c.entries {\n\t\t\tin[stateKey(string(e.Entry.GroupKey), e.Entry.Receiver)] = e\n\t\t}\n\t\tmsg, err := in.MarshalBinary()\n\t\trequire.NoError(t, err)\n\n\t\tout, err := decodeState(bytes.NewReader(msg))\n\t\trequire.NoError(t, err, \"decoding message failed\")\n\n\t\tfor id, expected := range in {\n\t\t\tactual, ok := out[id]\n\t\t\trequire.True(t, ok, \"silence %s missing from decoded state\", id)\n\t\t\trequire.True(t, proto.Equal(expected, actual), \"silence %s mismatch after decoding\", id)\n\t\t}\n\t}\n}\n\nfunc TestQuery(t *testing.T) {\n\topts := Options{Metrics: prometheus.NewRegistry(), Retention: time.Second}\n\tnl, err := New(opts)\n\tif err != nil {\n\t\trequire.NoError(t, err, \"constructing nflog failed\")\n\t}\n\n\trecv := new(pb.Receiver)\n\n\t// no key param\n\t_, err = nl.Query(QGroupKey(\"key\"))\n\trequire.EqualError(t, err, \"no query parameters specified\")\n\n\t// no recv param\n\t_, err = nl.Query(QReceiver(recv))\n\trequire.EqualError(t, err, \"no query parameters specified\")\n\n\t// no entry\n\t_, err = nl.Query(QGroupKey(\"nonexistentkey\"), QReceiver(recv))\n\trequire.EqualError(t, err, \"not found\")\n\n\t// existing entry\n\tfiringAlerts := []uint64{1, 2, 3}\n\tresolvedAlerts := []uint64{4, 5}\n\n\terr = nl.Log(recv, \"key\", firingAlerts, resolvedAlerts, nil, 0)\n\trequire.NoError(t, err, \"logging notification failed\")\n\n\tentries, err := nl.Query(QGroupKey(\"key\"), QReceiver(recv))\n\trequire.NoError(t, err, \"querying nflog failed\")\n\tentry := entries[0]\n\trequire.Equal(t, firingAlerts, entry.FiringAlerts)\n\trequire.Equal(t, resolvedAlerts, entry.ResolvedAlerts)\n}\n\nfunc TestStateDecodingError(t *testing.T) {\n\t// Check whether decoding copes with erroneous data.\n\ts := state{\"\": &pb.MeshEntry{}}\n\n\tmsg, err := s.MarshalBinary()\n\trequire.NoError(t, err)\n\n\t_, err = decodeState(bytes.NewReader(msg))\n\trequire.Equal(t, ErrInvalidState, err)\n}\n\n// runtime.Gosched() does not \"suspend\" the current goroutine so there's no guarantee that the main goroutine won't\n// be able to continue. For more see https://pkg.go.dev/runtime#Gosched.\nfunc gosched() {\n\ttime.Sleep(1 * time.Millisecond)\n}\n"
  },
  {
    "path": "nflog/nflogpb/nflog.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: nflog.proto\n\npackage nflogpb\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype Receiver struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Configured name of the receiver group.\n\tGroupName string `protobuf:\"bytes,1,opt,name=group_name,json=groupName,proto3\" json:\"group_name,omitempty\"`\n\t// Name of the integration of the receiver.\n\tIntegration string `protobuf:\"bytes,2,opt,name=integration,proto3\" json:\"integration,omitempty\"`\n\t// Index of the receiver with respect to the integration.\n\t// Every integration in a group may have 0..N configurations.\n\tIdx           uint32 `protobuf:\"varint,3,opt,name=idx,proto3\" json:\"idx,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Receiver) Reset() {\n\t*x = Receiver{}\n\tmi := &file_nflog_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Receiver) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Receiver) ProtoMessage() {}\n\nfunc (x *Receiver) ProtoReflect() protoreflect.Message {\n\tmi := &file_nflog_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Receiver.ProtoReflect.Descriptor instead.\nfunc (*Receiver) Descriptor() ([]byte, []int) {\n\treturn file_nflog_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Receiver) GetGroupName() string {\n\tif x != nil {\n\t\treturn x.GroupName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Receiver) GetIntegration() string {\n\tif x != nil {\n\t\treturn x.Integration\n\t}\n\treturn \"\"\n}\n\nfunc (x *Receiver) GetIdx() uint32 {\n\tif x != nil {\n\t\treturn x.Idx\n\t}\n\treturn 0\n}\n\n// Entry holds information about a successful notification\n// sent to a receiver.\ntype Entry struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The key identifying the dispatching group.\n\tGroupKey []byte `protobuf:\"bytes,1,opt,name=group_key,json=groupKey,proto3\" json:\"group_key,omitempty\"`\n\t// The receiver that was notified.\n\tReceiver *Receiver `protobuf:\"bytes,2,opt,name=receiver,proto3\" json:\"receiver,omitempty\"`\n\t// Hash over the state of the group at notification time.\n\t// Deprecated in favor of FiringAlerts field, but kept for compatibility.\n\tGroupHash []byte `protobuf:\"bytes,3,opt,name=group_hash,json=groupHash,proto3\" json:\"group_hash,omitempty\"`\n\t// Whether the notification was about a resolved alert.\n\t// Deprecated in favor of ResolvedAlerts field, but kept for compatibility.\n\tResolved bool `protobuf:\"varint,4,opt,name=resolved,proto3\" json:\"resolved,omitempty\"`\n\t// Timestamp of the succeeding notification.\n\tTimestamp *timestamppb.Timestamp `protobuf:\"bytes,5,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\t// FiringAlerts list of hashes of firing alerts at the last notification time.\n\tFiringAlerts []uint64 `protobuf:\"varint,6,rep,packed,name=firing_alerts,json=firingAlerts,proto3\" json:\"firing_alerts,omitempty\"`\n\t// ResolvedAlerts list of hashes of resolved alerts at the last notification time.\n\tResolvedAlerts []uint64 `protobuf:\"varint,7,rep,packed,name=resolved_alerts,json=resolvedAlerts,proto3\" json:\"resolved_alerts,omitempty\"`\n\t// Data specific to the receiver which sent the notification\n\tReceiverData  map[string]*ReceiverDataValue `protobuf:\"bytes,8,rep,name=receiver_data,json=receiverData,proto3\" json:\"receiver_data,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Entry) Reset() {\n\t*x = Entry{}\n\tmi := &file_nflog_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Entry) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Entry) ProtoMessage() {}\n\nfunc (x *Entry) ProtoReflect() protoreflect.Message {\n\tmi := &file_nflog_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Entry.ProtoReflect.Descriptor instead.\nfunc (*Entry) Descriptor() ([]byte, []int) {\n\treturn file_nflog_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *Entry) GetGroupKey() []byte {\n\tif x != nil {\n\t\treturn x.GroupKey\n\t}\n\treturn nil\n}\n\nfunc (x *Entry) GetReceiver() *Receiver {\n\tif x != nil {\n\t\treturn x.Receiver\n\t}\n\treturn nil\n}\n\nfunc (x *Entry) GetGroupHash() []byte {\n\tif x != nil {\n\t\treturn x.GroupHash\n\t}\n\treturn nil\n}\n\nfunc (x *Entry) GetResolved() bool {\n\tif x != nil {\n\t\treturn x.Resolved\n\t}\n\treturn false\n}\n\nfunc (x *Entry) GetTimestamp() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn nil\n}\n\nfunc (x *Entry) GetFiringAlerts() []uint64 {\n\tif x != nil {\n\t\treturn x.FiringAlerts\n\t}\n\treturn nil\n}\n\nfunc (x *Entry) GetResolvedAlerts() []uint64 {\n\tif x != nil {\n\t\treturn x.ResolvedAlerts\n\t}\n\treturn nil\n}\n\nfunc (x *Entry) GetReceiverData() map[string]*ReceiverDataValue {\n\tif x != nil {\n\t\treturn x.ReceiverData\n\t}\n\treturn nil\n}\n\n// MeshEntry is a wrapper message to communicate a notify log\n// entry through a mesh network.\ntype MeshEntry struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The original raw notify log entry.\n\tEntry *Entry `protobuf:\"bytes,1,opt,name=entry,proto3\" json:\"entry,omitempty\"`\n\t// A timestamp indicating when the mesh peer should evict\n\t// the log entry from its state.\n\tExpiresAt     *timestamppb.Timestamp `protobuf:\"bytes,2,opt,name=expires_at,json=expiresAt,proto3\" json:\"expires_at,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MeshEntry) Reset() {\n\t*x = MeshEntry{}\n\tmi := &file_nflog_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MeshEntry) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MeshEntry) ProtoMessage() {}\n\nfunc (x *MeshEntry) ProtoReflect() protoreflect.Message {\n\tmi := &file_nflog_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MeshEntry.ProtoReflect.Descriptor instead.\nfunc (*MeshEntry) Descriptor() ([]byte, []int) {\n\treturn file_nflog_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *MeshEntry) GetEntry() *Entry {\n\tif x != nil {\n\t\treturn x.Entry\n\t}\n\treturn nil\n}\n\nfunc (x *MeshEntry) GetExpiresAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.ExpiresAt\n\t}\n\treturn nil\n}\n\ntype ReceiverDataValue struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Types that are valid to be assigned to Value:\n\t//\n\t//\t*ReceiverDataValue_StrVal\n\t//\t*ReceiverDataValue_IntVal\n\t//\t*ReceiverDataValue_DoubleVal\n\tValue         isReceiverDataValue_Value `protobuf_oneof:\"value\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ReceiverDataValue) Reset() {\n\t*x = ReceiverDataValue{}\n\tmi := &file_nflog_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ReceiverDataValue) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ReceiverDataValue) ProtoMessage() {}\n\nfunc (x *ReceiverDataValue) ProtoReflect() protoreflect.Message {\n\tmi := &file_nflog_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ReceiverDataValue.ProtoReflect.Descriptor instead.\nfunc (*ReceiverDataValue) Descriptor() ([]byte, []int) {\n\treturn file_nflog_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *ReceiverDataValue) GetValue() isReceiverDataValue_Value {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn nil\n}\n\nfunc (x *ReceiverDataValue) GetStrVal() string {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*ReceiverDataValue_StrVal); ok {\n\t\t\treturn x.StrVal\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (x *ReceiverDataValue) GetIntVal() int64 {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*ReceiverDataValue_IntVal); ok {\n\t\t\treturn x.IntVal\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *ReceiverDataValue) GetDoubleVal() float64 {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*ReceiverDataValue_DoubleVal); ok {\n\t\t\treturn x.DoubleVal\n\t\t}\n\t}\n\treturn 0\n}\n\ntype isReceiverDataValue_Value interface {\n\tisReceiverDataValue_Value()\n}\n\ntype ReceiverDataValue_StrVal struct {\n\tStrVal string `protobuf:\"bytes,1,opt,name=str_val,json=strVal,proto3,oneof\"`\n}\n\ntype ReceiverDataValue_IntVal struct {\n\tIntVal int64 `protobuf:\"varint,2,opt,name=int_val,json=intVal,proto3,oneof\"`\n}\n\ntype ReceiverDataValue_DoubleVal struct {\n\tDoubleVal float64 `protobuf:\"fixed64,3,opt,name=double_val,json=doubleVal,proto3,oneof\"`\n}\n\nfunc (*ReceiverDataValue_StrVal) isReceiverDataValue_Value() {}\n\nfunc (*ReceiverDataValue_IntVal) isReceiverDataValue_Value() {}\n\nfunc (*ReceiverDataValue_DoubleVal) isReceiverDataValue_Value() {}\n\nvar File_nflog_proto protoreflect.FileDescriptor\n\nconst file_nflog_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\vnflog.proto\\x12\\anflogpb\\x1a\\x1fgoogle/protobuf/timestamp.proto\\\"]\\n\" +\n\t\"\\bReceiver\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"group_name\\x18\\x01 \\x01(\\tR\\tgroupName\\x12 \\n\" +\n\t\"\\vintegration\\x18\\x02 \\x01(\\tR\\vintegration\\x12\\x10\\n\" +\n\t\"\\x03idx\\x18\\x03 \\x01(\\rR\\x03idx\\\"\\xba\\x03\\n\" +\n\t\"\\x05Entry\\x12\\x1b\\n\" +\n\t\"\\tgroup_key\\x18\\x01 \\x01(\\fR\\bgroupKey\\x12-\\n\" +\n\t\"\\breceiver\\x18\\x02 \\x01(\\v2\\x11.nflogpb.ReceiverR\\breceiver\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"group_hash\\x18\\x03 \\x01(\\fR\\tgroupHash\\x12\\x1a\\n\" +\n\t\"\\bresolved\\x18\\x04 \\x01(\\bR\\bresolved\\x128\\n\" +\n\t\"\\ttimestamp\\x18\\x05 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\ttimestamp\\x12#\\n\" +\n\t\"\\rfiring_alerts\\x18\\x06 \\x03(\\x04R\\ffiringAlerts\\x12'\\n\" +\n\t\"\\x0fresolved_alerts\\x18\\a \\x03(\\x04R\\x0eresolvedAlerts\\x12E\\n\" +\n\t\"\\rreceiver_data\\x18\\b \\x03(\\v2 .nflogpb.Entry.ReceiverDataEntryR\\freceiverData\\x1a[\\n\" +\n\t\"\\x11ReceiverDataEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x120\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\v2\\x1a.nflogpb.ReceiverDataValueR\\x05value:\\x028\\x01\\\"l\\n\" +\n\t\"\\tMeshEntry\\x12$\\n\" +\n\t\"\\x05entry\\x18\\x01 \\x01(\\v2\\x0e.nflogpb.EntryR\\x05entry\\x129\\n\" +\n\t\"\\n\" +\n\t\"expires_at\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\texpiresAt\\\"s\\n\" +\n\t\"\\x11ReceiverDataValue\\x12\\x19\\n\" +\n\t\"\\astr_val\\x18\\x01 \\x01(\\tH\\x00R\\x06strVal\\x12\\x19\\n\" +\n\t\"\\aint_val\\x18\\x02 \\x01(\\x03H\\x00R\\x06intVal\\x12\\x1f\\n\" +\n\t\"\\n\" +\n\t\"double_val\\x18\\x03 \\x01(\\x01H\\x00R\\tdoubleValB\\a\\n\" +\n\t\"\\x05valueB2Z0github.com/prometheus/alertmanager/nflog/nflogpbb\\x06proto3\"\n\nvar (\n\tfile_nflog_proto_rawDescOnce sync.Once\n\tfile_nflog_proto_rawDescData []byte\n)\n\nfunc file_nflog_proto_rawDescGZIP() []byte {\n\tfile_nflog_proto_rawDescOnce.Do(func() {\n\t\tfile_nflog_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_nflog_proto_rawDesc), len(file_nflog_proto_rawDesc)))\n\t})\n\treturn file_nflog_proto_rawDescData\n}\n\nvar file_nflog_proto_msgTypes = make([]protoimpl.MessageInfo, 5)\nvar file_nflog_proto_goTypes = []any{\n\t(*Receiver)(nil),              // 0: nflogpb.Receiver\n\t(*Entry)(nil),                 // 1: nflogpb.Entry\n\t(*MeshEntry)(nil),             // 2: nflogpb.MeshEntry\n\t(*ReceiverDataValue)(nil),     // 3: nflogpb.ReceiverDataValue\n\tnil,                           // 4: nflogpb.Entry.ReceiverDataEntry\n\t(*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp\n}\nvar file_nflog_proto_depIdxs = []int32{\n\t0, // 0: nflogpb.Entry.receiver:type_name -> nflogpb.Receiver\n\t5, // 1: nflogpb.Entry.timestamp:type_name -> google.protobuf.Timestamp\n\t4, // 2: nflogpb.Entry.receiver_data:type_name -> nflogpb.Entry.ReceiverDataEntry\n\t1, // 3: nflogpb.MeshEntry.entry:type_name -> nflogpb.Entry\n\t5, // 4: nflogpb.MeshEntry.expires_at:type_name -> google.protobuf.Timestamp\n\t3, // 5: nflogpb.Entry.ReceiverDataEntry.value:type_name -> nflogpb.ReceiverDataValue\n\t6, // [6:6] is the sub-list for method output_type\n\t6, // [6:6] is the sub-list for method input_type\n\t6, // [6:6] is the sub-list for extension type_name\n\t6, // [6:6] is the sub-list for extension extendee\n\t0, // [0:6] is the sub-list for field type_name\n}\n\nfunc init() { file_nflog_proto_init() }\nfunc file_nflog_proto_init() {\n\tif File_nflog_proto != nil {\n\t\treturn\n\t}\n\tfile_nflog_proto_msgTypes[3].OneofWrappers = []any{\n\t\t(*ReceiverDataValue_StrVal)(nil),\n\t\t(*ReceiverDataValue_IntVal)(nil),\n\t\t(*ReceiverDataValue_DoubleVal)(nil),\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_nflog_proto_rawDesc), len(file_nflog_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   5,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_nflog_proto_goTypes,\n\t\tDependencyIndexes: file_nflog_proto_depIdxs,\n\t\tMessageInfos:      file_nflog_proto_msgTypes,\n\t}.Build()\n\tFile_nflog_proto = out.File\n\tfile_nflog_proto_goTypes = nil\n\tfile_nflog_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "nflog/nflogpb/nflog.proto",
    "content": "syntax = \"proto3\";\n\npackage nflogpb;\n\noption go_package = \"github.com/prometheus/alertmanager/nflog/nflogpb\";\n\nimport \"google/protobuf/timestamp.proto\";\n\nmessage Receiver {\n  // Configured name of the receiver group.\n  string group_name = 1;\n  // Name of the integration of the receiver.\n  string integration = 2;\n  // Index of the receiver with respect to the integration.\n  // Every integration in a group may have 0..N configurations.\n  uint32 idx = 3;\n}\n\n// Entry holds information about a successful notification\n// sent to a receiver.\nmessage Entry {\n  // The key identifying the dispatching group.\n  bytes group_key = 1;\n  // The receiver that was notified.\n  Receiver receiver = 2;\n  // Hash over the state of the group at notification time.\n  // Deprecated in favor of FiringAlerts field, but kept for compatibility.\n  bytes group_hash = 3;\n  // Whether the notification was about a resolved alert.\n  // Deprecated in favor of ResolvedAlerts field, but kept for compatibility.\n  bool resolved = 4;\n  // Timestamp of the succeeding notification.\n  google.protobuf.Timestamp timestamp = 5;\n  // FiringAlerts list of hashes of firing alerts at the last notification time.\n  repeated uint64 firing_alerts = 6;\n  // ResolvedAlerts list of hashes of resolved alerts at the last notification time.\n  repeated uint64 resolved_alerts = 7;\n  // Data specific to the receiver which sent the notification\n  map<string, ReceiverDataValue> receiver_data = 8;\n}\n\n// MeshEntry is a wrapper message to communicate a notify log\n// entry through a mesh network.\nmessage MeshEntry {\n  // The original raw notify log entry.\n  Entry entry = 1;\n  // A timestamp indicating when the mesh peer should evict\n  // the log entry from its state.\n  google.protobuf.Timestamp expires_at = 2;\n}\n\nmessage ReceiverDataValue {\n  oneof value {\n    string str_val = 1;\n    int64 int_val = 2;\n    double double_val = 3;\n  }\n}\n"
  },
  {
    "path": "nflog/nflogpb/set.go",
    "content": "// Copyright 2017 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage nflogpb\n\n// IsFiringSubset returns whether the given subset is a subset of the alerts\n// that were firing at the time of the last notification.\nfunc (m *Entry) IsFiringSubset(subset map[uint64]struct{}) bool {\n\tset := map[uint64]struct{}{}\n\tfor i := range m.FiringAlerts {\n\t\tset[m.FiringAlerts[i]] = struct{}{}\n\t}\n\n\treturn isSubset(set, subset)\n}\n\n// IsResolvedSubset returns whether the given subset is a subset of the alerts\n// that were resolved at the time of the last notification.\nfunc (m *Entry) IsResolvedSubset(subset map[uint64]struct{}) bool {\n\tset := map[uint64]struct{}{}\n\tfor i := range m.ResolvedAlerts {\n\t\tset[m.ResolvedAlerts[i]] = struct{}{}\n\t}\n\n\treturn isSubset(set, subset)\n}\n\nfunc isSubset(set, subset map[uint64]struct{}) bool {\n\tfor k := range subset {\n\t\t_, exists := set[k]\n\t\tif !exists {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "nflog/nflogpb/set_test.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage nflogpb\n\nimport (\n\t\"testing\"\n)\n\nfunc TestIsFiringSubset(t *testing.T) {\n\te := &Entry{\n\t\tFiringAlerts: []uint64{1, 2, 3},\n\t}\n\n\ttests := []struct {\n\t\tsubset   map[uint64]struct{}\n\t\texpected bool\n\t}{\n\t\t{newSubset(), true}, // empty subset\n\t\t{newSubset(1), true},\n\t\t{newSubset(2), true},\n\t\t{newSubset(3), true},\n\t\t{newSubset(1, 2), true},\n\t\t{newSubset(1, 2), true},\n\t\t{newSubset(1, 2, 3), true},\n\t\t{newSubset(4), false},\n\t\t{newSubset(1, 5), false},\n\t\t{newSubset(1, 2, 3, 6), false},\n\t}\n\n\tfor _, test := range tests {\n\t\tif result := e.IsFiringSubset(test.subset); result != test.expected {\n\t\t\tt.Errorf(\"Expected %t, got %t for subset %v\", test.expected, result, elements(test.subset))\n\t\t}\n\t}\n}\n\nfunc TestIsResolvedSubset(t *testing.T) {\n\te := &Entry{\n\t\tResolvedAlerts: []uint64{1, 2, 3},\n\t}\n\n\ttests := []struct {\n\t\tsubset   map[uint64]struct{}\n\t\texpected bool\n\t}{\n\t\t{newSubset(), true}, // empty subset\n\t\t{newSubset(1), true},\n\t\t{newSubset(2), true},\n\t\t{newSubset(3), true},\n\t\t{newSubset(1, 2), true},\n\t\t{newSubset(1, 2), true},\n\t\t{newSubset(1, 2, 3), true},\n\t\t{newSubset(4), false},\n\t\t{newSubset(1, 5), false},\n\t\t{newSubset(1, 2, 3, 6), false},\n\t}\n\n\tfor _, test := range tests {\n\t\tif result := e.IsResolvedSubset(test.subset); result != test.expected {\n\t\t\tt.Errorf(\"Expected %t, got %t for subset %v\", test.expected, result, elements(test.subset))\n\t\t}\n\t}\n}\n\nfunc newSubset(elements ...uint64) map[uint64]struct{} {\n\tsubset := make(map[uint64]struct{})\n\tfor _, el := range elements {\n\t\tsubset[el] = struct{}{}\n\t}\n\n\treturn subset\n}\n\nfunc elements(m map[uint64]struct{}) []uint64 {\n\tels := make([]uint64, 0, len(m))\n\tfor k := range m {\n\t\tels = append(els, k)\n\t}\n\n\treturn els\n}\n"
  },
  {
    "path": "notify/discord/discord.go",
    "content": "// Copyright 2021 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage discord\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\tnetUrl \"net/url\"\n\t\"os\"\n\t\"strings\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nconst (\n\t// https://discord.com/developers/docs/resources/channel#embed-object-embed-limits - 256 characters or runes.\n\tmaxTitleLenRunes = 256\n\t// https://discord.com/developers/docs/resources/channel#embed-object-embed-limits - 4096 characters or runes.\n\tmaxDescriptionLenRunes = 4096\n\n\tmaxContentLenRunes = 2000\n)\n\nconst (\n\tcolorRed   = 0x992D22\n\tcolorGreen = 0x2ECC71\n\tcolorGrey  = 0x95A5A6\n)\n\n// Notifier implements a Notifier for Discord notifications.\ntype Notifier struct {\n\tconf       *config.DiscordConfig\n\ttmpl       *template.Template\n\tlogger     *slog.Logger\n\tclient     *http.Client\n\tretrier    *notify.Retrier\n\twebhookURL *amcommoncfg.SecretURL\n}\n\n// New returns a new Discord notifier.\nfunc New(c *config.DiscordConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {\n\tclient, err := notify.NewClientWithTracing(*c.HTTPConfig, \"discord\", httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tn := &Notifier{\n\t\tconf:       c,\n\t\ttmpl:       t,\n\t\tlogger:     l,\n\t\tclient:     client,\n\t\tretrier:    &notify.Retrier{},\n\t\twebhookURL: c.WebhookURL,\n\t}\n\treturn n, nil\n}\n\ntype webhook struct {\n\tContent   string         `json:\"content\"`\n\tEmbeds    []webhookEmbed `json:\"embeds\"`\n\tUsername  string         `json:\"username,omitempty\"`\n\tAvatarURL string         `json:\"avatar_url,omitempty\"`\n}\n\ntype webhookEmbed struct {\n\tTitle       string `json:\"title\"`\n\tDescription string `json:\"description\"`\n\tColor       int    `json:\"color\"`\n}\n\n// Notify implements the Notifier interface.\nfunc (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {\n\tkey, err := notify.ExtractGroupKey(ctx)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tlogger := n.logger.With(\"group_key\", key)\n\tlogger.Debug(\"extracted group key\")\n\n\talerts := types.Alerts(as...)\n\tdata := notify.GetTemplateData(ctx, n.tmpl, as, logger)\n\ttmpl := notify.TmplText(n.tmpl, data, &err)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\ttitle, truncated := notify.TruncateInRunes(tmpl(n.conf.Title), maxTitleLenRunes)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif truncated {\n\t\tlogger.Warn(\"Truncated title\", \"max_runes\", maxTitleLenRunes)\n\t}\n\tdescription, truncated := notify.TruncateInRunes(tmpl(n.conf.Message), maxDescriptionLenRunes)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif truncated {\n\t\tlogger.Warn(\"Truncated message\", \"max_runes\", maxDescriptionLenRunes)\n\t}\n\n\tcontent, truncated := notify.TruncateInRunes(tmpl(n.conf.Content), maxContentLenRunes)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif truncated {\n\t\tlogger.Warn(\"Truncated message\", \"max_runes\", maxContentLenRunes)\n\t}\n\n\tcolor := colorGrey\n\tif alerts.Status() == model.AlertFiring {\n\t\tcolor = colorRed\n\t}\n\tif alerts.Status() == model.AlertResolved {\n\t\tcolor = colorGreen\n\t}\n\n\tvar url string\n\tif n.conf.WebhookURL != nil {\n\t\turl = n.conf.WebhookURL.String()\n\t} else {\n\t\tb, err := os.ReadFile(n.conf.WebhookURLFile)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"read webhook_url_file: %w\", err)\n\t\t}\n\t\turl = strings.TrimSpace(string(b))\n\t}\n\n\tw := webhook{\n\t\tContent:  content,\n\t\tUsername: n.conf.Username,\n\t\tEmbeds: []webhookEmbed{{\n\t\t\tTitle:       title,\n\t\t\tDescription: description,\n\t\t\tColor:       color,\n\t\t}},\n\t}\n\n\tif len(n.conf.AvatarURL) != 0 {\n\t\tif _, err := netUrl.Parse(n.conf.AvatarURL); err == nil {\n\t\t\tw.AvatarURL = n.conf.AvatarURL\n\t\t} else {\n\t\t\tlogger.Warn(\"Bad avatar url\", \"key\", key)\n\t\t}\n\t}\n\n\tvar payload bytes.Buffer\n\tif err = json.NewEncoder(&payload).Encode(w); err != nil {\n\t\treturn false, err\n\t}\n\n\tresp, err := notify.PostJSON(ctx, n.client, url, &payload)\n\tif err != nil {\n\t\treturn true, notify.RedactURL(err)\n\t}\n\n\tshouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)\n\tif err != nil {\n\t\treturn shouldRetry, err\n\t}\n\treturn false, nil\n}\n"
  },
  {
    "path": "notify/discord/discord_test.go",
    "content": "// Copyright 2021 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage discord\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/notify/test\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// This is a test URL that has been modified to not be valid.\nvar testWebhookURL, _ = url.Parse(\"https://discord.com/api/webhooks/971139602272503183/78ZWZ4V3xwZUBKRFF-G9m1nRtDtNTChl_WzW6Q4kxShjSB02oLSiPTPa8TS2tTGO9EYf\")\n\nfunc TestDiscordRetry(t *testing.T) {\n\tnotifier, err := New(\n\t\t&config.DiscordConfig{\n\t\t\tWebhookURL: &amcommoncfg.SecretURL{URL: testWebhookURL},\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\tfor statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) {\n\t\tactual, _ := notifier.retrier.Check(statusCode, nil)\n\t\trequire.Equal(t, expected, actual, \"retry - error on status %d\", statusCode)\n\t}\n}\n\nfunc TestDiscordTemplating(t *testing.T) {\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdec := json.NewDecoder(r.Body)\n\t\tout := make(map[string]any)\n\t\terr := dec.Decode(&out)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}))\n\tdefer srv.Close()\n\tu, _ := url.Parse(srv.URL)\n\n\tfor _, tc := range []struct {\n\t\ttitle string\n\t\tcfg   *config.DiscordConfig\n\n\t\tretry  bool\n\t\terrMsg string\n\t}{\n\t\t{\n\t\t\ttitle: \"full-blown message\",\n\t\t\tcfg: &config.DiscordConfig{\n\t\t\t\tTitle:   `{{ template \"discord.default.title\" . }}`,\n\t\t\t\tMessage: `{{ template \"discord.default.message\" . }}`,\n\t\t\t},\n\t\t\tretry: false,\n\t\t},\n\t\t{\n\t\t\ttitle: \"title with templating errors\",\n\t\t\tcfg: &config.DiscordConfig{\n\t\t\t\tTitle: \"{{ \",\n\t\t\t},\n\t\t\terrMsg: \"template: :1: unclosed action\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"message with templating errors\",\n\t\t\tcfg: &config.DiscordConfig{\n\t\t\t\tTitle:   `{{ template \"discord.default.title\" . }}`,\n\t\t\t\tMessage: \"{{ \",\n\t\t\t},\n\t\t\terrMsg: \"template: :1: unclosed action\",\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\ttc.cfg.WebhookURL = &amcommoncfg.SecretURL{URL: u}\n\t\t\ttc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{}\n\t\t\tpd, err := New(tc.cfg, test.CreateTmpl(t), promslog.NewNopLogger())\n\t\t\trequire.NoError(t, err)\n\n\t\t\tctx := context.Background()\n\t\t\tctx = notify.WithGroupKey(ctx, \"1\")\n\n\t\t\tok, err := pd.Notify(ctx, []*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"lbl1\": \"val1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}...)\n\t\t\tif tc.errMsg == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), tc.errMsg)\n\t\t\t}\n\t\t\trequire.Equal(t, tc.retry, ok)\n\t\t})\n\t}\n}\n\nfunc TestDiscordRedactedURL(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tsecret := \"secret\"\n\tnotifier, err := New(\n\t\t&config.DiscordConfig{\n\t\t\tWebhookURL: &amcommoncfg.SecretURL{URL: u},\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret)\n}\n\nfunc TestDiscordReadingURLFromFile(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tf, err := os.CreateTemp(t.TempDir(), \"webhook_url\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = f.WriteString(u.String() + \"\\n\")\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tnotifier, err := New(\n\t\t&config.DiscordConfig{\n\t\t\tWebhookURLFile: f.Name(),\n\t\t\tHTTPConfig:     &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String())\n}\n\nfunc TestDiscord_Notify(t *testing.T) {\n\t// Create a fake HTTP server to simulate the Discord webhook\n\tvar resp string\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t// Read the request as a string\n\t\tbody, err := io.ReadAll(r.Body)\n\t\trequire.NoError(t, err, \"reading request body failed\")\n\t\t// Store the request body in the response\n\t\tresp = string(body)\n\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\n\t// Create a temporary file to simulate the WebhookURLFile\n\ttempFile, err := os.CreateTemp(t.TempDir(), \"webhook_url\")\n\trequire.NoError(t, err)\n\n\t// Write the fake webhook URL to the temp file\n\t_, err = tempFile.WriteString(srv.URL)\n\trequire.NoError(t, err)\n\n\t// Create a DiscordConfig with the WebhookURLFile set\n\tcfg := &config.DiscordConfig{\n\t\tWebhookURLFile: tempFile.Name(),\n\t\tHTTPConfig:     &commoncfg.HTTPClientConfig{},\n\t\tTitle:          \"Test Title\",\n\t\tMessage:        \"Test Message\",\n\t\tContent:        \"Test Content\",\n\t\tUsername:       \"Test Username\",\n\t\tAvatarURL:      \"http://example.com/avatar.png\",\n\t}\n\n\t// Create a new Discord notifier\n\tnotifier, err := New(cfg, test.CreateTmpl(t), promslog.NewNopLogger())\n\trequire.NoError(t, err)\n\n\t// Create a context and alerts\n\tctx := context.Background()\n\tctx = notify.WithGroupKey(ctx, \"1\")\n\talerts := []*types.Alert{\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"lbl1\": \"val1\",\n\t\t\t\t},\n\t\t\t\tStartsAt: time.Now(),\n\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t},\n\t\t},\n\t}\n\n\t// Call the Notify method\n\tok, err := notifier.Notify(ctx, alerts...)\n\trequire.NoError(t, err)\n\trequire.False(t, ok)\n\n\trequire.JSONEq(t, `{\"content\":\"Test Content\",\"embeds\":[{\"title\":\"Test Title\",\"description\":\"Test Message\",\"color\":10038562}],\"username\":\"Test Username\",\"avatar_url\":\"http://example.com/avatar.png\"}`, resp)\n}\n"
  },
  {
    "path": "notify/email/email.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage email\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"math/rand\"\n\t\"mime\"\n\t\"mime/multipart\"\n\t\"mime/quotedprintable\"\n\t\"net\"\n\t\"net/mail\"\n\t\"net/smtp\"\n\t\"net/textproto\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// Email implements a Notifier for email notifications.\ntype Email struct {\n\tconf     *config.EmailConfig\n\ttmpl     *template.Template\n\tlogger   *slog.Logger\n\thostname string\n}\n\n// New returns a new Email notifier.\nfunc New(c *config.EmailConfig, t *template.Template, l *slog.Logger) *Email {\n\tif _, ok := c.Headers[\"Subject\"]; !ok {\n\t\tc.Headers[\"Subject\"] = config.DefaultEmailSubject\n\t}\n\tif _, ok := c.Headers[\"To\"]; !ok {\n\t\tc.Headers[\"To\"] = c.To\n\t}\n\tif _, ok := c.Headers[\"From\"]; !ok {\n\t\tc.Headers[\"From\"] = c.From\n\t}\n\n\th, err := os.Hostname()\n\t// If we can't get the hostname, we'll use localhost\n\tif err != nil {\n\t\th = \"localhost.localdomain\"\n\t}\n\treturn &Email{conf: c, tmpl: t, logger: l, hostname: h}\n}\n\n// auth resolves a string of authentication mechanisms.\nfunc (n *Email) auth(mechs string) (smtp.Auth, error) {\n\tusername := n.conf.AuthUsername\n\n\t// If no username is set, keep going without authentication.\n\tif n.conf.AuthUsername == \"\" {\n\t\tn.logger.Debug(\"smtp_auth_username is not configured. Attempting to send email without authenticating\")\n\t\treturn nil, nil\n\t}\n\n\tvar errs error\n\tfor mech := range strings.SplitSeq(mechs, \" \") {\n\t\tswitch mech {\n\t\tcase \"CRAM-MD5\":\n\t\t\tsecret, secretErr := n.getAuthSecret()\n\t\t\tif secretErr != nil {\n\t\t\t\terrs = errors.Join(errs, secretErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif secret == \"\" {\n\t\t\t\terrs = errors.Join(errs, errors.New(\"missing secret for CRAM-MD5 auth mechanism\"))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn smtp.CRAMMD5Auth(username, secret), nil\n\t\tcase \"PLAIN\":\n\t\t\tpassword, passwordErr := n.getPassword()\n\t\t\tif passwordErr != nil {\n\t\t\t\terrs = errors.Join(errs, passwordErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif password == \"\" {\n\t\t\t\terrs = errors.Join(errs, errors.New(\"missing password for PLAIN auth mechanism\"))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn smtp.PlainAuth(n.conf.AuthIdentity, username, password, n.conf.Smarthost.Host), nil\n\t\tcase \"LOGIN\":\n\t\t\tpassword, passwordErr := n.getPassword()\n\t\t\tif passwordErr != nil {\n\t\t\t\terrs = errors.Join(errs, passwordErr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif password == \"\" {\n\t\t\t\terrs = errors.Join(errs, errors.New(\"missing password for LOGIN auth mechanism\"))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn LoginAuth(username, password), nil\n\t\tdefault:\n\t\t\terrs = errors.Join(errs, errors.New(\"unknown auth mechanism: \"+mech))\n\t\t}\n\t}\n\treturn nil, errs\n}\n\n// Notify implements the Notifier interface.\nfunc (n *Email) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {\n\tvar (\n\t\tc       *smtp.Client\n\t\tconn    net.Conn\n\t\terr     error\n\t\tsuccess = false\n\t)\n\t// Determine whether to use Implicit TLS\n\tvar useImplicitTLS bool\n\tif n.conf.ForceImplicitTLS != nil {\n\t\tuseImplicitTLS = *n.conf.ForceImplicitTLS\n\t} else {\n\t\t// Default logic: port 465 uses implicit TLS (backward compatibility)\n\t\tuseImplicitTLS = n.conf.Smarthost.Port == \"465\"\n\t}\n\n\tif useImplicitTLS {\n\t\ttlsConfig, err := commoncfg.NewTLSConfig(n.conf.TLSConfig)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"parse TLS configuration: %w\", err)\n\t\t}\n\t\tif tlsConfig.ServerName == \"\" {\n\t\t\ttlsConfig.ServerName = n.conf.Smarthost.Host\n\t\t}\n\n\t\tconn, err = tls.Dial(\"tcp\", n.conf.Smarthost.String(), tlsConfig)\n\t\tif err != nil {\n\t\t\treturn true, fmt.Errorf(\"establish TLS connection to server: %w\", err)\n\t\t}\n\t} else {\n\t\tvar (\n\t\t\td   = net.Dialer{}\n\t\t\terr error\n\t\t)\n\t\tconn, err = d.DialContext(ctx, \"tcp\", n.conf.Smarthost.String())\n\t\tif err != nil {\n\t\t\treturn true, fmt.Errorf(\"establish connection to server: %w\", err)\n\t\t}\n\t}\n\tc, err = smtp.NewClient(conn, n.conf.Smarthost.Host)\n\tif err != nil {\n\t\tconn.Close()\n\t\treturn true, fmt.Errorf(\"create SMTP client: %w\", err)\n\t}\n\tdefer func() {\n\t\t// Try to clean up after ourselves but don't log anything if something has failed.\n\t\tif err := c.Quit(); success && err != nil {\n\t\t\tn.logger.Warn(\"failed to close SMTP connection\", \"err\", err)\n\t\t}\n\t}()\n\n\tif n.conf.Hello != \"\" {\n\t\terr = c.Hello(n.conf.Hello)\n\t\tif err != nil {\n\t\t\treturn true, fmt.Errorf(\"send EHLO command: %w\", err)\n\t\t}\n\t}\n\n\t// Global Config guarantees RequireTLS is not nil.\n\tif *n.conf.RequireTLS && !useImplicitTLS {\n\t\tif ok, _ := c.Extension(\"STARTTLS\"); !ok {\n\t\t\treturn true, fmt.Errorf(\"'require_tls' is true (default) but %q does not advertise the STARTTLS extension\", n.conf.Smarthost)\n\t\t}\n\n\t\ttlsConf, err := commoncfg.NewTLSConfig(n.conf.TLSConfig)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"parse TLS configuration: %w\", err)\n\t\t}\n\t\tif tlsConf.ServerName == \"\" {\n\t\t\ttlsConf.ServerName = n.conf.Smarthost.Host\n\t\t}\n\n\t\tif err := c.StartTLS(tlsConf); err != nil {\n\t\t\treturn true, fmt.Errorf(\"send STARTTLS command: %w\", err)\n\t\t}\n\t}\n\n\tif ok, mech := c.Extension(\"AUTH\"); ok {\n\t\tauth, err := n.auth(mech)\n\t\tif err != nil {\n\t\t\treturn true, fmt.Errorf(\"find auth mechanism: %w\", err)\n\t\t}\n\t\tif auth != nil {\n\t\t\tif err := c.Auth(auth); err != nil {\n\t\t\t\treturn true, fmt.Errorf(\"%T auth: %w\", auth, err)\n\t\t\t}\n\t\t}\n\t}\n\n\tvar (\n\t\ttmplErr error\n\t\tdata    = notify.GetTemplateData(ctx, n.tmpl, as, n.logger)\n\t\ttmpl    = notify.TmplText(n.tmpl, data, &tmplErr)\n\t)\n\tfrom := tmpl(n.conf.From)\n\tif tmplErr != nil {\n\t\treturn false, fmt.Errorf(\"execute 'from' template: %w\", tmplErr)\n\t}\n\tto := tmpl(n.conf.To)\n\tif tmplErr != nil {\n\t\treturn false, fmt.Errorf(\"execute 'to' template: %w\", tmplErr)\n\t}\n\n\taddrs, err := mail.ParseAddressList(from)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"parse 'from' addresses: %w\", err)\n\t}\n\tif len(addrs) != 1 {\n\t\treturn false, fmt.Errorf(\"must be exactly one 'from' address (got: %d)\", len(addrs))\n\t}\n\tif err = c.Mail(addrs[0].Address); err != nil {\n\t\treturn true, fmt.Errorf(\"send MAIL command: %w\", err)\n\t}\n\taddrs, err = mail.ParseAddressList(to)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"parse 'to' addresses: %w\", err)\n\t}\n\tfor _, addr := range addrs {\n\t\tif err = c.Rcpt(addr.Address); err != nil {\n\t\t\treturn true, fmt.Errorf(\"send RCPT command: %w\", err)\n\t\t}\n\t}\n\n\t// Send the email headers and body.\n\tmessage, err := c.Data()\n\tif err != nil {\n\t\treturn true, fmt.Errorf(\"send DATA command: %w\", err)\n\t}\n\tcloseOnce := sync.OnceValue(func() error {\n\t\treturn message.Close()\n\t})\n\t// Close the message when this method exits in order to not leak resources. Even though we're calling this explicitly\n\t// further down, the method may exit before then.\n\tdefer func() {\n\t\t// If we try close an already-closed writer, it'll send a subsequent request to the server which is invalid.\n\t\t_ = closeOnce()\n\t}()\n\n\tbuffer := &bytes.Buffer{}\n\tfor header, t := range n.conf.Headers {\n\t\tvalue, err := n.tmpl.ExecuteTextString(t, data)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"execute %q header template: %w\", header, err)\n\t\t}\n\t\tfmt.Fprintf(buffer, \"%s: %s\\r\\n\", header, mime.QEncoding.Encode(\"utf-8\", value))\n\t}\n\n\tif _, ok := n.conf.Headers[\"Message-Id\"]; !ok {\n\t\tfmt.Fprintf(buffer, \"Message-Id: %s\\r\\n\", fmt.Sprintf(\"<%d.%d@%s>\", time.Now().UnixNano(), rand.Uint64(), n.hostname))\n\t}\n\n\tif n.conf.Threading.Enabled {\n\t\tkey, err := notify.ExtractGroupKey(ctx)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\t// Add threading headers. All notifications for the same alert group\n\t\t// (identified by key hash) are threaded together.\n\t\tthreadBy := \"\"\n\t\tif n.conf.Threading.ThreadByDate != \"none\" {\n\t\t\t// ThreadByDate is 'daily':\n\t\t\t// Use current date so all mails for this alert today thread together.\n\t\t\tthreadBy = time.Now().Format(\"2006-01-02\")\n\t\t}\n\t\tkeyHash := key.Hash()\n\t\tif len(keyHash) > 16 {\n\t\t\tkeyHash = keyHash[:16]\n\t\t}\n\t\t// The thread root ID is a Message-ID that doesn't correspond to\n\t\t// any actual email. Email clients following the (commonly used) JWZ\n\t\t// algorithm will create a dummy container to group these messages.\n\t\tthreadRootID := fmt.Sprintf(\"<alert-%s-%s@alertmanager>\", keyHash, threadBy)\n\t\tfmt.Fprintf(buffer, \"References: %s\\r\\n\", threadRootID)\n\t\tfmt.Fprintf(buffer, \"In-Reply-To: %s\\r\\n\", threadRootID)\n\t}\n\n\tmultipartBuffer := &bytes.Buffer{}\n\tmultipartWriter := multipart.NewWriter(multipartBuffer)\n\n\tfmt.Fprintf(buffer, \"Date: %s\\r\\n\", time.Now().Format(time.RFC1123Z))\n\tfmt.Fprintf(buffer, \"Content-Type: multipart/alternative;  boundary=%s\\r\\n\", multipartWriter.Boundary())\n\tfmt.Fprintf(buffer, \"MIME-Version: 1.0\\r\\n\\r\\n\")\n\n\t// TODO: Add some useful headers here, such as URL of the alertmanager\n\t// and active/resolved.\n\t_, err = message.Write(buffer.Bytes())\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"write headers: %w\", err)\n\t}\n\n\tif len(n.conf.Text) > 0 {\n\t\t// Text template\n\t\tw, err := multipartWriter.CreatePart(textproto.MIMEHeader{\n\t\t\t\"Content-Transfer-Encoding\": {\"quoted-printable\"},\n\t\t\t\"Content-Type\":              {\"text/plain; charset=UTF-8\"},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"create part for text template: %w\", err)\n\t\t}\n\t\tbody, err := n.tmpl.ExecuteTextString(n.conf.Text, data)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"execute text template: %w\", err)\n\t\t}\n\t\tqw := quotedprintable.NewWriter(w)\n\t\t_, err = qw.Write([]byte(body))\n\t\tif err != nil {\n\t\t\treturn true, fmt.Errorf(\"write text part: %w\", err)\n\t\t}\n\t\terr = qw.Close()\n\t\tif err != nil {\n\t\t\treturn true, fmt.Errorf(\"close text part: %w\", err)\n\t\t}\n\t}\n\n\tif len(n.conf.HTML) > 0 {\n\t\t// Html template\n\t\t// Preferred alternative placed last per section 5.1.4 of RFC 2046\n\t\t// https://www.ietf.org/rfc/rfc2046.txt\n\t\tw, err := multipartWriter.CreatePart(textproto.MIMEHeader{\n\t\t\t\"Content-Transfer-Encoding\": {\"quoted-printable\"},\n\t\t\t\"Content-Type\":              {\"text/html; charset=UTF-8\"},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"create part for html template: %w\", err)\n\t\t}\n\t\tbody, err := n.tmpl.ExecuteHTMLString(n.conf.HTML, data)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"execute html template: %w\", err)\n\t\t}\n\t\tqw := quotedprintable.NewWriter(w)\n\t\t_, err = qw.Write([]byte(body))\n\t\tif err != nil {\n\t\t\treturn true, fmt.Errorf(\"write HTML part: %w\", err)\n\t\t}\n\t\terr = qw.Close()\n\t\tif err != nil {\n\t\t\treturn true, fmt.Errorf(\"close HTML part: %w\", err)\n\t\t}\n\t}\n\n\terr = multipartWriter.Close()\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"close multipartWriter: %w\", err)\n\t}\n\n\t_, err = message.Write(multipartBuffer.Bytes())\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"write body buffer: %w\", err)\n\t}\n\n\t// Complete the message and await response.\n\tif err = closeOnce(); err != nil {\n\t\treturn true, fmt.Errorf(\"delivery failure: %w\", err)\n\t}\n\n\tsuccess = true\n\treturn false, nil\n}\n\ntype loginAuth struct {\n\tusername, password string\n}\n\nfunc LoginAuth(username, password string) smtp.Auth {\n\treturn &loginAuth{username, password}\n}\n\nfunc (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {\n\treturn \"LOGIN\", []byte{}, nil\n}\n\n// Used for AUTH LOGIN. (Maybe password should be encrypted).\nfunc (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {\n\tif more {\n\t\tswitch strings.ToLower(string(fromServer)) {\n\t\tcase \"username:\":\n\t\t\treturn []byte(a.username), nil\n\t\tcase \"password:\":\n\t\t\treturn []byte(a.password), nil\n\t\tdefault:\n\t\t\treturn nil, errors.New(\"unexpected server challenge\")\n\t\t}\n\t}\n\treturn nil, nil\n}\n\nfunc (n *Email) getPassword() (string, error) {\n\tif len(n.conf.AuthPasswordFile) > 0 {\n\t\tcontent, err := os.ReadFile(n.conf.AuthPasswordFile)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"could not read %s: %w\", n.conf.AuthPasswordFile, err)\n\t\t}\n\t\treturn strings.TrimSpace(string(content)), nil\n\t}\n\treturn string(n.conf.AuthPassword), nil\n}\n\nfunc (n *Email) getAuthSecret() (string, error) {\n\tif len(n.conf.AuthSecretFile) > 0 {\n\t\tcontent, err := os.ReadFile(n.conf.AuthSecretFile)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"could not read %s: %w\", n.conf.AuthSecretFile, err)\n\t\t}\n\t\treturn string(content), nil\n\t}\n\treturn string(n.conf.AuthSecret), nil\n}\n"
  },
  {
    "path": "notify/email/email_test.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Some tests require a running mail catcher. We use MailDev for this purpose,\n// it can work without or with authentication (LOGIN only). It exposes a REST\n// API which we use to retrieve and check the sent emails.\n//\n// Those tests are only executed when specific environment variables are set,\n// otherwise they are skipped. The tests must be run by the CI.\n//\n// To run the tests locally, you should start 2 MailDev containers:\n//\n// $ docker run --rm -p 1080:1080 -p 1025:1025 --entrypoint bin/maildev maildev/maildev:2.2.1 -v\n// $ docker run --rm -p 1081:1080 -p 1026:1025 --entrypoint bin/maildev maildev/maildev:2.2.1 --incoming-user user --incoming-pass pass -v\n//\n// $ EMAIL_NO_AUTH_CONFIG=testdata/noauth-local.yml EMAIL_AUTH_CONFIG=testdata/auth-local.yml make\n//\n// See also https://github.com/maildev/maildev for more details.\npackage email\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/emersion/go-smtp\"\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\n\t// nolint:depguard // require cannot be called outside the main goroutine: https://pkg.go.dev/testing#T.FailNow\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"gopkg.in/yaml.v2\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nconst (\n\temailNoAuthConfigVar = \"EMAIL_NO_AUTH_CONFIG\"\n\temailAuthConfigVar   = \"EMAIL_AUTH_CONFIG\"\n\n\temailTo   = \"alerts@example.com\"\n\temailFrom = \"alertmanager@example.com\"\n)\n\n// email represents an email returned by the MailDev REST API.\n// See https://github.com/djfarrelly/MailDev/blob/master/docs/rest.md.\ntype email struct {\n\tTo      []map[string]string\n\tFrom    []map[string]string\n\tSubject string\n\tHTML    *string\n\tText    *string\n\tHeaders map[string]string\n}\n\n// mailDev is a client for the MailDev server.\ntype mailDev struct {\n\t*url.URL\n}\n\nfunc (m *mailDev) UnmarshalYAML(unmarshal func(any) error) error {\n\tvar s string\n\tif err := unmarshal(&s); err != nil {\n\t\treturn err\n\t}\n\turlp, err := url.Parse(s)\n\tif err != nil {\n\t\treturn err\n\t}\n\tm.URL = urlp\n\treturn nil\n}\n\n// getLastEmail returns the last received email.\nfunc (m *mailDev) getLastEmail(t *testing.T) (*email, error) {\n\t// The maildev API might be async. Waiting resolves some issues with flakes.\n\ttime.Sleep(100 * time.Millisecond)\n\tcode, b, err := m.doEmailRequest(http.MethodGet, \"/email\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif code != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"expected status OK, got %d\", code)\n\t}\n\n\tt.Logf(\"Raw email data (getLastEmail): %s\", string(b))\n\tvar emails []email\n\terr = yaml.Unmarshal(b, &emails)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(emails) == 0 {\n\t\treturn nil, nil\n\t}\n\treturn &emails[len(emails)-1], nil\n}\n\n// deleteAllEmails deletes all emails.\nfunc (m *mailDev) deleteAllEmails() error {\n\t_, _, err := m.doEmailRequest(http.MethodDelete, \"/email/all\")\n\treturn err\n}\n\n// doEmailRequest makes a request to the MailDev API.\nfunc (m *mailDev) doEmailRequest(method, path string) (int, []byte, error) {\n\treq, err := http.NewRequest(method, fmt.Sprintf(\"%s://%s%s\", m.Scheme, m.Host, path), nil)\n\tif err != nil {\n\t\treturn 0, nil, err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second)\n\treq = req.WithContext(ctx)\n\tdefer cancel()\n\tres, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn 0, nil, err\n\t}\n\tdefer res.Body.Close()\n\tb, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn 0, nil, err\n\t}\n\treturn res.StatusCode, b, nil\n}\n\n// emailTestConfig is the configuration for the tests.\ntype emailTestConfig struct {\n\tSmarthost config.HostPort `yaml:\"smarthost\"`\n\tUsername  string          `yaml:\"username\"`\n\tPassword  string          `yaml:\"password\"`\n\tServer    *mailDev        `yaml:\"server\"`\n}\n\nfunc loadEmailTestConfiguration(f string) (emailTestConfig, error) {\n\tc := emailTestConfig{}\n\tb, err := os.ReadFile(f)\n\tif err != nil {\n\t\treturn c, err\n\t}\n\n\terr = yaml.UnmarshalStrict(b, &c)\n\tif err != nil {\n\t\treturn c, err\n\t}\n\n\treturn c, nil\n}\n\nfunc notifyEmail(t *testing.T, cfg *config.EmailConfig, server *mailDev) (*email, bool, error) {\n\treturn notifyEmailWithContext(context.Background(), t, cfg, server)\n}\n\n// notifyEmailWithContext sends a notification with one firing alert and retrieves the\n// email from the SMTP server if the notification has been successfully delivered.\nfunc notifyEmailWithContext(ctx context.Context, t *testing.T, cfg *config.EmailConfig, server *mailDev) (*email, bool, error) {\n\ttmpl, firingAlert, err := prepare(cfg)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\n\terr = server.deleteAllEmails()\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\n\temail := New(cfg, tmpl, promslog.NewNopLogger())\n\n\tretry, err := email.Notify(ctx, firingAlert)\n\tif err != nil {\n\t\treturn nil, retry, err\n\t}\n\n\te, err := server.getLastEmail(t)\n\tif err != nil {\n\t\treturn nil, retry, err\n\t} else if e == nil {\n\t\treturn nil, retry, fmt.Errorf(\"email not found\")\n\t}\n\treturn e, retry, nil\n}\n\nfunc prepare(cfg *config.EmailConfig) (*template.Template, *types.Alert, error) {\n\tif cfg == nil {\n\t\tpanic(\"nil config passed\")\n\t}\n\n\tif cfg.RequireTLS == nil {\n\t\tcfg.RequireTLS = new(bool)\n\t}\n\tif cfg.Headers == nil {\n\t\tcfg.Headers = make(map[string]string)\n\t}\n\n\ttmpl, err := template.FromGlobs([]string{})\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\ttmpl.ExternalURL, _ = url.Parse(\"http://am\")\n\n\tfiringAlert := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels:   model.LabelSet{},\n\t\t\tStartsAt: time.Now(),\n\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t},\n\t}\n\treturn tmpl, firingAlert, nil\n}\n\n// TestEmailNotifyWithErrors tries to send emails with buggy inputs.\nfunc TestEmailNotifyWithErrors(t *testing.T) {\n\tcfgFile := os.Getenv(emailNoAuthConfigVar)\n\tif len(cfgFile) == 0 {\n\t\tt.Skipf(\"%s not set\", emailNoAuthConfigVar)\n\t}\n\n\tc, err := loadEmailTestConfiguration(cfgFile)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tfor _, tc := range []struct {\n\t\ttitle     string\n\t\tupdateCfg func(*config.EmailConfig)\n\n\t\terrMsg   string\n\t\thasEmail bool\n\t}{\n\t\t{\n\t\t\ttitle: \"invalid 'from' template\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.From = `{{ template \"invalid\" }}`\n\t\t\t},\n\t\t\terrMsg: \"execute 'from' template:\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"invalid 'from' address\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.From = `xxx`\n\t\t\t},\n\t\t\terrMsg: \"parse 'from' addresses:\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"invalid 'to' template\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.To = `{{ template \"invalid\" }}`\n\t\t\t},\n\t\t\terrMsg: \"execute 'to' template:\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"invalid 'to' address\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.To = `xxx`\n\t\t\t},\n\t\t\terrMsg: \"parse 'to' addresses:\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"invalid 'subject' template\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.Headers[\"subject\"] = `{{ template \"invalid\" }}`\n\t\t\t},\n\t\t\terrMsg:   `execute \"subject\" header template:`,\n\t\t\thasEmail: true,\n\t\t},\n\t\t{\n\t\t\ttitle: \"invalid 'text' template\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.Text = `{{ template \"invalid\" }}`\n\t\t\t},\n\t\t\terrMsg:   `execute text template:`,\n\t\t\thasEmail: true,\n\t\t},\n\t\t{\n\t\t\ttitle: \"invalid 'html' template\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.HTML = `{{ template \"invalid\" }}`\n\t\t\t},\n\t\t\terrMsg:   `execute html template:`,\n\t\t\thasEmail: true,\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\tif len(tc.errMsg) == 0 {\n\t\t\t\tt.Fatal(\"please define the expected error message\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\temailCfg := &config.EmailConfig{\n\t\t\t\tSmarthost: c.Smarthost,\n\t\t\t\tTo:        emailTo,\n\t\t\t\tFrom:      emailFrom,\n\t\t\t\tHTML:      \"HTML body\",\n\t\t\t\tText:      \"Text body\",\n\t\t\t\tHeaders: map[string]string{\n\t\t\t\t\t\"Subject\": \"{{ len .Alerts }} {{ .Status }} alert(s)\",\n\t\t\t\t},\n\t\t\t}\n\t\t\tif tc.updateCfg != nil {\n\t\t\t\ttc.updateCfg(emailCfg)\n\t\t\t}\n\n\t\t\t_, retry, err := notifyEmail(t, emailCfg, c.Server)\n\t\t\trequire.Error(t, err)\n\t\t\trequire.Contains(t, err.Error(), tc.errMsg)\n\t\t\trequire.False(t, retry)\n\n\t\t\te, err := c.Server.getLastEmail(t)\n\t\t\trequire.NoError(t, err)\n\t\t\tif tc.hasEmail {\n\t\t\t\trequire.NotNil(t, e)\n\t\t\t} else {\n\t\t\t\trequire.Nil(t, e)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestEmailNotifyWithDoneContext tries to send an email with a context that is done.\nfunc TestEmailNotifyWithDoneContext(t *testing.T) {\n\tcfgFile := os.Getenv(emailNoAuthConfigVar)\n\tif len(cfgFile) == 0 {\n\t\tt.Skipf(\"%s not set\", emailNoAuthConfigVar)\n\t}\n\n\tc, err := loadEmailTestConfiguration(cfgFile)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tcancel()\n\t_, _, err = notifyEmailWithContext(\n\t\tctx,\n\t\tt,\n\t\t&config.EmailConfig{\n\t\t\tSmarthost: c.Smarthost,\n\t\t\tTo:        emailTo,\n\t\t\tFrom:      emailFrom,\n\t\t\tHTML:      \"HTML body\",\n\t\t\tText:      \"Text body\",\n\t\t},\n\t\tc.Server,\n\t)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"establish connection to server\")\n}\n\n// TestEmailNotifyWithoutAuthentication sends an email to an instance of\n// MailDev configured with no authentication then it checks that the server has\n// successfully processed the email.\nfunc TestEmailNotifyWithoutAuthentication(t *testing.T) {\n\tcfgFile := os.Getenv(emailNoAuthConfigVar)\n\tif len(cfgFile) == 0 {\n\t\tt.Skipf(\"%s not set\", emailNoAuthConfigVar)\n\t}\n\n\tc, err := loadEmailTestConfiguration(cfgFile)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tmail, _, err := notifyEmail(\n\t\tt,\n\t\t&config.EmailConfig{\n\t\t\tSmarthost: c.Smarthost,\n\t\t\tTo:        emailTo,\n\t\t\tFrom:      emailFrom,\n\t\t\tHTML:      \"HTML body\",\n\t\t\tText:      \"Text body\",\n\t\t},\n\t\tc.Server,\n\t)\n\trequire.NoError(t, err)\n\tvar (\n\t\tfoundMsgID bool\n\t\theaders    []string\n\t)\n\tfor k := range mail.Headers {\n\t\tif strings.ToLower(k) == \"message-id\" {\n\t\t\tfoundMsgID = true\n\t\t\tbreak\n\t\t}\n\t\theaders = append(headers, k)\n\t}\n\trequire.True(t, foundMsgID, \"Couldn't find 'message-id' in %v\", headers)\n}\n\n// TestEmailNotifyWithSTARTTLS connects to the server, upgrades the connection\n// to TLS, sends an email then it checks that the server has successfully\n// processed the email.\n// MailDev doesn't support STARTTLS and authentication at the same time so it\n// is the only way to test successful STARTTLS.\nfunc TestEmailNotifyWithSTARTTLS(t *testing.T) {\n\tt.Skip(\"Skipping test as STARTTLS is funky with MailDev, see https://github.com/maildev/maildev/pull/469\")\n\tcfgFile := os.Getenv(emailNoAuthConfigVar)\n\tif len(cfgFile) == 0 {\n\t\tt.Skipf(\"%s not set\", emailNoAuthConfigVar)\n\t}\n\n\tc, err := loadEmailTestConfiguration(cfgFile)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttrueVar := true\n\t_, _, err = notifyEmail(\n\t\tt,\n\t\t&config.EmailConfig{\n\t\t\tSmarthost:  c.Smarthost,\n\t\t\tTo:         emailTo,\n\t\t\tFrom:       emailFrom,\n\t\t\tHTML:       \"HTML body\",\n\t\t\tText:       \"Text body\",\n\t\t\tRequireTLS: &trueVar,\n\t\t\t// MailDev embeds a self-signed certificate which can't be retrieved.\n\t\t\tTLSConfig: &commoncfg.TLSConfig{InsecureSkipVerify: true},\n\t\t},\n\t\tc.Server,\n\t)\n\trequire.NoError(t, err)\n}\n\n// TestEmailNotifyWithAuthentication sends emails to an instance of MailDev\n// configured with authentication.\nfunc TestEmailNotifyWithAuthentication(t *testing.T) {\n\tcfgFile := os.Getenv(emailAuthConfigVar)\n\tif len(cfgFile) == 0 {\n\t\tt.Skipf(\"%s not set\", emailAuthConfigVar)\n\t}\n\n\tc, err := loadEmailTestConfiguration(cfgFile)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttd := t.TempDir()\n\tfileWithCorrectPassword, err := os.CreateTemp(td, \"smtp-password-correct\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = fileWithCorrectPassword.WriteString(c.Password)\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tfileWithIncorrectPassword, err := os.CreateTemp(td, \"smtp-password-incorrect\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = fileWithIncorrectPassword.WriteString(c.Password + \"wrong\")\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tfor _, tc := range []struct {\n\t\ttitle     string\n\t\tupdateCfg func(*config.EmailConfig)\n\n\t\terrMsg string\n\t\tretry  bool\n\t}{\n\t\t{\n\t\t\ttitle: \"email with authentication\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.AuthUsername = c.Username\n\t\t\t\tcfg.AuthPassword = commoncfg.Secret(c.Password)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle: \"email with authentication (password from file)\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.AuthUsername = c.Username\n\t\t\t\tcfg.AuthPasswordFile = fileWithCorrectPassword.Name()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle: \"HTML-only email\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.AuthUsername = c.Username\n\t\t\t\tcfg.AuthPassword = commoncfg.Secret(c.Password)\n\t\t\t\tcfg.Text = \"\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle: \"text-only email\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.AuthUsername = c.Username\n\t\t\t\tcfg.AuthPassword = commoncfg.Secret(c.Password)\n\t\t\t\tcfg.HTML = \"\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle: \"multiple To addresses\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.AuthUsername = c.Username\n\t\t\t\tcfg.AuthPassword = commoncfg.Secret(c.Password)\n\t\t\t\tcfg.To = strings.Join([]string{emailTo, emailFrom}, \",\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle: \"no more than one From address\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.AuthUsername = c.Username\n\t\t\t\tcfg.AuthPassword = commoncfg.Secret(c.Password)\n\t\t\t\tcfg.From = strings.Join([]string{emailFrom, emailTo}, \",\")\n\t\t\t},\n\n\t\t\terrMsg: \"must be exactly one 'from' address\",\n\t\t\tretry:  false,\n\t\t},\n\t\t{\n\t\t\ttitle: \"wrong credentials\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.AuthUsername = c.Username\n\t\t\t\tcfg.AuthPassword = commoncfg.Secret(c.Password + \"wrong\")\n\t\t\t},\n\n\t\t\terrMsg: \"Invalid username or password\",\n\t\t\tretry:  true,\n\t\t},\n\t\t{\n\t\t\ttitle: \"wrong credentials (password from file)\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.AuthUsername = c.Username\n\t\t\t\tcfg.AuthPasswordFile = fileWithIncorrectPassword.Name()\n\t\t\t},\n\n\t\t\terrMsg: \"Invalid username or password\",\n\t\t\tretry:  true,\n\t\t},\n\t\t{\n\t\t\ttitle: \"wrong credentials (missing password file)\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.AuthUsername = c.Username\n\t\t\t\tcfg.AuthPasswordFile = \"/does/not/exist\"\n\t\t\t},\n\n\t\t\terrMsg: \"could not read\",\n\t\t\tretry:  true,\n\t\t},\n\t\t{\n\t\t\ttitle:  \"no credentials\",\n\t\t\terrMsg: \"authentication Required\",\n\t\t\tretry:  true,\n\t\t},\n\t\t{\n\t\t\ttitle: \"try to enable STARTTLS\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.RequireTLS = new(bool)\n\t\t\t\t*cfg.RequireTLS = true\n\t\t\t},\n\n\t\t\terrMsg: \"does not advertise the STARTTLS extension\",\n\t\t\tretry:  true,\n\t\t},\n\t\t{\n\t\t\ttitle: \"invalid Hello string\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.AuthUsername = c.Username\n\t\t\t\tcfg.AuthPassword = commoncfg.Secret(c.Password)\n\t\t\t\tcfg.Hello = \"invalid hello string\"\n\t\t\t},\n\n\t\t\terrMsg: \"501 Error\",\n\t\t\tretry:  true,\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\temailCfg := &config.EmailConfig{\n\t\t\t\tSmarthost: c.Smarthost,\n\t\t\t\tTo:        emailTo,\n\t\t\t\tFrom:      emailFrom,\n\t\t\t\tHTML:      \"HTML body\",\n\t\t\t\tText:      \"Text body\",\n\t\t\t\tHeaders: map[string]string{\n\t\t\t\t\t\"Subject\": \"{{ len .Alerts }} {{ .Status }} alert(s)\",\n\t\t\t\t},\n\t\t\t}\n\t\t\tif tc.updateCfg != nil {\n\t\t\t\ttc.updateCfg(emailCfg)\n\t\t\t}\n\n\t\t\te, retry, err := notifyEmail(t, emailCfg, c.Server)\n\t\t\tif len(tc.errMsg) > 0 {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), tc.errMsg)\n\t\t\t\trequire.Equal(t, tc.retry, retry)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.Equal(t, \"1 firing alert(s)\", e.Subject)\n\n\t\t\tgetAddresses := func(addresses []map[string]string) []string {\n\t\t\t\tres := make([]string, 0, len(addresses))\n\t\t\t\tfor _, addr := range addresses {\n\t\t\t\t\tres = append(res, addr[\"address\"])\n\t\t\t\t}\n\t\t\t\treturn res\n\t\t\t}\n\t\t\tto := getAddresses(e.To)\n\t\t\tfrom := getAddresses(e.From)\n\t\t\trequire.Equal(t, strings.Split(emailCfg.To, \",\"), to)\n\t\t\trequire.Equal(t, strings.Split(emailCfg.From, \",\"), from)\n\n\t\t\tif len(emailCfg.HTML) > 0 {\n\t\t\t\trequire.Equal(t, emailCfg.HTML, *e.HTML)\n\t\t\t} else {\n\t\t\t\trequire.Nil(t, e.HTML)\n\t\t\t}\n\n\t\t\tif len(emailCfg.Text) > 0 {\n\t\t\t\trequire.Equal(t, emailCfg.Text, *e.Text)\n\t\t\t} else {\n\t\t\t\trequire.Nil(t, e.Text)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEmailConfigNoAuthMechs(t *testing.T) {\n\temail := &Email{\n\t\tconf: &config.EmailConfig{AuthUsername: \"test\"}, tmpl: &template.Template{}, logger: promslog.NewNopLogger(),\n\t}\n\t_, err := email.auth(\"\")\n\trequire.Error(t, err)\n\trequire.Equal(t, \"unknown auth mechanism: \", err.Error())\n}\n\nfunc TestEmailConfigMissingAuthParam(t *testing.T) {\n\tconf := &config.EmailConfig{AuthUsername: \"test\"}\n\temail := &Email{\n\t\tconf: conf, tmpl: &template.Template{}, logger: promslog.NewNopLogger(),\n\t}\n\t_, err := email.auth(\"CRAM-MD5\")\n\trequire.Error(t, err)\n\trequire.Equal(t, \"missing secret for CRAM-MD5 auth mechanism\", err.Error())\n\n\t_, err = email.auth(\"PLAIN\")\n\trequire.Error(t, err)\n\trequire.Equal(t, \"missing password for PLAIN auth mechanism\", err.Error())\n\n\t_, err = email.auth(\"LOGIN\")\n\trequire.Error(t, err)\n\trequire.Equal(t, \"missing password for LOGIN auth mechanism\", err.Error())\n\n\t_, err = email.auth(\"PLAIN LOGIN\")\n\trequire.Error(t, err)\n\trequire.Equal(t, \"missing password for PLAIN auth mechanism\\nmissing password for LOGIN auth mechanism\", err.Error())\n}\n\nfunc TestEmailNoUsernameStillOk(t *testing.T) {\n\temail := &Email{\n\t\tconf: &config.EmailConfig{}, tmpl: &template.Template{}, logger: promslog.NewNopLogger(),\n\t}\n\ta, err := email.auth(\"CRAM-MD5\")\n\trequire.NoError(t, err)\n\trequire.Nil(t, a)\n}\n\n// TestEmailRejected simulates the failure of an otherwise valid message submission which fails at a later point than\n// was previously expected by the code.\nfunc TestEmailRejected(t *testing.T) {\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\n\tt.Cleanup(cancel)\n\n\t// Setup mock SMTP server which will reject at the DATA stage.\n\tsrv, l, err := mockSMTPServer(t)\n\trequire.NoError(t, err)\n\tt.Cleanup(func() {\n\t\t// We expect that the server has already been closed in the test.\n\t\trequire.ErrorIs(t, srv.Shutdown(ctx), smtp.ErrServerClosed)\n\t})\n\n\tdone := make(chan any, 1)\n\tgo func() {\n\t\t// nolint:testifylint // require cannot be called outside the main goroutine: https://pkg.go.dev/testing#T.FailNow\n\t\tassert.NoError(t, srv.Serve(l))\n\t\tclose(done)\n\t}()\n\n\t// Wait for mock SMTP server to become ready.\n\trequire.Eventuallyf(t, func() bool {\n\t\tc, err := smtp.Dial(srv.Addr)\n\t\tif err != nil {\n\t\t\tt.Logf(\"dial failed to %q: %s\", srv.Addr, err)\n\t\t\treturn false\n\t\t}\n\n\t\t// Ping.\n\t\tif err = c.Noop(); err != nil {\n\t\t\tt.Logf(\"ping failed to %q: %s\", srv.Addr, err)\n\t\t\treturn false\n\t\t}\n\n\t\t// Ensure we close the connection to not prevent server from shutting down cleanly.\n\t\tif err = c.Close(); err != nil {\n\t\t\tt.Logf(\"close failed to %q: %s\", srv.Addr, err)\n\t\t\treturn false\n\t\t}\n\n\t\treturn true\n\t}, time.Second*10, time.Millisecond*100, \"mock SMTP server failed to start\")\n\n\t// Use mock SMTP server and prepare alert to be sent.\n\trequire.IsType(t, &net.TCPAddr{}, l.Addr())\n\taddr := l.Addr().(*net.TCPAddr)\n\tcfg := &config.EmailConfig{\n\t\tSmarthost: config.HostPort{Host: addr.IP.String(), Port: strconv.Itoa(addr.Port)},\n\t\tHello:     \"localhost\",\n\t\tHeaders:   make(map[string]string),\n\t\tFrom:      \"alertmanager@system\",\n\t\tTo:        \"sre@company\",\n\t}\n\ttmpl, firingAlert, err := prepare(cfg)\n\trequire.NoError(t, err)\n\n\te := New(cfg, tmpl, promslog.NewNopLogger())\n\n\t// Send the alert to mock SMTP server.\n\tretry, err := e.Notify(context.Background(), firingAlert)\n\trequire.ErrorContains(t, err, \"501 5.5.4 Rejected!\")\n\trequire.True(t, retry)\n\trequire.NoError(t, srv.Shutdown(ctx))\n\n\trequire.Eventuallyf(t, func() bool {\n\t\t<-done\n\t\treturn true\n\t}, time.Second*10, time.Millisecond*100, \"mock SMTP server goroutine failed to close in time\")\n}\n\nfunc mockSMTPServer(t *testing.T) (*smtp.Server, net.Listener, error) {\n\tt.Helper()\n\n\t// Listen on the next available high port.\n\tl, err := net.Listen(\"tcp\", \"localhost:0\")\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"connect: %w\", err)\n\t}\n\n\taddr, ok := l.Addr().(*net.TCPAddr)\n\tif !ok {\n\t\treturn nil, nil, fmt.Errorf(\"unexpected address type: %T\", l.Addr())\n\t}\n\n\ts := smtp.NewServer(&rejectingBackend{})\n\ts.Addr = addr.String()\n\ts.WriteTimeout = 10 * time.Second\n\ts.ReadTimeout = 10 * time.Second\n\n\treturn s, l, nil\n}\n\n// rejectingBackend will reject submission at the DATA stage.\ntype rejectingBackend struct{}\n\nfunc (b *rejectingBackend) NewSession(c *smtp.Conn) (smtp.Session, error) {\n\treturn &mockSMTPSession{\n\t\tconn:    c,\n\t\tbackend: b,\n\t}, nil\n}\n\ntype mockSMTPSession struct {\n\tconn    *smtp.Conn\n\tbackend smtp.Backend\n}\n\nfunc (s *mockSMTPSession) Mail(string, *smtp.MailOptions) error {\n\treturn nil\n}\n\nfunc (s *mockSMTPSession) Rcpt(string, *smtp.RcptOptions) error {\n\treturn nil\n}\n\nfunc (s *mockSMTPSession) Data(io.Reader) error {\n\treturn &smtp.SMTPError{Code: 501, EnhancedCode: smtp.EnhancedCode{5, 5, 4}, Message: \"Rejected!\"}\n}\n\nfunc (*mockSMTPSession) Reset() {}\n\nfunc (*mockSMTPSession) Logout() error { return nil }\n\nfunc TestEmailNotifyWithThreading(t *testing.T) {\n\tcfgFile := os.Getenv(emailNoAuthConfigVar)\n\tif len(cfgFile) == 0 {\n\t\tt.Skipf(\"%s not set\", emailNoAuthConfigVar)\n\t}\n\n\tc, err := loadEmailTestConfiguration(cfgFile)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tfor _, tc := range []struct {\n\t\tname         string\n\t\tthreadByDate string\n\t\twantDatePart bool\n\t}{\n\t\t{\n\t\t\tname:         \"threading with daily date (default)\",\n\t\t\tthreadByDate: \"\",\n\t\t\twantDatePart: true,\n\t\t},\n\t\t{\n\t\t\tname:         \"threading with explicit daily\",\n\t\t\tthreadByDate: \"daily\",\n\t\t\twantDatePart: true,\n\t\t},\n\t\t{\n\t\t\tname:         \"threading without date\",\n\t\t\tthreadByDate: \"none\",\n\t\t\twantDatePart: false,\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Create context with group key (required for threading).\n\t\t\tctx := notify.WithGroupKey(context.Background(), \"test-group-key\")\n\n\t\t\temailCfg := &config.EmailConfig{\n\t\t\t\tSmarthost: c.Smarthost,\n\t\t\t\tTo:        emailTo,\n\t\t\t\tFrom:      emailFrom,\n\t\t\t\tHTML:      \"HTML body\",\n\t\t\t\tText:      \"Text body\",\n\t\t\t\tThreading: config.ThreadingConfig{\n\t\t\t\t\tEnabled:      true,\n\t\t\t\t\tThreadByDate: tc.threadByDate,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tmail, _, err := notifyEmailWithContext(ctx, t, emailCfg, c.Server)\n\t\t\trequire.NoError(t, err)\n\n\t\t\treferencesValue := mail.Headers[\"references\"]\n\t\t\tinReplyToValue := mail.Headers[\"in-reply-to\"]\n\n\t\t\trequire.NotEmpty(t, referencesValue, \"References header not found in %v\", mail.Headers)\n\t\t\trequire.NotEmpty(t, inReplyToValue, \"In-Reply-To header not found in %v\", mail.Headers)\n\n\t\t\trequire.Equal(t, referencesValue, inReplyToValue, \"References and In-Reply-To should match\")\n\n\t\t\t// Verify the format: <alert-HASH-DATE@alertmanager>\n\t\t\trequire.Contains(t, referencesValue, \"<alert-\")\n\t\t\trequire.Contains(t, referencesValue, \"@alertmanager>\")\n\n\t\t\tif tc.wantDatePart {\n\t\t\t\ttoday := time.Now().Format(\"2006-01-02\")\n\t\t\t\trequire.Contains(t, referencesValue, today, \"threading header should contain today's date\")\n\t\t\t} else {\n\t\t\t\t// With thread_by_date: none, there should be no date\n\t\t\t\t// (empty string between hash and @).\n\t\t\t\trequire.Contains(t, referencesValue, \"-@alertmanager>\", \"threading header should have empty date part\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEmailGetPassword(t *testing.T) {\n\tpasswordFile, err := os.CreateTemp(\"\", \"smtp-password\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = passwordFile.WriteString(\"secret\")\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tfor _, tc := range []struct {\n\t\ttitle     string\n\t\tupdateCfg func(*config.EmailConfig)\n\n\t\terrMsg string\n\t}{\n\t\t{\n\t\t\ttitle: \"password from field\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.AuthPassword = \"secret\"\n\t\t\t\tcfg.AuthPasswordFile = \"\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle: \"password from file field\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.AuthPassword = \"\"\n\t\t\t\tcfg.AuthPasswordFile = passwordFile.Name()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle: \"password file path incorrect\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.AuthPassword = \"\"\n\t\t\t\tcfg.AuthPasswordFile = \"/does/not/exist\"\n\t\t\t},\n\t\t\terrMsg: \"could not read\",\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\temail := &Email{\n\t\t\t\tconf: &config.EmailConfig{},\n\t\t\t}\n\n\t\t\ttc.updateCfg(email.conf)\n\n\t\t\tpassword, err := email.getPassword()\n\t\t\tif len(tc.errMsg) > 0 {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), tc.errMsg)\n\t\t\t\trequire.Empty(t, password)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, \"secret\", password)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEmailGetSecret(t *testing.T) {\n\tsecretFile, err := os.CreateTemp(\"\", \"smtp-password\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = secretFile.WriteString(\"secret\")\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tfor _, tc := range []struct {\n\t\ttitle     string\n\t\tupdateCfg func(*config.EmailConfig)\n\n\t\terrMsg string\n\t}{\n\t\t{\n\t\t\ttitle: \"secret from field\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.AuthSecret = \"secret\"\n\t\t\t\tcfg.AuthSecretFile = \"\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle: \"secret from file field\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.AuthSecret = \"\"\n\t\t\t\tcfg.AuthSecretFile = secretFile.Name()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle: \"secret file path incorrect\",\n\t\t\tupdateCfg: func(cfg *config.EmailConfig) {\n\t\t\t\tcfg.AuthSecret = \"\"\n\t\t\t\tcfg.AuthSecretFile = \"/does/not/exist\"\n\t\t\t},\n\t\t\terrMsg: \"could not read\",\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\temail := &Email{\n\t\t\t\tconf: &config.EmailConfig{},\n\t\t\t}\n\n\t\t\ttc.updateCfg(email.conf)\n\n\t\t\tsecret, err := email.getAuthSecret()\n\t\t\tif len(tc.errMsg) > 0 {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), tc.errMsg)\n\t\t\t\trequire.Empty(t, secret)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, \"secret\", secret)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEmailImplicitTLS(t *testing.T) {\n\ttests := []struct {\n\t\tname             string\n\t\tport             string\n\t\tforceImplicitTLS *bool\n\t\texpectImplicit   bool\n\t}{\n\t\t{\n\t\t\tname:             \"default behavior - port 465\",\n\t\t\tport:             \"465\",\n\t\t\tforceImplicitTLS: nil,\n\t\t\texpectImplicit:   true,\n\t\t},\n\t\t{\n\t\t\tname:             \"default behavior - port 587\",\n\t\t\tport:             \"587\",\n\t\t\tforceImplicitTLS: nil,\n\t\t\texpectImplicit:   false,\n\t\t},\n\t\t{\n\t\t\tname:             \"force implicit_tls=true on port 587\",\n\t\t\tport:             \"587\",\n\t\t\tforceImplicitTLS: ptrTo(true),\n\t\t\texpectImplicit:   true,\n\t\t},\n\t\t{\n\t\t\tname:             \"force implicit_tls=true on custom port\",\n\t\t\tport:             \"8465\",\n\t\t\tforceImplicitTLS: ptrTo(true),\n\t\t\texpectImplicit:   true,\n\t\t},\n\t\t{\n\t\t\tname:             \"implicit_tls=false disables implicit TLS on port 465\",\n\t\t\tport:             \"465\",\n\t\t\tforceImplicitTLS: ptrTo(false),\n\t\t\texpectImplicit:   false,\n\t\t},\n\t\t{\n\t\t\tname:             \"implicit_tls=false behaves like default on port 587\",\n\t\t\tport:             \"587\",\n\t\t\tforceImplicitTLS: ptrTo(false),\n\t\t\texpectImplicit:   false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcfg := &config.EmailConfig{\n\t\t\t\tSmarthost:        config.HostPort{Host: \"localhost\", Port: tt.port},\n\t\t\t\tForceImplicitTLS: tt.forceImplicitTLS,\n\t\t\t}\n\n\t\t\t// Simulate the judgment logic\n\t\t\tvar useImplicitTLS bool\n\t\t\tif cfg.ForceImplicitTLS != nil {\n\t\t\t\tuseImplicitTLS = *cfg.ForceImplicitTLS\n\t\t\t} else {\n\t\t\t\tuseImplicitTLS = cfg.Smarthost.Port == \"465\"\n\t\t\t}\n\n\t\t\trequire.Equal(t, tt.expectImplicit, useImplicitTLS,\n\t\t\t\t\"Expected useImplicitTLS=%v for port=%s with forceImplicitTLS=%v\",\n\t\t\t\ttt.expectImplicit, tt.port, tt.forceImplicitTLS)\n\t\t})\n\t}\n}\n\nfunc ptrTo(b bool) *bool {\n\treturn &b\n}\n"
  },
  {
    "path": "notify/email/testdata/auth-local.yml",
    "content": "smarthost: 127.0.0.1:1026\nserver: http://127.0.0.1:1081/\nusername: user\npassword: pass\n"
  },
  {
    "path": "notify/email/testdata/auth.yml",
    "content": "smarthost: maildev-auth:1025\nserver: http://maildev-auth:1080/\nusername: user\npassword: pass\n"
  },
  {
    "path": "notify/email/testdata/noauth-local.yml",
    "content": "smarthost: 127.0.0.1:1025\nserver: http://127.0.0.1:1080/\n"
  },
  {
    "path": "notify/email/testdata/noauth.yml",
    "content": "smarthost: maildev-noauth:1025\nserver: http://maildev-noauth:1080/\n"
  },
  {
    "path": "notify/incidentio/incidentio.go",
    "content": "// Copyright 2025 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage incidentio\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nconst (\n\t// MaxPayloadSize is the maximum size of the JSON payload incident.io accepts (512KB).\n\tmaxPayloadSize = 512 * 1024\n)\n\n// Notifier implements a Notifier for incident.io.\ntype Notifier struct {\n\tconf    *config.IncidentioConfig\n\ttmpl    *template.Template\n\tlogger  *slog.Logger\n\tclient  *http.Client\n\tretrier *notify.Retrier\n}\n\n// New returns a new incident.io notifier.\nfunc New(conf *config.IncidentioConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {\n\t// conf.HTTPConfig is likely to be the global shared HTTPConfig, so we take a\n\t// copy to avoid modifying it.\n\thttpConfig := *conf.HTTPConfig\n\n\t// If an alert source token is provided, we use that one instead of whatever configuration is included in `http_config`.\n\tvar token string\n\tif conf.AlertSourceToken != \"\" {\n\t\ttoken = string(conf.AlertSourceToken)\n\t}\n\n\tif conf.AlertSourceTokenFile != \"\" {\n\t\tcontent, err := os.ReadFile(conf.AlertSourceTokenFile)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to read alert_source_token_file: %w\", err)\n\t\t}\n\t\ttoken = strings.TrimSpace(string(content))\n\t}\n\n\tif token != \"\" {\n\t\thttpConfig.Authorization = &commoncfg.Authorization{\n\t\t\tType:        \"Bearer\",\n\t\t\tCredentials: commoncfg.Secret(token),\n\t\t}\n\t}\n\n\tclient, err := notify.NewClientWithTracing(httpConfig, \"incidentio\", httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Notifier{\n\t\tconf:   conf,\n\t\ttmpl:   t,\n\t\tlogger: l,\n\t\tclient: client,\n\t\t// Always retry on 429 (rate limiting) and 5xx response codes.\n\t\tretrier: &notify.Retrier{\n\t\t\tRetryCodes:        []int{http.StatusTooManyRequests},\n\t\t\tCustomDetailsFunc: errDetails,\n\t\t},\n\t}, nil\n}\n\n// Message defines the JSON object sent to incident.io endpoints.\ntype Message struct {\n\t*template.Data\n\n\t// The protocol version.\n\tVersion         string `json:\"version\"`\n\tGroupKey        string `json:\"groupKey\"`\n\tTruncatedAlerts uint64 `json:\"truncatedAlerts\"`\n}\n\nfunc truncateAlerts(maxAlerts uint64, alerts []*types.Alert) ([]*types.Alert, uint64) {\n\tif maxAlerts != 0 && uint64(len(alerts)) > maxAlerts {\n\t\treturn alerts[:maxAlerts], uint64(len(alerts)) - maxAlerts\n\t}\n\n\treturn alerts, 0\n}\n\n// encodeMessage encodes the message and drops all alerts except the first one if it exceeds maxPayloadSize.\nfunc (n *Notifier) encodeMessage(msg *Message) (bytes.Buffer, error) {\n\tvar buf bytes.Buffer\n\tif err := json.NewEncoder(&buf).Encode(msg); err != nil {\n\t\treturn buf, fmt.Errorf(\"failed to encode incident.io message: %w\", err)\n\t}\n\n\tif buf.Len() <= maxPayloadSize {\n\t\treturn buf, nil\n\t}\n\n\toriginalSize := buf.Len()\n\n\t// Drop all but the first alert in the message. For most use cases, a single\n\t// alert will be created in incident.io for the group, so including more than\n\t// one alert in that group is useful but non-essential.\n\tmsg.Alerts = msg.Alerts[:1]\n\n\t// Re-encode after annotation truncation\n\tbuf.Reset()\n\tif err := json.NewEncoder(&buf).Encode(msg); err != nil {\n\t\treturn buf, fmt.Errorf(\"failed to encode incident.io message after annotation truncation: %w\", err)\n\t}\n\n\tif buf.Len() <= maxPayloadSize {\n\t\tn.logger.Warn(\"Truncated alert content due to incident.io payload size limit\",\n\t\t\t\"original_size\", originalSize,\n\t\t\t\"final_size\", buf.Len(),\n\t\t\t\"max_size\", maxPayloadSize)\n\n\t\treturn buf, nil\n\t}\n\n\t// Still attempt to send the message even if it exceeds the limit, but log an\n\t// error to explain why this is likely to fail.\n\tn.logger.Error(\"Truncated alert content due to incident.io payload size limit, but still exceeds limit\",\n\t\t\"original_size\", originalSize,\n\t\t\"final_size\", buf.Len(),\n\t\t\"max_size\", maxPayloadSize)\n\n\treturn buf, nil\n}\n\n// Notify implements the Notifier interface.\nfunc (n *Notifier) Notify(ctx context.Context, alerts ...*types.Alert) (bool, error) {\n\talerts, numTruncated := truncateAlerts(n.conf.MaxAlerts, alerts)\n\tdata := notify.GetTemplateData(ctx, n.tmpl, alerts, n.logger)\n\n\tgroupKey, err := notify.ExtractGroupKey(ctx)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tn.logger.Debug(\"incident.io notification\", \"groupKey\", groupKey)\n\n\tmsg := &Message{\n\t\tVersion:         \"1\",\n\t\tData:            data,\n\t\tGroupKey:        groupKey.String(),\n\t\tTruncatedAlerts: numTruncated,\n\t}\n\n\tbuf, err := n.encodeMessage(msg)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tvar url string\n\tif n.conf.URL != nil {\n\t\turl = n.conf.URL.String()\n\t} else {\n\t\tcontent, err := os.ReadFile(n.conf.URLFile)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"read url_file: %w\", err)\n\t\t}\n\t\turl = strings.TrimSpace(string(content))\n\t}\n\n\tif n.conf.Timeout > 0 {\n\t\tctxWithTimeout, cancel := context.WithTimeoutCause(ctx, n.conf.Timeout, fmt.Errorf(\"configured incident.io timeout reached (%s)\", n.conf.Timeout))\n\t\tdefer cancel()\n\t\tctx = ctxWithTimeout\n\t}\n\n\tresp, err := notify.PostJSON(ctx, n.client, url, &buf)\n\tif err != nil {\n\t\tif ctx.Err() != nil {\n\t\t\terr = fmt.Errorf(\"%w: %w\", err, context.Cause(ctx))\n\t\t}\n\t\treturn true, notify.RedactURL(err)\n\t}\n\tdefer notify.Drain(resp)\n\n\tshouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)\n\tif err != nil {\n\t\treturn shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)\n\t}\n\treturn shouldRetry, err\n}\n\n// errDetails extracts error details from the response for better error messages.\nfunc errDetails(_ int, body io.Reader) string {\n\tif body == nil {\n\t\treturn \"\"\n\t}\n\n\t// Try to decode the error message from JSON response\n\tvar errorResponse struct {\n\t\tMessage string   `json:\"message\"`\n\t\tErrors  []string `json:\"errors\"`\n\t\tError   string   `json:\"error\"`\n\t}\n\n\tif err := json.NewDecoder(body).Decode(&errorResponse); err != nil {\n\t\treturn \"\"\n\t}\n\n\tvar parts []string\n\tif errorResponse.Message != \"\" {\n\t\tparts = append(parts, errorResponse.Message)\n\t}\n\tif errorResponse.Error != \"\" {\n\t\tparts = append(parts, errorResponse.Error)\n\t}\n\tif len(errorResponse.Errors) > 0 {\n\t\tparts = append(parts, strings.Join(errorResponse.Errors, \", \"))\n\t}\n\n\treturn strings.Join(parts, \": \")\n}\n"
  },
  {
    "path": "notify/incidentio/incidentio_test.go",
    "content": "// Copyright 2025 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage incidentio\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/notify/test\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nfunc TestIncidentIORetry(t *testing.T) {\n\tnotifier, err := New(\n\t\t&config.IncidentioConfig{\n\t\t\tURL:              &amcommoncfg.URL{URL: &url.URL{Scheme: \"https\", Host: \"example.com\"}},\n\t\t\tHTTPConfig:       &commoncfg.HTTPClientConfig{},\n\t\t\tAlertSourceToken: \"test-token\",\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\tretryCodes := append(test.DefaultRetryCodes(), http.StatusTooManyRequests)\n\tfor statusCode, expected := range test.RetryTests(retryCodes) {\n\t\tactual, _ := notifier.retrier.Check(statusCode, nil)\n\t\trequire.Equal(t, expected, actual, \"retry - error on status %d\", statusCode)\n\t}\n}\n\nfunc TestIncidentIORedactedURL(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tnotifier, err := New(\n\t\t&config.IncidentioConfig{\n\t\t\tURL:              &amcommoncfg.URL{URL: u},\n\t\t\tHTTPConfig:       &commoncfg.HTTPClientConfig{},\n\t\t\tAlertSourceToken: \"test-token\",\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String())\n}\n\nfunc TestIncidentIOURLFromFile(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tf, err := os.CreateTemp(t.TempDir(), \"incidentio_test\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = f.WriteString(u.String() + \"\\n\")\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tnotifier, err := New(\n\t\t&config.IncidentioConfig{\n\t\t\tURLFile:          f.Name(),\n\t\t\tHTTPConfig:       &commoncfg.HTTPClientConfig{},\n\t\t\tAlertSourceToken: \"test-token\",\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String())\n}\n\nfunc TestIncidentIOTruncateAlerts(t *testing.T) {\n\talerts := make([]*types.Alert, 10)\n\n\ttruncatedAlerts, numTruncated := truncateAlerts(0, alerts)\n\trequire.Len(t, truncatedAlerts, 10)\n\trequire.EqualValues(t, 0, numTruncated)\n\n\ttruncatedAlerts, numTruncated = truncateAlerts(4, alerts)\n\trequire.Len(t, truncatedAlerts, 4)\n\trequire.EqualValues(t, 6, numTruncated)\n\n\ttruncatedAlerts, numTruncated = truncateAlerts(100, alerts)\n\trequire.Len(t, truncatedAlerts, 10)\n\trequire.EqualValues(t, 0, numTruncated)\n}\n\nfunc TestIncidentIONotify(t *testing.T) {\n\t// Test regular notifications are correctly sent\n\tserver := httptest.NewServer(http.HandlerFunc(\n\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\t// Verify the content type header\n\t\t\tcontentType := r.Header.Get(\"Content-Type\")\n\t\t\trequire.Equal(t, \"application/json\", contentType)\n\n\t\t\t// Decode the webhook payload\n\t\t\tvar msg Message\n\t\t\trequire.NoError(t, json.NewDecoder(r.Body).Decode(&msg))\n\n\t\t\t// Verify required fields\n\t\t\trequire.Equal(t, \"1\", msg.Version)\n\t\t\trequire.NotEmpty(t, msg.GroupKey)\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t},\n\t))\n\tdefer server.Close()\n\n\tu, err := url.Parse(server.URL)\n\trequire.NoError(t, err)\n\n\tnotifier, err := New(\n\t\t&config.IncidentioConfig{\n\t\t\tURL:              &amcommoncfg.URL{URL: u},\n\t\t\tHTTPConfig:       &commoncfg.HTTPClientConfig{},\n\t\t\tAlertSourceToken: \"test-token\",\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\tctx := context.Background()\n\tctx = notify.WithGroupKey(ctx, \"1\")\n\n\talert := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels: model.LabelSet{\n\t\t\t\t\"alertname\": \"TestAlert\",\n\t\t\t\t\"severity\":  \"critical\",\n\t\t\t},\n\t\t\tStartsAt: time.Now(),\n\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t},\n\t}\n\n\tretry, err := notifier.Notify(ctx, alert)\n\trequire.NoError(t, err)\n\trequire.False(t, retry)\n}\n\nfunc TestIncidentIORetryScenarios(t *testing.T) {\n\ttestCases := []struct {\n\t\tname                   string\n\t\tstatusCode             int\n\t\tresponseBody           []byte\n\t\texpectRetry            bool\n\t\texpectErrorMsgContains string\n\t}{\n\t\t{\n\t\t\tname:                   \"success response\",\n\t\t\tstatusCode:             http.StatusOK,\n\t\t\tresponseBody:           []byte(`{\"status\":\"success\"}`),\n\t\t\texpectRetry:            false,\n\t\t\texpectErrorMsgContains: \"\",\n\t\t},\n\t\t{\n\t\t\tname:                   \"rate limit response\",\n\t\t\tstatusCode:             http.StatusTooManyRequests,\n\t\t\tresponseBody:           []byte(`{\"error\":\"rate limit exceeded\",\"message\":\"Too many requests\"}`),\n\t\t\texpectRetry:            true,\n\t\t\texpectErrorMsgContains: \"rate limit exceeded\",\n\t\t},\n\t\t{\n\t\t\tname:                   \"server error response\",\n\t\t\tstatusCode:             http.StatusInternalServerError,\n\t\t\tresponseBody:           []byte(`{\"error\":\"internal error\"}`),\n\t\t\texpectRetry:            true,\n\t\t\texpectErrorMsgContains: \"internal error\",\n\t\t},\n\t\t{\n\t\t\tname:                   \"client error response\",\n\t\t\tstatusCode:             http.StatusBadRequest,\n\t\t\tresponseBody:           []byte(`{\"error\":\"invalid request\",\"message\":\"Invalid payload format\"}`),\n\t\t\texpectRetry:            false,\n\t\t\texpectErrorMsgContains: \"invalid request\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tserver := httptest.NewServer(http.HandlerFunc(\n\t\t\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tw.WriteHeader(tc.statusCode)\n\t\t\t\t\tw.Write(tc.responseBody)\n\t\t\t\t},\n\t\t\t))\n\t\t\tdefer server.Close()\n\n\t\t\tu, err := url.Parse(server.URL)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tnotifier, err := New(\n\t\t\t\t&config.IncidentioConfig{\n\t\t\t\t\tURL:              &amcommoncfg.URL{URL: u},\n\t\t\t\t\tHTTPConfig:       &commoncfg.HTTPClientConfig{},\n\t\t\t\t\tAlertSourceToken: \"test-token\",\n\t\t\t\t},\n\t\t\t\ttest.CreateTmpl(t),\n\t\t\t\tpromslog.NewNopLogger(),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tctx := context.Background()\n\t\t\tctx = notify.WithGroupKey(ctx, \"1\")\n\n\t\t\talert := &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\"alertname\": \"TestAlert\",\n\t\t\t\t\t\t\"severity\":  \"critical\",\n\t\t\t\t\t},\n\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tretry, err := notifier.Notify(ctx, alert)\n\t\t\tif tc.expectErrorMsgContains == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), tc.expectErrorMsgContains)\n\t\t\t}\n\t\t\trequire.Equal(t, tc.expectRetry, retry)\n\t\t})\n\t}\n}\n\nfunc TestIncidentIOErrDetails(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tname   string\n\t\tstatus int\n\t\tbody   io.Reader\n\t\texpect string\n\t}{\n\t\t{\n\t\t\tname:   \"empty body\",\n\t\t\tstatus: http.StatusBadRequest,\n\t\t\tbody:   nil,\n\t\t\texpect: \"\",\n\t\t},\n\t\t{\n\t\t\tname:   \"single error field\",\n\t\t\tstatus: http.StatusBadRequest,\n\t\t\tbody:   bytes.NewBufferString(`{\"error\":\"Invalid request\"}`),\n\t\t\texpect: \"Invalid request\",\n\t\t},\n\t\t{\n\t\t\tname:   \"message and errors\",\n\t\t\tstatus: http.StatusBadRequest,\n\t\t\tbody:   bytes.NewBufferString(`{\"message\":\"Validation failed\",\"errors\":[\"Field is required\",\"Value too long\"]}`),\n\t\t\texpect: \"Validation failed: Field is required, Value too long\",\n\t\t},\n\t\t{\n\t\t\tname:   \"message and error\",\n\t\t\tstatus: http.StatusTooManyRequests,\n\t\t\tbody:   bytes.NewBufferString(`{\"message\":\"Too many requests\",\"error\":\"Rate limit exceeded\"}`),\n\t\t\texpect: \"Too many requests: Rate limit exceeded\",\n\t\t},\n\t\t{\n\t\t\tname:   \"invalid JSON\",\n\t\t\tstatus: http.StatusBadRequest,\n\t\t\tbody:   bytes.NewBufferString(`{invalid}`),\n\t\t\texpect: \"\",\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := errDetails(tc.status, tc.body)\n\t\t\tif tc.expect == \"\" {\n\t\t\t\trequire.Empty(t, result)\n\t\t\t} else {\n\t\t\t\trequire.Contains(t, result, tc.expect)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIncidentIOPayloadTruncation(t *testing.T) {\n\tlogger := promslog.NewNopLogger()\n\n\tnotifier, err := New(\n\t\t&config.IncidentioConfig{\n\t\t\tURL:              &amcommoncfg.URL{URL: &url.URL{Scheme: \"https\", Host: \"example.com\"}},\n\t\t\tHTTPConfig:       &commoncfg.HTTPClientConfig{},\n\t\t\tAlertSourceToken: \"test-token\",\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tlogger,\n\t)\n\trequire.NoError(t, err)\n\n\t// Create a large annotation that will push payload over 512KB\n\tlargeAnnotation := make([]byte, 100*1024) // 100KB per annotation\n\tfor i := range largeAnnotation {\n\t\tlargeAnnotation[i] = 'a' + byte(i%26)\n\t}\n\tlargeAnnotationStr := string(largeAnnotation)\n\n\t// Create alerts with large annotations\n\tvar alerts []*types.Alert\n\tfor i := range 10 { // 10 alerts * 100KB = 1MB total in annotations alone\n\t\talert := &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"alertname\": model.LabelValue(\"TestAlert\" + string(rune('0'+i))),\n\t\t\t\t\t\"severity\":  \"critical\",\n\t\t\t\t\t\"job\":       \"test-job\",\n\t\t\t\t\t\"instance\":  \"test-instance\",\n\t\t\t\t\t\"env\":       \"production\",\n\t\t\t\t\t\"team\":      \"sre\",\n\t\t\t\t},\n\t\t\t\tAnnotations: model.LabelSet{\n\t\t\t\t\t\"description\": model.LabelValue(largeAnnotationStr),\n\t\t\t\t\t\"runbook\":     model.LabelValue(largeAnnotationStr),\n\t\t\t\t\t\"summary\":     model.LabelValue(\"This is a test alert with very large annotations\"),\n\t\t\t\t},\n\t\t\t\tStartsAt: time.Now(),\n\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t},\n\t\t}\n\t\talerts = append(alerts, alert)\n\t}\n\n\t// Create template data\n\tctx := context.Background()\n\tctx = notify.WithGroupKey(ctx, \"test-group\")\n\tdata := notify.GetTemplateData(ctx, test.CreateTmpl(t), alerts, logger)\n\n\t// Create message\n\tmsg := &Message{\n\t\tVersion:         \"1\",\n\t\tData:            data,\n\t\tGroupKey:        \"test-group\",\n\t\tTruncatedAlerts: 0,\n\t}\n\n\t// Test encoding with truncation\n\tbuf, err := notifier.encodeMessage(msg)\n\trequire.NoError(t, err)\n\n\t// Verify the encoded message is under the size limit\n\trequire.LessOrEqual(t, buf.Len(), maxPayloadSize, \"Encoded message should be under maxPayloadSize after truncation\")\n\n\t// Decode the message to verify truncation happened\n\tvar decodedMsg Message\n\terr = json.NewDecoder(&buf).Decode(&decodedMsg)\n\trequire.NoError(t, err)\n\n\t// Check that all but the first alert was dropped\n\trequire.Len(t, decodedMsg.Alerts, 1, \"Only the first alert should be included after truncation\")\n}\n\nfunc TestIncidentIOPayloadTruncationWithLabelTruncation(t *testing.T) {\n\t// Test extreme case where even after annotation truncation, labels need to be truncated\n\tlogger := promslog.NewNopLogger()\n\n\tnotifier, err := New(\n\t\t&config.IncidentioConfig{\n\t\t\tURL:              &amcommoncfg.URL{URL: &url.URL{Scheme: \"https\", Host: \"example.com\"}},\n\t\t\tHTTPConfig:       &commoncfg.HTTPClientConfig{},\n\t\t\tAlertSourceToken: \"test-token\",\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tlogger,\n\t)\n\trequire.NoError(t, err)\n\n\t// Create many alerts with many labels to push size over limit even without annotations\n\tvar alerts []*types.Alert\n\tfor i := range 100 { // Many alerts\n\t\tlabels := model.LabelSet{\n\t\t\t\"alertname\": model.LabelValue(\"TestAlert\" + string(rune('0'+i%10))),\n\t\t\t\"severity\":  \"critical\",\n\t\t\t\"job\":       \"test-job\",\n\t\t\t\"instance\":  \"test-instance\",\n\t\t}\n\n\t\t// Add many extra labels with long values\n\t\tfor j := range 50 {\n\t\t\tlabelName := model.LabelName(\"label_\" + string(rune('a'+j%26)) + \"_\" + string(rune('0'+j/26)))\n\t\t\tlabelValue := make([]byte, 1024) // 1KB per label value\n\t\t\tfor k := range labelValue {\n\t\t\t\tlabelValue[k] = 'x'\n\t\t\t}\n\t\t\tlabels[labelName] = model.LabelValue(labelValue)\n\t\t}\n\n\t\talert := &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels:   labels,\n\t\t\t\tStartsAt: time.Now(),\n\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t},\n\t\t}\n\t\talerts = append(alerts, alert)\n\t}\n\n\t// Create template data\n\tctx := context.Background()\n\tctx = notify.WithGroupKey(ctx, \"test-group\")\n\tdata := notify.GetTemplateData(ctx, test.CreateTmpl(t), alerts, logger)\n\n\t// Create message\n\tmsg := &Message{\n\t\tVersion:         \"1\",\n\t\tData:            data,\n\t\tGroupKey:        \"test-group\",\n\t\tTruncatedAlerts: 0,\n\t}\n\n\t// Test encoding with truncation\n\tbuf, err := notifier.encodeMessage(msg)\n\trequire.NoError(t, err)\n\n\t// Verify the encoded message is under the size limit\n\trequire.LessOrEqual(t, buf.Len(), maxPayloadSize, \"Encoded message should be under maxPayloadSize after label truncation\")\n\n\t// Decode the message to verify truncation happened\n\tvar decodedMsg Message\n\terr = json.NewDecoder(&buf).Decode(&decodedMsg)\n\trequire.NoError(t, err)\n\n\t// Since we have a lot of alerts with large labels, the encoding might have reduced the number of alerts\n\t// Check that we have fewer alerts if truncation occurred\n\trequire.LessOrEqual(t, len(decodedMsg.Alerts), 100, \"Number of alerts may have been reduced\")\n\n\t// Check that essential labels are preserved in remaining alerts\n\tfor _, alert := range decodedMsg.Alerts {\n\t\t// Essential labels should be preserved\n\t\trequire.Contains(t, alert.Labels[\"alertname\"], \"TestAlert\")\n\t\trequire.Equal(t, \"critical\", alert.Labels[\"severity\"])\n\t\trequire.Equal(t, \"test-job\", alert.Labels[\"job\"])\n\t\trequire.Equal(t, \"test-instance\", alert.Labels[\"instance\"])\n\n\t\t// Check if labels were truncated (will have truncated_labels marker) or if we still have all labels\n\t\tif truncatedLabels, ok := alert.Labels[\"truncated_labels\"]; ok && truncatedLabels == \"true\" {\n\t\t\t// Non-essential labels should be removed\n\t\t\tfor k := range alert.Labels {\n\t\t\t\tif k != \"alertname\" &&\n\t\t\t\t\tk != \"severity\" &&\n\t\t\t\t\tk != \"job\" &&\n\t\t\t\t\tk != \"instance\" &&\n\t\t\t\t\tk != \"truncated_labels\" {\n\t\t\t\t\tt.Errorf(\"Found non-essential label %s that should have been truncated\", k)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "notify/jira/jira.go",
    "content": "// Copyright 2023 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage jira\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nconst (\n\tmaxSummaryLenRunes     = 255\n\tmaxDescriptionLenRunes = 32767\n)\n\n// Notifier implements a Notifier for JIRA notifications.\ntype Notifier struct {\n\tconf    *config.JiraConfig\n\ttmpl    *template.Template\n\tlogger  *slog.Logger\n\tclient  *http.Client\n\tretrier *notify.Retrier\n}\n\nfunc New(c *config.JiraConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {\n\tclient, err := notify.NewClientWithTracing(*c.HTTPConfig, \"jira\", httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Notifier{\n\t\tconf:    c,\n\t\ttmpl:    t,\n\t\tlogger:  l,\n\t\tclient:  client,\n\t\tretrier: &notify.Retrier{RetryCodes: []int{http.StatusTooManyRequests}},\n\t}, nil\n}\n\n// Notify implements the Notifier interface.\nfunc (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {\n\tkey, err := notify.ExtractGroupKey(ctx)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tlogger := n.logger.With(\"group_key\", key.String())\n\tlogger.Debug(\"extracted group key\")\n\n\tvar (\n\t\talerts = types.Alerts(as...)\n\n\t\ttmplTextErr  error\n\t\tdata         = notify.GetTemplateData(ctx, n.tmpl, as, logger)\n\t\ttmplText     = notify.TmplText(n.tmpl, data, &tmplTextErr)\n\t\ttmplTextFunc = func(tmpl string) (string, error) {\n\t\t\treturn tmplText(tmpl), tmplTextErr\n\t\t}\n\n\t\tpath   = \"issue\"\n\t\tmethod = http.MethodPost\n\t)\n\n\texistingIssue, shouldRetry, err := n.searchExistingIssue(ctx, logger, key.Hash(), alerts.HasFiring(), tmplTextFunc)\n\tif err != nil {\n\t\treturn shouldRetry, fmt.Errorf(\"failed to look up existing issues: %w\", err)\n\t}\n\n\tif existingIssue == nil {\n\t\t// Do not create new issues for resolved alerts\n\t\tif alerts.Status() == model.AlertResolved {\n\t\t\treturn false, nil\n\t\t}\n\n\t\tlogger.Debug(\"create new issue\")\n\t} else {\n\t\tpath = \"issue/\" + existingIssue.Key\n\t\tmethod = http.MethodPut\n\t\tlogger.Debug(\"updating existing issue\", \"issue_key\", existingIssue.Key, \"summary_update_enabled\", n.conf.Summary.EnableUpdateValue(), \"description_update_enabled\", n.conf.Description.EnableUpdateValue())\n\t}\n\n\trequestBody, err := n.prepareIssueRequestBody(ctx, logger, key.Hash(), tmplTextFunc)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif method == http.MethodPut && requestBody.Fields != nil {\n\t\tif !n.conf.Description.EnableUpdateValue() {\n\t\t\trequestBody.Fields.Description = nil\n\t\t}\n\t\tif !n.conf.Summary.EnableUpdateValue() {\n\t\t\trequestBody.Fields.Summary = nil\n\t\t}\n\t}\n\n\t_, shouldRetry, err = n.doAPIRequest(ctx, method, path, requestBody)\n\tif err != nil {\n\t\treturn shouldRetry, fmt.Errorf(\"failed to %s request to %q: %w\", method, path, err)\n\t}\n\n\treturn n.transitionIssue(ctx, logger, existingIssue, alerts.HasFiring())\n}\n\nfunc (n *Notifier) prepareIssueRequestBody(_ context.Context, logger *slog.Logger, groupID string, tmplTextFunc template.TemplateFunc) (issue, error) {\n\tsummary, err := tmplTextFunc(n.conf.Summary.Template)\n\tif err != nil {\n\t\treturn issue{}, fmt.Errorf(\"summary template: %w\", err)\n\t}\n\n\tproject, err := tmplTextFunc(n.conf.Project)\n\tif err != nil {\n\t\treturn issue{}, fmt.Errorf(\"project template: %w\", err)\n\t}\n\tissueType, err := tmplTextFunc(n.conf.IssueType)\n\tif err != nil {\n\t\treturn issue{}, fmt.Errorf(\"issue_type template: %w\", err)\n\t}\n\n\tfieldsWithStringKeys := make(map[string]any, len(n.conf.Fields))\n\tfor key, value := range n.conf.Fields {\n\t\tfieldsWithStringKeys[key], err = template.DeepCopyWithTemplate(value, tmplTextFunc)\n\t\tif err != nil {\n\t\t\treturn issue{}, fmt.Errorf(\"fields template: %w\", err)\n\t\t}\n\t}\n\n\tsummary, truncated := notify.TruncateInRunes(summary, maxSummaryLenRunes)\n\tif truncated {\n\t\tlogger.Warn(\"Truncated summary\", \"max_runes\", maxSummaryLenRunes)\n\t}\n\n\trequestBody := issue{Fields: &issueFields{\n\t\tProject:   &issueProject{Key: project},\n\t\tIssuetype: &idNameValue{Name: issueType},\n\t\tSummary:   &summary,\n\t\tLabels:    make([]string, 0, len(n.conf.Labels)+1),\n\t\tFields:    fieldsWithStringKeys,\n\t}}\n\n\tissueDescriptionString, err := tmplTextFunc(n.conf.Description.Template)\n\tif err != nil {\n\t\treturn issue{}, fmt.Errorf(\"description template: %w\", err)\n\t}\n\n\tissueDescriptionString, truncated = notify.TruncateInRunes(issueDescriptionString, maxDescriptionLenRunes)\n\tif truncated {\n\t\tlogger.Warn(\"Truncated description\", \"max_runes\", maxDescriptionLenRunes)\n\t}\n\n\tvar description *jiraDescription\n\tdescriptionCopy := issueDescriptionString\n\tif isAPIv3Path(n.conf.APIURL.Path) {\n\t\tdescriptionCopy = strings.TrimSpace(descriptionCopy)\n\t\tif descriptionCopy != \"\" {\n\t\t\tif !json.Valid([]byte(descriptionCopy)) {\n\t\t\t\treturn issue{}, fmt.Errorf(\"description template: invalid JSON for API v3\")\n\t\t\t}\n\t\t\traw := json.RawMessage(descriptionCopy)\n\t\t\tdescription = &jiraDescription{\n\t\t\t\tRawJSONDescription: append(json.RawMessage(nil), raw...),\n\t\t\t}\n\t\t}\n\t} else if descriptionCopy != \"\" {\n\t\tdesc := descriptionCopy\n\t\tdescription = &jiraDescription{StringDescription: &desc}\n\t}\n\n\trequestBody.Fields.Description = description\n\n\tfor i, label := range n.conf.Labels {\n\t\tlabel, err = tmplTextFunc(label)\n\t\tif err != nil {\n\t\t\treturn issue{}, fmt.Errorf(\"labels[%d] template: %w\", i, err)\n\t\t}\n\t\trequestBody.Fields.Labels = append(requestBody.Fields.Labels, label)\n\t}\n\trequestBody.Fields.Labels = append(requestBody.Fields.Labels, fmt.Sprintf(\"ALERT{%s}\", groupID))\n\tsort.Strings(requestBody.Fields.Labels)\n\n\tpriority, err := tmplTextFunc(n.conf.Priority)\n\tif err != nil {\n\t\treturn issue{}, fmt.Errorf(\"priority template: %w\", err)\n\t}\n\n\tif priority != \"\" {\n\t\trequestBody.Fields.Priority = &idNameValue{Name: priority}\n\t}\n\n\treturn requestBody, nil\n}\n\nfunc (n *Notifier) searchExistingIssue(ctx context.Context, logger *slog.Logger, groupID string, firing bool, tmplTextFunc template.TemplateFunc) (*issue, bool, error) {\n\tjql := strings.Builder{}\n\n\tif n.conf.WontFixResolution != \"\" {\n\t\tfmt.Fprintf(&jql, `resolution != %q and `, n.conf.WontFixResolution)\n\t}\n\n\t// If the group is firing, search for open issues. If a reopen transition is\n\t// defined, also search for issues that were closed within the reopen duration.\n\tif firing {\n\t\treopenDuration := int64(time.Duration(n.conf.ReopenDuration).Minutes())\n\t\tif n.conf.ReopenTransition != \"\" && reopenDuration > 0 {\n\t\t\tfmt.Fprintf(&jql, `(resolutiondate is EMPTY OR resolutiondate >= -%dm) and `, reopenDuration)\n\t\t} else {\n\t\t\tjql.WriteString(`statusCategory != Done and `)\n\t\t}\n\t} else {\n\t\tjql.WriteString(`statusCategory != Done and `)\n\t}\n\n\talertLabel := fmt.Sprintf(\"ALERT{%s}\", groupID)\n\tproject, err := tmplTextFunc(n.conf.Project)\n\tif err != nil {\n\t\treturn nil, false, fmt.Errorf(\"invalid project template or value: %w\", err)\n\t}\n\tfmt.Fprintf(&jql, `project=%q and labels=%q order by status ASC,resolutiondate DESC`, project, alertLabel)\n\n\trequestBody, searchPath := n.prepareSearchRequest(jql.String())\n\n\tlogger.Debug(\"search for recent issues\", \"jql\", jql.String())\n\n\tresponseBody, shouldRetry, err := n.doAPIRequestFullPath(ctx, http.MethodPost, searchPath, requestBody)\n\tif err != nil {\n\t\treturn nil, shouldRetry, fmt.Errorf(\"HTTP request to JIRA API: %w\", err)\n\t}\n\n\tvar issueSearchResult issueSearchResult\n\terr = json.Unmarshal(responseBody, &issueSearchResult)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\n\tissuesCount := len(issueSearchResult.Issues)\n\tif issuesCount == 0 {\n\t\tlogger.Debug(\"found no existing issue\")\n\t\treturn nil, false, nil\n\t}\n\n\tif issuesCount > 1 {\n\t\tlogger.Warn(\"more than one issue matched, selecting the most recently resolved\", \"selected_issue\", issueSearchResult.Issues[0].Key)\n\t}\n\n\treturn &issueSearchResult.Issues[0], false, nil\n}\n\n// prepareSearchRequest builds the request body and search path for Jira issue search.\n//\n// Atlassian announced (see https://developer.atlassian.com/changelog/#CHANGE-2046) that\n// the legacy /search endpoint is no longer available on Jira Cloud. The replacement\n// endpoint (/rest/api/3/search/jql) is currently not available in Jira Data Center.\n//\n// Selection logic:\n//   - If APIType is \"datacenter\", always use the v2 /search endpoint.\n//   - If APIType is \"cloud\", or if APIType is \"auto\" and the host ends with\n//     \"atlassian.net\", use the v3 /search/jql endpoint.\n//   - Otherwise (APIType is \"auto\" without an atlassian.net host),\n//     use the v2 /search endpoint.\nfunc (n *Notifier) prepareSearchRequest(jql string) (issueSearch, string) {\n\trequestBody := issueSearch{\n\t\tJQL:        jql,\n\t\tMaxResults: 2,\n\t\tFields:     []string{\"status\"},\n\t}\n\n\tif n.conf.APIType == \"datacenter\" {\n\t\tsearchPath := n.conf.APIURL.JoinPath(\"/search\").String()\n\t\treturn requestBody, searchPath\n\t}\n\n\tif n.conf.APIType == \"cloud\" || n.conf.APIType == \"auto\" && strings.HasSuffix(n.conf.APIURL.Host, \"atlassian.net\") {\n\t\tsearchPath := strings.Replace(n.conf.APIURL.JoinPath(\"/search/jql\").String(), \"/rest/api/2/\", \"/rest/api/3/\", 1)\n\t\treturn requestBody, searchPath\n\t}\n\n\tsearchPath := n.conf.APIURL.JoinPath(\"/search\").String()\n\treturn requestBody, searchPath\n}\n\nfunc (n *Notifier) getIssueTransitionByName(ctx context.Context, issueKey, transitionName string) (string, bool, error) {\n\tpath := fmt.Sprintf(\"issue/%s/transitions\", issueKey)\n\n\tresponseBody, shouldRetry, err := n.doAPIRequest(ctx, http.MethodGet, path, nil)\n\tif err != nil {\n\t\treturn \"\", shouldRetry, err\n\t}\n\n\tvar issueTransitions issueTransitions\n\terr = json.Unmarshal(responseBody, &issueTransitions)\n\tif err != nil {\n\t\treturn \"\", false, err\n\t}\n\n\tfor _, issueTransition := range issueTransitions.Transitions {\n\t\tif issueTransition.Name == transitionName {\n\t\t\treturn issueTransition.ID, false, nil\n\t\t}\n\t}\n\n\treturn \"\", false, fmt.Errorf(\"can't find transition %s for issue %s\", transitionName, issueKey)\n}\n\nfunc (n *Notifier) transitionIssue(ctx context.Context, logger *slog.Logger, i *issue, firing bool) (bool, error) {\n\tif i == nil || i.Key == \"\" || i.Fields == nil || i.Fields.Status == nil {\n\t\treturn false, nil\n\t}\n\n\tvar transition string\n\tif firing {\n\t\tif i.Fields.Status.StatusCategory.Key != \"done\" {\n\t\t\treturn false, nil\n\t\t}\n\n\t\ttransition = n.conf.ReopenTransition\n\t} else {\n\t\tif i.Fields.Status.StatusCategory.Key == \"done\" {\n\t\t\treturn false, nil\n\t\t}\n\n\t\ttransition = n.conf.ResolveTransition\n\t}\n\n\ttransitionID, shouldRetry, err := n.getIssueTransitionByName(ctx, i.Key, transition)\n\tif err != nil {\n\t\treturn shouldRetry, err\n\t}\n\n\trequestBody := issue{\n\t\tTransition: &idNameValue{\n\t\t\tID: transitionID,\n\t\t},\n\t}\n\n\tpath := fmt.Sprintf(\"issue/%s/transitions\", i.Key)\n\n\tlogger.Debug(\"transitions jira issue\", \"issue_key\", i.Key, \"transition\", transition)\n\t_, shouldRetry, err = n.doAPIRequest(ctx, http.MethodPost, path, requestBody)\n\n\treturn shouldRetry, err\n}\n\nfunc (n *Notifier) doAPIRequest(ctx context.Context, method, path string, requestBody any) ([]byte, bool, error) {\n\turl := n.conf.APIURL.JoinPath(path)\n\treturn n.doAPIRequestFullPath(ctx, method, url.String(), requestBody)\n}\n\nfunc (n *Notifier) doAPIRequestFullPath(ctx context.Context, method, path string, requestBody any) ([]byte, bool, error) {\n\tvar body io.Reader\n\tif requestBody != nil {\n\t\tvar buf bytes.Buffer\n\t\tif err := json.NewEncoder(&buf).Encode(requestBody); err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\n\t\tbody = &buf\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, path, body)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Accept-Language\", \"en\")\n\n\tresp, err := n.client.Do(req)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\n\tdefer notify.Drain(resp)\n\n\tresponseBody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\n\tshouldRetry, err := n.retrier.Check(resp.StatusCode, bytes.NewReader(responseBody))\n\tif err != nil {\n\t\treturn nil, shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)\n\t}\n\n\treturn responseBody, false, nil\n}\n\nfunc isAPIv3Path(path string) bool {\n\treturn strings.HasSuffix(strings.TrimRight(path, \"/\"), \"/3\")\n}\n"
  },
  {
    "path": "notify/jira/jira_test.go",
    "content": "// Copyright 2023 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage jira\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/notify/test\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nfunc jiraStringDescription(v string) *jiraDescription {\n\treturn &jiraDescription{StringDescription: stringPtr(v)}\n}\n\nfunc stringPtr(v string) *string {\n\treturn &v\n}\n\nfunc boolPtr(v bool) *bool {\n\treturn &v\n}\n\nfunc TestJiraRetry(t *testing.T) {\n\tnotifier, err := New(\n\t\t&config.JiraConfig{\n\t\t\tAPIURL: &amcommoncfg.URL{\n\t\t\t\tURL: &url.URL{\n\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\tHost:   \"example.atlassian.net\",\n\t\t\t\t\tPath:   \"/rest/api/2\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\tretryCodes := append(test.DefaultRetryCodes(), http.StatusTooManyRequests)\n\n\tfor statusCode, expected := range test.RetryTests(retryCodes) {\n\t\tactual, _ := notifier.retrier.Check(statusCode, nil)\n\t\trequire.Equal(t, expected, actual, \"retry - error on status %d\", statusCode)\n\t}\n}\n\nfunc TestSearchExistingIssue(t *testing.T) {\n\texpectedJQL := \"\"\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.URL.Path {\n\t\tcase \"/search\":\n\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, \"Error reading request body\", http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer r.Body.Close()\n\n\t\t\t// Unmarshal the JSON data into the struct\n\t\t\tvar data issueSearch\n\t\t\terr = json.Unmarshal(body, &data)\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, \"Error unmarshaling JSON\", http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.Equal(t, expectedJQL, data.JQL)\n\t\t\tw.Write([]byte(`{\"issues\": []}`))\n\t\t\treturn\n\t\tdefault:\n\t\t\tdec := json.NewDecoder(r.Body)\n\t\t\tout := make(map[string]any)\n\t\t\terr := dec.Decode(&out)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}\n\t}))\n\n\tdefer srv.Close()\n\tu, _ := url.Parse(srv.URL)\n\n\tfor _, tc := range []struct {\n\t\ttitle         string\n\t\tcfg           *config.JiraConfig\n\t\tgroupKey      string\n\t\tfiring        bool\n\t\texpectedJQL   string\n\t\texpectedIssue *issue\n\t\texpectedErr   bool\n\t\texpectedRetry bool\n\t}{\n\t\t{\n\t\t\ttitle: \"search existing issue with project template for firing alert\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tSummary:     config.JiraFieldConfig{Template: `{{ template \"jira.default.summary\" . }}`},\n\t\t\t\tDescription: config.JiraFieldConfig{Template: `{{ template \"jira.default.description\" . }}`},\n\t\t\t\tProject:     `{{ .CommonLabels.project }}`,\n\t\t\t},\n\t\t\tgroupKey:    \"1\",\n\t\t\tfiring:      true,\n\t\t\texpectedJQL: `statusCategory != Done and project=\"PROJ\" and labels=\"ALERT{1}\" order by status ASC,resolutiondate DESC`,\n\t\t},\n\t\t{\n\t\t\ttitle: \"search existing issue with reopen duration for firing alert\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tSummary:          config.JiraFieldConfig{Template: `{{ template \"jira.default.summary\" . }}`},\n\t\t\t\tDescription:      config.JiraFieldConfig{Template: `{{ template \"jira.default.description\" . }}`},\n\t\t\t\tProject:          `{{ .CommonLabels.project }}`,\n\t\t\t\tReopenDuration:   model.Duration(60 * time.Minute),\n\t\t\t\tReopenTransition: \"REOPEN\",\n\t\t\t},\n\t\t\tgroupKey:    \"1\",\n\t\t\tfiring:      true,\n\t\t\texpectedJQL: `(resolutiondate is EMPTY OR resolutiondate >= -60m) and project=\"PROJ\" and labels=\"ALERT{1}\" order by status ASC,resolutiondate DESC`,\n\t\t},\n\t\t{\n\t\t\ttitle: \"search existing issue for resolved alert\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tSummary:     config.JiraFieldConfig{Template: `{{ template \"jira.default.summary\" . }}`},\n\t\t\t\tDescription: config.JiraFieldConfig{Template: `{{ template \"jira.default.description\" . }}`},\n\t\t\t\tProject:     `{{ .CommonLabels.project }}`,\n\t\t\t},\n\t\t\tgroupKey:    \"1\",\n\t\t\tfiring:      false,\n\t\t\texpectedJQL: `statusCategory != Done and project=\"PROJ\" and labels=\"ALERT{1}\" order by status ASC,resolutiondate DESC`,\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\texpectedJQL = tc.expectedJQL\n\t\t\ttc.cfg.APIURL = &amcommoncfg.URL{URL: u}\n\t\t\ttc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{}\n\n\t\t\tas := []*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"project\": \"PROJ\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tpd, err := New(tc.cfg, test.CreateTmpl(t), promslog.NewNopLogger())\n\t\t\trequire.NoError(t, err)\n\t\t\tlogger := pd.logger.With(\"group_key\", tc.groupKey)\n\n\t\t\tctx := notify.WithGroupKey(context.Background(), tc.groupKey)\n\t\t\tdata := notify.GetTemplateData(ctx, pd.tmpl, as, logger)\n\n\t\t\tvar tmplTextErr error\n\t\t\ttmplText := notify.TmplText(pd.tmpl, data, &tmplTextErr)\n\t\t\ttmplTextFunc := func(tmpl string) (string, error) {\n\t\t\t\treturn tmplText(tmpl), tmplTextErr\n\t\t\t}\n\n\t\t\tissue, retry, err := pd.searchExistingIssue(ctx, logger, tc.groupKey, tc.firing, tmplTextFunc)\n\t\t\tif tc.expectedErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t\trequire.Equal(t, tc.expectedIssue, issue)\n\t\t\trequire.Equal(t, tc.expectedRetry, retry)\n\t\t})\n\t}\n}\n\nfunc TestPrepareSearchRequest(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\ttitle           string\n\t\tcfg             *config.JiraConfig\n\t\tjql             string\n\t\texpectedBody    any\n\t\texpectedURL     string\n\t\texpectedURLPath string\n\t}{\n\t\t{\n\t\t\ttitle: \"cloud API type\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tAPIType: \"cloud\",\n\t\t\t\tAPIURL: &amcommoncfg.URL{\n\t\t\t\t\tURL: &url.URL{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"example.atlassian.net\",\n\t\t\t\t\t\tPath:   \"/rest/api/2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tjql: \"project=TEST and labels=\\\"ALERT{123}\\\"\",\n\t\t\texpectedBody: issueSearch{\n\t\t\t\tJQL:        \"project=TEST and labels=\\\"ALERT{123}\\\"\",\n\t\t\t\tMaxResults: 2,\n\t\t\t\tFields:     []string{\"status\"},\n\t\t\t},\n\t\t\texpectedURL:     \"https://example.atlassian.net/rest/api/3/search/jql\",\n\t\t\texpectedURLPath: \"/rest/api/2\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"auto API type with atlassian.net url\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tAPIType: \"auto\",\n\t\t\t\tAPIURL: &amcommoncfg.URL{\n\t\t\t\t\tURL: &url.URL{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"example.atlassian.net\",\n\t\t\t\t\t\tPath:   \"/rest/api/2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tjql: \"project=TEST and labels=\\\"ALERT{123}\\\"\",\n\t\t\texpectedBody: issueSearch{\n\t\t\t\tJQL:        \"project=TEST and labels=\\\"ALERT{123}\\\"\",\n\t\t\t\tMaxResults: 2,\n\t\t\t\tFields:     []string{\"status\"},\n\t\t\t},\n\t\t\texpectedURL:     \"https://example.atlassian.net/rest/api/3/search/jql\",\n\t\t\texpectedURLPath: \"/rest/api/2\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"auto API type without atlassian.net url\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tAPIType: \"auto\",\n\t\t\t\tAPIURL: &amcommoncfg.URL{\n\t\t\t\t\tURL: &url.URL{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"jira.example.com\",\n\t\t\t\t\t\tPath:   \"/rest/api/2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tjql: \"project=TEST and labels=\\\"ALERT{123}\\\"\",\n\t\t\texpectedBody: issueSearch{\n\t\t\t\tJQL:        \"project=TEST and labels=\\\"ALERT{123}\\\"\",\n\t\t\t\tMaxResults: 2,\n\t\t\t\tFields:     []string{\"status\"},\n\t\t\t},\n\t\t\texpectedURL:     \"https://jira.example.com/rest/api/2/search\",\n\t\t\texpectedURLPath: \"/rest/api/2\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"atlassian.net URL suffix but datacenter api type\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tAPIType: \"datacenter\",\n\t\t\t\tAPIURL: &amcommoncfg.URL{\n\t\t\t\t\tURL: &url.URL{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"example.atlassian.net\",\n\t\t\t\t\t\tPath:   \"/rest/api/2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tjql: \"project=TEST and labels=\\\"ALERT{123}\\\"\",\n\t\t\texpectedBody: issueSearch{\n\t\t\t\tJQL:        \"project=TEST and labels=\\\"ALERT{123}\\\"\",\n\t\t\t\tMaxResults: 2,\n\t\t\t\tFields:     []string{\"status\"},\n\t\t\t},\n\t\t\texpectedURL:     \"https://example.atlassian.net/rest/api/2/search\",\n\t\t\texpectedURLPath: \"/rest/api/2\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"datacenter API type\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tAPIType: \"datacenter\",\n\t\t\t\tAPIURL: &amcommoncfg.URL{\n\t\t\t\t\tURL: &url.URL{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"jira.example.com\",\n\t\t\t\t\t\tPath:   \"/rest/api/2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tjql: \"project=TEST and labels=\\\"ALERT{123}\\\"\",\n\t\t\texpectedBody: issueSearch{\n\t\t\t\tJQL:        \"project=TEST and labels=\\\"ALERT{123}\\\"\",\n\t\t\t\tMaxResults: 2,\n\t\t\t\tFields:     []string{\"status\"},\n\t\t\t},\n\t\t\texpectedURL:     \"https://jira.example.com/rest/api/2/search\",\n\t\t\texpectedURLPath: \"/rest/api/2\",\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\ttc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{}\n\n\t\t\tnotifier, err := New(tc.cfg, test.CreateTmpl(t), promslog.NewNopLogger())\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequestBody, searchURL := notifier.prepareSearchRequest(tc.jql)\n\n\t\t\trequire.Equal(t, tc.expectedURL, searchURL)\n\t\t\trequire.Equal(t, tc.expectedBody, requestBody)\n\t\t\t// Verify that the original APIURL.Path is not modified\n\t\t\trequire.Equal(t, tc.expectedURLPath, notifier.conf.APIURL.Path)\n\t\t})\n\t}\n}\n\nfunc TestJiraTemplating(t *testing.T) {\n\tvar capturedBody map[string]any\n\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.URL.Path {\n\t\tcase \"/search\":\n\t\t\tw.Write([]byte(`{\"issues\": []}`))\n\t\t\treturn\n\t\tdefault:\n\t\t\tdec := json.NewDecoder(r.Body)\n\t\t\tout := make(map[string]any)\n\t\t\tif err := dec.Decode(&out); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tcapturedBody = out\n\t\t}\n\t}))\n\tdefer srv.Close()\n\tu, _ := url.Parse(srv.URL)\n\n\tfor _, tc := range []struct {\n\t\ttitle string\n\t\tcfg   *config.JiraConfig\n\n\t\tretry              bool\n\t\terrMsg             string\n\t\texpectedFieldKey   string\n\t\texpectedFieldValue any\n\t}{\n\t\t{\n\t\t\ttitle: \"full-blown message with templated custom field\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tSummary:     config.JiraFieldConfig{Template: `{{ template \"jira.default.summary\" . }}`},\n\t\t\t\tDescription: config.JiraFieldConfig{Template: `{{ template \"jira.default.description\" . }}`},\n\t\t\t\tFields: map[string]any{\n\t\t\t\t\t\"customfield_14400\": `{{ template \"jira.host\" . }}`,\n\t\t\t\t},\n\t\t\t},\n\t\t\tretry:              false,\n\t\t\texpectedFieldKey:   \"customfield_14400\",\n\t\t\texpectedFieldValue: \"host1.example.com\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"template project\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tProject:     `{{ .CommonLabels.lbl1 }}`,\n\t\t\t\tSummary:     config.JiraFieldConfig{Template: `{{ template \"jira.default.summary\" . }}`},\n\t\t\t\tDescription: config.JiraFieldConfig{Template: `{{ template \"jira.default.description\" . }}`},\n\t\t\t},\n\t\t\tretry: false,\n\t\t},\n\t\t{\n\t\t\ttitle: \"template issue type\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tIssueType:   `{{ .CommonLabels.lbl1 }}`,\n\t\t\t\tSummary:     config.JiraFieldConfig{Template: `{{ template \"jira.default.summary\" . }}`},\n\t\t\t\tDescription: config.JiraFieldConfig{Template: `{{ template \"jira.default.description\" . }}`},\n\t\t\t},\n\t\t\tretry: false,\n\t\t},\n\t\t{\n\t\t\ttitle: \"summary with templating errors\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tSummary: config.JiraFieldConfig{Template: \"{{ \"},\n\t\t\t},\n\t\t\terrMsg: \"template: :1: unclosed action\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"description with templating errors\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tSummary:     config.JiraFieldConfig{Template: `{{ template \"jira.default.summary\" . }}`},\n\t\t\t\tDescription: config.JiraFieldConfig{Template: \"{{ \"},\n\t\t\t},\n\t\t\terrMsg: \"template: :1: unclosed action\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"priority with templating errors\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tSummary:     config.JiraFieldConfig{Template: `{{ template \"jira.default.summary\" . }}`},\n\t\t\t\tDescription: config.JiraFieldConfig{Template: `{{ template \"jira.default.description\" . }}`},\n\t\t\t\tPriority:    \"{{ \",\n\t\t\t},\n\t\t\terrMsg: \"template: :1: unclosed action\",\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\tcapturedBody = nil\n\n\t\t\ttc.cfg.APIURL = &amcommoncfg.URL{URL: u}\n\t\t\ttc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{}\n\t\t\tpd, err := New(tc.cfg, test.CreateTmpl(t), promslog.NewNopLogger())\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Add the jira.host template just for this test\n\t\t\tif tc.expectedFieldKey == \"customfield_14400\" {\n\t\t\t\terr = pd.tmpl.Parse(strings.NewReader(`{{ define \"jira.host\" }}{{ .CommonLabels.hostname }}{{ end }}`))\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tctx := context.Background()\n\t\t\tctx = notify.WithGroupKey(ctx, \"1\")\n\t\t\tctx = notify.WithGroupLabels(ctx, model.LabelSet{\n\t\t\t\t\"lbl1\":     \"val1\",\n\t\t\t\t\"hostname\": \"host1.example.com\",\n\t\t\t})\n\n\t\t\tok, err := pd.Notify(ctx, []*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"lbl1\":     \"val1\",\n\t\t\t\t\t\t\t\"hostname\": \"host1.example.com\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}...)\n\t\t\tif tc.errMsg == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), tc.errMsg)\n\t\t\t}\n\t\t\trequire.Equal(t, tc.retry, ok)\n\n\t\t\t// Verify that custom fields were templated correctly\n\t\t\tif tc.expectedFieldKey != \"\" {\n\t\t\t\trequire.NotNil(t, capturedBody, \"expected request body\")\n\t\t\t\tfields, ok := capturedBody[\"fields\"].(map[string]any)\n\t\t\t\trequire.True(t, ok, \"fields should be a map\")\n\t\t\t\trequire.Equal(t, tc.expectedFieldValue, fields[tc.expectedFieldKey])\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJiraNotify(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\ttitle string\n\t\tcfg   *config.JiraConfig\n\n\t\talert *types.Alert\n\n\t\tcustomFieldAssetFn func(t *testing.T, issue map[string]any)\n\t\tsearchResponse     issueSearchResult\n\t\tissue              issue\n\t\terrMsg             string\n\t}{\n\t\t{\n\t\t\ttitle: \"create new issue\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tSummary:           config.JiraFieldConfig{Template: `{{ template \"jira.default.summary\" . }}`},\n\t\t\t\tDescription:       config.JiraFieldConfig{Template: `{{ template \"jira.default.description\" . }}`},\n\t\t\t\tIssueType:         \"Incident\",\n\t\t\t\tProject:           \"OPS\",\n\t\t\t\tPriority:          `{{ template \"jira.default.priority\" . }}`,\n\t\t\t\tLabels:            []string{\"alertmanager\", \"{{ .GroupLabels.alertname }}\"},\n\t\t\t\tReopenDuration:    model.Duration(1 * time.Hour),\n\t\t\t\tReopenTransition:  \"REOPEN\",\n\t\t\t\tResolveTransition: \"CLOSE\",\n\t\t\t\tWontFixResolution: \"WONTFIX\",\n\t\t\t},\n\t\t\talert: &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t\t\"severity\":  \"critical\",\n\t\t\t\t\t},\n\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t},\n\t\t\t},\n\t\t\tsearchResponse: issueSearchResult{\n\t\t\t\tIssues: []issue{},\n\t\t\t},\n\t\t\tissue: issue{\n\t\t\t\tKey: \"\",\n\t\t\t\tFields: &issueFields{\n\t\t\t\t\tSummary:     stringPtr(\"[FIRING:1] test (vm1 critical)\"),\n\t\t\t\t\tDescription: jiraStringDescription(\"\\n\\n# Alerts Firing:\\n\\nLabels:\\n  - alertname = test\\n  - instance = vm1\\n  - severity = critical\\n\\nAnnotations:\\n\\nSource: \\n\\n\\n\\n\\n\"),\n\t\t\t\t\tIssuetype:   &idNameValue{Name: \"Incident\"},\n\t\t\t\t\tLabels:      []string{\"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}\", \"alertmanager\", \"test\"},\n\t\t\t\t\tProject:     &issueProject{Key: \"OPS\"},\n\t\t\t\t\tPriority:    &idNameValue{Name: \"High\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcustomFieldAssetFn: func(t *testing.T, issue map[string]any) {},\n\t\t\terrMsg:             \"\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"update existing issue with disabled summary and description\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tSummary: config.JiraFieldConfig{\n\t\t\t\t\tTemplate:     `{{ template \"jira.default.summary\" . }}`,\n\t\t\t\t\tEnableUpdate: boolPtr(false),\n\t\t\t\t},\n\t\t\t\tDescription: config.JiraFieldConfig{\n\t\t\t\t\tTemplate:     `{{ template \"jira.default.description\" . }}`,\n\t\t\t\t\tEnableUpdate: boolPtr(false),\n\t\t\t\t},\n\t\t\t\tIssueType:         \"{{ .CommonLabels.issue_type }}\",\n\t\t\t\tProject:           \"{{ .CommonLabels.project }}\",\n\t\t\t\tPriority:          `{{ template \"jira.default.priority\" . }}`,\n\t\t\t\tLabels:            []string{\"alertmanager\", \"{{ .GroupLabels.alertname }}\"},\n\t\t\t\tReopenDuration:    model.Duration(1 * time.Hour),\n\t\t\t\tReopenTransition:  \"REOPEN\",\n\t\t\t\tResolveTransition: \"CLOSE\",\n\t\t\t\tWontFixResolution: \"WONTFIX\",\n\t\t\t},\n\t\t\talert: &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\"alertname\":  \"test\",\n\t\t\t\t\t\t\"instance\":   \"vm1\",\n\t\t\t\t\t\t\"severity\":   \"critical\",\n\t\t\t\t\t\t\"project\":    \"MONITORING\",\n\t\t\t\t\t\t\"issue_type\": \"MINOR\",\n\t\t\t\t\t},\n\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t},\n\t\t\t},\n\t\t\tsearchResponse: issueSearchResult{\n\t\t\t\tIssues: []issue{\n\t\t\t\t\t{\n\t\t\t\t\t\tKey: \"MONITORING-1\",\n\t\t\t\t\t\tFields: &issueFields{\n\t\t\t\t\t\t\tSummary:     stringPtr(\"Original Summary\"),\n\t\t\t\t\t\t\tDescription: jiraStringDescription(\"Original Description\"),\n\t\t\t\t\t\t\tStatus: &issueStatus{\n\t\t\t\t\t\t\t\tName: \"Open\",\n\t\t\t\t\t\t\t\tStatusCategory: struct {\n\t\t\t\t\t\t\t\t\tKey string `json:\"key\"`\n\t\t\t\t\t\t\t\t}{\n\t\t\t\t\t\t\t\t\tKey: \"open\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tissue: issue{\n\t\t\t\tKey: \"MONITORING-1\",\n\t\t\t\tFields: &issueFields{\n\t\t\t\t\t// Summary and Description should NOT be present in the update request\n\t\t\t\t\tIssuetype: &idNameValue{Name: \"MINOR\"},\n\t\t\t\t\tLabels:    []string{\"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}\", \"alertmanager\", \"test\"},\n\t\t\t\t\tProject:   &issueProject{Key: \"MONITORING\"},\n\t\t\t\t\tPriority:  &idNameValue{Name: \"High\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcustomFieldAssetFn: func(t *testing.T, issue map[string]any) {\n\t\t\t\t// Verify that summary and description are NOT in the update request\n\t\t\t\t_, hasSummary := issue[\"summary\"]\n\t\t\t\t_, hasDescription := issue[\"description\"]\n\t\t\t\trequire.False(t, hasSummary, \"summary should not be present in update request\")\n\t\t\t\trequire.False(t, hasDescription, \"description should not be present in update request\")\n\t\t\t},\n\t\t\terrMsg: \"\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"create new issue with template project and issue type\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tSummary:           config.JiraFieldConfig{Template: `{{ template \"jira.default.summary\" . }}`},\n\t\t\t\tDescription:       config.JiraFieldConfig{Template: `{{ template \"jira.default.description\" . }}`},\n\t\t\t\tIssueType:         \"{{ .CommonLabels.issue_type }}\",\n\t\t\t\tProject:           \"{{ .CommonLabels.project }}\",\n\t\t\t\tPriority:          `{{ template \"jira.default.priority\" . }}`,\n\t\t\t\tLabels:            []string{\"alertmanager\", \"{{ .GroupLabels.alertname }}\"},\n\t\t\t\tReopenDuration:    model.Duration(1 * time.Hour),\n\t\t\t\tReopenTransition:  \"REOPEN\",\n\t\t\t\tResolveTransition: \"CLOSE\",\n\t\t\t\tWontFixResolution: \"WONTFIX\",\n\t\t\t},\n\t\t\talert: &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\"alertname\":  \"test\",\n\t\t\t\t\t\t\"instance\":   \"vm1\",\n\t\t\t\t\t\t\"severity\":   \"critical\",\n\t\t\t\t\t\t\"project\":    \"MONITORING\",\n\t\t\t\t\t\t\"issue_type\": \"MINOR\",\n\t\t\t\t\t},\n\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t},\n\t\t\t},\n\t\t\tsearchResponse: issueSearchResult{\n\t\t\t\tIssues: []issue{},\n\t\t\t},\n\t\t\tissue: issue{\n\t\t\t\tKey: \"\",\n\t\t\t\tFields: &issueFields{\n\t\t\t\t\tSummary:     stringPtr(\"[FIRING:1] test (vm1 MINOR MONITORING critical)\"),\n\t\t\t\t\tDescription: jiraStringDescription(\"\\n\\n# Alerts Firing:\\n\\nLabels:\\n  - alertname = test\\n  - instance = vm1\\n  - issue_type = MINOR\\n  - project = MONITORING\\n  - severity = critical\\n\\nAnnotations:\\n\\nSource: \\n\\n\\n\\n\\n\"),\n\t\t\t\t\tIssuetype:   &idNameValue{Name: \"MINOR\"},\n\t\t\t\t\tLabels:      []string{\"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}\", \"alertmanager\", \"test\"},\n\t\t\t\t\tProject:     &issueProject{Key: \"MONITORING\"},\n\t\t\t\t\tPriority:    &idNameValue{Name: \"High\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcustomFieldAssetFn: func(t *testing.T, issue map[string]any) {},\n\t\t\terrMsg:             \"\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"create new issue with custom field and too long summary\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tSummary:     config.JiraFieldConfig{Template: strings.Repeat(\"A\", maxSummaryLenRunes+10)},\n\t\t\t\tDescription: config.JiraFieldConfig{Template: `{{ template \"jira.default.description\" . }}`},\n\t\t\t\tIssueType:   \"Incident\",\n\t\t\t\tProject:     \"OPS\",\n\t\t\t\tPriority:    `{{ template \"jira.default.priority\" . }}`,\n\t\t\t\tLabels:      []string{\"alertmanager\", \"{{ .GroupLabels.alertname }}\"},\n\t\t\t\tFields: map[string]any{\n\t\t\t\t\t\"components\":        map[any]any{\"name\": \"Monitoring\"},\n\t\t\t\t\t\"customfield_10001\": \"value\",\n\t\t\t\t\t\"customfield_10002\": 0,\n\t\t\t\t\t\"customfield_10003\": []any{0},\n\t\t\t\t\t\"customfield_10004\": map[any]any{\"value\": \"red\"},\n\t\t\t\t\t\"customfield_10005\": map[any]any{\"value\": 0},\n\t\t\t\t\t\"customfield_10006\": []map[any]any{{\"value\": \"red\"}, {\"value\": \"blue\"}, {\"value\": \"green\"}},\n\t\t\t\t\t\"customfield_10007\": []map[any]any{{\"value\": \"red\"}, {\"value\": \"blue\"}, {\"value\": 0}},\n\t\t\t\t\t\"customfield_10008\": []map[any]any{{\"value\": 0}, {\"value\": 1}, {\"value\": 2}},\n\t\t\t\t\t\"customfield_10009\": []map[any]any{{1: 0}, {1.0: 1}, {\"a\": []any{2}}},\n\t\t\t\t\t\"customfield_10010\": []any{map[any]any{1: 0}, []int{3}},\n\t\t\t\t},\n\t\t\t\tReopenDuration:    model.Duration(1 * time.Hour),\n\t\t\t\tReopenTransition:  \"REOPEN\",\n\t\t\t\tResolveTransition: \"CLOSE\",\n\t\t\t\tWontFixResolution: \"WONTFIX\",\n\t\t\t},\n\t\t\talert: &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t},\n\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t},\n\t\t\t},\n\t\t\tsearchResponse: issueSearchResult{\n\t\t\t\tIssues: []issue{},\n\t\t\t},\n\t\t\tissue: issue{\n\t\t\t\tKey: \"\",\n\t\t\t\tFields: &issueFields{\n\t\t\t\t\tSummary:     stringPtr(strings.Repeat(\"A\", maxSummaryLenRunes-1) + \"…\"),\n\t\t\t\t\tDescription: jiraStringDescription(\"\\n\\n# Alerts Firing:\\n\\nLabels:\\n  - alertname = test\\n  - instance = vm1\\n\\nAnnotations:\\n\\nSource: \\n\\n\\n\\n\\n\"),\n\t\t\t\t\tIssuetype:   &idNameValue{Name: \"Incident\"},\n\t\t\t\t\tLabels:      []string{\"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}\", \"alertmanager\", \"test\"},\n\t\t\t\t\tProject:     &issueProject{Key: \"OPS\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcustomFieldAssetFn: func(t *testing.T, issue map[string]any) {\n\t\t\t\trequire.Equal(t, \"value\", issue[\"customfield_10001\"])\n\t\t\t\trequire.Equal(t, float64(0), issue[\"customfield_10002\"])\n\t\t\t\trequire.Equal(t, []any{float64(0)}, issue[\"customfield_10003\"])\n\t\t\t\trequire.Equal(t, map[string]any{\"value\": \"red\"}, issue[\"customfield_10004\"])\n\t\t\t\trequire.Equal(t, map[string]any{\"value\": float64(0)}, issue[\"customfield_10005\"])\n\t\t\t\trequire.Equal(t, []any{map[string]any{\"value\": \"red\"}, map[string]any{\"value\": \"blue\"}, map[string]any{\"value\": \"green\"}}, issue[\"customfield_10006\"])\n\t\t\t\trequire.Equal(t, []any{map[string]any{\"value\": \"red\"}, map[string]any{\"value\": \"blue\"}, map[string]any{\"value\": float64(0)}}, issue[\"customfield_10007\"])\n\t\t\t\trequire.Equal(t, []any{map[string]any{\"value\": float64(0)}, map[string]any{\"value\": float64(1)}, map[string]any{\"value\": float64(2)}}, issue[\"customfield_10008\"])\n\t\t\t\trequire.Equal(t, []any([]any{map[string]any{}, map[string]any{}, map[string]any{\"a\": []any{2.0}}}),\n\t\t\t\t\tissue[\"customfield_10009\"])\n\t\t\t\trequire.Equal(t, []any{map[string]any{}, []any{3.0}}, issue[\"customfield_10010\"])\n\t\t\t},\n\t\t\terrMsg: \"\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"reopen issue\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tSummary:           config.JiraFieldConfig{Template: `{{ template \"jira.default.summary\" . }}`},\n\t\t\t\tDescription:       config.JiraFieldConfig{Template: `{{ template \"jira.default.description\" . }}`},\n\t\t\t\tIssueType:         \"Incident\",\n\t\t\t\tProject:           \"OPS\",\n\t\t\t\tPriority:          `{{ template \"jira.default.priority\" . }}`,\n\t\t\t\tLabels:            []string{\"alertmanager\", \"{{ .GroupLabels.alertname }}\"},\n\t\t\t\tReopenDuration:    model.Duration(1 * time.Hour),\n\t\t\t\tReopenTransition:  \"REOPEN\",\n\t\t\t\tResolveTransition: \"CLOSE\",\n\t\t\t\tWontFixResolution: \"WONTFIX\",\n\t\t\t},\n\t\t\talert: &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t},\n\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t},\n\t\t\t},\n\t\t\tsearchResponse: issueSearchResult{\n\t\t\t\tIssues: []issue{\n\t\t\t\t\t{\n\t\t\t\t\t\tKey: \"OPS-1\",\n\t\t\t\t\t\tFields: &issueFields{\n\t\t\t\t\t\t\tStatus: &issueStatus{\n\t\t\t\t\t\t\t\tName: \"Closed\",\n\t\t\t\t\t\t\t\tStatusCategory: struct {\n\t\t\t\t\t\t\t\t\tKey string `json:\"key\"`\n\t\t\t\t\t\t\t\t}{\n\t\t\t\t\t\t\t\t\tKey: \"done\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tissue: issue{\n\t\t\t\tKey: \"\",\n\t\t\t\tFields: &issueFields{\n\t\t\t\t\tSummary:     stringPtr(\"[FIRING:1] test (vm1)\"),\n\t\t\t\t\tDescription: jiraStringDescription(\"\\n\\n# Alerts Firing:\\n\\nLabels:\\n  - alertname = test\\n  - instance = vm1\\n\\nAnnotations:\\n\\nSource: \\n\\n\\n\\n\\n\"),\n\t\t\t\t\tIssuetype:   &idNameValue{Name: \"Incident\"},\n\t\t\t\t\tLabels:      []string{\"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}\", \"alertmanager\", \"test\"},\n\t\t\t\t\tProject:     &issueProject{Key: \"OPS\"},\n\t\t\t\t\tPriority:    &idNameValue{Name: \"High\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcustomFieldAssetFn: func(t *testing.T, issue map[string]any) {},\n\t\t\terrMsg:             \"\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"error resolve transition not found\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tSummary:           config.JiraFieldConfig{Template: `{{ template \"jira.default.summary\" . }}`},\n\t\t\t\tDescription:       config.JiraFieldConfig{Template: `{{ template \"jira.default.description\" . }}`},\n\t\t\t\tIssueType:         \"Incident\",\n\t\t\t\tProject:           \"OPS\",\n\t\t\t\tPriority:          `{{ template \"jira.default.priority\" . }}`,\n\t\t\t\tLabels:            []string{\"alertmanager\", \"{{ .GroupLabels.alertname }}\"},\n\t\t\t\tReopenDuration:    model.Duration(1 * time.Hour),\n\t\t\t\tReopenTransition:  \"REOPEN\",\n\t\t\t\tResolveTransition: \"CLOSE\",\n\t\t\t\tWontFixResolution: \"WONTFIX\",\n\t\t\t},\n\t\t\talert: &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t},\n\t\t\t\t\tStartsAt: time.Now().Add(-time.Hour),\n\t\t\t\t\tEndsAt:   time.Now().Add(-time.Hour),\n\t\t\t\t},\n\t\t\t},\n\t\t\tsearchResponse: issueSearchResult{\n\t\t\t\tIssues: []issue{\n\t\t\t\t\t{\n\t\t\t\t\t\tKey: \"OPS-3\",\n\t\t\t\t\t\tFields: &issueFields{\n\t\t\t\t\t\t\tStatus: &issueStatus{\n\t\t\t\t\t\t\t\tName: \"Open\",\n\t\t\t\t\t\t\t\tStatusCategory: struct {\n\t\t\t\t\t\t\t\t\tKey string `json:\"key\"`\n\t\t\t\t\t\t\t\t}{\n\t\t\t\t\t\t\t\t\tKey: \"open\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tissue: issue{\n\t\t\t\tKey: \"\",\n\t\t\t\tFields: &issueFields{\n\t\t\t\t\tSummary:     stringPtr(\"[RESOLVED] test (vm1)\"),\n\t\t\t\t\tDescription: jiraStringDescription(\"\\n\\n\\n# Alerts Resolved:\\n\\nLabels:\\n  - alertname = test\\n  - instance = vm1\\n\\nAnnotations:\\n\\nSource: \\n\\n\\n\\n\"),\n\t\t\t\t\tIssuetype:   &idNameValue{Name: \"Incident\"},\n\t\t\t\t\tLabels:      []string{\"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}\", \"alertmanager\", \"test\"},\n\t\t\t\t\tProject:     &issueProject{Key: \"OPS\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcustomFieldAssetFn: func(t *testing.T, issue map[string]any) {},\n\t\t\terrMsg:             \"can't find transition CLOSE for issue OPS-3\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"error reopen transition not found\",\n\t\t\tcfg: &config.JiraConfig{\n\t\t\t\tSummary:           config.JiraFieldConfig{Template: `{{ template \"jira.default.summary\" . }}`},\n\t\t\t\tDescription:       config.JiraFieldConfig{Template: `{{ template \"jira.default.description\" . }}`},\n\t\t\t\tIssueType:         \"Incident\",\n\t\t\t\tProject:           \"OPS\",\n\t\t\t\tPriority:          `{{ template \"jira.default.priority\" . }}`,\n\t\t\t\tLabels:            []string{\"alertmanager\", \"{{ .GroupLabels.alertname }}\"},\n\t\t\t\tReopenDuration:    model.Duration(1 * time.Hour),\n\t\t\t\tReopenTransition:  \"REOPEN\",\n\t\t\t\tResolveTransition: \"CLOSE\",\n\t\t\t\tWontFixResolution: \"WONTFIX\",\n\t\t\t},\n\t\t\talert: &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t},\n\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t},\n\t\t\t},\n\t\t\tsearchResponse: issueSearchResult{\n\t\t\t\tIssues: []issue{\n\t\t\t\t\t{\n\t\t\t\t\t\tKey: \"OPS-3\",\n\t\t\t\t\t\tFields: &issueFields{\n\t\t\t\t\t\t\tStatus: &issueStatus{\n\t\t\t\t\t\t\t\tName: \"Closed\",\n\t\t\t\t\t\t\t\tStatusCategory: struct {\n\t\t\t\t\t\t\t\t\tKey string `json:\"key\"`\n\t\t\t\t\t\t\t\t}{\n\t\t\t\t\t\t\t\t\tKey: \"done\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tissue: issue{\n\t\t\t\tKey: \"\",\n\t\t\t\tFields: &issueFields{\n\t\t\t\t\tSummary:     stringPtr(\"[FIRING:1] test (vm1)\"),\n\t\t\t\t\tDescription: jiraStringDescription(\"\\n\\n# Alerts Firing:\\n\\nLabels:\\n  - alertname = test\\n  - instance = vm1\\n\\nAnnotations:\\n\\nSource: \\n\\n\\n\\n\\n\"),\n\t\t\t\t\tIssuetype:   &idNameValue{Name: \"Incident\"},\n\t\t\t\t\tLabels:      []string{\"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}\", \"alertmanager\", \"test\"},\n\t\t\t\t\tProject:     &issueProject{Key: \"OPS\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcustomFieldAssetFn: func(t *testing.T, issue map[string]any) {},\n\t\t\terrMsg:             \"can't find transition REOPEN for issue OPS-3\",\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tswitch r.URL.Path {\n\t\t\t\tcase \"/search\":\n\t\t\t\t\tenc := json.NewEncoder(w)\n\t\t\t\t\tif err := enc.Encode(tc.searchResponse); err != nil {\n\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn\n\t\t\t\tcase \"/issue/OPS-1/transitions\":\n\t\t\t\t\tswitch r.Method {\n\t\t\t\t\tcase http.MethodGet:\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\t\t\t\t\ttransitions := issueTransitions{\n\t\t\t\t\t\t\tTransitions: []idNameValue{\n\t\t\t\t\t\t\t\t{ID: \"12345\", Name: \"REOPEN\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tenc := json.NewEncoder(w)\n\t\t\t\t\t\tif err := enc.Encode(transitions); err != nil {\n\t\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t\t}\n\t\t\t\t\tcase http.MethodPost:\n\t\t\t\t\t\tdec := json.NewDecoder(r.Body)\n\t\t\t\t\t\tvar out issue\n\t\t\t\t\t\terr := dec.Decode(&out)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\trequire.Equal(t, issue{Transition: &idNameValue{ID: \"12345\"}}, out)\n\t\t\t\t\t\tw.WriteHeader(http.StatusNoContent)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Fatalf(\"unexpected method %s\", r.Method)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn\n\t\t\t\tcase \"/issue/OPS-2/transitions\":\n\t\t\t\t\tswitch r.Method {\n\t\t\t\t\tcase http.MethodGet:\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\t\t\t\t\ttransitions := issueTransitions{\n\t\t\t\t\t\t\tTransitions: []idNameValue{\n\t\t\t\t\t\t\t\t{ID: \"54321\", Name: \"CLOSE\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tenc := json.NewEncoder(w)\n\t\t\t\t\t\tif err := enc.Encode(transitions); err != nil {\n\t\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t\t}\n\t\t\t\t\tcase http.MethodPost:\n\t\t\t\t\t\tdec := json.NewDecoder(r.Body)\n\t\t\t\t\t\tvar out issue\n\t\t\t\t\t\terr := dec.Decode(&out)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\trequire.Equal(t, issue{Transition: &idNameValue{ID: \"54321\"}}, out)\n\t\t\t\t\t\tw.WriteHeader(http.StatusNoContent)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Fatalf(\"unexpected method %s\", r.Method)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn\n\t\t\t\tcase \"/issue/OPS-3/transitions\":\n\t\t\t\t\tswitch r.Method {\n\t\t\t\t\tcase http.MethodGet:\n\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\t\t\t\t\ttransitions := issueTransitions{\n\t\t\t\t\t\t\tTransitions: []idNameValue{},\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tenc := json.NewEncoder(w)\n\t\t\t\t\t\tif err := enc.Encode(transitions); err != nil {\n\t\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t\t}\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Fatalf(\"unexpected method %s\", r.Method)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn\n\t\t\t\tcase \"/issue/MONITORING-1\":\n\t\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t}\n\n\t\t\t\t\tvar raw map[string]any\n\t\t\t\t\tif err := json.Unmarshal(body, &raw); err != nil {\n\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t}\n\n\t\t\t\t\tif fields, ok := raw[\"fields\"].(map[string]any); ok {\n\t\t\t\t\t\ttc.customFieldAssetFn(t, fields)\n\t\t\t\t\t}\n\n\t\t\t\t\tw.WriteHeader(http.StatusNoContent)\n\t\t\t\t\treturn\n\t\t\t\tcase \"/issue/OPS-1\":\n\t\t\t\tcase \"/issue/OPS-2\":\n\t\t\t\tcase \"/issue/OPS-3\":\n\t\t\t\tcase \"/issue/OPS-4\":\n\t\t\t\t\tfallthrough\n\t\t\t\tcase \"/issue\":\n\t\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t}\n\n\t\t\t\t\tvar (\n\t\t\t\t\t\tissue issue\n\t\t\t\t\t\traw   map[string]any\n\t\t\t\t\t)\n\n\t\t\t\t\tif err := json.Unmarshal(body, &issue); err != nil {\n\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t}\n\n\t\t\t\t\t// We don't care about the key, so copy it over.\n\t\t\t\t\tissue.Fields.Fields = tc.issue.Fields.Fields\n\n\t\t\t\t\trequire.Equal(t, tc.issue.Key, issue.Key)\n\t\t\t\t\trequire.Equal(t, tc.issue.Fields, issue.Fields)\n\n\t\t\t\t\tif err := json.Unmarshal(body, &raw); err != nil {\n\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t}\n\n\t\t\t\t\tif fields, ok := raw[\"fields\"].(map[string]any); ok {\n\t\t\t\t\t\ttc.customFieldAssetFn(t, fields)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.Errorf(\"fields should a map of string\")\n\t\t\t\t\t}\n\n\t\t\t\t\tw.WriteHeader(http.StatusCreated)\n\n\t\t\t\t\tw.WriteHeader(http.StatusCreated)\n\n\t\t\t\tdefault:\n\t\t\t\t\tt.Fatalf(\"unexpected path %s\", r.URL.Path)\n\t\t\t\t}\n\t\t\t}))\n\t\t\tdefer srv.Close()\n\t\t\tu, _ := url.Parse(srv.URL)\n\n\t\t\ttc.cfg.APIURL = &amcommoncfg.URL{URL: u}\n\t\t\ttc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{}\n\n\t\t\tnotifier, err := New(tc.cfg, test.CreateTmpl(t), promslog.NewNopLogger())\n\t\t\trequire.NoError(t, err)\n\n\t\t\tctx := context.Background()\n\t\t\tctx = notify.WithGroupKey(ctx, \"1\")\n\t\t\tctx = notify.WithGroupLabels(ctx, model.LabelSet{\"alertname\": \"test\"})\n\n\t\t\t_, err = notifier.Notify(ctx, tc.alert)\n\t\t\tif tc.errMsg == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.EqualError(t, err, tc.errMsg)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJiraPriority(t *testing.T) {\n\tt.Parallel()\n\tfor _, tc := range []struct {\n\t\ttitle string\n\n\t\talerts []*types.Alert\n\n\t\texpectedPriority string\n\t}{\n\t\t{\n\t\t\t\"empty\",\n\t\t\t[]*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"critical\",\n\t\t\t[]*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t\t\t\"severity\":  \"critical\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"High\",\n\t\t},\n\t\t{\n\t\t\t\"warning\",\n\t\t\t[]*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t\t\t\"severity\":  \"warning\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"Medium\",\n\t\t},\n\t\t{\n\t\t\t\"info\",\n\t\t\t[]*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t\t\t\"severity\":  \"info\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"Low\",\n\t\t},\n\t\t{\n\t\t\t\"critical+warning+info\",\n\t\t\t[]*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t\t\t\"severity\":  \"critical\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t\t\t\"severity\":  \"warning\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t\t\t\"severity\":  \"info\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"High\",\n\t\t},\n\t\t{\n\t\t\t\"warning+info\",\n\t\t\t[]*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t\t\t\"severity\":  \"warning\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t\t\t\"severity\":  \"info\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"Medium\",\n\t\t},\n\t\t{\n\t\t\t\"critical(resolved)+warning+info\",\n\t\t\t[]*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t\t\t\"severity\":  \"critical\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now().Add(-time.Hour),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(-time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t\t\t\"severity\":  \"warning\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t\t\t\"severity\":  \"info\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"Medium\",\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tu, err := url.Parse(\"http://example.com/\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttmpl, err := template.FromGlobs([]string{})\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttmpl.ExternalURL = u\n\n\t\t\tvar (\n\t\t\t\tdata = tmpl.Data(\"jira\", model.LabelSet{}, notify.ReasonFirstNotification.String(), tc.alerts...)\n\n\t\t\t\ttmplTextErr  error\n\t\t\t\ttmplText     = notify.TmplText(tmpl, data, &tmplTextErr)\n\t\t\t\ttmplTextFunc = func(tmpl string) (string, error) {\n\t\t\t\t\tresult := tmplText(tmpl)\n\t\t\t\t\treturn result, tmplTextErr\n\t\t\t\t}\n\t\t\t)\n\n\t\t\tpriority, err := tmplTextFunc(`{{ template \"jira.default.priority\" . }}`)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.expectedPriority, priority)\n\t\t})\n\t}\n}\n\nfunc TestPrepareIssueRequestBodyAPIv3DescriptionValidation(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tname                string\n\t\tdescriptionTemplate string\n\t\texpectErrSubstring  string\n\t}{\n\t\t{\n\t\t\tname:                \"valid JSON description\",\n\t\t\tdescriptionTemplate: `{\"type\":\"doc\",\"version\":1,\"content\":[{\"type\":\"paragraph\",\"content\":[{\"type\":\"text\",\"text\":\"hello\"}]}]}`,\n\t\t},\n\t\t{\n\t\t\tname:                \"invalid JSON description\",\n\t\t\tdescriptionTemplate: `not-json`,\n\t\t\texpectErrSubstring:  \"invalid JSON for API v3\",\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tcfg := &config.JiraConfig{\n\t\t\t\tSummary:     config.JiraFieldConfig{Template: `{{ template \"jira.default.summary\" . }}`},\n\t\t\t\tDescription: config.JiraFieldConfig{Template: tc.descriptionTemplate},\n\t\t\t\tIssueType:   \"Incident\",\n\t\t\t\tProject:     \"OPS\",\n\t\t\t\tLabels:      []string{\"alertmanager\"},\n\t\t\t\tPriority:    `{{ template \"jira.default.priority\" . }}`,\n\t\t\t\tAPIURL: &amcommoncfg.URL{\n\t\t\t\t\tURL: &url.URL{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"example.atlassian.net\",\n\t\t\t\t\t\tPath:   \"/rest/api/3\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\t}\n\n\t\t\tnotifier, err := New(cfg, test.CreateTmpl(t), promslog.NewNopLogger())\n\t\t\trequire.NoError(t, err)\n\n\t\t\talert := &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\"alertname\": \"test\",\n\t\t\t\t\t\t\"instance\":  \"vm1\",\n\t\t\t\t\t\t\"severity\":  \"critical\",\n\t\t\t\t\t},\n\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tctx := context.Background()\n\t\t\tgroupID := \"1\"\n\t\t\tctx = notify.WithGroupKey(ctx, groupID)\n\t\t\tctx = notify.WithGroupLabels(ctx, alert.Labels)\n\n\t\t\talerts := []*types.Alert{alert}\n\t\t\tlogger := notifier.logger.With(\"group_key\", groupID)\n\t\t\tdata := notify.GetTemplateData(ctx, notifier.tmpl, alerts, logger)\n\n\t\t\tvar tmplErr error\n\t\t\ttmplText := notify.TmplText(notifier.tmpl, data, &tmplErr)\n\t\t\ttmplTextFunc := func(tmpl string) (string, error) {\n\t\t\t\treturn tmplText(tmpl), tmplErr\n\t\t\t}\n\n\t\t\tissue, err := notifier.prepareIssueRequestBody(ctx, logger, groupID, tmplTextFunc)\n\t\t\tif tc.expectErrSubstring != \"\" {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.ErrorContains(t, err, tc.expectErrSubstring)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, issue.Fields)\n\n\t\t\trequire.NotNil(t, issue.Fields.Description)\n\t\t\trequire.JSONEq(t, tc.descriptionTemplate, string(issue.Fields.Description.RawJSONDescription))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "notify/jira/types.go",
    "content": "// Copyright 2023 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage jira\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"maps\"\n)\n\n// issue represents a Jira issue wrapper.\ntype issue struct {\n\tKey        string       `json:\"key,omitempty\"`\n\tFields     *issueFields `json:\"fields,omitempty\"`\n\tTransition *idNameValue `json:\"transition,omitempty\"`\n}\n\ntype issueFields struct {\n\tDescription *jiraDescription `json:\"description,omitempty\"`\n\tIssuetype   *idNameValue     `json:\"issuetype,omitempty\"`\n\tLabels      []string         `json:\"labels,omitempty\"`\n\tPriority    *idNameValue     `json:\"priority,omitempty\"`\n\tProject     *issueProject    `json:\"project,omitempty\"`\n\tResolution  *idNameValue     `json:\"resolution,omitempty\"`\n\tSummary     *string          `json:\"summary,omitempty\"`\n\tStatus      *issueStatus     `json:\"status,omitempty\"`\n\n\tFields map[string]any `json:\"-\"`\n}\n\ntype idNameValue struct {\n\tID   string `json:\"id,omitempty\"`\n\tName string `json:\"name,omitempty\"`\n}\n\ntype issueProject struct {\n\tKey string `json:\"key\"`\n}\n\ntype issueStatus struct {\n\tName           string `json:\"name\"`\n\tStatusCategory struct {\n\t\tKey string `json:\"key\"`\n\t} `json:\"statusCategory\"`\n}\n\ntype issueSearch struct {\n\tFields     []string `json:\"fields\"`\n\tJQL        string   `json:\"jql\"`\n\tMaxResults int      `json:\"maxResults\"`\n}\n\ntype issueSearchResult struct {\n\tIssues []issue `json:\"issues\"`\n}\n\ntype issueTransitions struct {\n\tTransitions []idNameValue `json:\"transitions\"`\n}\n\n// MarshalJSON merges the struct issueFields and issueFields.CustomField together.\nfunc (i issueFields) MarshalJSON() ([]byte, error) {\n\tjsonFields := map[string]any{}\n\n\tif i.Summary != nil {\n\t\tjsonFields[\"summary\"] = *i.Summary\n\t}\n\n\t// Only include description when it has content.\n\tif i.Description != nil && !i.Description.IsEmpty() {\n\t\tjsonFields[\"description\"] = i.Description\n\t}\n\n\tif i.Issuetype != nil {\n\t\tjsonFields[\"issuetype\"] = i.Issuetype\n\t}\n\tif i.Labels != nil {\n\t\tjsonFields[\"labels\"] = i.Labels\n\t}\n\tif i.Priority != nil {\n\t\tjsonFields[\"priority\"] = i.Priority\n\t}\n\tif i.Project != nil {\n\t\tjsonFields[\"project\"] = i.Project\n\t}\n\tif i.Resolution != nil {\n\t\tjsonFields[\"resolution\"] = i.Resolution\n\t}\n\tif i.Status != nil {\n\t\tjsonFields[\"status\"] = i.Status\n\t}\n\n\t// copy custom/unknown fields into the outgoing map\n\tif i.Fields != nil {\n\t\tmaps.Copy(jsonFields, i.Fields)\n\t}\n\n\treturn json.Marshal(jsonFields)\n}\n\n// jiraDescription holds either a plain string (v2 API) description or ADF (Atlassian Document Format) JSON (v3 API).\ntype jiraDescription struct {\n\tStringDescription  *string         // non-nil if the description is a simple string\n\tRawJSONDescription json.RawMessage // non-empty if the description is structured JSON\n}\n\nfunc (jd jiraDescription) MarshalJSON() ([]byte, error) {\n\t// If there's a structured JSON payload, return it as-is.\n\tif len(jd.RawJSONDescription) > 0 {\n\t\tout := make([]byte, len(jd.RawJSONDescription))\n\t\tcopy(out, jd.RawJSONDescription)\n\t\treturn out, nil\n\t}\n\n\t// If we have a string representation, let json.Marshal quote it properly.\n\tif jd.StringDescription != nil {\n\t\treturn json.Marshal(*jd.StringDescription)\n\t}\n\n\t// No value: represent as JSON null.\n\treturn []byte(\"null\"), nil\n}\n\nfunc (jd *jiraDescription) UnmarshalJSON(data []byte) error {\n\t// Reset current state\n\tjd.StringDescription = nil\n\tjd.RawJSONDescription = nil\n\n\ttrimmed := bytes.TrimSpace(data)\n\tif len(trimmed) == 0 || bytes.Equal(trimmed, []byte(\"null\")) {\n\t\t// nothing to do (leave both fields nil/empty)\n\t\treturn nil\n\t}\n\n\t// If it starts with object or array token, treat as structured JSON and keep raw bytes.\n\tswitch trimmed[0] {\n\tcase '{', '[':\n\t\t// store a copy of the raw JSON\n\t\tjd.RawJSONDescription = append(json.RawMessage(nil), trimmed...)\n\t\treturn nil\n\tdefault:\n\t\t// otherwise try to unmarshal as string (expected for Jira v2)\n\t\tvar s string\n\t\tif err := json.Unmarshal(trimmed, &s); err != nil {\n\t\t\t// fallback: if it's not a string but also not an object/array, keep raw bytes\n\t\t\tjd.RawJSONDescription = append(json.RawMessage(nil), trimmed...)\n\t\t\treturn nil\n\t\t}\n\t\tjd.StringDescription = &s\n\t\treturn nil\n\t}\n}\n\n// IsEmpty reports whether the jiraDescription contains no useful value.\nfunc (jd *jiraDescription) IsEmpty() bool {\n\tif jd == nil {\n\t\treturn true\n\t}\n\treturn jd.StringDescription == nil && len(jd.RawJSONDescription) == 0\n}\n"
  },
  {
    "path": "notify/mattermost/mattermost.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage mattermost\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// Mattermost supports 16383 chars max.\n// https://developers.mattermost.com/integrate/webhooks/incoming/#tips-and-best-practices\nconst maxTextLenRunes = 16383\n\n// Notifier implements a Notifier for Mattermost notifications.\ntype Notifier struct {\n\tconf    *config.MattermostConfig\n\ttmpl    *template.Template\n\tlogger  *slog.Logger\n\tclient  *http.Client\n\tretrier *notify.Retrier\n\n\tpostJSONFunc func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error)\n}\n\n// New returns a new Mattermost notifier.\nfunc New(c *config.MattermostConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {\n\tclient, err := notify.NewClientWithTracing(*c.HTTPConfig, \"mattermost\", httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Notifier{\n\t\tconf:         c,\n\t\ttmpl:         t,\n\t\tlogger:       l,\n\t\tclient:       client,\n\t\tretrier:      &notify.Retrier{},\n\t\tpostJSONFunc: notify.PostJSON,\n\t}, nil\n}\n\n// request is the request for sending a Mattermost notification.\n// https://developers.mattermost.com/integrate/webhooks/incoming/#parameters\ntype request struct {\n\tText        string                     `json:\"text,omitempty\"`\n\tChannel     string                     `json:\"channel,omitempty\"`\n\tUsername    string                     `json:\"username,omitempty\"`\n\tIconURL     string                     `json:\"icon_url,omitempty\"`\n\tIconEmoji   string                     `json:\"icon_emoji,omitempty\"`\n\tAttachments []attachment               `json:\"attachments,omitempty\"`\n\tType        string                     `json:\"type,omitempty\"`\n\tProps       *config.MattermostProps    `json:\"props,omitempty\"`\n\tPriority    *config.MattermostPriority `json:\"priority,omitempty\"`\n}\n\n// attachment is used to display a richly-formatted message block for compatibility with Slack.\n// https://developers.mattermost.com/integrate/reference/message-attachments/\ntype attachment struct {\n\tFallback   string                   `json:\"fallback,omitempty\"`\n\tColor      string                   `json:\"color,omitempty\"`\n\tPretext    string                   `json:\"pretext,omitempty\"`\n\tText       string                   `json:\"text,omitempty\"`\n\tAuthorName string                   `json:\"author_name,omitempty\"`\n\tAuthorLink string                   `json:\"author_link,omitempty\"`\n\tAuthorIcon string                   `json:\"author_icon,omitempty\"`\n\tTitle      string                   `json:\"title,omitempty\"`\n\tTitleLink  string                   `json:\"title_link,omitempty\"`\n\tFields     []config.MattermostField `json:\"fields,omitempty\"`\n\tThumbURL   string                   `json:\"thumb_url,omitempty\"`\n\tFooter     string                   `json:\"footer,omitempty\"`\n\tFooterIcon string                   `json:\"footer_icon,omitempty\"`\n\tImageURL   string                   `json:\"image_url,omitempty\"`\n}\n\n// Notify implements the Notifier interface.\nfunc (n *Notifier) Notify(ctx context.Context, alert ...*types.Alert) (bool, error) {\n\tvar (\n\t\terr  error\n\t\turl  string\n\t\tdata = notify.GetTemplateData(ctx, n.tmpl, alert, n.logger)\n\t)\n\n\tif n.conf.WebhookURL != nil {\n\t\turl = n.conf.WebhookURL.String()\n\t} else {\n\t\tcontent, err := os.ReadFile(n.conf.WebhookURLFile)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\turl = strings.TrimSpace(string(content))\n\t}\n\tif url == \"\" {\n\t\treturn false, errors.New(\"webhook url missing\")\n\t}\n\n\treq := n.createRequest(notify.TmplText(n.tmpl, data, &err))\n\tif err != nil {\n\t\treturn false, err\n\t}\n\terr = n.sanitizeRequest(ctx, req)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tvar buf bytes.Buffer\n\tif err := json.NewEncoder(&buf).Encode(req); err != nil {\n\t\treturn false, err\n\t}\n\n\tresp, err := n.postJSONFunc(ctx, n.client, url, &buf)\n\tif err != nil {\n\t\treturn true, notify.RedactURL(err)\n\t}\n\tdefer notify.Drain(resp)\n\n\t// Use a retrier to generate an error message for non-200 responses and\n\t// classify them as retriable or not.\n\tretry, err := n.retrier.Check(resp.StatusCode, resp.Body)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"channel %q: %w\", req.Channel, err)\n\t\treturn retry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)\n\t}\n\tn.logger.Debug(\"Message sent to Mattermost successfully\",\n\t\t\"status\", resp.StatusCode)\n\n\treturn false, nil\n}\n\nfunc (n *Notifier) createRequest(tmpl func(string) string) *request {\n\ttext := tmpl(n.conf.Text)\n\treq := &request{\n\t\tChannel:   tmpl(n.conf.Channel),\n\t\tUsername:  tmpl(n.conf.Username),\n\t\tIconURL:   tmpl(n.conf.IconURL),\n\t\tIconEmoji: tmpl(n.conf.IconEmoji),\n\t\tType:      tmpl(n.conf.Type),\n\t}\n\n\tif n.conf.Priority != nil && n.conf.Priority.Priority != \"\" {\n\t\treq.Priority = &config.MattermostPriority{\n\t\t\tPriority:                tmpl(n.conf.Priority.Priority),\n\t\t\tRequestedAck:            n.conf.Priority.RequestedAck,\n\t\t\tPersistentNotifications: n.conf.Priority.PersistentNotifications,\n\t\t}\n\t}\n\n\tif n.conf.Props != nil && n.conf.Props.Card != \"\" {\n\t\treq.Props = &config.MattermostProps{\n\t\t\tCard: tmpl(n.conf.Props.Card),\n\t\t}\n\t}\n\n\tlenAtt := len(n.conf.Attachments)\n\tif lenAtt > 0 {\n\t\treq.Attachments = make([]attachment, lenAtt)\n\t\tfor idxAtt, cfgAtt := range n.conf.Attachments {\n\t\t\tatt := attachment{\n\t\t\t\tFallback:   tmpl(cfgAtt.Fallback),\n\t\t\t\tColor:      tmpl(cfgAtt.Color),\n\t\t\t\tPretext:    tmpl(cfgAtt.Pretext),\n\t\t\t\tText:       tmpl(cfgAtt.Text),\n\t\t\t\tAuthorName: tmpl(cfgAtt.AuthorName),\n\t\t\t\tAuthorLink: tmpl(cfgAtt.AuthorLink),\n\t\t\t\tAuthorIcon: tmpl(cfgAtt.AuthorIcon),\n\t\t\t\tTitle:      tmpl(cfgAtt.Title),\n\t\t\t\tTitleLink:  tmpl(cfgAtt.TitleLink),\n\t\t\t\tThumbURL:   tmpl(cfgAtt.ThumbURL),\n\t\t\t\tFooter:     tmpl(cfgAtt.Footer),\n\t\t\t\tFooterIcon: tmpl(cfgAtt.FooterIcon),\n\t\t\t\tImageURL:   tmpl(cfgAtt.ImageURL),\n\t\t\t}\n\n\t\t\tlenFields := len(cfgAtt.Fields)\n\t\t\tif lenFields > 0 {\n\t\t\t\tatt.Fields = make([]config.MattermostField, lenFields)\n\t\t\t\tfor idxField, field := range cfgAtt.Fields {\n\t\t\t\t\tatt.Fields[idxField] = config.MattermostField{\n\t\t\t\t\t\tTitle: tmpl(field.Title),\n\t\t\t\t\t\tValue: tmpl(field.Value),\n\t\t\t\t\t\tShort: field.Short,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treq.Attachments[idxAtt] = att\n\t\t\treq.Text = text\n\t\t}\n\t} else {\n\t\treq.Attachments = make([]attachment, 1)\n\t\tatt := attachment{\n\t\t\tText:       text,\n\t\t\tFallback:   tmpl(n.conf.Fallback),\n\t\t\tColor:      tmpl(n.conf.Color),\n\t\t\tPretext:    tmpl(n.conf.Pretext),\n\t\t\tAuthorName: tmpl(n.conf.AuthorName),\n\t\t\tAuthorLink: tmpl(n.conf.AuthorLink),\n\t\t\tAuthorIcon: tmpl(n.conf.AuthorIcon),\n\t\t\tTitle:      tmpl(n.conf.Title),\n\t\t\tTitleLink:  tmpl(n.conf.TitleLink),\n\t\t\tThumbURL:   tmpl(n.conf.ThumbURL),\n\t\t\tFooter:     tmpl(n.conf.Footer),\n\t\t\tFooterIcon: tmpl(n.conf.FooterIcon),\n\t\t\tImageURL:   tmpl(n.conf.ImageURL),\n\t\t}\n\n\t\tlenFields := len(n.conf.Fields)\n\t\tif lenFields > 0 {\n\t\t\tatt.Fields = make([]config.MattermostField, lenFields)\n\t\t\tfor idxField, field := range n.conf.Fields {\n\t\t\t\tatt.Fields[idxField] = config.MattermostField{\n\t\t\t\t\tTitle: tmpl(field.Title),\n\t\t\t\t\tValue: tmpl(field.Value),\n\t\t\t\t\tShort: field.Short,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treq.Attachments[0] = att\n\n\t}\n\n\treturn req\n}\n\nfunc (n *Notifier) sanitizeRequest(ctx context.Context, r *request) error {\n\tkey, err := notify.ExtractGroupKey(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Truncate the text if it's too long.\n\ttext, truncated := notify.TruncateInRunes(r.Text, maxTextLenRunes)\n\tif truncated {\n\t\tn.logger.Warn(\"Truncated text\",\n\t\t\t\"key\", key,\n\t\t\t\"max_runes\", maxTextLenRunes)\n\t\tr.Text = text\n\t}\n\n\tif r.Priority == nil {\n\t\treturn nil\n\t}\n\n\t// Check priority\n\tconst (\n\t\tpriorityUrgent    = \"urgent\"\n\t\tpriorityImportant = \"important\"\n\t\tpriorityStandard  = \"standard\"\n\t)\n\n\tswitch strings.ToLower(r.Priority.Priority) {\n\tcase priorityUrgent, priorityImportant, priorityStandard:\n\t\tr.Priority.Priority = strings.ToLower(r.Priority.Priority)\n\tdefault:\n\t\tn.logger.Warn(\"Priority is set to standard due to invalid value\",\n\t\t\t\"key\", key,\n\t\t\t\"priority\", r.Priority.Priority)\n\t\tr.Priority.Priority = priorityStandard\n\t}\n\n\t// Check RequestedAck flag\n\tif r.Priority.RequestedAck && r.Priority.Priority == priorityStandard {\n\t\tn.logger.Warn(\"RequestedAck is set to false due to priority is standard\",\n\t\t\t\"key\", key,\n\t\t)\n\t\tr.Priority.RequestedAck = false\n\t}\n\n\t// Check PersistentNotifications flag\n\tif r.Priority.PersistentNotifications && r.Priority.Priority != priorityUrgent {\n\t\tn.logger.Warn(\"PersistentNotifications is set to false due to priority is not urgent\",\n\t\t\t\"key\", key,\n\t\t)\n\t\tr.Priority.PersistentNotifications = false\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "notify/mattermost/mattermost_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage mattermost\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/notify/test\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nvar testWebhookURL, _ = url.Parse(\"https://mattermost.example.com/hooks/xxxxxxxxxxxxxxxxxxxxxxxxxx\")\n\nfunc TestMattermostRetry(t *testing.T) {\n\tnotifier, err := New(\n\t\t&config.MattermostConfig{\n\t\t\tWebhookURL: &amcommoncfg.SecretURL{URL: testWebhookURL},\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\tfor statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) {\n\t\tactual, _ := notifier.retrier.Check(statusCode, nil)\n\t\trequire.Equal(t, expected, actual, \"retry - error on status %d\", statusCode)\n\t}\n}\n\nfunc TestMattermostTemplating(t *testing.T) {\n\t// Create a fake HTTP server to simulate the Mattermost webhook\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdec := json.NewDecoder(r.Body)\n\t\tout := make(map[string]any)\n\t\terr := dec.Decode(&out)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}))\n\tdefer srv.Close()\n\tu, _ := url.Parse(srv.URL)\n\n\tfor _, tc := range []struct {\n\t\ttitle string\n\t\tcfg   *config.MattermostConfig\n\n\t\tretry  bool\n\t\terrMsg string\n\t}{\n\t\t{\n\t\t\ttitle: \"text with default templating\",\n\t\t\tcfg:   &config.DefaultMattermostConfig,\n\t\t\tretry: false,\n\t\t},\n\t\t{\n\t\t\ttitle: \"text with templating errors\",\n\t\t\tcfg: &config.MattermostConfig{\n\t\t\t\tText: \"{{ \",\n\t\t\t},\n\t\t\terrMsg: \"template: :1: unclosed action\",\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\ttc.cfg.WebhookURL = &amcommoncfg.SecretURL{URL: u}\n\t\t\ttc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{}\n\t\t\tpd, err := New(tc.cfg, test.CreateTmpl(t), promslog.NewNopLogger())\n\t\t\trequire.NoError(t, err)\n\n\t\t\tctx := context.Background()\n\t\t\tctx = notify.WithGroupKey(ctx, \"1\")\n\n\t\t\tok, err := pd.Notify(ctx, []*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"lbl1\": \"val1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}...)\n\t\t\tif tc.errMsg == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), tc.errMsg)\n\t\t\t}\n\t\t\trequire.Equal(t, tc.retry, ok)\n\t\t})\n\t}\n}\n\nfunc TestMattermostRedactedURL(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tsecret := \"secret\"\n\tnotifier, err := New(\n\t\t&config.MattermostConfig{\n\t\t\tWebhookURL: &amcommoncfg.SecretURL{URL: u},\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret)\n}\n\nfunc TestMattermostReadingURLFromFile(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tf, err := os.CreateTemp(t.TempDir(), \"webhook_url\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = f.WriteString(u.String() + \"\\n\")\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tnotifier, err := New(\n\t\t&config.MattermostConfig{\n\t\t\tWebhookURLFile: f.Name(),\n\t\t\tHTTPConfig:     &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String())\n}\n\nfunc TestMattermost_Notify(t *testing.T) {\n\t// Create a fake HTTP server to simulate the Mattermost webhook\n\tvar resp string\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t// Read the request as a string\n\t\tbody, err := io.ReadAll(r.Body)\n\t\trequire.NoError(t, err, \"reading request body failed\")\n\t\t// Store the request body in the response\n\t\tresp = string(body)\n\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\n\t// Create a temporary file to simulate the WebhookURLFile\n\ttempFile, err := os.CreateTemp(t.TempDir(), \"webhook_url\")\n\trequire.NoError(t, err)\n\n\t// Write the fake webhook URL to the temp file\n\t_, err = tempFile.WriteString(srv.URL)\n\trequire.NoError(t, err)\n\n\t// Create a context and alerts\n\tctx := context.Background()\n\tctx = notify.WithGroupKey(ctx, \"1\")\n\talerts := []*types.Alert{\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"lbl1\": \"val1\",\n\t\t\t\t},\n\t\t\t\tStartsAt: time.Now(),\n\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t},\n\t\t},\n\t}\n\n\ttype testcase struct {\n\t\tname        string\n\t\ttext        string\n\t\tprops       *config.MattermostProps\n\t\tpriority    *config.MattermostPriority\n\t\tattachments []*config.MattermostAttachment\n\t\tresult      string\n\t}\n\ttests := []testcase{\n\t\t{\n\t\t\tname:   \"with text only\",\n\t\t\ttext:   \"Test Text\",\n\t\t\tresult: \"{\\\"attachments\\\":[{\\\"text\\\":\\\"Test Text\\\"}]}\\n\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with text and props\",\n\t\t\ttext:     \"Test Text\",\n\t\t\tprops:    &config.MattermostProps{Card: \"Test Card\"},\n\t\t\tpriority: nil,\n\t\t\tresult:   \"{\\\"attachments\\\":[{\\\"text\\\":\\\"Test Text\\\"}],\\\"props\\\":{\\\"card\\\":\\\"Test Card\\\"}}\\n\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with text and priority standard\",\n\t\t\ttext:     \"Test Text\",\n\t\t\tprops:    nil,\n\t\t\tpriority: &config.MattermostPriority{Priority: \"standard\", RequestedAck: true, PersistentNotifications: true},\n\t\t\tresult:   \"{\\\"attachments\\\":[{\\\"text\\\":\\\"Test Text\\\"}],\\\"priority\\\":{\\\"priority\\\":\\\"standard\\\"}}\\n\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with text, props and priority\",\n\t\t\ttext:     \"Test Text\",\n\t\t\tprops:    &config.MattermostProps{Card: \"Test Card\"},\n\t\t\tpriority: &config.MattermostPriority{Priority: \"urgent\"},\n\t\t\tresult:   \"{\\\"attachments\\\":[{\\\"text\\\":\\\"Test Text\\\"}],\\\"props\\\":{\\\"card\\\":\\\"Test Card\\\"},\\\"priority\\\":{\\\"priority\\\":\\\"urgent\\\"}}\\n\",\n\t\t},\n\t\t{\n\t\t\tname:   \"with empty text - should omit text field\",\n\t\t\ttext:   \"\",\n\t\t\tresult: \"{\\\"attachments\\\":[{}]}\\n\",\n\t\t},\n\t\t{\n\t\t\tname: \"with empty text and attachments - should omit text field\",\n\t\t\ttext: \"\",\n\t\t\tattachments: []*config.MattermostAttachment{\n\t\t\t\t{\n\t\t\t\t\tTitle: \"Test Attachment\",\n\t\t\t\t\tText:  \"Attachment Text\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tresult: \"{\\\"attachments\\\":[{\\\"text\\\":\\\"Attachment Text\\\",\\\"title\\\":\\\"Test Attachment\\\"}]}\\n\",\n\t\t},\n\t\t{\n\t\t\tname: \"with text and attachments\",\n\t\t\ttext: \"Test Text\",\n\t\t\tattachments: []*config.MattermostAttachment{\n\t\t\t\t{\n\t\t\t\t\tTitle: \"Test Attachment\",\n\t\t\t\t\tText:  \"Attachment Text\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tresult: \"{\\\"text\\\":\\\"Test Text\\\",\\\"attachments\\\":[{\\\"text\\\":\\\"Attachment Text\\\",\\\"title\\\":\\\"Test Attachment\\\"}]}\\n\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Create a MattermostConfig with the WebhookURLFile set\n\t\t\tcfg := &config.MattermostConfig{\n\t\t\t\tWebhookURLFile: tempFile.Name(),\n\t\t\t\tHTTPConfig:     &commoncfg.HTTPClientConfig{},\n\t\t\t\tText:           tc.text,\n\t\t\t\tProps:          tc.props,\n\t\t\t\tPriority:       tc.priority,\n\t\t\t\tAttachments:    tc.attachments,\n\t\t\t}\n\n\t\t\t// Create a new Mattermost notifier\n\t\t\tnotifier, err := New(cfg, test.CreateTmpl(t), promslog.NewNopLogger())\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Call the Notify method\n\t\t\tok, err := notifier.Notify(ctx, alerts...)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.False(t, ok)\n\n\t\t\trequire.Equal(t, tc.result, resp)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "notify/msteams/msteams.go",
    "content": "// Copyright 2023 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage msteams\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nconst (\n\tcolorRed   = \"8C1A1A\"\n\tcolorGreen = \"2DC72D\"\n\tcolorGrey  = \"808080\"\n)\n\ntype Notifier struct {\n\tconf         *config.MSTeamsConfig\n\ttmpl         *template.Template\n\tlogger       *slog.Logger\n\tclient       *http.Client\n\tretrier      *notify.Retrier\n\twebhookURL   *amcommoncfg.SecretURL\n\tpostJSONFunc func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error)\n}\n\n// Message card reference can be found at https://learn.microsoft.com/en-us/outlook/actionable-messages/message-card-reference.\ntype teamsMessage struct {\n\tContext    string `json:\"@context\"`\n\tType       string `json:\"type\"`\n\tTitle      string `json:\"title\"`\n\tSummary    string `json:\"summary\"`\n\tText       string `json:\"text\"`\n\tThemeColor string `json:\"themeColor\"`\n}\n\n// New returns a new notifier that uses the Microsoft Teams Webhook API.\nfunc New(c *config.MSTeamsConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {\n\tclient, err := notify.NewClientWithTracing(*c.HTTPConfig, \"msteams\", httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tn := &Notifier{\n\t\tconf:         c,\n\t\ttmpl:         t,\n\t\tlogger:       l,\n\t\tclient:       client,\n\t\tretrier:      &notify.Retrier{},\n\t\twebhookURL:   c.WebhookURL,\n\t\tpostJSONFunc: notify.PostJSON,\n\t}\n\n\treturn n, nil\n}\n\nfunc (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {\n\tkey, err := notify.ExtractGroupKey(ctx)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tlogger := n.logger.With(\"group_key\", key)\n\tlogger.Debug(\"extracted group key\")\n\n\tdata := notify.GetTemplateData(ctx, n.tmpl, as, logger)\n\ttmpl := notify.TmplText(n.tmpl, data, &err)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\ttitle := tmpl(n.conf.Title)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\ttext := tmpl(n.conf.Text)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tsummary := tmpl(n.conf.Summary)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\talerts := types.Alerts(as...)\n\tcolor := colorGrey\n\tswitch alerts.Status() {\n\tcase model.AlertFiring:\n\t\tcolor = colorRed\n\tcase model.AlertResolved:\n\t\tcolor = colorGreen\n\t}\n\n\tvar url string\n\tif n.conf.WebhookURL != nil {\n\t\turl = n.conf.WebhookURL.String()\n\t} else {\n\t\tcontent, err := os.ReadFile(n.conf.WebhookURLFile)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"read webhook_url_file: %w\", err)\n\t\t}\n\t\turl = strings.TrimSpace(string(content))\n\t}\n\n\tt := teamsMessage{\n\t\tContext:    \"http://schema.org/extensions\",\n\t\tType:       \"MessageCard\",\n\t\tTitle:      title,\n\t\tSummary:    summary,\n\t\tText:       text,\n\t\tThemeColor: color,\n\t}\n\n\tvar payload bytes.Buffer\n\tif err = json.NewEncoder(&payload).Encode(t); err != nil {\n\t\treturn false, err\n\t}\n\n\tresp, err := n.postJSONFunc(ctx, n.client, url, &payload)\n\tif err != nil {\n\t\treturn true, notify.RedactURL(err)\n\t}\n\tdefer notify.Drain(resp)\n\n\t// https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL#rate-limiting-for-connectors\n\tshouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)\n\tif err != nil {\n\t\treturn shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)\n\t}\n\treturn shouldRetry, err\n}\n"
  },
  {
    "path": "notify/msteams/msteams_test.go",
    "content": "// Copyright 2023 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage msteams\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/notify/test\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// This is a test URL that has been modified to not be valid.\nvar testWebhookURL, _ = url.Parse(\"https://example.webhook.office.com/webhookb2/xxxxxx/IncomingWebhook/xxx/xxx\")\n\nfunc TestMSTeamsRetry(t *testing.T) {\n\tnotifier, err := New(\n\t\t&config.MSTeamsConfig{\n\t\t\tWebhookURL: &amcommoncfg.SecretURL{URL: testWebhookURL},\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\tfor statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) {\n\t\tactual, _ := notifier.retrier.Check(statusCode, nil)\n\t\trequire.Equal(t, expected, actual, \"retry - error on status %d\", statusCode)\n\t}\n}\n\nfunc TestMSTeamsTemplating(t *testing.T) {\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdec := json.NewDecoder(r.Body)\n\t\tout := make(map[string]any)\n\t\terr := dec.Decode(&out)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}))\n\tdefer srv.Close()\n\tu, _ := url.Parse(srv.URL)\n\n\tfor _, tc := range []struct {\n\t\ttitle string\n\t\tcfg   *config.MSTeamsConfig\n\n\t\tretry  bool\n\t\terrMsg string\n\t}{\n\t\t{\n\t\t\ttitle: \"full-blown message\",\n\t\t\tcfg: &config.MSTeamsConfig{\n\t\t\t\tTitle:   `{{ template \"msteams.default.title\" . }}`,\n\t\t\t\tSummary: `{{ template \"msteams.default.summary\" . }}`,\n\t\t\t\tText:    `{{ template \"msteams.default.text\" . }}`,\n\t\t\t},\n\t\t\tretry: false,\n\t\t},\n\t\t{\n\t\t\ttitle: \"title with templating errors\",\n\t\t\tcfg: &config.MSTeamsConfig{\n\t\t\t\tTitle: \"{{ \",\n\t\t\t},\n\t\t\terrMsg: \"template: :1: unclosed action\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"summary with templating errors\",\n\t\t\tcfg: &config.MSTeamsConfig{\n\t\t\t\tTitle:   `{{ template \"msteams.default.title\" . }}`,\n\t\t\t\tSummary: \"{{ \",\n\t\t\t},\n\t\t\terrMsg: \"template: :1: unclosed action\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"message with templating errors\",\n\t\t\tcfg: &config.MSTeamsConfig{\n\t\t\t\tTitle:   `{{ template \"msteams.default.title\" . }}`,\n\t\t\t\tSummary: `{{ template \"msteams.default.summary\" . }}`,\n\t\t\t\tText:    \"{{ \",\n\t\t\t},\n\t\t\terrMsg: \"template: :1: unclosed action\",\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\ttc.cfg.WebhookURL = &amcommoncfg.SecretURL{URL: u}\n\t\t\ttc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{}\n\t\t\tpd, err := New(tc.cfg, test.CreateTmpl(t), promslog.NewNopLogger())\n\t\t\trequire.NoError(t, err)\n\n\t\t\tctx := context.Background()\n\t\t\tctx = notify.WithGroupKey(ctx, \"1\")\n\n\t\t\tok, err := pd.Notify(ctx, []*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"lbl1\": \"val1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}...)\n\t\t\tif tc.errMsg == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), tc.errMsg)\n\t\t\t}\n\t\t\trequire.Equal(t, tc.retry, ok)\n\t\t})\n\t}\n}\n\nfunc TestNotifier_Notify_WithReason(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\tstatusCode      int\n\t\tresponseContent string\n\t\texpectedReason  notify.Reason\n\t\tnoError         bool\n\t}{\n\t\t{\n\t\t\tname:            \"with a 2xx status code and response 1\",\n\t\t\tstatusCode:      http.StatusOK,\n\t\t\tresponseContent: \"1\",\n\t\t\tnoError:         true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnotifier, err := New(\n\t\t\t\t&config.MSTeamsConfig{\n\t\t\t\t\tWebhookURL: &amcommoncfg.SecretURL{URL: testWebhookURL},\n\t\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\t\t},\n\t\t\t\ttest.CreateTmpl(t),\n\t\t\t\tpromslog.NewNopLogger(),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tnotifier.postJSONFunc = func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error) {\n\t\t\t\tresp := httptest.NewRecorder()\n\t\t\t\tresp.WriteString(tt.responseContent)\n\t\t\t\tresp.WriteHeader(tt.statusCode)\n\t\t\t\treturn resp.Result(), nil\n\t\t\t}\n\t\t\tctx := context.Background()\n\t\t\tctx = notify.WithGroupKey(ctx, \"1\")\n\n\t\t\talert1 := &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t},\n\t\t\t}\n\t\t\t_, err = notifier.Notify(ctx, alert1)\n\t\t\tif tt.noError {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tvar reasonError *notify.ErrorWithReason\n\t\t\t\trequire.ErrorAs(t, err, &reasonError)\n\t\t\t\trequire.Equal(t, tt.expectedReason, reasonError.Reason)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMSTeamsRedactedURL(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tsecret := \"secret\"\n\tnotifier, err := New(\n\t\t&config.MSTeamsConfig{\n\t\t\tWebhookURL: &amcommoncfg.SecretURL{URL: u},\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret)\n}\n\nfunc TestMSTeamsReadingURLFromFile(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tf, err := os.CreateTemp(t.TempDir(), \"webhook_url\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = f.WriteString(u.String() + \"\\n\")\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tnotifier, err := New(\n\t\t&config.MSTeamsConfig{\n\t\t\tWebhookURLFile: f.Name(),\n\t\t\tHTTPConfig:     &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String())\n}\n"
  },
  {
    "path": "notify/msteamsv2/msteamsv2.go",
    "content": "// Copyright 2024 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage msteamsv2\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nconst (\n\tcolorRed   = \"Attention\"\n\tcolorGreen = \"Good\"\n\tcolorGrey  = \"Warning\"\n)\n\ntype Notifier struct {\n\tconf         *config.MSTeamsV2Config\n\ttmpl         *template.Template\n\tlogger       *slog.Logger\n\tclient       *http.Client\n\tretrier      *notify.Retrier\n\twebhookURL   *amcommoncfg.SecretURL\n\tpostJSONFunc func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error)\n}\n\n// https://learn.microsoft.com/en-us/connectors/teams/?tabs=text1#adaptivecarditemschema\ntype Content struct {\n\tSchema  string  `json:\"$schema\"`\n\tType    string  `json:\"type\"`\n\tVersion string  `json:\"version\"`\n\tBody    []Body  `json:\"body\"`\n\tMsteams Msteams `json:\"msteams,omitempty\"`\n}\n\ntype Body struct {\n\tType   string `json:\"type\"`\n\tText   string `json:\"text\"`\n\tWeight string `json:\"weight,omitempty\"`\n\tSize   string `json:\"size,omitempty\"`\n\tWrap   bool   `json:\"wrap,omitempty\"`\n\tStyle  string `json:\"style,omitempty\"`\n\tColor  string `json:\"color,omitempty\"`\n}\n\ntype Msteams struct {\n\tWidth string `json:\"width\"`\n}\n\ntype Attachment struct {\n\tContentType string  `json:\"contentType\"`\n\tContentURL  *string `json:\"contentUrl\"` // Use a pointer to handle null values\n\tContent     Content `json:\"content\"`\n}\n\ntype teamsMessage struct {\n\tType        string       `json:\"type\"`\n\tAttachments []Attachment `json:\"attachments\"`\n}\n\n// New returns a new notifier that uses the Microsoft Teams Power Platform connector.\nfunc New(c *config.MSTeamsV2Config, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {\n\tclient, err := notify.NewClientWithTracing(*c.HTTPConfig, \"msteamsv2\", httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tn := &Notifier{\n\t\tconf:         c,\n\t\ttmpl:         t,\n\t\tlogger:       l,\n\t\tclient:       client,\n\t\tretrier:      &notify.Retrier{},\n\t\twebhookURL:   c.WebhookURL,\n\t\tpostJSONFunc: notify.PostJSON,\n\t}\n\n\treturn n, nil\n}\n\nfunc (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {\n\tkey, err := notify.ExtractGroupKey(ctx)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tlogger := n.logger.With(\"group_key\", key)\n\tlogger.Debug(\"extracted group key\")\n\n\tdata := notify.GetTemplateData(ctx, n.tmpl, as, logger)\n\ttmpl := notify.TmplText(n.tmpl, data, &err)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\ttitle := tmpl(n.conf.Title)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\ttext := tmpl(n.conf.Text)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\talerts := types.Alerts(as...)\n\tcolor := colorGrey\n\tswitch alerts.Status() {\n\tcase model.AlertFiring:\n\t\tcolor = colorRed\n\tcase model.AlertResolved:\n\t\tcolor = colorGreen\n\t}\n\n\tvar url string\n\tif n.conf.WebhookURL != nil {\n\t\turl = n.conf.WebhookURL.String()\n\t} else {\n\t\tcontent, err := os.ReadFile(n.conf.WebhookURLFile)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"read webhook_url_file: %w\", err)\n\t\t}\n\t\turl = strings.TrimSpace(string(content))\n\t}\n\n\t// A message as referenced in https://learn.microsoft.com/en-us/connectors/teams/?tabs=text1%2Cdotnet#request-body-schema\n\tt := teamsMessage{\n\t\tType: \"message\",\n\t\tAttachments: []Attachment{\n\t\t\t{\n\t\t\t\tContentType: \"application/vnd.microsoft.card.adaptive\",\n\t\t\t\tContentURL:  nil,\n\t\t\t\tContent: Content{\n\t\t\t\t\tSchema:  \"http://adaptivecards.io/schemas/adaptive-card.json\",\n\t\t\t\t\tType:    \"AdaptiveCard\",\n\t\t\t\t\tVersion: \"1.2\",\n\t\t\t\t\tBody: []Body{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:   \"TextBlock\",\n\t\t\t\t\t\t\tText:   title,\n\t\t\t\t\t\t\tWeight: \"Bolder\",\n\t\t\t\t\t\t\tSize:   \"Medium\",\n\t\t\t\t\t\t\tWrap:   true,\n\t\t\t\t\t\t\tStyle:  \"heading\",\n\t\t\t\t\t\t\tColor:  color,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: \"TextBlock\",\n\t\t\t\t\t\t\tText: text,\n\t\t\t\t\t\t\tWrap: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMsteams: Msteams{\n\t\t\t\t\t\tWidth: \"full\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tvar payload bytes.Buffer\n\tif err = json.NewEncoder(&payload).Encode(t); err != nil {\n\t\treturn false, err\n\t}\n\n\tresp, err := n.postJSONFunc(ctx, n.client, url, &payload)\n\tif err != nil {\n\t\treturn true, notify.RedactURL(err)\n\t}\n\tdefer notify.Drain(resp)\n\n\t// https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL#rate-limiting-for-connectors\n\tshouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)\n\tif err != nil {\n\t\treturn shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)\n\t}\n\treturn shouldRetry, err\n}\n"
  },
  {
    "path": "notify/msteamsv2/msteamsv2_test.go",
    "content": "// Copyright 2024 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage msteamsv2\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/notify/test\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// This is a test URL that has been modified to not be valid.\nvar testWebhookURL, _ = url.Parse(\"https://example.westeurope.logic.azure.com:443/workflows/xxx/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=xxx\")\n\nfunc TestMSTeamsV2Retry(t *testing.T) {\n\tnotifier, err := New(\n\t\t&config.MSTeamsV2Config{\n\t\t\tWebhookURL: &amcommoncfg.SecretURL{URL: testWebhookURL},\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\tfor statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) {\n\t\tactual, _ := notifier.retrier.Check(statusCode, nil)\n\t\trequire.Equal(t, expected, actual, \"retry - error on status %d\", statusCode)\n\t}\n}\n\nfunc TestNotifier_Notify_WithReason(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\tstatusCode      int\n\t\tresponseContent string\n\t\texpectedReason  notify.Reason\n\t\tnoError         bool\n\t}{\n\t\t{\n\t\t\tname:            \"with a 2xx status code and response 1\",\n\t\t\tstatusCode:      http.StatusOK,\n\t\t\tresponseContent: \"1\",\n\t\t\tnoError:         true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnotifier, err := New(\n\t\t\t\t&config.MSTeamsV2Config{\n\t\t\t\t\tWebhookURL: &amcommoncfg.SecretURL{URL: testWebhookURL},\n\t\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\t\t},\n\t\t\t\ttest.CreateTmpl(t),\n\t\t\t\tpromslog.NewNopLogger(),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tnotifier.postJSONFunc = func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error) {\n\t\t\t\tresp := httptest.NewRecorder()\n\t\t\t\tresp.WriteString(tt.responseContent)\n\t\t\t\tresp.WriteHeader(tt.statusCode)\n\t\t\t\treturn resp.Result(), nil\n\t\t\t}\n\t\t\tctx := context.Background()\n\t\t\tctx = notify.WithGroupKey(ctx, \"1\")\n\n\t\t\talert1 := &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t},\n\t\t\t}\n\t\t\t_, err = notifier.Notify(ctx, alert1)\n\t\t\tif tt.noError {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tvar reasonError *notify.ErrorWithReason\n\t\t\t\trequire.ErrorAs(t, err, &reasonError)\n\t\t\t\trequire.Equal(t, tt.expectedReason, reasonError.Reason)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMSTeamsV2Templating(t *testing.T) {\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdec := json.NewDecoder(r.Body)\n\t\tout := make(map[string]any)\n\t\terr := dec.Decode(&out)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}))\n\tdefer srv.Close()\n\tu, _ := url.Parse(srv.URL)\n\n\tfor _, tc := range []struct {\n\t\ttitle string\n\t\tcfg   *config.MSTeamsV2Config\n\n\t\tretry  bool\n\t\terrMsg string\n\t}{\n\t\t{\n\t\t\ttitle: \"full-blown message\",\n\t\t\tcfg: &config.MSTeamsV2Config{\n\t\t\t\tTitle: `{{ template \"msteams.default.title\" . }}`,\n\t\t\t\tText:  `{{ template \"msteams.default.text\" . }}`,\n\t\t\t},\n\t\t\tretry: false,\n\t\t},\n\t\t{\n\t\t\ttitle: \"title with templating errors\",\n\t\t\tcfg: &config.MSTeamsV2Config{\n\t\t\t\tTitle: \"{{ \",\n\t\t\t},\n\t\t\terrMsg: \"template: :1: unclosed action\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"message with templating errors\",\n\t\t\tcfg: &config.MSTeamsV2Config{\n\t\t\t\tTitle: `{{ template \"msteams.default.title\" . }}`,\n\t\t\t\tText:  \"{{ \",\n\t\t\t},\n\t\t\terrMsg: \"template: :1: unclosed action\",\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\ttc.cfg.WebhookURL = &amcommoncfg.SecretURL{URL: u}\n\t\t\ttc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{}\n\t\t\tpd, err := New(tc.cfg, test.CreateTmpl(t), promslog.NewNopLogger())\n\t\t\trequire.NoError(t, err)\n\n\t\t\tctx := context.Background()\n\t\t\tctx = notify.WithGroupKey(ctx, \"1\")\n\n\t\t\tok, err := pd.Notify(ctx, []*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"lbl1\": \"val1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}...)\n\t\t\tif tc.errMsg == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), tc.errMsg)\n\t\t\t}\n\t\t\trequire.Equal(t, tc.retry, ok)\n\t\t})\n\t}\n}\n\nfunc TestMSTeamsV2RedactedURL(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tsecret := \"secret\"\n\tnotifier, err := New(\n\t\t&config.MSTeamsV2Config{\n\t\t\tWebhookURL: &amcommoncfg.SecretURL{URL: u},\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret)\n}\n\nfunc TestMSTeamsV2ReadingURLFromFile(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tf, err := os.CreateTemp(t.TempDir(), \"webhook_url\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = f.WriteString(u.String() + \"\\n\")\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tnotifier, err := New(\n\t\t&config.MSTeamsV2Config{\n\t\t\tWebhookURLFile: f.Name(),\n\t\t\tHTTPConfig:     &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String())\n}\n"
  },
  {
    "path": "notify/mute.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage notify\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/prometheus/common/model\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/codes\"\n\t\"go.opentelemetry.io/otel/trace\"\n\n\t\"github.com/prometheus/alertmanager/inhibit\"\n\t\"github.com/prometheus/alertmanager/silence\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// A Muter determines whether a given label set is muted. Implementers that\n// maintain an underlying AlertMarker are expected to update it during a call of\n// Mutes.\ntype Muter interface {\n\tMutes(ctx context.Context, lset model.LabelSet) bool\n}\n\n// A MuteFunc is a function that implements the Muter interface.\ntype MuteFunc func(ctx context.Context, lset model.LabelSet) bool\n\n// Mutes implements the Muter interface.\nfunc (f MuteFunc) Mutes(ctx context.Context, lset model.LabelSet) bool { return f(ctx, lset) }\n\n// MuteStage filters alerts through a Muter.\ntype MuteStage struct {\n\tmuter   Muter\n\tmetrics *Metrics\n}\n\n// NewMuteStage return a new MuteStage.\nfunc NewMuteStage(m Muter, metrics *Metrics) *MuteStage {\n\treturn &MuteStage{muter: m, metrics: metrics}\n}\n\n// Exec implements the Stage interface.\nfunc (n *MuteStage) Exec(ctx context.Context, logger *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {\n\tctx, span := tracer.Start(ctx, \"notify.MuteStage.Exec\",\n\t\ttrace.WithAttributes(attribute.Int(\"alerting.alerts.count\", len(alerts))),\n\t\ttrace.WithSpanKind(trace.SpanKindInternal),\n\t)\n\tdefer span.End()\n\n\tvar (\n\t\tfiltered []*types.Alert\n\t\tmuted    []*types.Alert\n\t)\n\tfor _, a := range alerts {\n\t\t// TODO(fabxc): increment total alerts counter.\n\t\t// Do not send the alert if muted.\n\t\tif n.muter.Mutes(ctx, a.Labels) {\n\t\t\tmuted = append(muted, a)\n\t\t} else {\n\t\t\tfiltered = append(filtered, a)\n\t\t}\n\t\t// TODO(fabxc): increment muted alerts counter if muted.\n\t}\n\tif len(muted) > 0 {\n\n\t\tvar reason string\n\t\tswitch n.muter.(type) {\n\t\tcase *silence.Silencer:\n\t\t\treason = SuppressedReasonSilence\n\t\tcase *inhibit.Inhibitor:\n\t\t\treason = SuppressedReasonInhibition\n\t\tdefault:\n\t\t}\n\t\tspan.SetAttributes(\n\t\t\tattribute.Int(\"alerting.alerts.muted.count\", len(muted)),\n\t\t\tattribute.Int(\"alerting.alerts.filtered.count\", len(filtered)),\n\t\t\tattribute.String(\"alerting.suppressed.reason\", reason),\n\t\t)\n\t\tn.metrics.numNotificationSuppressedTotal.WithLabelValues(reason).Add(float64(len(muted)))\n\t\tlogger.Debug(\"Notifications will not be sent for muted alerts\", \"alerts\", fmt.Sprintf(\"%v\", muted), \"reason\", reason)\n\t}\n\n\treturn ctx, filtered, nil\n}\n\n// A TimeMuter determines if the time is muted by one or more active or mute\n// time intervals. If the time is muted, it returns true and the names of the\n// time intervals that muted it. Otherwise, it returns false and a nil slice.\ntype TimeMuter interface {\n\tMutes(timeIntervalNames []string, now time.Time) (bool, []string, error)\n}\n\ntype timeStage struct {\n\tmuter   TimeMuter\n\tmarker  types.GroupMarker\n\tmetrics *Metrics\n}\n\ntype TimeMuteStage timeStage\n\nfunc NewTimeMuteStage(muter TimeMuter, marker types.GroupMarker, metrics *Metrics) *TimeMuteStage {\n\treturn &TimeMuteStage{muter, marker, metrics}\n}\n\n// Exec implements the stage interface for TimeMuteStage.\n// TimeMuteStage is responsible for muting alerts whose route is not in an active time.\nfunc (tms TimeMuteStage) Exec(ctx context.Context, l *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {\n\tctx, span := tracer.Start(ctx, \"notify.TimeMuteStage.Exec\",\n\t\ttrace.WithAttributes(attribute.Int(\"alerting.alerts.count\", len(alerts))),\n\t\ttrace.WithSpanKind(trace.SpanKindInternal),\n\t)\n\tdefer span.End()\n\n\trouteID, ok := RouteID(ctx)\n\tif !ok {\n\t\terr := errors.New(\"route ID missing\")\n\t\tspan.SetStatus(codes.Error, err.Error())\n\t\tspan.RecordError(err)\n\t\treturn ctx, nil, err\n\t}\n\tspan.SetAttributes(attribute.String(\"alerting.route.id\", routeID))\n\n\tgkey, ok := GroupKey(ctx)\n\tif !ok {\n\t\treturn ctx, nil, errors.New(\"group key missing\")\n\t}\n\tspan.SetAttributes(attribute.String(\"alerting.group.key\", gkey))\n\n\tmuteTimeIntervalNames, ok := MuteTimeIntervalNames(ctx)\n\tif !ok {\n\t\treturn ctx, alerts, nil\n\t}\n\tnow, ok := Now(ctx)\n\tif !ok {\n\t\treturn ctx, alerts, errors.New(\"missing now timestamp\")\n\t}\n\n\t// Skip this stage if there are no mute timings.\n\tif len(muteTimeIntervalNames) == 0 {\n\t\treturn ctx, alerts, nil\n\t}\n\n\tmuted, mutedBy, err := tms.muter.Mutes(muteTimeIntervalNames, now)\n\tif err != nil {\n\t\tspan.SetStatus(codes.Error, err.Error())\n\t\tspan.RecordError(err)\n\t\treturn ctx, alerts, err\n\t}\n\t// If muted is false then mutedBy is nil and the muted marker is removed.\n\ttms.marker.SetMuted(routeID, gkey, mutedBy)\n\n\t// If the current time is inside a mute time, all alerts are removed from the pipeline.\n\tif muted {\n\t\ttms.metrics.numNotificationSuppressedTotal.WithLabelValues(SuppressedReasonMuteTimeInterval).Add(float64(len(alerts)))\n\t\tl.Debug(\"Notifications not sent, route is within mute time\", \"alerts\", len(alerts))\n\t\tspan.AddEvent(\"notify.TimeMuteStage.Exec muted the alerts\")\n\t\treturn ctx, nil, nil\n\t}\n\n\treturn ctx, alerts, nil\n}\n\ntype TimeActiveStage timeStage\n\nfunc NewTimeActiveStage(muter TimeMuter, marker types.GroupMarker, metrics *Metrics) *TimeActiveStage {\n\treturn &TimeActiveStage{muter, marker, metrics}\n}\n\n// Exec implements the stage interface for TimeActiveStage.\n// TimeActiveStage is responsible for muting alerts whose route is not in an active time.\nfunc (tas TimeActiveStage) Exec(ctx context.Context, l *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {\n\trouteID, ok := RouteID(ctx)\n\tif !ok {\n\t\treturn ctx, nil, errors.New(\"route ID missing\")\n\t}\n\n\tctx, span := tracer.Start(ctx, \"notify.TimeActiveStage.Exec\",\n\t\ttrace.WithAttributes(attribute.String(\"alerting.route.id\", routeID)),\n\t\ttrace.WithAttributes(attribute.Int(\"alerting.alerts.count\", len(alerts))),\n\t\ttrace.WithSpanKind(trace.SpanKindInternal),\n\t)\n\tdefer span.End()\n\n\tgkey, ok := GroupKey(ctx)\n\tif !ok {\n\t\treturn ctx, nil, errors.New(\"group key missing\")\n\t}\n\n\tactiveTimeIntervalNames, ok := ActiveTimeIntervalNames(ctx)\n\tif !ok {\n\t\treturn ctx, alerts, nil\n\t}\n\n\t// if we don't have active time intervals at all it is always active.\n\tif len(activeTimeIntervalNames) == 0 {\n\t\treturn ctx, alerts, nil\n\t}\n\n\tnow, ok := Now(ctx)\n\tif !ok {\n\t\treturn ctx, alerts, errors.New(\"missing now timestamp\")\n\t}\n\n\tactive, _, err := tas.muter.Mutes(activeTimeIntervalNames, now)\n\tif err != nil {\n\t\treturn ctx, alerts, err\n\t}\n\n\tvar mutedBy []string\n\tif !active {\n\t\t// If the group is muted, then it must be muted by all active time intervals.\n\t\t// Otherwise, the group must be in at least one active time interval for it\n\t\t// to be active.\n\t\tmutedBy = activeTimeIntervalNames\n\t}\n\ttas.marker.SetMuted(routeID, gkey, mutedBy)\n\n\t// If the current time is not inside an active time, all alerts are removed from the pipeline\n\tif !active {\n\t\tspan.AddEvent(\"notify.TimeActiveStage.Exec not active, removing all alerts\")\n\t\ttas.metrics.numNotificationSuppressedTotal.WithLabelValues(SuppressedReasonActiveTimeInterval).Add(float64(len(alerts)))\n\t\tl.Debug(\"Notifications not sent, route is not within active time\", \"alerts\", len(alerts))\n\t\treturn ctx, nil, nil\n\t}\n\n\treturn ctx, alerts, nil\n}\n"
  },
  {
    "path": "notify/mute_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage notify\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\tprom_testutil \"github.com/prometheus/client_golang/prometheus/testutil\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/prometheus/alertmanager/featurecontrol\"\n\t\"github.com/prometheus/alertmanager/silence\"\n\t\"github.com/prometheus/alertmanager/silence/silencepb\"\n\t\"github.com/prometheus/alertmanager/timeinterval\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nfunc TestMuteStage(t *testing.T) {\n\t// Mute all label sets that have a \"mute\" key.\n\tmuter := MuteFunc(func(ctx context.Context, lset model.LabelSet) bool {\n\t\t_, ok := lset[\"mute\"]\n\t\treturn ok\n\t})\n\n\tmetrics := NewMetrics(prometheus.NewRegistry(), featurecontrol.NoopFlags{})\n\tstage := NewMuteStage(muter, metrics)\n\n\tin := []model.LabelSet{\n\t\t{},\n\t\t{\"test\": \"set\"},\n\t\t{\"mute\": \"me\"},\n\t\t{\"foo\": \"bar\", \"test\": \"set\"},\n\t\t{\"foo\": \"bar\", \"mute\": \"me\"},\n\t\t{},\n\t\t{\"not\": \"muted\"},\n\t}\n\tout := []model.LabelSet{\n\t\t{},\n\t\t{\"test\": \"set\"},\n\t\t{\"foo\": \"bar\", \"test\": \"set\"},\n\t\t{},\n\t\t{\"not\": \"muted\"},\n\t}\n\n\tvar inAlerts []*types.Alert\n\tfor _, lset := range in {\n\t\tinAlerts = append(inAlerts, &types.Alert{\n\t\t\tAlert: model.Alert{Labels: lset},\n\t\t})\n\t}\n\n\t_, alerts, err := stage.Exec(context.Background(), promslog.NewNopLogger(), inAlerts...)\n\tif err != nil {\n\t\tt.Fatalf(\"Exec failed: %s\", err)\n\t}\n\n\tvar got []model.LabelSet\n\tfor _, a := range alerts {\n\t\tgot = append(got, a.Labels)\n\t}\n\n\tif !reflect.DeepEqual(got, out) {\n\t\tt.Fatalf(\"Muting failed, expected: %v\\ngot %v\", out, got)\n\t}\n\tsuppressed := int(prom_testutil.ToFloat64(metrics.numNotificationSuppressedTotal))\n\tif (len(in) - len(got)) != suppressed {\n\t\tt.Fatalf(\"Expected %d alerts counted in suppressed metric but got %d\", (len(in) - len(got)), suppressed)\n\t}\n}\n\nfunc TestMuteStageWithSilences(t *testing.T) {\n\tsilences, err := silence.New(silence.Options{Metrics: prometheus.NewRegistry(), Retention: time.Hour})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tsil := &silencepb.Silence{\n\t\tEndsAt: timestamppb.New(utcNow().Add(time.Hour)),\n\t\tMatcherSets: []*silencepb.MatcherSet{{\n\t\t\tMatchers: []*silencepb.Matcher{{Name: \"mute\", Pattern: \"me\"}},\n\t\t}},\n\t}\n\tif err = silences.Set(t.Context(), sil); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treg := prometheus.NewRegistry()\n\tmarker := types.NewMarker(reg)\n\tsilencer := silence.NewSilencer(silences, marker, promslog.NewNopLogger())\n\tmetrics := NewMetrics(reg, featurecontrol.NoopFlags{})\n\tstage := NewMuteStage(silencer, metrics)\n\n\tin := []model.LabelSet{\n\t\t{},\n\t\t{\"test\": \"set\"},\n\t\t{\"mute\": \"me\"},\n\t\t{\"foo\": \"bar\", \"test\": \"set\"},\n\t\t{\"foo\": \"bar\", \"mute\": \"me\"},\n\t\t{},\n\t\t{\"not\": \"muted\"},\n\t}\n\tout := []model.LabelSet{\n\t\t{},\n\t\t{\"test\": \"set\"},\n\t\t{\"foo\": \"bar\", \"test\": \"set\"},\n\t\t{},\n\t\t{\"not\": \"muted\"},\n\t}\n\n\tvar inAlerts []*types.Alert\n\tfor _, lset := range in {\n\t\tinAlerts = append(inAlerts, &types.Alert{\n\t\t\tAlert: model.Alert{Labels: lset},\n\t\t})\n\t}\n\n\t// Set the second alert as previously silenced with an old version\n\t// number. This is expected to get unsilenced by the stage.\n\tmarker.SetActiveOrSilenced(inAlerts[1].Fingerprint(), []string{\"123\"})\n\n\t_, alerts, err := stage.Exec(context.Background(), promslog.NewNopLogger(), inAlerts...)\n\tif err != nil {\n\t\tt.Fatalf(\"Exec failed: %s\", err)\n\t}\n\n\tvar got []model.LabelSet\n\tfor _, a := range alerts {\n\t\tgot = append(got, a.Labels)\n\t}\n\n\tif !reflect.DeepEqual(got, out) {\n\t\tt.Fatalf(\"Muting failed, expected: %v\\ngot %v\", out, got)\n\t}\n\tsuppressedRoundOne := int(prom_testutil.ToFloat64(metrics.numNotificationSuppressedTotal))\n\tif (len(in) - len(got)) != suppressedRoundOne {\n\t\tt.Fatalf(\"Expected %d alerts counted in suppressed metric but got %d\", (len(in) - len(got)), suppressedRoundOne)\n\t}\n\n\t// Do it again to exercise the version tracking of silences.\n\t_, alerts, err = stage.Exec(context.Background(), promslog.NewNopLogger(), inAlerts...)\n\tif err != nil {\n\t\tt.Fatalf(\"Exec failed: %s\", err)\n\t}\n\n\tgot = got[:0]\n\tfor _, a := range alerts {\n\t\tgot = append(got, a.Labels)\n\t}\n\n\tif !reflect.DeepEqual(got, out) {\n\t\tt.Fatalf(\"Muting failed, expected: %v\\ngot %v\", out, got)\n\t}\n\n\tsuppressedRoundTwo := int(prom_testutil.ToFloat64(metrics.numNotificationSuppressedTotal))\n\tif (len(in) - len(got) + suppressedRoundOne) != suppressedRoundTwo {\n\t\tt.Fatalf(\"Expected %d alerts counted in suppressed metric but got %d\", (len(in) - len(got)), suppressedRoundTwo)\n\t}\n\n\t// Expire the silence and verify that no alerts are silenced now.\n\tif err := silences.Expire(t.Context(), sil.Id); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t_, alerts, err = stage.Exec(t.Context(), promslog.NewNopLogger(), inAlerts...)\n\tif err != nil {\n\t\tt.Fatalf(\"Exec failed: %s\", err)\n\t}\n\tgot = got[:0]\n\tfor _, a := range alerts {\n\t\tgot = append(got, a.Labels)\n\t}\n\n\tif !reflect.DeepEqual(got, in) {\n\t\tt.Fatalf(\"Unmuting failed, expected: %v\\ngot %v\", in, got)\n\t}\n\tsuppressedRoundThree := int(prom_testutil.ToFloat64(metrics.numNotificationSuppressedTotal))\n\tif (len(in) - len(got) + suppressedRoundTwo) != suppressedRoundThree {\n\t\tt.Fatalf(\"Expected %d alerts counted in suppressed metric but got %d\", (len(in) - len(got)), suppressedRoundThree)\n\t}\n}\n\nfunc TestTimeMuteStage(t *testing.T) {\n\tsydney, err := time.LoadLocation(\"Australia/Sydney\")\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to load location Australia/Sydney: %s\", err)\n\t}\n\teveningsAndWeekends := map[string][]timeinterval.TimeInterval{\n\t\t\"evenings\": {{\n\t\t\tTimes: []timeinterval.TimeRange{{\n\t\t\t\tStartMinute: 0,   // 00:00\n\t\t\t\tEndMinute:   540, // 09:00\n\t\t\t}, {\n\t\t\t\tStartMinute: 1020, // 17:00\n\t\t\t\tEndMinute:   1440, // 24:00\n\t\t\t}},\n\t\t\tLocation: &timeinterval.Location{Location: sydney},\n\t\t}},\n\t\t\"weekends\": {{\n\t\t\tWeekdays: []timeinterval.WeekdayRange{{\n\t\t\t\tInclusiveRange: timeinterval.InclusiveRange{Begin: 6, End: 6}, // Saturday\n\t\t\t}, {\n\t\t\t\tInclusiveRange: timeinterval.InclusiveRange{Begin: 0, End: 0}, // Sunday\n\t\t\t}},\n\t\t\tLocation: &timeinterval.Location{Location: sydney},\n\t\t}},\n\t}\n\n\ttests := []struct {\n\t\tname      string\n\t\tintervals map[string][]timeinterval.TimeInterval\n\t\tnow       time.Time\n\t\talerts    []*types.Alert\n\t\tmutedBy   []string\n\t}{{\n\t\tname:      \"Should be muted outside working hours\",\n\t\tintervals: eveningsAndWeekends,\n\t\tnow:       time.Date(2024, 1, 1, 0, 0, 0, 0, sydney),\n\t\talerts:    []*types.Alert{{Alert: model.Alert{Labels: model.LabelSet{\"foo\": \"bar\"}}}},\n\t\tmutedBy:   []string{\"evenings\"},\n\t}, {\n\t\tname:      \"Should not be muted during workings hours\",\n\t\tintervals: eveningsAndWeekends,\n\t\tnow:       time.Date(2024, 1, 1, 9, 0, 0, 0, sydney),\n\t\talerts:    []*types.Alert{{Alert: model.Alert{Labels: model.LabelSet{\"foo\": \"bar\"}}}},\n\t\tmutedBy:   nil,\n\t}, {\n\t\tname:      \"Should be muted during weekends\",\n\t\tintervals: eveningsAndWeekends,\n\t\tnow:       time.Date(2024, 1, 6, 10, 0, 0, 0, sydney),\n\t\talerts:    []*types.Alert{{Alert: model.Alert{Labels: model.LabelSet{\"foo\": \"bar\"}}}},\n\t\tmutedBy:   []string{\"weekends\"},\n\t}, {\n\t\tname:      \"Should be muted at 12pm UTC on a weekday\",\n\t\tintervals: eveningsAndWeekends,\n\t\tnow:       time.Date(2024, 1, 1, 10, 0, 0, 0, time.UTC),\n\t\talerts:    []*types.Alert{{Alert: model.Alert{Labels: model.LabelSet{\"foo\": \"bar\"}}}},\n\t\tmutedBy:   []string{\"evenings\"},\n\t}, {\n\t\tname:      \"Should be muted at 12pm UTC on a weekend\",\n\t\tintervals: eveningsAndWeekends,\n\t\tnow:       time.Date(2024, 1, 6, 10, 0, 0, 0, time.UTC),\n\t\talerts:    []*types.Alert{{Alert: model.Alert{Labels: model.LabelSet{\"foo\": \"bar\"}}}},\n\t\tmutedBy:   []string{\"evenings\", \"weekends\"},\n\t}}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tr := prometheus.NewRegistry()\n\t\t\tmarker := types.NewMarker(r)\n\t\t\tmetrics := NewMetrics(r, featurecontrol.NoopFlags{})\n\t\t\tintervener := timeinterval.NewIntervener(test.intervals)\n\t\t\tst := NewTimeMuteStage(intervener, marker, metrics)\n\n\t\t\t// Get the names of all time intervals for the context.\n\t\t\tmuteTimeIntervalNames := make([]string, 0, len(test.intervals))\n\t\t\tfor name := range test.intervals {\n\t\t\t\tmuteTimeIntervalNames = append(muteTimeIntervalNames, name)\n\t\t\t}\n\t\t\t// Sort the names so we can compare mutedBy with test.mutedBy.\n\t\t\tsort.Strings(muteTimeIntervalNames)\n\n\t\t\tctx := context.Background()\n\t\t\tctx = WithNow(ctx, test.now)\n\t\t\tctx = WithGroupKey(ctx, \"group1\")\n\t\t\tctx = WithActiveTimeIntervals(ctx, nil)\n\t\t\tctx = WithMuteTimeIntervals(ctx, muteTimeIntervalNames)\n\t\t\tctx = WithRouteID(ctx, \"route1\")\n\n\t\t\t_, active, err := st.Exec(ctx, promslog.NewNopLogger(), test.alerts...)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif len(test.mutedBy) == 0 {\n\t\t\t\t// All alerts should be active.\n\t\t\t\trequire.Len(t, active, len(test.alerts))\n\t\t\t\t// The group should not be marked.\n\t\t\t\tmutedBy, isMuted := marker.Muted(\"route1\", \"group1\")\n\t\t\t\trequire.False(t, isMuted)\n\t\t\t\trequire.Empty(t, mutedBy)\n\t\t\t\t// The metric for total suppressed notifications should not\n\t\t\t\t// have been incremented, which means it will not be collected.\n\t\t\t\trequire.NoError(t, prom_testutil.GatherAndCompare(r, strings.NewReader(`\n# HELP alertmanager_marked_alerts How many alerts by state are currently marked in the Alertmanager regardless of their expiry.\n# TYPE alertmanager_marked_alerts gauge\nalertmanager_marked_alerts{state=\"active\"} 0\nalertmanager_marked_alerts{state=\"suppressed\"} 0\nalertmanager_marked_alerts{state=\"unprocessed\"} 0\n`)))\n\t\t\t} else {\n\t\t\t\t// All alerts should be muted.\n\t\t\t\trequire.Empty(t, active)\n\t\t\t\t// The group should be marked as muted.\n\t\t\t\tmutedBy, isMuted := marker.Muted(\"route1\", \"group1\")\n\t\t\t\trequire.True(t, isMuted)\n\t\t\t\trequire.Equal(t, test.mutedBy, mutedBy)\n\t\t\t\t// Gets the metric for total suppressed notifications.\n\t\t\t\trequire.NoError(t, prom_testutil.GatherAndCompare(r, strings.NewReader(fmt.Sprintf(`\n# HELP alertmanager_marked_alerts How many alerts by state are currently marked in the Alertmanager regardless of their expiry.\n# TYPE alertmanager_marked_alerts gauge\nalertmanager_marked_alerts{state=\"active\"} 0\nalertmanager_marked_alerts{state=\"suppressed\"} 0\nalertmanager_marked_alerts{state=\"unprocessed\"} 0\n# HELP alertmanager_notifications_suppressed_total The total number of notifications suppressed for being silenced, inhibited, outside of active time intervals or within muted time intervals.\n# TYPE alertmanager_notifications_suppressed_total counter\nalertmanager_notifications_suppressed_total{reason=\"mute_time_interval\"} %d\n`, len(test.alerts)))))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTimeActiveStage(t *testing.T) {\n\tsydney, err := time.LoadLocation(\"Australia/Sydney\")\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to load location Australia/Sydney: %s\", err)\n\t}\n\tweekdays := map[string][]timeinterval.TimeInterval{\n\t\t\"weekdays\": {{\n\t\t\tWeekdays: []timeinterval.WeekdayRange{{\n\t\t\t\tInclusiveRange: timeinterval.InclusiveRange{\n\t\t\t\t\tBegin: 1, // Monday\n\t\t\t\t\tEnd:   5, // Friday\n\t\t\t\t},\n\t\t\t}},\n\t\t\tTimes: []timeinterval.TimeRange{{\n\t\t\t\tStartMinute: 540,  // 09:00\n\t\t\t\tEndMinute:   1020, // 17:00\n\t\t\t}},\n\t\t\tLocation: &timeinterval.Location{Location: sydney},\n\t\t}},\n\t}\n\n\ttests := []struct {\n\t\tname      string\n\t\tintervals map[string][]timeinterval.TimeInterval\n\t\tnow       time.Time\n\t\talerts    []*types.Alert\n\t\tmutedBy   []string\n\t}{{\n\t\tname:      \"Should be muted outside working hours\",\n\t\tintervals: weekdays,\n\t\tnow:       time.Date(2024, 1, 1, 0, 0, 0, 0, sydney),\n\t\talerts:    []*types.Alert{{Alert: model.Alert{Labels: model.LabelSet{\"foo\": \"bar\"}}}},\n\t\tmutedBy:   []string{\"weekdays\"},\n\t}, {\n\t\tname:      \"Should not be muted during workings hours\",\n\t\tintervals: weekdays,\n\t\tnow:       time.Date(2024, 1, 1, 9, 0, 0, 0, sydney),\n\t\talerts:    []*types.Alert{{Alert: model.Alert{Labels: model.LabelSet{\"foo\": \"bar\"}}}},\n\t\tmutedBy:   nil,\n\t}, {\n\t\tname:      \"Should be muted during weekends\",\n\t\tintervals: weekdays,\n\t\tnow:       time.Date(2024, 1, 6, 10, 0, 0, 0, sydney),\n\t\talerts:    []*types.Alert{{Alert: model.Alert{Labels: model.LabelSet{\"foo\": \"bar\"}}}},\n\t\tmutedBy:   []string{\"weekdays\"},\n\t}, {\n\t\tname:      \"Should be muted at 12pm UTC\",\n\t\tintervals: weekdays,\n\t\tnow:       time.Date(2024, 1, 6, 10, 0, 0, 0, time.UTC),\n\t\talerts:    []*types.Alert{{Alert: model.Alert{Labels: model.LabelSet{\"foo\": \"bar\"}}}},\n\t\tmutedBy:   []string{\"weekdays\"},\n\t}}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tr := prometheus.NewRegistry()\n\t\t\tmarker := types.NewMarker(r)\n\t\t\tmetrics := NewMetrics(r, featurecontrol.NoopFlags{})\n\t\t\tintervener := timeinterval.NewIntervener(test.intervals)\n\t\t\tst := NewTimeActiveStage(intervener, marker, metrics)\n\n\t\t\t// Get the names of all time intervals for the context.\n\t\t\tactiveTimeIntervalNames := make([]string, 0, len(test.intervals))\n\t\t\tfor name := range test.intervals {\n\t\t\t\tactiveTimeIntervalNames = append(activeTimeIntervalNames, name)\n\t\t\t}\n\t\t\t// Sort the names so we can compare mutedBy with test.mutedBy.\n\t\t\tsort.Strings(activeTimeIntervalNames)\n\n\t\t\tctx := context.Background()\n\t\t\tctx = WithNow(ctx, test.now)\n\t\t\tctx = WithGroupKey(ctx, \"group1\")\n\t\t\tctx = WithActiveTimeIntervals(ctx, activeTimeIntervalNames)\n\t\t\tctx = WithMuteTimeIntervals(ctx, nil)\n\t\t\tctx = WithRouteID(ctx, \"route1\")\n\n\t\t\t_, active, err := st.Exec(ctx, promslog.NewNopLogger(), test.alerts...)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif len(test.mutedBy) == 0 {\n\t\t\t\t// All alerts should be active.\n\t\t\t\trequire.Len(t, active, len(test.alerts))\n\t\t\t\t// The group should not be marked.\n\t\t\t\tmutedBy, isMuted := marker.Muted(\"route1\", \"group1\")\n\t\t\t\trequire.False(t, isMuted)\n\t\t\t\trequire.Empty(t, mutedBy)\n\t\t\t\t// The metric for total suppressed notifications should not\n\t\t\t\t// have been incremented, which means it will not be collected.\n\t\t\t\trequire.NoError(t, prom_testutil.GatherAndCompare(r, strings.NewReader(`\n# HELP alertmanager_marked_alerts How many alerts by state are currently marked in the Alertmanager regardless of their expiry.\n# TYPE alertmanager_marked_alerts gauge\nalertmanager_marked_alerts{state=\"active\"} 0\nalertmanager_marked_alerts{state=\"suppressed\"} 0\nalertmanager_marked_alerts{state=\"unprocessed\"} 0\n`)))\n\t\t\t} else {\n\t\t\t\t// All alerts should be muted.\n\t\t\t\trequire.Empty(t, active)\n\t\t\t\t// The group should be marked as muted.\n\t\t\t\tmutedBy, isMuted := marker.Muted(\"route1\", \"group1\")\n\t\t\t\trequire.True(t, isMuted)\n\t\t\t\trequire.Equal(t, test.mutedBy, mutedBy)\n\t\t\t\t// Gets the metric for total suppressed notifications.\n\t\t\t\trequire.NoError(t, prom_testutil.GatherAndCompare(r, strings.NewReader(fmt.Sprintf(`\n# HELP alertmanager_marked_alerts How many alerts by state are currently marked in the Alertmanager regardless of their expiry.\n# TYPE alertmanager_marked_alerts gauge\nalertmanager_marked_alerts{state=\"active\"} 0\nalertmanager_marked_alerts{state=\"suppressed\"} 0\nalertmanager_marked_alerts{state=\"unprocessed\"} 0\n# HELP alertmanager_notifications_suppressed_total The total number of notifications suppressed for being silenced, inhibited, outside of active time intervals or within muted time intervals.\n# TYPE alertmanager_notifications_suppressed_total counter\nalertmanager_notifications_suppressed_total{reason=\"active_time_interval\"} %d\n`, len(test.alerts)))))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "notify/notify.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage notify\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v4\"\n\t\"github.com/cespare/xxhash/v2\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n\t\"github.com/prometheus/common/model\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/codes\"\n\t\"go.opentelemetry.io/otel/trace\"\n\n\t\"github.com/prometheus/alertmanager/featurecontrol\"\n\t\"github.com/prometheus/alertmanager/inhibit\"\n\t\"github.com/prometheus/alertmanager/nflog\"\n\t\"github.com/prometheus/alertmanager/nflog/nflogpb\"\n\t\"github.com/prometheus/alertmanager/silence\"\n\t\"github.com/prometheus/alertmanager/timeinterval\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nvar tracer = otel.Tracer(\"github.com/prometheus/alertmanager/notify\")\n\n// ResolvedSender returns true if resolved notifications should be sent.\ntype ResolvedSender interface {\n\tSendResolved() bool\n}\n\n// Peer represents the cluster node from where we are the sending the notification.\ntype Peer interface {\n\t// WaitReady waits until the node silences and notifications have settled before attempting to send a notification.\n\tWaitReady(context.Context) error\n}\n\n// MinTimeout is the minimum timeout that is set for the context of a call\n// to a notification pipeline.\nconst MinTimeout = 10 * time.Second\n\n// Notifier notifies about alerts under constraints of the given context. It\n// returns an error if unsuccessful and a flag whether the error is\n// recoverable. This information is useful for a retry logic.\ntype Notifier interface {\n\tNotify(context.Context, ...*types.Alert) (bool, error)\n}\n\n// Integration wraps a notifier and its configuration to be uniquely identified\n// by name and index from its origin in the configuration.\ntype Integration struct {\n\tnotifier     Notifier\n\trs           ResolvedSender\n\tname         string\n\tidx          int\n\treceiverName string\n}\n\n// NewIntegration returns a new integration.\nfunc NewIntegration(notifier Notifier, rs ResolvedSender, name string, idx int, receiverName string) Integration {\n\treturn Integration{\n\t\tnotifier:     notifier,\n\t\trs:           rs,\n\t\tname:         name,\n\t\tidx:          idx,\n\t\treceiverName: receiverName,\n\t}\n}\n\n// Notify implements the Notifier interface.\nfunc (i *Integration) Notify(ctx context.Context, alerts ...*types.Alert) (recoverable bool, err error) {\n\tctx, span := tracer.Start(ctx, \"notify.Integration.Notify\",\n\t\ttrace.WithAttributes(attribute.String(\"alerting.notify.integration.name\", i.name)),\n\t\ttrace.WithAttributes(attribute.Int(\"alerting.alerts.count\", len(alerts))),\n\t\ttrace.WithSpanKind(trace.SpanKindClient),\n\t)\n\n\tdefer func() {\n\t\tspan.SetAttributes(attribute.Bool(\"alerting.notify.error.recoverable\", recoverable))\n\t\tif err != nil {\n\t\t\tspan.SetStatus(codes.Error, err.Error())\n\t\t\tspan.RecordError(err)\n\t\t}\n\t\tspan.End()\n\t}()\n\n\trecoverable, err = i.notifier.Notify(ctx, alerts...)\n\treturn recoverable, err\n}\n\n// SendResolved implements the ResolvedSender interface.\nfunc (i *Integration) SendResolved() bool {\n\treturn i.rs.SendResolved()\n}\n\n// Name returns the name of the integration.\nfunc (i *Integration) Name() string {\n\treturn i.name\n}\n\n// Index returns the index of the integration.\nfunc (i *Integration) Index() int {\n\treturn i.idx\n}\n\n// String implements the Stringer interface.\nfunc (i *Integration) String() string {\n\treturn fmt.Sprintf(\"%s[%d]\", i.name, i.idx)\n}\n\n// notifyKey defines a custom type with which a context is populated to\n// avoid accidental collisions.\ntype notifyKey int\n\nconst (\n\tkeyReceiverName notifyKey = iota\n\tkeyRepeatInterval\n\tkeyGroupLabels\n\tkeyGroupKey\n\tkeyFiringAlerts\n\tkeyResolvedAlerts\n\tkeyNow\n\tkeyMuteTimeIntervals\n\tkeyActiveTimeIntervals\n\tkeyRouteID\n\tkeyNflogStore\n\tkeyNotificationReason\n)\n\n// WithReceiverName populates a context with a receiver name.\nfunc WithReceiverName(ctx context.Context, rcv string) context.Context {\n\treturn context.WithValue(ctx, keyReceiverName, rcv)\n}\n\n// WithGroupKey populates a context with a group key.\nfunc WithGroupKey(ctx context.Context, s string) context.Context {\n\treturn context.WithValue(ctx, keyGroupKey, s)\n}\n\n// WithFiringAlerts populates a context with a slice of firing alerts.\nfunc WithFiringAlerts(ctx context.Context, alerts []uint64) context.Context {\n\treturn context.WithValue(ctx, keyFiringAlerts, alerts)\n}\n\n// WithResolvedAlerts populates a context with a slice of resolved alerts.\nfunc WithResolvedAlerts(ctx context.Context, alerts []uint64) context.Context {\n\treturn context.WithValue(ctx, keyResolvedAlerts, alerts)\n}\n\n// WithGroupLabels populates a context with grouping labels.\nfunc WithGroupLabels(ctx context.Context, lset model.LabelSet) context.Context {\n\treturn context.WithValue(ctx, keyGroupLabels, lset)\n}\n\n// WithNow populates a context with a now timestamp.\nfunc WithNow(ctx context.Context, t time.Time) context.Context {\n\treturn context.WithValue(ctx, keyNow, t)\n}\n\n// WithRepeatInterval populates a context with a repeat interval.\nfunc WithRepeatInterval(ctx context.Context, t time.Duration) context.Context {\n\treturn context.WithValue(ctx, keyRepeatInterval, t)\n}\n\n// WithMuteTimeIntervals populates a context with a slice of mute time names.\nfunc WithMuteTimeIntervals(ctx context.Context, mt []string) context.Context {\n\treturn context.WithValue(ctx, keyMuteTimeIntervals, mt)\n}\n\nfunc WithActiveTimeIntervals(ctx context.Context, at []string) context.Context {\n\treturn context.WithValue(ctx, keyActiveTimeIntervals, at)\n}\n\nfunc WithRouteID(ctx context.Context, routeID string) context.Context {\n\treturn context.WithValue(ctx, keyRouteID, routeID)\n}\n\nfunc WithNotificationReason(ctx context.Context, reason NotifyReason) context.Context {\n\treturn context.WithValue(ctx, keyNotificationReason, reason)\n}\n\n// RepeatInterval extracts a repeat interval from the context. Iff none exists, the\n// second argument is false.\nfunc RepeatInterval(ctx context.Context) (time.Duration, bool) {\n\tv, ok := ctx.Value(keyRepeatInterval).(time.Duration)\n\treturn v, ok\n}\n\n// ReceiverName extracts a receiver name from the context. Iff none exists, the\n// second argument is false.\nfunc ReceiverName(ctx context.Context) (string, bool) {\n\tv, ok := ctx.Value(keyReceiverName).(string)\n\treturn v, ok\n}\n\n// GroupKey extracts a group key from the context. Iff none exists, the\n// second argument is false.\nfunc GroupKey(ctx context.Context) (string, bool) {\n\tv, ok := ctx.Value(keyGroupKey).(string)\n\treturn v, ok\n}\n\n// GroupLabels extracts grouping label set from the context. Iff none exists, the\n// second argument is false.\nfunc GroupLabels(ctx context.Context) (model.LabelSet, bool) {\n\tv, ok := ctx.Value(keyGroupLabels).(model.LabelSet)\n\treturn v, ok\n}\n\n// Now extracts a now timestamp from the context. Iff none exists, the\n// second argument is false.\nfunc Now(ctx context.Context) (time.Time, bool) {\n\tv, ok := ctx.Value(keyNow).(time.Time)\n\treturn v, ok\n}\n\n// FiringAlerts extracts a slice of firing alerts from the context.\n// Iff none exists, the second argument is false.\nfunc FiringAlerts(ctx context.Context) ([]uint64, bool) {\n\tv, ok := ctx.Value(keyFiringAlerts).([]uint64)\n\treturn v, ok\n}\n\n// ResolvedAlerts extracts a slice of firing alerts from the context.\n// Iff none exists, the second argument is false.\nfunc ResolvedAlerts(ctx context.Context) ([]uint64, bool) {\n\tv, ok := ctx.Value(keyResolvedAlerts).([]uint64)\n\treturn v, ok\n}\n\n// MuteTimeIntervalNames extracts a slice of mute time names from the context. If and only if none exists, the\n// second argument is false.\nfunc MuteTimeIntervalNames(ctx context.Context) ([]string, bool) {\n\tv, ok := ctx.Value(keyMuteTimeIntervals).([]string)\n\treturn v, ok\n}\n\n// ActiveTimeIntervalNames extracts a slice of active time names from the context. If none exists, the\n// second argument is false.\nfunc ActiveTimeIntervalNames(ctx context.Context) ([]string, bool) {\n\tv, ok := ctx.Value(keyActiveTimeIntervals).([]string)\n\treturn v, ok\n}\n\n// RouteID extracts a RouteID from the context. Iff none exists, the\n// // second argument is false.\nfunc RouteID(ctx context.Context) (string, bool) {\n\tv, ok := ctx.Value(keyRouteID).(string)\n\treturn v, ok\n}\n\nfunc NotificationReason(ctx context.Context) (NotifyReason, bool) {\n\tv, ok := ctx.Value(keyNotificationReason).(NotifyReason)\n\treturn v, ok\n}\n\nfunc WithNflogStore(ctx context.Context, store *nflog.Store) context.Context {\n\treturn context.WithValue(ctx, keyNflogStore, store)\n}\n\nfunc NflogStore(ctx context.Context) (*nflog.Store, bool) {\n\tv, ok := ctx.Value(keyNflogStore).(*nflog.Store)\n\treturn v, ok\n}\n\n// A Stage processes alerts under the constraints of the given context.\ntype Stage interface {\n\tExec(ctx context.Context, l *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error)\n}\n\n// StageFunc wraps a function to represent a Stage.\ntype StageFunc func(ctx context.Context, l *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error)\n\n// Exec implements Stage interface.\nfunc (f StageFunc) Exec(ctx context.Context, l *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {\n\treturn f(ctx, l, alerts...)\n}\n\ntype NotificationLog interface {\n\tLog(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64, store *nflog.Store, expiry time.Duration) error\n\tQuery(params ...nflog.QueryParam) ([]*nflogpb.Entry, error)\n}\n\ntype Metrics struct {\n\tnumNotifications                   *prometheus.CounterVec\n\tnumTotalFailedNotifications        *prometheus.CounterVec\n\tnumNotificationRequestsTotal       *prometheus.CounterVec\n\tnumNotificationRequestsFailedTotal *prometheus.CounterVec\n\tnumNotificationSuppressedTotal     *prometheus.CounterVec\n\tnotificationLatencySeconds         *prometheus.HistogramVec\n\n\tff featurecontrol.Flagger\n}\n\nfunc NewMetrics(r prometheus.Registerer, ff featurecontrol.Flagger) *Metrics {\n\tlabels := []string{\"integration\"}\n\n\tif ff.EnableReceiverNamesInMetrics() {\n\t\tlabels = append(labels, \"receiver_name\")\n\t}\n\n\tm := &Metrics{\n\t\tnumNotifications: promauto.With(r).NewCounterVec(prometheus.CounterOpts{\n\t\t\tNamespace: \"alertmanager\",\n\t\t\tName:      \"notifications_total\",\n\t\t\tHelp:      \"The total number of attempted notifications.\",\n\t\t}, labels),\n\t\tnumTotalFailedNotifications: promauto.With(r).NewCounterVec(prometheus.CounterOpts{\n\t\t\tNamespace: \"alertmanager\",\n\t\t\tName:      \"notifications_failed_total\",\n\t\t\tHelp:      \"The total number of failed notifications.\",\n\t\t}, append(labels, \"reason\")),\n\t\tnumNotificationRequestsTotal: promauto.With(r).NewCounterVec(prometheus.CounterOpts{\n\t\t\tNamespace: \"alertmanager\",\n\t\t\tName:      \"notification_requests_total\",\n\t\t\tHelp:      \"The total number of attempted notification requests.\",\n\t\t}, labels),\n\t\tnumNotificationRequestsFailedTotal: promauto.With(r).NewCounterVec(prometheus.CounterOpts{\n\t\t\tNamespace: \"alertmanager\",\n\t\t\tName:      \"notification_requests_failed_total\",\n\t\t\tHelp:      \"The total number of failed notification requests.\",\n\t\t}, labels),\n\t\tnumNotificationSuppressedTotal: promauto.With(r).NewCounterVec(prometheus.CounterOpts{\n\t\t\tNamespace: \"alertmanager\",\n\t\t\tName:      \"notifications_suppressed_total\",\n\t\t\tHelp:      \"The total number of notifications suppressed for being silenced, inhibited, outside of active time intervals or within muted time intervals.\",\n\t\t}, []string{\"reason\"}),\n\t\tnotificationLatencySeconds: promauto.With(r).NewHistogramVec(prometheus.HistogramOpts{\n\t\t\tNamespace:                       \"alertmanager\",\n\t\t\tName:                            \"notification_latency_seconds\",\n\t\t\tHelp:                            \"The latency of notifications in seconds.\",\n\t\t\tBuckets:                         []float64{1, 5, 10, 15, 20},\n\t\t\tNativeHistogramBucketFactor:     1.1,\n\t\t\tNativeHistogramMaxBucketNumber:  100,\n\t\t\tNativeHistogramMinResetDuration: 1 * time.Hour,\n\t\t}, labels),\n\t\tff: ff,\n\t}\n\n\treturn m\n}\n\nfunc (m *Metrics) InitializeFor(receiver map[string][]Integration) {\n\tif m.ff.EnableReceiverNamesInMetrics() {\n\n\t\t// Reset the vectors to take into account receiver names changing after hot reloads.\n\t\tm.numNotifications.Reset()\n\t\tm.numNotificationRequestsTotal.Reset()\n\t\tm.numNotificationRequestsFailedTotal.Reset()\n\t\tm.notificationLatencySeconds.Reset()\n\t\tm.numTotalFailedNotifications.Reset()\n\n\t\tfor name, integrations := range receiver {\n\t\t\tfor _, integration := range integrations {\n\n\t\t\t\tm.numNotifications.WithLabelValues(integration.Name(), name)\n\t\t\t\tm.numNotificationRequestsTotal.WithLabelValues(integration.Name(), name)\n\t\t\t\tm.numNotificationRequestsFailedTotal.WithLabelValues(integration.Name(), name)\n\t\t\t\tm.notificationLatencySeconds.WithLabelValues(integration.Name(), name)\n\n\t\t\t\tfor _, reason := range possibleFailureReasonCategory {\n\t\t\t\t\tm.numTotalFailedNotifications.WithLabelValues(integration.Name(), name, reason)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn\n\t}\n\n\t// When the feature flag is not enabled, we just carry on registering _all_ the integrations.\n\tfor _, integration := range []string{\n\t\t\"email\",\n\t\t\"pagerduty\",\n\t\t\"wechat\",\n\t\t\"pushover\",\n\t\t\"slack\",\n\t\t\"opsgenie\",\n\t\t\"webhook\",\n\t\t\"victorops\",\n\t\t\"sns\",\n\t\t\"telegram\",\n\t\t\"discord\",\n\t\t\"webex\",\n\t\t\"msteams\",\n\t\t\"msteamsv2\",\n\t\t\"incidentio\",\n\t\t\"jira\",\n\t\t\"rocketchat\",\n\t\t\"mattermost\",\n\t} {\n\t\tm.numNotifications.WithLabelValues(integration)\n\t\tm.numNotificationRequestsTotal.WithLabelValues(integration)\n\t\tm.numNotificationRequestsFailedTotal.WithLabelValues(integration)\n\t\tm.notificationLatencySeconds.WithLabelValues(integration)\n\n\t\tfor _, reason := range possibleFailureReasonCategory {\n\t\t\tm.numTotalFailedNotifications.WithLabelValues(integration, reason)\n\t\t}\n\t}\n}\n\ntype PipelineBuilder struct {\n\tmetrics *Metrics\n\tff      featurecontrol.Flagger\n}\n\nfunc NewPipelineBuilder(r prometheus.Registerer, ff featurecontrol.Flagger) *PipelineBuilder {\n\treturn &PipelineBuilder{\n\t\tmetrics: NewMetrics(r, ff),\n\t\tff:      ff,\n\t}\n}\n\n// New returns a map of receivers to Stages.\nfunc (pb *PipelineBuilder) New(\n\treceivers map[string][]Integration,\n\twait func() time.Duration,\n\tinhibitor *inhibit.Inhibitor,\n\tsilencer *silence.Silencer,\n\tintervener *timeinterval.Intervener,\n\tmarker types.GroupMarker,\n\tnotificationLog NotificationLog,\n\tpeer Peer,\n) RoutingStage {\n\trs := make(RoutingStage, len(receivers))\n\n\tms := NewGossipSettleStage(peer)\n\tis := NewMuteStage(inhibitor, pb.metrics)\n\ttas := NewTimeActiveStage(intervener, marker, pb.metrics)\n\ttms := NewTimeMuteStage(intervener, marker, pb.metrics)\n\tss := NewMuteStage(silencer, pb.metrics)\n\n\tfor name := range receivers {\n\t\tst := createReceiverStage(name, receivers[name], wait, notificationLog, pb.metrics)\n\t\trs[name] = MultiStage{ms, is, tas, tms, ss, st}\n\t}\n\n\tpb.metrics.InitializeFor(receivers)\n\n\treturn rs\n}\n\n// createReceiverStage creates a pipeline of stages for a receiver.\nfunc createReceiverStage(\n\tname string,\n\tintegrations []Integration,\n\twait func() time.Duration,\n\tnotificationLog NotificationLog,\n\tmetrics *Metrics,\n) Stage {\n\tvar fs FanoutStage\n\tfor i := range integrations {\n\t\trecv := &nflogpb.Receiver{\n\t\t\tGroupName:   name,\n\t\t\tIntegration: integrations[i].Name(),\n\t\t\tIdx:         uint32(integrations[i].Index()),\n\t\t}\n\t\tvar s MultiStage\n\t\ts = append(s, NewWaitStage(wait))\n\t\ts = append(s, NewDedupStage(&integrations[i], notificationLog, recv))\n\t\ts = append(s, NewRetryStage(integrations[i], name, metrics))\n\t\ts = append(s, NewSetNotifiesStage(notificationLog, recv))\n\n\t\tfs = append(fs, s)\n\t}\n\treturn fs\n}\n\n// RoutingStage executes the inner stages based on the receiver specified in\n// the context.\ntype RoutingStage map[string]Stage\n\n// Exec implements the Stage interface.\nfunc (rs RoutingStage) Exec(ctx context.Context, l *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {\n\treceiver, ok := ReceiverName(ctx)\n\tif !ok {\n\t\treturn ctx, nil, errors.New(\"receiver missing\")\n\t}\n\n\tctx, span := tracer.Start(ctx, \"notify.RoutingStage.Exec\",\n\t\ttrace.WithAttributes(\n\t\t\tattribute.String(\"alerting.notify.receiver.name\", receiver),\n\t\t\tattribute.Int(\"alerting.alerts.count\", len(alerts)),\n\t\t),\n\t\ttrace.WithSpanKind(trace.SpanKindInternal),\n\t)\n\tdefer span.End()\n\n\ts, ok := rs[receiver]\n\tif !ok {\n\t\treturn ctx, nil, errors.New(\"stage for receiver missing\")\n\t}\n\n\treturn s.Exec(ctx, l, alerts...)\n}\n\n// A MultiStage executes a series of stages sequentially.\ntype MultiStage []Stage\n\n// Exec implements the Stage interface.\nfunc (ms MultiStage) Exec(ctx context.Context, l *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {\n\tvar err error\n\tfor _, s := range ms {\n\t\tif len(alerts) == 0 {\n\t\t\treturn ctx, nil, nil\n\t\t}\n\n\t\tctx, alerts, err = s.Exec(ctx, l, alerts...)\n\t\tif err != nil {\n\t\t\treturn ctx, nil, err\n\t\t}\n\t}\n\treturn ctx, alerts, nil\n}\n\n// FanoutStage executes its stages concurrently.\ntype FanoutStage []Stage\n\n// Exec attempts to execute all stages concurrently and discards the results.\n// It returns its input alerts and an error if one or more stages fail.\nfunc (fs FanoutStage) Exec(ctx context.Context, l *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {\n\tvar (\n\t\twg   sync.WaitGroup\n\t\tmtx  sync.Mutex\n\t\terrs error\n\t)\n\twg.Add(len(fs))\n\n\tfor _, s := range fs {\n\t\tgo func(s Stage) {\n\t\t\tif _, _, err := s.Exec(ctx, l, alerts...); err != nil {\n\t\t\t\tmtx.Lock()\n\t\t\t\terrs = errors.Join(errs, err)\n\t\t\t\tmtx.Unlock()\n\t\t\t}\n\t\t\twg.Done()\n\t\t}(s)\n\t}\n\twg.Wait()\n\n\treturn ctx, alerts, errs\n}\n\n// GossipSettleStage waits until the Gossip has settled to forward alerts.\ntype GossipSettleStage struct {\n\tpeer Peer\n}\n\n// NewGossipSettleStage returns a new GossipSettleStage.\nfunc NewGossipSettleStage(p Peer) *GossipSettleStage {\n\treturn &GossipSettleStage{peer: p}\n}\n\nfunc (n *GossipSettleStage) Exec(ctx context.Context, _ *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {\n\tif n.peer != nil {\n\t\tif err := n.peer.WaitReady(ctx); err != nil {\n\t\t\treturn ctx, nil, err\n\t\t}\n\t}\n\treturn ctx, alerts, nil\n}\n\nconst (\n\tSuppressedReasonSilence            = \"silence\"\n\tSuppressedReasonInhibition         = \"inhibition\"\n\tSuppressedReasonMuteTimeInterval   = \"mute_time_interval\"\n\tSuppressedReasonActiveTimeInterval = \"active_time_interval\"\n)\n\n// WaitStage waits for a certain amount of time before continuing or until the\n// context is done.\ntype WaitStage struct {\n\twait func() time.Duration\n}\n\n// NewWaitStage returns a new WaitStage.\nfunc NewWaitStage(wait func() time.Duration) *WaitStage {\n\treturn &WaitStage{\n\t\twait: wait,\n\t}\n}\n\n// Exec implements the Stage interface.\nfunc (ws *WaitStage) Exec(ctx context.Context, _ *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {\n\tselect {\n\tcase <-time.After(ws.wait()):\n\tcase <-ctx.Done():\n\t\treturn ctx, nil, ctx.Err()\n\t}\n\treturn ctx, alerts, nil\n}\n\n// DedupStage filters alerts.\n// Filtering happens based on a notification log.\ntype DedupStage struct {\n\trs    ResolvedSender\n\tnflog NotificationLog\n\trecv  *nflogpb.Receiver\n\n\tnow  func() time.Time\n\thash func(*types.Alert) uint64\n}\n\n// NewDedupStage wraps a DedupStage that runs against the given notification log.\nfunc NewDedupStage(rs ResolvedSender, l NotificationLog, recv *nflogpb.Receiver) *DedupStage {\n\treturn &DedupStage{\n\t\trs:    rs,\n\t\tnflog: l,\n\t\trecv:  recv,\n\t\tnow:   utcNow,\n\t\thash:  hashAlert,\n\t}\n}\n\nfunc utcNow() time.Time {\n\treturn time.Now().UTC()\n}\n\n// Wrap a slice in a struct so we can store a pointer in sync.Pool.\ntype hashBuffer struct {\n\tbuf []byte\n}\n\nvar hashBuffers = sync.Pool{\n\tNew: func() any { return &hashBuffer{buf: make([]byte, 0, 1024)} },\n}\n\nfunc hashAlert(a *types.Alert) uint64 {\n\tconst sep = '\\xff'\n\n\thb := hashBuffers.Get().(*hashBuffer)\n\tdefer hashBuffers.Put(hb)\n\tb := hb.buf[:0]\n\n\tnames := make(model.LabelNames, 0, len(a.Labels))\n\n\tfor ln := range a.Labels {\n\t\tnames = append(names, ln)\n\t}\n\tsort.Sort(names)\n\n\tfor _, ln := range names {\n\t\tb = append(b, string(ln)...)\n\t\tb = append(b, sep)\n\t\tb = append(b, string(a.Labels[ln])...)\n\t\tb = append(b, sep)\n\t}\n\n\thash := xxhash.Sum64(b)\n\n\treturn hash\n}\n\ntype NotifyReason int\n\nconst (\n\tReasonDoNotNotify NotifyReason = iota\n\tReasonFirstNotification\n\tReasonNewAlertsInGroup\n\tReasonNewResolvedAlerts\n\tReasonAllAlertsResolved\n\tReasonRepeatIntervalElapsed\n\tReasonUnknown\n)\n\nfunc (r NotifyReason) shouldNotify() bool {\n\treturn r != ReasonDoNotNotify\n}\n\nfunc (r NotifyReason) String() string {\n\tswitch r {\n\tcase ReasonDoNotNotify:\n\t\treturn \"none\"\n\tcase ReasonFirstNotification:\n\t\treturn \"first notification\"\n\tcase ReasonNewAlertsInGroup:\n\t\treturn \"new alerts added\"\n\tcase ReasonNewResolvedAlerts:\n\t\treturn \"some alerts resolved\"\n\tcase ReasonAllAlertsResolved:\n\t\treturn \"all alerts resolved\"\n\tcase ReasonRepeatIntervalElapsed:\n\t\treturn \"repeat interval elapsed\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\nfunc (n *DedupStage) needsUpdate(entry *nflogpb.Entry, firing, resolved map[uint64]struct{}, repeat time.Duration, now time.Time) NotifyReason {\n\t// If we haven't notified about the alert group before, notify right away\n\t// unless we only have resolved alerts.\n\tif entry == nil {\n\t\tif len(firing) > 0 {\n\t\t\treturn ReasonFirstNotification\n\t\t}\n\t\treturn ReasonDoNotNotify\n\t}\n\n\t// new alerts in the group\n\tif !entry.IsFiringSubset(firing) {\n\t\t// If the previous entry has no firing alerts, it was a resolution and we\n\t\t// should treat this as the first notification for the group.\n\t\tif len(entry.FiringAlerts) == 0 {\n\t\t\treturn ReasonFirstNotification\n\t\t}\n\t\treturn ReasonNewAlertsInGroup\n\t}\n\n\t// Notify about all alerts being resolved.\n\t// This is done irrespective of the send_resolved flag to make sure that\n\t// the firing alerts are cleared from the notification log.\n\tif len(firing) == 0 {\n\t\t// If the current alert group and last notification contain no firing\n\t\t// alert, it means that some alerts have been fired and resolved during the\n\t\t// last interval. In this case, there is no need to notify the receiver\n\t\t// since it doesn't know about them.\n\t\tif len(entry.FiringAlerts) > 0 {\n\t\t\treturn ReasonAllAlertsResolved\n\t\t}\n\t\treturn ReasonDoNotNotify\n\t}\n\n\tif n.rs.SendResolved() && !entry.IsResolvedSubset(resolved) {\n\t\treturn ReasonNewResolvedAlerts\n\t}\n\n\t// Nothing changed, only notify if the repeat interval has passed.\n\tisRepeatIntervalElapsed := entry.Timestamp.AsTime().Before(now.Add(-repeat))\n\tif isRepeatIntervalElapsed {\n\t\treturn ReasonRepeatIntervalElapsed\n\t}\n\treturn ReasonDoNotNotify\n}\n\n// partitionAlertsByState separates alerts into firing and resolved, returning both slices and sets.\nfunc partitionAlertsByState(alerts []*types.Alert, hashFn func(*types.Alert) uint64) (firing, resolved []uint64, firingSet, resolvedSet map[uint64]struct{}) {\n\tfiringSet = make(map[uint64]struct{}, len(alerts))\n\tresolvedSet = make(map[uint64]struct{}, len(alerts))\n\tfiring = make([]uint64, 0, len(alerts))\n\tresolved = make([]uint64, 0, len(alerts))\n\n\tfor _, a := range alerts {\n\t\thash := hashFn(a)\n\t\tif a.Resolved() {\n\t\t\tresolved = append(resolved, hash)\n\t\t\tresolvedSet[hash] = struct{}{}\n\t\t} else {\n\t\t\tfiring = append(firing, hash)\n\t\t\tfiringSet[hash] = struct{}{}\n\t\t}\n\t}\n\treturn firing, resolved, firingSet, resolvedSet\n}\n\n// Exec implements the Stage interface.\nfunc (n *DedupStage) Exec(ctx context.Context, _ *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {\n\tgkey, ok := GroupKey(ctx)\n\tif !ok {\n\t\treturn ctx, nil, errors.New(\"group key missing\")\n\t}\n\n\tctx, span := tracer.Start(ctx, \"notify.DedupStage.Exec\",\n\t\ttrace.WithAttributes(attribute.String(\"alerting.group.key\", gkey)),\n\t\ttrace.WithAttributes(attribute.Int(\"alerting.alerts.count\", len(alerts))),\n\t\ttrace.WithSpanKind(trace.SpanKindInternal),\n\t)\n\tdefer span.End()\n\n\trepeatInterval, ok := RepeatInterval(ctx)\n\tif !ok {\n\t\treturn ctx, nil, errors.New(\"repeat interval missing\")\n\t}\n\n\tfiring, resolved, firingSet, resolvedSet := partitionAlertsByState(alerts, n.hash)\n\n\tctx = WithFiringAlerts(ctx, firing)\n\tctx = WithResolvedAlerts(ctx, resolved)\n\n\tentries, err := n.nflog.Query(nflog.QGroupKey(gkey), nflog.QReceiver(n.recv))\n\tif err != nil && !errors.Is(err, nflog.ErrNotFound) {\n\t\treturn ctx, nil, err\n\t}\n\n\tvar entry *nflogpb.Entry\n\tswitch len(entries) {\n\tcase 0:\n\tcase 1:\n\t\tentry = entries[0]\n\tdefault:\n\t\treturn ctx, nil, fmt.Errorf(\"unexpected entry result size %d\", len(entries))\n\t}\n\n\tnow := n.now()\n\tif ctxNow, ok := Now(ctx); ok {\n\t\tnow = ctxNow\n\t}\n\tupdateReason := n.needsUpdate(entry, firingSet, resolvedSet, repeatInterval, now)\n\tctx = WithNotificationReason(ctx, updateReason)\n\n\tif updateReason == ReasonFirstNotification {\n\t\tctx = WithNflogStore(ctx, nflog.NewStore(nil))\n\t} else {\n\t\tctx = WithNflogStore(ctx, nflog.NewStore(entry))\n\t}\n\n\tif updateReason.shouldNotify() {\n\t\tspan.AddEvent(\"notify.DedupStage.Exec nflog needs update\")\n\t\treturn ctx, alerts, nil\n\t}\n\treturn ctx, nil, nil\n}\n\n// RetryStage notifies via passed integration with exponential backoff until it\n// succeeds. It aborts if the context is canceled or timed out.\ntype RetryStage struct {\n\tintegration Integration\n\tgroupName   string\n\tmetrics     *Metrics\n\tlabelValues []string\n}\n\n// NewRetryStage returns a new instance of a RetryStage.\nfunc NewRetryStage(i Integration, groupName string, metrics *Metrics) *RetryStage {\n\tlabelValues := []string{i.Name()}\n\n\tif metrics.ff.EnableReceiverNamesInMetrics() {\n\t\tlabelValues = append(labelValues, i.receiverName)\n\t}\n\n\treturn &RetryStage{\n\t\tintegration: i,\n\t\tgroupName:   groupName,\n\t\tmetrics:     metrics,\n\t\tlabelValues: labelValues,\n\t}\n}\n\nfunc (r RetryStage) Exec(ctx context.Context, l *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {\n\tr.metrics.numNotifications.WithLabelValues(r.labelValues...).Inc()\n\n\tctx, span := tracer.Start(ctx, \"notify.RetryStage.Exec\",\n\t\ttrace.WithAttributes(attribute.String(\"alerting.group.name\", r.groupName)),\n\t\ttrace.WithAttributes(attribute.String(\"alerting.integration.name\", r.integration.name)),\n\t\ttrace.WithAttributes(attribute.StringSlice(\"alerting.label.values\", r.labelValues)),\n\t\ttrace.WithAttributes(attribute.Int(\"alerting.alerts.count\", len(alerts))),\n\t\ttrace.WithSpanKind(trace.SpanKindInternal),\n\t)\n\tdefer span.End()\n\n\tctx, alerts, err := r.exec(ctx, l, alerts...)\n\n\tfailureReason := DefaultReason.String()\n\tif err != nil {\n\t\tspan.SetStatus(codes.Error, err.Error())\n\t\tspan.RecordError(err)\n\n\t\tvar e *ErrorWithReason\n\t\tif errors.As(err, &e) {\n\t\t\tfailureReason = e.Reason.String()\n\t\t}\n\t\tr.metrics.numTotalFailedNotifications.WithLabelValues(append(r.labelValues, failureReason)...).Inc()\n\t}\n\treturn ctx, alerts, err\n}\n\nfunc (r RetryStage) exec(ctx context.Context, l *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {\n\tvar sent []*types.Alert\n\n\t// If we shouldn't send notifications for resolved alerts, but there are only\n\t// resolved alerts, report them all as successfully notified (we still want the\n\t// notification log to log them for the next run of DedupStage).\n\tif !r.integration.SendResolved() {\n\t\tfiring, ok := FiringAlerts(ctx)\n\t\tif !ok {\n\t\t\treturn ctx, nil, errors.New(\"firing alerts missing\")\n\t\t}\n\t\tif len(firing) == 0 {\n\t\t\treturn ctx, alerts, nil\n\t\t}\n\t\tfor _, a := range alerts {\n\t\t\tif a.Status() != model.AlertResolved {\n\t\t\t\tsent = append(sent, a)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tsent = alerts\n\t}\n\n\tb := backoff.NewExponentialBackOff()\n\tb.MaxElapsedTime = 0 // Always retry.\n\n\ttick := backoff.NewTicker(b)\n\tdefer tick.Stop()\n\n\tvar (\n\t\ti    = 0\n\t\tiErr error\n\t)\n\n\tl = l.With(\"receiver\", r.groupName, \"integration\", r.integration.String())\n\tif groupKey, ok := GroupKey(ctx); ok {\n\t\tl = l.With(\"aggrGroup\", groupKey)\n\t}\n\n\tfor {\n\n\t\t// Always check the context first to not notify again.\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tif iErr == nil {\n\t\t\t\tiErr = ctx.Err()\n\t\t\t\tif errors.Is(iErr, context.Canceled) {\n\t\t\t\t\tiErr = NewErrorWithReason(ContextCanceledReason, iErr)\n\t\t\t\t} else if errors.Is(iErr, context.DeadlineExceeded) {\n\t\t\t\t\tiErr = NewErrorWithReason(ContextDeadlineExceededReason, iErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif iErr != nil {\n\t\t\t\treturn ctx, nil, fmt.Errorf(\"%s/%s: notify retry canceled after %d attempts: %w\", r.groupName, r.integration.String(), i, iErr)\n\t\t\t}\n\t\t\treturn ctx, nil, nil\n\t\tdefault:\n\t\t}\n\n\t\tselect {\n\t\tcase <-tick.C:\n\t\t\tnow := time.Now()\n\t\t\tretry, err := r.integration.Notify(ctx, sent...)\n\t\t\ti++\n\t\t\tdur := time.Since(now)\n\t\t\tr.metrics.notificationLatencySeconds.WithLabelValues(r.labelValues...).Observe(dur.Seconds())\n\t\t\tr.metrics.numNotificationRequestsTotal.WithLabelValues(r.labelValues...).Inc()\n\t\t\tif err != nil {\n\t\t\t\tr.metrics.numNotificationRequestsFailedTotal.WithLabelValues(r.labelValues...).Inc()\n\t\t\t\tif !retry {\n\t\t\t\t\treturn ctx, alerts, fmt.Errorf(\"%s/%s: notify retry canceled due to unrecoverable error after %d attempts: %w\", r.groupName, r.integration.String(), i, err)\n\t\t\t\t}\n\t\t\t\tif ctx.Err() == nil {\n\t\t\t\t\tif iErr == nil || err.Error() != iErr.Error() {\n\t\t\t\t\t\t// Log the error if the context isn't done and the error isn't the same as before.\n\t\t\t\t\t\tl.Warn(\"Notify attempt failed, will retry later\", \"attempts\", i, \"err\", err)\n\t\t\t\t\t}\n\t\t\t\t\t// Save this error to be able to return the last seen error by an\n\t\t\t\t\t// integration upon context timeout.\n\t\t\t\t\tiErr = err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tl := l.With(\"attempts\", i, \"duration\", dur)\n\t\t\t\tif i <= 1 {\n\t\t\t\t\tl = l.With(\"alerts\", fmt.Sprintf(\"%v\", alerts))\n\t\t\t\t\tl.Debug(\"Notify success\")\n\t\t\t\t} else {\n\t\t\t\t\tl.Info(\"Notify success\")\n\t\t\t\t}\n\n\t\t\t\treturn ctx, alerts, nil\n\t\t\t}\n\t\tcase <-ctx.Done():\n\t\t}\n\t}\n}\n\n// SetNotifiesStage sets the notification information about passed alerts. The\n// passed alerts should have already been sent to the receivers.\ntype SetNotifiesStage struct {\n\tnflog NotificationLog\n\trecv  *nflogpb.Receiver\n}\n\n// NewSetNotifiesStage returns a new instance of a SetNotifiesStage.\nfunc NewSetNotifiesStage(l NotificationLog, recv *nflogpb.Receiver) *SetNotifiesStage {\n\treturn &SetNotifiesStage{\n\t\tnflog: l,\n\t\trecv:  recv,\n\t}\n}\n\n// Exec implements the Stage interface.\nfunc (n SetNotifiesStage) Exec(ctx context.Context, l *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {\n\tgkey, ok := GroupKey(ctx)\n\tif !ok {\n\t\treturn ctx, nil, errors.New(\"group key missing\")\n\t}\n\n\tctx, span := tracer.Start(ctx, \"notify.SetNotifiesStage.Exec\",\n\t\ttrace.WithAttributes(attribute.String(\"alerting.group.key\", gkey)),\n\t\ttrace.WithAttributes(attribute.Int(\"alerting.alerts.count\", len(alerts))),\n\t\ttrace.WithSpanKind(trace.SpanKindInternal),\n\t)\n\tdefer span.End()\n\n\tfiring, ok := FiringAlerts(ctx)\n\tif !ok {\n\t\treturn ctx, nil, errors.New(\"firing alerts missing\")\n\t}\n\n\tresolved, ok := ResolvedAlerts(ctx)\n\tif !ok {\n\t\treturn ctx, nil, errors.New(\"resolved alerts missing\")\n\t}\n\n\trepeat, ok := RepeatInterval(ctx)\n\tif !ok {\n\t\treturn ctx, nil, errors.New(\"repeat interval missing\")\n\t}\n\texpiry := 2 * repeat\n\n\tspan.SetAttributes(\n\t\tattribute.Int(\"alerting.alerts.firing.count\", len(firing)),\n\t\tattribute.Int(\"alerting.alerts.resolved.count\", len(resolved)),\n\t)\n\n\t// Extract receiver data from context if present (it's ok for it to be nil).\n\tstore, _ := NflogStore(ctx)\n\treturn ctx, alerts, n.nflog.Log(n.recv, gkey, firing, resolved, store, expiry)\n}\n"
  },
  {
    "path": "notify/notify_test.go",
    "content": "// Copyright 2015 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage notify\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\tprom_testutil \"github.com/prometheus/client_golang/prometheus/testutil\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/prometheus/alertmanager/featurecontrol\"\n\t\"github.com/prometheus/alertmanager/nflog\"\n\t\"github.com/prometheus/alertmanager/nflog/nflogpb\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\ntype sendResolved bool\n\nfunc (s sendResolved) SendResolved() bool {\n\treturn bool(s)\n}\n\ntype notifierFunc func(ctx context.Context, alerts ...*types.Alert) (bool, error)\n\nfunc (f notifierFunc) Notify(ctx context.Context, alerts ...*types.Alert) (bool, error) {\n\treturn f(ctx, alerts...)\n}\n\ntype failStage struct{}\n\nfunc (s failStage) Exec(ctx context.Context, l *slog.Logger, as ...*types.Alert) (context.Context, []*types.Alert, error) {\n\treturn ctx, nil, fmt.Errorf(\"some error\")\n}\n\ntype testNflog struct {\n\tqres []*nflogpb.Entry\n\tqerr error\n\n\tlogFunc func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error\n}\n\nfunc (l *testNflog) Query(p ...nflog.QueryParam) ([]*nflogpb.Entry, error) {\n\treturn l.qres, l.qerr\n}\n\nfunc (l *testNflog) Log(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error {\n\treturn l.logFunc(r, gkey, firingAlerts, resolvedAlerts, receiverData, expiry)\n}\n\nfunc (l *testNflog) GC() (int, error) {\n\treturn 0, nil\n}\n\nfunc (l *testNflog) Snapshot(w io.Writer) (int, error) {\n\treturn 0, nil\n}\n\nfunc alertHashSet(hashes ...uint64) map[uint64]struct{} {\n\tres := map[uint64]struct{}{}\n\n\tfor _, h := range hashes {\n\t\tres[h] = struct{}{}\n\t}\n\n\treturn res\n}\n\nfunc TestDedupStageNeedsUpdate(t *testing.T) {\n\tnow := utcNow()\n\n\tcases := []struct {\n\t\tentry          *nflogpb.Entry\n\t\tfiringAlerts   map[uint64]struct{}\n\t\tresolvedAlerts map[uint64]struct{}\n\t\trepeat         time.Duration\n\t\tresolve        bool\n\n\t\tres bool\n\t}{\n\t\t{\n\t\t\t// No matching nflog entry should update.\n\t\t\tentry:        nil,\n\t\t\tfiringAlerts: alertHashSet(2, 3, 4),\n\t\t\tres:          true,\n\t\t}, {\n\t\t\t// No matching nflog entry shouldn't update if no alert fires.\n\t\t\tentry:          nil,\n\t\t\tresolvedAlerts: alertHashSet(2, 3, 4),\n\t\t\tres:            false,\n\t\t}, {\n\t\t\t// Different sets of firing alerts should update.\n\t\t\tentry:        &nflogpb.Entry{FiringAlerts: []uint64{1, 2, 3}},\n\t\t\tfiringAlerts: alertHashSet(2, 3, 4),\n\t\t\tres:          true,\n\t\t}, {\n\t\t\t// Zero timestamp in the nflog entry should always update.\n\t\t\tentry: &nflogpb.Entry{\n\t\t\t\tFiringAlerts: []uint64{1, 2, 3},\n\t\t\t\tTimestamp:    &timestamppb.Timestamp{},\n\t\t\t},\n\t\t\tfiringAlerts: alertHashSet(1, 2, 3),\n\t\t\tres:          true,\n\t\t}, {\n\t\t\t// Identical sets of alerts shouldn't update before repeat_interval.\n\t\t\tentry: &nflogpb.Entry{\n\t\t\t\tFiringAlerts: []uint64{1, 2, 3},\n\t\t\t\tTimestamp:    timestamppb.New(now.Add(-9 * time.Minute)),\n\t\t\t},\n\t\t\trepeat:       10 * time.Minute,\n\t\t\tfiringAlerts: alertHashSet(1, 2, 3),\n\t\t\tres:          false,\n\t\t}, {\n\t\t\t// Identical sets of alerts should update after repeat_interval.\n\t\t\tentry: &nflogpb.Entry{\n\t\t\t\tFiringAlerts: []uint64{1, 2, 3},\n\t\t\t\tTimestamp:    timestamppb.New(now.Add(-11 * time.Minute)),\n\t\t\t},\n\t\t\trepeat:       10 * time.Minute,\n\t\t\tfiringAlerts: alertHashSet(1, 2, 3),\n\t\t\tres:          true,\n\t\t}, {\n\t\t\t// Different sets of resolved alerts without firing alerts shouldn't update after repeat_interval.\n\t\t\tentry: &nflogpb.Entry{\n\t\t\t\tResolvedAlerts: []uint64{1, 2, 3},\n\t\t\t\tTimestamp:      timestamppb.New(now.Add(-11 * time.Minute)),\n\t\t\t},\n\t\t\trepeat:         10 * time.Minute,\n\t\t\tresolvedAlerts: alertHashSet(3, 4, 5),\n\t\t\tresolve:        true,\n\t\t\tres:            false,\n\t\t}, {\n\t\t\t// Different sets of resolved alerts shouldn't update when resolve is false.\n\t\t\tentry: &nflogpb.Entry{\n\t\t\t\tFiringAlerts:   []uint64{1, 2},\n\t\t\t\tResolvedAlerts: []uint64{3},\n\t\t\t\tTimestamp:      timestamppb.New(now.Add(-9 * time.Minute)),\n\t\t\t},\n\t\t\trepeat:         10 * time.Minute,\n\t\t\tfiringAlerts:   alertHashSet(1),\n\t\t\tresolvedAlerts: alertHashSet(2, 3),\n\t\t\tresolve:        false,\n\t\t\tres:            false,\n\t\t}, {\n\t\t\t// Different sets of resolved alerts should update when resolve is true.\n\t\t\tentry: &nflogpb.Entry{\n\t\t\t\tFiringAlerts:   []uint64{1, 2},\n\t\t\t\tResolvedAlerts: []uint64{3},\n\t\t\t\tTimestamp:      timestamppb.New(now.Add(-9 * time.Minute)),\n\t\t\t},\n\t\t\trepeat:         10 * time.Minute,\n\t\t\tfiringAlerts:   alertHashSet(1),\n\t\t\tresolvedAlerts: alertHashSet(2, 3),\n\t\t\tresolve:        true,\n\t\t\tres:            true,\n\t\t}, {\n\t\t\t// Empty set of firing alerts should update when resolve is false.\n\t\t\tentry: &nflogpb.Entry{\n\t\t\t\tFiringAlerts:   []uint64{1, 2},\n\t\t\t\tResolvedAlerts: []uint64{3},\n\t\t\t\tTimestamp:      timestamppb.New(now.Add(-9 * time.Minute)),\n\t\t\t},\n\t\t\trepeat:         10 * time.Minute,\n\t\t\tfiringAlerts:   alertHashSet(),\n\t\t\tresolvedAlerts: alertHashSet(1, 2, 3),\n\t\t\tresolve:        false,\n\t\t\tres:            true,\n\t\t}, {\n\t\t\t// Empty set of firing alerts should update when resolve is true.\n\t\t\tentry: &nflogpb.Entry{\n\t\t\t\tFiringAlerts:   []uint64{1, 2},\n\t\t\t\tResolvedAlerts: []uint64{3},\n\t\t\t\tTimestamp:      timestamppb.New(now.Add(-9 * time.Minute)),\n\t\t\t},\n\t\t\trepeat:         10 * time.Minute,\n\t\t\tfiringAlerts:   alertHashSet(),\n\t\t\tresolvedAlerts: alertHashSet(1, 2, 3),\n\t\t\tresolve:        true,\n\t\t\tres:            true,\n\t\t},\n\t}\n\tfor i, c := range cases {\n\t\tt.Log(\"case\", i)\n\n\t\ts := &DedupStage{\n\t\t\tnow: func() time.Time { return now },\n\t\t\trs:  sendResolved(c.resolve),\n\t\t}\n\t\tres := s.needsUpdate(c.entry, c.firingAlerts, c.resolvedAlerts, c.repeat, now).shouldNotify()\n\t\trequire.Equal(t, c.res, res)\n\t}\n}\n\nfunc TestDedupStageUsesContextNow(t *testing.T) {\n\tbase := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)\n\ts := &DedupStage{\n\t\thash: func(*types.Alert) uint64 { return 1 },\n\t\tnow: func() time.Time {\n\t\t\treturn base.Add(time.Hour)\n\t\t},\n\t\trs: sendResolved(false),\n\t\tnflog: &testNflog{\n\t\t\tqerr: nil,\n\t\t\tqres: []*nflogpb.Entry{{\n\t\t\t\tFiringAlerts: []uint64{1},\n\t\t\t\tTimestamp:    timestamppb.New(base),\n\t\t\t}},\n\t\t},\n\t}\n\n\tctx := context.Background()\n\tctx = WithGroupKey(ctx, \"group\")\n\tctx = WithRepeatInterval(ctx, 30*time.Minute)\n\tctx = WithNow(ctx, base.Add(10*time.Minute))\n\n\talerts := []*types.Alert{{Alert: model.Alert{Labels: model.LabelSet{\"alertname\": \"test\"}}}}\n\n\t_, res, err := s.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\trequire.Empty(t, res)\n}\n\nfunc TestDedupStage(t *testing.T) {\n\ti := 0\n\tnow := utcNow()\n\ts := &DedupStage{\n\t\thash: func(a *types.Alert) uint64 {\n\t\t\tres := uint64(i)\n\t\t\ti++\n\t\t\treturn res\n\t\t},\n\t\tnow: func() time.Time {\n\t\t\treturn now\n\t\t},\n\t\trs: sendResolved(false),\n\t}\n\n\tctx := context.Background()\n\n\t_, _, err := s.Exec(ctx, promslog.NewNopLogger())\n\trequire.EqualError(t, err, \"group key missing\")\n\n\tctx = WithGroupKey(ctx, \"1\")\n\n\t_, _, err = s.Exec(ctx, promslog.NewNopLogger())\n\trequire.EqualError(t, err, \"repeat interval missing\")\n\n\tctx = WithRepeatInterval(ctx, time.Hour)\n\n\talerts := []*types.Alert{{}, {}, {}}\n\n\t// Must catch notification log query errors.\n\ts.nflog = &testNflog{\n\t\tqerr: errors.New(\"bad things\"),\n\t}\n\tctx, _, err = s.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.EqualError(t, err, \"bad things\")\n\n\t// ... but skip ErrNotFound.\n\ts.nflog = &testNflog{\n\t\tqerr: nflog.ErrNotFound,\n\t}\n\tctx, res, err := s.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err, \"unexpected error on not found log entry\")\n\trequire.Equal(t, alerts, res, \"input alerts differ from result alerts\")\n\treason, ok := NotificationReason(ctx)\n\trequire.True(t, ok, \"NotificationReason should be in context\")\n\trequire.Equal(t, ReasonFirstNotification, reason, \"should be first notification\")\n\n\ts.nflog = &testNflog{\n\t\tqerr: nil,\n\t\tqres: []*nflogpb.Entry{\n\t\t\t{FiringAlerts: []uint64{0, 1, 2}},\n\t\t\t{FiringAlerts: []uint64{1, 2, 3}},\n\t\t},\n\t}\n\tctx, _, err = s.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.Contains(t, err.Error(), \"result size\")\n\n\t// Must return no error and no alerts no need to update.\n\ti = 0\n\ts.nflog = &testNflog{\n\t\tqerr: nflog.ErrNotFound,\n\t\tqres: []*nflogpb.Entry{\n\t\t\t{\n\t\t\t\tFiringAlerts: []uint64{0, 1, 2},\n\t\t\t\tTimestamp:    timestamppb.New(now),\n\t\t\t},\n\t\t},\n\t}\n\tctx, res, err = s.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\trequire.Nil(t, res, \"unexpected alerts returned\")\n\treason, ok = NotificationReason(ctx)\n\trequire.True(t, ok, \"NotificationReason should be in context\")\n\trequire.Equal(t, ReasonDoNotNotify, reason, \"should not notify when nothing changed\")\n\n\t// Must return no error and all input alerts on changes.\n\ti = 0\n\ts.nflog = &testNflog{\n\t\tqerr: nil,\n\t\tqres: []*nflogpb.Entry{\n\t\t\t{\n\t\t\t\tFiringAlerts: []uint64{1, 2, 3, 4},\n\t\t\t\tTimestamp:    timestamppb.New(now),\n\t\t\t},\n\t\t},\n\t}\n\tctx, res, err = s.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\trequire.Equal(t, alerts, res, \"unexpected alerts returned\")\n\treason, ok = NotificationReason(ctx)\n\trequire.True(t, ok, \"NotificationReason should be in context\")\n\trequire.Equal(t, ReasonNewAlertsInGroup, reason, \"should notify when alerts change\")\n}\n\nfunc TestMultiStage(t *testing.T) {\n\tvar (\n\t\talerts1 = []*types.Alert{{}}\n\t\talerts2 = []*types.Alert{{}, {}}\n\t\talerts3 = []*types.Alert{{}, {}, {}}\n\t)\n\n\tstage := MultiStage{\n\t\tStageFunc(func(ctx context.Context, l *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {\n\t\t\tif !reflect.DeepEqual(alerts, alerts1) {\n\t\t\t\tt.Fatal(\"Input not equal to input of MultiStage\")\n\t\t\t}\n\t\t\t//nolint:staticcheck // Ignore SA1029\n\t\t\tctx = context.WithValue(ctx, \"key\", \"value\")\n\t\t\treturn ctx, alerts2, nil\n\t\t}),\n\t\tStageFunc(func(ctx context.Context, l *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {\n\t\t\tif !reflect.DeepEqual(alerts, alerts2) {\n\t\t\t\tt.Fatal(\"Input not equal to output of previous stage\")\n\t\t\t}\n\t\t\tv, ok := ctx.Value(\"key\").(string)\n\t\t\tif !ok || v != \"value\" {\n\t\t\t\tt.Fatalf(\"Expected value %q for key %q but got %q\", \"value\", \"key\", v)\n\t\t\t}\n\t\t\treturn ctx, alerts3, nil\n\t\t}),\n\t}\n\n\t_, alerts, err := stage.Exec(context.Background(), promslog.NewNopLogger(), alerts1...)\n\tif err != nil {\n\t\tt.Fatalf(\"Exec failed: %s\", err)\n\t}\n\n\tif !reflect.DeepEqual(alerts, alerts3) {\n\t\tt.Fatal(\"Output of MultiStage is not equal to the output of the last stage\")\n\t}\n}\n\nfunc TestMultiStageFailure(t *testing.T) {\n\tvar (\n\t\tctx   = context.Background()\n\t\ts1    = failStage{}\n\t\tstage = MultiStage{s1}\n\t)\n\n\t_, _, err := stage.Exec(ctx, promslog.NewNopLogger(), nil)\n\tif err.Error() != \"some error\" {\n\t\tt.Fatal(\"Errors were not propagated correctly by MultiStage\")\n\t}\n}\n\nfunc TestRoutingStage(t *testing.T) {\n\tvar (\n\t\talerts1 = []*types.Alert{{}}\n\t\talerts2 = []*types.Alert{{}, {}}\n\t)\n\n\tstage := RoutingStage{\n\t\t\"name\": StageFunc(func(ctx context.Context, l *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {\n\t\t\tif !reflect.DeepEqual(alerts, alerts1) {\n\t\t\t\tt.Fatal(\"Input not equal to input of RoutingStage\")\n\t\t\t}\n\t\t\treturn ctx, alerts2, nil\n\t\t}),\n\t\t\"not\": failStage{},\n\t}\n\n\tctx := WithReceiverName(context.Background(), \"name\")\n\n\t_, alerts, err := stage.Exec(ctx, promslog.NewNopLogger(), alerts1...)\n\tif err != nil {\n\t\tt.Fatalf(\"Exec failed: %s\", err)\n\t}\n\n\tif !reflect.DeepEqual(alerts, alerts2) {\n\t\tt.Fatal(\"Output of RoutingStage is not equal to the output of the inner stage\")\n\t}\n}\n\nfunc TestRetryStageWithError(t *testing.T) {\n\tfail, retry := true, true\n\tsent := []*types.Alert{}\n\ti := Integration{\n\t\tnotifier: notifierFunc(func(ctx context.Context, alerts ...*types.Alert) (bool, error) {\n\t\t\tif fail {\n\t\t\t\tfail = false\n\t\t\t\treturn retry, errors.New(\"fail to deliver notification\")\n\t\t\t}\n\t\t\tsent = append(sent, alerts...)\n\t\t\treturn false, nil\n\t\t}),\n\t\trs: sendResolved(false),\n\t}\n\tr := NewRetryStage(i, \"\", NewMetrics(prometheus.NewRegistry(), featurecontrol.NoopFlags{}))\n\n\talerts := []*types.Alert{\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tEndsAt: time.Now().Add(time.Hour),\n\t\t\t},\n\t\t},\n\t}\n\n\tctx := context.Background()\n\tctx = WithFiringAlerts(ctx, []uint64{0})\n\n\t// Notify with a recoverable error should retry and succeed.\n\tresctx, res, err := r.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\trequire.Equal(t, alerts, res)\n\trequire.Equal(t, alerts, sent)\n\trequire.NotNil(t, resctx)\n\n\t// Notify with an unrecoverable error should fail.\n\tsent = sent[:0]\n\tfail = true\n\tretry = false\n\tresctx, _, err = r.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.Error(t, err)\n\trequire.NotNil(t, resctx)\n}\n\nfunc TestRetryStageWithErrorCode(t *testing.T) {\n\ttestcases := map[string]struct {\n\t\tisNewErrorWithReason bool\n\t\treason               Reason\n\t\treasonlabel          string\n\t\texpectedCount        int\n\t}{\n\t\t\"for clientError\":     {isNewErrorWithReason: true, reason: ClientErrorReason, reasonlabel: ClientErrorReason.String(), expectedCount: 1},\n\t\t\"for serverError\":     {isNewErrorWithReason: true, reason: ServerErrorReason, reasonlabel: ServerErrorReason.String(), expectedCount: 1},\n\t\t\"for unexpected code\": {isNewErrorWithReason: false, reason: DefaultReason, reasonlabel: DefaultReason.String(), expectedCount: 1},\n\t}\n\tfor _, testData := range testcases {\n\t\tretry := false\n\t\ttestData := testData\n\t\ti := Integration{\n\t\t\tname: \"test\",\n\t\t\tnotifier: notifierFunc(func(ctx context.Context, alerts ...*types.Alert) (bool, error) {\n\t\t\t\tif !testData.isNewErrorWithReason {\n\t\t\t\t\treturn retry, errors.New(\"fail to deliver notification\")\n\t\t\t\t}\n\t\t\t\treturn retry, NewErrorWithReason(testData.reason, errors.New(\"fail to deliver notification\"))\n\t\t\t}),\n\t\t\trs: sendResolved(false),\n\t\t}\n\t\tr := NewRetryStage(i, \"\", NewMetrics(prometheus.NewRegistry(), featurecontrol.NoopFlags{}))\n\n\t\talerts := []*types.Alert{\n\t\t\t{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tEndsAt: time.Now().Add(time.Hour),\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tctx := context.Background()\n\t\tctx = WithFiringAlerts(ctx, []uint64{0})\n\n\t\t// Notify with a non-recoverable error.\n\t\tresctx, _, err := r.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\t\tcounter := r.metrics.numTotalFailedNotifications\n\n\t\trequire.Equal(t, testData.expectedCount, int(prom_testutil.ToFloat64(counter.WithLabelValues(r.integration.Name(), testData.reasonlabel))))\n\n\t\trequire.Error(t, err)\n\t\trequire.NotNil(t, resctx)\n\t}\n}\n\nfunc TestRetryStageWithContextCanceled(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\n\ti := Integration{\n\t\tname: \"test\",\n\t\tnotifier: notifierFunc(func(ctx context.Context, alerts ...*types.Alert) (bool, error) {\n\t\t\tcancel()\n\t\t\treturn true, errors.New(\"request failed: context canceled\")\n\t\t}),\n\t\trs: sendResolved(false),\n\t}\n\tr := NewRetryStage(i, \"\", NewMetrics(prometheus.NewRegistry(), featurecontrol.NoopFlags{}))\n\n\talerts := []*types.Alert{\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tEndsAt: time.Now().Add(time.Hour),\n\t\t\t},\n\t\t},\n\t}\n\n\tctx = WithFiringAlerts(ctx, []uint64{0})\n\n\t// Notify with a non-recoverable error.\n\tresctx, _, err := r.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\tcounter := r.metrics.numTotalFailedNotifications\n\n\trequire.Equal(t, 1, int(prom_testutil.ToFloat64(counter.WithLabelValues(r.integration.Name(), ContextCanceledReason.String()))))\n\trequire.Contains(t, err.Error(), \"notify retry canceled after 1 attempts: context canceled\")\n\n\trequire.Error(t, err)\n\trequire.NotNil(t, resctx)\n}\n\nfunc TestRetryStageNoResolved(t *testing.T) {\n\tsent := []*types.Alert{}\n\ti := Integration{\n\t\tnotifier: notifierFunc(func(ctx context.Context, alerts ...*types.Alert) (bool, error) {\n\t\t\tsent = append(sent, alerts...)\n\t\t\treturn false, nil\n\t\t}),\n\t\trs: sendResolved(false),\n\t}\n\tr := NewRetryStage(i, \"\", NewMetrics(prometheus.NewRegistry(), featurecontrol.NoopFlags{}))\n\n\talerts := []*types.Alert{\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tEndsAt: time.Now().Add(-time.Hour),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tEndsAt: time.Now().Add(time.Hour),\n\t\t\t},\n\t\t},\n\t}\n\n\tctx := context.Background()\n\n\tresctx, res, err := r.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.EqualError(t, err, \"firing alerts missing\")\n\trequire.Nil(t, res)\n\trequire.NotNil(t, resctx)\n\n\tctx = WithFiringAlerts(ctx, []uint64{0})\n\n\tresctx, res, err = r.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\trequire.Equal(t, alerts, res)\n\trequire.Equal(t, []*types.Alert{alerts[1]}, sent)\n\trequire.NotNil(t, resctx)\n\n\t// All alerts are resolved.\n\tsent = sent[:0]\n\tctx = WithFiringAlerts(ctx, []uint64{})\n\talerts[1].EndsAt = time.Now().Add(-time.Hour)\n\n\tresctx, res, err = r.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\trequire.Equal(t, alerts, res)\n\trequire.Equal(t, []*types.Alert{}, sent)\n\trequire.NotNil(t, resctx)\n}\n\nfunc TestRetryStageSendResolved(t *testing.T) {\n\tsent := []*types.Alert{}\n\ti := Integration{\n\t\tnotifier: notifierFunc(func(ctx context.Context, alerts ...*types.Alert) (bool, error) {\n\t\t\tsent = append(sent, alerts...)\n\t\t\treturn false, nil\n\t\t}),\n\t\trs: sendResolved(true),\n\t}\n\tr := NewRetryStage(i, \"\", NewMetrics(prometheus.NewRegistry(), featurecontrol.NoopFlags{}))\n\n\talerts := []*types.Alert{\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tEndsAt: time.Now().Add(-time.Hour),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tEndsAt: time.Now().Add(time.Hour),\n\t\t\t},\n\t\t},\n\t}\n\n\tctx := context.Background()\n\tctx = WithFiringAlerts(ctx, []uint64{0})\n\n\tresctx, res, err := r.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\trequire.Equal(t, alerts, res)\n\trequire.Equal(t, alerts, sent)\n\trequire.NotNil(t, resctx)\n\n\t// All alerts are resolved.\n\tsent = sent[:0]\n\tctx = WithFiringAlerts(ctx, []uint64{})\n\talerts[1].EndsAt = time.Now().Add(-time.Hour)\n\n\tresctx, res, err = r.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\trequire.Equal(t, alerts, res)\n\trequire.Equal(t, alerts, sent)\n\trequire.NotNil(t, resctx)\n}\n\nfunc TestSetNotifiesStage(t *testing.T) {\n\ttnflog := &testNflog{}\n\ts := &SetNotifiesStage{\n\t\trecv:  &nflogpb.Receiver{GroupName: \"test\"},\n\t\tnflog: tnflog,\n\t}\n\talerts := []*types.Alert{{}, {}, {}}\n\tctx := context.Background()\n\n\tresctx, res, err := s.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.EqualError(t, err, \"group key missing\")\n\trequire.Nil(t, res)\n\trequire.NotNil(t, resctx)\n\n\tctx = WithGroupKey(ctx, \"1\")\n\n\tresctx, res, err = s.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.EqualError(t, err, \"firing alerts missing\")\n\trequire.Nil(t, res)\n\trequire.NotNil(t, resctx)\n\n\tctx = WithFiringAlerts(ctx, []uint64{0, 1, 2})\n\n\tresctx, res, err = s.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.EqualError(t, err, \"resolved alerts missing\")\n\trequire.Nil(t, res)\n\trequire.NotNil(t, resctx)\n\n\tctx = WithResolvedAlerts(ctx, []uint64{})\n\tctx = WithRepeatInterval(ctx, time.Hour)\n\n\ttnflog.logFunc = func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error {\n\t\trequire.Equal(t, s.recv, r)\n\t\trequire.Equal(t, \"1\", gkey)\n\t\trequire.Equal(t, []uint64{0, 1, 2}, firingAlerts)\n\t\trequire.Equal(t, []uint64{}, resolvedAlerts)\n\t\trequire.Equal(t, 2*time.Hour, expiry)\n\t\treturn nil\n\t}\n\tresctx, res, err = s.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\trequire.Equal(t, alerts, res)\n\trequire.NotNil(t, resctx)\n\n\tctx = WithFiringAlerts(ctx, []uint64{})\n\tctx = WithResolvedAlerts(ctx, []uint64{0, 1, 2})\n\n\ttnflog.logFunc = func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error {\n\t\trequire.Equal(t, s.recv, r)\n\t\trequire.Equal(t, \"1\", gkey)\n\t\trequire.Equal(t, []uint64{}, firingAlerts)\n\t\trequire.Equal(t, []uint64{0, 1, 2}, resolvedAlerts)\n\t\trequire.Equal(t, 2*time.Hour, expiry)\n\t\treturn nil\n\t}\n\tresctx, res, err = s.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\trequire.Equal(t, alerts, res)\n\trequire.NotNil(t, resctx)\n}\n\nfunc TestReceiverData_PreservationWhenNotifierDoesNotUpdate(t *testing.T) {\n\tvar storedData *nflog.Store\n\tcallCount := 0\n\n\ttnflog := &testNflog{\n\t\tlogFunc: func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error {\n\t\t\tstoredData = receiverData\n\t\t\treturn nil\n\t\t},\n\t}\n\n\ttnflog.qres = []*nflogpb.Entry{}\n\n\trecv := &nflogpb.Receiver{GroupName: \"test\"}\n\tdedupStage := NewDedupStage(sendResolved(true), tnflog, recv)\n\n\tnotifier := notifierFunc(func(ctx context.Context, alerts ...*types.Alert) (bool, error) {\n\t\tcallCount++\n\n\t\tif callCount == 1 {\n\t\t\t// First call - store some data\n\t\t\tif store, ok := NflogStore(ctx); ok {\n\t\t\t\tstore.SetStr(\"threadTs\", \"1234.5678\")\n\t\t\t}\n\t\t\treturn false, nil\n\t\t}\n\t\t// Second call - notifier doesn't update ReceiverData\n\t\t// Does NOT call StoreStr - just returns success\n\t\treturn false, nil\n\t})\n\n\tintegration := NewIntegration(notifier, sendResolved(true), \"test\", 0, \"test-receiver\")\n\tretryStage := NewRetryStage(integration, \"test\", NewMetrics(prometheus.NewRegistry(), featurecontrol.NoopFlags{}))\n\tsetNotifiesStage := NewSetNotifiesStage(tnflog, recv)\n\n\tctx := context.Background()\n\tctx = WithGroupKey(ctx, \"testkey\")\n\tctx = WithRepeatInterval(ctx, time.Hour)\n\n\talerts := []*types.Alert{\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\"alertname\": \"test\"},\n\t\t\t},\n\t\t},\n\t}\n\n\t// First notification\n\tctx, _, err := dedupStage.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\n\tctx, _, err = retryStage.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\n\t_, _, err = setNotifiesStage.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\n\t// Verify first notification stored data\n\trequire.NotNil(t, storedData)\n\tthreadTs, found := storedData.GetStr(\"threadTs\")\n\trequire.True(t, found, \"threadTs should be in stored data\")\n\trequire.Equal(t, \"1234.5678\", threadTs)\n\n\tfirstReceiverData := map[string]*nflogpb.ReceiverDataValue{\n\t\t\"threadTs\": {\n\t\t\tValue: &nflogpb.ReceiverDataValue_StrVal{StrVal: \"1234.5678\"},\n\t\t},\n\t}\n\n\t// Second notification - load previous state\n\ttnflog.qres = []*nflogpb.Entry{\n\t\t{\n\t\t\tReceiver:       recv,\n\t\t\tGroupKey:       []byte(\"testkey\"),\n\t\t\tFiringAlerts:   []uint64{1},\n\t\t\tResolvedAlerts: []uint64{},\n\t\t\tReceiverData:   firstReceiverData,\n\t\t},\n\t}\n\n\tctx = context.Background()\n\tctx = WithGroupKey(ctx, \"testkey\")\n\tctx = WithRepeatInterval(ctx, time.Hour)\n\n\tctx, _, err = dedupStage.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\n\tctx, _, err = retryStage.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\n\t_, _, err = setNotifiesStage.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\n\tif storedData == nil {\n\t\tt.Error(\"ReceiverData was lost! Second notification has nil data\")\n\t} else {\n\t\tif threadTs, exists := storedData.GetStr(\"threadTs\"); !exists {\n\t\t\tt.Error(\"ReceiverData 'threadTs' was lost! Second notification doesn't have it\")\n\t\t} else {\n\t\t\tt.Logf(\"threadTs value: %s\", threadTs)\n\t\t}\n\t}\n}\n\nfunc TestDedupStageExtractsReceiverData_DataPresent(t *testing.T) {\n\treceiverData := map[string]*nflogpb.ReceiverDataValue{\n\t\t\"threadTs\": {\n\t\t\tValue: &nflogpb.ReceiverDataValue_StrVal{StrVal: \"1234.5678\"},\n\t\t},\n\t\t\"counter\": {\n\t\t\tValue: &nflogpb.ReceiverDataValue_IntVal{IntVal: 42},\n\t\t},\n\t}\n\n\tentry := &nflogpb.Entry{\n\t\tReceiver:     &nflogpb.Receiver{GroupName: \"test\"},\n\t\tGroupKey:     []byte(\"key\"),\n\t\tFiringAlerts: []uint64{1, 2, 3},\n\t\tReceiverData: receiverData,\n\t}\n\n\ttnflog := &testNflog{\n\t\tqres: []*nflogpb.Entry{entry},\n\t}\n\n\tstage := NewDedupStage(sendResolved(false), tnflog, &nflogpb.Receiver{GroupName: \"test\"})\n\n\tctx := context.Background()\n\tctx = WithGroupKey(ctx, \"key\")\n\tctx = WithRepeatInterval(ctx, time.Hour)\n\n\talerts := []*types.Alert{\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\"alertname\": \"test\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tresCtx, _, err := stage.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\n\tstore, ok := NflogStore(resCtx)\n\trequire.True(t, ok, \"NflogStore should be in context\")\n\trequire.NotNil(t, store)\n\n\tthreadTs, found := store.GetStr(\"threadTs\")\n\trequire.True(t, found)\n\trequire.Equal(t, \"1234.5678\", threadTs)\n\n\tcounter, found := store.GetInt(\"counter\")\n\trequire.True(t, found)\n\trequire.Equal(t, int64(42), counter)\n}\n\nfunc TestDedupStageExtractsReceiverData_NilReceiverData(t *testing.T) {\n\tentry := &nflogpb.Entry{\n\t\tReceiver:     &nflogpb.Receiver{GroupName: \"test\"},\n\t\tGroupKey:     []byte(\"key\"),\n\t\tFiringAlerts: []uint64{1, 2, 3},\n\t\tReceiverData: nil,\n\t}\n\n\ttnflog := &testNflog{\n\t\tqres: []*nflogpb.Entry{entry},\n\t}\n\n\tstage := NewDedupStage(sendResolved(false), tnflog, &nflogpb.Receiver{GroupName: \"test\"})\n\n\tctx := context.Background()\n\tctx = WithGroupKey(ctx, \"key\")\n\tctx = WithRepeatInterval(ctx, time.Hour)\n\n\talerts := []*types.Alert{\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\"alertname\": \"test\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tresCtx, _, err := stage.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\n\tstore, ok := NflogStore(resCtx)\n\trequire.True(t, ok, \"NflogStore should be in context even when ReceiverData is nil\")\n\trequire.NotNil(t, store)\n}\n\nfunc TestDedupStageExtractsReceiverData_NoEntry(t *testing.T) {\n\ttnflog := &testNflog{\n\t\tqres: []*nflogpb.Entry{},\n\t}\n\n\tstage := NewDedupStage(sendResolved(false), tnflog, &nflogpb.Receiver{GroupName: \"test\"})\n\n\tctx := context.Background()\n\tctx = WithGroupKey(ctx, \"key\")\n\tctx = WithRepeatInterval(ctx, time.Hour)\n\n\talerts := []*types.Alert{\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\"alertname\": \"test\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tresCtx, _, err := stage.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\n\tstore, ok := NflogStore(resCtx)\n\trequire.True(t, ok, \"NflogStore should be in context even when no entry exists\")\n\trequire.NotNil(t, store)\n}\n\nfunc TestNflogStore_NoLeakBetweenNotificationSequences(t *testing.T) {\n\tvar storedData *nflog.Store\n\tcallCount := 0\n\tvar capturedStoreValues []map[string]string\n\n\ttnflog := &testNflog{\n\t\tlogFunc: func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64, receiverData *nflog.Store, expiry time.Duration) error {\n\t\t\tstoredData = receiverData\n\t\t\treturn nil\n\t\t},\n\t}\n\n\trecv := &nflogpb.Receiver{GroupName: \"test\"}\n\tdedupStage := NewDedupStage(sendResolved(true), tnflog, recv)\n\n\tnotifier := notifierFunc(func(ctx context.Context, alerts ...*types.Alert) (bool, error) {\n\t\tcallCount++\n\t\tstore, ok := NflogStore(ctx)\n\t\trequire.True(t, ok, \"Store should be available in context\")\n\n\t\tstoreSnapshot := make(map[string]string)\n\t\tif val, found := store.GetStr(\"session_data\"); found {\n\t\t\tstoreSnapshot[\"session_data\"] = val\n\t\t}\n\t\tcapturedStoreValues = append(capturedStoreValues, storeSnapshot)\n\n\t\tstore.SetStr(\"session_data\", fmt.Sprintf(\"session_%d\", callCount))\n\t\treturn false, nil\n\t})\n\n\tintegration := NewIntegration(notifier, sendResolved(true), \"test\", 0, \"test-receiver\")\n\tretryStage := NewRetryStage(integration, \"test\", NewMetrics(prometheus.NewRegistry(), featurecontrol.NoopFlags{}))\n\tsetNotifiesStage := NewSetNotifiesStage(tnflog, recv)\n\n\talerts := []*types.Alert{\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\"alertname\": \"test\"},\n\t\t\t\tEndsAt: time.Now().Add(time.Hour),\n\t\t\t},\n\t\t},\n\t}\n\n\t// Scenario 1: First notification ever (no previous nflog entry)\n\ttnflog.qres = []*nflogpb.Entry{}\n\n\tctx := context.Background()\n\tctx = WithGroupKey(ctx, \"testkey\")\n\tctx = WithRepeatInterval(ctx, time.Hour)\n\n\tctx, _, err := dedupStage.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\n\tctx, _, err = retryStage.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\n\t_, _, err = setNotifiesStage.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, 1, callCount)\n\trequire.Empty(t, capturedStoreValues[0], \"First notification should see empty Store\")\n\n\trequire.NotNil(t, storedData)\n\tsessionData, found := storedData.GetStr(\"session_data\")\n\trequire.True(t, found)\n\trequire.Equal(t, \"session_1\", sessionData)\n\n\t// Scenario 2: Alert resolves, then fires again (new firing sequence)\n\tfirstSessionData := map[string]*nflogpb.ReceiverDataValue{\n\t\t\"session_data\": {\n\t\t\tValue: &nflogpb.ReceiverDataValue_StrVal{StrVal: \"session_1\"},\n\t\t},\n\t}\n\n\ttnflog.qres = []*nflogpb.Entry{\n\t\t{\n\t\t\tReceiver:       recv,\n\t\t\tGroupKey:       []byte(\"testkey\"),\n\t\t\tFiringAlerts:   []uint64{},\n\t\t\tResolvedAlerts: []uint64{1},\n\t\t\tReceiverData:   firstSessionData,\n\t\t},\n\t}\n\n\tctx = context.Background()\n\tctx = WithGroupKey(ctx, \"testkey\")\n\tctx = WithRepeatInterval(ctx, time.Hour)\n\n\tctx, _, err = dedupStage.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\n\tctx, _, err = retryStage.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\n\t_, _, err = setNotifiesStage.Exec(ctx, promslog.NewNopLogger(), alerts...)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, 2, callCount)\n\trequire.Len(t, capturedStoreValues, 2)\n\trequire.Empty(t, capturedStoreValues[1], \"New firing sequence should see empty Store (no leak from resolved entry)\")\n\n\trequire.NotNil(t, storedData)\n\tsessionData, found = storedData.GetStr(\"session_data\")\n\trequire.True(t, found)\n\trequire.Equal(t, \"session_2\", sessionData)\n}\n\nfunc BenchmarkHashAlert(b *testing.B) {\n\talert := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels: model.LabelSet{\"foo\": \"the_first_value\", \"bar\": \"the_second_value\", \"another\": \"value\"},\n\t\t},\n\t}\n\tfor b.Loop() {\n\t\thashAlert(alert)\n\t}\n}\n"
  },
  {
    "path": "notify/opsgenie/api_key_file",
    "content": "my_secret_api_key\n\n"
  },
  {
    "path": "notify/opsgenie/opsgenie.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage opsgenie\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"maps\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// https://docs.opsgenie.com/docs/alert-api - 130 characters meaning runes.\nconst maxMessageLenRunes = 130\n\n// Notifier implements a Notifier for OpsGenie notifications.\ntype Notifier struct {\n\tconf    *config.OpsGenieConfig\n\ttmpl    *template.Template\n\tlogger  *slog.Logger\n\tclient  *http.Client\n\tretrier *notify.Retrier\n}\n\n// New returns a new OpsGenie notifier.\nfunc New(c *config.OpsGenieConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {\n\tclient, err := notify.NewClientWithTracing(*c.HTTPConfig, \"opsgenie\", httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Notifier{\n\t\tconf:    c,\n\t\ttmpl:    t,\n\t\tlogger:  l,\n\t\tclient:  client,\n\t\tretrier: &notify.Retrier{RetryCodes: []int{http.StatusTooManyRequests}},\n\t}, nil\n}\n\ntype opsGenieCreateMessage struct {\n\tAlias       string                           `json:\"alias\"`\n\tMessage     string                           `json:\"message\"`\n\tDescription string                           `json:\"description,omitempty\"`\n\tDetails     map[string]string                `json:\"details\"`\n\tSource      string                           `json:\"source\"`\n\tResponders  []opsGenieCreateMessageResponder `json:\"responders,omitempty\"`\n\tTags        []string                         `json:\"tags,omitempty\"`\n\tNote        string                           `json:\"note,omitempty\"`\n\tPriority    string                           `json:\"priority,omitempty\"`\n\tEntity      string                           `json:\"entity,omitempty\"`\n\tActions     []string                         `json:\"actions,omitempty\"`\n}\n\ntype opsGenieCreateMessageResponder struct {\n\tID       string `json:\"id,omitempty\"`\n\tName     string `json:\"name,omitempty\"`\n\tUsername string `json:\"username,omitempty\"`\n\tType     string `json:\"type\"` // team, user, escalation, schedule etc.\n}\n\ntype opsGenieCloseMessage struct {\n\tSource string `json:\"source\"`\n}\n\ntype opsGenieUpdateMessageMessage struct {\n\tMessage string `json:\"message,omitempty\"`\n}\n\ntype opsGenieUpdateDescriptionMessage struct {\n\tDescription string `json:\"description,omitempty\"`\n}\n\n// Notify implements the Notifier interface.\nfunc (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {\n\trequests, retry, err := n.createRequests(ctx, as...)\n\tif err != nil {\n\t\treturn retry, err\n\t}\n\n\tfor _, req := range requests {\n\t\treq.Header.Set(\"User-Agent\", notify.UserAgentHeader)\n\t\tresp, err := n.client.Do(req)\n\t\tif err != nil {\n\t\t\treturn true, err\n\t\t}\n\t\tshouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)\n\t\tnotify.Drain(resp)\n\t\tif err != nil {\n\t\t\treturn shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)\n\t\t}\n\t}\n\treturn true, nil\n}\n\n// Like Split but filter out empty strings.\nfunc safeSplit(s, sep string) []string {\n\ta := strings.Split(strings.TrimSpace(s), sep)\n\tb := a[:0]\n\tfor _, x := range a {\n\t\tif x != \"\" {\n\t\t\tb = append(b, x)\n\t\t}\n\t}\n\treturn b\n}\n\n// Create requests for a list of alerts.\nfunc (n *Notifier) createRequests(ctx context.Context, as ...*types.Alert) ([]*http.Request, bool, error) {\n\tkey, err := notify.ExtractGroupKey(ctx)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\tlogger := n.logger.With(\"group_key\", key)\n\tlogger.Debug(\"extracted group key\")\n\n\tdata := notify.GetTemplateData(ctx, n.tmpl, as, logger)\n\n\ttmpl := notify.TmplText(n.tmpl, data, &err)\n\n\tdetails := make(map[string]string)\n\n\tmaps.Copy(details, data.CommonLabels)\n\n\tfor k, v := range n.conf.Details {\n\t\tdetails[k] = tmpl(v)\n\t}\n\n\trequests := []*http.Request{}\n\n\tvar (\n\t\talias  = key.Hash()\n\t\talerts = types.Alerts(as...)\n\t)\n\tswitch alerts.Status() {\n\tcase model.AlertResolved:\n\t\tresolvedEndpointURL := n.conf.APIURL.Copy()\n\t\tresolvedEndpointURL.Path += fmt.Sprintf(\"v2/alerts/%s/close\", alias)\n\t\tq := resolvedEndpointURL.Query()\n\t\tq.Set(\"identifierType\", \"alias\")\n\t\tresolvedEndpointURL.RawQuery = q.Encode()\n\t\tmsg := &opsGenieCloseMessage{Source: tmpl(n.conf.Source)}\n\t\tvar buf bytes.Buffer\n\t\tif err := json.NewEncoder(&buf).Encode(msg); err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\t\treq, err := http.NewRequest(\"POST\", resolvedEndpointURL.String(), &buf)\n\t\tif err != nil {\n\t\t\treturn nil, true, err\n\t\t}\n\t\trequests = append(requests, req.WithContext(ctx))\n\tdefault:\n\t\tmessage, truncated := notify.TruncateInRunes(tmpl(n.conf.Message), maxMessageLenRunes)\n\t\tif truncated {\n\t\t\tlogger.Warn(\"Truncated message\", \"alert\", key, \"max_runes\", maxMessageLenRunes)\n\t\t}\n\n\t\tcreateEndpointURL := n.conf.APIURL.Copy()\n\t\tcreateEndpointURL.Path += \"v2/alerts\"\n\n\t\tvar responders []opsGenieCreateMessageResponder\n\t\tfor _, r := range n.conf.Responders {\n\t\t\tresponder := opsGenieCreateMessageResponder{\n\t\t\t\tID:       tmpl(r.ID),\n\t\t\t\tName:     tmpl(r.Name),\n\t\t\t\tUsername: tmpl(r.Username),\n\t\t\t\tType:     tmpl(r.Type),\n\t\t\t}\n\n\t\t\tif responder == (opsGenieCreateMessageResponder{}) {\n\t\t\t\t// Filter out empty responders. This is useful if you want to fill\n\t\t\t\t// responders dynamically from alert's common labels.\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif responder.Type == \"teams\" {\n\t\t\t\tteams := safeSplit(responder.Name, \",\")\n\t\t\t\tfor _, team := range teams {\n\t\t\t\t\tnewResponder := opsGenieCreateMessageResponder{\n\t\t\t\t\t\tName: tmpl(team),\n\t\t\t\t\t\tType: tmpl(\"team\"),\n\t\t\t\t\t}\n\t\t\t\t\tresponders = append(responders, newResponder)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tresponders = append(responders, responder)\n\t\t}\n\n\t\tmsg := &opsGenieCreateMessage{\n\t\t\tAlias:       alias,\n\t\t\tMessage:     message,\n\t\t\tDescription: tmpl(n.conf.Description),\n\t\t\tDetails:     details,\n\t\t\tSource:      tmpl(n.conf.Source),\n\t\t\tResponders:  responders,\n\t\t\tTags:        safeSplit(tmpl(n.conf.Tags), \",\"),\n\t\t\tNote:        tmpl(n.conf.Note),\n\t\t\tPriority:    tmpl(n.conf.Priority),\n\t\t\tEntity:      tmpl(n.conf.Entity),\n\t\t\tActions:     safeSplit(tmpl(n.conf.Actions), \",\"),\n\t\t}\n\t\tvar buf bytes.Buffer\n\t\tif err := json.NewEncoder(&buf).Encode(msg); err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\t\treq, err := http.NewRequest(\"POST\", createEndpointURL.String(), &buf)\n\t\tif err != nil {\n\t\t\treturn nil, true, err\n\t\t}\n\t\trequests = append(requests, req.WithContext(ctx))\n\n\t\tif n.conf.UpdateAlerts {\n\t\t\tupdateMessageEndpointURL := n.conf.APIURL.Copy()\n\t\t\tupdateMessageEndpointURL.Path += fmt.Sprintf(\"v2/alerts/%s/message\", alias)\n\t\t\tq := updateMessageEndpointURL.Query()\n\t\t\tq.Set(\"identifierType\", \"alias\")\n\t\t\tupdateMessageEndpointURL.RawQuery = q.Encode()\n\t\t\tupdateMsgMsg := &opsGenieUpdateMessageMessage{\n\t\t\t\tMessage: msg.Message,\n\t\t\t}\n\t\t\tvar updateMessageBuf bytes.Buffer\n\t\t\tif err := json.NewEncoder(&updateMessageBuf).Encode(updateMsgMsg); err != nil {\n\t\t\t\treturn nil, false, err\n\t\t\t}\n\t\t\treq, err := http.NewRequest(\"PUT\", updateMessageEndpointURL.String(), &updateMessageBuf)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, true, err\n\t\t\t}\n\t\t\trequests = append(requests, req)\n\n\t\t\tupdateDescriptionEndpointURL := n.conf.APIURL.Copy()\n\t\t\tupdateDescriptionEndpointURL.Path += fmt.Sprintf(\"v2/alerts/%s/description\", alias)\n\t\t\tq = updateDescriptionEndpointURL.Query()\n\t\t\tq.Set(\"identifierType\", \"alias\")\n\t\t\tupdateDescriptionEndpointURL.RawQuery = q.Encode()\n\t\t\tupdateDescMsg := &opsGenieUpdateDescriptionMessage{\n\t\t\t\tDescription: msg.Description,\n\t\t\t}\n\n\t\t\tvar updateDescriptionBuf bytes.Buffer\n\t\t\tif err := json.NewEncoder(&updateDescriptionBuf).Encode(updateDescMsg); err != nil {\n\t\t\t\treturn nil, false, err\n\t\t\t}\n\t\t\treq, err = http.NewRequest(\"PUT\", updateDescriptionEndpointURL.String(), &updateDescriptionBuf)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, true, err\n\t\t\t}\n\t\t\trequests = append(requests, req.WithContext(ctx))\n\t\t}\n\t}\n\n\tvar apiKey string\n\tif n.conf.APIKey != \"\" {\n\t\tapiKey = tmpl(string(n.conf.APIKey))\n\t} else {\n\t\tcontent, err := os.ReadFile(n.conf.APIKeyFile)\n\t\tif err != nil {\n\t\t\treturn nil, false, fmt.Errorf(\"read key_file error: %w\", err)\n\t\t}\n\t\tapiKey = tmpl(string(content))\n\t\tapiKey = strings.TrimSpace(string(apiKey))\n\t}\n\n\tif err != nil {\n\t\treturn nil, false, fmt.Errorf(\"templating error: %w\", err)\n\t}\n\n\tfor _, req := range requests {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"GenieKey %s\", apiKey))\n\t}\n\n\treturn requests, true, nil\n}\n"
  },
  {
    "path": "notify/opsgenie/opsgenie_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage opsgenie\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/notify/test\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nfunc TestOpsGenieRetry(t *testing.T) {\n\tnotifier, err := New(\n\t\t&config.OpsGenieConfig{\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\tretryCodes := append(test.DefaultRetryCodes(), http.StatusTooManyRequests)\n\tfor statusCode, expected := range test.RetryTests(retryCodes) {\n\t\tactual, _ := notifier.retrier.Check(statusCode, nil)\n\t\trequire.Equal(t, expected, actual, \"error on status %d\", statusCode)\n\t}\n}\n\nfunc TestOpsGenieRedactedURL(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tkey := \"key\"\n\tnotifier, err := New(\n\t\t&config.OpsGenieConfig{\n\t\t\tAPIURL:     &amcommoncfg.URL{URL: u},\n\t\t\tAPIKey:     commoncfg.Secret(key),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, key)\n}\n\nfunc TestGettingOpsGegineApikeyFromFile(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tkey := \"key\"\n\n\tf, err := os.CreateTemp(t.TempDir(), \"opsgenie_test\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = f.WriteString(key)\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tnotifier, err := New(\n\t\t&config.OpsGenieConfig{\n\t\t\tAPIURL:     &amcommoncfg.URL{URL: u},\n\t\t\tAPIKeyFile: f.Name(),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, key)\n}\n\nfunc TestOpsGenie(t *testing.T) {\n\tu, err := url.Parse(\"https://opsgenie/api\")\n\tif err != nil {\n\t\tt.Fatalf(\"failed to parse URL: %v\", err)\n\t}\n\tlogger := promslog.NewNopLogger()\n\ttmpl := test.CreateTmpl(t)\n\n\tfor _, tc := range []struct {\n\t\ttitle string\n\t\tcfg   *config.OpsGenieConfig\n\n\t\texpectedEmptyAlertBody string\n\t\texpectedBody           string\n\t}{\n\t\t{\n\t\t\ttitle: \"config without details\",\n\t\t\tcfg: &config.OpsGenieConfig{\n\t\t\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\t\t\tVSendResolved: true,\n\t\t\t\t},\n\t\t\t\tMessage:     `{{ .CommonLabels.Message }}`,\n\t\t\t\tDescription: `{{ .CommonLabels.Description }}`,\n\t\t\t\tSource:      `{{ .CommonLabels.Source }}`,\n\t\t\t\tResponders: []config.OpsGenieConfigResponder{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: `{{ .CommonLabels.ResponderName1 }}`,\n\t\t\t\t\t\tType: `{{ .CommonLabels.ResponderType1 }}`,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName: `{{ .CommonLabels.ResponderName2 }}`,\n\t\t\t\t\t\tType: `{{ .CommonLabels.ResponderType2 }}`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tTags:       `{{ .CommonLabels.Tags }}`,\n\t\t\t\tNote:       `{{ .CommonLabels.Note }}`,\n\t\t\t\tPriority:   `{{ .CommonLabels.Priority }}`,\n\t\t\t\tEntity:     `{{ .CommonLabels.Entity }}`,\n\t\t\t\tActions:    `{{ .CommonLabels.Actions }}`,\n\t\t\t\tAPIKey:     `{{ .ExternalURL }}`,\n\t\t\t\tAPIURL:     &amcommoncfg.URL{URL: u},\n\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\t},\n\t\t\texpectedEmptyAlertBody: `{\"alias\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\",\"message\":\"\",\"details\":{},\"source\":\"\"}\n`,\n\t\t\texpectedBody: `{\"alias\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\",\"message\":\"message\",\"description\":\"description\",\"details\":{\"Actions\":\"doThis,doThat\",\"Description\":\"description\",\"Entity\":\"test-domain\",\"Message\":\"message\",\"Note\":\"this is a note\",\"Priority\":\"P1\",\"ResponderName1\":\"TeamA\",\"ResponderName2\":\"EscalationA\",\"ResponderName3\":\"TeamA,TeamB\",\"ResponderType1\":\"team\",\"ResponderType2\":\"escalation\",\"ResponderType3\":\"teams\",\"Source\":\"http://prometheus\",\"Tags\":\"tag1,tag2\"},\"source\":\"http://prometheus\",\"responders\":[{\"name\":\"TeamA\",\"type\":\"team\"},{\"name\":\"EscalationA\",\"type\":\"escalation\"}],\"tags\":[\"tag1\",\"tag2\"],\"note\":\"this is a note\",\"priority\":\"P1\",\"entity\":\"test-domain\",\"actions\":[\"doThis\",\"doThat\"]}\n`,\n\t\t},\n\t\t{\n\t\t\ttitle: \"config with details\",\n\t\t\tcfg: &config.OpsGenieConfig{\n\t\t\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\t\t\tVSendResolved: true,\n\t\t\t\t},\n\t\t\t\tMessage:     `{{ .CommonLabels.Message }}`,\n\t\t\t\tDescription: `{{ .CommonLabels.Description }}`,\n\t\t\t\tSource:      `{{ .CommonLabels.Source }}`,\n\t\t\t\tDetails: map[string]string{\n\t\t\t\t\t\"Description\": `adjusted {{ .CommonLabels.Description }}`,\n\t\t\t\t},\n\t\t\t\tResponders: []config.OpsGenieConfigResponder{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: `{{ .CommonLabels.ResponderName1 }}`,\n\t\t\t\t\t\tType: `{{ .CommonLabels.ResponderType1 }}`,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName: `{{ .CommonLabels.ResponderName2 }}`,\n\t\t\t\t\t\tType: `{{ .CommonLabels.ResponderType2 }}`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tTags:       `{{ .CommonLabels.Tags }}`,\n\t\t\t\tNote:       `{{ .CommonLabels.Note }}`,\n\t\t\t\tPriority:   `{{ .CommonLabels.Priority }}`,\n\t\t\t\tEntity:     `{{ .CommonLabels.Entity }}`,\n\t\t\t\tActions:    `{{ .CommonLabels.Actions }}`,\n\t\t\t\tAPIKey:     `{{ .ExternalURL }}`,\n\t\t\t\tAPIURL:     &amcommoncfg.URL{URL: u},\n\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\t},\n\t\t\texpectedEmptyAlertBody: `{\"alias\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\",\"message\":\"\",\"details\":{\"Description\":\"adjusted \"},\"source\":\"\"}\n`,\n\t\t\texpectedBody: `{\"alias\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\",\"message\":\"message\",\"description\":\"description\",\"details\":{\"Actions\":\"doThis,doThat\",\"Description\":\"adjusted description\",\"Entity\":\"test-domain\",\"Message\":\"message\",\"Note\":\"this is a note\",\"Priority\":\"P1\",\"ResponderName1\":\"TeamA\",\"ResponderName2\":\"EscalationA\",\"ResponderName3\":\"TeamA,TeamB\",\"ResponderType1\":\"team\",\"ResponderType2\":\"escalation\",\"ResponderType3\":\"teams\",\"Source\":\"http://prometheus\",\"Tags\":\"tag1,tag2\"},\"source\":\"http://prometheus\",\"responders\":[{\"name\":\"TeamA\",\"type\":\"team\"},{\"name\":\"EscalationA\",\"type\":\"escalation\"}],\"tags\":[\"tag1\",\"tag2\"],\"note\":\"this is a note\",\"priority\":\"P1\",\"entity\":\"test-domain\",\"actions\":[\"doThis\",\"doThat\"]}\n`,\n\t\t},\n\t\t{\n\t\t\ttitle: \"config with multiple teams\",\n\t\t\tcfg: &config.OpsGenieConfig{\n\t\t\t\tNotifierConfig: amcommoncfg.NotifierConfig{\n\t\t\t\t\tVSendResolved: true,\n\t\t\t\t},\n\t\t\t\tMessage:     `{{ .CommonLabels.Message }}`,\n\t\t\t\tDescription: `{{ .CommonLabels.Description }}`,\n\t\t\t\tSource:      `{{ .CommonLabels.Source }}`,\n\t\t\t\tDetails: map[string]string{\n\t\t\t\t\t\"Description\": `adjusted {{ .CommonLabels.Description }}`,\n\t\t\t\t},\n\t\t\t\tResponders: []config.OpsGenieConfigResponder{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: `{{ .CommonLabels.ResponderName3 }}`,\n\t\t\t\t\t\tType: `{{ .CommonLabels.ResponderType3 }}`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tTags:       `{{ .CommonLabels.Tags }}`,\n\t\t\t\tNote:       `{{ .CommonLabels.Note }}`,\n\t\t\t\tPriority:   `{{ .CommonLabels.Priority }}`,\n\t\t\t\tAPIKey:     `{{ .ExternalURL }}`,\n\t\t\t\tAPIURL:     &amcommoncfg.URL{URL: u},\n\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\t},\n\t\t\texpectedEmptyAlertBody: `{\"alias\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\",\"message\":\"\",\"details\":{\"Description\":\"adjusted \"},\"source\":\"\"}\n`,\n\t\t\texpectedBody: `{\"alias\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\",\"message\":\"message\",\"description\":\"description\",\"details\":{\"Actions\":\"doThis,doThat\",\"Description\":\"adjusted description\",\"Entity\":\"test-domain\",\"Message\":\"message\",\"Note\":\"this is a note\",\"Priority\":\"P1\",\"ResponderName1\":\"TeamA\",\"ResponderName2\":\"EscalationA\",\"ResponderName3\":\"TeamA,TeamB\",\"ResponderType1\":\"team\",\"ResponderType2\":\"escalation\",\"ResponderType3\":\"teams\",\"Source\":\"http://prometheus\",\"Tags\":\"tag1,tag2\"},\"source\":\"http://prometheus\",\"responders\":[{\"name\":\"TeamA\",\"type\":\"team\"},{\"name\":\"TeamB\",\"type\":\"team\"}],\"tags\":[\"tag1\",\"tag2\"],\"note\":\"this is a note\",\"priority\":\"P1\"}\n`,\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\tnotifier, err := New(tc.cfg, tmpl, logger)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tctx := context.Background()\n\t\t\tctx = notify.WithGroupKey(ctx, \"1\")\n\n\t\t\texpectedURL, _ := url.Parse(\"https://opsgenie/apiv2/alerts\")\n\n\t\t\t// Empty alert.\n\t\t\talert1 := &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t},\n\t\t\t}\n\n\t\t\treq, retry, err := notifier.createRequests(ctx, alert1)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, req, 1)\n\t\t\trequire.True(t, retry)\n\t\t\trequire.Equal(t, expectedURL, req[0].URL)\n\t\t\trequire.Equal(t, \"GenieKey http://am\", req[0].Header.Get(\"Authorization\"))\n\t\t\trequire.Equal(t, tc.expectedEmptyAlertBody, readBody(t, req[0]))\n\n\t\t\t// Fully defined alert.\n\t\t\talert2 := &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\"Message\":        \"message\",\n\t\t\t\t\t\t\"Description\":    \"description\",\n\t\t\t\t\t\t\"Source\":         \"http://prometheus\",\n\t\t\t\t\t\t\"ResponderName1\": \"TeamA\",\n\t\t\t\t\t\t\"ResponderType1\": \"team\",\n\t\t\t\t\t\t\"ResponderName2\": \"EscalationA\",\n\t\t\t\t\t\t\"ResponderType2\": \"escalation\",\n\t\t\t\t\t\t\"ResponderName3\": \"TeamA,TeamB\",\n\t\t\t\t\t\t\"ResponderType3\": \"teams\",\n\t\t\t\t\t\t\"Tags\":           \"tag1,tag2\",\n\t\t\t\t\t\t\"Note\":           \"this is a note\",\n\t\t\t\t\t\t\"Priority\":       \"P1\",\n\t\t\t\t\t\t\"Entity\":         \"test-domain\",\n\t\t\t\t\t\t\"Actions\":        \"doThis,doThat\",\n\t\t\t\t\t},\n\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t},\n\t\t\t}\n\t\t\treq, retry, err = notifier.createRequests(ctx, alert2)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.True(t, retry)\n\t\t\trequire.Len(t, req, 1)\n\t\t\trequire.Equal(t, tc.expectedBody, readBody(t, req[0]))\n\n\t\t\t// Broken API Key Template.\n\t\t\ttc.cfg.APIKey = \"{{ kaput \"\n\t\t\t_, _, err = notifier.createRequests(ctx, alert2)\n\t\t\trequire.Error(t, err)\n\t\t\trequire.Equal(t, \"templating error: template: :1: function \\\"kaput\\\" not defined\", err.Error())\n\t\t})\n\t}\n}\n\nfunc TestOpsGenieWithUpdate(t *testing.T) {\n\tu, err := url.Parse(\"https://test-opsgenie-url\")\n\trequire.NoError(t, err)\n\ttmpl := test.CreateTmpl(t)\n\tctx := context.Background()\n\tctx = notify.WithGroupKey(ctx, \"1\")\n\topsGenieConfigWithUpdate := config.OpsGenieConfig{\n\t\tMessage:      `{{ .CommonLabels.Message }}`,\n\t\tDescription:  `{{ .CommonLabels.Description }}`,\n\t\tUpdateAlerts: true,\n\t\tAPIKey:       \"test-api-key\",\n\t\tAPIURL:       &amcommoncfg.URL{URL: u},\n\t\tHTTPConfig:   &commoncfg.HTTPClientConfig{},\n\t}\n\tnotifierWithUpdate, err := New(&opsGenieConfigWithUpdate, tmpl, promslog.NewNopLogger())\n\talert := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tStartsAt: time.Now(),\n\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\tLabels: model.LabelSet{\n\t\t\t\t\"Message\":     \"new message\",\n\t\t\t\t\"Description\": \"new description\",\n\t\t\t},\n\t\t},\n\t}\n\trequire.NoError(t, err)\n\trequests, retry, err := notifierWithUpdate.createRequests(ctx, alert)\n\trequire.NoError(t, err)\n\trequire.True(t, retry)\n\trequire.Len(t, requests, 3)\n\n\tbody0 := readBody(t, requests[0])\n\tbody1 := readBody(t, requests[1])\n\tbody2 := readBody(t, requests[2])\n\tkey, _ := notify.ExtractGroupKey(ctx)\n\talias := key.Hash()\n\n\trequire.Equal(t, \"https://test-opsgenie-url/v2/alerts\", requests[0].URL.String())\n\trequire.NotEmpty(t, body0)\n\n\trequire.Equal(t, requests[1].URL.String(), fmt.Sprintf(\"https://test-opsgenie-url/v2/alerts/%s/message?identifierType=alias\", alias))\n\trequire.JSONEq(t, `{\"message\":\"new message\"}`, body1)\n\trequire.Equal(t, requests[2].URL.String(), fmt.Sprintf(\"https://test-opsgenie-url/v2/alerts/%s/description?identifierType=alias\", alias))\n\trequire.JSONEq(t, `{\"description\":\"new description\"}`, body2)\n}\n\nfunc TestOpsGenieApiKeyFile(t *testing.T) {\n\tu, err := url.Parse(\"https://test-opsgenie-url\")\n\trequire.NoError(t, err)\n\ttmpl := test.CreateTmpl(t)\n\tctx := context.Background()\n\tctx = notify.WithGroupKey(ctx, \"1\")\n\topsGenieConfigWithUpdate := config.OpsGenieConfig{\n\t\tAPIKeyFile: `./api_key_file`,\n\t\tAPIURL:     &amcommoncfg.URL{URL: u},\n\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t}\n\tnotifierWithUpdate, err := New(&opsGenieConfigWithUpdate, tmpl, promslog.NewNopLogger())\n\n\trequire.NoError(t, err)\n\trequests, _, err := notifierWithUpdate.createRequests(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"GenieKey my_secret_api_key\", requests[0].Header.Get(\"Authorization\"))\n}\n\nfunc readBody(t *testing.T, r *http.Request) string {\n\tt.Helper()\n\tbody, err := io.ReadAll(r.Body)\n\trequire.NoError(t, err)\n\treturn string(body)\n}\n"
  },
  {
    "path": "notify/pagerduty/pagerduty.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage pagerduty\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/alecthomas/units\"\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nconst (\n\tmaxEventSize int = 512000\n\t// https://developer.pagerduty.com/docs/ZG9jOjExMDI5NTc4-send-a-v1-event - 1024 characters or runes.\n\tmaxV1DescriptionLenRunes = 1024\n\t// https://developer.pagerduty.com/docs/ZG9jOjExMDI5NTgx-send-an-alert-event - 1024 characters or runes.\n\tmaxV2SummaryLenRunes = 1024\n)\n\n// Notifier implements a Notifier for PagerDuty notifications.\ntype Notifier struct {\n\tconf    *config.PagerdutyConfig\n\ttmpl    *template.Template\n\tlogger  *slog.Logger\n\tapiV1   string // for tests.\n\tclient  *http.Client\n\tretrier *notify.Retrier\n}\n\n// New returns a new PagerDuty notifier.\nfunc New(c *config.PagerdutyConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {\n\tclient, err := notify.NewClientWithTracing(*c.HTTPConfig, \"pagerduty\", httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tn := &Notifier{conf: c, tmpl: t, logger: l, client: client}\n\tif c.ServiceKey != \"\" || c.ServiceKeyFile != \"\" {\n\t\tn.apiV1 = \"https://events.pagerduty.com/generic/2010-04-15/create_event.json\"\n\t\t// Retrying can solve the issue on 403 (rate limiting) and 5xx response codes.\n\t\t// https://developer.pagerduty.com/docs/events-api-v1-overview#api-response-codes--retry-logic\n\t\tn.retrier = &notify.Retrier{RetryCodes: []int{http.StatusForbidden}, CustomDetailsFunc: errDetails}\n\t} else {\n\t\t// Retrying can solve the issue on 429 (rate limiting) and 5xx response codes.\n\t\t// https://developer.pagerduty.com/docs/events-api-v2-overview#response-codes--retry-logic\n\t\tn.retrier = &notify.Retrier{RetryCodes: []int{http.StatusTooManyRequests}, CustomDetailsFunc: errDetails}\n\t}\n\treturn n, nil\n}\n\nconst (\n\tpagerDutyEventTrigger = \"trigger\"\n\tpagerDutyEventResolve = \"resolve\"\n)\n\ntype pagerDutyMessage struct {\n\tRoutingKey  string            `json:\"routing_key,omitempty\"`\n\tServiceKey  string            `json:\"service_key,omitempty\"`\n\tDedupKey    string            `json:\"dedup_key,omitempty\"`\n\tIncidentKey string            `json:\"incident_key,omitempty\"`\n\tEventType   string            `json:\"event_type,omitempty\"`\n\tDescription string            `json:\"description,omitempty\"`\n\tEventAction string            `json:\"event_action\"`\n\tPayload     *pagerDutyPayload `json:\"payload\"`\n\tClient      string            `json:\"client,omitempty\"`\n\tClientURL   string            `json:\"client_url,omitempty\"`\n\tDetails     map[string]any    `json:\"details,omitempty\"`\n\tImages      []pagerDutyImage  `json:\"images,omitempty\"`\n\tLinks       []pagerDutyLink   `json:\"links,omitempty\"`\n}\n\ntype pagerDutyLink struct {\n\tHRef string `json:\"href\"`\n\tText string `json:\"text\"`\n}\n\ntype pagerDutyImage struct {\n\tSrc  string `json:\"src\"`\n\tAlt  string `json:\"alt\"`\n\tHref string `json:\"href\"`\n}\n\ntype pagerDutyPayload struct {\n\tSummary       string         `json:\"summary\"`\n\tSource        string         `json:\"source\"`\n\tSeverity      string         `json:\"severity\"`\n\tTimestamp     string         `json:\"timestamp,omitempty\"`\n\tClass         string         `json:\"class,omitempty\"`\n\tComponent     string         `json:\"component,omitempty\"`\n\tGroup         string         `json:\"group,omitempty\"`\n\tCustomDetails map[string]any `json:\"custom_details,omitempty\"`\n}\n\nfunc (n *Notifier) encodeMessage(msg *pagerDutyMessage) (bytes.Buffer, error) {\n\tvar buf bytes.Buffer\n\tif err := json.NewEncoder(&buf).Encode(msg); err != nil {\n\t\treturn buf, fmt.Errorf(\"failed to encode PagerDuty message: %w\", err)\n\t}\n\n\tif buf.Len() > maxEventSize {\n\t\ttruncatedMsg := fmt.Sprintf(\"Custom details have been removed because the original event exceeds the maximum size of %s\", units.MetricBytes(maxEventSize).String())\n\n\t\tif n.apiV1 != \"\" {\n\t\t\tmsg.Details = map[string]any{\"error\": truncatedMsg}\n\t\t} else {\n\t\t\tmsg.Payload.CustomDetails = map[string]any{\"error\": truncatedMsg}\n\t\t}\n\n\t\twarningMsg := fmt.Sprintf(\"Truncated Details because message of size %s exceeds limit %s\", units.MetricBytes(buf.Len()).String(), units.MetricBytes(maxEventSize).String())\n\t\tn.logger.Warn(warningMsg)\n\n\t\tbuf.Reset()\n\t\tif err := json.NewEncoder(&buf).Encode(msg); err != nil {\n\t\t\treturn buf, fmt.Errorf(\"failed to encode PagerDuty message: %w\", err)\n\t\t}\n\t}\n\n\treturn buf, nil\n}\n\nfunc (n *Notifier) notifyV1(\n\tctx context.Context,\n\teventType string,\n\tkey notify.Key,\n\tdata *template.Data,\n\tdetails map[string]any,\n) (bool, error) {\n\tvar tmplErr error\n\ttmpl := notify.TmplText(n.tmpl, data, &tmplErr)\n\n\tdescription, truncated := notify.TruncateInRunes(tmpl(n.conf.Description), maxV1DescriptionLenRunes)\n\tif truncated {\n\t\tn.logger.Warn(\"Truncated description\", \"key\", key, \"max_runes\", maxV1DescriptionLenRunes)\n\t}\n\n\tserviceKey := string(n.conf.ServiceKey)\n\tif serviceKey == \"\" {\n\t\tcontent, fileErr := os.ReadFile(n.conf.ServiceKeyFile)\n\t\tif fileErr != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to read service key from file: %w\", fileErr)\n\t\t}\n\t\tserviceKey = strings.TrimSpace(string(content))\n\t}\n\n\tmsg := &pagerDutyMessage{\n\t\tServiceKey:  tmpl(serviceKey),\n\t\tEventType:   eventType,\n\t\tIncidentKey: key.Hash(),\n\t\tDescription: description,\n\t\tDetails:     details,\n\t}\n\n\tif eventType == pagerDutyEventTrigger {\n\t\tmsg.Client = tmpl(n.conf.Client)\n\t\tmsg.ClientURL = tmpl(n.conf.ClientURL)\n\t}\n\n\tif tmplErr != nil {\n\t\treturn false, fmt.Errorf(\"failed to template PagerDuty v1 message: %w\", tmplErr)\n\t}\n\n\t// Ensure that the service key isn't empty after templating.\n\tif msg.ServiceKey == \"\" {\n\t\treturn false, errors.New(\"service key cannot be empty\")\n\t}\n\n\tencodedMsg, err := n.encodeMessage(msg)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tresp, err := notify.PostJSON(ctx, n.client, n.apiV1, &encodedMsg)\n\tif err != nil {\n\t\treturn true, fmt.Errorf(\"failed to post message to PagerDuty v1: %w\", err)\n\t}\n\tdefer notify.Drain(resp)\n\n\treturn n.retrier.Check(resp.StatusCode, resp.Body)\n}\n\nfunc (n *Notifier) notifyV2(\n\tctx context.Context,\n\teventType string,\n\tkey notify.Key,\n\tdata *template.Data,\n\tdetails map[string]any,\n) (bool, error) {\n\tvar tmplErr error\n\ttmpl := notify.TmplText(n.tmpl, data, &tmplErr)\n\n\tif n.conf.Severity == \"\" {\n\t\tn.conf.Severity = \"error\"\n\t}\n\n\tsummary, truncated := notify.TruncateInRunes(tmpl(n.conf.Description), maxV2SummaryLenRunes)\n\tif truncated {\n\t\tn.logger.Warn(\"Truncated summary\", \"key\", key, \"max_runes\", maxV2SummaryLenRunes)\n\t}\n\n\troutingKey := string(n.conf.RoutingKey)\n\tif routingKey == \"\" {\n\t\tcontent, fileErr := os.ReadFile(n.conf.RoutingKeyFile)\n\t\tif fileErr != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to read routing key from file: %w\", fileErr)\n\t\t}\n\t\troutingKey = strings.TrimSpace(string(content))\n\t}\n\n\tmsg := &pagerDutyMessage{\n\t\tClient:      tmpl(n.conf.Client),\n\t\tClientURL:   tmpl(n.conf.ClientURL),\n\t\tRoutingKey:  tmpl(routingKey),\n\t\tEventAction: eventType,\n\t\tDedupKey:    key.Hash(),\n\t\tImages:      make([]pagerDutyImage, 0, len(n.conf.Images)),\n\t\tLinks:       make([]pagerDutyLink, 0, len(n.conf.Links)),\n\t\tPayload: &pagerDutyPayload{\n\t\t\tSummary:       summary,\n\t\t\tSource:        tmpl(n.conf.Source),\n\t\t\tSeverity:      tmpl(n.conf.Severity),\n\t\t\tCustomDetails: details,\n\t\t\tClass:         tmpl(n.conf.Class),\n\t\t\tComponent:     tmpl(n.conf.Component),\n\t\t\tGroup:         tmpl(n.conf.Group),\n\t\t},\n\t}\n\n\tfor _, item := range n.conf.Images {\n\t\timage := pagerDutyImage{\n\t\t\tSrc:  tmpl(item.Src),\n\t\t\tAlt:  tmpl(item.Alt),\n\t\t\tHref: tmpl(item.Href),\n\t\t}\n\n\t\tif image.Src != \"\" {\n\t\t\tmsg.Images = append(msg.Images, image)\n\t\t}\n\t}\n\n\tfor _, item := range n.conf.Links {\n\t\tlink := pagerDutyLink{\n\t\t\tHRef: tmpl(item.Href),\n\t\t\tText: tmpl(item.Text),\n\t\t}\n\n\t\tif link.HRef != \"\" {\n\t\t\tmsg.Links = append(msg.Links, link)\n\t\t}\n\t}\n\n\tif tmplErr != nil {\n\t\treturn false, fmt.Errorf(\"failed to template PagerDuty v2 message: %w\", tmplErr)\n\t}\n\n\t// Ensure that the routing key isn't empty after templating.\n\tif msg.RoutingKey == \"\" {\n\t\treturn false, errors.New(\"routing key cannot be empty\")\n\t}\n\n\tencodedMsg, err := n.encodeMessage(msg)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tresp, err := notify.PostJSON(ctx, n.client, n.conf.URL.String(), &encodedMsg)\n\tif err != nil {\n\t\treturn true, fmt.Errorf(\"failed to post message to PagerDuty: %w\", err)\n\t}\n\tdefer notify.Drain(resp)\n\n\tretry, err := n.retrier.Check(resp.StatusCode, resp.Body)\n\tif err != nil {\n\t\treturn retry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)\n\t}\n\treturn retry, err\n}\n\n// Notify implements the Notifier interface.\nfunc (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {\n\tkey, err := notify.ExtractGroupKey(ctx)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tlogger := n.logger.With(\"group_key\", key)\n\n\tvar (\n\t\talerts    = types.Alerts(as...)\n\t\tdata      = notify.GetTemplateData(ctx, n.tmpl, as, logger)\n\t\teventType = pagerDutyEventTrigger\n\t)\n\n\tif alerts.Status() == model.AlertResolved {\n\t\teventType = pagerDutyEventResolve\n\t}\n\n\tlogger.Debug(\"extracted group key\", \"eventType\", eventType)\n\n\tdetails, err := n.renderDetails(data)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"failed to render details: %w\", err)\n\t}\n\n\tif n.conf.Timeout > 0 {\n\t\tnfCtx, cancel := context.WithTimeoutCause(ctx, n.conf.Timeout, fmt.Errorf(\"configured pagerduty timeout reached (%s)\", n.conf.Timeout))\n\t\tdefer cancel()\n\t\tctx = nfCtx\n\t}\n\n\tnf := n.notifyV2\n\tif n.apiV1 != \"\" {\n\t\tnf = n.notifyV1\n\t}\n\tretry, err := nf(ctx, eventType, key, data, details)\n\tif err != nil {\n\t\tif ctx.Err() != nil {\n\t\t\terr = fmt.Errorf(\"%w: %w\", err, context.Cause(ctx))\n\t\t}\n\t\treturn retry, err\n\t}\n\treturn retry, nil\n}\n\nfunc errDetails(status int, body io.Reader) string {\n\t// See https://v2.developer.pagerduty.com/docs/trigger-events for the v1 events API.\n\t// See https://v2.developer.pagerduty.com/docs/send-an-event-events-api-v2 for the v2 events API.\n\tif status != http.StatusBadRequest || body == nil {\n\t\treturn \"\"\n\t}\n\tvar pgr struct {\n\t\tStatus  string   `json:\"status\"`\n\t\tMessage string   `json:\"message\"`\n\t\tErrors  []string `json:\"errors\"`\n\t}\n\tif err := json.NewDecoder(body).Decode(&pgr); err != nil {\n\t\treturn \"\"\n\t}\n\treturn fmt.Sprintf(\"%s: %s\", pgr.Message, strings.Join(pgr.Errors, \",\"))\n}\n\nfunc (n *Notifier) renderDetails(\n\tdata *template.Data,\n) (map[string]any, error) {\n\tvar (\n\t\ttmplTextErr  error\n\t\ttmplText     = notify.TmplText(n.tmpl, data, &tmplTextErr)\n\t\ttmplTextFunc = func(tmpl string) (string, error) {\n\t\t\treturn tmplText(tmpl), tmplTextErr\n\t\t}\n\t)\n\tvar err error\n\trendered := make(map[string]any, len(n.conf.Details))\n\tfor k, v := range n.conf.Details {\n\t\trendered[k], err = template.DeepCopyWithTemplate(v, tmplTextFunc)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn rendered, nil\n}\n"
  },
  {
    "path": "notify/pagerduty/pagerduty_test.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage pagerduty\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/notify/test\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nfunc TestPagerDutyRetryV1(t *testing.T) {\n\tnotifier, err := New(\n\t\t&config.PagerdutyConfig{\n\t\t\tServiceKey: commoncfg.Secret(\"01234567890123456789012345678901\"),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\tretryCodes := append(test.DefaultRetryCodes(), http.StatusForbidden)\n\tfor statusCode, expected := range test.RetryTests(retryCodes) {\n\t\tactual, _ := notifier.retrier.Check(statusCode, nil)\n\t\trequire.Equal(t, expected, actual, \"retryv1 - error on status %d\", statusCode)\n\t}\n}\n\nfunc TestPagerDutyRetryV2(t *testing.T) {\n\tnotifier, err := New(\n\t\t&config.PagerdutyConfig{\n\t\t\tRoutingKey: commoncfg.Secret(\"01234567890123456789012345678901\"),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\tretryCodes := append(test.DefaultRetryCodes(), http.StatusTooManyRequests)\n\tfor statusCode, expected := range test.RetryTests(retryCodes) {\n\t\tactual, _ := notifier.retrier.Check(statusCode, nil)\n\t\trequire.Equal(t, expected, actual, \"retryv2 - error on status %d\", statusCode)\n\t}\n}\n\nfunc TestPagerDutyRedactedURLV1(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tkey := \"01234567890123456789012345678901\"\n\tnotifier, err := New(\n\t\t&config.PagerdutyConfig{\n\t\t\tServiceKey: commoncfg.Secret(key),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\tnotifier.apiV1 = u.String()\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, key)\n}\n\nfunc TestPagerDutyRedactedURLV2(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tkey := \"01234567890123456789012345678901\"\n\tnotifier, err := New(\n\t\t&config.PagerdutyConfig{\n\t\t\tURL:        &amcommoncfg.URL{URL: u},\n\t\t\tRoutingKey: commoncfg.Secret(key),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, key)\n}\n\nfunc TestPagerDutyV1ServiceKeyFromFile(t *testing.T) {\n\tkey := \"01234567890123456789012345678901\"\n\tf, err := os.CreateTemp(t.TempDir(), \"pagerduty_test\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = f.WriteString(key)\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tnotifier, err := New(\n\t\t&config.PagerdutyConfig{\n\t\t\tServiceKeyFile: f.Name(),\n\t\t\tHTTPConfig:     &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\tnotifier.apiV1 = u.String()\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, key)\n}\n\nfunc TestPagerDutyV2RoutingKeyFromFile(t *testing.T) {\n\tkey := \"01234567890123456789012345678901\"\n\tf, err := os.CreateTemp(t.TempDir(), \"pagerduty_test\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = f.WriteString(key)\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tnotifier, err := New(\n\t\t&config.PagerdutyConfig{\n\t\t\tURL:            &amcommoncfg.URL{URL: u},\n\t\t\tRoutingKeyFile: f.Name(),\n\t\t\tHTTPConfig:     &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, key)\n}\n\nfunc TestPagerDutyTemplating(t *testing.T) {\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdec := json.NewDecoder(r.Body)\n\t\tout := make(map[string]any)\n\t\terr := dec.Decode(&out)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}))\n\tdefer srv.Close()\n\tu, _ := url.Parse(srv.URL)\n\n\tfor _, tc := range []struct {\n\t\ttitle string\n\t\tcfg   *config.PagerdutyConfig\n\n\t\tretry  bool\n\t\terrMsg string\n\t}{\n\t\t{\n\t\t\ttitle: \"full-blown legacy message\",\n\t\t\tcfg: &config.PagerdutyConfig{\n\t\t\t\tRoutingKey: commoncfg.Secret(\"01234567890123456789012345678901\"),\n\t\t\t\tImages: []config.PagerdutyImage{\n\t\t\t\t\t{\n\t\t\t\t\t\tSrc:  \"{{ .Status }}\",\n\t\t\t\t\t\tAlt:  \"{{ .Status }}\",\n\t\t\t\t\t\tHref: \"{{ .Status }}\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tLinks: []config.PagerdutyLink{\n\t\t\t\t\t{\n\t\t\t\t\t\tHref: \"{{ .Status }}\",\n\t\t\t\t\t\tText: \"{{ .Status }}\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tDetails: map[string]any{\n\t\t\t\t\t\"firing\":       `{{ .Alerts.Firing | toJson }}`,\n\t\t\t\t\t\"resolved\":     `{{ .Alerts.Resolved | toJson }}`,\n\t\t\t\t\t\"num_firing\":   `{{ .Alerts.Firing | len }}`,\n\t\t\t\t\t\"num_resolved\": `{{ .Alerts.Resolved | len }}`,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle: \"full-blown legacy message\",\n\t\t\tcfg: &config.PagerdutyConfig{\n\t\t\t\tRoutingKey: commoncfg.Secret(\"01234567890123456789012345678901\"),\n\t\t\t\tImages: []config.PagerdutyImage{\n\t\t\t\t\t{\n\t\t\t\t\t\tSrc:  \"{{ .Status }}\",\n\t\t\t\t\t\tAlt:  \"{{ .Status }}\",\n\t\t\t\t\t\tHref: \"{{ .Status }}\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tLinks: []config.PagerdutyLink{\n\t\t\t\t\t{\n\t\t\t\t\t\tHref: \"{{ .Status }}\",\n\t\t\t\t\t\tText: \"{{ .Status }}\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tDetails: map[string]any{\n\t\t\t\t\t\"firing\":       `{{ template \"pagerduty.default.instances\" .Alerts.Firing }}`,\n\t\t\t\t\t\"resolved\":     `{{ template \"pagerduty.default.instances\" .Alerts.Resolved }}`,\n\t\t\t\t\t\"num_firing\":   `{{ .Alerts.Firing | len }}`,\n\t\t\t\t\t\"num_resolved\": `{{ .Alerts.Resolved | len }}`,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle: \"nested details\",\n\t\t\tcfg: &config.PagerdutyConfig{\n\t\t\t\tRoutingKey: commoncfg.Secret(\"01234567890123456789012345678901\"),\n\t\t\t\tDetails: map[string]any{\n\t\t\t\t\t\"a\": map[string]any{\n\t\t\t\t\t\t\"b\": map[string]any{\n\t\t\t\t\t\t\t\"c\": map[string]any{\n\t\t\t\t\t\t\t\t\"firing\":       `{{ .Alerts.Firing | toJson }}`,\n\t\t\t\t\t\t\t\t\"resolved\":     `{{ .Alerts.Resolved | toJson }}`,\n\t\t\t\t\t\t\t\t\"num_firing\":   `{{ .Alerts.Firing | len }}`,\n\t\t\t\t\t\t\t\t\"num_resolved\": `{{ .Alerts.Resolved | len }}`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle: \"nested details with template error\",\n\t\t\tcfg: &config.PagerdutyConfig{\n\t\t\t\tRoutingKey: commoncfg.Secret(\"01234567890123456789012345678901\"),\n\t\t\t\tDetails: map[string]any{\n\t\t\t\t\t\"a\": map[string]any{\n\t\t\t\t\t\t\"b\": map[string]any{\n\t\t\t\t\t\t\t\"c\": map[string]any{\n\t\t\t\t\t\t\t\t\"firing\": `{{ template \"pagerduty.default.instances\" .Alerts.Firing`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\terrMsg: \"failed to render details: template: :1: unclosed action\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"details with templating errors\",\n\t\t\tcfg: &config.PagerdutyConfig{\n\t\t\t\tRoutingKey: commoncfg.Secret(\"01234567890123456789012345678901\"),\n\t\t\t\tDetails: map[string]any{\n\t\t\t\t\t\"firing\":       `{{ .Alerts.Firing | toJson`,\n\t\t\t\t\t\"resolved\":     `{{ .Alerts.Resolved | toJson }}`,\n\t\t\t\t\t\"num_firing\":   `{{ .Alerts.Firing | len }}`,\n\t\t\t\t\t\"num_resolved\": `{{ .Alerts.Resolved | len }}`,\n\t\t\t\t},\n\t\t\t},\n\t\t\terrMsg: \"failed to render details: template: :1: unclosed action\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"v2 message with templating errors\",\n\t\t\tcfg: &config.PagerdutyConfig{\n\t\t\t\tRoutingKey: commoncfg.Secret(\"01234567890123456789012345678901\"),\n\t\t\t\tSeverity:   \"{{ \",\n\t\t\t},\n\t\t\terrMsg: \"failed to template\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"v1 message with templating errors\",\n\t\t\tcfg: &config.PagerdutyConfig{\n\t\t\t\tServiceKey: commoncfg.Secret(\"01234567890123456789012345678901\"),\n\t\t\t\tClient:     \"{{ \",\n\t\t\t},\n\t\t\terrMsg: \"failed to template\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"routing key cannot be empty\",\n\t\t\tcfg: &config.PagerdutyConfig{\n\t\t\t\tRoutingKey: commoncfg.Secret(`{{ \"\" }}`),\n\t\t\t},\n\t\t\terrMsg: \"routing key cannot be empty\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"service_key cannot be empty\",\n\t\t\tcfg: &config.PagerdutyConfig{\n\t\t\t\tServiceKey: commoncfg.Secret(`{{ \"\" }}`),\n\t\t\t},\n\t\t\terrMsg: \"service key cannot be empty\",\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\ttc.cfg.URL = &amcommoncfg.URL{URL: u}\n\t\t\ttc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{}\n\t\t\tpd, err := New(tc.cfg, test.CreateTmpl(t), promslog.NewNopLogger())\n\t\t\trequire.NoError(t, err)\n\t\t\tif pd.apiV1 != \"\" {\n\t\t\t\tpd.apiV1 = u.String()\n\t\t\t}\n\n\t\t\tctx := context.Background()\n\t\t\tctx = notify.WithGroupKey(ctx, \"1\")\n\n\t\t\tok, err := pd.Notify(ctx, []*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"lbl1\": \"val1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}...)\n\t\t\tif tc.errMsg == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), tc.errMsg)\n\t\t\t}\n\t\t\trequire.Equal(t, tc.retry, ok)\n\t\t})\n\t}\n}\n\nfunc TestErrDetails(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tstatus int\n\t\tbody   io.Reader\n\n\t\texp string\n\t}{\n\t\t{\n\t\t\tstatus: http.StatusBadRequest,\n\t\t\tbody: bytes.NewBuffer([]byte(\n\t\t\t\t`{\"status\":\"invalid event\",\"message\":\"Event object is invalid\",\"errors\":[\"Length of 'routing_key' is incorrect (should be 32 characters)\"]}`,\n\t\t\t)),\n\n\t\t\texp: \"Length of 'routing_key' is incorrect\",\n\t\t},\n\t\t{\n\t\t\tstatus: http.StatusBadRequest,\n\t\t\tbody:   bytes.NewBuffer([]byte(`{\"status\"}`)),\n\n\t\t\texp: \"\",\n\t\t},\n\t\t{\n\t\t\tstatus: http.StatusBadRequest,\n\n\t\t\texp: \"\",\n\t\t},\n\t\t{\n\t\t\tstatus: http.StatusTooManyRequests,\n\n\t\t\texp: \"\",\n\t\t},\n\t} {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\terr := errDetails(tc.status, tc.body)\n\t\t\trequire.Contains(t, err, tc.exp)\n\t\t})\n\t}\n}\n\nfunc TestEventSizeEnforcement(t *testing.T) {\n\tbigDetailsV1 := map[string]any{\n\t\t\"firing\": strings.Repeat(\"a\", 513000),\n\t}\n\tbigDetailsV2 := map[string]any{\n\t\t\"firing\": strings.Repeat(\"a\", 513000),\n\t}\n\n\t// V1 Messages\n\tmsgV1 := &pagerDutyMessage{\n\t\tServiceKey: \"01234567890123456789012345678901\",\n\t\tEventType:  \"trigger\",\n\t\tDetails:    bigDetailsV1,\n\t}\n\n\tnotifierV1, err := New(\n\t\t&config.PagerdutyConfig{\n\t\t\tServiceKey: commoncfg.Secret(\"01234567890123456789012345678901\"),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\tencodedV1, err := notifierV1.encodeMessage(msgV1)\n\trequire.NoError(t, err)\n\trequire.Contains(t, encodedV1.String(), `\"details\":{\"error\":\"Custom details have been removed because the original event exceeds the maximum size of 512KB\"}`)\n\n\t// V2 Messages\n\tmsgV2 := &pagerDutyMessage{\n\t\tRoutingKey:  \"01234567890123456789012345678901\",\n\t\tEventAction: \"trigger\",\n\t\tPayload: &pagerDutyPayload{\n\t\t\tCustomDetails: bigDetailsV2,\n\t\t},\n\t}\n\n\tnotifierV2, err := New(\n\t\t&config.PagerdutyConfig{\n\t\t\tRoutingKey: commoncfg.Secret(\"01234567890123456789012345678901\"),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\tencodedV2, err := notifierV2.encodeMessage(msgV2)\n\trequire.NoError(t, err)\n\trequire.Contains(t, encodedV2.String(), `\"custom_details\":{\"error\":\"Custom details have been removed because the original event exceeds the maximum size of 512KB\"}`)\n}\n\nfunc TestPagerDutyEmptySrcHref(t *testing.T) {\n\ttype pagerDutyEvent struct {\n\t\tRoutingKey  string           `json:\"routing_key\"`\n\t\tEventAction string           `json:\"event_action\"`\n\t\tDedupKey    string           `json:\"dedup_key\"`\n\t\tPayload     pagerDutyPayload `json:\"payload\"`\n\t\tImages      []pagerDutyImage\n\t\tLinks       []pagerDutyLink\n\t}\n\n\timages := []config.PagerdutyImage{\n\t\t{\n\t\t\tSrc:  \"\",\n\t\t\tAlt:  \"Empty src\",\n\t\t\tHref: \"https://example.com/\",\n\t\t},\n\t\t{\n\t\t\tSrc:  \"https://example.com/cat.jpg\",\n\t\t\tAlt:  \"Empty href\",\n\t\t\tHref: \"\",\n\t\t},\n\t\t{\n\t\t\tSrc:  \"https://example.com/cat.jpg\",\n\t\t\tAlt:  \"\",\n\t\t\tHref: \"https://example.com/\",\n\t\t},\n\t}\n\n\tlinks := []config.PagerdutyLink{\n\t\t{\n\t\t\tHref: \"\",\n\t\t\tText: \"Empty href\",\n\t\t},\n\t\t{\n\t\t\tHref: \"https://example.com/\",\n\t\t\tText: \"\",\n\t\t},\n\t}\n\n\texpectedImages := make([]pagerDutyImage, 0, len(images))\n\tfor _, image := range images {\n\t\tif image.Src == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\texpectedImages = append(expectedImages, pagerDutyImage{\n\t\t\tSrc:  image.Src,\n\t\t\tAlt:  image.Alt,\n\t\t\tHref: image.Href,\n\t\t})\n\t}\n\n\texpectedLinks := make([]pagerDutyLink, 0, len(links))\n\tfor _, link := range links {\n\t\tif link.Href == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\texpectedLinks = append(expectedLinks, pagerDutyLink{\n\t\t\tHRef: link.Href,\n\t\t\tText: link.Text,\n\t\t})\n\t}\n\n\tserver := httptest.NewServer(http.HandlerFunc(\n\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\tdecoder := json.NewDecoder(r.Body)\n\t\t\tvar event pagerDutyEvent\n\t\t\tif err := decoder.Decode(&event); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\n\t\t\tif event.RoutingKey == \"\" || event.EventAction == \"\" {\n\t\t\t\thttp.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor _, image := range event.Images {\n\t\t\t\tif image.Src == \"\" {\n\t\t\t\t\thttp.Error(w, \"Event object is invalid: 'image src' is missing or blank\", http.StatusBadRequest)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor _, link := range event.Links {\n\t\t\t\tif link.HRef == \"\" {\n\t\t\t\t\thttp.Error(w, \"Event object is invalid: 'link href' is missing or blank\", http.StatusBadRequest)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\trequire.Equal(t, expectedImages, event.Images)\n\t\t\trequire.Equal(t, expectedLinks, event.Links)\n\t\t},\n\t))\n\tdefer server.Close()\n\n\turl, err := url.Parse(server.URL)\n\trequire.NoError(t, err)\n\n\tpagerDutyConfig := config.PagerdutyConfig{\n\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\tRoutingKey: commoncfg.Secret(\"01234567890123456789012345678901\"),\n\t\tURL:        &amcommoncfg.URL{URL: url},\n\t\tImages:     images,\n\t\tLinks:      links,\n\t}\n\n\tpagerDuty, err := New(&pagerDutyConfig, test.CreateTmpl(t), promslog.NewNopLogger())\n\trequire.NoError(t, err)\n\n\tctx := context.Background()\n\tctx = notify.WithGroupKey(ctx, \"1\")\n\n\t_, err = pagerDuty.Notify(ctx, []*types.Alert{\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"lbl1\": \"val1\",\n\t\t\t\t},\n\t\t\t\tStartsAt: time.Now(),\n\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t},\n\t\t},\n\t}...)\n\trequire.NoError(t, err)\n}\n\nfunc TestPagerDutyTimeout(t *testing.T) {\n\ttype pagerDutyEvent struct {\n\t\tRoutingKey  string           `json:\"routing_key\"`\n\t\tEventAction string           `json:\"event_action\"`\n\t\tDedupKey    string           `json:\"dedup_key\"`\n\t\tPayload     pagerDutyPayload `json:\"payload\"`\n\t\tImages      []pagerDutyImage\n\t\tLinks       []pagerDutyLink\n\t}\n\n\ttests := map[string]struct {\n\t\tlatency time.Duration\n\t\ttimeout time.Duration\n\t\twantErr bool\n\t}{\n\t\t\"success\": {latency: 100 * time.Millisecond, timeout: 120 * time.Millisecond, wantErr: false},\n\t\t\"error\":   {latency: 100 * time.Millisecond, timeout: 80 * time.Millisecond, wantErr: true},\n\t}\n\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tsrv := httptest.NewServer(http.HandlerFunc(\n\t\t\t\tfunc(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tdecoder := json.NewDecoder(r.Body)\n\t\t\t\t\tvar event pagerDutyEvent\n\t\t\t\t\tif err := decoder.Decode(&event); err != nil {\n\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t}\n\n\t\t\t\t\tif event.RoutingKey == \"\" || event.EventAction == \"\" {\n\t\t\t\t\t\thttp.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\ttime.Sleep(tt.latency)\n\t\t\t\t},\n\t\t\t))\n\t\t\tdefer srv.Close()\n\t\t\tu, err := url.Parse(srv.URL)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcfg := config.PagerdutyConfig{\n\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\t\tRoutingKey: commoncfg.Secret(\"01234567890123456789012345678901\"),\n\t\t\t\tURL:        &amcommoncfg.URL{URL: u},\n\t\t\t\tTimeout:    tt.timeout,\n\t\t\t}\n\n\t\t\tpd, err := New(&cfg, test.CreateTmpl(t), promslog.NewNopLogger())\n\t\t\trequire.NoError(t, err)\n\n\t\t\tctx := context.Background()\n\t\t\tctx = notify.WithGroupKey(ctx, \"1\")\n\t\t\talert := &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\"lbl1\": \"val1\",\n\t\t\t\t\t},\n\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t},\n\t\t\t}\n\t\t\t_, err = pd.Notify(ctx, alert)\n\t\t\trequire.Equal(t, tt.wantErr, err != nil)\n\t\t})\n\t}\n}\n\nfunc TestRenderDetails(t *testing.T) {\n\ttype args struct {\n\t\tdetails map[string]any\n\t\tdata    *template.Data\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    map[string]any\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"flat\",\n\t\t\targs: args{\n\t\t\t\tdetails: map[string]any{\n\t\t\t\t\t\"a\": \"{{ .Status }}\",\n\t\t\t\t\t\"b\": \"String\",\n\t\t\t\t},\n\t\t\t\tdata: &template.Data{\n\t\t\t\t\tStatus: \"Flat\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: map[string]any{\n\t\t\t\t\"a\": \"Flat\",\n\t\t\t\t\"b\": \"String\",\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"flat error\",\n\t\t\targs: args{\n\t\t\t\tdetails: map[string]any{\n\t\t\t\t\t\"a\": \"{{ .Status\",\n\t\t\t\t},\n\t\t\t\tdata: &template.Data{\n\t\t\t\t\tStatus: \"Error\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant:    nil,\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"nested\",\n\t\t\targs: args{\n\t\t\t\tdetails: map[string]any{\n\t\t\t\t\t\"a\": map[string]any{\n\t\t\t\t\t\t\"b\": map[string]any{\n\t\t\t\t\t\t\t\"c\": \"{{ .Status }}\",\n\t\t\t\t\t\t\t\"d\": \"String\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdata: &template.Data{\n\t\t\t\t\tStatus: \"Nested\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: map[string]any{\n\t\t\t\t\"a\": map[string]any{\n\t\t\t\t\t\"b\": map[string]any{\n\t\t\t\t\t\t\"c\": \"Nested\",\n\t\t\t\t\t\t\"d\": \"String\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"nested error\",\n\t\t\targs: args{\n\t\t\t\tdetails: map[string]any{\n\t\t\t\t\t\"a\": map[string]any{\n\t\t\t\t\t\t\"b\": map[string]any{\n\t\t\t\t\t\t\t\"c\": \"{{ .Status\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdata: &template.Data{\n\t\t\t\t\tStatus: \"Error\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant:    nil,\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"alerts\",\n\t\t\targs: args{\n\t\t\t\tdetails: map[string]any{\n\t\t\t\t\t\"alerts\": map[string]any{\n\t\t\t\t\t\t\"firing\":       \"{{ .Alerts.Firing | toJson }}\",\n\t\t\t\t\t\t\"resolved\":     \"{{ .Alerts.Resolved | toJson }}\",\n\t\t\t\t\t\t\"num_firing\":   \"{{ len .Alerts.Firing }}\",\n\t\t\t\t\t\t\"num_resolved\": \"{{ len .Alerts.Resolved }}\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdata: &template.Data{\n\t\t\t\t\tAlerts: template.Alerts{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tStatus: \"firing\",\n\t\t\t\t\t\t\tAnnotations: template.KV{\n\t\t\t\t\t\t\t\t\"annotation1\": \"value1\",\n\t\t\t\t\t\t\t\t\"annotation2\": \"value2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tLabels: template.KV{\n\t\t\t\t\t\t\t\t\"alertname\": \"Firing1\",\n\t\t\t\t\t\t\t\t\"label1\":    \"value1\",\n\t\t\t\t\t\t\t\t\"label2\":    \"value2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFingerprint:  \"fingerprint1\",\n\t\t\t\t\t\t\tGeneratorURL: \"http://generator1\",\n\t\t\t\t\t\t\tStartsAt:     time.Date(2001, time.January, 1, 0, 0, 0, 0, time.UTC),\n\t\t\t\t\t\t\tEndsAt:       time.Date(2001, time.January, 1, 1, 0, 0, 0, time.UTC),\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tStatus: \"firing\",\n\t\t\t\t\t\t\tAnnotations: template.KV{\n\t\t\t\t\t\t\t\t\"annotation1\": \"value1\",\n\t\t\t\t\t\t\t\t\"annotation2\": \"value2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tLabels: template.KV{\n\t\t\t\t\t\t\t\t\"alertname\": \"Firing2\",\n\t\t\t\t\t\t\t\t\"label1\":    \"value1\",\n\t\t\t\t\t\t\t\t\"label2\":    \"value2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFingerprint:  \"fingerprint2\",\n\t\t\t\t\t\t\tGeneratorURL: \"http://generator2\",\n\t\t\t\t\t\t\tStartsAt:     time.Date(2002, time.January, 1, 0, 0, 0, 0, time.UTC),\n\t\t\t\t\t\t\tEndsAt:       time.Date(2002, time.January, 1, 1, 0, 0, 0, time.UTC),\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tStatus: \"resolved\",\n\t\t\t\t\t\t\tAnnotations: template.KV{\n\t\t\t\t\t\t\t\t\"annotation1\": \"value1\",\n\t\t\t\t\t\t\t\t\"annotation2\": \"value2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tLabels: template.KV{\n\t\t\t\t\t\t\t\t\"alertname\": \"Resolved1\",\n\t\t\t\t\t\t\t\t\"label1\":    \"value1\",\n\t\t\t\t\t\t\t\t\"label2\":    \"value2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFingerprint:  \"fingerprint3\",\n\t\t\t\t\t\t\tGeneratorURL: \"http://generator3\",\n\t\t\t\t\t\t\tStartsAt:     time.Date(2001, time.January, 1, 0, 0, 0, 0, time.UTC),\n\t\t\t\t\t\t\tEndsAt:       time.Date(2001, time.January, 1, 1, 0, 0, 0, time.UTC),\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tStatus: \"resolved\",\n\t\t\t\t\t\t\tAnnotations: template.KV{\n\t\t\t\t\t\t\t\t\"annotation1\": \"value1\",\n\t\t\t\t\t\t\t\t\"annotation2\": \"value2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tLabels: template.KV{\n\t\t\t\t\t\t\t\t\"alertname\": \"Resolved2\",\n\t\t\t\t\t\t\t\t\"label1\":    \"value1\",\n\t\t\t\t\t\t\t\t\"label2\":    \"value2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tFingerprint:  \"fingerprint4\",\n\t\t\t\t\t\t\tGeneratorURL: \"http://generator4\",\n\t\t\t\t\t\t\tStartsAt:     time.Date(2002, time.January, 1, 0, 0, 0, 0, time.UTC),\n\t\t\t\t\t\t\tEndsAt:       time.Date(2002, time.January, 1, 1, 0, 0, 0, time.UTC),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: map[string]any{\n\t\t\t\t\"alerts\": map[string]any{\n\t\t\t\t\t\"firing\": []any{\n\t\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\t\"status\": \"firing\",\n\t\t\t\t\t\t\t\"labels\": map[string]any{\n\t\t\t\t\t\t\t\t\"alertname\": \"Firing1\",\n\t\t\t\t\t\t\t\t\"label1\":    \"value1\",\n\t\t\t\t\t\t\t\t\"label2\":    \"value2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"annotations\": map[string]any{\n\t\t\t\t\t\t\t\t\"annotation1\": \"value1\",\n\t\t\t\t\t\t\t\t\"annotation2\": \"value2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"startsAt\":     time.Date(2001, time.January, 1, 0, 0, 0, 0, time.UTC).Format(time.RFC3339),\n\t\t\t\t\t\t\t\"endsAt\":       time.Date(2001, time.January, 1, 1, 0, 0, 0, time.UTC).Format(time.RFC3339),\n\t\t\t\t\t\t\t\"fingerprint\":  \"fingerprint1\",\n\t\t\t\t\t\t\t\"generatorURL\": \"http://generator1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\t\"status\": \"firing\",\n\t\t\t\t\t\t\t\"labels\": map[string]any{\n\t\t\t\t\t\t\t\t\"alertname\": \"Firing2\",\n\t\t\t\t\t\t\t\t\"label1\":    \"value1\",\n\t\t\t\t\t\t\t\t\"label2\":    \"value2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"annotations\": map[string]any{\n\t\t\t\t\t\t\t\t\"annotation1\": \"value1\",\n\t\t\t\t\t\t\t\t\"annotation2\": \"value2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"startsAt\":     time.Date(2002, time.January, 1, 0, 0, 0, 0, time.UTC).Format(time.RFC3339),\n\t\t\t\t\t\t\t\"endsAt\":       time.Date(2002, time.January, 1, 1, 0, 0, 0, time.UTC).Format(time.RFC3339),\n\t\t\t\t\t\t\t\"fingerprint\":  \"fingerprint2\",\n\t\t\t\t\t\t\t\"generatorURL\": \"http://generator2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t\"resolved\": []any{\n\t\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\t\"status\": \"resolved\",\n\t\t\t\t\t\t\t\"labels\": map[string]any{\n\t\t\t\t\t\t\t\t\"alertname\": \"Resolved1\",\n\t\t\t\t\t\t\t\t\"label1\":    \"value1\",\n\t\t\t\t\t\t\t\t\"label2\":    \"value2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"annotations\": map[string]any{\n\t\t\t\t\t\t\t\t\"annotation1\": \"value1\",\n\t\t\t\t\t\t\t\t\"annotation2\": \"value2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"startsAt\":     time.Date(2001, time.January, 1, 0, 0, 0, 0, time.UTC).Format(time.RFC3339),\n\t\t\t\t\t\t\t\"endsAt\":       time.Date(2001, time.January, 1, 1, 0, 0, 0, time.UTC).Format(time.RFC3339),\n\t\t\t\t\t\t\t\"fingerprint\":  \"fingerprint3\",\n\t\t\t\t\t\t\t\"generatorURL\": \"http://generator3\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\t\"status\": \"resolved\",\n\t\t\t\t\t\t\t\"labels\": map[string]any{\n\t\t\t\t\t\t\t\t\"alertname\": \"Resolved2\",\n\t\t\t\t\t\t\t\t\"label1\":    \"value1\",\n\t\t\t\t\t\t\t\t\"label2\":    \"value2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"annotations\": map[string]any{\n\t\t\t\t\t\t\t\t\"annotation1\": \"value1\",\n\t\t\t\t\t\t\t\t\"annotation2\": \"value2\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"startsAt\":     time.Date(2002, time.January, 1, 0, 0, 0, 0, time.UTC).Format(time.RFC3339),\n\t\t\t\t\t\t\t\"endsAt\":       time.Date(2002, time.January, 1, 1, 0, 0, 0, time.UTC).Format(time.RFC3339),\n\t\t\t\t\t\t\t\"fingerprint\":  \"fingerprint4\",\n\t\t\t\t\t\t\t\"generatorURL\": \"http://generator4\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t\"num_firing\":   2,\n\t\t\t\t\t\"num_resolved\": 2,\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tn := &Notifier{\n\t\t\t\tconf: &config.PagerdutyConfig{\n\t\t\t\t\tDetails: tt.args.details,\n\t\t\t\t},\n\t\t\t\ttmpl: test.CreateTmpl(t),\n\t\t\t}\n\t\t\tgot, err := n.renderDetails(tt.args.data)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"renderDetails() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "notify/pushover/pushover.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage pushover\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nconst (\n\t// https://pushover.net/api#limits - 250 characters or runes.\n\tmaxTitleLenRunes = 250\n\t// https://pushover.net/api#limits - 1024 characters or runes.\n\tmaxMessageLenRunes = 1024\n\t// https://pushover.net/api#limits - 512 characters or runes.\n\tmaxURLLenRunes = 512\n)\n\n// Notifier implements a Notifier for Pushover notifications.\ntype Notifier struct {\n\tconf    *config.PushoverConfig\n\ttmpl    *template.Template\n\tlogger  *slog.Logger\n\tclient  *http.Client\n\tretrier *notify.Retrier\n\tapiURL  string // for tests.\n}\n\n// New returns a new Pushover notifier.\nfunc New(c *config.PushoverConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {\n\tclient, err := notify.NewClientWithTracing(*c.HTTPConfig, \"pushover\", httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Notifier{\n\t\tconf:    c,\n\t\ttmpl:    t,\n\t\tlogger:  l,\n\t\tclient:  client,\n\t\tretrier: &notify.Retrier{},\n\t\tapiURL:  \"https://api.pushover.net/1/messages.json\",\n\t}, nil\n}\n\n// Notify implements the Notifier interface.\nfunc (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {\n\tkey, ok := notify.GroupKey(ctx)\n\tif !ok {\n\t\treturn false, fmt.Errorf(\"group key missing\")\n\t}\n\tlogger := n.logger.With(\"group_key\", key)\n\tlogger.Debug(\"extracted group key\")\n\n\tdata := notify.GetTemplateData(ctx, n.tmpl, as, logger)\n\n\tvar (\n\t\terr     error\n\t\tmessage string\n\t)\n\ttmpl := notify.TmplText(n.tmpl, data, &err)\n\ttmplHTML := notify.TmplHTML(n.tmpl, data, &err)\n\n\tvar (\n\t\ttoken   string\n\t\tuserKey string\n\t)\n\tif n.conf.Token != \"\" {\n\t\ttoken = string(n.conf.Token)\n\t} else {\n\t\tcontent, err := os.ReadFile(n.conf.TokenFile)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"read token_file: %w\", err)\n\t\t}\n\t\ttoken = string(content)\n\t}\n\tif n.conf.UserKey != \"\" {\n\t\tuserKey = string(n.conf.UserKey)\n\t} else {\n\t\tcontent, err := os.ReadFile(n.conf.UserKeyFile)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"read user_key_file: %w\", err)\n\t\t}\n\t\tuserKey = string(content)\n\t}\n\n\tparameters := url.Values{}\n\tparameters.Add(\"token\", tmpl(token))\n\tparameters.Add(\"user\", tmpl(userKey))\n\n\ttitle, truncated := notify.TruncateInRunes(tmpl(n.conf.Title), maxTitleLenRunes)\n\tif truncated {\n\t\tlogger.Warn(\"Truncated title\", \"incident\", key, \"max_runes\", maxTitleLenRunes)\n\t}\n\tparameters.Add(\"title\", title)\n\n\tif n.conf.HTML {\n\t\tparameters.Add(\"html\", \"1\")\n\t\tmessage = tmplHTML(n.conf.Message)\n\t} else {\n\t\tmessage = tmpl(n.conf.Message)\n\t}\n\n\tif n.conf.Monospace {\n\t\tparameters.Add(\"monospace\", \"1\")\n\t}\n\n\tmessage, truncated = notify.TruncateInRunes(message, maxMessageLenRunes)\n\tif truncated {\n\t\tlogger.Warn(\"Truncated message\", \"incident\", key, \"max_runes\", maxMessageLenRunes)\n\t}\n\tmessage = strings.TrimSpace(message)\n\tif message == \"\" {\n\t\t// Pushover rejects empty messages.\n\t\tmessage = \"(no details)\"\n\t}\n\tparameters.Add(\"message\", message)\n\n\tsupplementaryURL, truncated := notify.TruncateInRunes(tmpl(n.conf.URL), maxURLLenRunes)\n\tif truncated {\n\t\tlogger.Warn(\"Truncated URL\", \"incident\", key, \"max_runes\", maxURLLenRunes)\n\t}\n\tparameters.Add(\"url\", supplementaryURL)\n\tparameters.Add(\"url_title\", tmpl(n.conf.URLTitle))\n\n\tparameters.Add(\"priority\", tmpl(n.conf.Priority))\n\tparameters.Add(\"retry\", fmt.Sprintf(\"%d\", int64(time.Duration(n.conf.Retry).Seconds())))\n\tparameters.Add(\"expire\", fmt.Sprintf(\"%d\", int64(time.Duration(n.conf.Expire).Seconds())))\n\tparameters.Add(\"device\", tmpl(n.conf.Device))\n\tparameters.Add(\"sound\", tmpl(n.conf.Sound))\n\n\tnewttl := int64(time.Duration(n.conf.TTL).Seconds())\n\tif newttl > 0 {\n\t\tparameters.Add(\"ttl\", fmt.Sprintf(\"%d\", newttl))\n\t}\n\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tu, err := url.Parse(n.apiURL)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tu.RawQuery = parameters.Encode()\n\t// Don't log the URL as it contains secret data (see #1825).\n\tlogger.Debug(\"Sending message\", \"incident\", key)\n\tresp, err := notify.PostText(ctx, n.client, u.String(), nil)\n\tif err != nil {\n\t\treturn true, notify.RedactURL(err)\n\t}\n\tdefer notify.Drain(resp)\n\n\tshouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)\n\tif err != nil {\n\t\treturn shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)\n\t}\n\treturn shouldRetry, err\n}\n"
  },
  {
    "path": "notify/pushover/pushover_test.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage pushover\n\nimport (\n\t\"net/http\"\n\t\"os\"\n\t\"testing\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/notify/test\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nfunc TestPushoverRetry(t *testing.T) {\n\tnotifier, err := New(\n\t\t&config.PushoverConfig{\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\tfor statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) {\n\t\tactual, _ := notifier.retrier.Check(statusCode, nil)\n\t\trequire.Equal(t, expected, actual, \"error on status %d\", statusCode)\n\t}\n}\n\nfunc TestPushoverRedactedURL(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tkey, token := \"user_key\", \"token\"\n\tnotifier, err := New(\n\t\t&config.PushoverConfig{\n\t\t\tUserKey:    commoncfg.Secret(key),\n\t\t\tToken:      commoncfg.Secret(token),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\tnotifier.apiURL = u.String()\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, key, token)\n}\n\nfunc TestPushoverReadingUserKeyFromFile(t *testing.T) {\n\tctx, apiURL, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tconst userKey = \"user key\"\n\tf, err := os.CreateTemp(t.TempDir(), \"pushover_user_key\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = f.WriteString(userKey)\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tnotifier, err := New(\n\t\t&config.PushoverConfig{\n\t\t\tUserKeyFile: f.Name(),\n\t\t\tToken:       commoncfg.Secret(\"token\"),\n\t\t\tHTTPConfig:  &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\tnotifier.apiURL = apiURL.String()\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, userKey)\n}\n\nfunc TestPushoverReadingTokenFromFile(t *testing.T) {\n\tctx, apiURL, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tconst token = \"token\"\n\tf, err := os.CreateTemp(t.TempDir(), \"pushover_token\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = f.WriteString(token)\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tnotifier, err := New(\n\t\t&config.PushoverConfig{\n\t\t\tUserKey:    commoncfg.Secret(\"user key\"),\n\t\t\tTokenFile:  f.Name(),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\tnotifier.apiURL = apiURL.String()\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, token)\n}\n\nfunc TestPushoverMonospaceParameter(t *testing.T) {\n\tctx, apiURL, fn := test.GetContextWithCancelingURL(func(w http.ResponseWriter, r *http.Request) {\n\t\trequire.NoError(t, r.ParseForm())\n\t\trequire.Equal(t, \"1\", r.FormValue(\"monospace\"), `expected monospace parameter to be set to \"1\"`)\n\t})\n\tdefer fn()\n\n\tnotifier, err := New(\n\t\t&config.PushoverConfig{\n\t\t\tUserKey:    commoncfg.Secret(\"user_key\"),\n\t\t\tToken:      commoncfg.Secret(\"token\"),\n\t\t\tMonospace:  true,\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\tnotifier.apiURL = apiURL.String()\n\trequire.NoError(t, err)\n\n\t_, err = notifier.Notify(notify.WithGroupKey(ctx, \"1\"), &types.Alert{})\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "notify/rocketchat/rocketchat.go",
    "content": "// Copyright 2022 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage rocketchat\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nconst maxTitleLenRunes = 1024\n\ntype Notifier struct {\n\tconf    *config.RocketchatConfig\n\ttmpl    *template.Template\n\tlogger  *slog.Logger\n\tclient  *http.Client\n\tretrier *notify.Retrier\n\ttoken   string\n\ttokenID string\n\n\tpostJSONFunc func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error)\n}\n\n// PostMessage Payload for postmessage rest API\n//\n// https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage/\ntype Attachment struct {\n\tTitle     string                              `json:\"title,omitempty\"`\n\tTitleLink string                              `json:\"title_link,omitempty\"`\n\tText      string                              `json:\"text,omitempty\"`\n\tImageURL  string                              `json:\"image_url,omitempty\"`\n\tThumbURL  string                              `json:\"thumb_url,omitempty\"`\n\tColor     string                              `json:\"color,omitempty\"`\n\tFields    []config.RocketchatAttachmentField  `json:\"fields,omitempty\"`\n\tActions   []config.RocketchatAttachmentAction `json:\"actions,omitempty\"`\n}\n\n// PostMessage Payload for postmessage rest API\n//\n// https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage/\ntype PostMessage struct {\n\tChannel     string                              `json:\"channel,omitempty\"`\n\tText        string                              `json:\"text,omitempty\"`\n\tParseUrls   bool                                `json:\"parseUrls,omitempty\"`\n\tAlias       string                              `json:\"alias,omitempty\"`\n\tEmoji       string                              `json:\"emoji,omitempty\"`\n\tAvatar      string                              `json:\"avatar,omitempty\"`\n\tAttachments []Attachment                        `json:\"attachments,omitempty\"`\n\tActions     []config.RocketchatAttachmentAction `json:\"actions,omitempty\"`\n}\n\ntype rocketchatRoundTripper struct {\n\twrapped http.RoundTripper\n\ttoken   string\n\ttokenID string\n}\n\nfunc (t *rocketchatRoundTripper) RoundTrip(req *http.Request) (res *http.Response, e error) {\n\treq.Header.Set(\"X-Auth-Token\", t.token)\n\treq.Header.Set(\"X-User-Id\", t.tokenID)\n\treturn t.wrapped.RoundTrip(req)\n}\n\n// New returns a new Rocketchat notification handler.\nfunc New(c *config.RocketchatConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {\n\tclient, err := notify.NewClientWithTracing(*c.HTTPConfig, \"rocketchat\", httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttoken, err := getToken(c)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttokenID, err := getTokenID(c)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.Transport = &rocketchatRoundTripper{wrapped: client.Transport, token: token, tokenID: tokenID}\n\treturn &Notifier{\n\t\tconf:         c,\n\t\ttmpl:         t,\n\t\tlogger:       l,\n\t\tclient:       client,\n\t\tretrier:      &notify.Retrier{},\n\t\tpostJSONFunc: notify.PostJSON,\n\t\ttoken:        token,\n\t\ttokenID:      tokenID,\n\t}, nil\n}\n\nfunc getTokenID(c *config.RocketchatConfig) (string, error) {\n\tif len(c.TokenIDFile) > 0 {\n\t\tcontent, err := os.ReadFile(c.TokenIDFile)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"could not read %s: %w\", c.TokenIDFile, err)\n\t\t}\n\t\treturn strings.TrimSpace(string(content)), nil\n\t}\n\treturn string(*c.TokenID), nil\n}\n\nfunc getToken(c *config.RocketchatConfig) (string, error) {\n\tif len(c.TokenFile) > 0 {\n\t\tcontent, err := os.ReadFile(c.TokenFile)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"could not read %s: %w\", c.TokenFile, err)\n\t\t}\n\t\treturn strings.TrimSpace(string(content)), nil\n\t}\n\treturn string(*c.Token), nil\n}\n\n// Notify implements the Notifier interface.\nfunc (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {\n\tvar err error\n\n\tkey, err := notify.ExtractGroupKey(ctx)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tlogger := n.logger.With(\"group_key\", key)\n\tlogger.Debug(\"extracted group key\")\n\n\tdata := notify.GetTemplateData(ctx, n.tmpl, as, logger)\n\ttmplText := notify.TmplText(n.tmpl, data, &err)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\ttitle := tmplText(n.conf.Title)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\ttitle, truncated := notify.TruncateInRunes(title, maxTitleLenRunes)\n\tif truncated {\n\t\tlogger.Warn(\"Truncated title\", \"max_runes\", maxTitleLenRunes)\n\t}\n\tatt := &Attachment{\n\t\tTitle:     title,\n\t\tTitleLink: tmplText(n.conf.TitleLink),\n\t\tText:      tmplText(n.conf.Text),\n\t\tImageURL:  tmplText(n.conf.ImageURL),\n\t\tThumbURL:  tmplText(n.conf.ThumbURL),\n\t\tColor:     tmplText(n.conf.Color),\n\t}\n\tnumFields := len(n.conf.Fields)\n\tif numFields > 0 {\n\t\tfields := make([]config.RocketchatAttachmentField, numFields)\n\t\tfor index, field := range n.conf.Fields {\n\t\t\t// Check if short was defined for the field otherwise fallback to the global setting\n\t\t\tvar short bool\n\t\t\tif field.Short != nil {\n\t\t\t\tshort = *field.Short\n\t\t\t} else {\n\t\t\t\tshort = n.conf.ShortFields\n\t\t\t}\n\n\t\t\t// Rebuild the field by executing any templates and setting the new value for short\n\t\t\tfields[index] = config.RocketchatAttachmentField{\n\t\t\t\tTitle: tmplText(field.Title),\n\t\t\t\tValue: tmplText(field.Value),\n\t\t\t\tShort: &short,\n\t\t\t}\n\t\t}\n\t\tatt.Fields = fields\n\t}\n\tnumActions := len(n.conf.Actions)\n\tif numActions > 0 {\n\t\tactions := make([]config.RocketchatAttachmentAction, numActions)\n\t\tfor index, action := range n.conf.Actions {\n\t\t\tactions[index] = config.RocketchatAttachmentAction{\n\t\t\t\tType: \"button\", // Only button type is supported\n\t\t\t\tText: tmplText(action.Text),\n\t\t\t\tURL:  tmplText(action.URL),\n\t\t\t\tMsg:  tmplText(action.Msg),\n\t\t\t}\n\t\t}\n\t\tatt.Actions = actions\n\t}\n\n\tbody := &PostMessage{\n\t\tChannel:     tmplText(n.conf.Channel),\n\t\tEmoji:       tmplText(n.conf.Emoji),\n\t\tAvatar:      tmplText(n.conf.IconURL),\n\t\tAttachments: []Attachment{*att},\n\t}\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tvar buf bytes.Buffer\n\tif err := json.NewEncoder(&buf).Encode(body); err != nil {\n\t\treturn false, err\n\t}\n\turl := n.conf.APIURL.JoinPath(\"api/v1/chat.postMessage\").String()\n\tresp, err := n.postJSONFunc(ctx, n.client, url, &buf)\n\tif err != nil {\n\t\treturn true, notify.RedactURL(err)\n\t}\n\tdefer notify.Drain(resp)\n\n\t// Use a retrier to generate an error message for non-200 responses and\n\t// classify them as retriable or not.\n\tretry, err := n.retrier.Check(resp.StatusCode, resp.Body)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"channel %q: %w\", body.Channel, err)\n\t\treturn retry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)\n\t}\n\n\t// Rocketchat web API might return errors with a 200 response code.\n\tretry, err = checkResponseError(resp)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"channel %q: %w\", body.Channel, err)\n\t\treturn retry, notify.NewErrorWithReason(notify.ClientErrorReason, err)\n\t}\n\n\treturn retry, nil\n}\n\n// checkResponseError parses out the error message from Rocketchat API response.\nfunc checkResponseError(resp *http.Response) (bool, error) {\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn true, fmt.Errorf(\"could not read response body: %w\", err)\n\t}\n\n\treturn checkJSONResponseError(body)\n}\n\n// checkJSONResponseError classifies JSON responses from Rocketchat.\nfunc checkJSONResponseError(body []byte) (bool, error) {\n\t// response is for parsing out errors from the JSON response.\n\ttype response struct {\n\t\tSuccess bool   `json:\"success\"`\n\t\tError   string `json:\"error\"`\n\t}\n\n\tvar data response\n\tif err := json.Unmarshal(body, &data); err != nil {\n\t\treturn true, fmt.Errorf(\"could not unmarshal JSON response %q: %w\", string(body), err)\n\t}\n\tif !data.Success {\n\t\treturn false, fmt.Errorf(\"error response from Rocketchat: %s\", data.Error)\n\t}\n\treturn false, nil\n}\n"
  },
  {
    "path": "notify/rocketchat/rocketchat_test.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage rocketchat\n\nimport (\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify/test\"\n)\n\nfunc TestRocketchatRetry(t *testing.T) {\n\tsecret := commoncfg.Secret(\"xxxxx\")\n\tnotifier, err := New(\n\t\t&config.RocketchatConfig{\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\tToken:      &secret,\n\t\t\tTokenID:    &secret,\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\tfor statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) {\n\t\tactual, _ := notifier.retrier.Check(statusCode, nil)\n\t\trequire.Equal(t, expected, actual, \"error on status %d\", statusCode)\n\t}\n}\n\nfunc TestGettingRocketchatTokenFromFile(t *testing.T) {\n\tf, err := os.CreateTemp(t.TempDir(), \"rocketchat_test\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = f.WriteString(\"secret\")\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\t_, err = New(\n\t\t&config.RocketchatConfig{\n\t\t\tTokenFile:   f.Name(),\n\t\t\tTokenIDFile: f.Name(),\n\t\t\tHTTPConfig:  &commoncfg.HTTPClientConfig{},\n\t\t\tAPIURL:      &amcommoncfg.URL{URL: &url.URL{Scheme: \"http\", Host: \"example.com\", Path: \"/api/v1/\"}},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "notify/slack/slack.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage slack\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/nflog\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// https://api.slack.com/reference/messaging/attachments#legacy_fields - 1024, no units given, assuming runes or characters.\nconst maxTitleLenRunes = 1024\n\n// Notifier implements a Notifier for Slack notifications.\ntype Notifier struct {\n\tconf    *config.SlackConfig\n\ttmpl    *template.Template\n\tlogger  *slog.Logger\n\tclient  *http.Client\n\tretrier *notify.Retrier\n\n\tpostJSONFunc func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error)\n}\n\n// New returns a new Slack notification handler.\nfunc New(c *config.SlackConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {\n\tclient, err := notify.NewClientWithTracing(*c.HTTPConfig, \"slack\", httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Notifier{\n\t\tconf:         c,\n\t\ttmpl:         t,\n\t\tlogger:       l,\n\t\tclient:       client,\n\t\tretrier:      &notify.Retrier{},\n\t\tpostJSONFunc: notify.PostJSON,\n\t}, nil\n}\n\n// request is the request for sending a slack notification.\ntype request struct {\n\tChannel     string       `json:\"channel,omitempty\"`\n\tTimestamp   string       `json:\"ts,omitempty\"`\n\tUsername    string       `json:\"username,omitempty\"`\n\tIconEmoji   string       `json:\"icon_emoji,omitempty\"`\n\tIconURL     string       `json:\"icon_url,omitempty\"`\n\tLinkNames   bool         `json:\"link_names,omitempty\"`\n\tText        string       `json:\"text,omitempty\"`\n\tAttachments []attachment `json:\"attachments\"`\n}\n\n// attachment is used to display a richly-formatted message block.\ntype attachment struct {\n\tTitle      string               `json:\"title,omitempty\"`\n\tTitleLink  string               `json:\"title_link,omitempty\"`\n\tPretext    string               `json:\"pretext,omitempty\"`\n\tText       string               `json:\"text\"`\n\tFallback   string               `json:\"fallback\"`\n\tCallbackID string               `json:\"callback_id\"`\n\tFields     []config.SlackField  `json:\"fields,omitempty\"`\n\tActions    []config.SlackAction `json:\"actions,omitempty\"`\n\tImageURL   string               `json:\"image_url,omitempty\"`\n\tThumbURL   string               `json:\"thumb_url,omitempty\"`\n\tFooter     string               `json:\"footer\"`\n\tColor      string               `json:\"color,omitempty\"`\n\tMrkdwnIn   []string             `json:\"mrkdwn_in,omitempty\"`\n}\n\n// slackResponse represents the response from Slack API.\ntype slackResponse struct {\n\tOK        bool   `json:\"ok\"`\n\tError     string `json:\"error,omitempty\"`\n\tChannel   string `json:\"channel,omitempty\"`\n\tTimestamp string `json:\"ts,omitempty\"`\n}\n\n// Notify implements the Notifier interface.\nfunc (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {\n\tvar err error\n\tkey, err := notify.ExtractGroupKey(ctx)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tlogger := n.logger.With(\"group_key\", key)\n\tlogger.Debug(\"extracted group key\")\n\n\tvar (\n\t\tdata     = notify.GetTemplateData(ctx, n.tmpl, as, logger)\n\t\ttmplText = notify.TmplText(n.tmpl, data, &err)\n\t)\n\tvar markdownIn []string\n\n\tif len(n.conf.MrkdwnIn) == 0 {\n\t\tmarkdownIn = []string{\"fallback\", \"pretext\", \"text\"}\n\t} else {\n\t\tmarkdownIn = n.conf.MrkdwnIn\n\t}\n\n\ttitle, truncated := notify.TruncateInRunes(tmplText(n.conf.Title), maxTitleLenRunes)\n\tif truncated {\n\t\tlogger.Warn(\"Truncated title\", \"max_runes\", maxTitleLenRunes)\n\t}\n\tatt := &attachment{\n\t\tTitle:      title,\n\t\tTitleLink:  tmplText(n.conf.TitleLink),\n\t\tPretext:    tmplText(n.conf.Pretext),\n\t\tText:       tmplText(n.conf.Text),\n\t\tFallback:   tmplText(n.conf.Fallback),\n\t\tCallbackID: tmplText(n.conf.CallbackID),\n\t\tImageURL:   tmplText(n.conf.ImageURL),\n\t\tThumbURL:   tmplText(n.conf.ThumbURL),\n\t\tFooter:     tmplText(n.conf.Footer),\n\t\tColor:      tmplText(n.conf.Color),\n\t\tMrkdwnIn:   markdownIn,\n\t}\n\n\tnumFields := len(n.conf.Fields)\n\tif numFields > 0 {\n\t\tfields := make([]config.SlackField, numFields)\n\t\tfor index, field := range n.conf.Fields {\n\t\t\t// Check if short was defined for the field otherwise fallback to the global setting\n\t\t\tvar short bool\n\t\t\tif field.Short != nil {\n\t\t\t\tshort = *field.Short\n\t\t\t} else {\n\t\t\t\tshort = n.conf.ShortFields\n\t\t\t}\n\n\t\t\t// Rebuild the field by executing any templates and setting the new value for short\n\t\t\tfields[index] = config.SlackField{\n\t\t\t\tTitle: tmplText(field.Title),\n\t\t\t\tValue: tmplText(field.Value),\n\t\t\t\tShort: &short,\n\t\t\t}\n\t\t}\n\t\tatt.Fields = fields\n\t}\n\n\tnumActions := len(n.conf.Actions)\n\tif numActions > 0 {\n\t\tactions := make([]config.SlackAction, numActions)\n\t\tfor index, action := range n.conf.Actions {\n\t\t\tslackAction := config.SlackAction{\n\t\t\t\tType:  tmplText(action.Type),\n\t\t\t\tText:  tmplText(action.Text),\n\t\t\t\tURL:   tmplText(action.URL),\n\t\t\t\tStyle: tmplText(action.Style),\n\t\t\t\tName:  tmplText(action.Name),\n\t\t\t\tValue: tmplText(action.Value),\n\t\t\t}\n\n\t\t\tif action.ConfirmField != nil {\n\t\t\t\tslackAction.ConfirmField = &config.SlackConfirmationField{\n\t\t\t\t\tTitle:       tmplText(action.ConfirmField.Title),\n\t\t\t\t\tText:        tmplText(action.ConfirmField.Text),\n\t\t\t\t\tOkText:      tmplText(action.ConfirmField.OkText),\n\t\t\t\t\tDismissText: tmplText(action.ConfirmField.DismissText),\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tactions[index] = slackAction\n\t\t}\n\t\tatt.Actions = actions\n\t}\n\n\tvar u string\n\tif n.conf.APIURL != nil {\n\t\tu = n.conf.APIURL.String()\n\t} else {\n\t\tcontent, err := os.ReadFile(n.conf.APIURLFile)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tu = strings.TrimSpace(string(content))\n\t}\n\n\tif n.conf.Timeout > 0 {\n\t\tpostCtx, cancel := context.WithTimeoutCause(ctx, n.conf.Timeout, fmt.Errorf(\"configured slack timeout reached (%s)\", n.conf.Timeout))\n\t\tdefer cancel()\n\t\tctx = postCtx\n\t}\n\n\treq := &request{\n\t\tChannel:     tmplText(n.conf.Channel),\n\t\tUsername:    tmplText(n.conf.Username),\n\t\tIconEmoji:   tmplText(n.conf.IconEmoji),\n\t\tIconURL:     tmplText(n.conf.IconURL),\n\t\tLinkNames:   n.conf.LinkNames,\n\t\tText:        tmplText(n.conf.MessageText),\n\t\tAttachments: []attachment{*att},\n\t}\n\n\t// If a notification for this alert group has already been sent and `update_message` config is set\n\t// edit API endpoint and payload to update notification instead of sending a new one.\n\tvar store *nflog.Store\n\n\tif n.conf.UpdateMessage {\n\t\tvar ok bool\n\t\tstore, ok = notify.NflogStore(ctx)\n\t\tif !ok {\n\t\t\tlogger.Warn(\"cannot create NflogStore, updatable messages will be disabled.\")\n\t\t} else {\n\t\t\tthreadTs, _ := store.GetStr(\"threadTs\")\n\t\t\tchannelId, _ := store.GetStr(\"channelId\")\n\t\t\tlogger.Debug(\"attempt recovering threadTs and channelId to update an existing message\", \"threadTs\", threadTs, \"channelId\", channelId)\n\t\t\tif threadTs != \"\" && channelId != \"\" {\n\t\t\t\tu = \"https://slack.com/api/chat.update\"\n\t\t\t\treq.Timestamp = threadTs\n\t\t\t\treq.Channel = channelId\n\t\t\t\tlogger.Debug(\"updating previously sent message\", \"threadTs\", threadTs, \"channelId\", channelId)\n\t\t\t}\n\t\t}\n\t}\n\tvar buf bytes.Buffer\n\tif err := json.NewEncoder(&buf).Encode(req); err != nil {\n\t\treturn false, err\n\t}\n\n\tresp, err := n.postJSONFunc(ctx, n.client, u, &buf)\n\tif err != nil {\n\t\tif ctx.Err() != nil {\n\t\t\terr = fmt.Errorf(\"%w: %w\", err, context.Cause(ctx))\n\t\t}\n\t\treturn true, notify.RedactURL(err)\n\t}\n\tdefer notify.Drain(resp)\n\n\t// Use a retrier to generate an error message for non-200 responses and\n\t// classify them as retriable or not.\n\tretry, err := n.retrier.Check(resp.StatusCode, resp.Body)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"channel %q: %w\", req.Channel, err)\n\t\treturn retry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)\n\t}\n\n\tretry, err = n.slackResponseHandler(resp, store)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"channel %q: %w\", req.Channel, err)\n\t\treturn retry, notify.NewErrorWithReason(notify.ClientErrorReason, err)\n\t}\n\treturn retry, nil\n}\n\n// slackResponseHandler parses the response body of the request, handles retryable errors\n// and saves the response timestamp and channelId to nflog.\nfunc (n *Notifier) slackResponseHandler(resp *http.Response, store *nflog.Store) (bool, error) {\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn true, fmt.Errorf(\"could not read response body: %w\", err)\n\t}\n\tif !strings.HasPrefix(resp.Header.Get(\"Content-Type\"), \"application/json\") {\n\t\treturn checkTextResponseError(body)\n\t}\n\tvar data slackResponse\n\tif err := json.Unmarshal(body, &data); err != nil {\n\t\treturn true, fmt.Errorf(\"could not unmarshal JSON response %q: %w\", string(body), err)\n\t}\n\tif !data.OK {\n\t\treturn false, fmt.Errorf(\"error response from Slack: %s\", data.Error)\n\t}\n\t// If store, TS and Channel are set, store the threadTS and channelId\n\tif store != nil && data.Timestamp != \"\" && data.Channel != \"\" {\n\t\tstore.SetStr(\"threadTs\", data.Timestamp)\n\t\tstore.SetStr(\"channelId\", data.Channel)\n\t\tn.logger.Debug(\"stored threadTs and channelId\", \"threadTs\", data.Timestamp, \"channelId\", data.Channel)\n\t}\n\treturn false, nil\n}\n\n// checkTextResponseError classifies plaintext responses from Slack.\n// A plaintext (non-JSON) response is successful if it's a string \"ok\".\n// This is typically a response for an Incoming Webhook\n// (https://api.slack.com/messaging/webhooks#handling_errors)\nfunc checkTextResponseError(body []byte) (bool, error) {\n\tif !bytes.Equal(body, []byte(\"ok\")) {\n\t\treturn false, fmt.Errorf(\"received an error response from Slack: %s\", string(body))\n\t}\n\treturn false, nil\n}\n"
  },
  {
    "path": "notify/slack/slack_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage slack\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/notify/test\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nfunc TestSlackRetry(t *testing.T) {\n\tnotifier, err := New(\n\t\t&config.SlackConfig{\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\tfor statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) {\n\t\tactual, _ := notifier.retrier.Check(statusCode, nil)\n\t\trequire.Equal(t, expected, actual, \"error on status %d\", statusCode)\n\t}\n}\n\nfunc TestSlackRedactedURL(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tnotifier, err := New(\n\t\t&config.SlackConfig{\n\t\t\tAPIURL:     &amcommoncfg.SecretURL{URL: u},\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String())\n}\n\nfunc TestGettingSlackURLFromFile(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tf, err := os.CreateTemp(t.TempDir(), \"slack_test\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = f.WriteString(u.String())\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tnotifier, err := New(\n\t\t&config.SlackConfig{\n\t\t\tAPIURLFile: f.Name(),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String())\n}\n\nfunc TestTrimmingSlackURLFromFile(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tf, err := os.CreateTemp(t.TempDir(), \"slack_test_newline\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = f.WriteString(u.String() + \"\\n\\n\")\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tnotifier, err := New(\n\t\t&config.SlackConfig{\n\t\t\tAPIURLFile: f.Name(),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String())\n}\n\nfunc TestNotifier_Notify_WithReason(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tstatusCode     int\n\t\tresponseBody   string\n\t\texpectedReason notify.Reason\n\t\texpectedErr    string\n\t\texpectedRetry  bool\n\t\tnoError        bool\n\t}{\n\t\t{\n\t\t\tname:           \"with a 4xx status code\",\n\t\t\tstatusCode:     http.StatusUnauthorized,\n\t\t\texpectedReason: notify.ClientErrorReason,\n\t\t\texpectedRetry:  false,\n\t\t\texpectedErr:    \"unexpected status code 401\",\n\t\t},\n\t\t{\n\t\t\tname:           \"with a 5xx status code\",\n\t\t\tstatusCode:     http.StatusInternalServerError,\n\t\t\texpectedReason: notify.ServerErrorReason,\n\t\t\texpectedRetry:  true,\n\t\t\texpectedErr:    \"unexpected status code 500\",\n\t\t},\n\t\t{\n\t\t\tname:           \"with a 3xx status code\",\n\t\t\tstatusCode:     http.StatusTemporaryRedirect,\n\t\t\texpectedReason: notify.DefaultReason,\n\t\t\texpectedRetry:  false,\n\t\t\texpectedErr:    \"unexpected status code 307\",\n\t\t},\n\t\t{\n\t\t\tname:           \"with a 1xx status code\",\n\t\t\tstatusCode:     http.StatusSwitchingProtocols,\n\t\t\texpectedReason: notify.DefaultReason,\n\t\t\texpectedRetry:  false,\n\t\t\texpectedErr:    \"unexpected status code 101\",\n\t\t},\n\t\t{\n\t\t\tname:           \"2xx response with invalid JSON\",\n\t\t\tstatusCode:     http.StatusOK,\n\t\t\tresponseBody:   `{\"not valid json\"}`,\n\t\t\texpectedReason: notify.ClientErrorReason,\n\t\t\texpectedRetry:  true,\n\t\t\texpectedErr:    \"could not unmarshal\",\n\t\t},\n\t\t{\n\t\t\tname:           \"2xx response with a JSON error\",\n\t\t\tstatusCode:     http.StatusOK,\n\t\t\tresponseBody:   `{\"ok\":false,\"error\":\"error_message\"}`,\n\t\t\texpectedReason: notify.ClientErrorReason,\n\t\t\texpectedRetry:  false,\n\t\t\texpectedErr:    \"error response from Slack: error_message\",\n\t\t},\n\t\t{\n\t\t\tname:           \"2xx response with a plaintext error\",\n\t\t\tstatusCode:     http.StatusOK,\n\t\t\tresponseBody:   \"no_channel\",\n\t\t\texpectedReason: notify.ClientErrorReason,\n\t\t\texpectedRetry:  false,\n\t\t\texpectedErr:    \"error response from Slack: no_channel\",\n\t\t},\n\t\t{\n\t\t\tname:         \"successful JSON response\",\n\t\t\tstatusCode:   http.StatusOK,\n\t\t\tresponseBody: `{\"ok\":true}`,\n\t\t\tnoError:      true,\n\t\t},\n\t\t{\n\t\t\tname:         \"successful plaintext response\",\n\t\t\tstatusCode:   http.StatusOK,\n\t\t\tresponseBody: \"ok\",\n\t\t\tnoError:      true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tapiurl, _ := url.Parse(\"https://slack.com/post.Message\")\n\t\t\tnotifier, err := New(\n\t\t\t\t&config.SlackConfig{\n\t\t\t\t\tNotifierConfig: amcommoncfg.NotifierConfig{},\n\t\t\t\t\tHTTPConfig:     &commoncfg.HTTPClientConfig{},\n\t\t\t\t\tAPIURL:         &amcommoncfg.SecretURL{URL: apiurl},\n\t\t\t\t\tChannel:        \"channelname\",\n\t\t\t\t},\n\t\t\t\ttest.CreateTmpl(t),\n\t\t\t\tpromslog.NewNopLogger(),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tnotifier.postJSONFunc = func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error) {\n\t\t\t\tresp := httptest.NewRecorder()\n\t\t\t\tif strings.HasPrefix(tt.responseBody, \"{\") {\n\t\t\t\t\tresp.Header().Add(\"Content-Type\", \"application/json; charset=utf-8\")\n\t\t\t\t}\n\t\t\t\tresp.WriteHeader(tt.statusCode)\n\t\t\t\tresp.WriteString(tt.responseBody)\n\t\t\t\treturn resp.Result(), nil\n\t\t\t}\n\t\t\tctx := context.Background()\n\t\t\tctx = notify.WithGroupKey(ctx, \"1\")\n\n\t\t\talert1 := &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t},\n\t\t\t}\n\t\t\tretry, err := notifier.Notify(ctx, alert1)\n\t\t\trequire.Equal(t, tt.expectedRetry, retry)\n\t\t\tif tt.noError {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tvar reasonError *notify.ErrorWithReason\n\t\t\t\trequire.ErrorAs(t, err, &reasonError)\n\t\t\t\trequire.Equal(t, tt.expectedReason, reasonError.Reason)\n\t\t\t\trequire.Contains(t, err.Error(), tt.expectedErr)\n\t\t\t\trequire.Contains(t, err.Error(), \"channelname\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSlackTimeout(t *testing.T) {\n\ttests := map[string]struct {\n\t\tlatency time.Duration\n\t\ttimeout time.Duration\n\t\twantErr bool\n\t}{\n\t\t\"success\": {latency: 100 * time.Millisecond, timeout: 120 * time.Millisecond, wantErr: false},\n\t\t\"error\":   {latency: 100 * time.Millisecond, timeout: 80 * time.Millisecond, wantErr: true},\n\t}\n\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tu, _ := url.Parse(\"https://slack.com/post.Message\")\n\t\t\tnotifier, err := New(\n\t\t\t\t&config.SlackConfig{\n\t\t\t\t\tNotifierConfig: amcommoncfg.NotifierConfig{},\n\t\t\t\t\tHTTPConfig:     &commoncfg.HTTPClientConfig{},\n\t\t\t\t\tAPIURL:         &amcommoncfg.SecretURL{URL: u},\n\t\t\t\t\tChannel:        \"channelname\",\n\t\t\t\t\tTimeout:        tt.timeout,\n\t\t\t\t},\n\t\t\t\ttest.CreateTmpl(t),\n\t\t\t\tpromslog.NewNopLogger(),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\t\t\tnotifier.postJSONFunc = func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error) {\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn nil, ctx.Err()\n\t\t\t\tcase <-time.After(tt.latency):\n\t\t\t\t\tresp := httptest.NewRecorder()\n\t\t\t\t\tresp.Header().Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\t\t\t\t\tresp.WriteHeader(http.StatusOK)\n\t\t\t\t\tresp.WriteString(`{\"ok\":true}`)\n\n\t\t\t\t\treturn resp.Result(), nil\n\t\t\t\t}\n\t\t\t}\n\t\t\tctx := context.Background()\n\t\t\tctx = notify.WithGroupKey(ctx, \"1\")\n\n\t\t\talert := &types.Alert{\n\t\t\t\tAlert: model.Alert{\n\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t},\n\t\t\t}\n\t\t\t_, err = notifier.Notify(ctx, alert)\n\t\t\trequire.Equal(t, tt.wantErr, err != nil)\n\t\t})\n\t}\n}\n\nfunc TestSlackMessageField(t *testing.T) {\n\t// 1. Setup a fake Slack server\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tvar body map[string]any\n\t\tif err := json.NewDecoder(r.Body).Decode(&body); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\t// 2. VERIFY: Top-level text exists\n\t\tif body[\"text\"] != \"My Top Level Message\" {\n\t\t\tt.Errorf(\"Expected top-level 'text' to be 'My Top Level Message', got %v\", body[\"text\"])\n\t\t}\n\n\t\t// 3. VERIFY: Old attachments still exist\n\t\tattachments, ok := body[\"attachments\"].([]any)\n\t\tif !ok || len(attachments) == 0 {\n\t\t\tt.Errorf(\"Expected attachments to exist\")\n\t\t} else {\n\t\t\tfirst := attachments[0].(map[string]any)\n\t\t\tif first[\"title\"] != \"Old Attachment Title\" {\n\t\t\t\tt.Errorf(\"Expected attachment title 'Old Attachment Title', got %v\", first[\"title\"])\n\t\t\t}\n\t\t}\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tw.Write([]byte(`{\"ok\": true}`))\n\t}))\n\tdefer server.Close()\n\n\t// 4. Configure Notifier with BOTH new and old fields\n\tu, _ := url.Parse(server.URL)\n\tconf := &config.SlackConfig{\n\t\tAPIURL:      &amcommoncfg.SecretURL{URL: u},\n\t\tMessageText: \"My Top Level Message\", // Your NEW field\n\t\tTitle:       \"Old Attachment Title\", // An OLD field\n\t\tChannel:     \"#test-channel\",\n\t\tHTTPConfig:  &commoncfg.HTTPClientConfig{},\n\t}\n\n\ttmpl, err := template.FromGlobs([]string{})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttmpl.ExternalURL = u\n\n\tlogger := slog.New(slog.DiscardHandler)\n\tnotifier, err := New(conf, tmpl, logger)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tctx := context.Background()\n\tctx = notify.WithGroupKey(ctx, \"test-group-key\")\n\n\tif _, err := notifier.Notify(ctx); err != nil {\n\t\tt.Fatal(\"Notify failed:\", err)\n\t}\n}\n"
  },
  {
    "path": "notify/sns/sns.go",
    "content": "// Copyright 2021 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage sns\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials/stscreds\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sns\"\n\tsnstypes \"github.com/aws/aws-sdk-go-v2/service/sns/types\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sts\"\n\t\"github.com/aws/smithy-go\"\n\tsmithyhttp \"github.com/aws/smithy-go/transport/http\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// Notifier implements a Notifier for SNS notifications.\ntype Notifier struct {\n\tconf    *config.SNSConfig\n\ttmpl    *template.Template\n\tlogger  *slog.Logger\n\tclient  *http.Client\n\tretrier *notify.Retrier\n}\n\n// New returns a new SNS notification handler.\nfunc New(c *config.SNSConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {\n\tclient, err := notify.NewClientWithTracing(*c.HTTPConfig, \"sns\", httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Notifier{\n\t\tconf:    c,\n\t\ttmpl:    t,\n\t\tlogger:  l,\n\t\tclient:  client,\n\t\tretrier: &notify.Retrier{},\n\t}, nil\n}\n\nfunc (n *Notifier) Notify(ctx context.Context, alert ...*types.Alert) (bool, error) {\n\tvar (\n\t\ttmplErr error\n\t\tdata    = notify.GetTemplateData(ctx, n.tmpl, alert, n.logger)\n\t\ttmpl    = notify.TmplText(n.tmpl, data, &tmplErr)\n\t)\n\n\tclient, err := n.createSNSClient(ctx, tmpl, &tmplErr)\n\tif err != nil {\n\t\t// V2 error handling is different. We don't have awserr.RequestFailure.\n\t\t// We can check for a generic smithy.APIError to see if it's a service error.\n\t\tvar apiErr smithy.APIError\n\t\tif errors.As(err, &apiErr) {\n\t\t\t// To maintain compatibility with the retrier, we attempt to get an HTTP status code.\n\t\t\tvar respErr *smithyhttp.ResponseError\n\t\t\tif errors.As(err, &respErr) && respErr.Response != nil {\n\t\t\t\treturn n.retrier.Check(respErr.Response.StatusCode, strings.NewReader(apiErr.ErrorMessage()))\n\t\t\t}\n\t\t\t// Fallback if we can't get a status code.\n\t\t\treturn true, fmt.Errorf(\"failed to create SNS client: %s: %s\", apiErr.ErrorCode(), apiErr.ErrorMessage())\n\t\t}\n\t\treturn true, err\n\t}\n\n\tpublishInput, err := n.createPublishInput(ctx, tmpl, &tmplErr)\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tpublishOutput, err := client.Publish(ctx, publishInput)\n\tif err != nil {\n\t\t// V2 error handling uses errors.As to inspect the error chain.\n\t\tvar apiErr smithy.APIError\n\t\tif errors.As(err, &apiErr) {\n\t\t\tvar statusCode int\n\t\t\tvar respErr *smithyhttp.ResponseError\n\t\t\t// Try to extract the HTTP status code for the retrier.\n\t\t\tif errors.As(err, &respErr) && respErr.Response != nil {\n\t\t\t\tstatusCode = respErr.Response.StatusCode\n\t\t\t}\n\n\t\t\t// If we got a status code, use the retrier logic.\n\t\t\tif statusCode != 0 {\n\t\t\t\tretryable, checkErr := n.retrier.Check(statusCode, strings.NewReader(apiErr.ErrorMessage()))\n\t\t\t\treasonErr := notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(statusCode), checkErr)\n\t\t\t\treturn retryable, reasonErr\n\t\t\t}\n\t\t}\n\t\t// Fallback for non-API errors or if status code extraction fails.\n\t\treturn true, err\n\t}\n\n\tn.logger.Debug(\"SNS message successfully published\", \"message_id\", aws.ToString(publishOutput.MessageId), \"sequence_number\", aws.ToString(publishOutput.SequenceNumber))\n\n\treturn false, nil\n}\n\nfunc (n *Notifier) createSNSClient(ctx context.Context, tmpl func(string) string, tmplErr *error) (*sns.Client, error) {\n\t// Base configuration options that apply to both STS (if used) and the final SNS client.\n\tbaseCfgOpts := []func(*awsconfig.LoadOptions) error{\n\t\tawsconfig.WithHTTPClient(n.client),\n\t\tawsconfig.WithRegion(n.conf.Sigv4.Region),\n\t}\n\tif n.conf.Sigv4.Profile != \"\" {\n\t\tbaseCfgOpts = append(baseCfgOpts, awsconfig.WithSharedConfigProfile(n.conf.Sigv4.Profile))\n\t}\n\tif n.conf.Sigv4.AccessKey != \"\" {\n\t\tcreds := credentials.NewStaticCredentialsProvider(n.conf.Sigv4.AccessKey, string(n.conf.Sigv4.SecretKey), \"\")\n\t\tbaseCfgOpts = append(baseCfgOpts, awsconfig.WithCredentialsProvider(creds))\n\t}\n\n\t// Final configuration options for the SNS client.\n\tsnsCfgOpts := baseCfgOpts\n\n\t// If a RoleARN is provided, create an STS client to assume the role.\n\t// This uses a separate config load to ensure the STS client does not use a custom SNS endpoint.\n\tif n.conf.Sigv4.RoleARN != \"\" {\n\t\tstsCfg, err := awsconfig.LoadDefaultConfig(ctx, baseCfgOpts...)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to load base config for STS: %w\", err)\n\t\t}\n\t\tstsClient := sts.NewFromConfig(stsCfg)\n\t\tstsProvider := stscreds.NewAssumeRoleProvider(stsClient, n.conf.Sigv4.RoleARN)\n\t\t// Add the AssumeRole provider to the options for the SNS client config.\n\t\tsnsCfgOpts = append(snsCfgOpts, awsconfig.WithCredentialsProvider(aws.NewCredentialsCache(stsProvider)))\n\t}\n\n\t// Resolve the API URL from the template.\n\tapiURL := tmpl(n.conf.APIUrl)\n\tif *tmplErr != nil {\n\t\treturn nil, notify.NewErrorWithReason(notify.ClientErrorReason, fmt.Errorf(\"execute 'api_url' template: %w\", *tmplErr))\n\t}\n\tif apiURL != \"\" {\n\t\tsnsCfgOpts = append(snsCfgOpts, awsconfig.WithBaseEndpoint(apiURL))\n\t}\n\n\t// Load the final configuration for the SNS client.\n\tsnsCfg, err := awsconfig.LoadDefaultConfig(ctx, snsCfgOpts...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to load final config for SNS: %w\", err)\n\t}\n\n\t// We will always need a region to be set.\n\tif snsCfg.Region == \"\" {\n\t\treturn nil, fmt.Errorf(\"region not configured in sns.sigv4.region or in default credentials chain\")\n\t}\n\n\treturn sns.NewFromConfig(snsCfg), nil\n}\n\nfunc (n *Notifier) createPublishInput(ctx context.Context, tmpl func(string) string, tmplErr *error) (*sns.PublishInput, error) {\n\tpublishInput := &sns.PublishInput{}\n\tmessageAttributes := n.createMessageAttributes(tmpl)\n\tif *tmplErr != nil {\n\t\treturn nil, notify.NewErrorWithReason(notify.ClientErrorReason, fmt.Errorf(\"execute 'attributes' template: %w\", *tmplErr))\n\t}\n\n\t// Max message size for a message in an SNS publish request is 256KB,\n\t// except for SMS messages where the limit is 1600 characters/runes.\n\tmessageSizeLimit := 256 * 1024\n\tif n.conf.TopicARN != \"\" {\n\t\ttopicARN := tmpl(n.conf.TopicARN)\n\t\tif *tmplErr != nil {\n\t\t\treturn nil, notify.NewErrorWithReason(notify.ClientErrorReason, fmt.Errorf(\"execute 'topic_arn' template: %w\", *tmplErr))\n\t\t}\n\t\tpublishInput.TopicArn = aws.String(topicARN)\n\t\t// If we are using a topic ARN, it could be a FIFO topic specified by the topic's suffix \".fifo\".\n\t\tif strings.HasSuffix(topicARN, \".fifo\") {\n\t\t\tkey, err := notify.ExtractGroupKey(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tpublishInput.MessageDeduplicationId = aws.String(key.Hash())\n\t\t\tpublishInput.MessageGroupId = aws.String(key.Hash())\n\t\t}\n\t}\n\tif n.conf.PhoneNumber != \"\" {\n\t\tpublishInput.PhoneNumber = aws.String(tmpl(n.conf.PhoneNumber))\n\t\tif *tmplErr != nil {\n\t\t\treturn nil, notify.NewErrorWithReason(notify.ClientErrorReason, fmt.Errorf(\"execute 'phone_number' template: %w\", *tmplErr))\n\t\t}\n\t\t// If we have an SMS message, we need to truncate to 1600 characters/runes.\n\t\tmessageSizeLimit = 1600\n\t}\n\tif n.conf.TargetARN != \"\" {\n\t\tpublishInput.TargetArn = aws.String(tmpl(n.conf.TargetARN))\n\t\tif *tmplErr != nil {\n\t\t\treturn nil, notify.NewErrorWithReason(notify.ClientErrorReason, fmt.Errorf(\"execute 'target_arn' template: %w\", *tmplErr))\n\t\t}\n\t}\n\n\ttmplMessage := tmpl(n.conf.Message)\n\tif *tmplErr != nil {\n\t\treturn nil, notify.NewErrorWithReason(notify.ClientErrorReason, fmt.Errorf(\"execute 'message' template: %w\", *tmplErr))\n\t}\n\tmessageToSend, isTrunc, err := validateAndTruncateMessage(tmplMessage, messageSizeLimit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif isTrunc {\n\t\t// If we truncated the message we need to add a message attribute showing that it was truncated.\n\t\tmessageAttributes[\"truncated\"] = snstypes.MessageAttributeValue{DataType: aws.String(\"String\"), StringValue: aws.String(\"true\")}\n\t}\n\n\tpublishInput.Message = aws.String(messageToSend)\n\tpublishInput.MessageAttributes = messageAttributes\n\n\tif n.conf.Subject != \"\" {\n\t\tpublishInput.Subject = aws.String(tmpl(n.conf.Subject))\n\t\tif *tmplErr != nil {\n\t\t\treturn nil, notify.NewErrorWithReason(notify.ClientErrorReason, fmt.Errorf(\"execute 'subject' template: %w\", *tmplErr))\n\t\t}\n\t}\n\n\treturn publishInput, nil\n}\n\nfunc validateAndTruncateMessage(message string, maxMessageSizeInBytes int) (string, bool, error) {\n\tif !utf8.ValidString(message) {\n\t\treturn \"\", false, fmt.Errorf(\"non utf8 encoded message string\")\n\t}\n\tif len(message) <= maxMessageSizeInBytes {\n\t\treturn message, false, nil\n\t}\n\t// If the message is larger than our specified size we have to truncate.\n\ttruncated := make([]byte, maxMessageSizeInBytes)\n\tcopy(truncated, message)\n\treturn string(truncated), true, nil\n}\n\nfunc (n *Notifier) createMessageAttributes(tmpl func(string) string) map[string]snstypes.MessageAttributeValue {\n\t// Convert the given attributes map into the AWS Message Attributes Format.\n\tattributes := make(map[string]snstypes.MessageAttributeValue, len(n.conf.Attributes))\n\tfor k, v := range n.conf.Attributes {\n\t\tattributes[tmpl(k)] = snstypes.MessageAttributeValue{DataType: aws.String(\"String\"), StringValue: aws.String(tmpl(v))}\n\t}\n\treturn attributes\n}\n"
  },
  {
    "path": "notify/sns/sns_test.go",
    "content": "// Copyright 2021 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage sns\n\nimport (\n\t\"context\"\n\t\"net/url\"\n\t\"testing\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/prometheus/sigv4\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nvar logger = promslog.NewNopLogger()\n\nfunc TestValidateAndTruncateMessage(t *testing.T) {\n\tsBuff := make([]byte, 257*1024)\n\tfor i := range sBuff {\n\t\tsBuff[i] = byte(33)\n\t}\n\ttruncatedMessage, isTruncated, err := validateAndTruncateMessage(string(sBuff), 256*1024)\n\trequire.True(t, isTruncated)\n\trequire.NoError(t, err)\n\trequire.NotEqual(t, sBuff, truncatedMessage)\n\trequire.Len(t, truncatedMessage, 256*1024)\n\n\tsBuff = make([]byte, 100)\n\tfor i := range sBuff {\n\t\tsBuff[i] = byte(33)\n\t}\n\ttruncatedMessage, isTruncated, err = validateAndTruncateMessage(string(sBuff), 100)\n\trequire.False(t, isTruncated)\n\trequire.NoError(t, err)\n\trequire.Equal(t, string(sBuff), truncatedMessage)\n\n\tinvalidUtf8String := \"\\xc3\\x28\"\n\t_, _, err = validateAndTruncateMessage(invalidUtf8String, 100)\n\trequire.Error(t, err)\n}\n\nfunc TestNotifyWithInvalidTemplate(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\ttitle     string\n\t\terrMsg    string\n\t\tupdateCfg func(*config.SNSConfig)\n\t}{\n\t\t{\n\t\t\ttitle:  \"with invalid Attribute template\",\n\t\t\terrMsg: \"execute 'attributes' template\",\n\t\t\tupdateCfg: func(cfg *config.SNSConfig) {\n\t\t\t\tcfg.Attributes = map[string]string{\n\t\t\t\t\t\"attribName1\": \"{{ template \\\"unknown_template\\\" . }}\",\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle:  \"with invalid TopicArn template\",\n\t\t\terrMsg: \"execute 'topic_arn' template\",\n\t\t\tupdateCfg: func(cfg *config.SNSConfig) {\n\t\t\t\tcfg.TopicARN = \"{{ template \\\"unknown_template\\\" . }}\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle:  \"with invalid PhoneNumber template\",\n\t\t\terrMsg: \"execute 'phone_number' template\",\n\t\t\tupdateCfg: func(cfg *config.SNSConfig) {\n\t\t\t\tcfg.PhoneNumber = \"{{ template \\\"unknown_template\\\" . }}\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle:  \"with invalid Message template\",\n\t\t\terrMsg: \"execute 'message' template\",\n\t\t\tupdateCfg: func(cfg *config.SNSConfig) {\n\t\t\t\tcfg.Message = \"{{ template \\\"unknown_template\\\" . }}\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle:  \"with invalid Subject template\",\n\t\t\terrMsg: \"execute 'subject' template\",\n\t\t\tupdateCfg: func(cfg *config.SNSConfig) {\n\t\t\t\tcfg.Subject = \"{{ template \\\"unknown_template\\\" . }}\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle:  \"with invalid APIUrl template\",\n\t\t\terrMsg: \"execute 'api_url' template\",\n\t\t\tupdateCfg: func(cfg *config.SNSConfig) {\n\t\t\t\tcfg.APIUrl = \"{{ template \\\"unknown_template\\\" . }}\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle:  \"with invalid TargetARN template\",\n\t\t\terrMsg: \"execute 'target_arn' template\",\n\t\t\tupdateCfg: func(cfg *config.SNSConfig) {\n\t\t\t\tcfg.TargetARN = \"{{ template \\\"unknown_template\\\" . }}\"\n\t\t\t},\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\tsnsCfg := &config.SNSConfig{\n\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\t\tTopicARN:   \"TestTopic\",\n\t\t\t\tSigv4: sigv4.SigV4Config{\n\t\t\t\t\tRegion: \"us-west-2\",\n\t\t\t\t},\n\t\t\t}\n\t\t\tif tc.updateCfg != nil {\n\t\t\t\ttc.updateCfg(snsCfg)\n\t\t\t}\n\t\t\tnotifier, err := New(\n\t\t\t\tsnsCfg,\n\t\t\t\tcreateTmpl(t),\n\t\t\t\tlogger,\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\t\t\tvar alerts []*types.Alert\n\t\t\t_, err = notifier.Notify(context.Background(), alerts...)\n\t\t\trequire.Error(t, err)\n\t\t\trequire.Contains(t, err.Error(), \"template \\\"unknown_template\\\" not defined\")\n\t\t\trequire.Contains(t, err.Error(), tc.errMsg)\n\t\t})\n\t}\n}\n\n// CreateTmpl returns a ready-to-use template.\nfunc createTmpl(t *testing.T) *template.Template {\n\ttmpl, err := template.FromGlobs([]string{})\n\trequire.NoError(t, err)\n\ttmpl.ExternalURL, _ = url.Parse(\"http://am\")\n\treturn tmpl\n}\n"
  },
  {
    "path": "notify/telegram/telegram.go",
    "content": "// Copyright 2022 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage telegram\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"gopkg.in/telebot.v3\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// Telegram supports 4096 chars max - from https://limits.tginfo.me/en.\nconst maxMessageLenRunes = 4096\n\n// Notifier implements a Notifier for telegram notifications.\ntype Notifier struct {\n\tconf    *config.TelegramConfig\n\ttmpl    *template.Template\n\tlogger  *slog.Logger\n\tclient  *telebot.Bot\n\tretrier *notify.Retrier\n}\n\n// New returns a new Telegram notification handler.\nfunc New(conf *config.TelegramConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {\n\thttpclient, err := notify.NewClientWithTracing(*conf.HTTPConfig, \"telegram\", httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient, err := createTelegramClient(conf.APIUrl.String(), conf.ParseMode, httpclient)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Notifier{\n\t\tconf:    conf,\n\t\ttmpl:    t,\n\t\tlogger:  l,\n\t\tclient:  client,\n\t\tretrier: &notify.Retrier{},\n\t}, nil\n}\n\nfunc (n *Notifier) Notify(ctx context.Context, alert ...*types.Alert) (bool, error) {\n\tkey, ok := notify.GroupKey(ctx)\n\tif !ok {\n\t\treturn false, fmt.Errorf(\"group key missing\")\n\t}\n\n\tlogger := n.logger.With(\"group_key\", key)\n\tlogger.Debug(\"extracted group key\")\n\n\tvar (\n\t\terr         error\n\t\tdata        = notify.GetTemplateData(ctx, n.tmpl, alert, logger)\n\t\ttmpl        = notify.TmplText(n.tmpl, data, &err)\n\t\tmessageText string\n\t\ttruncated   bool\n\t)\n\n\tswitch n.conf.ParseMode {\n\tcase \"HTML\":\n\t\ttmpl = notify.TmplHTML(n.tmpl, data, &err)\n\t\tmessageText = tmpl(n.conf.Message)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif len([]rune(messageText)) > maxMessageLenRunes {\n\t\t\tmessageText = `Alertmanager notification could not be sent: message length exceeds Telegram limits.\n\t\t\tPlease check the template used for producing the message content.`\n\t\t}\n\tdefault:\n\t\tmessageText, truncated = notify.TruncateInRunes(tmpl(n.conf.Message), maxMessageLenRunes)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif truncated {\n\t\t\tlogger.Warn(\"Truncated message\", \"max_runes\", maxMessageLenRunes)\n\t\t}\n\t}\n\n\tn.client.Token, err = n.getBotToken()\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tchatID, err := n.getChatID()\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tmessage, err := n.client.Send(telebot.ChatID(chatID), messageText, &telebot.SendOptions{\n\t\tDisableNotification:   n.conf.DisableNotifications,\n\t\tDisableWebPagePreview: true,\n\t\tThreadID:              n.conf.MessageThreadID,\n\t\tParseMode:             n.conf.ParseMode,\n\t})\n\tif err != nil {\n\t\treturn true, err\n\t}\n\tlogger.Debug(\"Telegram message successfully published\", \"message_id\", message.ID, \"chat_id\", message.Chat.ID)\n\n\treturn false, nil\n}\n\nfunc createTelegramClient(apiURL, parseMode string, httpClient *http.Client) (*telebot.Bot, error) {\n\tbot, err := telebot.NewBot(telebot.Settings{\n\t\tURL:       apiURL,\n\t\tParseMode: parseMode,\n\t\tClient:    httpClient,\n\t\tOffline:   true,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn bot, nil\n}\n\nfunc (n *Notifier) getBotToken() (string, error) {\n\tif len(n.conf.BotTokenFile) > 0 {\n\t\tcontent, err := os.ReadFile(n.conf.BotTokenFile)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"could not read %s: %w\", n.conf.BotTokenFile, err)\n\t\t}\n\t\treturn strings.TrimSpace(string(content)), nil\n\t}\n\treturn string(n.conf.BotToken), nil\n}\n\nfunc (n *Notifier) getChatID() (int64, error) {\n\tif len(n.conf.ChatIDFile) > 0 {\n\t\tcontent, err := os.ReadFile(n.conf.ChatIDFile)\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"could not read %s: %w\", n.conf.ChatIDFile, err)\n\t\t}\n\t\tchatID, err := strconv.ParseInt(strings.TrimSpace(string(content)), 10, 64)\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"could not parse chat_id from %s: %w\", n.conf.ChatIDFile, err)\n\t\t}\n\t\treturn chatID, nil\n\t}\n\treturn n.conf.ChatID, nil\n}\n"
  },
  {
    "path": "notify/telegram/telegram_test.go",
    "content": "// Copyright 2022 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage telegram\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\t\"gopkg.in/yaml.v2\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/notify/test\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nfunc TestTelegramUnmarshal(t *testing.T) {\n\tin := `\nroute:\n  receiver: test\nreceivers:\n- name: test\n  telegram_configs:\n  - chat_id: 1234\n    bot_token: secret\n    message_thread_id: 1357\n`\n\tvar c config.Config\n\terr := yaml.Unmarshal([]byte(in), &c)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, c.Receivers, 1)\n\trequire.Len(t, c.Receivers[0].TelegramConfigs, 1)\n\n\trequire.Equal(t, \"https://api.telegram.org\", c.Receivers[0].TelegramConfigs[0].APIUrl.String())\n\trequire.Equal(t, commoncfg.Secret(\"secret\"), c.Receivers[0].TelegramConfigs[0].BotToken)\n\trequire.Equal(t, int64(1234), c.Receivers[0].TelegramConfigs[0].ChatID)\n\trequire.Equal(t, 1357, c.Receivers[0].TelegramConfigs[0].MessageThreadID)\n\trequire.Equal(t, \"HTML\", c.Receivers[0].TelegramConfigs[0].ParseMode)\n}\n\nfunc TestTelegramRetry(t *testing.T) {\n\t// Fake url for testing purposes\n\tfakeURL := amcommoncfg.URL{\n\t\tURL: &url.URL{\n\t\t\tScheme: \"https\",\n\t\t\tHost:   \"FAKE_API\",\n\t\t},\n\t}\n\tnotifier, err := New(\n\t\t&config.TelegramConfig{\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\tAPIUrl:     &fakeURL,\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\tfor statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) {\n\t\tactual, _ := notifier.retrier.Check(statusCode, nil)\n\t\trequire.Equal(t, expected, actual, \"error on status %d\", statusCode)\n\t}\n}\n\nfunc TestTelegramNotify(t *testing.T) {\n\ttoken := \"secret\"\n\n\tfileWithToken, err := os.CreateTemp(t.TempDir(), \"telegram-bot-token\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = fileWithToken.WriteString(token)\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tfor _, tc := range []struct {\n\t\tname    string\n\t\tcfg     config.TelegramConfig\n\t\texpText string\n\t}{\n\t\t{\n\t\t\tname: \"No escaping by default\",\n\t\t\tcfg: config.TelegramConfig{\n\t\t\t\tMessage:    \"<code>x < y</code>\",\n\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\t\tBotToken:   commoncfg.Secret(token),\n\t\t\t},\n\t\t\texpText: \"<code>x < y</code>\",\n\t\t},\n\t\t{\n\t\t\tname: \"Characters escaped in HTML mode\",\n\t\t\tcfg: config.TelegramConfig{\n\t\t\t\tParseMode:  \"HTML\",\n\t\t\t\tMessage:    \"<code>x < y</code>\",\n\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\t\tBotToken:   commoncfg.Secret(token),\n\t\t\t},\n\t\t\texpText: \"<code>x &lt; y</code>\",\n\t\t},\n\t\t{\n\t\t\tname: \"Bot token from file\",\n\t\t\tcfg: config.TelegramConfig{\n\t\t\t\tMessage:      \"test\",\n\t\t\t\tHTTPConfig:   &commoncfg.HTTPClientConfig{},\n\t\t\t\tBotTokenFile: fileWithToken.Name(),\n\t\t\t},\n\t\t\texpText: \"test\",\n\t\t},\n\t\t{\n\t\t\tname: \"HTML mode with too-large message\",\n\t\t\tcfg: config.TelegramConfig{\n\t\t\t\tParseMode:  \"HTML\",\n\t\t\t\tMessage:    strings.Repeat(\"x\", 5000),\n\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\t\tBotToken:   commoncfg.Secret(token),\n\t\t\t},\n\t\t\texpText: `Alertmanager notification could not be sent: message length exceeds Telegram limits.\n\t\t\tPlease check the template used for producing the message content.`,\n\t\t},\n\t\t{\n\t\t\tname: \"Default mode with too-large message\",\n\t\t\tcfg: config.TelegramConfig{\n\t\t\t\tMessage:    strings.Repeat(\"y\", 5000),\n\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\t\tBotToken:   commoncfg.Secret(token),\n\t\t\t},\n\t\t\texpText: strings.Repeat(\"y\", maxMessageLenRunes-1) + \"…\",\n\t\t},\n\t\t{\n\t\t\tname: \"HTML mode with message smaller than limit\",\n\t\t\tcfg: config.TelegramConfig{\n\t\t\t\tParseMode:  \"HTML\",\n\t\t\t\tMessage:    strings.Repeat(\"a\", 100),\n\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\t\tBotToken:   commoncfg.Secret(token),\n\t\t\t},\n\t\t\texpText: strings.Repeat(\"a\", 100),\n\t\t},\n\t\t{\n\t\t\tname: \"Default mode with message smaller than limit\",\n\t\t\tcfg: config.TelegramConfig{\n\t\t\t\tMessage:    strings.Repeat(\"b\", 100),\n\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\t\tBotToken:   commoncfg.Secret(token),\n\t\t\t},\n\t\t\texpText: strings.Repeat(\"b\", 100),\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar out []byte\n\t\t\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\trequire.Equal(t, \"/bot\"+token+\"/sendMessage\", r.URL.Path)\n\t\t\t\tvar err error\n\t\t\t\tout, err = io.ReadAll(r.Body)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tw.Write([]byte(`{\"ok\":true,\"result\":{\"chat\":{}}}`))\n\t\t\t}))\n\t\t\tdefer srv.Close()\n\t\t\tu, _ := url.Parse(srv.URL)\n\n\t\t\ttc.cfg.APIUrl = &amcommoncfg.URL{URL: u}\n\n\t\t\tnotifier, err := New(&tc.cfg, test.CreateTmpl(t), promslog.NewNopLogger())\n\t\t\trequire.NoError(t, err)\n\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\t\t\tdefer cancel()\n\t\t\tctx = notify.WithGroupKey(ctx, \"1\")\n\n\t\t\tretry, err := notifier.Notify(ctx, []*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"lbl1\": \"val1\",\n\t\t\t\t\t\t\t\"lbl3\": \"val3\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}...)\n\n\t\t\trequire.False(t, retry)\n\t\t\trequire.NoError(t, err)\n\n\t\t\treq := map[string]string{}\n\t\t\terr = json.Unmarshal(out, &req)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.expText, req[\"text\"])\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "notify/test/test.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage test\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// RetryTests returns a map of HTTP status codes to bool indicating whether the notifier should retry or not.\nfunc RetryTests(retryCodes []int) map[int]bool {\n\ttests := map[int]bool{\n\t\t// 1xx\n\t\thttp.StatusContinue:           false,\n\t\thttp.StatusSwitchingProtocols: false,\n\t\thttp.StatusProcessing:         false,\n\n\t\t// 2xx\n\t\thttp.StatusOK:                   false,\n\t\thttp.StatusCreated:              false,\n\t\thttp.StatusAccepted:             false,\n\t\thttp.StatusNonAuthoritativeInfo: false,\n\t\thttp.StatusNoContent:            false,\n\t\thttp.StatusResetContent:         false,\n\t\thttp.StatusPartialContent:       false,\n\t\thttp.StatusMultiStatus:          false,\n\t\thttp.StatusAlreadyReported:      false,\n\t\thttp.StatusIMUsed:               false,\n\n\t\t// 3xx\n\t\thttp.StatusMultipleChoices:   false,\n\t\thttp.StatusMovedPermanently:  false,\n\t\thttp.StatusFound:             false,\n\t\thttp.StatusSeeOther:          false,\n\t\thttp.StatusNotModified:       false,\n\t\thttp.StatusUseProxy:          false,\n\t\thttp.StatusTemporaryRedirect: false,\n\t\thttp.StatusPermanentRedirect: false,\n\n\t\t// 4xx\n\t\thttp.StatusBadRequest:                   false,\n\t\thttp.StatusUnauthorized:                 false,\n\t\thttp.StatusPaymentRequired:              false,\n\t\thttp.StatusForbidden:                    false,\n\t\thttp.StatusNotFound:                     false,\n\t\thttp.StatusMethodNotAllowed:             false,\n\t\thttp.StatusNotAcceptable:                false,\n\t\thttp.StatusProxyAuthRequired:            false,\n\t\thttp.StatusRequestTimeout:               false,\n\t\thttp.StatusConflict:                     false,\n\t\thttp.StatusGone:                         false,\n\t\thttp.StatusLengthRequired:               false,\n\t\thttp.StatusPreconditionFailed:           false,\n\t\thttp.StatusRequestEntityTooLarge:        false,\n\t\thttp.StatusRequestURITooLong:            false,\n\t\thttp.StatusUnsupportedMediaType:         false,\n\t\thttp.StatusRequestedRangeNotSatisfiable: false,\n\t\thttp.StatusExpectationFailed:            false,\n\t\thttp.StatusTeapot:                       false,\n\t\thttp.StatusUnprocessableEntity:          false,\n\t\thttp.StatusLocked:                       false,\n\t\thttp.StatusFailedDependency:             false,\n\t\thttp.StatusUpgradeRequired:              false,\n\t\thttp.StatusPreconditionRequired:         false,\n\t\thttp.StatusTooManyRequests:              false,\n\t\thttp.StatusRequestHeaderFieldsTooLarge:  false,\n\t\thttp.StatusUnavailableForLegalReasons:   false,\n\n\t\t// 5xx\n\t\thttp.StatusInternalServerError:           false,\n\t\thttp.StatusNotImplemented:                false,\n\t\thttp.StatusBadGateway:                    false,\n\t\thttp.StatusServiceUnavailable:            false,\n\t\thttp.StatusGatewayTimeout:                false,\n\t\thttp.StatusHTTPVersionNotSupported:       false,\n\t\thttp.StatusVariantAlsoNegotiates:         false,\n\t\thttp.StatusInsufficientStorage:           false,\n\t\thttp.StatusLoopDetected:                  false,\n\t\thttp.StatusNotExtended:                   false,\n\t\thttp.StatusNetworkAuthenticationRequired: false,\n\t}\n\n\tfor _, statusCode := range retryCodes {\n\t\ttests[statusCode] = true\n\t}\n\n\treturn tests\n}\n\n// DefaultRetryCodes returns the list of HTTP status codes that need to be retried.\nfunc DefaultRetryCodes() []int {\n\treturn []int{\n\t\thttp.StatusInternalServerError,\n\t\thttp.StatusNotImplemented,\n\t\thttp.StatusBadGateway,\n\t\thttp.StatusServiceUnavailable,\n\t\thttp.StatusGatewayTimeout,\n\t\thttp.StatusHTTPVersionNotSupported,\n\t\thttp.StatusVariantAlsoNegotiates,\n\t\thttp.StatusInsufficientStorage,\n\t\thttp.StatusLoopDetected,\n\t\thttp.StatusNotExtended,\n\t\thttp.StatusNetworkAuthenticationRequired,\n\t}\n}\n\n// CreateTmpl returns a ready-to-use template.\nfunc CreateTmpl(t *testing.T) *template.Template {\n\ttmpl, err := template.FromGlobs([]string{})\n\trequire.NoError(t, err)\n\ttmpl.ExternalURL, _ = url.Parse(\"http://am\")\n\treturn tmpl\n}\n\n// AssertNotifyLeaksNoSecret calls the Notify() method of the notifier, expects\n// it to fail because the context is canceled by the server and checks that no\n// secret data is leaked in the error message returned by Notify().\nfunc AssertNotifyLeaksNoSecret(ctx context.Context, t *testing.T, n notify.Notifier, secret ...string) {\n\tt.Helper()\n\trequire.NotEmpty(t, secret)\n\n\tctx = notify.WithGroupKey(ctx, \"1\")\n\tok, err := n.Notify(ctx, []*types.Alert{\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"lbl1\": \"val1\",\n\t\t\t\t},\n\t\t\t\tStartsAt: time.Now(),\n\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t},\n\t\t},\n\t}...)\n\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), context.Canceled.Error())\n\tfor _, s := range secret {\n\t\trequire.NotContains(t, err.Error(), s)\n\t}\n\trequire.True(t, ok)\n}\n\n// GetContextWithCancelingURL returns a context that gets canceled when a\n// client does a GET request to the returned URL.\n// Handlers passed to the function will be invoked in order before the context gets canceled.\n// The last argument is a function that needs to be called before the caller returns.\nfunc GetContextWithCancelingURL(h ...func(w http.ResponseWriter, r *http.Request)) (context.Context, *url.URL, func()) {\n\tdone := make(chan struct{})\n\tctx, cancel := context.WithCancel(context.Background())\n\ti := 0\n\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif i < len(h) {\n\t\t\th[i](w, r)\n\t\t} else {\n\t\t\tcancel()\n\t\t\t<-done\n\t\t}\n\t\ti++\n\t}))\n\n\t// No need to check the error since httptest.NewServer always return a valid URL.\n\tu, _ := url.Parse(srv.URL)\n\n\treturn ctx, u, func() {\n\t\tclose(done)\n\t\tsrv.Close()\n\t}\n}\n"
  },
  {
    "path": "notify/util.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage notify\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"slices\"\n\t\"strings\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/version\"\n\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/tracing\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// truncationMarker is the character used to represent a truncation.\nconst truncationMarker = \"…\"\n\n// UserAgentHeader is the default User-Agent for notification requests.\nvar UserAgentHeader = version.ComponentUserAgent(\"Alertmanager\")\n\n// NewClientWithTracing creates a new HTTP client with tracing included\n// Clients are reused across requests, so tracing is configured once at creation\n// rather than on each request.\nfunc NewClientWithTracing(cfg commoncfg.HTTPClientConfig, name string, httpOpts ...commoncfg.HTTPClientOption) (*http.Client, error) {\n\tclient, err := commoncfg.NewClientFromConfig(cfg, name, httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient.Transport = tracing.Transport(client.Transport)\n\treturn client, nil\n}\n\n// RedactURL removes the URL part from an error of *url.Error type.\nfunc RedactURL(err error) error {\n\tvar e *url.Error\n\tif !errors.As(err, &e) {\n\t\treturn err\n\t}\n\te.URL = \"<redacted>\"\n\treturn e\n}\n\n// Get sends a GET request to the given URL.\nfunc Get(ctx context.Context, client *http.Client, url string) (*http.Response, error) {\n\treturn request(ctx, client, http.MethodGet, url, \"\", nil)\n}\n\n// PostJSON sends a POST request with JSON payload to the given URL.\nfunc PostJSON(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error) {\n\treturn post(ctx, client, url, \"application/json\", body)\n}\n\n// PostText sends a POST request with text payload to the given URL.\nfunc PostText(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error) {\n\treturn post(ctx, client, url, \"text/plain\", body)\n}\n\nfunc post(ctx context.Context, client *http.Client, url, bodyType string, body io.Reader) (*http.Response, error) {\n\treturn request(ctx, client, http.MethodPost, url, bodyType, body)\n}\n\nfunc request(ctx context.Context, client *http.Client, method, url, bodyType string, body io.Reader) (*http.Response, error) {\n\treq, err := http.NewRequest(method, url, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"User-Agent\", UserAgentHeader)\n\tif bodyType != \"\" {\n\t\treq.Header.Set(\"Content-Type\", bodyType)\n\t}\n\n\treturn client.Do(req.WithContext(ctx))\n}\n\n// Drain consumes and closes the response's body to make sure that the\n// HTTP client can reuse existing connections.\nfunc Drain(r *http.Response) {\n\tio.Copy(io.Discard, r.Body)\n\tr.Body.Close()\n}\n\n// TruncateInRunes truncates a string to fit the given size in Runes.\nfunc TruncateInRunes(s string, n int) (string, bool) {\n\tr := []rune(s)\n\tif len(r) <= n {\n\t\treturn s, false\n\t}\n\n\tif n <= 3 {\n\t\treturn string(r[:n]), true\n\t}\n\n\treturn string(r[:n-1]) + truncationMarker, true\n}\n\n// TruncateInBytes truncates a string to fit the given size in Bytes.\nfunc TruncateInBytes(s string, n int) (string, bool) {\n\t// First, measure the string the w/o a to-rune conversion.\n\tif len(s) <= n {\n\t\treturn s, false\n\t}\n\n\t// The truncationMarker itself is 3 bytes, we can't return any part of the string when it's less than 3.\n\tif n <= 3 {\n\t\tswitch n {\n\t\tcase 3:\n\t\t\treturn truncationMarker, true\n\t\tdefault:\n\t\t\treturn strings.Repeat(\".\", n), true\n\t\t}\n\t}\n\n\t// Now, to ensure we don't butcher the string we need to remove using runes.\n\tr := []rune(s)\n\ttruncationTarget := n - 3\n\n\t// Next, let's truncate the runes to the lower possible number.\n\ttruncatedRunes := r[:truncationTarget]\n\tfor len(string(truncatedRunes)) > truncationTarget {\n\t\ttruncatedRunes = r[:len(truncatedRunes)-1]\n\t}\n\n\treturn string(truncatedRunes) + truncationMarker, true\n}\n\n// TmplText is using monadic error handling in order to make string templating\n// less verbose. Use with care as the final error checking is easily missed.\nfunc TmplText(tmpl *template.Template, data *template.Data, err *error) func(string) string {\n\treturn func(name string) (s string) {\n\t\tif *err != nil {\n\t\t\treturn s\n\t\t}\n\t\ts, *err = tmpl.ExecuteTextString(name, data)\n\t\treturn s\n\t}\n}\n\n// TmplHTML is using monadic error handling in order to make string templating\n// less verbose. Use with care as the final error checking is easily missed.\nfunc TmplHTML(tmpl *template.Template, data *template.Data, err *error) func(string) string {\n\treturn func(name string) (s string) {\n\t\tif *err != nil {\n\t\t\treturn s\n\t\t}\n\t\ts, *err = tmpl.ExecuteHTMLString(name, data)\n\t\treturn s\n\t}\n}\n\n// Key is a string that can be hashed.\ntype Key string\n\n// ExtractGroupKey gets the group key from the context.\nfunc ExtractGroupKey(ctx context.Context) (Key, error) {\n\tkey, ok := GroupKey(ctx)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"group key missing\")\n\t}\n\treturn Key(key), nil\n}\n\n// Hash returns the sha256 for a group key as integrations may have\n// maximum length requirements on deduplication keys.\nfunc (k Key) Hash() string {\n\th := sha256.New()\n\t// hash.Hash.Write never returns an error.\n\t//nolint: errcheck\n\th.Write([]byte(string(k)))\n\treturn fmt.Sprintf(\"%x\", h.Sum(nil))\n}\n\nfunc (k Key) String() string {\n\treturn string(k)\n}\n\n// GetTemplateData creates the template data from the context and the alerts.\nfunc GetTemplateData(ctx context.Context, tmpl *template.Template, alerts []*types.Alert, l *slog.Logger) *template.Data {\n\trecv, ok := ReceiverName(ctx)\n\tif !ok {\n\t\tl.Error(\"Missing receiver\")\n\t}\n\tgroupLabels, ok := GroupLabels(ctx)\n\tif !ok {\n\t\tl.Error(\"Missing group labels\")\n\t}\n\tnotificationReason, ok := NotificationReason(ctx)\n\tif !ok {\n\t\tl.Error(\"Missing notification reason\")\n\t\tnotificationReason = ReasonUnknown\n\t}\n\treturn tmpl.Data(recv, groupLabels, notificationReason.String(), alerts...)\n}\n\nfunc readAll(r io.Reader) string {\n\tif r == nil {\n\t\treturn \"\"\n\t}\n\tbs, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn string(bs)\n}\n\n// Retrier knows when to retry an HTTP request to a receiver. 2xx status codes\n// are successful, anything else is a failure and only 5xx status codes should\n// be retried.\ntype Retrier struct {\n\t// Function to return additional information in the error message.\n\tCustomDetailsFunc func(code int, body io.Reader) string\n\t// Additional HTTP status codes that should be retried.\n\tRetryCodes []int\n}\n\n// Check returns a boolean indicating whether the request should be retried\n// and an optional error if the request has failed. If body is not nil, it will\n// be included in the error message.\nfunc (r *Retrier) Check(statusCode int, body io.Reader) (bool, error) {\n\t// 2xx responses are considered to be always successful.\n\tif statusCode/100 == 2 {\n\t\treturn false, nil\n\t}\n\n\t// 5xx responses are considered to be always retried.\n\tretry := statusCode/100 == 5 || slices.Contains(r.RetryCodes, statusCode)\n\n\ts := fmt.Sprintf(\"unexpected status code %v\", statusCode)\n\tvar details string\n\tif r.CustomDetailsFunc != nil {\n\t\tdetails = r.CustomDetailsFunc(statusCode, body)\n\t} else {\n\t\tdetails = readAll(body)\n\t}\n\tif details != \"\" {\n\t\ts = fmt.Sprintf(\"%s: %s\", s, details)\n\t}\n\treturn retry, errors.New(s)\n}\n\ntype ErrorWithReason struct {\n\tErr error\n\n\tReason Reason\n}\n\nfunc NewErrorWithReason(reason Reason, err error) *ErrorWithReason {\n\treturn &ErrorWithReason{\n\t\tErr:    err,\n\t\tReason: reason,\n\t}\n}\n\nfunc (e *ErrorWithReason) Error() string {\n\treturn e.Err.Error()\n}\n\n// Reason is the failure reason.\ntype Reason int\n\nconst (\n\tDefaultReason Reason = iota\n\tClientErrorReason\n\tServerErrorReason\n\tContextCanceledReason\n\tContextDeadlineExceededReason\n)\n\nfunc (s Reason) String() string {\n\tswitch s {\n\tcase DefaultReason:\n\t\treturn \"other\"\n\tcase ClientErrorReason:\n\t\treturn \"clientError\"\n\tcase ServerErrorReason:\n\t\treturn \"serverError\"\n\tcase ContextCanceledReason:\n\t\treturn \"contextCanceled\"\n\tcase ContextDeadlineExceededReason:\n\t\treturn \"contextDeadlineExceeded\"\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unknown Reason: %d\", s))\n\t}\n}\n\n// possibleFailureReasonCategory is a list of possible failure reason.\nvar possibleFailureReasonCategory = []string{DefaultReason.String(), ClientErrorReason.String(), ServerErrorReason.String(), ContextCanceledReason.String(), ContextDeadlineExceededReason.String()}\n\n// GetFailureReasonFromStatusCode returns the reason for the failure based on the status code provided.\nfunc GetFailureReasonFromStatusCode(statusCode int) Reason {\n\tif statusCode/100 == 4 {\n\t\treturn ClientErrorReason\n\t}\n\tif statusCode/100 == 5 {\n\t\treturn ServerErrorReason\n\t}\n\n\treturn DefaultReason\n}\n"
  },
  {
    "path": "notify/util_test.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage notify\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"path\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestTruncate(t *testing.T) {\n\ttype expect struct {\n\t\tout   string\n\t\ttrunc bool\n\t}\n\n\ttestCases := []struct {\n\t\tin string\n\t\tn  int\n\n\t\trunes expect\n\t\tbytes expect\n\t}{\n\t\t{\n\t\t\tin:    \"\",\n\t\t\tn:     5,\n\t\t\trunes: expect{out: \"\", trunc: false},\n\t\t\tbytes: expect{out: \"\", trunc: false},\n\t\t},\n\t\t{\n\t\t\tin:    \"abcde\",\n\t\t\tn:     2,\n\t\t\trunes: expect{out: \"ab\", trunc: true},\n\t\t\tbytes: expect{out: \"..\", trunc: true},\n\t\t},\n\t\t{\n\t\t\tin:    \"abcde\",\n\t\t\tn:     4,\n\t\t\trunes: expect{out: \"abc…\", trunc: true},\n\t\t\tbytes: expect{out: \"a…\", trunc: true},\n\t\t},\n\t\t{\n\t\t\tin:    \"abcde\",\n\t\t\tn:     5,\n\t\t\trunes: expect{out: \"abcde\", trunc: false},\n\t\t\tbytes: expect{out: \"abcde\", trunc: false},\n\t\t},\n\t\t{\n\t\t\tin:    \"abcdefgh\",\n\t\t\tn:     5,\n\t\t\trunes: expect{out: \"abcd…\", trunc: true},\n\t\t\tbytes: expect{out: \"ab…\", trunc: true},\n\t\t},\n\t\t{\n\t\t\tin:    \"a⌘cde\",\n\t\t\tn:     5,\n\t\t\trunes: expect{out: \"a⌘cde\", trunc: false},\n\t\t\tbytes: expect{out: \"a…\", trunc: true},\n\t\t},\n\t\t{\n\t\t\tin:    \"a⌘cdef\",\n\t\t\tn:     5,\n\t\t\trunes: expect{out: \"a⌘cd…\", trunc: true},\n\t\t\tbytes: expect{out: \"a…\", trunc: true},\n\t\t},\n\t\t{\n\t\t\tin:    \"世界cdef\",\n\t\t\tn:     3,\n\t\t\trunes: expect{out: \"世界c\", trunc: true},\n\t\t\tbytes: expect{out: \"…\", trunc: true},\n\t\t},\n\t\t{\n\t\t\tin:    \"❤️✅🚀🔥❌❤️✅🚀🔥❌❤️✅🚀🔥❌❤️✅🚀🔥❌\",\n\t\t\tn:     19,\n\t\t\trunes: expect{out: \"❤️✅🚀🔥❌❤️✅🚀🔥❌❤️✅🚀🔥❌…\", trunc: true},\n\t\t\tbytes: expect{out: \"❤️✅🚀…\", trunc: true},\n\t\t},\n\t}\n\n\ttype truncateFunc func(string, int) (string, bool)\n\n\tfor _, tc := range testCases {\n\t\tfor _, fn := range []truncateFunc{TruncateInBytes, TruncateInRunes} {\n\t\t\tvar truncated bool\n\t\t\tvar out string\n\n\t\t\tfnPath := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()\n\t\t\tfnName := path.Base(fnPath)\n\t\t\tswitch fnName {\n\t\t\tcase \"notify.TruncateInRunes\":\n\t\t\t\ttruncated = tc.runes.trunc\n\t\t\t\tout = tc.runes.out\n\t\t\tcase \"notify.TruncateInBytes\":\n\t\t\t\ttruncated = tc.bytes.trunc\n\t\t\t\tout = tc.bytes.out\n\t\t\tdefault:\n\t\t\t\tt.Fatalf(\"unknown function\")\n\t\t\t}\n\n\t\t\tt.Run(fmt.Sprintf(\"%s(%s,%d)\", fnName, tc.in, tc.n), func(t *testing.T) {\n\t\t\t\ts, trunc := fn(tc.in, tc.n)\n\t\t\t\trequire.Equal(t, out, s)\n\t\t\t\trequire.Equal(t, truncated, trunc)\n\t\t\t})\n\t\t}\n\t}\n}\n\ntype brokenReader struct{}\n\nfunc (b brokenReader) Read([]byte) (int, error) {\n\treturn 0, fmt.Errorf(\"some error\")\n}\n\nfunc TestRetrierCheck(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tretrier Retrier\n\t\tstatus  int\n\t\tbody    io.Reader\n\n\t\tretry       bool\n\t\texpectedErr string\n\t}{\n\t\t{\n\t\t\tretrier: Retrier{},\n\t\t\tstatus:  http.StatusOK,\n\t\t\tbody:    bytes.NewBuffer([]byte(\"ok\")),\n\n\t\t\tretry: false,\n\t\t},\n\t\t{\n\t\t\tretrier: Retrier{},\n\t\t\tstatus:  http.StatusNoContent,\n\n\t\t\tretry: false,\n\t\t},\n\t\t{\n\t\t\tretrier: Retrier{},\n\t\t\tstatus:  http.StatusBadRequest,\n\n\t\t\tretry:       false,\n\t\t\texpectedErr: \"unexpected status code 400\",\n\t\t},\n\t\t{\n\t\t\tretrier: Retrier{RetryCodes: []int{http.StatusTooManyRequests}},\n\t\t\tstatus:  http.StatusBadRequest,\n\t\t\tbody:    bytes.NewBuffer([]byte(\"invalid request\")),\n\n\t\t\tretry:       false,\n\t\t\texpectedErr: \"unexpected status code 400: invalid request\",\n\t\t},\n\t\t{\n\t\t\tretrier: Retrier{RetryCodes: []int{http.StatusTooManyRequests}},\n\t\t\tstatus:  http.StatusTooManyRequests,\n\n\t\t\tretry:       true,\n\t\t\texpectedErr: \"unexpected status code 429\",\n\t\t},\n\t\t{\n\t\t\tretrier: Retrier{},\n\t\t\tstatus:  http.StatusServiceUnavailable,\n\t\t\tbody:    bytes.NewBuffer([]byte(\"retry later\")),\n\n\t\t\tretry:       true,\n\t\t\texpectedErr: \"unexpected status code 503: retry later\",\n\t\t},\n\t\t{\n\t\t\tretrier: Retrier{},\n\t\t\tstatus:  http.StatusBadGateway,\n\t\t\tbody:    &brokenReader{},\n\n\t\t\tretry:       true,\n\t\t\texpectedErr: \"unexpected status code 502\",\n\t\t},\n\t\t{\n\t\t\tretrier: Retrier{CustomDetailsFunc: func(status int, b io.Reader) string {\n\t\t\t\tif status != http.StatusServiceUnavailable {\n\t\t\t\t\treturn \"invalid\"\n\t\t\t\t}\n\t\t\t\tbs, _ := io.ReadAll(b)\n\t\t\t\treturn fmt.Sprintf(\"server response is %q\", string(bs))\n\t\t\t}},\n\t\t\tstatus: http.StatusServiceUnavailable,\n\t\t\tbody:   bytes.NewBuffer([]byte(\"retry later\")),\n\n\t\t\tretry:       true,\n\t\t\texpectedErr: \"unexpected status code 503: server response is \\\"retry later\\\"\",\n\t\t},\n\t} {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tretry, err := tc.retrier.Check(tc.status, tc.body)\n\t\t\trequire.Equal(t, tc.retry, retry)\n\t\t\tif tc.expectedErr == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.EqualError(t, err, tc.expectedErr)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "notify/victorops/victorops.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage victorops\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// https://help.victorops.com/knowledge-base/incident-fields-glossary/ - 20480 characters.\nconst maxMessageLenRunes = 20480\n\n// Notifier implements a Notifier for VictorOps notifications.\ntype Notifier struct {\n\tconf    *config.VictorOpsConfig\n\ttmpl    *template.Template\n\tlogger  *slog.Logger\n\tclient  *http.Client\n\tretrier *notify.Retrier\n}\n\n// New returns a new VictorOps notifier.\nfunc New(c *config.VictorOpsConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {\n\tclient, err := notify.NewClientWithTracing(*c.HTTPConfig, \"victorops\", httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Notifier{\n\t\tconf:   c,\n\t\ttmpl:   t,\n\t\tlogger: l,\n\t\tclient: client,\n\t\t// Missing documentation therefore assuming only 5xx response codes are\n\t\t// recoverable.\n\t\tretrier: &notify.Retrier{},\n\t}, nil\n}\n\nconst (\n\tvictorOpsEventTrigger = \"CRITICAL\"\n\tvictorOpsEventResolve = \"RECOVERY\"\n)\n\n// Notify implements the Notifier interface.\nfunc (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {\n\tvar err error\n\tvar (\n\t\tdata   = notify.GetTemplateData(ctx, n.tmpl, as, n.logger)\n\t\ttmpl   = notify.TmplText(n.tmpl, data, &err)\n\t\tapiURL = n.conf.APIURL.Copy()\n\t)\n\n\tvar apiKey string\n\tif n.conf.APIKey != \"\" {\n\t\tapiKey = string(n.conf.APIKey)\n\t} else {\n\t\tcontent, fileErr := os.ReadFile(n.conf.APIKeyFile)\n\t\tif fileErr != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to read API key from file: %w\", fileErr)\n\t\t}\n\t\tapiKey = strings.TrimSpace(string(content))\n\t}\n\n\tapiURL.Path += fmt.Sprintf(\"%s/%s\", apiKey, tmpl(n.conf.RoutingKey))\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"templating error: %w\", err)\n\t}\n\n\tbuf, err := n.createVictorOpsPayload(ctx, as...)\n\tif err != nil {\n\t\treturn true, err\n\t}\n\n\tresp, err := notify.PostJSON(ctx, n.client, apiURL.String(), buf)\n\tif err != nil {\n\t\treturn true, notify.RedactURL(err)\n\t}\n\tdefer notify.Drain(resp)\n\n\tshouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)\n\tif err != nil {\n\t\treturn shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)\n\t}\n\treturn shouldRetry, err\n}\n\n// Create the JSON payload to be sent to the VictorOps API.\nfunc (n *Notifier) createVictorOpsPayload(ctx context.Context, as ...*types.Alert) (*bytes.Buffer, error) {\n\tvictorOpsAllowedEvents := map[string]bool{\n\t\t\"INFO\":     true,\n\t\t\"WARNING\":  true,\n\t\t\"CRITICAL\": true,\n\t}\n\n\tkey, err := notify.ExtractGroupKey(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar (\n\t\talerts = types.Alerts(as...)\n\t\tdata   = notify.GetTemplateData(ctx, n.tmpl, as, n.logger)\n\t\ttmpl   = notify.TmplText(n.tmpl, data, &err)\n\n\t\tmessageType  = tmpl(n.conf.MessageType)\n\t\tstateMessage = tmpl(n.conf.StateMessage)\n\t)\n\n\tif alerts.Status() == model.AlertFiring && !victorOpsAllowedEvents[messageType] {\n\t\tmessageType = victorOpsEventTrigger\n\t}\n\n\tif alerts.Status() == model.AlertResolved {\n\t\tmessageType = victorOpsEventResolve\n\t}\n\n\tstateMessage, truncated := notify.TruncateInRunes(stateMessage, maxMessageLenRunes)\n\tif truncated {\n\t\tn.logger.Warn(\"Truncated state_message\", \"group_key\", key, \"max_runes\", maxMessageLenRunes)\n\t}\n\n\tmsg := map[string]string{\n\t\t\"message_type\":        messageType,\n\t\t\"entity_id\":           key.Hash(),\n\t\t\"entity_display_name\": tmpl(n.conf.EntityDisplayName),\n\t\t\"state_message\":       stateMessage,\n\t\t\"monitoring_tool\":     tmpl(n.conf.MonitoringTool),\n\t}\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"templating error: %w\", err)\n\t}\n\n\t// Add custom fields to the payload.\n\tfor k, v := range n.conf.CustomFields {\n\t\tmsg[k] = tmpl(v)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"templating error: %w\", err)\n\t\t}\n\t}\n\n\tvar buf bytes.Buffer\n\tif err := json.NewEncoder(&buf).Encode(msg); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &buf, nil\n}\n"
  },
  {
    "path": "notify/victorops/victorops_test.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage victorops\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/notify/test\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nfunc TestVictorOpsCustomFields(t *testing.T) {\n\tlogger := promslog.NewNopLogger()\n\ttmpl := test.CreateTmpl(t)\n\n\turl, err := url.Parse(\"http://nowhere.com\")\n\n\trequire.NoError(t, err, \"unexpected error parsing mock url\")\n\n\tconf := &config.VictorOpsConfig{\n\t\tAPIKey:            `12345`,\n\t\tAPIURL:            &amcommoncfg.URL{URL: url},\n\t\tEntityDisplayName: `{{ .CommonLabels.Message }}`,\n\t\tStateMessage:      `{{ .CommonLabels.Message }}`,\n\t\tRoutingKey:        `test`,\n\t\tMessageType:       ``,\n\t\tMonitoringTool:    `AM`,\n\t\tCustomFields: map[string]string{\n\t\t\t\"Field_A\": \"{{ .CommonLabels.Message }}\",\n\t\t},\n\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t}\n\n\tnotifier, err := New(conf, tmpl, logger)\n\trequire.NoError(t, err)\n\n\tctx := context.Background()\n\tctx = notify.WithGroupKey(ctx, \"1\")\n\n\talert := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels: model.LabelSet{\n\t\t\t\t\"Message\": \"message\",\n\t\t\t},\n\t\t\tStartsAt: time.Now(),\n\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t},\n\t}\n\n\tmsg, err := notifier.createVictorOpsPayload(ctx, alert)\n\trequire.NoError(t, err)\n\n\tvar m map[string]string\n\terr = json.Unmarshal(msg.Bytes(), &m)\n\n\trequire.NoError(t, err)\n\n\t// Verify that a custom field was added to the payload and templatized.\n\trequire.Equal(t, \"message\", m[\"Field_A\"])\n}\n\nfunc TestVictorOpsRetry(t *testing.T) {\n\tnotifier, err := New(\n\t\t&config.VictorOpsConfig{\n\t\t\tAPIKey:     commoncfg.Secret(\"secret\"),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\tfor statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) {\n\t\tactual, _ := notifier.retrier.Check(statusCode, nil)\n\t\trequire.Equal(t, expected, actual, \"error on status %d\", statusCode)\n\t}\n}\n\nfunc TestVictorOpsRedactedURL(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tsecret := \"secret\"\n\tnotifier, err := New(\n\t\t&config.VictorOpsConfig{\n\t\t\tAPIURL:     &amcommoncfg.URL{URL: u},\n\t\t\tAPIKey:     commoncfg.Secret(secret),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret)\n}\n\nfunc TestVictorOpsReadingApiKeyFromFile(t *testing.T) {\n\tkey := \"key\"\n\tf, err := os.CreateTemp(t.TempDir(), \"victorops_test\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = f.WriteString(key)\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tnotifier, err := New(\n\t\t&config.VictorOpsConfig{\n\t\t\tAPIURL:     &amcommoncfg.URL{URL: u},\n\t\t\tAPIKeyFile: f.Name(),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, key)\n}\n\nfunc TestVictorOpsTemplating(t *testing.T) {\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdec := json.NewDecoder(r.Body)\n\t\tout := make(map[string]any)\n\t\terr := dec.Decode(&out)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}))\n\tdefer srv.Close()\n\tu, _ := url.Parse(srv.URL)\n\n\ttests := []struct {\n\t\tname   string\n\t\tcfg    *config.VictorOpsConfig\n\t\terrMsg string\n\t}{\n\t\t{\n\t\t\tname: \"default valid templates\",\n\t\t\tcfg:  &config.VictorOpsConfig{},\n\t\t},\n\t\t{\n\t\t\tname: \"invalid message_type\",\n\t\t\tcfg: &config.VictorOpsConfig{\n\t\t\t\tMessageType: \"{{ .CommonLabels.alertname }\",\n\t\t\t},\n\t\t\terrMsg: \"templating error\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid entity_display_name\",\n\t\t\tcfg: &config.VictorOpsConfig{\n\t\t\t\tEntityDisplayName: \"{{ .CommonLabels.alertname }\",\n\t\t\t},\n\t\t\terrMsg: \"templating error\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid state_message\",\n\t\t\tcfg: &config.VictorOpsConfig{\n\t\t\t\tStateMessage: \"{{ .CommonLabels.alertname }\",\n\t\t\t},\n\t\t\terrMsg: \"templating error\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid monitoring tool\",\n\t\t\tcfg: &config.VictorOpsConfig{\n\t\t\t\tMonitoringTool: \"{{ .CommonLabels.alertname }\",\n\t\t\t},\n\t\t\terrMsg: \"templating error\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid routing_key\",\n\t\t\tcfg: &config.VictorOpsConfig{\n\t\t\t\tRoutingKey: \"{{ .CommonLabels.alertname }\",\n\t\t\t},\n\t\t\terrMsg: \"templating error\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ttc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{}\n\t\t\ttc.cfg.APIURL = &amcommoncfg.URL{URL: u}\n\t\t\ttc.cfg.APIKey = \"test\"\n\t\t\tvo, err := New(tc.cfg, test.CreateTmpl(t), promslog.NewNopLogger())\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := context.Background()\n\t\t\tctx = notify.WithGroupKey(ctx, \"1\")\n\n\t\t\t_, err = vo.Notify(ctx, []*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"lbl1\": \"val1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}...)\n\t\t\tif tc.errMsg == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Contains(t, err.Error(), tc.errMsg)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "notify/webex/webex.go",
    "content": "// Copyright 2022 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage webex\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"log/slog\"\n\t\"net/http\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nconst (\n\t// nolint:godot\n\t// maxMessageSize represents the maximum message length that Webex supports.\n\tmaxMessageSize = 7439\n)\n\ntype Notifier struct {\n\tconf    *config.WebexConfig\n\ttmpl    *template.Template\n\tlogger  *slog.Logger\n\tclient  *http.Client\n\tretrier *notify.Retrier\n}\n\n// New returns a new Webex notifier.\nfunc New(c *config.WebexConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {\n\tclient, err := notify.NewClientWithTracing(*c.HTTPConfig, \"webex\", httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tn := &Notifier{\n\t\tconf:    c,\n\t\ttmpl:    t,\n\t\tlogger:  l,\n\t\tclient:  client,\n\t\tretrier: &notify.Retrier{},\n\t}\n\n\treturn n, nil\n}\n\ntype webhook struct {\n\tMarkdown string `json:\"markdown\"`\n\tRoomID   string `json:\"roomId,omitempty\"`\n}\n\n// Notify implements the Notifier interface.\nfunc (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {\n\tkey, err := notify.ExtractGroupKey(ctx)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tlogger := n.logger.With(\"group_key\", key)\n\tlogger.Debug(\"extracted group key\")\n\n\tdata := notify.GetTemplateData(ctx, n.tmpl, as, logger)\n\ttmpl := notify.TmplText(n.tmpl, data, &err)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tmessage := tmpl(n.conf.Message)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tmessage, truncated := notify.TruncateInBytes(message, maxMessageSize)\n\tif truncated {\n\t\tlogger.Debug(\"message truncated due to exceeding maximum allowed length by webex\", \"truncated_message\", message)\n\t}\n\n\tw := webhook{\n\t\tMarkdown: message,\n\t\tRoomID:   tmpl(n.conf.RoomID),\n\t}\n\n\tvar payload bytes.Buffer\n\tif err = json.NewEncoder(&payload).Encode(w); err != nil {\n\t\treturn false, err\n\t}\n\n\tresp, err := notify.PostJSON(ctx, n.client, n.conf.APIURL.String(), &payload)\n\tif err != nil {\n\t\treturn true, notify.RedactURL(err)\n\t}\n\n\tshouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)\n\tif err != nil {\n\t\treturn shouldRetry, err\n\t}\n\n\treturn false, nil\n}\n"
  },
  {
    "path": "notify/webex/webex_test.go",
    "content": "// Copyright 2022 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage webex\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/notify/test\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nfunc TestWebexRetry(t *testing.T) {\n\ttestWebhookURL, err := url.Parse(\"https://api.ciscospark.com/v1/message\")\n\trequire.NoError(t, err)\n\n\tnotifier, err := New(\n\t\t&config.WebexConfig{\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\tAPIURL:     &amcommoncfg.URL{URL: testWebhookURL},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\tfor statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) {\n\t\tactual, _ := notifier.retrier.Check(statusCode, nil)\n\t\trequire.Equal(t, expected, actual, \"error on status %d\", statusCode)\n\t}\n}\n\nfunc TestWebexTemplating(t *testing.T) {\n\ttc := []struct {\n\t\tname string\n\n\t\tcfg       *config.WebexConfig\n\t\tMessage   string\n\t\texpJSON   string\n\t\tcommonCfg *commoncfg.HTTPClientConfig\n\n\t\tretry     bool\n\t\terrMsg    string\n\t\texpHeader string\n\t}{\n\t\t{\n\t\t\tname: \"with a valid message and a set http_config.authorization, it is formatted as expected\",\n\t\t\tcfg: &config.WebexConfig{\n\t\t\t\tMessage: `{{ template \"webex.default.message\" . }}`,\n\t\t\t},\n\t\t\tcommonCfg: &commoncfg.HTTPClientConfig{\n\t\t\t\tAuthorization: &commoncfg.Authorization{Type: \"Bearer\", Credentials: \"anewsecret\"},\n\t\t\t},\n\n\t\t\texpJSON:   `{\"markdown\":\"\\n\\nAlerts Firing:\\nLabels:\\n - lbl1 = val1\\n - lbl3 = val3\\nAnnotations:\\nSource: \\nLabels:\\n - lbl1 = val1\\n - lbl2 = val2\\nAnnotations:\\nSource: \\n\\n\\n\\n\"}`,\n\t\t\tretry:     false,\n\t\t\texpHeader: \"Bearer anewsecret\",\n\t\t},\n\t\t{\n\t\t\tname: \"with message templating errors, it fails.\",\n\t\t\tcfg: &config.WebexConfig{\n\t\t\t\tMessage: \"{{ \",\n\t\t\t},\n\t\t\tcommonCfg: &commoncfg.HTTPClientConfig{},\n\t\t\terrMsg:    \"template: :1: unclosed action\",\n\t\t},\n\t\t{\n\t\t\tname: \"with a valid roomID set, the roomID is used accordingly.\",\n\t\t\tcfg: &config.WebexConfig{\n\t\t\t\tRoomID: \"my-room-id\",\n\t\t\t},\n\t\t\tcommonCfg: &commoncfg.HTTPClientConfig{},\n\t\t\texpJSON:   `{\"markdown\":\"\", \"roomId\":\"my-room-id\"}`,\n\t\t\tretry:     false,\n\t\t},\n\t\t{\n\t\t\tname: \"with a valid roomID template, the roomID is used accordingly.\",\n\t\t\tcfg: &config.WebexConfig{\n\t\t\t\tRoomID: \"{{.GroupLabels.webex_room_id}}\",\n\t\t\t},\n\t\t\tcommonCfg: &commoncfg.HTTPClientConfig{},\n\t\t\texpJSON:   `{\"markdown\":\"\", \"roomId\":\"group-label-room-id\"}`,\n\t\t\tretry:     false,\n\t\t},\n\t}\n\n\tfor _, tt := range tc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar out []byte\n\t\t\tvar header http.Header\n\t\t\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tvar err error\n\t\t\t\tout, err = io.ReadAll(r.Body)\n\t\t\t\theader = r.Header.Clone()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}))\n\t\t\tdefer srv.Close()\n\t\t\tu, _ := url.Parse(srv.URL)\n\n\t\t\ttt.cfg.APIURL = &amcommoncfg.URL{URL: u}\n\t\t\ttt.cfg.HTTPConfig = tt.commonCfg\n\t\t\tnotifierWebex, err := New(tt.cfg, test.CreateTmpl(t), promslog.NewNopLogger())\n\t\t\trequire.NoError(t, err)\n\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\t\t\tdefer cancel()\n\t\t\tctx = notify.WithGroupKey(ctx, \"1\")\n\t\t\tctx = notify.WithGroupLabels(ctx, model.LabelSet{\"webex_room_id\": \"group-label-room-id\"})\n\n\t\t\tok, err := notifierWebex.Notify(ctx, []*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"lbl1\": \"val1\",\n\t\t\t\t\t\t\t\"lbl3\": \"val3\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\t\"lbl1\": \"val1\",\n\t\t\t\t\t\t\t\"lbl2\": \"val2\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}...)\n\n\t\t\tif tt.errMsg == \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, tt.expHeader, header.Get(\"Authorization\"))\n\t\t\t\trequire.JSONEq(t, tt.expJSON, string(out))\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), tt.errMsg)\n\t\t\t}\n\n\t\t\trequire.Equal(t, tt.retry, ok)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "notify/webhook/webhook.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage webhook\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// Notifier implements a Notifier for generic webhooks.\ntype Notifier struct {\n\tconf    *config.WebhookConfig\n\ttmpl    *template.Template\n\tlogger  *slog.Logger\n\tclient  *http.Client\n\tretrier *notify.Retrier\n}\n\n// New returns a new Webhook.\nfunc New(conf *config.WebhookConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {\n\tclient, err := notify.NewClientWithTracing(*conf.HTTPConfig, \"webhook\", httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Notifier{\n\t\tconf:   conf,\n\t\ttmpl:   t,\n\t\tlogger: l,\n\t\tclient: client,\n\t\t// Webhooks are assumed to respond with 2xx response codes on a successful\n\t\t// request and 5xx response codes are assumed to be recoverable.\n\t\tretrier: &notify.Retrier{},\n\t}, nil\n}\n\n// Message defines the JSON object send to webhook endpoints.\ntype Message struct {\n\t*template.Data\n\n\t// The protocol version.\n\tVersion         string `json:\"version\"`\n\tGroupKey        string `json:\"groupKey\"`\n\tTruncatedAlerts uint64 `json:\"truncatedAlerts\"`\n}\n\nfunc truncateAlerts(maxAlerts uint64, alerts []*types.Alert) ([]*types.Alert, uint64) {\n\tif maxAlerts != 0 && uint64(len(alerts)) > maxAlerts {\n\t\treturn alerts[:maxAlerts], uint64(len(alerts)) - maxAlerts\n\t}\n\n\treturn alerts, 0\n}\n\n// Notify implements the Notifier interface.\nfunc (n *Notifier) Notify(ctx context.Context, alerts ...*types.Alert) (bool, error) {\n\talerts, numTruncated := truncateAlerts(n.conf.MaxAlerts, alerts)\n\tdata := notify.GetTemplateData(ctx, n.tmpl, alerts, n.logger)\n\n\tgroupKey, err := notify.ExtractGroupKey(ctx)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tlogger := n.logger.With(\"group_key\", groupKey)\n\tlogger.Debug(\"extracted group key\")\n\n\tmsg := &Message{\n\t\tVersion:         \"4\",\n\t\tData:            data,\n\t\tGroupKey:        groupKey.String(),\n\t\tTruncatedAlerts: numTruncated,\n\t}\n\n\tvar buf bytes.Buffer\n\tif err := json.NewEncoder(&buf).Encode(msg); err != nil {\n\t\treturn false, err\n\t}\n\n\t// Override the payload if a custom one is configured.\n\tif n.conf.Payload != nil {\n\t\tbuf, err = n.renderPayload(msg)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to render custom payload: %w\", err)\n\t\t}\n\t}\n\n\tvar url string\n\tvar tmplErr error\n\ttmpl := notify.TmplText(n.tmpl, data, &tmplErr)\n\n\tif n.conf.URL != \"\" {\n\t\turl = tmpl(string(n.conf.URL))\n\t} else {\n\t\tcontent, err := os.ReadFile(n.conf.URLFile)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"read url_file: %w\", err)\n\t\t}\n\t\turl = tmpl(strings.TrimSpace(string(content)))\n\t}\n\n\tif tmplErr != nil {\n\t\treturn false, fmt.Errorf(\"failed to template webhook URL: %w\", tmplErr)\n\t}\n\n\tif url == \"\" {\n\t\treturn false, errors.New(\"webhook URL is empty after templating\")\n\t}\n\n\tif n.conf.Timeout > 0 {\n\t\tpostCtx, cancel := context.WithTimeoutCause(ctx, n.conf.Timeout, fmt.Errorf(\"configured webhook timeout reached (%s)\", n.conf.Timeout))\n\t\tdefer cancel()\n\t\tctx = postCtx\n\t}\n\n\tresp, err := notify.PostJSON(ctx, n.client, url, &buf)\n\tif err != nil {\n\t\tif ctx.Err() != nil {\n\t\t\terr = fmt.Errorf(\"%w: %w\", err, context.Cause(ctx))\n\t\t}\n\t\treturn true, notify.RedactURL(err)\n\t}\n\tdefer notify.Drain(resp)\n\n\tshouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)\n\tif err != nil {\n\t\treturn shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)\n\t}\n\treturn shouldRetry, err\n}\n\nfunc (n *Notifier) renderPayload(\n\tdata *Message,\n) (bytes.Buffer, error) {\n\tvar (\n\t\ttmplTextErr  error\n\t\ttmplText     = notify.TmplText(n.tmpl, data.Data, &tmplTextErr)\n\t\ttmplTextFunc = func(tmpl string) (string, error) {\n\t\t\treturn tmplText(tmpl), tmplTextErr\n\t\t}\n\t)\n\tvar err error\n\trendered := make(map[string]any, len(n.conf.Payload))\n\tfor k, v := range n.conf.Payload {\n\t\trendered[k], err = template.DeepCopyWithTemplate(v, tmplTextFunc)\n\t\tif err != nil {\n\t\t\treturn bytes.Buffer{}, err\n\t\t}\n\t}\n\tvar buf bytes.Buffer\n\tif err := json.NewEncoder(&buf).Encode(rendered); err != nil {\n\t\treturn bytes.Buffer{}, err\n\t}\n\treturn buf, nil\n}\n"
  },
  {
    "path": "notify/webhook/webhook_test.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage webhook\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/notify/test\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nfunc TestWebhookRetry(t *testing.T) {\n\tnotifier, err := New(\n\t\t&config.WebhookConfig{\n\t\t\tURL:        config.SecretTemplateURL(\"http://example.com\"),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\tif err != nil {\n\t\trequire.NoError(t, err)\n\t}\n\n\tt.Run(\"test retry status code\", func(t *testing.T) {\n\t\tfor statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) {\n\t\t\tactual, _ := notifier.retrier.Check(statusCode, nil)\n\t\t\trequire.Equal(t, expected, actual, \"error on status %d\", statusCode)\n\t\t}\n\t})\n\n\tt.Run(\"test retry error details\", func(t *testing.T) {\n\t\tfor _, tc := range []struct {\n\t\t\tstatus int\n\t\t\tbody   io.Reader\n\n\t\t\texp string\n\t\t}{\n\t\t\t{\n\t\t\t\tstatus: http.StatusBadRequest,\n\t\t\t\tbody: bytes.NewBuffer([]byte(\n\t\t\t\t\t`{\"status\":\"invalid event\"}`,\n\t\t\t\t)),\n\n\t\t\t\texp: fmt.Sprintf(`unexpected status code %d: {\"status\":\"invalid event\"}`, http.StatusBadRequest),\n\t\t\t},\n\t\t\t{\n\t\t\t\tstatus: http.StatusBadRequest,\n\n\t\t\t\texp: fmt.Sprintf(`unexpected status code %d`, http.StatusBadRequest),\n\t\t\t},\n\t\t} {\n\t\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\t\t_, err = notifier.retrier.Check(tc.status, tc.body)\n\t\t\t\trequire.Equal(t, tc.exp, err.Error())\n\t\t\t})\n\t\t}\n\t})\n}\n\nfunc TestWebhookTruncateAlerts(t *testing.T) {\n\talerts := make([]*types.Alert, 10)\n\n\ttruncatedAlerts, numTruncated := truncateAlerts(0, alerts)\n\trequire.Len(t, truncatedAlerts, 10)\n\trequire.EqualValues(t, 0, numTruncated)\n\n\ttruncatedAlerts, numTruncated = truncateAlerts(4, alerts)\n\trequire.Len(t, truncatedAlerts, 4)\n\trequire.EqualValues(t, 6, numTruncated)\n\n\ttruncatedAlerts, numTruncated = truncateAlerts(100, alerts)\n\trequire.Len(t, truncatedAlerts, 10)\n\trequire.EqualValues(t, 0, numTruncated)\n}\n\nfunc TestWebhookRedactedURL(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tsecret := \"secret\"\n\tnotifier, err := New(\n\t\t&config.WebhookConfig{\n\t\t\tURL:        config.SecretTemplateURL(u.String()),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret)\n}\n\nfunc TestWebhookReadingURLFromFile(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tf, err := os.CreateTemp(t.TempDir(), \"webhook_url\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\t_, err = f.WriteString(u.String() + \"\\n\")\n\trequire.NoError(t, err, \"writing to temp file failed\")\n\n\tnotifier, err := New(\n\t\t&config.WebhookConfig{\n\t\t\tURLFile:    f.Name(),\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String())\n}\n\nfunc TestWebhookURLTemplating(t *testing.T) {\n\tvar calledURL string\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tcalledURL = r.URL.Path\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer srv.Close()\n\n\ttests := []struct {\n\t\tname           string\n\t\turl            string\n\t\tgroupLabels    model.LabelSet\n\t\talertLabels    model.LabelSet\n\t\texpectError    bool\n\t\texpectedErrMsg string\n\t\texpectedPath   string\n\t}{\n\t\t{\n\t\t\tname:         \"templating with alert labels\",\n\t\t\turl:          srv.URL + \"/{{ .GroupLabels.alertname }}/{{ .CommonLabels.severity }}\",\n\t\t\tgroupLabels:  model.LabelSet{\"alertname\": \"TestAlert\"},\n\t\t\talertLabels:  model.LabelSet{\"alertname\": \"TestAlert\", \"severity\": \"critical\"},\n\t\t\texpectError:  false,\n\t\t\texpectedPath: \"/TestAlert/critical\",\n\t\t},\n\t\t{\n\t\t\tname:           \"invalid template field\",\n\t\t\turl:            srv.URL + \"/{{ .InvalidField }}\",\n\t\t\tgroupLabels:    model.LabelSet{\"alertname\": \"TestAlert\"},\n\t\t\talertLabels:    model.LabelSet{\"alertname\": \"TestAlert\"},\n\t\t\texpectError:    true,\n\t\t\texpectedErrMsg: \"failed to template webhook URL\",\n\t\t},\n\t\t{\n\t\t\tname:           \"template renders to empty string\",\n\t\t\turl:            \"{{ if .CommonLabels.nonexistent }}http://example.com{{ end }}\",\n\t\t\tgroupLabels:    model.LabelSet{\"alertname\": \"TestAlert\"},\n\t\t\talertLabels:    model.LabelSet{\"alertname\": \"TestAlert\"},\n\t\t\texpectError:    true,\n\t\t\texpectedErrMsg: \"webhook URL is empty after templating\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tcalledURL = \"\" // Reset for each test\n\n\t\t\tnotifier, err := New(\n\t\t\t\t&config.WebhookConfig{\n\t\t\t\t\tURL:        config.SecretTemplateURL(tc.url),\n\t\t\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\t\t},\n\t\t\t\ttest.CreateTmpl(t),\n\t\t\t\tpromslog.NewNopLogger(),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tctx := context.Background()\n\t\t\tctx = notify.WithGroupKey(ctx, \"test-group\")\n\t\t\tif tc.groupLabels != nil {\n\t\t\t\tctx = notify.WithGroupLabels(ctx, tc.groupLabels)\n\t\t\t}\n\n\t\t\talerts := []*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels:   tc.alertLabels,\n\t\t\t\t\t\tStartsAt: time.Now(),\n\t\t\t\t\t\tEndsAt:   time.Now().Add(time.Hour),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\t_, err = notifier.Notify(ctx, alerts...)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), tc.expectedErrMsg)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, tc.expectedPath, calledURL)\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype roundTripFunc func(req *http.Request) *http.Response\n\nfunc (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {\n\treturn f(req), nil\n}\n\n// TestWebhookDefaultPayload tests that the default payload sent by the webhook notifier matches\n// the behaviour before introducing templating.\nfunc TestWebhookDefaultPayload(t *testing.T) {\n\tvar capturedPayload []byte\n\n\tmockTransport := roundTripFunc(func(req *http.Request) *http.Response {\n\t\tvar err error\n\t\tcapturedPayload, err = io.ReadAll(req.Body)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn &http.Response{\n\t\t\tStatusCode: http.StatusOK,\n\t\t\tBody:       http.NoBody,\n\t\t}\n\t})\n\n\tu, err := url.Parse(\"http://localhost\")\n\trequire.NoError(t, err)\n\n\tconf := &config.WebhookConfig{\n\t\tURL:        config.SecretTemplateURL(u.String()),\n\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t}\n\n\talerts := []*types.Alert{\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels:       model.LabelSet{\"alertname\": \"TestAlert\"},\n\t\t\t\tAnnotations:  model.LabelSet{\"summary\": \"Test summary\"},\n\t\t\t\tStartsAt:     time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t\t\tEndsAt:       time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC),\n\t\t\t\tGeneratorURL: \"http://generator.url\",\n\t\t\t},\n\t\t},\n\t}\n\ttmpl := test.CreateTmpl(t)\n\tctx := notify.WithGroupKey(context.Background(), \"{}:{alertname=\\\"test1\\\"}\")\n\tctx = notify.WithReceiverName(ctx, \"test_receiver\")\n\tdata := notify.GetTemplateData(ctx, tmpl, alerts, promslog.NewNopLogger())\n\n\tmsg := &Message{\n\t\tVersion:  \"4\",\n\t\tData:     data,\n\t\tGroupKey: \"{}:{alertname=\\\"test1\\\"}\",\n\t}\n\n\tvar buf bytes.Buffer\n\tjson.NewEncoder(&buf).Encode(msg)\n\tn, err := New(conf, tmpl, promslog.NewNopLogger())\n\trequire.NoError(t, err)\n\tn.client.Transport = mockTransport\n\t_, err = n.Notify(ctx, alerts...)\n\trequire.NoError(t, err)\n\n\trequire.NotEmpty(t, capturedPayload)\n\trequire.JSONEq(t, buf.String(), string(capturedPayload))\n}\n\nfunc TestWebhookCustomPayload(t *testing.T) {\n\tvar capturedPayload []byte\n\n\tmockTransport := roundTripFunc(func(req *http.Request) *http.Response {\n\t\tvar err error\n\t\tcapturedPayload, err = io.ReadAll(req.Body)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn &http.Response{\n\t\t\tStatusCode: http.StatusOK,\n\t\t\tBody:       http.NoBody,\n\t\t}\n\t})\n\n\tu, err := url.Parse(\"http://localhost\")\n\trequire.NoError(t, err)\n\n\tconf := &config.WebhookConfig{\n\t\tURL:        config.SecretTemplateURL(u.String()),\n\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\tPayload: map[string]any{\n\t\t\t\"custom\":       `some custom content`,\n\t\t\t\"commonLabels\": \"{{ .CommonLabels  | toJson }}\",\n\t\t},\n\t}\n\n\talerts := []*types.Alert{\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels:       model.LabelSet{\"alertname\": \"TestAlert\"},\n\t\t\t\tAnnotations:  model.LabelSet{\"summary\": \"Test summary\"},\n\t\t\t\tStartsAt:     time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),\n\t\t\t\tEndsAt:       time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC),\n\t\t\t\tGeneratorURL: \"http://generator.url\",\n\t\t\t},\n\t\t},\n\t}\n\ttmpl := test.CreateTmpl(t)\n\tctx := notify.WithGroupKey(context.Background(), \"{}:{alertname=\\\"test1\\\"}\")\n\tctx = notify.WithReceiverName(ctx, \"test_receiver\")\n\n\tmsg := map[string]any{\n\t\t\"custom\":       `some custom content`,\n\t\t\"commonLabels\": map[string]string{\"alertname\": \"TestAlert\"},\n\t}\n\n\tvar buf bytes.Buffer\n\tjson.NewEncoder(&buf).Encode(msg)\n\tn, err := New(conf, tmpl, promslog.NewNopLogger())\n\trequire.NoError(t, err)\n\tn.client.Transport = mockTransport\n\t_, err = n.Notify(ctx, alerts...)\n\trequire.NoError(t, err)\n\n\trequire.NotEmpty(t, capturedPayload)\n\trequire.JSONEq(t, buf.String(), string(capturedPayload))\n}\n"
  },
  {
    "path": "notify/wechat/wechat.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage wechat\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify\"\n\t\"github.com/prometheus/alertmanager/template\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// Notifier implements a Notifier for wechat notifications.\ntype Notifier struct {\n\tconf   *config.WechatConfig\n\ttmpl   *template.Template\n\tlogger *slog.Logger\n\tclient *http.Client\n\n\taccessToken   string\n\taccessTokenAt time.Time\n}\n\n// token is the AccessToken with corpid and corpsecret.\ntype token struct {\n\tAccessToken string `json:\"access_token\"`\n}\n\ntype weChatMessage struct {\n\tText     weChatMessageContent `yaml:\"text,omitempty\" json:\"text,omitempty\"`\n\tToUser   string               `yaml:\"touser,omitempty\" json:\"touser,omitempty\"`\n\tToParty  string               `yaml:\"toparty,omitempty\" json:\"toparty,omitempty\"`\n\tTotag    string               `yaml:\"totag,omitempty\" json:\"totag,omitempty\"`\n\tAgentID  string               `yaml:\"agentid,omitempty\" json:\"agentid,omitempty\"`\n\tSafe     string               `yaml:\"safe,omitempty\" json:\"safe,omitempty\"`\n\tType     string               `yaml:\"msgtype,omitempty\" json:\"msgtype,omitempty\"`\n\tMarkdown weChatMessageContent `yaml:\"markdown,omitempty\" json:\"markdown,omitempty\"`\n}\n\ntype weChatMessageContent struct {\n\tContent string `json:\"content\"`\n}\n\ntype weChatResponse struct {\n\tCode  int    `json:\"errcode\"`\n\tError string `json:\"errmsg\"`\n}\n\n// New returns a new Wechat notifier.\nfunc New(c *config.WechatConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {\n\tclient, err := notify.NewClientWithTracing(*c.HTTPConfig, \"wechat\", httpOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Notifier{conf: c, tmpl: t, logger: l, client: client}, nil\n}\n\n// Notify implements the Notifier interface.\nfunc (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {\n\tkey, err := notify.ExtractGroupKey(ctx)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tlogger := n.logger.With(\"group_key\", key)\n\tlogger.Debug(\"extracted group key\")\n\n\tdata := notify.GetTemplateData(ctx, n.tmpl, as, logger)\n\n\ttmpl := notify.TmplText(n.tmpl, data, &err)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t// Refresh AccessToken over 2 hours\n\tif n.accessToken == \"\" || time.Since(n.accessTokenAt) > 2*time.Hour {\n\t\tparameters := url.Values{}\n\t\tapiSecret, err := n.getApiSecret()\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tparameters.Add(\"corpsecret\", tmpl(apiSecret))\n\t\tparameters.Add(\"corpid\", tmpl(string(n.conf.CorpID)))\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"templating error: %w\", err)\n\t\t}\n\n\t\tu := n.conf.APIURL.Copy()\n\t\tu.Path += \"gettoken\"\n\t\tu.RawQuery = parameters.Encode()\n\n\t\tresp, err := notify.Get(ctx, n.client, u.String())\n\t\tif err != nil {\n\t\t\treturn true, notify.RedactURL(err)\n\t\t}\n\t\tdefer notify.Drain(resp)\n\n\t\tvar wechatToken token\n\t\tif err := json.NewDecoder(resp.Body).Decode(&wechatToken); err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tif wechatToken.AccessToken == \"\" {\n\t\t\treturn false, fmt.Errorf(\"invalid APISecret for CorpID: %s\", n.conf.CorpID)\n\t\t}\n\n\t\t// Cache accessToken\n\t\tn.accessToken = wechatToken.AccessToken\n\t\tn.accessTokenAt = time.Now()\n\t}\n\n\tmsg := &weChatMessage{\n\t\tToUser:  tmpl(n.conf.ToUser),\n\t\tToParty: tmpl(n.conf.ToParty),\n\t\tTotag:   tmpl(n.conf.ToTag),\n\t\tAgentID: tmpl(n.conf.AgentID),\n\t\tType:    n.conf.MessageType,\n\t\tSafe:    \"0\",\n\t}\n\n\tif msg.Type == \"markdown\" {\n\t\tmsg.Markdown = weChatMessageContent{\n\t\t\tContent: tmpl(n.conf.Message),\n\t\t}\n\t} else {\n\t\tmsg.Text = weChatMessageContent{\n\t\t\tContent: tmpl(n.conf.Message),\n\t\t}\n\t}\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"templating error: %w\", err)\n\t}\n\n\tvar buf bytes.Buffer\n\tif err := json.NewEncoder(&buf).Encode(msg); err != nil {\n\t\treturn false, err\n\t}\n\n\tpostMessageURL := n.conf.APIURL.Copy()\n\tpostMessageURL.Path += \"message/send\"\n\tq := postMessageURL.Query()\n\tq.Set(\"access_token\", n.accessToken)\n\tpostMessageURL.RawQuery = q.Encode()\n\n\tresp, err := notify.PostJSON(ctx, n.client, postMessageURL.String(), &buf)\n\tif err != nil {\n\t\treturn true, notify.RedactURL(err)\n\t}\n\tdefer notify.Drain(resp)\n\n\tif resp.StatusCode != 200 {\n\t\treturn true, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), fmt.Errorf(\"unexpected status code %v\", resp.StatusCode))\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn true, err\n\t}\n\tlogger.Debug(string(body))\n\n\tvar weResp weChatResponse\n\tif err := json.Unmarshal(body, &weResp); err != nil {\n\t\treturn true, err\n\t}\n\n\t// https://work.weixin.qq.com/api/doc#10649\n\tif weResp.Code == 0 {\n\t\treturn false, nil\n\t}\n\n\t// AccessToken is expired\n\tif weResp.Code == 42001 {\n\t\tn.accessToken = \"\"\n\t\treturn true, errors.New(weResp.Error)\n\t}\n\n\treturn false, errors.New(weResp.Error)\n}\n\nfunc (n *Notifier) getApiSecret() (string, error) {\n\tif len(n.conf.APISecretFile) > 0 {\n\t\tcontent, err := os.ReadFile(n.conf.APISecretFile)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn strings.TrimSpace(string(content)), nil\n\t}\n\treturn string(n.conf.APISecret), nil\n}\n"
  },
  {
    "path": "notify/wechat/wechat_test.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage wechat\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"testing\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\tamcommoncfg \"github.com/prometheus/alertmanager/config/common\"\n\n\t\"github.com/prometheus/alertmanager/config\"\n\t\"github.com/prometheus/alertmanager/notify/test\"\n)\n\nfunc TestWechatRedactedURLOnInitialAuthentication(t *testing.T) {\n\tctx, u, fn := test.GetContextWithCancelingURL()\n\tdefer fn()\n\n\tsecret := \"secret_key\"\n\tnotifier, err := New(\n\t\t&config.WechatConfig{\n\t\t\tAPIURL:     &amcommoncfg.URL{URL: u},\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\tCorpID:     \"corpid\",\n\t\t\tAPISecret:  commoncfg.Secret(secret),\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret)\n}\n\nfunc TestWechatRedactedURLOnNotify(t *testing.T) {\n\tsecret, token := \"secret\", \"token\"\n\tctx, u, fn := test.GetContextWithCancelingURL(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprintf(w, `{\"access_token\":\"%s\"}`, token)\n\t})\n\tdefer fn()\n\n\tnotifier, err := New(\n\t\t&config.WechatConfig{\n\t\t\tAPIURL:     &amcommoncfg.URL{URL: u},\n\t\t\tHTTPConfig: &commoncfg.HTTPClientConfig{},\n\t\t\tCorpID:     \"corpid\",\n\t\t\tAPISecret:  commoncfg.Secret(secret),\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret, token)\n}\n\nfunc TestWechatMessageTypeSelector(t *testing.T) {\n\tsecret, token := \"secret\", \"token\"\n\tctx, u, fn := test.GetContextWithCancelingURL(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprintf(w, `{\"access_token\":\"%s\"}`, token)\n\t})\n\tdefer fn()\n\n\tnotifier, err := New(\n\t\t&config.WechatConfig{\n\t\t\tAPIURL:      &amcommoncfg.URL{URL: u},\n\t\t\tHTTPConfig:  &commoncfg.HTTPClientConfig{},\n\t\t\tCorpID:      \"corpid\",\n\t\t\tAPISecret:   commoncfg.Secret(secret),\n\t\t\tMessageType: \"markdown\",\n\t\t},\n\t\ttest.CreateTmpl(t),\n\t\tpromslog.NewNopLogger(),\n\t)\n\trequire.NoError(t, err)\n\n\ttest.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret, token)\n}\n\nfunc TestGetApiSecretFromSecret(t *testing.T) {\n\tn := &Notifier{conf: &config.WechatConfig{APISecret: commoncfg.Secret(\"shhh\")}}\n\ts, err := n.getApiSecret()\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"shhh\", s)\n}\n\nfunc TestGetApiSecretFromFile(t *testing.T) {\n\ttmpFile, err := os.CreateTemp(t.TempDir(), \"wechat-secret-*\")\n\trequire.NoError(t, err)\n\tsecretContent := \"file-secret\\n\"\n\t_, err = tmpFile.WriteString(secretContent)\n\trequire.NoError(t, err)\n\trequire.NoError(t, tmpFile.Close())\n\n\tn := &Notifier{conf: &config.WechatConfig{APISecretFile: tmpFile.Name()}}\n\ts, err := n.getApiSecret()\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"file-secret\", s)\n}\n\nfunc TestGetApiSecretFromMissingFile(t *testing.T) {\n\tn := &Notifier{conf: &config.WechatConfig{APISecretFile: \"/non/existent/wechat-secret.txt\"}}\n\ts, err := n.getApiSecret()\n\tvar pathErr *os.PathError\n\trequire.ErrorAs(t, err, &pathErr)\n\trequire.Equal(t, \"/non/existent/wechat-secret.txt\", pathErr.Path)\n\trequire.ErrorIs(t, err, os.ErrNotExist)\n\trequire.Empty(t, s)\n}\n"
  },
  {
    "path": "pkg/README.md",
    "content": "The `pkg` directory is deprecated.\nPlease do not add new packages to this directory.\nExisting packages will be moved elsewhere eventually.\n"
  },
  {
    "path": "pkg/labels/matcher.go",
    "content": "// Copyright 2017 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage labels\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/prometheus/common/model\"\n)\n\n// MatchType is an enum for label matching types.\ntype MatchType int\n\n// Possible MatchTypes.\nconst (\n\tMatchEqual MatchType = iota\n\tMatchNotEqual\n\tMatchRegexp\n\tMatchNotRegexp\n)\n\nfunc (m MatchType) String() string {\n\ttypeToStr := map[MatchType]string{\n\t\tMatchEqual:     \"=\",\n\t\tMatchNotEqual:  \"!=\",\n\t\tMatchRegexp:    \"=~\",\n\t\tMatchNotRegexp: \"!~\",\n\t}\n\tif str, ok := typeToStr[m]; ok {\n\t\treturn str\n\t}\n\tpanic(\"unknown match type\")\n}\n\n// Matcher models the matching of a label.\ntype Matcher struct {\n\tType  MatchType\n\tName  string\n\tValue string\n\n\tre *regexp.Regexp\n}\n\n// NewMatcher returns a matcher object.\nfunc NewMatcher(t MatchType, n, v string) (*Matcher, error) {\n\tm := &Matcher{\n\t\tType:  t,\n\t\tName:  n,\n\t\tValue: v,\n\t}\n\tif t == MatchRegexp || t == MatchNotRegexp {\n\t\tre, err := regexp.Compile(\"^(?:\" + v + \")$\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tm.re = re\n\t}\n\treturn m, nil\n}\n\nfunc (m *Matcher) String() string {\n\tif strings.ContainsFunc(m.Name, isReserved) {\n\t\treturn fmt.Sprintf(`%s%s%s`, strconv.Quote(m.Name), m.Type, strconv.Quote(m.Value))\n\t}\n\treturn fmt.Sprintf(`%s%s\"%s\"`, m.Name, m.Type, openMetricsEscape(m.Value))\n}\n\n// Matches returns whether the matcher matches the given string value.\nfunc (m *Matcher) Matches(s string) bool {\n\tswitch m.Type {\n\tcase MatchEqual:\n\t\treturn s == m.Value\n\tcase MatchNotEqual:\n\t\treturn s != m.Value\n\tcase MatchRegexp:\n\t\treturn m.re.MatchString(s)\n\tcase MatchNotRegexp:\n\t\treturn !m.re.MatchString(s)\n\t}\n\tpanic(\"labels.Matcher.Matches: invalid match type\")\n}\n\ntype apiV1Matcher struct {\n\tName    string `json:\"name\"`\n\tValue   string `json:\"value\"`\n\tIsRegex bool   `json:\"isRegex\"`\n\tIsEqual bool   `json:\"isEqual\"`\n}\n\n// MarshalJSON retains backwards compatibility with types.Matcher for the v1 API.\nfunc (m Matcher) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(apiV1Matcher{\n\t\tName:    m.Name,\n\t\tValue:   m.Value,\n\t\tIsRegex: m.Type == MatchRegexp || m.Type == MatchNotRegexp,\n\t\tIsEqual: m.Type == MatchRegexp || m.Type == MatchEqual,\n\t})\n}\n\nfunc (m *Matcher) UnmarshalJSON(data []byte) error {\n\tv1m := apiV1Matcher{\n\t\tIsEqual: true,\n\t}\n\n\tif err := json.Unmarshal(data, &v1m); err != nil {\n\t\treturn err\n\t}\n\n\tvar t MatchType\n\tswitch {\n\tcase v1m.IsEqual && !v1m.IsRegex:\n\t\tt = MatchEqual\n\tcase !v1m.IsEqual && !v1m.IsRegex:\n\t\tt = MatchNotEqual\n\tcase v1m.IsEqual && v1m.IsRegex:\n\t\tt = MatchRegexp\n\tcase !v1m.IsEqual && v1m.IsRegex:\n\t\tt = MatchNotRegexp\n\t}\n\n\tmatcher, err := NewMatcher(t, v1m.Name, v1m.Value)\n\tif err != nil {\n\t\treturn err\n\t}\n\t*m = *matcher\n\treturn nil\n}\n\n// openMetricsEscape is similar to the usual string escaping, but more\n// restricted. It merely replaces a new-line character with '\\n', a double-quote\n// character with '\\\"', and a backslash with '\\\\', which is the escaping used by\n// OpenMetrics.\nfunc openMetricsEscape(s string) string {\n\tr := strings.NewReplacer(\n\t\t`\\`, `\\\\`,\n\t\t\"\\n\", `\\n`,\n\t\t`\"`, `\\\"`,\n\t)\n\treturn r.Replace(s)\n}\n\n// Matchers is a slice of Matchers that is sortable, implements Stringer, and\n// provides a Matches method to match a LabelSet against all Matchers in the\n// slice. Note that some users of Matchers might require it to be sorted.\ntype Matchers []*Matcher\n\nfunc (ms Matchers) Len() int      { return len(ms) }\nfunc (ms Matchers) Swap(i, j int) { ms[i], ms[j] = ms[j], ms[i] }\n\nfunc (ms Matchers) Less(i, j int) bool {\n\tif ms[i].Name > ms[j].Name {\n\t\treturn false\n\t}\n\tif ms[i].Name < ms[j].Name {\n\t\treturn true\n\t}\n\tif ms[i].Value > ms[j].Value {\n\t\treturn false\n\t}\n\tif ms[i].Value < ms[j].Value {\n\t\treturn true\n\t}\n\treturn ms[i].Type < ms[j].Type\n}\n\n// Matches checks whether all matchers are fulfilled against the given label set.\nfunc (ms Matchers) Matches(lset model.LabelSet) bool {\n\tfor _, m := range ms {\n\t\tif !m.Matches(string(lset[model.LabelName(m.Name)])) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (ms Matchers) String() string {\n\tvar buf bytes.Buffer\n\n\tbuf.WriteByte('{')\n\tfor i, m := range ms {\n\t\tif i > 0 {\n\t\t\tbuf.WriteByte(',')\n\t\t}\n\t\tbuf.WriteString(m.String())\n\t}\n\tbuf.WriteByte('}')\n\n\treturn buf.String()\n}\n\n// MatcherSet is a slice of Matchers pointers that implements OR logic across\n// multiple matcher sets. At least one matcher set must match for the MatcherSet\n// to match.\ntype MatcherSet []*Matchers\n\n// Matches checks whether at least one matcher set is fulfilled against the given\n// label set (OR logic across matcher sets, AND logic within each set).\nfunc (ms MatcherSet) Matches(lset model.LabelSet) bool {\n\tfor _, matchers := range ms {\n\t\tif (*matchers).Matches(lset) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// This is copied from matcher/parse/lexer.go. It will be removed when\n// the transition window from classic matchers to UTF-8 matchers is complete,\n// as then we can use double quotes when printing the label name for all\n// matchers. Until then, the classic parser does not understand double quotes\n// around the label name, so we use this function as a heuristic to tell if\n// the matcher was parsed with the UTF-8 parser or the classic parser.\nfunc isReserved(r rune) bool {\n\treturn unicode.IsSpace(r) || strings.ContainsRune(\"{}!=~,\\\\\\\"'`\", r)\n}\n"
  },
  {
    "path": "pkg/labels/matcher_test.go",
    "content": "// Copyright 2017 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage labels\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n)\n\nfunc mustNewMatcher(t *testing.T, mType MatchType, value string) *Matcher {\n\tm, err := NewMatcher(mType, \"\", value)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn m\n}\n\nfunc TestMatcher(t *testing.T) {\n\ttests := []struct {\n\t\tmatcher *Matcher\n\t\tvalue   string\n\t\tmatch   bool\n\t}{\n\t\t{\n\t\t\tmatcher: mustNewMatcher(t, MatchEqual, \"bar\"),\n\t\t\tvalue:   \"bar\",\n\t\t\tmatch:   true,\n\t\t},\n\t\t{\n\t\t\tmatcher: mustNewMatcher(t, MatchEqual, \"bar\"),\n\t\t\tvalue:   \"foo-bar\",\n\t\t\tmatch:   false,\n\t\t},\n\t\t{\n\t\t\tmatcher: mustNewMatcher(t, MatchNotEqual, \"bar\"),\n\t\t\tvalue:   \"bar\",\n\t\t\tmatch:   false,\n\t\t},\n\t\t{\n\t\t\tmatcher: mustNewMatcher(t, MatchNotEqual, \"bar\"),\n\t\t\tvalue:   \"foo-bar\",\n\t\t\tmatch:   true,\n\t\t},\n\t\t{\n\t\t\tmatcher: mustNewMatcher(t, MatchRegexp, \"bar\"),\n\t\t\tvalue:   \"bar\",\n\t\t\tmatch:   true,\n\t\t},\n\t\t{\n\t\t\tmatcher: mustNewMatcher(t, MatchRegexp, \"bar\"),\n\t\t\tvalue:   \"foo-bar\",\n\t\t\tmatch:   false,\n\t\t},\n\t\t{\n\t\t\tmatcher: mustNewMatcher(t, MatchRegexp, \".*bar\"),\n\t\t\tvalue:   \"foo-bar\",\n\t\t\tmatch:   true,\n\t\t},\n\t\t{\n\t\t\tmatcher: mustNewMatcher(t, MatchNotRegexp, \"bar\"),\n\t\t\tvalue:   \"bar\",\n\t\t\tmatch:   false,\n\t\t},\n\t\t{\n\t\t\tmatcher: mustNewMatcher(t, MatchNotRegexp, \"bar\"),\n\t\t\tvalue:   \"foo-bar\",\n\t\t\tmatch:   true,\n\t\t},\n\t\t{\n\t\t\tmatcher: mustNewMatcher(t, MatchNotRegexp, \".*bar\"),\n\t\t\tvalue:   \"foo-bar\",\n\t\t\tmatch:   false,\n\t\t},\n\t\t{\n\t\t\tmatcher: mustNewMatcher(t, MatchRegexp, `foo.bar`),\n\t\t\tvalue:   \"foo-bar\",\n\t\t\tmatch:   true,\n\t\t},\n\t\t{\n\t\t\tmatcher: mustNewMatcher(t, MatchRegexp, `foo\\.bar`),\n\t\t\tvalue:   \"foo-bar\",\n\t\t\tmatch:   false,\n\t\t},\n\t\t{\n\t\t\tmatcher: mustNewMatcher(t, MatchRegexp, `foo\\.bar`),\n\t\t\tvalue:   \"foo.bar\",\n\t\t\tmatch:   true,\n\t\t},\n\t\t{\n\t\t\tmatcher: mustNewMatcher(t, MatchEqual, \"foo\\nbar\"),\n\t\t\tvalue:   \"foo\\nbar\",\n\t\t\tmatch:   true,\n\t\t},\n\t\t{\n\t\t\tmatcher: mustNewMatcher(t, MatchRegexp, \"foo.bar\"),\n\t\t\tvalue:   \"foo\\nbar\",\n\t\t\tmatch:   false,\n\t\t},\n\t\t{\n\t\t\tmatcher: mustNewMatcher(t, MatchRegexp, \"(?s)foo.bar\"),\n\t\t\tvalue:   \"foo\\nbar\",\n\t\t\tmatch:   true,\n\t\t},\n\t\t{\n\t\t\tmatcher: mustNewMatcher(t, MatchEqual, \"~!=\\\"\"),\n\t\t\tvalue:   \"~!=\\\"\",\n\t\t\tmatch:   true,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tif test.matcher.Matches(test.value) != test.match {\n\t\t\tt.Fatalf(\"Unexpected match result for matcher %v and value %q; want %v, got %v\", test.matcher, test.value, test.match, !test.match)\n\t\t}\n\t}\n}\n\nfunc TestMatcherString(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\top    MatchType\n\t\tvalue string\n\t\twant  string\n\t}{\n\t\t{\n\t\t\tname:  `foo`,\n\t\t\top:    MatchEqual,\n\t\t\tvalue: `bar`,\n\t\t\twant:  `foo=\"bar\"`,\n\t\t},\n\t\t{\n\t\t\tname:  `foo`,\n\t\t\top:    MatchNotEqual,\n\t\t\tvalue: `bar`,\n\t\t\twant:  `foo!=\"bar\"`,\n\t\t},\n\t\t{\n\t\t\tname:  `foo`,\n\t\t\top:    MatchRegexp,\n\t\t\tvalue: `bar`,\n\t\t\twant:  `foo=~\"bar\"`,\n\t\t},\n\t\t{\n\t\t\tname:  `foo`,\n\t\t\top:    MatchNotRegexp,\n\t\t\tvalue: `bar`,\n\t\t\twant:  `foo!~\"bar\"`,\n\t\t},\n\t\t{\n\t\t\tname:  `foo`,\n\t\t\top:    MatchEqual,\n\t\t\tvalue: `back\\slash`,\n\t\t\twant:  `foo=\"back\\\\slash\"`,\n\t\t},\n\t\t{\n\t\t\tname:  `foo`,\n\t\t\top:    MatchEqual,\n\t\t\tvalue: `double\"quote`,\n\t\t\twant:  `foo=\"double\\\"quote\"`,\n\t\t},\n\t\t{\n\t\t\tname: `foo`,\n\t\t\top:   MatchEqual,\n\t\t\tvalue: `new\nline`,\n\t\t\twant: `foo=\"new\\nline\"`,\n\t\t},\n\t\t{\n\t\t\tname:  `foo`,\n\t\t\top:    MatchEqual,\n\t\t\tvalue: `tab\tstop`,\n\t\t\twant:  `foo=\"tab\tstop\"`,\n\t\t},\n\t\t{\n\t\t\tname:  `foo`,\n\t\t\top:    MatchEqual,\n\t\t\tvalue: `🙂`,\n\t\t\twant:  `foo=\"🙂\"`,\n\t\t},\n\t\t{\n\t\t\tname:  `foo!`,\n\t\t\top:    MatchNotEqual,\n\t\t\tvalue: `bar`,\n\t\t\twant:  `\"foo!\"!=\"bar\"`,\n\t\t},\n\t\t{\n\t\t\tname:  `foo🙂`,\n\t\t\top:    MatchEqual,\n\t\t\tvalue: `bar`,\n\t\t\twant:  `foo🙂=\"bar\"`,\n\t\t},\n\t\t{\n\t\t\tname:  `foo bar`,\n\t\t\top:    MatchEqual,\n\t\t\tvalue: `baz`,\n\t\t\twant:  `\"foo bar\"=\"baz\"`,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tm, err := NewMatcher(test.op, test.name, test.value)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif got := m.String(); got != test.want {\n\t\t\tt.Errorf(\"Unexpected string representation of matcher; want %v, got %v\", test.want, got)\n\t\t}\n\t}\n}\n\nfunc TestMatcherJSONMarshal(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\top    MatchType\n\t\tvalue string\n\t\twant  string\n\t}{\n\t\t{\n\t\t\tname:  `foo`,\n\t\t\top:    MatchEqual,\n\t\t\tvalue: `bar`,\n\t\t\twant:  `{\"name\":\"foo\",\"value\":\"bar\",\"isRegex\":false,\"isEqual\":true}`,\n\t\t},\n\t\t{\n\t\t\tname:  `foo`,\n\t\t\top:    MatchNotEqual,\n\t\t\tvalue: `bar`,\n\t\t\twant:  `{\"name\":\"foo\",\"value\":\"bar\",\"isRegex\":false,\"isEqual\":false}`,\n\t\t},\n\t\t{\n\t\t\tname:  `foo`,\n\t\t\top:    MatchRegexp,\n\t\t\tvalue: `bar`,\n\t\t\twant:  `{\"name\":\"foo\",\"value\":\"bar\",\"isRegex\":true,\"isEqual\":true}`,\n\t\t},\n\t\t{\n\t\t\tname:  `foo`,\n\t\t\top:    MatchNotRegexp,\n\t\t\tvalue: `bar`,\n\t\t\twant:  `{\"name\":\"foo\",\"value\":\"bar\",\"isRegex\":true,\"isEqual\":false}`,\n\t\t},\n\t}\n\n\tcmp := func(m1, m2 Matcher) bool {\n\t\treturn m1.Name == m2.Name && m1.Value == m2.Value && m1.Type == m2.Type\n\t}\n\n\tfor _, test := range tests {\n\t\tm, err := NewMatcher(test.op, test.name, test.value)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tb, err := json.Marshal(m)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif got := string(b); got != test.want {\n\t\t\tt.Errorf(\"Unexpected JSON representation of matcher:\\nwant:\\t%v\\ngot:\\t%v\", test.want, got)\n\t\t}\n\n\t\tvar m2 Matcher\n\t\tif err := json.Unmarshal(b, &m2); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif !cmp(*m, m2) {\n\t\t\tt.Errorf(\"Doing Marshal and Unmarshal seems to be losing data; before %#v, after %#v\", m, m2)\n\t\t}\n\t}\n}\n\nfunc TestMatcherJSONUnmarshal(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\top    MatchType\n\t\tvalue string\n\t\twant  string\n\t}{\n\t\t{\n\t\t\tname:  \"foo\",\n\t\t\top:    MatchEqual,\n\t\t\tvalue: \"bar\",\n\t\t\twant:  `{\"name\":\"foo\",\"value\":\"bar\",\"isRegex\":false}`,\n\t\t},\n\t\t{\n\t\t\tname:  `foo`,\n\t\t\top:    MatchEqual,\n\t\t\tvalue: `bar`,\n\t\t\twant:  `{\"name\":\"foo\",\"value\":\"bar\",\"isRegex\":false,\"isEqual\":true}`,\n\t\t},\n\t\t{\n\t\t\tname:  `foo`,\n\t\t\top:    MatchNotEqual,\n\t\t\tvalue: `bar`,\n\t\t\twant:  `{\"name\":\"foo\",\"value\":\"bar\",\"isRegex\":false,\"isEqual\":false}`,\n\t\t},\n\t\t{\n\t\t\tname:  `foo`,\n\t\t\top:    MatchRegexp,\n\t\t\tvalue: `bar`,\n\t\t\twant:  `{\"name\":\"foo\",\"value\":\"bar\",\"isRegex\":true,\"isEqual\":true}`,\n\t\t},\n\t\t{\n\t\t\tname:  `foo`,\n\t\t\top:    MatchNotRegexp,\n\t\t\tvalue: `bar`,\n\t\t\twant:  `{\"name\":\"foo\",\"value\":\"bar\",\"isRegex\":true,\"isEqual\":false}`,\n\t\t},\n\t}\n\n\tcmp := func(m1, m2 Matcher) bool {\n\t\treturn m1.Name == m2.Name && m1.Value == m2.Value && m1.Type == m2.Type\n\t}\n\n\tfor _, test := range tests {\n\t\tvar m Matcher\n\t\tif err := json.Unmarshal([]byte(test.want), &m); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tm2, err := NewMatcher(test.op, test.name, test.value)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif !cmp(m, *m2) {\n\t\t\tt.Errorf(\"Unmarshaling seems to be producing unexpected matchers; got %#v, expected %#v\", m, m2)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/labels/parse.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage labels\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\t\"unicode/utf8\"\n)\n\nvar (\n\t// '=~' has to come before '=' because otherwise only the '='\n\t// will be consumed, and the '~' will be part of the 3rd token.\n\tre      = regexp.MustCompile(`^\\s*([a-zA-Z_:][a-zA-Z0-9_:]*)\\s*(=~|=|!=|!~)\\s*((?s).*?)\\s*$`)\n\ttypeMap = map[string]MatchType{\n\t\t\"=\":  MatchEqual,\n\t\t\"!=\": MatchNotEqual,\n\t\t\"=~\": MatchRegexp,\n\t\t\"!~\": MatchNotRegexp,\n\t}\n)\n\n// ParseMatchers parses a comma-separated list of Matchers. A leading '{' and/or\n// a trailing '}' is optional and will be trimmed before further\n// parsing. Individual Matchers are separated by commas outside of quoted parts\n// of the input string. Those commas may be surrounded by whitespace. Parts of the\n// string inside unescaped double quotes ('\"…\"') are considered quoted (and\n// commas don't act as separators there). If double quotes are escaped with a\n// single backslash ('\\\"'), they are ignored for the purpose of identifying\n// quoted parts of the input string. If the input string, after trimming the\n// optional trailing '}', ends with a comma, followed by optional whitespace,\n// this comma and whitespace will be trimmed.\n//\n// Examples for valid input strings:\n//\n//\t{foo = \"bar\", dings != \"bums\", }\n//\tfoo=bar,dings!=bums\n//\tfoo=bar, dings!=bums\n//\t{quote=\"She said: \\\"Hi, ladies! That's gender-neutral…\\\"\"}\n//\tstatuscode=~\"5..\"\n//\n// See ParseMatcher for details on how an individual Matcher is parsed.\nfunc ParseMatchers(s string) ([]*Matcher, error) {\n\tmatchers := []*Matcher{}\n\ts = strings.TrimPrefix(s, \"{\")\n\ts = strings.TrimSuffix(s, \"}\")\n\n\tvar (\n\t\tinsideQuotes bool\n\t\tescaped      bool\n\t\ttoken        strings.Builder\n\t\ttokens       []string\n\t)\n\tfor _, r := range s {\n\t\tswitch r {\n\t\tcase ',':\n\t\t\tif !insideQuotes {\n\t\t\t\ttokens = append(tokens, token.String())\n\t\t\t\ttoken.Reset()\n\t\t\t\tcontinue\n\t\t\t}\n\t\tcase '\"':\n\t\t\tif !escaped {\n\t\t\t\tinsideQuotes = !insideQuotes\n\t\t\t} else {\n\t\t\t\tescaped = false\n\t\t\t}\n\t\tcase '\\\\':\n\t\t\tescaped = !escaped\n\t\tdefault:\n\t\t\tescaped = false\n\t\t}\n\t\ttoken.WriteRune(r)\n\t}\n\tif s := strings.TrimSpace(token.String()); s != \"\" {\n\t\ttokens = append(tokens, s)\n\t}\n\tfor _, token := range tokens {\n\t\tm, err := ParseMatcher(token)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmatchers = append(matchers, m)\n\t}\n\n\treturn matchers, nil\n}\n\n// ParseMatcher parses a matcher with a syntax inspired by PromQL and\n// OpenMetrics. This syntax is convenient to describe filters and selectors in\n// UIs and config files. To support the interactive nature of the use cases, the\n// parser is in various aspects fairly tolerant.\n//\n// The syntax of a matcher consists of three tokens: (1) A valid Prometheus\n// label name. (2) One of '=', '!=', '=~', or '!~', with the same meaning as\n// known from PromQL selectors. (3) A UTF-8 string, which may be enclosed in\n// double quotes. Before or after each token, there may be any amount of\n// whitespace, which will be discarded. The 3rd token may be the empty\n// string. Within the 3rd token, OpenMetrics escaping rules apply: '\\\"' for a\n// double-quote, '\\n' for a line feed, '\\\\' for a literal backslash. Unescaped\n// '\"' must not occur inside the 3rd token (only as the 1st or last\n// character). However, literal line feed characters are tolerated, as are\n// single '\\' characters not followed by '\\', 'n', or '\"'. They act as a literal\n// backslash in that case.\nfunc ParseMatcher(s string) (_ *Matcher, err error) {\n\tms := re.FindStringSubmatch(s)\n\tif len(ms) == 0 {\n\t\treturn nil, fmt.Errorf(\"bad matcher format: %s\", s)\n\t}\n\n\tvar (\n\t\trawValue            = ms[3]\n\t\tvalue               strings.Builder\n\t\tescaped             bool\n\t\texpectTrailingQuote bool\n\t)\n\n\tif after, ok := strings.CutPrefix(rawValue, \"\\\"\"); ok {\n\t\trawValue = after\n\t\texpectTrailingQuote = true\n\t}\n\n\tif !utf8.ValidString(rawValue) {\n\t\treturn nil, fmt.Errorf(\"matcher value not valid UTF-8: %s\", ms[3])\n\t}\n\n\t// Unescape the rawValue.\n\tfor i, r := range rawValue {\n\t\tif escaped {\n\t\t\tescaped = false\n\t\t\tswitch r {\n\t\t\tcase 'n':\n\t\t\t\tvalue.WriteByte('\\n')\n\t\t\tcase '\"', '\\\\':\n\t\t\t\tvalue.WriteRune(r)\n\t\t\tdefault:\n\t\t\t\t// This was a spurious escape, so treat the '\\' as literal.\n\t\t\t\tvalue.WriteByte('\\\\')\n\t\t\t\tvalue.WriteRune(r)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tswitch r {\n\t\tcase '\\\\':\n\t\t\tif i < len(rawValue)-1 {\n\t\t\t\tescaped = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// '\\' encountered as last byte. Treat it as literal.\n\t\t\tvalue.WriteByte('\\\\')\n\t\tcase '\"':\n\t\t\tif !expectTrailingQuote || i < len(rawValue)-1 {\n\t\t\t\treturn nil, fmt.Errorf(\"matcher value contains unescaped double quote: %s\", ms[3])\n\t\t\t}\n\t\t\texpectTrailingQuote = false\n\t\tdefault:\n\t\t\tvalue.WriteRune(r)\n\t\t}\n\t}\n\n\tif expectTrailingQuote {\n\t\treturn nil, fmt.Errorf(\"matcher value contains unescaped double quote: %s\", ms[3])\n\t}\n\n\treturn NewMatcher(typeMap[ms[2]], ms[1], value.String())\n}\n"
  },
  {
    "path": "pkg/labels/parse_test.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage labels\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestMatchers(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tinput string\n\t\twant  []*Matcher\n\t\terr   string\n\t}{\n\t\t{\n\t\t\tinput: `{}`,\n\t\t\twant:  make([]*Matcher, 0),\n\t\t},\n\t\t{\n\t\t\tinput: `,`,\n\t\t\terr:   \"bad matcher format: \",\n\t\t},\n\t\t{\n\t\t\tinput: `{,}`,\n\t\t\terr:   \"bad matcher format: \",\n\t\t},\n\t\t{\n\t\t\tinput: `{foo='}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"'\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: \"{foo=`}\",\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"`\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: \"{foo=\\\\\\\"}\",\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"\\\"\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=bar}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"bar\"}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=~bar.*}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchRegexp, \"foo\", \"bar.*\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=~\"bar.*\"}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchRegexp, \"foo\", \"bar.*\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo!=bar}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchNotEqual, \"foo\", \"bar\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo!=\"bar\"}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchNotEqual, \"foo\", \"bar\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo!~bar.*}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchNotRegexp, \"foo\", \"bar.*\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo!~\"bar.*\"}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchNotRegexp, \"foo\", \"bar.*\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"bar\", baz!=\"quux\"}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar\")\n\t\t\t\tm2, _ := NewMatcher(MatchNotEqual, \"baz\", \"quux\")\n\t\t\t\treturn append(ms, m, m2)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"bar\", baz!~\"quux.*\"}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar\")\n\t\t\t\tm2, _ := NewMatcher(MatchNotRegexp, \"baz\", \"quux.*\")\n\t\t\t\treturn append(ms, m, m2)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"bar\",baz!~\".*quux\", derp=\"wat\"}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar\")\n\t\t\t\tm2, _ := NewMatcher(MatchNotRegexp, \"baz\", \".*quux\")\n\t\t\t\tm3, _ := NewMatcher(MatchEqual, \"derp\", \"wat\")\n\t\t\t\treturn append(ms, m, m2, m3)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"bar\", baz!=\"quux\", derp=\"wat\"}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar\")\n\t\t\t\tm2, _ := NewMatcher(MatchNotEqual, \"baz\", \"quux\")\n\t\t\t\tm3, _ := NewMatcher(MatchEqual, \"derp\", \"wat\")\n\t\t\t\treturn append(ms, m, m2, m3)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"bar\", baz!~\".*quux.*\", derp=\"wat\"}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar\")\n\t\t\t\tm2, _ := NewMatcher(MatchNotRegexp, \"baz\", \".*quux.*\")\n\t\t\t\tm3, _ := NewMatcher(MatchEqual, \"derp\", \"wat\")\n\t\t\t\treturn append(ms, m, m2, m3)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"bar\", instance=~\"some-api.*\"}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar\")\n\t\t\t\tm2, _ := NewMatcher(MatchRegexp, \"instance\", \"some-api.*\")\n\t\t\t\treturn append(ms, m, m2)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"\"}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=\"bar,quux\", job=\"job1\"}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar,quux\")\n\t\t\t\tm2, _ := NewMatcher(MatchEqual, \"job\", \"job1\")\n\t\t\t\treturn append(ms, m, m2)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo = \"bar\", dings != \"bums\", }`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar\")\n\t\t\t\tm2, _ := NewMatcher(MatchNotEqual, \"dings\", \"bums\")\n\t\t\t\treturn append(ms, m, m2)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `foo=bar,dings!=bums`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar\")\n\t\t\t\tm2, _ := NewMatcher(MatchNotEqual, \"dings\", \"bums\")\n\t\t\t\treturn append(ms, m, m2)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{quote=\"She said: \\\"Hi, ladies! That's gender-neutral…\\\"\"}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"quote\", `She said: \"Hi, ladies! That's gender-neutral…\"`)\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `statuscode=~\"5..\"`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchRegexp, \"statuscode\", \"5..\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `tricky=~~~`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchRegexp, \"tricky\", \"~~\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `trickier==\\\\=\\=\\\"`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"trickier\", `=\\=\\=\"`)\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `contains_quote != \"\\\"\" , contains_comma !~ \"foo,bar\" , `,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchNotEqual, \"contains_quote\", `\"`)\n\t\t\t\tm2, _ := NewMatcher(MatchNotRegexp, \"contains_comma\", \"foo,bar\")\n\t\t\t\treturn append(ms, m, m2)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=bar}}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar}\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=bar}},}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar}}\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=,bar=}}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm1, _ := NewMatcher(MatchEqual, \"foo\", \"\")\n\t\t\t\tm2, _ := NewMatcher(MatchEqual, \"bar\", \"}\")\n\t\t\t\treturn append(ms, m1, m2)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=bar\\t}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar\\\\t\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=bar\\n}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar\\n\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=bar\\}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar\\\\\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=bar\\\\}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar\\\\\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=bar\\\"}`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"bar\\\"\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `job=`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"job\", \"\")\n\t\t\t\treturn []*Matcher{m}\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `job=\"value`,\n\t\t\terr:   `matcher value contains unescaped double quote: \"value`,\n\t\t},\n\t\t{\n\t\t\tinput: `job=value\"`,\n\t\t\terr:   `matcher value contains unescaped double quote: value\"`,\n\t\t},\n\t\t{\n\t\t\tinput: `trickier==\\\\=\\=\\\"\"`,\n\t\t\terr:   `matcher value contains unescaped double quote: =\\\\=\\=\\\"\"`,\n\t\t},\n\t\t{\n\t\t\tinput: `contains_unescaped_quote = foo\"bar`,\n\t\t\terr:   `matcher value contains unescaped double quote: foo\"bar`,\n\t\t},\n\t\t{\n\t\t\tinput: `{invalid-name = \"valid label\"}`,\n\t\t\terr:   `bad matcher format: invalid-name = \"valid label\"`,\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=~\"invalid[regexp\"}`,\n\t\t\terr:   \"error parsing regexp: missing closing ]: `[regexp)$`\",\n\t\t},\n\t\t// Double escaped strings.\n\t\t{\n\t\t\tinput: `\"{foo=\\\"bar\"}`,\n\t\t\terr:   `bad matcher format: \"{foo=\\\"bar\"`,\n\t\t},\n\t\t{\n\t\t\tinput: `\"foo=\\\"bar\"`,\n\t\t\terr:   `bad matcher format: \"foo=\\\"bar\"`,\n\t\t},\n\t\t{\n\t\t\tinput: `\"foo=\\\"bar\\\"\"`,\n\t\t\terr:   `bad matcher format: \"foo=\\\"bar\\\"\"`,\n\t\t},\n\t\t{\n\t\t\tinput: `\"foo=\\\"bar\\\"`,\n\t\t\terr:   `bad matcher format: \"foo=\\\"bar\\\"`,\n\t\t},\n\t\t{\n\t\t\tinput: `\"{foo=\\\"bar\\\"}\"`,\n\t\t\terr:   `bad matcher format: \"{foo=\\\"bar\\\"}\"`,\n\t\t},\n\t\t{\n\t\t\tinput: `\"foo=\"bar\"\"`,\n\t\t\terr:   `bad matcher format: \"foo=\"bar\"\"`,\n\t\t},\n\t\t{\n\t\t\tinput: `{{foo=`,\n\t\t\terr:   `bad matcher format: {foo=`,\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tinput: `{foo=}b`,\n\t\t\twant: func() []*Matcher {\n\t\t\t\tms := []*Matcher{}\n\t\t\t\tm, _ := NewMatcher(MatchEqual, \"foo\", \"}b\")\n\t\t\t\treturn append(ms, m)\n\t\t\t}(),\n\t\t},\n\t} {\n\t\tt.Run(tc.input, func(t *testing.T) {\n\t\t\tgot, err := ParseMatchers(tc.input)\n\t\t\tif err != nil && tc.err == \"\" {\n\t\t\t\tt.Fatalf(\"got error where none expected: %v\", err)\n\t\t\t}\n\t\t\tif err == nil && tc.err != \"\" {\n\t\t\t\tt.Fatalf(\"expected error but got none: %v\", tc.err)\n\t\t\t}\n\t\t\tif err != nil && err.Error() != tc.err {\n\t\t\t\tt.Fatalf(\"error not equal:\\ngot  %v\\nwant %v\", err, tc.err)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tc.want) {\n\t\t\t\tt.Fatalf(\"labels not equal:\\ngot  %v\\nwant %v\", got, tc.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/modtimevfs/modtimevfs.go",
    "content": "// Copyright 2018 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package modtimevfs implements a virtual file system that returns a fixed\n// modification time for all files and directories.\npackage modtimevfs\n\nimport (\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n)\n\ntype timefs struct {\n\tfs http.FileSystem\n\tt  time.Time\n}\n\n// New returns a file system that returns constant modification time for all files.\nfunc New(fs http.FileSystem, t time.Time) http.FileSystem {\n\treturn &timefs{fs: fs, t: t}\n}\n\ntype file struct {\n\thttp.File\n\tos.FileInfo\n\tt time.Time\n}\n\nfunc (t *timefs) Open(name string) (http.File, error) {\n\tf, err := t.fs.Open(name)\n\tif err != nil {\n\t\treturn f, err\n\t}\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tf.Close()\n\t\t}\n\t}()\n\n\tfstat, err := f.Stat()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &file{f, fstat, t.t}, nil\n}\n\n// Stat implements the http.File interface.\nfunc (f *file) Stat() (os.FileInfo, error) {\n\treturn f, nil\n}\n\n// ModTime implements the os.FileInfo interface.\nfunc (f *file) ModTime() time.Time {\n\treturn f.t\n}\n"
  },
  {
    "path": "provider/mem/mem.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage mem\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log/slog\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n\t\"github.com/prometheus/common/model\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/propagation\"\n\t\"go.opentelemetry.io/otel/trace\"\n\n\t\"github.com/prometheus/alertmanager/featurecontrol\"\n\t\"github.com/prometheus/alertmanager/provider\"\n\t\"github.com/prometheus/alertmanager/store\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nconst alertChannelLength = 200\n\nvar tracer = otel.Tracer(\"github.com/prometheus/alertmanager/provider/mem\")\n\n// Alerts gives access to a set of alerts. All methods are goroutine-safe.\ntype Alerts struct {\n\tcancel context.CancelFunc\n\n\tmtx sync.Mutex\n\n\talerts *store.Alerts\n\tmarker types.AlertMarker\n\n\tlisteners map[int]listeningAlerts\n\tnext      int\n\n\tcallback AlertStoreCallback\n\n\tlogger     *slog.Logger\n\tpropagator propagation.TextMapPropagator\n\tflagger    featurecontrol.Flagger\n\n\talertsLimit             prometheus.Gauge\n\talertsLimitedTotal      *prometheus.CounterVec\n\tsubscriberChannelWrites *prometheus.CounterVec\n}\n\ntype AlertStoreCallback interface {\n\t// PreStore is called before alert is stored into the store. If this method returns error,\n\t// alert is not stored.\n\t// Existing flag indicates whether alert has existed before (and is only updated) or not.\n\t// If alert has existed before, then alert passed to PreStore is result of merging existing alert with new alert.\n\tPreStore(alert *types.Alert, existing bool) error\n\n\t// PostStore is called after alert has been put into store.\n\tPostStore(alert *types.Alert, existing bool)\n\n\t// PostDelete is called after alert have been removed from the store due to alert garbage collection.\n\tPostDelete(alert *types.Alert)\n\n\t// PostGC is called after alerts have been removed from the store due to alert garbage collection.\n\tPostGC(fingerprints model.Fingerprints)\n}\n\ntype listeningAlerts struct {\n\tname   string\n\talerts chan *provider.Alert\n\tdone   chan struct{}\n}\n\nfunc (a *Alerts) registerMetrics(r prometheus.Registerer) {\n\tr.MustRegister(&alertsCollector{alerts: a})\n\n\ta.alertsLimit = promauto.With(r).NewGauge(prometheus.GaugeOpts{\n\t\tName: \"alertmanager_alerts_per_alert_limit\",\n\t\tHelp: \"Current limit on number of alerts per alert name\",\n\t})\n\n\tlabels := []string{}\n\tif a.flagger.EnableAlertNamesInMetrics() {\n\t\tlabels = append(labels, \"alertname\")\n\t}\n\ta.alertsLimitedTotal = promauto.With(r).NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tName: \"alertmanager_alerts_limited_total\",\n\t\t\tHelp: \"Total number of alerts that were dropped due to per alert name limit\",\n\t\t},\n\t\tlabels,\n\t)\n\n\ta.subscriberChannelWrites = promauto.With(r).NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tName: \"alertmanager_alerts_subscriber_channel_writes_total\",\n\t\t\tHelp: \"Number of times alerts were written to subscriber channels\",\n\t\t},\n\t\t[]string{\"subscriber\"},\n\t)\n}\n\n// NewAlerts returns a new alert provider.\nfunc NewAlerts(\n\tctx context.Context,\n\tm types.AlertMarker,\n\tintervalGC time.Duration,\n\tperAlertNameLimit int,\n\talertCallback AlertStoreCallback,\n\tl *slog.Logger,\n\tr prometheus.Registerer,\n\tflagger featurecontrol.Flagger,\n) (*Alerts, error) {\n\tif alertCallback == nil {\n\t\talertCallback = noopCallback{}\n\t}\n\n\tif perAlertNameLimit > 0 {\n\t\tl.Info(\"per alert name limit enabled\", \"limit\", perAlertNameLimit)\n\t}\n\n\tif flagger == nil {\n\t\tflagger = featurecontrol.NoopFlags{}\n\t}\n\n\tctx, cancel := context.WithCancel(ctx)\n\ta := &Alerts{\n\t\tmarker:     m,\n\t\talerts:     store.NewAlerts().WithPerAlertLimit(perAlertNameLimit),\n\t\tcancel:     cancel,\n\t\tlisteners:  map[int]listeningAlerts{},\n\t\tnext:       0,\n\t\tlogger:     l.With(\"component\", \"provider\"),\n\t\tpropagator: otel.GetTextMapPropagator(),\n\t\tcallback:   alertCallback,\n\t\tflagger:    flagger,\n\t}\n\n\tif r != nil {\n\t\ta.registerMetrics(r)\n\t\ta.alertsLimit.Set(float64(perAlertNameLimit))\n\t}\n\n\tgo a.gcLoop(ctx, intervalGC)\n\n\treturn a, nil\n}\n\nfunc (a *Alerts) gcLoop(ctx context.Context, interval time.Duration) {\n\tt := time.NewTicker(interval)\n\tdefer t.Stop()\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-t.C:\n\t\t\ta.gc()\n\t\t}\n\t}\n}\n\nfunc (a *Alerts) gc() {\n\ta.gcListeners()\n\n\t// As we don't persist alerts, we no longer consider them after\n\t// they are resolved. Alerts waiting for resolved notifications are\n\t// held in memory in aggregation groups redundantly.\n\tdeleted := a.gcAlerts()\n\n\t// If there are no deleted alerts, there is nothing to do.\n\tif len(deleted) == 0 {\n\t\treturn\n\t}\n\n\t// Delete markers for deleted alerts.\n\tff := make(model.Fingerprints, len(deleted))\n\tfor i, alert := range deleted {\n\t\tff[i] = alert.Fingerprint()\n\t\ta.callback.PostDelete(alert)\n\t}\n\ta.marker.Delete(ff...)\n\ta.callback.PostGC(ff)\n}\n\nfunc (a *Alerts) gcAlerts() []*types.Alert {\n\ta.mtx.Lock()\n\tdefer a.mtx.Unlock()\n\treturn a.alerts.GC()\n}\n\nfunc (a *Alerts) gcListeners() {\n\ta.mtx.Lock()\n\tdefer a.mtx.Unlock()\n\n\tfor i, l := range a.listeners {\n\t\tselect {\n\t\tcase <-l.done:\n\t\t\tdelete(a.listeners, i)\n\t\t\tclose(l.alerts)\n\t\tdefault:\n\t\t\t// listener is not closed yet, hence proceed.\n\t\t}\n\t}\n}\n\n// Close the alert provider.\nfunc (a *Alerts) Close() {\n\tif a.cancel != nil {\n\t\ta.cancel()\n\t}\n}\n\n// Subscribe returns an iterator over active alerts that have not been\n// resolved and successfully notified about.\n// They are not guaranteed to be in chronological order.\nfunc (a *Alerts) Subscribe(name string) provider.AlertIterator {\n\ta.mtx.Lock()\n\tdefer a.mtx.Unlock()\n\tvar (\n\t\tdone   = make(chan struct{})\n\t\talerts = a.alerts.List()\n\t\tch     = make(chan *provider.Alert, max(len(alerts), alertChannelLength))\n\t)\n\n\tfor _, a := range alerts {\n\t\tch <- &provider.Alert{\n\t\t\tHeader: map[string]string{},\n\t\t\tData:   a,\n\t\t}\n\t}\n\n\ta.listeners[a.next] = listeningAlerts{name: name, alerts: ch, done: done}\n\ta.next++\n\n\treturn provider.NewAlertIterator(ch, done, nil)\n}\n\nfunc (a *Alerts) SlurpAndSubscribe(name string) ([]*types.Alert, provider.AlertIterator) {\n\ta.mtx.Lock()\n\tdefer a.mtx.Unlock()\n\n\tvar (\n\t\tdone   = make(chan struct{})\n\t\talerts = a.alerts.List()\n\t\tch     = make(chan *provider.Alert, alertChannelLength)\n\t)\n\n\ta.listeners[a.next] = listeningAlerts{name: name, alerts: ch, done: done}\n\ta.next++\n\n\treturn alerts, provider.NewAlertIterator(ch, done, nil)\n}\n\n// GetPending returns an iterator over all the alerts that have\n// pending notifications.\nfunc (a *Alerts) GetPending() provider.AlertIterator {\n\tvar (\n\t\tch   = make(chan *provider.Alert, alertChannelLength)\n\t\tdone = make(chan struct{})\n\t)\n\ta.mtx.Lock()\n\tdefer a.mtx.Unlock()\n\talerts := a.alerts.List()\n\n\tgo func() {\n\t\tdefer close(ch)\n\t\tfor _, a := range alerts {\n\t\t\tselect {\n\t\t\tcase ch <- &provider.Alert{\n\t\t\t\tHeader: map[string]string{},\n\t\t\t\tData:   a,\n\t\t\t}:\n\t\t\tcase <-done:\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn provider.NewAlertIterator(ch, done, nil)\n}\n\n// Get returns the alert for a given fingerprint.\nfunc (a *Alerts) Get(fp model.Fingerprint) (*types.Alert, error) {\n\ta.mtx.Lock()\n\tdefer a.mtx.Unlock()\n\treturn a.alerts.Get(fp)\n}\n\n// Put adds the given alert to the set.\nfunc (a *Alerts) Put(ctx context.Context, alerts ...*types.Alert) error {\n\ta.mtx.Lock()\n\tdefer a.mtx.Unlock()\n\n\tctx, span := tracer.Start(ctx, \"provider.mem.Put\",\n\t\ttrace.WithAttributes(\n\t\t\tattribute.Int(\"alerting.alerts.count\", len(alerts)),\n\t\t),\n\t\ttrace.WithSpanKind(trace.SpanKindProducer),\n\t)\n\tdefer span.End()\n\n\tfor _, alert := range alerts {\n\t\tfp := alert.Fingerprint()\n\n\t\texisting := false\n\n\t\t// Check that there's an alert existing within the store before\n\t\t// trying to merge.\n\t\tif old, err := a.alerts.Get(fp); err == nil {\n\t\t\texisting = true\n\n\t\t\t// Merge alerts if there is an overlap in activity range.\n\t\t\tif (alert.EndsAt.After(old.StartsAt) && alert.EndsAt.Before(old.EndsAt)) ||\n\t\t\t\t(alert.StartsAt.After(old.StartsAt) && alert.StartsAt.Before(old.EndsAt)) {\n\t\t\t\talert = old.Merge(alert)\n\t\t\t}\n\t\t}\n\n\t\tif err := a.callback.PreStore(alert, existing); err != nil {\n\t\t\ta.logger.Error(\"pre-store callback returned error on set alert\", \"err\", err)\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := a.alerts.Set(alert); err != nil {\n\t\t\ta.logger.Warn(\"error on set alert\", \"alertname\", alert.Name(), \"err\", err)\n\t\t\tif errors.Is(err, store.ErrLimited) {\n\t\t\t\tlabels := []string{}\n\t\t\t\tif a.flagger.EnableAlertNamesInMetrics() {\n\t\t\t\t\tlabels = append(labels, alert.Name())\n\t\t\t\t}\n\t\t\t\ta.alertsLimitedTotal.WithLabelValues(labels...).Inc()\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\ta.callback.PostStore(alert, existing)\n\n\t\tmetadata := map[string]string{}\n\t\ta.propagator.Inject(ctx, propagation.MapCarrier(metadata))\n\t\tmsg := &provider.Alert{\n\t\t\tData:   alert,\n\t\t\tHeader: metadata,\n\t\t}\n\n\t\tfor _, l := range a.listeners {\n\t\t\tselect {\n\t\t\tcase l.alerts <- msg:\n\t\t\t\ta.subscriberChannelWrites.WithLabelValues(l.name).Inc()\n\t\t\tcase <-l.done:\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// countByState returns the number of non-resolved alerts by state.\nfunc (a *Alerts) countByState() (active, suppressed, unprocessed int) {\n\tfor _, alert := range a.alerts.List() {\n\t\tif alert.Resolved() {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch a.marker.Status(alert.Fingerprint()).State {\n\t\tcase types.AlertStateActive:\n\t\t\tactive++\n\t\tcase types.AlertStateSuppressed:\n\t\t\tsuppressed++\n\t\tcase types.AlertStateUnprocessed:\n\t\t\tunprocessed++\n\t\t}\n\t}\n\treturn active, suppressed, unprocessed\n}\n\n// alertsCollector implements prometheus.Collector to collect all alert count metrics in a single pass.\ntype alertsCollector struct {\n\talerts *Alerts\n}\n\nvar alertsDesc = prometheus.NewDesc(\n\t\"alertmanager_alerts\",\n\t\"How many alerts by state.\",\n\t[]string{\"state\"}, nil,\n)\n\nfunc (c *alertsCollector) Describe(ch chan<- *prometheus.Desc) {\n\tch <- alertsDesc\n}\n\nfunc (c *alertsCollector) Collect(ch chan<- prometheus.Metric) {\n\tactive, suppressed, unprocessed := c.alerts.countByState()\n\n\tch <- prometheus.MustNewConstMetric(alertsDesc, prometheus.GaugeValue, float64(active), string(types.AlertStateActive))\n\tch <- prometheus.MustNewConstMetric(alertsDesc, prometheus.GaugeValue, float64(suppressed), string(types.AlertStateSuppressed))\n\tch <- prometheus.MustNewConstMetric(alertsDesc, prometheus.GaugeValue, float64(unprocessed), string(types.AlertStateUnprocessed))\n}\n\ntype noopCallback struct{}\n\nfunc (n noopCallback) PreStore(_ *types.Alert, _ bool) error { return nil }\nfunc (n noopCallback) PostStore(_ *types.Alert, _ bool)      {}\nfunc (n noopCallback) PostDelete(_ *types.Alert)             {}\nfunc (n noopCallback) PostGC(_ model.Fingerprints)           {}\n"
  },
  {
    "path": "provider/mem/mem_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage mem\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/prometheus/alertmanager/store\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nvar (\n\tt0 = time.Now()\n\tt1 = t0.Add(100 * time.Millisecond)\n\n\talert1 = &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels:       model.LabelSet{\"bar\": \"foo\"},\n\t\t\tAnnotations:  model.LabelSet{\"foo\": \"bar\"},\n\t\t\tStartsAt:     t0,\n\t\t\tEndsAt:       t1,\n\t\t\tGeneratorURL: \"http://example.com/prometheus\",\n\t\t},\n\t\tUpdatedAt: t0,\n\t\tTimeout:   false,\n\t}\n\n\talert2 = &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels:       model.LabelSet{\"bar\": \"foo2\"},\n\t\t\tAnnotations:  model.LabelSet{\"foo\": \"bar2\"},\n\t\t\tStartsAt:     t0,\n\t\t\tEndsAt:       t1,\n\t\t\tGeneratorURL: \"http://example.com/prometheus\",\n\t\t},\n\t\tUpdatedAt: t0,\n\t\tTimeout:   false,\n\t}\n\n\talert3 = &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels:       model.LabelSet{\"bar\": \"foo3\"},\n\t\t\tAnnotations:  model.LabelSet{\"foo\": \"bar3\"},\n\t\t\tStartsAt:     t0,\n\t\t\tEndsAt:       t1,\n\t\t\tGeneratorURL: \"http://example.com/prometheus\",\n\t\t},\n\t\tUpdatedAt: t0,\n\t\tTimeout:   false,\n\t}\n)\n\n// TestAlertsSubscribePutStarvation tests starvation of `iterator.Close` and\n// `alerts.Put`. Both `Subscribe` and `Put` use the Alerts.mtx lock. `Subscribe`\n// needs it to subscribe and more importantly unsubscribe `Alerts.listeners`. `Put`\n// uses the lock to add additional alerts and iterate the `Alerts.listeners` map.\n// If the channel of a listener is at its limit, `alerts.Lock` is blocked, whereby\n// a listener can not unsubscribe as the lock is hold by `alerts.Lock`.\nfunc TestAlertsSubscribePutStarvation(t *testing.T) {\n\tmarker := types.NewMarker(prometheus.NewRegistry())\n\talerts, err := NewAlerts(context.Background(), marker, 30*time.Minute, 0, noopCallback{}, promslog.NewNopLogger(), prometheus.NewRegistry(), nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\titerator := alerts.Subscribe(\"test\")\n\n\talertsToInsert := []*types.Alert{}\n\t// Exhaust alert channel\n\tfor i := range alertChannelLength + 1 {\n\t\talertsToInsert = append(alertsToInsert, &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\t// Make sure the fingerprints differ\n\t\t\t\tLabels:       model.LabelSet{\"iteration\": model.LabelValue(strconv.Itoa(i))},\n\t\t\t\tAnnotations:  model.LabelSet{\"foo\": \"bar\"},\n\t\t\t\tStartsAt:     t0,\n\t\t\t\tEndsAt:       t1,\n\t\t\t\tGeneratorURL: \"http://example.com/prometheus\",\n\t\t\t},\n\t\t\tUpdatedAt: t0,\n\t\t\tTimeout:   false,\n\t\t})\n\t}\n\n\tputIsDone := make(chan struct{})\n\tputsErr := make(chan error, 1)\n\tgo func() {\n\t\tif err := alerts.Put(context.Background(), alertsToInsert...); err != nil {\n\t\t\tputsErr <- err\n\t\t\treturn\n\t\t}\n\n\t\tputIsDone <- struct{}{}\n\t}()\n\n\t// Increase probability that `iterator.Close` is called after `alerts.Put`.\n\ttime.Sleep(100 * time.Millisecond)\n\titerator.Close()\n\n\tselect {\n\tcase <-putsErr:\n\t\tt.Fatal(err)\n\tcase <-putIsDone:\n\t\t// continue\n\tcase <-time.After(100 * time.Millisecond):\n\t\tt.Fatal(\"expected `alerts.Put` and `iterator.Close` not to starve each other\")\n\t}\n}\n\nfunc TestDeadLock(t *testing.T) {\n\tt0 := time.Now()\n\tt1 := t0.Add(5 * time.Second)\n\n\tmarker := types.NewMarker(prometheus.NewRegistry())\n\t// Run gc every 5 milliseconds to increase the possibility of a deadlock with Subscribe()\n\talerts, err := NewAlerts(context.Background(), marker, 5*time.Millisecond, 0, noopCallback{}, promslog.NewNopLogger(), prometheus.NewRegistry(), nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\talertsToInsert := []*types.Alert{}\n\tfor i := range 200 + 1 {\n\t\talertsToInsert = append(alertsToInsert, &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\t// Make sure the fingerprints differ\n\t\t\t\tLabels:       model.LabelSet{\"iteration\": model.LabelValue(strconv.Itoa(i))},\n\t\t\t\tAnnotations:  model.LabelSet{\"foo\": \"bar\"},\n\t\t\t\tStartsAt:     t0,\n\t\t\t\tEndsAt:       t1,\n\t\t\t\tGeneratorURL: \"http://example.com/prometheus\",\n\t\t\t},\n\t\t\tUpdatedAt: t0,\n\t\t\tTimeout:   false,\n\t\t})\n\t}\n\n\tif err := alerts.Put(context.Background(), alertsToInsert...); err != nil {\n\t\tt.Fatal(\"Unable to add alerts\")\n\t}\n\tdone := make(chan bool)\n\n\t// call subscribe repeatedly in a goroutine to increase\n\t// the possibility of a deadlock occurring\n\tgo func() {\n\t\ttick := time.NewTicker(10 * time.Millisecond)\n\t\tdefer tick.Stop()\n\t\tstopAfter := time.After(1 * time.Second)\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-tick.C:\n\t\t\t\talerts.Subscribe(\"test\")\n\t\t\tcase <-stopAfter:\n\t\t\t\tdone <- true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}()\n\n\tselect {\n\tcase <-done:\n\t\t// no deadlock\n\t\talerts.Close()\n\tcase <-time.After(10 * time.Second):\n\t\tt.Error(\"Deadlock detected\")\n\t}\n}\n\nfunc TestAlertsPut(t *testing.T) {\n\tmarker := types.NewMarker(prometheus.NewRegistry())\n\talerts, err := NewAlerts(context.Background(), marker, 30*time.Minute, 0, noopCallback{}, promslog.NewNopLogger(), prometheus.NewRegistry(), nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tinsert := []*types.Alert{alert1, alert2, alert3}\n\n\tif err := alerts.Put(context.Background(), insert...); err != nil {\n\t\tt.Fatalf(\"Insert failed: %s\", err)\n\t}\n\n\tfor i, a := range insert {\n\t\tres, err := alerts.Get(a.Fingerprint())\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"retrieval error: %s\", err)\n\t\t}\n\t\trequire.NoError(t, alertDiff(a, res), \"unexpected alert: %d\", i)\n\t}\n}\n\nfunc TestAlertsSubscribe(t *testing.T) {\n\tmarker := types.NewMarker(prometheus.NewRegistry())\n\n\tctx := t.Context()\n\talerts, err := NewAlerts(ctx, marker, 30*time.Minute, 0, noopCallback{}, promslog.NewNopLogger(), prometheus.NewRegistry(), nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Add alert1 to validate if pending alerts will be sent.\n\tif err := alerts.Put(ctx, alert1); err != nil {\n\t\tt.Fatalf(\"Insert failed: %s\", err)\n\t}\n\n\texpectedAlerts := map[model.Fingerprint]*types.Alert{\n\t\talert1.Fingerprint(): alert1,\n\t\talert2.Fingerprint(): alert2,\n\t\talert3.Fingerprint(): alert3,\n\t}\n\n\t// Start many consumers and make sure that each receives all the subsequent alerts.\n\tvar (\n\t\tnb     = 100\n\t\tfatalc = make(chan string, nb)\n\t\twg     sync.WaitGroup\n\t)\n\twg.Add(nb)\n\tfor i := range nb {\n\t\tgo func(i int) {\n\t\t\tdefer wg.Done()\n\n\t\t\tit := alerts.Subscribe(\"test\")\n\t\t\tdefer it.Close()\n\n\t\t\treceived := make(map[model.Fingerprint]struct{})\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase got, ok := <-it.Next():\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tfatalc <- fmt.Sprintf(\"Iterator %d closed\", i)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif it.Err() != nil {\n\t\t\t\t\t\tfatalc <- fmt.Sprintf(\"Iterator %d: %v\", i, it.Err())\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\texpected := expectedAlerts[got.Data.Fingerprint()]\n\t\t\t\t\tif err := alertDiff(got.Data, expected); err != nil {\n\t\t\t\t\t\tfatalc <- fmt.Sprintf(\"Unexpected alert (iterator %d)\\n%s\", i, err.Error())\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\treceived[got.Data.Fingerprint()] = struct{}{}\n\t\t\t\t\tif len(received) == len(expectedAlerts) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\tcase <-time.After(5 * time.Second):\n\t\t\t\t\tfatalc <- fmt.Sprintf(\"Unexpected number of alerts for iterator %d, got: %d, expected: %d\", i, len(received), len(expectedAlerts))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}(i)\n\t}\n\n\t// Add more alerts that should be received by the subscribers.\n\tif err := alerts.Put(ctx, alert2); err != nil {\n\t\tt.Fatalf(\"Insert failed: %s\", err)\n\t}\n\tif err := alerts.Put(ctx, alert3); err != nil {\n\t\tt.Fatalf(\"Insert failed: %s\", err)\n\t}\n\n\twg.Wait()\n\tclose(fatalc)\n\tfatal, ok := <-fatalc\n\tif ok {\n\t\tt.Fatal(fatal)\n\t}\n}\n\nfunc TestAlertsGetPending(t *testing.T) {\n\tmarker := types.NewMarker(prometheus.NewRegistry())\n\talerts, err := NewAlerts(context.Background(), marker, 30*time.Minute, 0, noopCallback{}, promslog.NewNopLogger(), nil, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tctx := context.Background()\n\tif err := alerts.Put(ctx, alert1, alert2); err != nil {\n\t\tt.Fatalf(\"Insert failed: %s\", err)\n\t}\n\n\texpectedAlerts := map[model.Fingerprint]*types.Alert{\n\t\talert1.Fingerprint(): alert1,\n\t\talert2.Fingerprint(): alert2,\n\t}\n\titerator := alerts.GetPending()\n\tfor actual := range iterator.Next() {\n\t\texpected := expectedAlerts[actual.Data.Fingerprint()]\n\t\trequire.NoError(t, alertDiff(actual.Data, expected))\n\t}\n\n\tif err := alerts.Put(ctx, alert3); err != nil {\n\t\tt.Fatalf(\"Insert failed: %s\", err)\n\t}\n\n\texpectedAlerts = map[model.Fingerprint]*types.Alert{\n\t\talert1.Fingerprint(): alert1,\n\t\talert2.Fingerprint(): alert2,\n\t\talert3.Fingerprint(): alert3,\n\t}\n\titerator = alerts.GetPending()\n\tfor actual := range iterator.Next() {\n\t\texpected := expectedAlerts[actual.Data.Fingerprint()]\n\t\trequire.NoError(t, alertDiff(actual.Data, expected))\n\t}\n}\n\nfunc TestAlertsGC(t *testing.T) {\n\tmarker := types.NewMarker(prometheus.NewRegistry())\n\talerts, err := NewAlerts(context.Background(), marker, 200*time.Millisecond, 0, noopCallback{}, promslog.NewNopLogger(), nil, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tinsert := []*types.Alert{alert1, alert2, alert3}\n\n\tif err := alerts.Put(context.Background(), insert...); err != nil {\n\t\tt.Fatalf(\"Insert failed: %s\", err)\n\t}\n\n\tfor _, a := range insert {\n\t\tmarker.SetActiveOrSilenced(a.Fingerprint(), nil)\n\t\tmarker.SetInhibited(a.Fingerprint())\n\t\tif !marker.Active(a.Fingerprint()) {\n\t\t\tt.Errorf(\"error setting status: %v\", a)\n\t\t}\n\t}\n\n\ttime.Sleep(300 * time.Millisecond)\n\n\tfor i, a := range insert {\n\t\t_, err := alerts.Get(a.Fingerprint())\n\t\trequire.Error(t, err)\n\t\trequire.Equal(t, store.ErrNotFound, err, \"alert %d didn't get GC'd: %v\", i, err)\n\n\t\ts := marker.Status(a.Fingerprint())\n\t\tif s.State != types.AlertStateUnprocessed {\n\t\t\tt.Errorf(\"marker %d didn't get GC'd: %v\", i, s)\n\t\t}\n\t}\n}\n\nfunc TestAlertsStoreCallback(t *testing.T) {\n\tcb := &limitCountCallback{limit: 3}\n\n\tmarker := types.NewMarker(prometheus.NewRegistry())\n\talerts, err := NewAlerts(context.Background(), marker, 200*time.Millisecond, 0, cb, promslog.NewNopLogger(), nil, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tctx := context.Background()\n\terr = alerts.Put(ctx, alert1, alert2, alert3)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif num := cb.alerts.Load(); num != 3 {\n\t\tt.Fatalf(\"unexpected number of alerts in the store, expected %v, got %v\", 3, num)\n\t}\n\n\talert1Mod := *alert1\n\talert1Mod.Annotations = model.LabelSet{\"foo\": \"bar\", \"new\": \"test\"} // Update annotations for alert1\n\n\talert4 := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels:       model.LabelSet{\"bar4\": \"foo4\"},\n\t\t\tAnnotations:  model.LabelSet{\"foo4\": \"bar4\"},\n\t\t\tStartsAt:     t0,\n\t\t\tEndsAt:       t1,\n\t\t\tGeneratorURL: \"http://example.com/prometheus\",\n\t\t},\n\t\tUpdatedAt: t0,\n\t\tTimeout:   false,\n\t}\n\n\terr = alerts.Put(ctx, &alert1Mod, alert4)\n\t// Verify that we failed to put new alert into store (not reported via error, only checked using Load)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error %v\", err)\n\t}\n\n\tif num := cb.alerts.Load(); num != 3 {\n\t\tt.Fatalf(\"unexpected number of alerts in the store, expected %v, got %v\", 3, num)\n\t}\n\n\t// But we still managed to update alert1, since callback doesn't report error when updating existing alert.\n\ta, err := alerts.Get(alert1.Fingerprint())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\trequire.NoError(t, alertDiff(a, &alert1Mod))\n\n\t// Now wait until existing alerts are GC-ed, and make sure that callback was called.\n\ttime.Sleep(300 * time.Millisecond)\n\n\tif num := cb.alerts.Load(); num != 0 {\n\t\tt.Fatalf(\"unexpected number of alerts in the store, expected %v, got %v\", 0, num)\n\t}\n\n\terr = alerts.Put(ctx, alert4)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestAlerts_CountByState(t *testing.T) {\n\tmarker := types.NewMarker(prometheus.NewRegistry())\n\talerts, err := NewAlerts(context.Background(), marker, 200*time.Millisecond, 0, nil, promslog.NewNopLogger(), nil, nil)\n\trequire.NoError(t, err)\n\n\tcountTotal := func() int {\n\t\tactive, suppressed, unprocessed := alerts.countByState()\n\t\treturn active + suppressed + unprocessed\n\t}\n\n\t// First, there shouldn't be any alerts.\n\trequire.Equal(t, 0, countTotal())\n\n\t// When you insert a new alert that will eventually be active, it should be unprocessed first.\n\tnow := time.Now()\n\ta1 := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels:       model.LabelSet{\"bar\": \"foo\"},\n\t\t\tAnnotations:  model.LabelSet{\"foo\": \"bar\"},\n\t\t\tStartsAt:     now,\n\t\t\tEndsAt:       now.Add(400 * time.Millisecond),\n\t\t\tGeneratorURL: \"http://example.com/prometheus\",\n\t\t},\n\t\tUpdatedAt: now,\n\t\tTimeout:   false,\n\t}\n\n\tctx := context.Background()\n\talerts.Put(ctx, a1)\n\t_, _, unprocessed := alerts.countByState()\n\trequire.Equal(t, 1, unprocessed)\n\trequire.Equal(t, 1, countTotal())\n\trequire.Eventually(t, func() bool {\n\t\t// When the alert will eventually expire and is considered resolved - it won't count.\n\t\treturn countTotal() == 0\n\t}, 600*time.Millisecond, 100*time.Millisecond)\n\n\tnow = time.Now()\n\ta2 := &types.Alert{\n\t\tAlert: model.Alert{\n\t\t\tLabels:       model.LabelSet{\"bar\": \"foo\"},\n\t\t\tAnnotations:  model.LabelSet{\"foo\": \"bar\"},\n\t\t\tStartsAt:     now,\n\t\t\tEndsAt:       now.Add(400 * time.Millisecond),\n\t\t\tGeneratorURL: \"http://example.com/prometheus\",\n\t\t},\n\t\tUpdatedAt: now,\n\t\tTimeout:   false,\n\t}\n\n\t// When insert an alert, and then silence it. It shows up with the correct filter.\n\talerts.Put(ctx, a2)\n\tmarker.SetActiveOrSilenced(a2.Fingerprint(), []string{\"1\"})\n\t_, suppressed, _ := alerts.countByState()\n\trequire.Equal(t, 1, suppressed)\n\trequire.Equal(t, 1, countTotal())\n\n\trequire.Eventually(t, func() bool {\n\t\t// When the alert will eventually expire and is considered resolved - it won't count.\n\t\treturn countTotal() == 0\n\t}, 600*time.Millisecond, 100*time.Millisecond)\n}\n\nfunc alertDiff(left, right *types.Alert) error {\n\tif left == nil || right == nil {\n\t\treturn errors.New(\"should not be nil\")\n\t}\n\tcomparisons := []struct {\n\t\tname     string\n\t\tisEqual  bool\n\t\texpected any\n\t\tgot      any\n\t}{\n\t\t{\"Labels\", reflect.DeepEqual(right.Labels, left.Labels), right.Labels, left.Labels},\n\t\t{\"Annotations\", reflect.DeepEqual(right.Annotations, left.Annotations), right.Annotations, left.Annotations},\n\t\t{\"StartsAt\", right.StartsAt.Equal(left.StartsAt), right.StartsAt, left.StartsAt},\n\t\t{\"EndsAt\", right.EndsAt.Equal(left.EndsAt), right.EndsAt, left.EndsAt},\n\t\t{\"UpdatedAt\", right.UpdatedAt.Equal(left.UpdatedAt), right.UpdatedAt, left.UpdatedAt},\n\t\t{\"GeneratorURL\", right.GeneratorURL == left.GeneratorURL, right.GeneratorURL, left.GeneratorURL},\n\t\t{\"Timeout\", right.Timeout == left.Timeout, right.Timeout, left.Timeout},\n\t}\n\tvar errs []error\n\tfor _, comp := range comparisons {\n\t\tif !comp.isEqual {\n\t\t\terrs = append(errs, fmt.Errorf(\"field `%s` mismatch.\\n Expected: %v\\n Got: %v\", comp.name, comp.expected, comp.got))\n\t\t}\n\t}\n\treturn errors.Join(errs...)\n}\n\ntype limitCountCallback struct {\n\talerts  atomic.Int32\n\tgcCount atomic.Int32\n\tlimit   int\n}\n\nvar errTooManyAlerts = fmt.Errorf(\"too many alerts\")\n\nfunc (l *limitCountCallback) PreStore(_ *types.Alert, existing bool) error {\n\tif existing {\n\t\treturn nil\n\t}\n\n\tif int(l.alerts.Load())+1 > l.limit {\n\t\treturn errTooManyAlerts\n\t}\n\n\treturn nil\n}\n\nfunc (l *limitCountCallback) PostStore(_ *types.Alert, existing bool) {\n\tif !existing {\n\t\tl.alerts.Add(1)\n\t\tl.gcCount.Add(1)\n\t}\n}\n\nfunc (l *limitCountCallback) PostDelete(_ *types.Alert) {\n\tl.alerts.Add(-1)\n}\n\nfunc (l *limitCountCallback) PostGC(fingerprints model.Fingerprints) {\n\tl.gcCount.Add(-int32(fingerprints.Len()))\n}\n\nfunc TestAlertsConcurrently(t *testing.T) {\n\tcallback := &limitCountCallback{limit: 100}\n\ta, err := NewAlerts(context.Background(), types.NewMarker(prometheus.NewRegistry()), time.Millisecond, 0, callback, promslog.NewNopLogger(), nil, nil)\n\trequire.NoError(t, err)\n\n\tstopc := make(chan struct{})\n\tfailc := make(chan struct{})\n\tgo func() {\n\t\ttime.Sleep(2 * time.Second)\n\t\tclose(stopc)\n\t}()\n\texpire := 10 * time.Millisecond\n\twg := sync.WaitGroup{}\n\tfor range 100 {\n\t\twg.Go(func() {\n\t\t\tj := 0\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-failc:\n\t\t\t\t\treturn\n\t\t\t\tcase <-stopc:\n\t\t\t\t\treturn\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t\tnow := time.Now()\n\t\t\t\terr := a.Put(context.Background(), &types.Alert{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tLabels:   model.LabelSet{\"bar\": model.LabelValue(strconv.Itoa(j))},\n\t\t\t\t\t\tStartsAt: now,\n\t\t\t\t\t\tEndsAt:   now.Add(expire),\n\t\t\t\t\t},\n\t\t\t\t\tUpdatedAt: now,\n\t\t\t\t})\n\t\t\t\tif err != nil && !errors.Is(err, errTooManyAlerts) {\n\t\t\t\t\tclose(failc)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tj++\n\t\t\t}\n\t\t})\n\t}\n\twg.Wait()\n\tselect {\n\tcase <-failc:\n\t\tt.Fatalf(\"unexpected error happened\")\n\tdefault:\n\t}\n\n\ttime.Sleep(expire)\n\trequire.Eventually(t, func() bool {\n\t\t// When the alert will eventually expire and is considered resolved - it won't count.\n\t\tactive, _, _ := a.countByState()\n\t\treturn active == 0\n\t}, 2*expire, expire)\n\trequire.Equal(t, int32(0), callback.alerts.Load())\n\trequire.Equal(t, int32(0), callback.gcCount.Load())\n}\n\nfunc TestSubscriberChannelMetrics(t *testing.T) {\n\tmarker := types.NewMarker(prometheus.NewRegistry())\n\treg := prometheus.NewRegistry()\n\talerts, err := NewAlerts(context.Background(), marker, 30*time.Minute, 0, noopCallback{}, promslog.NewNopLogger(), reg, nil)\n\trequire.NoError(t, err)\n\n\tsubscriberName := \"test_subscriber\"\n\n\t// Subscribe to alerts\n\titerator := alerts.Subscribe(subscriberName)\n\tdefer iterator.Close()\n\n\t// Consume alerts in the background\n\tgo func() {\n\t\tfor range iterator.Next() {\n\t\t\t// Just drain the channel\n\t\t}\n\t}()\n\n\t// Helper function to get counter value\n\tgetCounterValue := func(name, labelName, labelValue string) float64 {\n\t\tmetrics, err := reg.Gather()\n\t\trequire.NoError(t, err)\n\t\tfor _, mf := range metrics {\n\t\t\tif mf.GetName() == name {\n\t\t\t\tfor _, m := range mf.GetMetric() {\n\t\t\t\t\tfor _, label := range m.GetLabel() {\n\t\t\t\t\t\tif label.GetName() == labelName && label.GetValue() == labelValue {\n\t\t\t\t\t\t\treturn m.GetCounter().GetValue()\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn 0\n\t}\n\n\t// Initially, the counter should be 0\n\twriteCount := getCounterValue(\"alertmanager_alerts_subscriber_channel_writes_total\", \"subscriber\", subscriberName)\n\trequire.Equal(t, 0.0, writeCount, \"subscriberChannelWrites should start at 0\")\n\n\t// Put some alerts\n\tnow := time.Now()\n\talertsToSend := []*types.Alert{\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels:       model.LabelSet{\"test\": \"1\"},\n\t\t\t\tAnnotations:  model.LabelSet{\"foo\": \"bar\"},\n\t\t\t\tStartsAt:     now,\n\t\t\t\tEndsAt:       now.Add(1 * time.Hour),\n\t\t\t\tGeneratorURL: \"http://example.com/prometheus\",\n\t\t\t},\n\t\t\tUpdatedAt: now,\n\t\t\tTimeout:   false,\n\t\t},\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels:       model.LabelSet{\"test\": \"2\"},\n\t\t\t\tAnnotations:  model.LabelSet{\"foo\": \"bar\"},\n\t\t\t\tStartsAt:     now,\n\t\t\t\tEndsAt:       now.Add(1 * time.Hour),\n\t\t\t\tGeneratorURL: \"http://example.com/prometheus\",\n\t\t\t},\n\t\t\tUpdatedAt: now,\n\t\t\tTimeout:   false,\n\t\t},\n\t\t{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels:       model.LabelSet{\"test\": \"3\"},\n\t\t\t\tAnnotations:  model.LabelSet{\"foo\": \"bar\"},\n\t\t\t\tStartsAt:     now,\n\t\t\t\tEndsAt:       now.Add(1 * time.Hour),\n\t\t\t\tGeneratorURL: \"http://example.com/prometheus\",\n\t\t\t},\n\t\t\tUpdatedAt: now,\n\t\t\tTimeout:   false,\n\t\t},\n\t}\n\n\terr = alerts.Put(context.Background(), alertsToSend...)\n\trequire.NoError(t, err)\n\n\t// Verify the counter incremented for each successful write\n\trequire.Eventually(t, func() bool {\n\t\twriteCount := getCounterValue(\"alertmanager_alerts_subscriber_channel_writes_total\", \"subscriber\", subscriberName)\n\t\treturn writeCount == float64(len(alertsToSend))\n\t}, 1*time.Second, 10*time.Millisecond, \"subscriberChannelWrites should equal the number of alerts sent\")\n}\n"
  },
  {
    "path": "provider/provider.go",
    "content": "// Copyright 2015 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage provider\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/prometheus/common/model\"\n\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// ErrNotFound is returned if a provider cannot find a requested item.\nvar ErrNotFound = fmt.Errorf(\"item not found\")\n\ntype Alert struct {\n\t// Header contains metadata, for example propagated tracing information.\n\tHeader map[string]string\n\tData   *types.Alert\n}\n\n// Iterator provides the functions common to all iterators. To be useful, a\n// specific iterator interface (e.g. AlertIterator) has to be implemented that\n// provides a Next method.\ntype Iterator interface {\n\t// Err returns the current error. It is not safe to call it concurrently\n\t// with other iterator methods or while reading from a channel returned\n\t// by the iterator.\n\tErr() error\n\t// Close must be called to release resources once the iterator is not\n\t// used anymore.\n\tClose()\n}\n\n// AlertIterator is an Iterator for Alerts.\ntype AlertIterator interface {\n\tIterator\n\t// Next returns a channel that will be closed once the iterator is\n\t// exhausted. It is not necessary to exhaust the iterator but Close must\n\t// be called in any case to release resources used by the iterator (even\n\t// if the iterator is exhausted).\n\tNext() <-chan *Alert\n}\n\n// NewAlertIterator returns a new AlertIterator based on the generic alertIterator type.\nfunc NewAlertIterator(ch <-chan *Alert, done chan struct{}, err error) AlertIterator {\n\treturn &alertIterator{\n\t\tch:   ch,\n\t\tdone: done,\n\t\terr:  err,\n\t}\n}\n\n// alertIterator implements AlertIterator. So far, this one fits all providers.\ntype alertIterator struct {\n\tch   <-chan *Alert\n\tdone chan struct{}\n\terr  error\n}\n\nfunc (ai alertIterator) Next() <-chan *Alert {\n\treturn ai.ch\n}\n\nfunc (ai alertIterator) Err() error { return ai.err }\nfunc (ai alertIterator) Close()     { close(ai.done) }\n\n// Alerts gives access to a set of alerts. All methods are goroutine-safe.\ntype Alerts interface {\n\t// Subscribe returns an iterator over active alerts that have not been\n\t// resolved and successfully notified about.\n\t// They are not guaranteed to be in chronological order.\n\tSubscribe(name string) AlertIterator\n\n\t// SlurpAndSubcribe returns a list of all active alerts which are available\n\t// in the provider before the call to SlurpAndSubcribe and an iterator\n\t// of all alerts available after the call to SlurpAndSubcribe.\n\t// SlurpAndSubcribe can be used by clients which need to build in memory state\n\t// to know when they've processed the 'initial' batch of alerts in a provider\n\t// after they reload their subscription.\n\t// Implementation of SlurpAndSubcribe is optional - providers may choose to\n\t// return an empty list for the first return value and the result of Subscribe\n\t// for the second return value.\n\tSlurpAndSubscribe(name string) ([]*types.Alert, AlertIterator)\n\n\t// GetPending returns an iterator over all alerts that have\n\t// pending notifications.\n\tGetPending() AlertIterator\n\t// Get returns the alert for a given fingerprint.\n\tGet(model.Fingerprint) (*types.Alert, error)\n\t// Put adds the given set of alerts to the set.\n\tPut(ctx context.Context, alerts ...*types.Alert) error\n}\n"
  },
  {
    "path": "scripts/genproto.sh",
    "content": "#!/usr/bin/env bash\n# Generate all protobuf bindings.\nset -euo pipefail\nshopt -s failglob\n\nif ! [[ \"$0\" = \"scripts/genproto.sh\" ]]; then\n  echo \"must be run from repository root\"\n  exit 255\nfi\n\necho \"generating files\"\ngo tool -modfile=internal/tools/go.mod buf dep update\ngo tool -modfile=internal/tools/go.mod buf generate\n"
  },
  {
    "path": "scripts/swagger.sh",
    "content": "#!/usr/bin/env bash\n# Generate api\nset -euo pipefail\nshopt -s failglob\n\nif ! [[ \"$0\" = \"scripts/swagger.sh\" ]]; then\n  echo \"must be run from repository root\"\n  exit 255\nfi\n\necho \"generating files\"\n  rm -r api/v2/{client,models,restapi} ||:\n  go tool -modfile=internal/tools/go.mod swagger generate server -f api/v2/openapi.yaml --copyright-file=COPYRIGHT.txt --exclude-main -A alertmanager --target api/v2/\n  go tool -modfile=internal/tools/go.mod swagger generate client -f api/v2/openapi.yaml --copyright-file=COPYRIGHT.txt --skip-models --target api/v2\n"
  },
  {
    "path": "silence/cache.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage silence\n\nimport (\n\t\"sync\"\n\n\t\"github.com/prometheus/common/model\"\n)\n\n// cacheEntry stores the IDs of silences that match an alert and the version of the silences state the\n// result is based on.\ntype cacheEntry struct {\n\tsilenceIDs []string\n\tversion    int\n}\n\n// newCacheEntry creates a new cacheEntry.\nfunc newCacheEntry(version int, silenceIDs ...string) *cacheEntry {\n\treturn &cacheEntry{\n\t\tsilenceIDs: silenceIDs,\n\t\tversion:    version,\n\t}\n}\n\n// count returns the number of silence IDs in the cacheEntry.\nfunc (e *cacheEntry) count() int {\n\treturn len(e.silenceIDs)\n}\n\n// cache stores the IDs of silences that match an alert and the version of the silences state the\n// result is based on.\ntype cache struct {\n\tentries map[model.Fingerprint]*cacheEntry\n\tmtx     sync.RWMutex\n}\n\n// delete removes the cacheEntry for the given fingerprint.\nfunc (c *cache) delete(fp model.Fingerprint) {\n\tc.mtx.Lock()\n\tdefer c.mtx.Unlock()\n\tdelete(c.entries, fp)\n}\n\n// get returns the cacheEntry for the given fingerprint.\n// The returned entry is not a copy, so it should not be modified.\nfunc (c *cache) get(fp model.Fingerprint) *cacheEntry {\n\tc.mtx.RLock()\n\tdefer c.mtx.RUnlock()\n\tif e, found := c.entries[fp]; found {\n\t\treturn e\n\t}\n\treturn &cacheEntry{}\n}\n\n// set sets the cacheEntry for the given fingerprint.\nfunc (c *cache) set(fp model.Fingerprint, entry *cacheEntry) {\n\tc.mtx.Lock()\n\tdefer c.mtx.Unlock()\n\tc.entries[fp] = entry\n}\n"
  },
  {
    "path": "silence/cache_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage silence\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc newTestCache() *cache {\n\treturn &cache{entries: map[model.Fingerprint]*cacheEntry{}}\n}\n\nfunc TestCacheEntryCount(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tentry    *cacheEntry\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tname:     \"zero for no silence IDs\",\n\t\t\tentry:    newCacheEntry(1),\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"one entry\",\n\t\t\tentry:    newCacheEntry(2, \"a\"),\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple entries\",\n\t\t\tentry:    newCacheEntry(3, \"a\", \"b\", \"c\"),\n\t\t\texpected: 3,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\trequire.Equal(t, tt.expected, tt.entry.count())\n\t\t})\n\t}\n}\n\nfunc TestNewCacheEntry(t *testing.T) {\n\te := newCacheEntry(42, \"s1\", \"s2\")\n\n\trequire.Equal(t, []string{\"s1\", \"s2\"}, e.silenceIDs)\n\trequire.Equal(t, 42, e.version)\n}\n\nfunc TestCacheSetAndGet(t *testing.T) {\n\tc := newTestCache()\n\tfp := model.Fingerprint(1)\n\n\t// Get on empty cache returns zero-value entry.\n\tentry := c.get(fp)\n\trequire.Equal(t, 0, entry.count())\n\trequire.Equal(t, 0, entry.version)\n\n\t// Set and retrieve.\n\tc.set(fp, newCacheEntry(5, \"s1\"))\n\tentry = c.get(fp)\n\trequire.Equal(t, []string{\"s1\"}, entry.silenceIDs)\n\trequire.Equal(t, 5, entry.version)\n}\n\nfunc TestCacheOverwrite(t *testing.T) {\n\tc := newTestCache()\n\tfp := model.Fingerprint(1)\n\n\tc.set(fp, newCacheEntry(1, \"s1\"))\n\tc.set(fp, newCacheEntry(2, \"s2\", \"s3\"))\n\n\tentry := c.get(fp)\n\trequire.Equal(t, []string{\"s2\", \"s3\"}, entry.silenceIDs)\n\trequire.Equal(t, 2, entry.version)\n}\n\nfunc TestCacheDelete(t *testing.T) {\n\tc := newTestCache()\n\tfp := model.Fingerprint(1)\n\n\tc.set(fp, newCacheEntry(1, \"s1\"))\n\tbefore := c.get(fp)\n\trequire.Positive(t, before.count())\n\n\tc.delete(fp)\n\n\tentry := c.get(fp)\n\trequire.Equal(t, 0, entry.count())\n\trequire.Equal(t, 0, entry.version)\n}\n\nfunc TestCacheDeleteNonExistent(t *testing.T) {\n\tc := newTestCache()\n\n\t// Deleting a key that doesn't exist should not panic.\n\trequire.NotPanics(t, func() {\n\t\tc.delete(model.Fingerprint(999))\n\t})\n}\n\nfunc TestCacheDeleteIsolation(t *testing.T) {\n\tc := newTestCache()\n\tfp1 := model.Fingerprint(1)\n\tfp2 := model.Fingerprint(2)\n\n\tc.set(fp1, newCacheEntry(1, \"s1\"))\n\tc.set(fp2, newCacheEntry(2, \"s2\"))\n\n\tc.delete(fp1)\n\n\t// fp1 should be gone.\n\tentry1 := c.get(fp1)\n\trequire.Equal(t, 0, entry1.count())\n\t// fp2 should be untouched.\n\tentry2 := c.get(fp2)\n\trequire.Equal(t, []string{\"s2\"}, entry2.silenceIDs)\n}\n\nfunc TestCacheMultipleFingerprints(t *testing.T) {\n\tc := newTestCache()\n\n\tfor i := range 100 {\n\t\tfp := model.Fingerprint(i)\n\t\tc.set(fp, newCacheEntry(i, \"s\"))\n\t}\n\n\tfor i := range 100 {\n\t\tfp := model.Fingerprint(i)\n\t\tentry := c.get(fp)\n\t\trequire.Equal(t, 1, entry.count())\n\t\trequire.Equal(t, i, entry.version)\n\t}\n}\n\nfunc TestCacheConcurrentAccess(t *testing.T) {\n\tc := newTestCache()\n\tfp := model.Fingerprint(1)\n\tc.set(fp, newCacheEntry(0, \"initial\"))\n\n\tvar wg sync.WaitGroup\n\tconst goroutines = 50\n\n\t// Concurrent readers.\n\tfor range goroutines {\n\t\twg.Go(func() {\n\t\t\tfor range 100 {\n\t\t\t\t_ = c.get(fp)\n\t\t\t}\n\t\t})\n\t}\n\n\t// Concurrent writers.\n\tfor i := range goroutines {\n\t\twg.Go(func() {\n\t\t\tfor j := range 100 {\n\t\t\t\tc.set(fp, newCacheEntry(i*100+j, \"w\"))\n\t\t\t}\n\t\t})\n\t}\n\n\t// Concurrent deleters.\n\tfor range goroutines {\n\t\twg.Go(func() {\n\t\t\tfor range 100 {\n\t\t\t\tc.delete(fp)\n\t\t\t}\n\t\t})\n\t}\n\n\twg.Wait()\n}\n"
  },
  {
    "path": "silence/silence.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Package silence provides a storage for silences, which can share its\n// state over a mesh network and snapshot it.\npackage silence\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"math/rand\"\n\t\"os\"\n\t\"regexp\"\n\t\"slices\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/coder/quartz\"\n\tuuid \"github.com/google/uuid\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/codes\"\n\t\"go.opentelemetry.io/otel/trace\"\n\t\"google.golang.org/protobuf/encoding/protodelim\"\n\t\"google.golang.org/protobuf/proto\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/prometheus/alertmanager/cluster\"\n\t\"github.com/prometheus/alertmanager/matcher/compat\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n\tpb \"github.com/prometheus/alertmanager/silence/silencepb\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nvar tracer = otel.Tracer(\"github.com/prometheus/alertmanager/silence\")\n\n// ErrNotFound is returned if a silence was not found.\nvar ErrNotFound = errors.New(\"silence not found\")\n\n// ErrInvalidState is returned if the state isn't valid.\nvar ErrInvalidState = errors.New(\"invalid state\")\n\ntype matcherIndex map[string]labels.MatcherSet\n\n// get retrieves the matcher set for a given silence.\nfunc (c matcherIndex) get(s *pb.Silence) (labels.MatcherSet, error) {\n\tif m, ok := c[s.Id]; ok {\n\t\treturn m, nil\n\t}\n\treturn nil, ErrNotFound\n}\n\n// add compiles a silences' matchers and adds them to the cache.\n// It returns the compiled matcher set.\nfunc (c matcherIndex) add(s *pb.Silence) (labels.MatcherSet, error) {\n\tmatcherSet := make(labels.MatcherSet, 0, len(s.MatcherSets))\n\n\tfor _, ms := range s.MatcherSets {\n\t\tmatchers := make(labels.Matchers, len(ms.Matchers))\n\n\t\tfor i, m := range ms.Matchers {\n\t\t\tvar mt labels.MatchType\n\t\t\tswitch m.Type {\n\t\t\tcase pb.Matcher_EQUAL:\n\t\t\t\tmt = labels.MatchEqual\n\t\t\tcase pb.Matcher_NOT_EQUAL:\n\t\t\t\tmt = labels.MatchNotEqual\n\t\t\tcase pb.Matcher_REGEXP:\n\t\t\t\tmt = labels.MatchRegexp\n\t\t\tcase pb.Matcher_NOT_REGEXP:\n\t\t\t\tmt = labels.MatchNotRegexp\n\t\t\tdefault:\n\t\t\t\treturn nil, fmt.Errorf(\"unknown matcher type %q\", m.Type)\n\t\t\t}\n\t\t\tmatcher, err := labels.NewMatcher(mt, m.Name, m.Pattern)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tmatchers[i] = matcher\n\t\t}\n\n\t\tmatcherSet = append(matcherSet, &matchers)\n\t}\n\n\tc[s.Id] = matcherSet\n\treturn matcherSet, nil\n}\n\n// silenceVersion associates a silence with the Silences version when it was created.\ntype silenceVersion struct {\n\tid      string\n\tversion int\n}\n\n// versionIndex is a index into Silences ordered by the version of Silences when the\n// silence was created. The index is always sorted from lowest to highest version.\n//\n// The versionIndex allows clients of Silences.Query to incrementally update local caches\n// of query results. Instead of a new version requiring the client to scan  everything\n// again to get an up-to-date picture of Silences, they can use the versionIndex to figure\n// out which silences have been added since the last version they saw. This means they can\n// just scan the NEW silences, rather than all of them.\ntype versionIndex []silenceVersion\n\n// add pushes a new silenceVersionMapping to the back of the silenceVersionIndex. It does not\n// validate the input.\nfunc (s *versionIndex) add(version int, sil string) {\n\t*s = append(*s, silenceVersion{version: version, id: sil})\n}\n\n// findVersionGreaterThan uses a log(n) search to find the first index of the versionIndex\n// which has a version higher than version. If any entries with a higher version exist,\n// it returns true and the starting index (which is guaranteed to be a valid index into\n// the slice). Otherwise it returns false.\nfunc (s versionIndex) findVersionGreaterThan(version int) (index int, found bool) {\n\tstartIdx := sort.Search(len(s), func(i int) bool {\n\t\treturn s[i].version > version\n\t})\n\treturn startIdx, startIdx < len(s)\n}\n\n// Silencer binds together a AlertMarker and a Silences to implement the Muter\n// interface.\ntype Silencer struct {\n\tsilences *Silences\n\tcache    *cache\n\tmarker   types.AlertMarker\n\tlogger   *slog.Logger\n}\n\n// NewSilencer returns a new Silencer.\nfunc NewSilencer(silences *Silences, marker types.AlertMarker, logger *slog.Logger) *Silencer {\n\treturn &Silencer{\n\t\tsilences: silences,\n\t\tcache:    &cache{entries: map[model.Fingerprint]*cacheEntry{}},\n\t\tmarker:   marker,\n\t\tlogger:   logger,\n\t}\n}\n\n// Mutes implements the Muter interface.\nfunc (s *Silencer) Mutes(ctx context.Context, lset model.LabelSet) bool {\n\tfp := lset.Fingerprint()\n\tctx, span := tracer.Start(ctx, \"silence.Silencer.Mutes\",\n\t\ttrace.WithAttributes(\n\t\t\tattribute.String(\"alerting.alert.fingerprint\", fp.String()),\n\t\t),\n\t\ttrace.WithSpanKind(trace.SpanKindInternal),\n\t)\n\tdefer span.End()\n\n\t// Get the cached entry for this fingerprint.\n\tcachedEntry := s.cache.get(fp)\n\n\tvar (\n\t\toldSils    []*pb.Silence\n\t\tnewSils    []*pb.Silence\n\t\tnewVersion = cachedEntry.version\n\t)\n\tcacheIsUpToDate := cachedEntry.version == s.silences.Version()\n\n\tif cacheIsUpToDate && cachedEntry.count() == 0 {\n\t\t// Very fast path: no new silences have been added and this lset was not\n\t\t// silenced last time we checked.\n\t\tspan.AddEvent(\"No new silences to match since last check\",\n\t\t\ttrace.WithAttributes(\n\t\t\t\tattribute.Int(\"alerting.silences.cache.count\", cachedEntry.count()),\n\t\t\t),\n\t\t)\n\t\treturn false\n\t}\n\t// Either there are new silences and we need to check if those match lset or there were\n\t// silences last time we queried so we need to see if those are still active/have become\n\t// active. It's possible for there to be both old and new silences.\n\n\tif cachedEntry.count() > 0 {\n\t\t// there were old silences for this lset, we need to find them to check if they\n\t\t// are still active/pending, or have ended.\n\t\tvar err error\n\t\toldSils, _, err = s.silences.Query(\n\t\t\tctx,\n\t\t\tQIDs(cachedEntry.silenceIDs...),\n\t\t\tQState(SilenceStateActive, SilenceStatePending),\n\t\t)\n\t\tif err != nil {\n\t\t\tspan.SetStatus(codes.Error, err.Error())\n\t\t\tspan.RecordError(err)\n\t\t\ts.logger.Error(\n\t\t\t\t\"Querying old silences failed, alerts might not get silenced correctly\",\n\t\t\t\t\"err\", err,\n\t\t\t)\n\t\t}\n\t}\n\n\tif !cacheIsUpToDate {\n\t\t// New silences have been added since the last time the marker was updated. Do a full\n\t\t// query for any silences newer than the markerVersion that match the lset.\n\t\t// On this branch we WILL update newVersion since we can be sure we've seen any silences\n\t\t// newer than markerVersion.\n\t\tvar err error\n\t\tnewSils, newVersion, err = s.silences.Query(\n\t\t\tctx,\n\t\t\tQSince(cachedEntry.version),\n\t\t\tQState(SilenceStateActive, SilenceStatePending),\n\t\t\tQMatches(lset),\n\t\t)\n\t\tif err != nil {\n\t\t\tspan.SetStatus(codes.Error, err.Error())\n\t\t\tspan.RecordError(err)\n\t\t\ts.logger.Error(\n\t\t\t\t\"Querying silences failed, alerts might not get silenced correctly\",\n\t\t\t\t\"err\", err,\n\t\t\t)\n\t\t}\n\t}\n\t// Note: if cacheIsUpToDate, newVersion is left at cachedEntry.version because the Query call\n\t// might already return a newer version, which is not the version our old list of\n\t// applicable silences is based on.\n\n\ttotalSilences := len(oldSils) + len(newSils)\n\tif totalSilences == 0 {\n\t\t// Easy case, neither active nor pending silences anymore.\n\t\ts.cache.set(fp, newCacheEntry(newVersion))\n\t\ts.marker.SetActiveOrSilenced(fp, nil)\n\t\tspan.AddEvent(\"No silences to match\", trace.WithAttributes(\n\t\t\tattribute.Int(\"alerting.silences.count\", totalSilences),\n\t\t))\n\t\treturn false\n\t}\n\n\t// It is still possible that nothing has changed, but finding out is not\n\t// much less effort than just recreating the IDs from the query\n\t// result. So let's do it in any case. Note that we cannot reuse the\n\t// current ID slices for concurrency reasons.\n\tactiveIDs := make([]string, 0, totalSilences)\n\tallIDs := make([]string, 0, totalSilences)\n\tseen := make(map[string]struct{}, totalSilences)\n\tnow := s.silences.nowUTC()\n\n\t// Categorize old and new silences by their current state.\n\t// oldSils and newSils may overlap if a cached silence was updated\n\t// (receiving a new version), so we deduplicate by ID.\n\tfor _, sils := range [...][]*pb.Silence{oldSils, newSils} {\n\t\tfor _, sil := range sils {\n\t\t\tif _, ok := seen[sil.Id]; ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tseen[sil.Id] = struct{}{}\n\t\t\tswitch getState(sil, now) {\n\t\t\tcase SilenceStatePending:\n\t\t\t\tallIDs = append(allIDs, sil.Id)\n\t\t\tcase SilenceStateActive:\n\t\t\t\tactiveIDs = append(activeIDs, sil.Id)\n\t\t\t\tallIDs = append(allIDs, sil.Id)\n\t\t\tdefault:\n\t\t\t\t// Do nothing, silence has expired in the meantime.\n\t\t\t}\n\t\t}\n\t}\n\ts.logger.Debug(\n\t\t\"determined current silences state\",\n\t\t\"now\", now,\n\t\t\"total\", len(allIDs),\n\t\t\"active\", len(activeIDs),\n\t\t\"pending\", len(allIDs)-len(activeIDs),\n\t)\n\t// TODO: remove this sort once the marker is removed.\n\tsort.Strings(activeIDs)\n\n\ts.cache.set(fp, newCacheEntry(newVersion, allIDs...))\n\ts.marker.SetActiveOrSilenced(fp, activeIDs)\n\n\tt := trace.WithAttributes(\n\t\tattribute.Int(\"alerting.silences.active.count\", len(activeIDs)),\n\t\tattribute.Int(\"alerting.silences.pending.count\", len(allIDs)-len(activeIDs)),\n\t\tattribute.Int(\"alerting.silences.total.count\", len(allIDs)),\n\t)\n\n\tmutes := len(activeIDs) > 0\n\tif mutes {\n\t\tspan.AddEvent(\"Silencer mutes alert\", t)\n\t} else {\n\t\tspan.AddEvent(\"Silencer does not mute alert\", t)\n\t}\n\treturn mutes\n}\n\n// The following methods implement mem.AlertStoreCallback.\nfunc (s *Silencer) PreStore(_ *types.Alert, _ bool) error { return nil }\nfunc (s *Silencer) PostStore(_ *types.Alert, _ bool)      {}\nfunc (s *Silencer) PostDelete(alert *types.Alert)         {}\nfunc (s *Silencer) PostGC(ff model.Fingerprints) {\n\tfor _, fp := range ff {\n\t\ts.cache.delete(fp)\n\t}\n}\n\n// Silences holds a silence state that can be modified, queried, and snapshot.\ntype Silences struct {\n\tclock quartz.Clock\n\n\tlogger    *slog.Logger\n\tmetrics   *metrics\n\tretention time.Duration\n\tlimits    Limits\n\n\tmtx       sync.RWMutex\n\tst        state\n\tversion   int // Increments whenever silences are added.\n\tbroadcast func([]byte)\n\tmi        matcherIndex\n\tvi        versionIndex\n}\n\n// Limits contains the limits for silences.\ntype Limits struct {\n\t// MaxSilences limits the maximum number of silences, including expired\n\t// silences.\n\tMaxSilences func() int\n\t// MaxSilenceSizeBytes is the maximum size of an individual silence as\n\t// stored on disk.\n\tMaxSilenceSizeBytes func() int\n}\n\n// MaintenanceFunc represents the function to run as part of the periodic maintenance for silences.\n// It returns the size of the snapshot taken or an error if it failed.\ntype MaintenanceFunc func() (int64, error)\n\ntype metrics struct {\n\tgcDuration                            prometheus.Summary\n\tgcErrorsTotal                         prometheus.Counter\n\tsnapshotDuration                      prometheus.Summary\n\tsnapshotSize                          prometheus.Gauge\n\tqueriesTotal                          prometheus.Counter\n\tqueryErrorsTotal                      prometheus.Counter\n\tqueryDuration                         prometheus.Histogram\n\tqueryScannedTotal                     prometheus.Counter\n\tquerySkippedTotal                     prometheus.Counter\n\tsilencesActive                        prometheus.GaugeFunc\n\tsilencesPending                       prometheus.GaugeFunc\n\tsilencesExpired                       prometheus.GaugeFunc\n\tstateSize                             prometheus.Gauge\n\tmatcherIndexSize                      prometheus.Gauge\n\tversionIndexSize                      prometheus.Gauge\n\tpropagatedMessagesTotal               prometheus.Counter\n\tmaintenanceTotal                      prometheus.Counter\n\tmaintenanceErrorsTotal                prometheus.Counter\n\tmatcherCompileIndexSilenceErrorsTotal prometheus.Counter\n\tmatcherCompileLoadSnapshotErrorsTotal prometheus.Counter\n}\n\nfunc newSilenceMetricByState(r prometheus.Registerer, s *Silences, st SilenceState) prometheus.GaugeFunc {\n\treturn promauto.With(r).NewGaugeFunc(\n\t\tprometheus.GaugeOpts{\n\t\t\tName:        \"alertmanager_silences\",\n\t\t\tHelp:        \"How many silences by state.\",\n\t\t\tConstLabels: prometheus.Labels{\"state\": string(st)},\n\t\t},\n\t\tfunc() float64 {\n\t\t\tcount, err := s.CountState(context.Background(), st)\n\t\t\tif err != nil {\n\t\t\t\ts.logger.Error(\"Counting silences failed\", \"err\", err)\n\t\t\t}\n\t\t\treturn float64(count)\n\t\t},\n\t)\n}\n\nfunc newMetrics(r prometheus.Registerer, s *Silences) *metrics {\n\tm := &metrics{}\n\n\tm.gcDuration = promauto.With(r).NewSummary(prometheus.SummaryOpts{\n\t\tName:       \"alertmanager_silences_gc_duration_seconds\",\n\t\tHelp:       \"Duration of the last silence garbage collection cycle.\",\n\t\tObjectives: map[float64]float64{},\n\t})\n\tm.gcErrorsTotal = promauto.With(r).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_silences_gc_errors_total\",\n\t\tHelp: \"How many silence GC errors were encountered.\",\n\t})\n\tm.snapshotDuration = promauto.With(r).NewSummary(prometheus.SummaryOpts{\n\t\tName:       \"alertmanager_silences_snapshot_duration_seconds\",\n\t\tHelp:       \"Duration of the last silence snapshot.\",\n\t\tObjectives: map[float64]float64{},\n\t})\n\tm.snapshotSize = promauto.With(r).NewGauge(prometheus.GaugeOpts{\n\t\tName: \"alertmanager_silences_snapshot_size_bytes\",\n\t\tHelp: \"Size of the last silence snapshot in bytes.\",\n\t})\n\tm.maintenanceTotal = promauto.With(r).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_silences_maintenance_total\",\n\t\tHelp: \"How many maintenances were executed for silences.\",\n\t})\n\tm.maintenanceErrorsTotal = promauto.With(r).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_silences_maintenance_errors_total\",\n\t\tHelp: \"How many maintenances were executed for silences that failed.\",\n\t})\n\tmatcherCompileErrorsTotal := promauto.With(r).NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tName: \"alertmanager_silences_matcher_compile_errors_total\",\n\t\t\tHelp: \"How many silence matcher compilations failed.\",\n\t\t},\n\t\t[]string{\"stage\"},\n\t)\n\tm.matcherCompileIndexSilenceErrorsTotal = matcherCompileErrorsTotal.WithLabelValues(\"index\")\n\tm.matcherCompileLoadSnapshotErrorsTotal = matcherCompileErrorsTotal.WithLabelValues(\"load_snapshot\")\n\tm.queriesTotal = promauto.With(r).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_silences_queries_total\",\n\t\tHelp: \"How many silence queries were received.\",\n\t})\n\tm.queryErrorsTotal = promauto.With(r).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_silences_query_errors_total\",\n\t\tHelp: \"How many silence received queries did not succeed.\",\n\t})\n\tm.queryDuration = promauto.With(r).NewHistogram(prometheus.HistogramOpts{\n\t\tName:                            \"alertmanager_silences_query_duration_seconds\",\n\t\tHelp:                            \"Duration of silence query evaluation.\",\n\t\tBuckets:                         prometheus.DefBuckets,\n\t\tNativeHistogramBucketFactor:     1.1,\n\t\tNativeHistogramMaxBucketNumber:  100,\n\t\tNativeHistogramMinResetDuration: 1 * time.Hour,\n\t})\n\tm.queryScannedTotal = promauto.With(r).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_silences_query_silences_scanned_total\",\n\t\tHelp: \"How many silences were scanned during query evaluation.\",\n\t})\n\tm.querySkippedTotal = promauto.With(r).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_silences_query_silences_skipped_total\",\n\t\tHelp: \"How many silences were skipped during query evaluation using the version index.\",\n\t})\n\tm.propagatedMessagesTotal = promauto.With(r).NewCounter(prometheus.CounterOpts{\n\t\tName: \"alertmanager_silences_gossip_messages_propagated_total\",\n\t\tHelp: \"Number of received gossip messages that have been further gossiped.\",\n\t})\n\tif s != nil {\n\t\tm.silencesActive = newSilenceMetricByState(r, s, SilenceStateActive)\n\t\tm.silencesPending = newSilenceMetricByState(r, s, SilenceStatePending)\n\t\tm.silencesExpired = newSilenceMetricByState(r, s, SilenceStateExpired)\n\t\tm.stateSize = promauto.With(r).NewGauge(prometheus.GaugeOpts{\n\t\t\tName: \"alertmanager_silences_state_size\",\n\t\t\tHelp: \"The number of silences in the state map.\",\n\t\t})\n\t\tm.matcherIndexSize = promauto.With(r).NewGauge(prometheus.GaugeOpts{\n\t\t\tName: \"alertmanager_silences_matcher_index_size\",\n\t\t\tHelp: \"The number of entries in the matcher cache index.\",\n\t\t})\n\t\tm.versionIndexSize = promauto.With(r).NewGauge(prometheus.GaugeOpts{\n\t\t\tName: \"alertmanager_silences_version_index_size\",\n\t\t\tHelp: \"The number of entries in the version index.\",\n\t\t})\n\t}\n\n\treturn m\n}\n\n// Options exposes configuration options for creating a new Silences object.\n// Its zero value is a safe default.\ntype Options struct {\n\t// A snapshot file or reader from which the initial state is loaded.\n\t// None or only one of them must be set.\n\tSnapshotFile   string\n\tSnapshotReader io.Reader\n\n\t// Retention time for newly created Silences. Silences may be\n\t// garbage collected after the given duration after they ended.\n\tRetention time.Duration\n\tLimits    Limits\n\n\t// A logger used by background processing.\n\tLogger  *slog.Logger\n\tMetrics prometheus.Registerer\n}\n\nfunc (o *Options) validate() error {\n\tif o.SnapshotFile != \"\" && o.SnapshotReader != nil {\n\t\treturn errors.New(\"only one of SnapshotFile and SnapshotReader must be set\")\n\t}\n\treturn nil\n}\n\n// New returns a new Silences object with the given configuration.\nfunc New(o Options) (*Silences, error) {\n\tif err := o.validate(); err != nil {\n\t\treturn nil, err\n\t}\n\n\ts := &Silences{\n\t\tclock:     quartz.NewReal(),\n\t\tmi:        make(matcherIndex, 512),\n\t\tvi:        make(versionIndex, 0, 512),\n\t\tlogger:    promslog.NewNopLogger(),\n\t\tretention: o.Retention,\n\t\tlimits:    o.Limits,\n\t\tbroadcast: func([]byte) {},\n\t\tst:        state{},\n\t}\n\tif o.Metrics == nil {\n\t\treturn nil, errors.New(\"Options.Metrics is nil\")\n\t}\n\ts.metrics = newMetrics(o.Metrics, s)\n\n\tif o.Logger != nil {\n\t\ts.logger = o.Logger\n\t}\n\n\tif o.SnapshotFile != \"\" {\n\t\tif r, err := os.Open(o.SnapshotFile); err != nil {\n\t\t\tif !os.IsNotExist(err) {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\ts.logger.Debug(\"silences snapshot file doesn't exist\", \"err\", err)\n\t\t} else {\n\t\t\to.SnapshotReader = r\n\t\t\tdefer r.Close()\n\t\t}\n\t}\n\n\tif o.SnapshotReader != nil {\n\t\tif err := s.loadSnapshot(o.SnapshotReader); err != nil {\n\t\t\treturn s, err\n\t\t}\n\t}\n\treturn s, nil\n}\n\nfunc (s *Silences) nowUTC() time.Time {\n\treturn s.clock.Now().UTC()\n}\n\n// updateSizeMetrics updates the size metrics for state, matcher index, and version index.\n// Must be called while holding s.mtx.\nfunc (s *Silences) updateSizeMetrics() {\n\tif s.metrics != nil && s.metrics.stateSize != nil {\n\t\ts.metrics.stateSize.Set(float64(len(s.st)))\n\t\ts.metrics.matcherIndexSize.Set(float64(len(s.mi)))\n\t\ts.metrics.versionIndexSize.Set(float64(len(s.vi)))\n\t}\n}\n\n// Maintenance garbage collects the silence state at the given interval. If the snapshot\n// file is set, a snapshot is written to it afterwards.\n// Terminates on receiving from stopc.\n// If not nil, the last argument is an override for what to do as part of the maintenance - for advanced usage.\nfunc (s *Silences) Maintenance(interval time.Duration, snapf string, stopc <-chan struct{}, override MaintenanceFunc) {\n\tif interval == 0 || stopc == nil {\n\t\ts.logger.Error(\"interval or stop signal are missing - not running maintenance\")\n\t\treturn\n\t}\n\tt := s.clock.NewTicker(interval)\n\tdefer t.Stop()\n\n\tvar doMaintenance MaintenanceFunc\n\tdoMaintenance = func() (int64, error) {\n\t\tvar size int64\n\n\t\tif _, err := s.GC(); err != nil {\n\t\t\treturn size, err\n\t\t}\n\t\tif snapf == \"\" {\n\t\t\treturn size, nil\n\t\t}\n\t\tf, err := openReplace(snapf)\n\t\tif err != nil {\n\t\t\treturn size, err\n\t\t}\n\t\tif size, err = s.Snapshot(f); err != nil {\n\t\t\tf.Close()\n\t\t\treturn size, err\n\t\t}\n\t\treturn size, f.Close()\n\t}\n\n\tif override != nil {\n\t\tdoMaintenance = override\n\t}\n\n\trunMaintenance := func(do MaintenanceFunc) error {\n\t\ts.metrics.maintenanceTotal.Inc()\n\t\ts.logger.Debug(\"Running maintenance\")\n\t\tstart := s.nowUTC()\n\t\tsize, err := do()\n\t\ts.metrics.snapshotSize.Set(float64(size))\n\t\tif err != nil {\n\t\t\ts.metrics.maintenanceErrorsTotal.Inc()\n\t\t\treturn err\n\t\t}\n\t\ts.logger.Debug(\"Maintenance done\", \"duration\", s.clock.Since(start), \"size\", size)\n\t\treturn nil\n\t}\n\nLoop:\n\tfor {\n\t\tselect {\n\t\tcase <-stopc:\n\t\t\tbreak Loop\n\t\tcase <-t.C:\n\t\t\tif err := runMaintenance(doMaintenance); err != nil {\n\t\t\t\ts.logger.Error(\"Running maintenance failed\", \"err\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\t// No need for final maintenance if we don't want to snapshot.\n\tif snapf == \"\" {\n\t\treturn\n\t}\n\tif err := runMaintenance(doMaintenance); err != nil {\n\t\ts.logger.Error(\"Creating shutdown snapshot failed\", \"err\", err)\n\t}\n}\n\n// GC runs a garbage collection that removes silences that have ended longer\n// than the configured retention time ago.\nfunc (s *Silences) GC() (int, error) {\n\tstart := time.Now()\n\tdefer func() { s.metrics.gcDuration.Observe(time.Since(start).Seconds()) }()\n\n\tnow := s.nowUTC()\n\tvar n int\n\tvar errs error\n\n\ts.mtx.Lock()\n\tdefer s.mtx.Unlock()\n\n\t// During GC we will delete expired silences from the state map and the indices.\n\t// If between the last GC's deletion, and including any silences that were added\n\t// until now, we have more than 50% spare capacity, we want to reallocate to a smaller\n\t// slice, while still leaving some growth buffer.\n\tneedsRealloc := cap(s.vi) > 1024 && len(s.vi) < cap(s.vi)/2\n\n\tvar targetVi versionIndex\n\tif needsRealloc {\n\t\t// Allocate new slice with growth buffer.\n\t\tnewCap := max(len(s.vi)*5/4, 1024)\n\t\ttargetVi = make(versionIndex, 0, newCap)\n\t} else {\n\t\ttargetVi = s.vi[:0]\n\t}\n\n\t// Iterate state map directly (fast - no extra lookups).\n\tfor _, sv := range s.vi {\n\t\tsil, ok := s.st[sv.id]\n\t\texpire := false\n\t\tif !ok {\n\t\t\t// Silence in version index but not in state - remove from version index and count error\n\t\t\ts.metrics.gcErrorsTotal.Inc()\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"silence %s in version index missing from state\", sv.id))\n\t\t\t// not adding to targetVi effectively removes it\n\t\t\tcontinue\n\t\t}\n\t\tif sil.ExpiresAt == nil || sil.ExpiresAt.AsTime().IsZero() {\n\t\t\t// Invalid expiration timestamp - remove silence and count error\n\t\t\ts.metrics.gcErrorsTotal.Inc()\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"silence %s has zero expiration timestamp\", sil.Silence.Id))\n\t\t\texpire = true\n\t\t}\n\t\tif expire || !sil.ExpiresAt.AsTime().After(now) {\n\t\t\tdelete(s.st, sil.Silence.Id)\n\t\t\tdelete(s.mi, sil.Silence.Id)\n\t\t\tn++\n\t\t} else {\n\t\t\ttargetVi = append(targetVi, sv)\n\t\t}\n\t}\n\n\tif !needsRealloc {\n\t\t// If we didn't reallocate, clear tail to prevent string pointer leaks\n\t\tclear(s.vi[len(targetVi):])\n\t}\n\ts.vi = targetVi\n\ts.updateSizeMetrics()\n\n\treturn n, errs\n}\n\nfunc validateMatcher(m *pb.Matcher) error {\n\tif !compat.IsValidLabelName(model.LabelName(m.Name)) {\n\t\treturn fmt.Errorf(\"invalid label name %q\", m.Name)\n\t}\n\tswitch m.Type {\n\tcase pb.Matcher_EQUAL, pb.Matcher_NOT_EQUAL:\n\t\tif !model.LabelValue(m.Pattern).IsValid() {\n\t\t\treturn fmt.Errorf(\"invalid label value %q\", m.Pattern)\n\t\t}\n\tcase pb.Matcher_REGEXP, pb.Matcher_NOT_REGEXP:\n\t\tif _, err := regexp.Compile(m.Pattern); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid regular expression %q: %w\", m.Pattern, err)\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"unknown matcher type %q\", m.Type)\n\t}\n\treturn nil\n}\n\nfunc matchesEmpty(m *pb.Matcher) bool {\n\tswitch m.Type {\n\tcase pb.Matcher_EQUAL:\n\t\treturn m.Pattern == \"\"\n\tcase pb.Matcher_REGEXP:\n\t\tmatched, _ := regexp.MatchString(m.Pattern, \"\")\n\t\treturn matched\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc validateSilence(s *pb.Silence) error {\n\t// Convert old-style Matchers to MatcherSets for backward compatibility\n\tpostprocessUnmarshalledSilence(s)\n\n\tif len(s.MatcherSets) == 0 {\n\t\treturn errors.New(\"at least one matcher set required\")\n\t}\n\n\tfor setIdx, ms := range s.MatcherSets {\n\t\tif len(ms.Matchers) == 0 {\n\t\t\treturn fmt.Errorf(\"matcher set %d is empty\", setIdx)\n\t\t}\n\t\tallMatchEmpty := true\n\n\t\tfor i, m := range ms.Matchers {\n\t\t\tif err := validateMatcher(m); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid label matcher %d in set %d: %w\", i, setIdx, err)\n\t\t\t}\n\t\t\tallMatchEmpty = allMatchEmpty && matchesEmpty(m)\n\t\t}\n\t\tif allMatchEmpty {\n\t\t\treturn fmt.Errorf(\"matcher set %d: at least one matcher must not match the empty string\", setIdx)\n\t\t}\n\t}\n\n\tif s.StartsAt == nil || s.StartsAt.AsTime().IsZero() {\n\t\treturn errors.New(\"invalid zero start timestamp\")\n\t}\n\tif s.EndsAt == nil || s.EndsAt.AsTime().IsZero() {\n\t\treturn errors.New(\"invalid zero end timestamp\")\n\t}\n\tif s.EndsAt.AsTime().Before(s.StartsAt.AsTime()) {\n\t\treturn errors.New(\"end time must not be before start time\")\n\t}\n\treturn nil\n}\n\n// cloneSilence returns a copy of a silence.\nfunc cloneSilence(sil *pb.Silence) *pb.Silence {\n\treturn proto.Clone(sil).(*pb.Silence)\n}\n\nfunc (s *Silences) checkSizeLimits(msil *pb.MeshSilence) error {\n\tif s.limits.MaxSilenceSizeBytes != nil {\n\t\tn := proto.Size(msil)\n\t\tif m := s.limits.MaxSilenceSizeBytes(); m > 0 && n > m {\n\t\t\treturn fmt.Errorf(\"silence exceeded maximum size: %d bytes (limit: %d bytes)\", n, m)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *Silences) indexSilence(sil *pb.Silence) {\n\ts.version++\n\ts.vi.add(s.version, sil.Id)\n\t_, err := s.mi.add(sil)\n\tif err != nil {\n\t\ts.metrics.matcherCompileIndexSilenceErrorsTotal.Inc()\n\t\ts.logger.Error(\"Failed to compile silence matchers\", \"silence_id\", sil.Id, \"err\", err)\n\t}\n}\n\nfunc (s *Silences) getSilence(id string) (*pb.Silence, bool) {\n\tmsil, ok := s.st[id]\n\tif !ok {\n\t\treturn nil, false\n\t}\n\treturn msil.Silence, true\n}\n\nfunc (s *Silences) toMeshSilence(sil *pb.Silence) *pb.MeshSilence {\n\treturn &pb.MeshSilence{\n\t\tSilence:   sil,\n\t\tExpiresAt: timestamppb.New(sil.EndsAt.AsTime().Add(s.retention)),\n\t}\n}\n\nfunc (s *Silences) setSilence(msil *pb.MeshSilence, now time.Time) error {\n\tb, err := marshalMeshSilence(msil)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, added := s.st.merge(msil, now)\n\tif added {\n\t\ts.indexSilence(msil.Silence)\n\t\ts.updateSizeMetrics()\n\t}\n\ts.broadcast(b)\n\treturn nil\n}\n\n// Set the specified silence. If a silence with the ID already exists and the modification\n// modifies history, the old silence gets expired and a new one is created.\nfunc (s *Silences) Set(ctx context.Context, sil *pb.Silence) error {\n\t_, span := tracer.Start(ctx, \"silences.Set\")\n\tdefer span.End()\n\n\tnow := s.nowUTC()\n\tif sil.StartsAt == nil || sil.StartsAt.AsTime().IsZero() {\n\t\tsil.StartsAt = timestamppb.New(now)\n\t}\n\n\tif err := validateSilence(sil); err != nil {\n\t\treturn fmt.Errorf(\"invalid silence: %w\", err)\n\t}\n\n\ts.mtx.Lock()\n\tdefer s.mtx.Unlock()\n\n\tprev, ok := s.getSilence(sil.Id)\n\tif sil.Id != \"\" && !ok {\n\t\treturn ErrNotFound\n\t}\n\n\tif ok && canUpdate(prev, sil, now) {\n\t\tsil.UpdatedAt = timestamppb.New(now)\n\t\tmsil := s.toMeshSilence(sil)\n\t\tif err := s.checkSizeLimits(msil); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn s.setSilence(msil, now)\n\t}\n\n\t// If we got here it's either a new silence or a replacing one (which would\n\t// also create a new silence) so we need to make sure we have capacity for\n\t// the new silence.\n\tif s.limits.MaxSilences != nil {\n\t\tif m := s.limits.MaxSilences(); m > 0 && len(s.st)+1 > m {\n\t\t\treturn fmt.Errorf(\"exceeded maximum number of silences: %d (limit: %d)\", len(s.st), m)\n\t\t}\n\t}\n\n\tuid, err := uuid.NewRandom()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"generate uuid: %w\", err)\n\t}\n\tsil.Id = uid.String()\n\n\tif sil.StartsAt.AsTime().Before(now) {\n\t\tsil.StartsAt = timestamppb.New(now)\n\t}\n\tsil.UpdatedAt = timestamppb.New(now)\n\n\tmsil := s.toMeshSilence(sil)\n\tif err := s.checkSizeLimits(msil); err != nil {\n\t\treturn err\n\t}\n\n\tif ok && getState(prev, s.nowUTC()) != SilenceStateExpired {\n\t\t// We cannot update the silence, expire the old one to leave a history of\n\t\t// the silence before modification.\n\t\tif err := s.expire(prev.Id); err != nil {\n\t\t\treturn fmt.Errorf(\"expire previous silence: %w\", err)\n\t\t}\n\t}\n\n\treturn s.setSilence(msil, now)\n}\n\n// canUpdate returns true if silence a can be updated to b without\n// affecting the historic view of silencing.\nfunc canUpdate(a, b *pb.Silence, now time.Time) bool {\n\tif !slices.EqualFunc(a.MatcherSets, b.MatcherSets, func(x, y *pb.MatcherSet) bool {\n\t\treturn proto.Equal(x, y)\n\t}) {\n\t\treturn false\n\t}\n\t// Allowed timestamp modifications depend on the current time.\n\tswitch st := getState(a, now); st {\n\tcase SilenceStateActive:\n\t\tif a.StartsAt.AsTime().Unix() != b.StartsAt.AsTime().Unix() {\n\t\t\treturn false\n\t\t}\n\t\tif b.EndsAt.AsTime().Before(now) {\n\t\t\treturn false\n\t\t}\n\tcase SilenceStatePending:\n\t\tif b.StartsAt.AsTime().Before(now) {\n\t\t\treturn false\n\t\t}\n\tcase SilenceStateExpired:\n\t\treturn false\n\tdefault:\n\t\tpanic(\"unknown silence state\")\n\t}\n\treturn true\n}\n\n// Expire the silence with the given ID immediately.\nfunc (s *Silences) Expire(ctx context.Context, id string) error {\n\ts.mtx.Lock()\n\tdefer s.mtx.Unlock()\n\n\t_, span := tracer.Start(ctx, \"silences.Expire\", trace.WithAttributes(\n\t\tattribute.String(\"alerting.silence.id\", id),\n\t))\n\tdefer span.End()\n\n\treturn s.expire(id)\n}\n\n// Expire the silence with the given ID immediately.\n// It is idempotent, nil is returned if the silence already expired before it is GC'd.\n// If the silence is not found an error is returned.\nfunc (s *Silences) expire(id string) error {\n\tsil, ok := s.getSilence(id)\n\tif !ok {\n\t\treturn ErrNotFound\n\t}\n\tsil = cloneSilence(sil)\n\tnow := s.nowUTC()\n\n\tswitch getState(sil, now) {\n\tcase SilenceStateExpired:\n\t\treturn nil\n\tcase SilenceStateActive:\n\t\tsil.EndsAt = timestamppb.New(now)\n\tcase SilenceStatePending:\n\t\t// Set both to now to make Silence move to \"expired\" state\n\t\tsil.StartsAt = timestamppb.New(now)\n\t\tsil.EndsAt = timestamppb.New(now)\n\t}\n\tsil.UpdatedAt = timestamppb.New(now)\n\treturn s.setSilence(s.toMeshSilence(sil), now)\n}\n\n// QueryParam expresses parameters along which silences are queried.\ntype QueryParam func(*query) error\n\ntype query struct {\n\tids     []string\n\tsince   *int\n\tfilters []silenceFilter\n}\n\n// silenceFilter is a function that returns true if a silence\n// should be dropped from a result set for a given time.\ntype silenceFilter func(*pb.Silence, *Silences, time.Time) (bool, error)\n\n// QIDs configures a query to select the given silence IDs.\nfunc QIDs(ids ...string) QueryParam {\n\treturn func(q *query) error {\n\t\tif len(ids) == 0 {\n\t\t\treturn errors.New(\"QIDs filter must have at least one id\")\n\t\t}\n\t\tif q.since != nil {\n\t\t\treturn fmt.Errorf(\"QSince cannot be used with QIDs\")\n\t\t}\n\t\tq.ids = append(q.ids, ids...)\n\t\treturn nil\n\t}\n}\n\n// QSince filters silences to those created after the provided version. This can be used to\n// scan all silences which have been added after the provided version to incrementally update\n// a cache.\nfunc QSince(version int) QueryParam {\n\treturn func(q *query) error {\n\t\tif len(q.ids) != 0 {\n\t\t\treturn fmt.Errorf(\"QSince cannot be used with QIDs\")\n\t\t}\n\t\tq.since = &version\n\t\treturn nil\n\t}\n}\n\n// QMatches returns silences that match the given label set.\nfunc QMatches(set model.LabelSet) QueryParam {\n\treturn func(q *query) error {\n\t\tf := func(sil *pb.Silence, s *Silences, _ time.Time) (bool, error) {\n\t\t\tm, err := s.mi.get(sil)\n\t\t\tif err != nil {\n\t\t\t\treturn true, err\n\t\t\t}\n\t\t\treturn m.Matches(set), nil\n\t\t}\n\t\tq.filters = append(q.filters, f)\n\t\treturn nil\n\t}\n}\n\n// getState returns a silence's SilenceState at the given timestamp.\nfunc getState(sil *pb.Silence, ts time.Time) SilenceState {\n\tif ts.Before(sil.StartsAt.AsTime()) {\n\t\treturn SilenceStatePending\n\t}\n\tif ts.After(sil.EndsAt.AsTime()) {\n\t\treturn SilenceStateExpired\n\t}\n\treturn SilenceStateActive\n}\n\n// QState filters queried silences by the given states.\nfunc QState(states ...SilenceState) QueryParam {\n\treturn func(q *query) error {\n\t\tf := func(sil *pb.Silence, _ *Silences, now time.Time) (bool, error) {\n\t\t\ts := getState(sil, now)\n\n\t\t\tif slices.Contains(states, s) {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t\treturn false, nil\n\t\t}\n\t\tq.filters = append(q.filters, f)\n\t\treturn nil\n\t}\n}\n\n// QueryOne queries with the given parameters and returns the first result.\n// Returns ErrNotFound if the query result is empty.\nfunc (s *Silences) QueryOne(ctx context.Context, params ...QueryParam) (*pb.Silence, error) {\n\t_, span := tracer.Start(ctx, \"silence.Silences.QueryOne\",\n\t\ttrace.WithSpanKind(trace.SpanKindInternal),\n\t)\n\tdefer span.End()\n\tres, _, err := s.Query(ctx, params...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(res) == 0 {\n\t\treturn nil, ErrNotFound\n\t}\n\treturn res[0], nil\n}\n\n// Query for silences based on the given query parameters. It returns the\n// resulting silences and the state version the result is based on.\nfunc (s *Silences) Query(ctx context.Context, params ...QueryParam) ([]*pb.Silence, int, error) {\n\t_, span := tracer.Start(ctx, \"silence.Silences.Query\",\n\t\ttrace.WithSpanKind(trace.SpanKindInternal),\n\t)\n\tdefer span.End()\n\ts.metrics.queriesTotal.Inc()\n\tdefer prometheus.NewTimer(s.metrics.queryDuration).ObserveDuration()\n\n\tq := &query{}\n\tfor _, p := range params {\n\t\tif err := p(q); err != nil {\n\t\t\ts.metrics.queryErrorsTotal.Inc()\n\t\t\treturn nil, s.Version(), err\n\t\t}\n\t}\n\tsils, version, err := s.query(q, s.nowUTC())\n\tif err != nil {\n\t\ts.metrics.queryErrorsTotal.Inc()\n\t}\n\treturn sils, version, err\n}\n\n// Version of the silence state.\nfunc (s *Silences) Version() int {\n\ts.mtx.RLock()\n\tdefer s.mtx.RUnlock()\n\treturn s.version\n}\n\n// CountState counts silences by state.\nfunc (s *Silences) CountState(ctx context.Context, states ...SilenceState) (int, error) {\n\t_, span := tracer.Start(ctx, \"silence.Silences.CountState\",\n\t\ttrace.WithSpanKind(trace.SpanKindInternal),\n\t)\n\tdefer span.End()\n\t// This could probably be optimized.\n\tsils, _, err := s.Query(ctx, QState(states...))\n\tif err != nil {\n\t\treturn -1, err\n\t}\n\treturn len(sils), nil\n}\n\n// query executes the given query and returns the resulting silences.\nfunc (s *Silences) query(q *query, now time.Time) ([]*pb.Silence, int, error) {\n\tvar res []*pb.Silence\n\tvar err error\n\n\tscannedCount := 0\n\tdefer func() {\n\t\ts.metrics.queryScannedTotal.Add(float64(scannedCount))\n\t}()\n\n\t// appendIfFiltersMatch appends the given silence to the result set\n\t// if it matches all filters in the query. In case of a filter error, the error is returned.\n\tappendIfFiltersMatch := func(res []*pb.Silence, sil *pb.Silence) ([]*pb.Silence, error) {\n\t\tfor _, f := range q.filters {\n\t\t\tmatches, err := f(sil, s, now)\n\t\t\t// In case of error return it immediately and don't process further filters.\n\t\t\tif err != nil {\n\t\t\t\treturn res, err\n\t\t\t}\n\t\t\t// If one filter doesn't match, return the result unchanged, immediately.\n\t\t\tif !matches {\n\t\t\t\treturn res, nil\n\t\t\t}\n\t\t}\n\t\t// All filters matched, append the silence to the result.\n\t\treturn append(res, cloneSilence(sil)), nil\n\t}\n\n\t// Preallocate result slice if we have IDs (if not this will be a no-op)\n\tres = make([]*pb.Silence, 0, len(q.ids))\n\n\t// Take a read lock on Silences: we can read but not modify the Silences struct.\n\ts.mtx.RLock()\n\tdefer s.mtx.RUnlock()\n\n\t// If we have IDs, only consider the silences with the given IDs, if they exist.\n\tif q.ids != nil {\n\t\tfor _, id := range q.ids {\n\t\t\tif sil, ok := s.st[id]; ok {\n\t\t\t\tscannedCount++\n\t\t\t\t// append the silence to the results if it satisfies the query.\n\t\t\t\tres, err = appendIfFiltersMatch(res, sil.Silence)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, s.version, err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\tstart := 0\n\t\tif q.since != nil {\n\t\t\tvar found bool\n\t\t\tstart, found = s.vi.findVersionGreaterThan(*q.since)\n\t\t\t// no new silences, nothing to do\n\t\t\tif !found {\n\t\t\t\treturn res, s.version, nil\n\t\t\t}\n\t\t\t// Track how many silences we skipped using the version index.\n\t\t\ts.metrics.querySkippedTotal.Add(float64(start))\n\t\t}\n\t\t// Preallocate result slice with a reasonable capacity. If we are\n\t\t// scanning less than 64 silences, we can allocate that many,\n\t\t// otherwise we just allocate 64 and let it grow as needed.\n\t\tres = make([]*pb.Silence, 0, min(64, len(s.vi)-start))\n\t\tfor _, sv := range s.vi[start:] {\n\t\t\tscannedCount++\n\t\t\tsil := s.st[sv.id]\n\t\t\t// append the silence to the results if it satisfies the query.\n\t\t\tres, err = appendIfFiltersMatch(res, sil.Silence)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, s.version, err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn res, s.version, nil\n}\n\n// loadSnapshot loads a snapshot generated by Snapshot() into the state.\n// Any previous state is wiped.\nfunc (s *Silences) loadSnapshot(r io.Reader) error {\n\tst, err := decodeState(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmi := make(matcherIndex, len(st)) // for a map, len is ok as a size hint.\n\t// Choose new version index capacity with some growth buffer.\n\tvi := make(versionIndex, 0, max(len(st)*5/4, 1024))\n\n\tfor _, e := range st {\n\t\t// Comments list was moved to a single comment. Upgrade on loading the snapshot.\n\t\tif len(e.Silence.Comments) > 0 {\n\t\t\te.Silence.Comment = e.Silence.Comments[0].Comment\n\t\t\te.Silence.CreatedBy = e.Silence.Comments[0].Author\n\t\t\te.Silence.Comments = nil\n\t\t}\n\t\t// Add to matcher index, and only if successful, to the new state.\n\t\tif _, err := mi.add(e.Silence); err != nil {\n\t\t\ts.metrics.matcherCompileLoadSnapshotErrorsTotal.Inc()\n\t\t\ts.logger.Error(\"Failed to compile silence matchers during snapshot load\", \"silence_id\", e.Silence.Id, \"err\", err)\n\t\t} else {\n\t\t\tst[e.Silence.Id] = e\n\n\t\t\tvi.add(s.version+1, e.Silence.Id)\n\t\t}\n\t}\n\ts.mtx.Lock()\n\ts.st = st\n\ts.mi = mi\n\ts.vi = vi\n\ts.version++\n\ts.updateSizeMetrics()\n\ts.mtx.Unlock()\n\n\treturn nil\n}\n\n// Snapshot writes the full internal state into the writer and returns the number of bytes\n// written.\nfunc (s *Silences) Snapshot(w io.Writer) (int64, error) {\n\tstart := time.Now()\n\tdefer func() { s.metrics.snapshotDuration.Observe(time.Since(start).Seconds()) }()\n\n\ts.mtx.RLock()\n\tdefer s.mtx.RUnlock()\n\n\tb, err := s.st.MarshalBinary()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn io.Copy(w, bytes.NewReader(b))\n}\n\n// MarshalBinary serializes all silences.\nfunc (s *Silences) MarshalBinary() ([]byte, error) {\n\ts.mtx.RLock()\n\tdefer s.mtx.RUnlock()\n\n\treturn s.st.MarshalBinary()\n}\n\n// Merge merges silence state received from the cluster with the local state.\nfunc (s *Silences) Merge(b []byte) error {\n\tst, err := decodeState(bytes.NewReader(b))\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.mtx.Lock()\n\tdefer s.mtx.Unlock()\n\n\tnow := s.nowUTC()\n\n\tfor _, e := range st {\n\t\tmerged, added := s.st.merge(e, now)\n\t\tif merged {\n\t\t\tif added {\n\t\t\t\ts.indexSilence(e.Silence)\n\t\t\t}\n\t\t\tif !cluster.OversizedMessage(b) {\n\t\t\t\t// If this is the first we've seen the message and it's\n\t\t\t\t// not oversized, gossip it to other nodes. We don't\n\t\t\t\t// propagate oversized messages because they're sent to\n\t\t\t\t// all nodes already.\n\t\t\t\ts.broadcast(b)\n\t\t\t\ts.metrics.propagatedMessagesTotal.Inc()\n\t\t\t\ts.logger.Debug(\"Gossiping new silence\", \"silence\", e)\n\t\t\t}\n\t\t}\n\t}\n\ts.updateSizeMetrics()\n\treturn nil\n}\n\n// SetBroadcast sets the provided function as the one creating data to be\n// broadcast.\nfunc (s *Silences) SetBroadcast(f func([]byte)) {\n\ts.mtx.Lock()\n\ts.broadcast = f\n\ts.mtx.Unlock()\n}\n\ntype state map[string]*pb.MeshSilence\n\n// merge returns two bools: the first is true when merge caused a state change. The second\n// is true if that state change added a new silence. In other words, the second return is\n// true whenever a silence with a new ID has been added to the state as a result of merge.\nfunc (s state) merge(e *pb.MeshSilence, now time.Time) (bool, bool) {\n\tid := e.Silence.Id\n\tif e.ExpiresAt.AsTime().Before(now) {\n\t\treturn false, false\n\t}\n\t// Comments list was moved to a single comment. Apply upgrade\n\t// on silences received from peers.\n\tif len(e.Silence.Comments) > 0 {\n\t\te.Silence.Comment = e.Silence.Comments[0].Comment\n\t\te.Silence.CreatedBy = e.Silence.Comments[0].Author\n\t\te.Silence.Comments = nil\n\t}\n\n\tprev, ok := s[id]\n\tif !ok || prev.Silence.UpdatedAt.AsTime().Before(e.Silence.UpdatedAt.AsTime()) {\n\t\ts[id] = e\n\t\treturn true, !ok\n\t}\n\treturn false, false\n}\n\nfunc (s state) MarshalBinary() ([]byte, error) {\n\tvar buf bytes.Buffer\n\n\tfor _, e := range s {\n\t\tif _, err := protodelim.MarshalTo(&buf, e); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn buf.Bytes(), nil\n}\n\nfunc decodeState(r io.Reader) (state, error) {\n\tst := state{}\n\tbr := bufio.NewReader(r)\n\tfor {\n\t\tvar s pb.MeshSilence\n\t\terr := protodelim.UnmarshalFrom(br, &s)\n\t\tif err == nil {\n\t\t\tif s.Silence == nil {\n\t\t\t\treturn nil, ErrInvalidState\n\t\t\t}\n\t\t\tpostprocessUnmarshalledSilence(s.Silence)\n\t\t\tst[s.Silence.Id] = &s\n\t\t\tcontinue\n\t\t}\n\t\tif errors.Is(err, io.EOF) {\n\t\t\tbreak\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn st, nil\n}\n\n// prepareSilenceForMarshalling prepares a silence for marshalling by copying\n// the first matcher set to the matchers field for backward compatibility with\n// older alertmanager versions.\nfunc prepareSilenceForMarshalling(sil *pb.Silence) {\n\tif len(sil.MatcherSets) > 0 {\n\t\tsil.Matchers = sil.MatcherSets[0].Matchers\n\t}\n}\n\n// postprocessUnmarshalledSilence processes a silence after unmarshalling by\n// moving matchers to MatcherSets if needed for backward compatibility.\nfunc postprocessUnmarshalledSilence(sil *pb.Silence) {\n\t// maintain compatibility with older versions of Alertmanager\n\t// if the silence was serialized with the old format we need to move the matchers from sil.Matchers\n\t// to sil.MatcherSets\n\tif len(sil.MatcherSets) == 0 && len(sil.Matchers) > 0 {\n\t\tsil.MatcherSets = append(sil.MatcherSets, &pb.MatcherSet{Matchers: sil.Matchers})\n\t}\n\tsil.Matchers = nil\n}\n\nfunc marshalMeshSilence(e *pb.MeshSilence) ([]byte, error) {\n\t// Make a copy to avoid modifying the original silence\n\tmeshCopy := &pb.MeshSilence{\n\t\tSilence:   cloneSilence(e.Silence),\n\t\tExpiresAt: e.ExpiresAt,\n\t}\n\tprepareSilenceForMarshalling(meshCopy.Silence)\n\tvar buf bytes.Buffer\n\tif _, err := protodelim.MarshalTo(&buf, meshCopy); err != nil {\n\t\treturn nil, err\n\t}\n\treturn buf.Bytes(), nil\n}\n\n// replaceFile wraps a file that is moved to another filename on closing.\ntype replaceFile struct {\n\t*os.File\n\tfilename string\n}\n\nfunc (f *replaceFile) Close() error {\n\tif err := f.Sync(); err != nil {\n\t\treturn err\n\t}\n\tif err := f.File.Close(); err != nil {\n\t\treturn err\n\t}\n\treturn os.Rename(f.Name(), f.filename)\n}\n\n// openReplace opens a new temporary file that is moved to filename on closing.\nfunc openReplace(filename string) (*replaceFile, error) {\n\ttmpFilename := fmt.Sprintf(\"%s.%x\", filename, uint64(rand.Int63()))\n\n\tf, err := os.Create(tmpFilename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trf := &replaceFile{\n\t\tFile:     f,\n\t\tfilename: filename,\n\t}\n\treturn rf, nil\n}\n"
  },
  {
    "path": "silence/silence_bench_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage silence\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strconv\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/coder/quartz\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/prometheus/alertmanager/silence/silencepb\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// BenchmarkMutes benchmarks the Mutes method for the Muter interface for\n// different numbers of silences with varying match ratios.\nfunc BenchmarkMutes(b *testing.B) {\n\tb.Run(\"0 total, 0 matching\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, 0, 0)\n\t})\n\tb.Run(\"1 total, 1 matching\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, 1, 1)\n\t})\n\tb.Run(\"100 total, 10 matching\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, 100, 10)\n\t})\n\tb.Run(\"1000 total, 1 matching\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, 1000, 1)\n\t})\n\tb.Run(\"1000 total, 10 matching\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, 1000, 10)\n\t})\n\tb.Run(\"1000 total, 100 matching\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, 1000, 100)\n\t})\n\tb.Run(\"10000 total, 0 matching\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, 10000, 10)\n\t})\n\tb.Run(\"10000 total, 10 matching\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, 10000, 10)\n\t})\n\tb.Run(\"10000 total, 1000 matching\", func(b *testing.B) {\n\t\tbenchmarkMutes(b, 10000, 1000)\n\t})\n}\n\nfunc benchmarkMutes(b *testing.B, totalSilences, matchingSilences int) {\n\trequire.LessOrEqual(b, matchingSilences, totalSilences)\n\n\tsilences, err := New(Options{Metrics: prometheus.NewRegistry()})\n\trequire.NoError(b, err)\n\n\tclock := quartz.NewMock(b).WithLogger(quartz.NoOpLogger)\n\tsilences.clock = clock\n\tnow := clock.Now()\n\n\t// Calculate interval to intersperse matching silences\n\tvar interval int\n\tif matchingSilences > 0 {\n\t\tinterval = totalSilences / matchingSilences\n\t}\n\n\t// Create silences with matching ones interspersed throughout\n\tmatchingCreated := 0\n\tfor i := range totalSilences {\n\t\tvar s *silencepb.Silence\n\t\t// Create matching silences at calculated intervals, but make sure there are always enough\n\t\tif matchingCreated < matchingSilences && (i%interval == 0 || i == totalSilences-matchingSilences+matchingCreated) {\n\t\t\t// Create a matching silence\n\t\t\ts = &silencepb.Silence{\n\t\t\t\tMatchers: []*silencepb.Matcher{{\n\t\t\t\t\tType:    silencepb.Matcher_EQUAL,\n\t\t\t\t\tName:    \"foo\",\n\t\t\t\t\tPattern: \"bar\",\n\t\t\t\t}},\n\t\t\t\tStartsAt: timestamppb.New(now),\n\t\t\t\tEndsAt:   timestamppb.New(now.Add(time.Minute)),\n\t\t\t}\n\t\t\tmatchingCreated++\n\t\t} else {\n\t\t\t// Create a non-matching silence\n\t\t\ts = &silencepb.Silence{\n\t\t\t\tMatchers: []*silencepb.Matcher{{\n\t\t\t\t\tType:    silencepb.Matcher_EQUAL,\n\t\t\t\t\tName:    \"job\",\n\t\t\t\t\tPattern: \"job\" + strconv.Itoa(i),\n\t\t\t\t}},\n\t\t\t\tStartsAt: timestamppb.New(now),\n\t\t\t\tEndsAt:   timestamppb.New(now.Add(time.Minute)),\n\t\t\t}\n\t\t}\n\t\trequire.NoError(b, silences.Set(b.Context(), s))\n\t}\n\n\tm := types.NewMarker(prometheus.NewRegistry())\n\ts := NewSilencer(silences, m, promslog.NewNopLogger())\n\n\tfor b.Loop() {\n\t\ts.Mutes(context.Background(), model.LabelSet{\"foo\": \"bar\"})\n\t}\n\tb.StopTimer()\n\n\t// The alert should be marked as silenced for each matching silence.\n\tactiveIDs, silenced := m.Silenced(model.LabelSet{\"foo\": \"bar\"}.Fingerprint())\n\trequire.True(b, silenced || matchingSilences == 0)\n\trequire.Len(b, activeIDs, matchingSilences)\n}\n\n// BenchmarkMutesIncremental tests the incremental query optimization when a small\n// number of silences are added to a large existing set. This measures the real-world\n// scenario that the QSince optimization is designed for.\nfunc BenchmarkMutesIncremental(b *testing.B) {\n\tcases := []struct {\n\t\tname     string\n\t\tbaseSize int\n\t}{\n\t\t{\"1000 base silences\", 1000},\n\t\t{\"3000 base silences\", 3000},\n\t\t{\"7000 base silences\", 7000},\n\t\t{\"10000 base silences\", 10000},\n\t}\n\n\tfor _, tc := range cases {\n\t\tb.Run(tc.name, func(b *testing.B) {\n\t\t\tsilences, err := New(Options{Metrics: prometheus.NewRegistry()})\n\t\t\trequire.NoError(b, err)\n\n\t\t\tclock := quartz.NewMock(b).WithLogger(quartz.NoOpLogger)\n\t\t\tsilences.clock = clock\n\t\t\tnow := clock.Now()\n\n\t\t\t// Create base set of silences - most don't match, some do\n\t\t\t// This simulates a realistic production scenario\n\t\t\t// Intersperse matching silences throughout the base set\n\t\t\tfor i := 0; i < tc.baseSize; i++ {\n\t\t\t\tvar s *silencepb.Silence\n\t\t\t\tif i%2000 == 0 && i > 0 {\n\t\t\t\t\t// Sprinkle 1 silence matching every 2000\n\t\t\t\t\ts = &silencepb.Silence{\n\t\t\t\t\t\tMatchers: []*silencepb.Matcher{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tType:    silencepb.Matcher_EQUAL,\n\t\t\t\t\t\t\t\tName:    \"service\",\n\t\t\t\t\t\t\t\tPattern: \"test\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tType:    silencepb.Matcher_EQUAL,\n\t\t\t\t\t\t\t\tName:    \"instance\",\n\t\t\t\t\t\t\t\tPattern: \"instance1\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tStartsAt: timestamppb.New(now),\n\t\t\t\t\t\tEndsAt:   timestamppb.New(now.Add(time.Hour)),\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\ts = &silencepb.Silence{\n\t\t\t\t\t\tMatchers: []*silencepb.Matcher{{\n\t\t\t\t\t\t\tType:    silencepb.Matcher_EQUAL,\n\t\t\t\t\t\t\tName:    \"job\",\n\t\t\t\t\t\t\tPattern: \"job\" + strconv.Itoa(i),\n\t\t\t\t\t\t}},\n\t\t\t\t\t\tStartsAt: timestamppb.New(now),\n\t\t\t\t\t\tEndsAt:   timestamppb.New(now.Add(time.Hour)),\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\trequire.NoError(b, silences.Set(b.Context(), s))\n\t\t\t}\n\n\t\t\tmarker := types.NewMarker(prometheus.NewRegistry())\n\t\t\tsilencer := NewSilencer(silences, marker, promslog.NewNopLogger())\n\n\t\t\t// Warm up: Establish cache state (cachedEntry.version = current version)\n\t\t\t// This simulates a system that has been running for a while\n\t\t\tlset := model.LabelSet{\"service\": \"test\", \"instance\": \"instance1\"}\n\t\t\tsilencer.Mutes(context.Background(), lset)\n\n\t\t\t// Benchmark: Measure Mutes() performance with incremental additions\n\t\t\t// Every other iteration adds 1 new silence, all iterations call Mutes()\n\t\t\t// This simulates realistic traffic with a mix of incremental and cached queries\n\t\t\t// With QSince optimization, this should only scan new silences when added\n\t\t\tb.ResetTimer()\n\t\t\titeration := 0\n\t\t\tfor b.Loop() {\n\t\t\t\t// Don't measure the Set() time, only Mutes()\n\t\t\t\tb.StopTimer()\n\n\t\t\t\t// Add one new silence every other iteration to simulate realistic traffic\n\t\t\t\t// where Mutes() is sometimes called without new silences\n\t\t\t\tif iteration%2 == 0 {\n\t\t\t\t\tvar s *silencepb.Silence\n\t\t\t\t\tif iteration%20 == 0 && iteration > 0 {\n\t\t\t\t\t\t// Only 1 in 20 silences matches the labelset\n\t\t\t\t\t\ts = &silencepb.Silence{\n\t\t\t\t\t\t\tMatchers: []*silencepb.Matcher{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tType:    silencepb.Matcher_EQUAL,\n\t\t\t\t\t\t\t\t\tName:    \"service\",\n\t\t\t\t\t\t\t\t\tPattern: \"test\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tType:    silencepb.Matcher_EQUAL,\n\t\t\t\t\t\t\t\t\tName:    \"instance\",\n\t\t\t\t\t\t\t\t\tPattern: \"instance1\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tStartsAt: timestamppb.New(now),\n\t\t\t\t\t\t\tEndsAt:   timestamppb.New(now.Add(time.Hour)),\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Most don't match\n\t\t\t\t\t\ts = &silencepb.Silence{\n\t\t\t\t\t\t\tMatchers: []*silencepb.Matcher{{\n\t\t\t\t\t\t\t\tType:    silencepb.Matcher_EQUAL,\n\t\t\t\t\t\t\t\tName:    \"instance\",\n\t\t\t\t\t\t\t\tPattern: \"host\" + strconv.Itoa(iteration),\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t\tStartsAt: timestamppb.New(now),\n\t\t\t\t\t\t\tEndsAt:   timestamppb.New(now.Add(time.Hour)),\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\trequire.NoError(b, silences.Set(b.Context(), s))\n\t\t\t\t}\n\n\t\t\t\tb.StartTimer()\n\t\t\t\t// Now query - should use incremental path or cached paths\n\t\t\t\tsilencer.Mutes(context.Background(), lset)\n\t\t\t\titeration++\n\t\t\t}\n\t\t})\n\t}\n}\n\n// BenchmarkQuery benchmarks the Query method for the Silences struct\n// for different numbers of silences. Not all silences match the query\n// to prevent compiler and runtime optimizations from affecting the benchmarks.\nfunc BenchmarkQuery(b *testing.B) {\n\tb.Run(\"100 silences\", func(b *testing.B) {\n\t\tbenchmarkQuery(b, 100)\n\t})\n\tb.Run(\"1000 silences\", func(b *testing.B) {\n\t\tbenchmarkQuery(b, 1000)\n\t})\n\tb.Run(\"10000 silences\", func(b *testing.B) {\n\t\tbenchmarkQuery(b, 10000)\n\t})\n}\n\nfunc benchmarkQuery(b *testing.B, numSilences int) {\n\ts, err := New(Options{Metrics: prometheus.NewRegistry()})\n\trequire.NoError(b, err)\n\n\tclock := quartz.NewMock(b).WithLogger(quartz.NoOpLogger)\n\ts.clock = clock\n\tnow := clock.Now()\n\n\tlset := model.LabelSet{\"aaaa\": \"AAAA\", \"bbbb\": \"BBBB\", \"cccc\": \"CCCC\"}\n\n\t// Create silences using Set() to properly populate indices\n\tfor i := range numSilences {\n\t\tid := strconv.Itoa(i)\n\t\t// Include an offset to avoid optimizations.\n\t\tpatA := \"A{4}|\" + id\n\t\tpatB := id // Does not match.\n\t\tif i%10 == 0 {\n\t\t\t// Every 10th time, have an actually matching pattern.\n\t\t\tpatB = \"B(B|C)B.|\" + id\n\t\t}\n\n\t\tsil := &silencepb.Silence{\n\t\t\tMatchers: []*silencepb.Matcher{\n\t\t\t\t{Type: silencepb.Matcher_REGEXP, Name: \"aaaa\", Pattern: patA},\n\t\t\t\t{Type: silencepb.Matcher_REGEXP, Name: \"bbbb\", Pattern: patB},\n\t\t\t},\n\t\t\tStartsAt:  timestamppb.New(now.Add(-time.Minute)),\n\t\t\tEndsAt:    timestamppb.New(now.Add(time.Hour)),\n\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t}\n\t\trequire.NoError(b, s.Set(b.Context(), sil))\n\t}\n\n\t// Run things once to populate the matcherCache.\n\tsils, _, err := s.Query(\n\t\tb.Context(),\n\t\tQState(SilenceStateActive),\n\t\tQMatches(lset),\n\t)\n\trequire.NoError(b, err)\n\trequire.Len(b, sils, numSilences/10)\n\n\tfor b.Loop() {\n\t\tsils, _, err := s.Query(\n\t\t\tb.Context(),\n\t\t\tQState(SilenceStateActive),\n\t\t\tQMatches(lset),\n\t\t)\n\t\trequire.NoError(b, err)\n\t\trequire.Len(b, sils, numSilences/10)\n\t}\n}\n\n// BenchmarkQueryParallel benchmarks concurrent queries to demonstrate\n// the performance improvement from using read locks (RLock) instead of\n// write locks (Lock). With the pre-compiled matcher cache, multiple\n// queries can now execute in parallel.\nfunc BenchmarkQueryParallel(b *testing.B) {\n\tb.Run(\"100 silences\", func(b *testing.B) {\n\t\tbenchmarkQueryParallel(b, 100)\n\t})\n\tb.Run(\"1000 silences\", func(b *testing.B) {\n\t\tbenchmarkQueryParallel(b, 1000)\n\t})\n\tb.Run(\"10000 silences\", func(b *testing.B) {\n\t\tbenchmarkQueryParallel(b, 10000)\n\t})\n}\n\nfunc benchmarkQueryParallel(b *testing.B, numSilences int) {\n\ts, err := New(Options{Metrics: prometheus.NewRegistry()})\n\trequire.NoError(b, err)\n\n\tclock := quartz.NewMock(b).WithLogger(quartz.NoOpLogger)\n\ts.clock = clock\n\tnow := clock.Now()\n\n\tlset := model.LabelSet{\"aaaa\": \"AAAA\", \"bbbb\": \"BBBB\", \"cccc\": \"CCCC\"}\n\n\t// Create silences with pre-compiled matchers\n\tfor i := range numSilences {\n\t\tid := strconv.Itoa(i)\n\t\tpatA := \"A{4}|\" + id\n\t\tpatB := id\n\t\tif i%10 == 0 {\n\t\t\tpatB = \"B(B|C)B.|\" + id\n\t\t}\n\n\t\tsil := &silencepb.Silence{\n\t\t\tMatchers: []*silencepb.Matcher{\n\t\t\t\t{Type: silencepb.Matcher_REGEXP, Name: \"aaaa\", Pattern: patA},\n\t\t\t\t{Type: silencepb.Matcher_REGEXP, Name: \"bbbb\", Pattern: patB},\n\t\t\t},\n\t\t\tStartsAt:  timestamppb.New(now.Add(-time.Minute)),\n\t\t\tEndsAt:    timestamppb.New(now.Add(time.Hour)),\n\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t}\n\t\trequire.NoError(b, s.Set(b.Context(), sil))\n\t}\n\n\t// Verify initial query works\n\tsils, _, err := s.Query(\n\t\tb.Context(),\n\t\tQState(SilenceStateActive),\n\t\tQMatches(lset),\n\t)\n\trequire.NoError(b, err)\n\trequire.Len(b, sils, numSilences/10)\n\n\tb.ResetTimer()\n\n\t// Run queries in parallel across multiple goroutines\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tsils, _, err := s.Query(\n\t\t\t\tb.Context(),\n\t\t\t\tQState(SilenceStateActive),\n\t\t\t\tQMatches(lset),\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tb.Error(err)\n\t\t\t}\n\t\t\tif len(sils) != numSilences/10 {\n\t\t\t\tb.Errorf(\"expected %d silences, got %d\", numSilences/10, len(sils))\n\t\t\t}\n\t\t}\n\t})\n}\n\n// BenchmarkQueryWithConcurrentAdds benchmarks the behavior when queries\n// are running concurrently with silence additions. This demonstrates how\n// the system handles read-heavy workloads with occasional writes.\nfunc BenchmarkQueryWithConcurrentAdds(b *testing.B) {\n\tb.Run(\"1000 initial silences, 10% add rate\", func(b *testing.B) {\n\t\tbenchmarkQueryWithConcurrentAdds(b, 1000, 0.1)\n\t})\n\tb.Run(\"1000 initial silences, 1% add rate\", func(b *testing.B) {\n\t\tbenchmarkQueryWithConcurrentAdds(b, 1000, 0.01)\n\t})\n\tb.Run(\"1000 initial silences, 0.1% add rate\", func(b *testing.B) {\n\t\tbenchmarkQueryWithConcurrentAdds(b, 1000, 0.001)\n\t})\n\tb.Run(\"10000 initial silences, 1% add rate\", func(b *testing.B) {\n\t\tbenchmarkQueryWithConcurrentAdds(b, 10000, 0.01)\n\t})\n\tb.Run(\"10000 initial silences, 0.1% add rate\", func(b *testing.B) {\n\t\tbenchmarkQueryWithConcurrentAdds(b, 10000, 0.001)\n\t})\n}\n\nfunc benchmarkQueryWithConcurrentAdds(b *testing.B, initialSilences int, addRatio float64) {\n\ts, err := New(Options{Metrics: prometheus.NewRegistry()})\n\trequire.NoError(b, err)\n\n\tclock := quartz.NewMock(b).WithLogger(quartz.NoOpLogger)\n\ts.clock = clock\n\tnow := clock.Now()\n\n\tlset := model.LabelSet{\"aaaa\": \"AAAA\", \"bbbb\": \"BBBB\", \"cccc\": \"CCCC\"}\n\n\t// Create initial silences\n\tfor i := range initialSilences {\n\t\tid := strconv.Itoa(i)\n\t\tpatA := \"A{4}|\" + id\n\t\tpatB := id\n\t\tif i%10 == 0 {\n\t\t\tpatB = \"B(B|C)B.|\" + id\n\t\t}\n\n\t\tsil := &silencepb.Silence{\n\t\t\tMatchers: []*silencepb.Matcher{\n\t\t\t\t{Type: silencepb.Matcher_REGEXP, Name: \"aaaa\", Pattern: patA},\n\t\t\t\t{Type: silencepb.Matcher_REGEXP, Name: \"bbbb\", Pattern: patB},\n\t\t\t},\n\t\t\tStartsAt:  timestamppb.New(now.Add(-time.Minute)),\n\t\t\tEndsAt:    timestamppb.New(now.Add(time.Hour)),\n\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t}\n\t\trequire.NoError(b, s.Set(b.Context(), sil))\n\t}\n\n\tvar addCounter int\n\tvar mu sync.Mutex\n\n\tb.ResetTimer()\n\n\t// Run parallel operations\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\t// Determine if this iteration should add a silence\n\t\t\tmu.Lock()\n\t\t\tshouldAdd := float64(addCounter) < float64(b.N)*addRatio\n\t\t\tif shouldAdd {\n\t\t\t\taddCounter++\n\t\t\t}\n\t\t\tlocalCounter := addCounter + initialSilences\n\t\t\tmu.Unlock()\n\n\t\t\tif shouldAdd {\n\t\t\t\t// Add a new silence\n\t\t\t\tid := strconv.Itoa(localCounter)\n\t\t\t\tpatA := \"A{4}|\" + id\n\t\t\t\tpatB := \"B(B|C)B.|\" + id\n\n\t\t\t\tsil := &silencepb.Silence{\n\t\t\t\t\tMatchers: []*silencepb.Matcher{\n\t\t\t\t\t\t{Type: silencepb.Matcher_REGEXP, Name: \"aaaa\", Pattern: patA},\n\t\t\t\t\t\t{Type: silencepb.Matcher_REGEXP, Name: \"bbbb\", Pattern: patB},\n\t\t\t\t\t},\n\t\t\t\t\tStartsAt:  timestamppb.New(now.Add(-time.Minute)),\n\t\t\t\t\tEndsAt:    timestamppb.New(now.Add(time.Hour)),\n\t\t\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t\t\t}\n\t\t\t\tif err := s.Set(b.Context(), sil); err != nil {\n\t\t\t\t\tb.Error(err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Query silences (the common operation)\n\t\t\t\t_, _, err := s.Query(\n\t\t\t\t\tb.Context(),\n\t\t\t\t\tQState(SilenceStateActive),\n\t\t\t\t\tQMatches(lset),\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\tb.Error(err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t})\n}\n\n// BenchmarkMutesParallel benchmarks concurrent Mutes calls to demonstrate\n// the performance improvement from parallel query execution.\nfunc BenchmarkMutesParallel(b *testing.B) {\n\tb.Run(\"100 silences\", func(b *testing.B) {\n\t\tbenchmarkMutesParallel(b, 100)\n\t})\n\tb.Run(\"1000 silences\", func(b *testing.B) {\n\t\tbenchmarkMutesParallel(b, 1000)\n\t})\n\tb.Run(\"10000 silences\", func(b *testing.B) {\n\t\tbenchmarkMutesParallel(b, 10000)\n\t})\n}\n\nfunc benchmarkMutesParallel(b *testing.B, numSilences int) {\n\tsilences, err := New(Options{Metrics: prometheus.NewRegistry()})\n\trequire.NoError(b, err)\n\n\tclock := quartz.NewMock(b).WithLogger(quartz.NoOpLogger)\n\tsilences.clock = clock\n\tnow := clock.Now()\n\n\t// Create silences that will match the alert\n\tfor range numSilences {\n\t\ts := &silencepb.Silence{\n\t\t\tMatchers: []*silencepb.Matcher{{\n\t\t\t\tType:    silencepb.Matcher_EQUAL,\n\t\t\t\tName:    \"foo\",\n\t\t\t\tPattern: \"bar\",\n\t\t\t}},\n\t\t\tStartsAt: timestamppb.New(now),\n\t\t\tEndsAt:   timestamppb.New(now.Add(time.Minute)),\n\t\t}\n\t\trequire.NoError(b, silences.Set(b.Context(), s))\n\t}\n\n\tm := types.NewMarker(prometheus.NewRegistry())\n\tsilencer := NewSilencer(silences, m, promslog.NewNopLogger())\n\n\tb.ResetTimer()\n\n\t// Run Mutes in parallel\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tsilencer.Mutes(b.Context(), model.LabelSet{\"foo\": \"bar\"})\n\t\t}\n\t})\n}\n\n// BenchmarkGC benchmarks the garbage collection performance for different\n// numbers of silences and different ratios of expired silences.\nfunc BenchmarkGC(b *testing.B) {\n\tb.Run(\"1000 silences, 0% expired\", func(b *testing.B) {\n\t\tbenchmarkGC(b, 1000, 0.0)\n\t})\n\tb.Run(\"1000 silences, 30% expired\", func(b *testing.B) {\n\t\tbenchmarkGC(b, 1000, 0.3)\n\t})\n\tb.Run(\"1000 silences, 80% expired\", func(b *testing.B) {\n\t\tbenchmarkGC(b, 1000, 0.8)\n\t})\n\tb.Run(\"10000 silences, 0% expired\", func(b *testing.B) {\n\t\tbenchmarkGC(b, 10000, 0.0)\n\t})\n\tb.Run(\"10000 silences, 10% expired\", func(b *testing.B) {\n\t\tbenchmarkGC(b, 10000, 0.1)\n\t})\n\tb.Run(\"10000 silences, 50% expired\", func(b *testing.B) {\n\t\tbenchmarkGC(b, 10000, 0.5)\n\t})\n\tb.Run(\"10000 silences, 80% expired\", func(b *testing.B) {\n\t\tbenchmarkGC(b, 10000, 0.8)\n\t})\n}\n\nfunc benchmarkGC(b *testing.B, numSilences int, expiredRatio float64) {\n\tb.ReportAllocs()\n\n\tclock := quartz.NewMock(b).WithLogger(quartz.NoOpLogger)\n\tnow := clock.Now()\n\n\tnumExpired := int(float64(numSilences) * expiredRatio)\n\tnumActive := numSilences - numExpired\n\n\tmatchers := []*silencepb.Matcher{{\n\t\tType:    silencepb.Matcher_EQUAL,\n\t\tName:    \"foo\",\n\t\tPattern: \"bar\",\n\t}}\n\tstartTime := timestamppb.New(now.Add(-2 * time.Hour))\n\tupdateTime := timestamppb.New(now.Add(-2 * time.Hour))\n\tendTime := timestamppb.New(now.Add(-time.Hour))\n\texpireTime := timestamppb.New(now.Add(-time.Minute))\n\tactiveTime := timestamppb.New(now.Add(2 * time.Hour))\n\n\tsils := make([]*silencepb.MeshSilence, 0, numSilences)\n\n\tfor _, j := range rand.Perm(numSilences) {\n\t\tif j < numExpired {\n\t\t\tsil := &silencepb.MeshSilence{\n\t\t\t\tSilence: &silencepb.Silence{\n\t\t\t\t\tId:        fmt.Sprintf(\"expired-%d\", j),\n\t\t\t\t\tMatchers:  matchers,\n\t\t\t\t\tStartsAt:  startTime,\n\t\t\t\t\tEndsAt:    endTime,\n\t\t\t\t\tUpdatedAt: updateTime,\n\t\t\t\t},\n\t\t\t\tExpiresAt: expireTime,\n\t\t\t}\n\t\t\tsils = append(sils, sil)\n\t\t} else {\n\t\t\tsil := &silencepb.MeshSilence{\n\t\t\t\tSilence: &silencepb.Silence{\n\t\t\t\t\tId:        fmt.Sprintf(\"active-%d\", j),\n\t\t\t\t\tMatchers:  matchers,\n\t\t\t\t\tStartsAt:  startTime,\n\t\t\t\t\tEndsAt:    endTime,\n\t\t\t\t\tUpdatedAt: updateTime,\n\t\t\t\t},\n\t\t\t\tExpiresAt: activeTime,\n\t\t\t}\n\t\t\tsils = append(sils, sil)\n\t\t}\n\t}\n\n\tb.ResetTimer()\n\n\tfor b.Loop() {\n\t\tb.StopTimer()\n\n\t\ts, err := New(Options{\n\t\t\tMetrics: prometheus.NewRegistry(),\n\t\t})\n\t\trequire.NoError(b, err)\n\t\ts.clock = clock\n\n\t\tfor _, sil := range sils {\n\t\t\ts.st[sil.Silence.Id] = sil\n\t\t\ts.indexSilence(sil.Silence)\n\t\t}\n\n\t\tb.StartTimer()\n\t\tn1, err := s.GC()\n\t\trequire.NoError(b, err)\n\t\tn2, err := s.GC()\n\t\trequire.NoError(b, err)\n\t\tb.StopTimer()\n\n\t\trequire.NoError(b, err)\n\t\trequire.Equal(b, numExpired, n1)\n\t\trequire.Equal(b, 0, n2)\n\t\trequire.Len(b, s.st, numActive)\n\t\trequire.Len(b, s.mi, numActive)\n\t\tb.StartTimer()\n\t}\n}\n"
  },
  {
    "path": "silence/silence_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage silence\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"os\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/coder/quartz\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/testutil\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\t\"google.golang.org/protobuf/encoding/protodelim\"\n\t\"google.golang.org/protobuf/proto\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/prometheus/alertmanager/featurecontrol\"\n\t\"github.com/prometheus/alertmanager/matcher/compat\"\n\tpb \"github.com/prometheus/alertmanager/silence/silencepb\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nfunc checkErr(t *testing.T, expected string, got error) {\n\tt.Helper()\n\n\tif expected == \"\" {\n\t\trequire.NoError(t, got)\n\t\treturn\n\t}\n\n\tif got == nil {\n\t\tt.Errorf(\"expected error containing %q but got none\", expected)\n\t\treturn\n\t}\n\n\trequire.Contains(t, got.Error(), expected)\n}\n\n// requireStatesEqual compares two silence states using proto.Equal for proper protobuf comparison.\nfunc requireStatesEqual(t *testing.T, expected, actual state, msgAndArgs ...any) {\n\tt.Helper()\n\trequire.Len(t, actual, len(expected), msgAndArgs...)\n\tfor id, expectedSil := range expected {\n\t\tactualSil, ok := actual[id]\n\t\trequire.True(t, ok, \"silence %s missing from actual state\", id)\n\t\trequire.True(t, proto.Equal(expectedSil, actualSil), \"silence %s mismatch: expected %v, got %v\", id, expectedSil, actualSil)\n\t}\n}\n\nfunc TestOptionsValidate(t *testing.T) {\n\tcases := []struct {\n\t\toptions *Options\n\t\terr     string\n\t}{\n\t\t{\n\t\t\toptions: &Options{\n\t\t\t\tMetrics:        prometheus.NewRegistry(),\n\t\t\t\tSnapshotReader: &bytes.Buffer{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\toptions: &Options{\n\t\t\t\tMetrics:      prometheus.NewRegistry(),\n\t\t\t\tSnapshotFile: \"test.bkp\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\toptions: &Options{\n\t\t\t\tMetrics:        prometheus.NewRegistry(),\n\t\t\t\tSnapshotFile:   \"test bkp\",\n\t\t\t\tSnapshotReader: &bytes.Buffer{},\n\t\t\t},\n\t\t\terr: \"only one of SnapshotFile and SnapshotReader must be set\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tcheckErr(t, c.err, c.options.validate())\n\t}\n}\n\nfunc TestSilenceGCOverTime(t *testing.T) {\n\tt.Run(\"GC does not remove active silences\", func(t *testing.T) {\n\t\ts, err := New(Options{Metrics: prometheus.NewRegistry()})\n\t\trequire.NoError(t, err)\n\t\ts.clock = quartz.NewMock(t)\n\t\tnow := s.nowUTC()\n\t\tinitialState := state{\n\t\t\t\"1\": &pb.MeshSilence{Silence: &pb.Silence{Id: \"1\"}, ExpiresAt: timestamppb.New(now)},\n\t\t\t\"2\": &pb.MeshSilence{Silence: &pb.Silence{Id: \"2\"}, ExpiresAt: timestamppb.New(now.Add(-time.Second))},\n\t\t\t\"3\": &pb.MeshSilence{Silence: &pb.Silence{Id: \"3\"}, ExpiresAt: timestamppb.New(now.Add(time.Second))},\n\t\t}\n\t\tfor _, sil := range initialState {\n\t\t\ts.st[sil.Silence.Id] = sil\n\t\t\ts.indexSilence(sil.Silence)\n\t\t}\n\t\twant := state{\n\t\t\t\"3\": &pb.MeshSilence{Silence: &pb.Silence{Id: \"3\"}, ExpiresAt: timestamppb.New(now.Add(time.Second))},\n\t\t}\n\t\tn, err := s.GC()\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 2, n)\n\t\trequireStatesEqual(t, want, s.st)\n\t})\n\n\tt.Run(\"GC does not leak cache entries\", func(t *testing.T) {\n\t\ts, err := New(Options{Metrics: prometheus.NewRegistry()})\n\t\trequire.NoError(t, err)\n\t\tclock := quartz.NewMock(t)\n\t\ts.clock = clock\n\t\tsil1 := &pb.Silence{\n\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\tMatchers: []*pb.Matcher{{\n\t\t\t\t\tType:    pb.Matcher_EQUAL,\n\t\t\t\t\tName:    \"foo\",\n\t\t\t\t\tPattern: \"bar\",\n\t\t\t\t}},\n\t\t\t}},\n\t\t\tStartsAt: timestamppb.New(clock.Now()),\n\t\t\tEndsAt:   timestamppb.New(clock.Now().Add(time.Minute)),\n\t\t}\n\t\trequire.NoError(t, s.Set(t.Context(), sil1))\n\t\trequire.Len(t, s.st, 1)\n\t\trequire.Len(t, s.mi, 1)\n\t\t// Move time forward and both silence and cache entry should be garbage\n\t\t// collected.\n\t\tclock.Advance(time.Minute)\n\t\tn, err := s.GC()\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 1, n)\n\t\trequire.Empty(t, s.st)\n\t\trequire.Empty(t, s.mi)\n\t})\n\n\tt.Run(\"replacing a silences does not leak cache entries\", func(t *testing.T) {\n\t\ts, err := New(Options{Metrics: prometheus.NewRegistry()})\n\t\trequire.NoError(t, err)\n\t\tclock := quartz.NewMock(t)\n\t\ts.clock = clock\n\t\tsil1 := &pb.Silence{\n\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\tMatchers: []*pb.Matcher{{\n\t\t\t\t\tType:    pb.Matcher_EQUAL,\n\t\t\t\t\tName:    \"foo\",\n\t\t\t\t\tPattern: \"bar\",\n\t\t\t\t}},\n\t\t\t}},\n\t\t\tStartsAt: timestamppb.New(clock.Now()),\n\t\t\tEndsAt:   timestamppb.New(clock.Now().Add(time.Minute)),\n\t\t}\n\t\trequire.NoError(t, s.Set(t.Context(), sil1))\n\t\trequire.Len(t, s.st, 1)\n\t\trequire.Len(t, s.mi, 1)\n\t\t// must clone sil1 before replacing it.\n\t\tsil2 := cloneSilence(sil1)\n\t\tsil2.MatcherSets = []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{{\n\t\t\t\tType:    pb.Matcher_EQUAL,\n\t\t\t\tName:    \"bar\",\n\t\t\t\tPattern: \"baz\",\n\t\t\t}},\n\t\t}}\n\t\trequire.NoError(t, s.Set(t.Context(), sil2))\n\t\trequire.Len(t, s.st, 2)\n\t\trequire.Len(t, s.mi, 2)\n\t\t// Move time forward and both silence and cache entry should be garbage\n\t\t// collected.\n\t\tclock.Advance(time.Minute)\n\t\tn, err := s.GC()\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 2, n)\n\t\trequire.Empty(t, s.st)\n\t\trequire.Empty(t, s.mi)\n\t})\n\n\t// This test checks for a memory leak that occurred in the matcher cache when\n\t// updating an existing silence.\n\tt.Run(\"updating a silence does not leak cache entries\", func(t *testing.T) {\n\t\ts, err := New(Options{Metrics: prometheus.NewRegistry()})\n\t\trequire.NoError(t, err)\n\t\tclock := quartz.NewMock(t)\n\t\ts.clock = clock\n\t\tsil1 := &pb.Silence{\n\t\t\tId: \"1\",\n\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\tMatchers: []*pb.Matcher{{\n\t\t\t\t\tType:    pb.Matcher_EQUAL,\n\t\t\t\t\tName:    \"foo\",\n\t\t\t\t\tPattern: \"bar\",\n\t\t\t\t}},\n\t\t\t}},\n\t\t\tStartsAt: timestamppb.New(clock.Now()),\n\t\t\tEndsAt:   timestamppb.New(clock.Now().Add(time.Minute)),\n\t\t}\n\t\ts.st[\"1\"] = &pb.MeshSilence{Silence: sil1, ExpiresAt: timestamppb.New(clock.Now().Add(time.Minute))}\n\t\ts.indexSilence(sil1)\n\t\trequire.Len(t, s.mi, 1)\n\t\t// must clone sil1 before updating it.\n\t\tsil2 := cloneSilence(sil1)\n\t\trequire.NoError(t, s.Set(t.Context(), sil2))\n\t\t// The memory leak occurred because updating a silence would add a new\n\t\t// entry in the matcher cache even though no new silence was created.\n\t\t// This check asserts that this no longer happens.\n\t\ts.Query(t.Context(), QMatches(model.LabelSet{\"foo\": \"bar\"}))\n\t\trequire.Len(t, s.st, 1)\n\t\trequire.Len(t, s.mi, 1)\n\t\t// Move time forward and both silence and cache entry should be garbage\n\t\t// collected.\n\t\tclock.Advance(time.Minute)\n\t\tn, err := s.GC()\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 1, n)\n\t\trequire.Empty(t, s.st)\n\t\trequire.Empty(t, s.mi)\n\t})\n\n\tt.Run(\"GC collects silences in multiple rounds\", func(t *testing.T) {\n\t\ts, err := New(Options{\n\t\t\tMetrics:   prometheus.NewRegistry(),\n\t\t\tRetention: time.Hour,\n\t\t})\n\t\tclock := quartz.NewMock(t)\n\t\ts.clock = clock\n\t\trequire.NoError(t, err)\n\t\tnow := s.nowUTC().UTC()\n\n\t\tmatcher := &pb.Matcher{\n\t\t\tType:    pb.Matcher_EQUAL,\n\t\t\tName:    \"job\",\n\t\t\tPattern: \"test\",\n\t\t}\n\n\t\t// Create silences that expire at different times.\n\t\t// Directly set them in state to create pre-expired silences.\n\t\t// Group 1: expires at now+30min (with retention: now+90min)\n\t\t// Group 2: expires at now+45min (with retention: now+105min)\n\t\t// Group 3: expires at now+60min (with retention: now+120min)\n\t\t// Group 4: active, expires at now+3hours (with retention: now+4hours)\n\n\t\tsils := make([]*pb.Silence, 0, 60)\n\t\tfor i := range 10 {\n\t\t\tsil := &pb.Silence{\n\t\t\t\tId: fmt.Sprintf(\"group1-%d\", i),\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{matcher},\n\t\t\t\t}},\n\t\t\t\tStartsAt:  timestamppb.New(now.Add(-time.Hour)),\n\t\t\t\tEndsAt:    timestamppb.New(now.Add(30 * time.Minute)),\n\t\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t\t}\n\t\t\tsils = append(sils, sil)\n\t\t}\n\n\t\tfor i := range 10 {\n\t\t\tsil := &pb.Silence{\n\t\t\t\tId: fmt.Sprintf(\"group2-%d\", i),\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{matcher},\n\t\t\t\t}},\n\t\t\t\tStartsAt:  timestamppb.New(now.Add(-time.Hour)),\n\t\t\t\tEndsAt:    timestamppb.New(now.Add(45 * time.Minute)),\n\t\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t\t}\n\t\t\tsils = append(sils, sil)\n\t\t}\n\n\t\tfor i := range 10 {\n\t\t\tsil := &pb.Silence{\n\t\t\t\tId: fmt.Sprintf(\"group3-%d\", i),\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{matcher},\n\t\t\t\t}},\n\t\t\t\tStartsAt:  timestamppb.New(now.Add(-time.Hour)),\n\t\t\t\tEndsAt:    timestamppb.New(now.Add(60 * time.Minute)),\n\t\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t\t}\n\t\t\tsils = append(sils, sil)\n\t\t}\n\n\t\tfor i := range 30 {\n\t\t\tsil := &pb.Silence{\n\t\t\t\tId: fmt.Sprintf(\"active-%d\", i),\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{matcher},\n\t\t\t\t}},\n\t\t\t\tStartsAt:  timestamppb.New(now.Add(-time.Hour)),\n\t\t\t\tEndsAt:    timestamppb.New(now.Add(3 * time.Hour)),\n\t\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t\t}\n\t\t\tsils = append(sils, sil)\n\t\t}\n\n\t\t// Shuffle silences to ensure GC order is not dependent on insertion order.\n\t\trand.Shuffle(len(sils), func(i, j int) {\n\t\t\tsils[i], sils[j] = sils[j], sils[i]\n\t\t})\n\t\tfor _, sil := range sils {\n\t\t\tms := s.toMeshSilence(sil)\n\t\t\ts.st[ms.Silence.Id] = ms\n\t\t\ts.indexSilence(ms.Silence)\n\t\t}\n\n\t\trequire.Len(t, s.st, 60)\n\t\trequire.Len(t, s.mi, 60)\n\n\t\t// First GC: nothing should be collected yet\n\t\tn, err := s.GC()\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 0, n)\n\t\trequire.Len(t, s.st, 60)\n\t\trequire.Len(t, s.mi, 60)\n\n\t\t// Advance time to 91 minutes - Group 1 should be GC'd\n\t\tclock.Advance(91 * time.Minute)\n\t\tn, err = s.GC()\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 10, n)\n\t\trequire.Len(t, s.st, 50)\n\t\trequire.Len(t, s.mi, 50)\n\n\t\t// Advance time to 106 minutes - Group 2 should be GC'd\n\t\tclock.Advance(15 * time.Minute)\n\t\tn, err = s.GC()\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 10, n)\n\t\trequire.Len(t, s.st, 40)\n\t\trequire.Len(t, s.mi, 40)\n\n\t\t// Advance time to 121 minutes - Group 3 should be GC'd\n\t\tclock.Advance(15 * time.Minute)\n\t\tn, err = s.GC()\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 10, n)\n\t\trequire.Len(t, s.st, 30)\n\t\trequire.Len(t, s.mi, 30)\n\n\t\t// Verify all remaining silences are active\n\t\tfor id := range s.st {\n\t\t\trequire.Contains(t, id, \"active-\")\n\t\t}\n\t})\n\n\tt.Run(\"GC continues and removes erroneous silences\", func(t *testing.T) {\n\t\treg := prometheus.NewRegistry()\n\t\ts, err := New(Options{Metrics: reg})\n\t\trequire.NoError(t, err)\n\t\tclock := quartz.NewMock(t)\n\t\ts.clock = clock\n\t\tnow := clock.Now()\n\n\t\t// Create a valid silence\n\t\tvalidSil := &pb.Silence{\n\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\tMatchers: []*pb.Matcher{{\n\t\t\t\t\tType:    pb.Matcher_EQUAL,\n\t\t\t\t\tName:    \"foo\",\n\t\t\t\t\tPattern: \"bar\",\n\t\t\t\t}},\n\t\t\t}},\n\t\t\tStartsAt: timestamppb.New(now),\n\t\t\tEndsAt:   timestamppb.New(now.Add(time.Minute)),\n\t\t}\n\t\trequire.NoError(t, s.Set(t.Context(), validSil))\n\t\tvalidID := validSil.Id\n\n\t\t// Manually add an erroneous silence with zero expiration\n\t\terroneousSil := &pb.MeshSilence{\n\t\t\tSilence: &pb.Silence{\n\t\t\t\tId: \"erroneous\",\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{{\n\t\t\t\t\t\tType:    pb.Matcher_EQUAL,\n\t\t\t\t\t\tName:    \"bar\",\n\t\t\t\t\t\tPattern: \"baz\",\n\t\t\t\t\t}},\n\t\t\t\t}},\n\t\t\t\tStartsAt: timestamppb.New(now),\n\t\t\t\tEndsAt:   timestamppb.New(now.Add(time.Minute)),\n\t\t\t},\n\t\t\tExpiresAt: nil, // Zero expiration - invalid\n\t\t}\n\t\ts.st[\"erroneous\"] = erroneousSil\n\t\ts.vi.add(s.version+1, \"erroneous\")\n\t\ts.version++\n\n\t\t// Manually add an entry to version index that doesn't exist in state\n\t\ts.vi.add(s.version+1, \"missing\")\n\t\ts.version++\n\n\t\trequire.Len(t, s.st, 2)\n\t\trequire.Len(t, s.vi, 3)\n\n\t\t// Run GC - should continue despite errors\n\t\tn, err := s.GC()\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"zero expiration timestamp\")\n\t\trequire.Contains(t, err.Error(), \"missing from state\")\n\n\t\t// GC should have removed erroneous silences\n\t\trequire.Equal(t, 1, n) // Only the erroneous silence with zero expiration\n\t\trequire.Len(t, s.st, 1)\n\t\trequire.Len(t, s.vi, 1)\n\t\trequire.Contains(t, s.st, validID)\n\t\trequire.NotContains(t, s.st, \"erroneous\")\n\n\t\t// Check that the error metric was incremented\n\t\tmetricValue := testutil.ToFloat64(s.metrics.gcErrorsTotal)\n\t\trequire.Equal(t, float64(2), metricValue)\n\t})\n}\n\nfunc TestSilencesSnapshot(t *testing.T) {\n\t// Check whether storing and loading the snapshot is symmetric.\n\tnow := quartz.NewMock(t).Now().UTC()\n\n\tcases := []struct {\n\t\tentries []*pb.MeshSilence\n\t}{\n\t\t{\n\t\t\tentries: []*pb.MeshSilence{\n\t\t\t\t{\n\t\t\t\t\tSilence: &pb.Silence{\n\t\t\t\t\t\tId: \"3be80475-e219-4ee7-b6fc-4b65114e362f\",\n\t\t\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val1\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t\t\t{Name: \"label2\", Pattern: \"val.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}},\n\t\t\t\t\t\tStartsAt:  timestamppb.New(now),\n\t\t\t\t\t\tEndsAt:    timestamppb.New(now),\n\t\t\t\t\t\tUpdatedAt: timestamppb.New(now),\n\t\t\t\t\t},\n\t\t\t\t\tExpiresAt: timestamppb.New(now),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tSilence: &pb.Silence{\n\t\t\t\t\t\tId: \"3dfb2528-59ce-41eb-b465-f875a4e744a4\",\n\t\t\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val1\", Type: pb.Matcher_NOT_EQUAL},\n\t\t\t\t\t\t\t\t{Name: \"label2\", Pattern: \"val.+\", Type: pb.Matcher_NOT_REGEXP},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}},\n\t\t\t\t\t\tStartsAt:  timestamppb.New(now),\n\t\t\t\t\t\tEndsAt:    timestamppb.New(now),\n\t\t\t\t\t\tUpdatedAt: timestamppb.New(now),\n\t\t\t\t\t},\n\t\t\t\t\tExpiresAt: timestamppb.New(now),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tSilence: &pb.Silence{\n\t\t\t\t\t\tId: \"4b1e760d-182c-4980-b873-c1a6827c9817\",\n\t\t\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val1\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}},\n\t\t\t\t\t\tStartsAt:  timestamppb.New(now.Add(time.Hour)),\n\t\t\t\t\t\tEndsAt:    timestamppb.New(now.Add(2 * time.Hour)),\n\t\t\t\t\t\tUpdatedAt: timestamppb.New(now),\n\t\t\t\t\t},\n\t\t\t\t\tExpiresAt: timestamppb.New(now.Add(24 * time.Hour)),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tf, err := os.CreateTemp(t.TempDir(), \"snapshot\")\n\t\trequire.NoError(t, err, \"creating temp file failed\")\n\n\t\ts1 := &Silences{st: state{}, metrics: newMetrics(nil, nil)}\n\t\t// Setup internal state manually.\n\t\tfor _, e := range c.entries {\n\t\t\ts1.st[e.Silence.Id] = e\n\t\t}\n\t\t_, err = s1.Snapshot(f)\n\t\trequire.NoError(t, err, \"creating snapshot failed\")\n\n\t\trequire.NoError(t, f.Close(), \"closing snapshot file failed\")\n\n\t\tf, err = os.Open(f.Name())\n\t\trequire.NoError(t, err, \"opening snapshot file failed\")\n\n\t\t// Check again against new nlog instance.\n\t\ts2 := &Silences{mi: matcherIndex{}, st: state{}}\n\t\terr = s2.loadSnapshot(f)\n\t\trequire.NoError(t, err, \"error loading snapshot\")\n\t\trequire.Len(t, s2.st, len(s1.st), \"state length mismatch after loading snapshot\")\n\t\tfor id, expected := range s1.st {\n\t\t\tactual, ok := s2.st[id]\n\t\t\trequire.True(t, ok, \"silence %s missing from loaded state\", id)\n\t\t\trequire.True(t, proto.Equal(expected, actual), \"silence %s mismatch after loading snapshot\", id)\n\t\t}\n\n\t\trequire.NoError(t, f.Close(), \"closing snapshot file failed\")\n\t}\n}\n\n// This tests a regression introduced by https://github.com/prometheus/alertmanager/pull/2689.\nfunc TestSilences_Maintenance_DefaultMaintenanceFuncDoesntCrash(t *testing.T) {\n\tf, err := os.CreateTemp(t.TempDir(), \"snapshot\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\tclock := quartz.NewMock(t)\n\ts := &Silences{st: state{}, logger: promslog.NewNopLogger(), clock: clock, metrics: newMetrics(nil, nil)}\n\tstopc := make(chan struct{})\n\n\tdone := make(chan struct{})\n\tgo func() {\n\t\ts.Maintenance(100*time.Millisecond, f.Name(), stopc, nil)\n\t\tclose(done)\n\t}()\n\truntime.Gosched()\n\n\tclock.Advance(100 * time.Millisecond)\n\tclose(stopc)\n\n\t<-done\n}\n\nfunc TestSilences_Maintenance_SupportsCustomCallback(t *testing.T) {\n\tf, err := os.CreateTemp(t.TempDir(), \"snapshot\")\n\trequire.NoError(t, err, \"creating temp file failed\")\n\tclock := quartz.NewMock(t)\n\treg := prometheus.NewRegistry()\n\ts := &Silences{st: state{}, logger: promslog.NewNopLogger(), clock: clock}\n\ts.metrics = newMetrics(reg, s)\n\tstopc := make(chan struct{})\n\n\tvar calls atomic.Int32\n\tvar wg sync.WaitGroup\n\n\twg.Go(func() {\n\t\ts.Maintenance(10*time.Second, f.Name(), stopc, func() (int64, error) {\n\t\t\tcalls.Add(1)\n\t\t\treturn 0, nil\n\t\t})\n\t})\n\tgosched()\n\n\t// Before the first tick, no maintenance executed.\n\tclock.Advance(9 * time.Second)\n\trequire.EqualValues(t, 0, calls.Load())\n\n\t// Tick once.\n\tclock.Advance(1 * time.Second)\n\trequire.Eventually(t, func() bool { return calls.Load() == 1 }, 5*time.Second, time.Second)\n\n\t// Stop the maintenance loop. We should get exactly one more execution of the maintenance func.\n\tclose(stopc)\n\twg.Wait()\n\n\trequire.EqualValues(t, 2, calls.Load())\n\n\t// Check the maintenance metrics.\n\trequire.NoError(t, testutil.GatherAndCompare(reg, bytes.NewBufferString(`\n# HELP alertmanager_silences_maintenance_errors_total How many maintenances were executed for silences that failed.\n# TYPE alertmanager_silences_maintenance_errors_total counter\nalertmanager_silences_maintenance_errors_total 0\n# HELP alertmanager_silences_maintenance_total How many maintenances were executed for silences.\n# TYPE alertmanager_silences_maintenance_total counter\nalertmanager_silences_maintenance_total 2\n`), \"alertmanager_silences_maintenance_total\", \"alertmanager_silences_maintenance_errors_total\"))\n}\n\nfunc TestSilencesSetSilence(t *testing.T) {\n\ts, err := New(Options{\n\t\tMetrics:   prometheus.NewRegistry(),\n\t\tRetention: time.Minute,\n\t})\n\trequire.NoError(t, err)\n\n\tclock := quartz.NewMock(t)\n\ts.clock = clock\n\n\tnowpb := s.nowUTC()\n\n\tsil := &pb.Silence{\n\t\tId: \"some_id\",\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{{Name: \"abc\", Pattern: \"def\"}},\n\t\t}},\n\t\tStartsAt: timestamppb.New(nowpb),\n\t\tEndsAt:   timestamppb.New(nowpb),\n\t}\n\n\twant := state{\n\t\t\"some_id\": &pb.MeshSilence{\n\t\t\tSilence:   sil,\n\t\t\tExpiresAt: timestamppb.New(nowpb.Add(time.Minute)),\n\t\t},\n\t}\n\n\twantBroadcast := &pb.MeshSilence{\n\t\tSilence: &pb.Silence{\n\t\t\tId:       \"some_id\",\n\t\t\tMatchers: sil.MatcherSets[0].Matchers, // Backward compatibility\n\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\tMatchers: []*pb.Matcher{{Name: \"abc\", Pattern: \"def\"}},\n\t\t\t}},\n\t\t\tStartsAt: timestamppb.New(nowpb),\n\t\t\tEndsAt:   timestamppb.New(nowpb),\n\t\t},\n\t\tExpiresAt: timestamppb.New(nowpb.Add(time.Minute)),\n\t}\n\n\tdone := make(chan struct{})\n\ts.broadcast = func(b []byte) {\n\t\tvar e pb.MeshSilence\n\t\tr := bytes.NewReader(b)\n\t\terr := protodelim.UnmarshalFrom(r, &e)\n\t\trequire.NoError(t, err)\n\n\t\trequire.True(t, proto.Equal(&e, wantBroadcast), \"broadcast message mismatch\")\n\t\tclose(done)\n\t}\n\n\t// setSilence() is always called with s.mtx locked() in the application code\n\tfunc() {\n\t\ts.mtx.Lock()\n\t\tdefer s.mtx.Unlock()\n\t\trequire.NoError(t, s.setSilence(s.toMeshSilence(sil), nowpb))\n\t}()\n\n\t// Ensure broadcast was called.\n\tif _, isOpen := <-done; isOpen {\n\t\tt.Fatal(\"broadcast was not called\")\n\t}\n\n\trequireStatesEqual(t, want, s.st, \"Unexpected silence state\")\n}\n\nfunc TestSilenceSet(t *testing.T) {\n\ts, err := New(Options{\n\t\tMetrics:   prometheus.NewRegistry(),\n\t\tRetention: time.Hour,\n\t})\n\trequire.NoError(t, err)\n\n\tclock := quartz.NewMock(t)\n\ts.clock = clock\n\tstart1 := s.nowUTC()\n\n\t// Insert silence with fixed start time.\n\tsil1 := &pb.Silence{\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{{Name: \"a\", Pattern: \"b\"}},\n\t\t}},\n\t\tStartsAt: timestamppb.New(start1.Add(2 * time.Minute)),\n\t\tEndsAt:   timestamppb.New(start1.Add(5 * time.Minute)),\n\t}\n\tversionBeforeOp := s.Version()\n\trequire.NoError(t, s.Set(t.Context(), sil1))\n\trequire.NotEmpty(t, sil1.Id)\n\trequire.NotEqual(t, versionBeforeOp, s.Version())\n\n\twant := state{\n\t\tsil1.Id: &pb.MeshSilence{\n\t\t\tSilence: &pb.Silence{\n\t\t\t\tId: sil1.Id,\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{{Name: \"a\", Pattern: \"b\"}},\n\t\t\t\t}},\n\t\t\t\tStartsAt:  timestamppb.New(start1.Add(2 * time.Minute)),\n\t\t\t\tEndsAt:    timestamppb.New(start1.Add(5 * time.Minute)),\n\t\t\t\tUpdatedAt: timestamppb.New(start1),\n\t\t\t},\n\t\t\tExpiresAt: timestamppb.New(start1.Add(5*time.Minute + s.retention)),\n\t\t},\n\t}\n\trequireStatesEqual(t, want, s.st, \"unexpected state after silence creation\")\n\n\t// Insert silence with unset start time. Must be set to now.\n\tclock.Advance(time.Minute)\n\tstart2 := s.nowUTC()\n\n\tsil2 := &pb.Silence{\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{{Name: \"a\", Pattern: \"b\"}},\n\t\t}},\n\t\tEndsAt: timestamppb.New(start2.Add(1 * time.Minute)),\n\t}\n\tversionBeforeOp = s.Version()\n\trequire.NoError(t, s.Set(t.Context(), sil2))\n\trequire.NotEmpty(t, sil2.Id)\n\trequire.NotEqual(t, versionBeforeOp, s.Version())\n\n\twant = state{\n\t\tsil1.Id: want[sil1.Id],\n\t\tsil2.Id: &pb.MeshSilence{\n\t\t\tSilence: &pb.Silence{\n\t\t\t\tId: sil2.Id,\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{{Name: \"a\", Pattern: \"b\"}},\n\t\t\t\t}},\n\t\t\t\tStartsAt:  timestamppb.New(start2),\n\t\t\t\tEndsAt:    timestamppb.New(start2.Add(1 * time.Minute)),\n\t\t\t\tUpdatedAt: timestamppb.New(start2),\n\t\t\t},\n\t\t\tExpiresAt: timestamppb.New(start2.Add(1*time.Minute + s.retention)),\n\t\t},\n\t}\n\trequireStatesEqual(t, want, s.st, \"unexpected state after silence creation\")\n\n\t// Should be able to update silence without modifications. It is expected to\n\t// keep the same ID.\n\tsil3 := cloneSilence(sil2)\n\tversionBeforeOp = s.Version()\n\trequire.NoError(t, s.Set(t.Context(), sil3))\n\trequire.Equal(t, sil2.Id, sil3.Id)\n\trequire.Equal(t, versionBeforeOp, s.Version())\n\n\t// Should be able to update silence with comment. It is also expected to\n\t// keep the same ID.\n\tsil4 := cloneSilence(sil3)\n\tsil4.Comment = \"c\"\n\tversionBeforeOp = s.Version()\n\trequire.NoError(t, s.Set(t.Context(), sil4))\n\trequire.Equal(t, sil3.Id, sil4.Id)\n\trequire.Equal(t, versionBeforeOp, s.Version())\n\n\t// Extend sil4 to expire at a later time. This should not expire the\n\t// existing silence, and so should also keep the same ID.\n\tclock.Advance(time.Minute)\n\tstart5 := s.nowUTC()\n\tsil5 := cloneSilence(sil4)\n\tsil5.EndsAt = timestamppb.New(start5.Add(100 * time.Minute))\n\tversionBeforeOp = s.Version()\n\trequire.NoError(t, s.Set(t.Context(), sil5))\n\trequire.Equal(t, sil4.Id, sil5.Id)\n\twant = state{\n\t\tsil1.Id: want[sil1.Id],\n\t\tsil2.Id: &pb.MeshSilence{\n\t\t\tSilence: &pb.Silence{\n\t\t\t\tId: sil2.Id,\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{{Name: \"a\", Pattern: \"b\"}},\n\t\t\t\t}},\n\t\t\t\tStartsAt:  timestamppb.New(start2),\n\t\t\t\tEndsAt:    timestamppb.New(start5.Add(100 * time.Minute)),\n\t\t\t\tUpdatedAt: timestamppb.New(start5),\n\t\t\t\tComment:   \"c\",\n\t\t\t},\n\t\t\tExpiresAt: timestamppb.New(start5.Add(100*time.Minute + s.retention)),\n\t\t},\n\t}\n\trequireStatesEqual(t, want, s.st, \"unexpected state after silence creation\")\n\trequire.Equal(t, versionBeforeOp, s.Version())\n\n\t// Replace the silence sil5 with another silence with different matchers.\n\t// Unlike previous updates, changing the matchers for an existing silence\n\t// will expire the existing silence and create a new silence. The new\n\t// silence is expected to have a different ID to preserve the history of\n\t// the previous silence.\n\tclock.Advance(time.Minute)\n\tstart6 := s.nowUTC()\n\n\tsil6 := cloneSilence(sil5)\n\tsil6.MatcherSets = []*pb.MatcherSet{{\n\t\tMatchers: []*pb.Matcher{{Name: \"a\", Pattern: \"c\"}},\n\t}}\n\tversionBeforeOp = s.Version()\n\trequire.NoError(t, s.Set(t.Context(), sil6))\n\trequire.NotEqual(t, sil5.Id, sil6.Id)\n\twant = state{\n\t\tsil1.Id: want[sil1.Id],\n\t\tsil2.Id: &pb.MeshSilence{\n\t\t\tSilence: &pb.Silence{\n\t\t\t\tId: sil2.Id,\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{{Name: \"a\", Pattern: \"b\"}},\n\t\t\t\t}},\n\t\t\t\tStartsAt:  timestamppb.New(start2),\n\t\t\t\tEndsAt:    timestamppb.New(start6), // Expired\n\t\t\t\tUpdatedAt: timestamppb.New(start6),\n\t\t\t\tComment:   \"c\",\n\t\t\t},\n\t\t\tExpiresAt: timestamppb.New(start6.Add(s.retention)),\n\t\t},\n\t\tsil6.Id: &pb.MeshSilence{\n\t\t\tSilence: &pb.Silence{\n\t\t\t\tId: sil6.Id,\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{{Name: \"a\", Pattern: \"c\"}},\n\t\t\t\t}},\n\t\t\t\tStartsAt:  timestamppb.New(start6),\n\t\t\t\tEndsAt:    timestamppb.New(start5.Add(100 * time.Minute)),\n\t\t\t\tUpdatedAt: timestamppb.New(start6),\n\t\t\t\tComment:   \"c\",\n\t\t\t},\n\t\t\tExpiresAt: timestamppb.New(start5.Add(100*time.Minute + s.retention)),\n\t\t},\n\t}\n\trequireStatesEqual(t, want, s.st, \"unexpected state after silence creation\")\n\trequire.NotEqual(t, versionBeforeOp, s.Version())\n\n\t// Re-create the silence that we just replaced. Changing the start time,\n\t// just like changing the matchers, creates a new silence with a different\n\t// ID. This is again to preserve the history of the original silence.\n\tclock.Advance(time.Minute)\n\tstart7 := s.nowUTC()\n\tsil7 := cloneSilence(sil5)\n\tsil7.StartsAt = timestamppb.New(start1)\n\tsil7.EndsAt = timestamppb.New(start1.Add(5 * time.Minute))\n\tversionBeforeOp = s.Version()\n\trequire.NoError(t, s.Set(t.Context(), sil7))\n\trequire.NotEqual(t, sil2.Id, sil7.Id)\n\twant = state{\n\t\tsil1.Id: want[sil1.Id],\n\t\tsil2.Id: want[sil2.Id],\n\t\tsil6.Id: want[sil6.Id],\n\t\tsil7.Id: &pb.MeshSilence{\n\t\t\tSilence: &pb.Silence{\n\t\t\t\tId: sil7.Id,\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{{Name: \"a\", Pattern: \"b\"}},\n\t\t\t\t}},\n\t\t\t\tStartsAt:  timestamppb.New(start7), // New silences have their start time set to \"now\" when created.\n\t\t\t\tEndsAt:    timestamppb.New(start1.Add(5 * time.Minute)),\n\t\t\t\tUpdatedAt: timestamppb.New(start7),\n\t\t\t\tComment:   \"c\",\n\t\t\t},\n\t\t\tExpiresAt: timestamppb.New(start1.Add(5*time.Minute + s.retention)),\n\t\t},\n\t}\n\trequireStatesEqual(t, want, s.st, \"unexpected state after silence creation\")\n\trequire.NotEqual(t, versionBeforeOp, s.Version())\n\n\t// Updating an existing silence with an invalid silence should not expire\n\t// the original silence.\n\tclock.Advance(time.Millisecond)\n\tsil8 := cloneSilence(sil7)\n\tsil8.EndsAt = nil // nil represents zero timestamp\n\tversionBeforeOp = s.Version()\n\trequire.EqualError(t, s.Set(t.Context(), sil8), \"invalid silence: invalid zero end timestamp\")\n\n\t// sil7 should not be expired because the update failed.\n\tclock.Advance(time.Millisecond)\n\tsil7, err = s.QueryOne(t.Context(), QIDs(sil7.Id))\n\trequire.NoError(t, err)\n\trequire.Equal(t, SilenceStateActive, getState(sil7, s.nowUTC()))\n\trequire.Equal(t, versionBeforeOp, s.Version())\n}\n\nfunc TestSilenceLimits(t *testing.T) {\n\ts, err := New(Options{\n\t\tLimits: Limits{\n\t\t\tMaxSilences:         func() int { return 1 },\n\t\t\tMaxSilenceSizeBytes: func() int { return 2 << 11 }, // 4KB\n\t\t},\n\t\tMetrics: prometheus.NewRegistry(),\n\t})\n\trequire.NoError(t, err)\n\n\t// Insert sil1 should succeed without error.\n\tsil1 := &pb.Silence{\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{{Name: \"a\", Pattern: \"b\"}},\n\t\t}},\n\t\tStartsAt: timestamppb.New(time.Now()),\n\t\tEndsAt:   timestamppb.New(time.Now().Add(5 * time.Minute)),\n\t}\n\trequire.NoError(t, s.Set(t.Context(), sil1))\n\n\t// Insert sil2 should fail because maximum number of silences has been\n\t// exceeded.\n\tsil2 := &pb.Silence{\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{{Name: \"c\", Pattern: \"d\"}},\n\t\t}},\n\t\tStartsAt: timestamppb.New(time.Now()),\n\t\tEndsAt:   timestamppb.New(time.Now().Add(5 * time.Minute)),\n\t}\n\trequire.EqualError(t, s.Set(t.Context(), sil2), \"exceeded maximum number of silences: 1 (limit: 1)\")\n\n\t// Expire sil1 and run the GC. This should allow sil2 to be inserted.\n\trequire.NoError(t, s.Expire(t.Context(), sil1.Id))\n\tn, err := s.GC()\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, n)\n\trequire.NoError(t, s.Set(t.Context(), sil2))\n\n\t// Expire sil2 and run the GC.\n\trequire.NoError(t, s.Expire(t.Context(), sil2.Id))\n\tn, err = s.GC()\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, n)\n\n\t// Insert sil3 should fail because it exceeds maximum size.\n\tsil3 := &pb.Silence{\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t{\n\t\t\t\t\tName:    strings.Repeat(\"e\", 2<<9),\n\t\t\t\t\tPattern: strings.Repeat(\"f\", 2<<9),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:    strings.Repeat(\"g\", 2<<9),\n\t\t\t\t\tPattern: strings.Repeat(\"h\", 2<<9),\n\t\t\t\t},\n\t\t\t},\n\t\t}},\n\t\tCreatedBy: strings.Repeat(\"i\", 2<<9),\n\t\tComment:   strings.Repeat(\"j\", 2<<9),\n\t\tStartsAt:  timestamppb.New(time.Now()),\n\t\tEndsAt:    timestamppb.New(time.Now().Add(5 * time.Minute)),\n\t}\n\trequire.EqualError(t, s.Set(t.Context(), sil3), fmt.Sprintf(\"silence exceeded maximum size: %d bytes (limit: 4096 bytes)\", proto.Size(s.toMeshSilence(sil3))))\n\n\t// Should be able to insert sil4.\n\tsil4 := &pb.Silence{\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{{Name: \"k\", Pattern: \"l\"}},\n\t\t}},\n\t\tStartsAt: timestamppb.New(time.Now()),\n\t\tEndsAt:   timestamppb.New(time.Now().Add(5 * time.Minute)),\n\t}\n\trequire.NoError(t, s.Set(t.Context(), sil4))\n\n\t// Should be able to update sil4 without modifications. It is expected to\n\t// keep the same ID.\n\tsil5 := cloneSilence(sil4)\n\trequire.NoError(t, s.Set(t.Context(), sil5))\n\trequire.Equal(t, sil4.Id, sil5.Id)\n\n\t// Should be able to update the comment. It is also expected to keep the\n\t// same ID.\n\tsil6 := cloneSilence(sil5)\n\tsil6.Comment = \"m\"\n\trequire.NoError(t, s.Set(t.Context(), sil6))\n\trequire.Equal(t, sil5.Id, sil6.Id)\n\n\t// Should not be able to update the start and end time as this requires\n\t// sil6 to be expired and a new silence to be created. However, this would\n\t// exceed the maximum number of silences, which counts both active and\n\t// expired silences.\n\tsil7 := cloneSilence(sil6)\n\tsil7.StartsAt = timestamppb.New(time.Now().Add(1 * time.Minute))\n\tsil7.EndsAt = timestamppb.New(time.Now().Add(10 * time.Minute))\n\trequire.EqualError(t, s.Set(t.Context(), sil7), \"exceeded maximum number of silences: 1 (limit: 1)\")\n\n\t// sil6 should not be expired because the update failed.\n\tsil6, err = s.QueryOne(t.Context(), QIDs(sil6.Id))\n\trequire.NoError(t, err)\n\trequire.Equal(t, SilenceStateActive, getState(sil6, s.nowUTC()))\n\n\t// Should not be able to update with a comment that exceeds maximum size.\n\t// Need to increase the maximum number of silences to test this.\n\ts.limits.MaxSilences = func() int { return 2 }\n\tsil8 := cloneSilence(sil6)\n\tsil8.Comment = strings.Repeat(\"m\", 2<<11)\n\trequire.EqualError(t, s.Set(t.Context(), sil8), fmt.Sprintf(\"silence exceeded maximum size: %d bytes (limit: 4096 bytes)\", proto.Size(s.toMeshSilence(sil8))))\n\n\t// sil6 should not be expired because the update failed.\n\tsil6, err = s.QueryOne(t.Context(), QIDs(sil6.Id))\n\trequire.NoError(t, err)\n\trequire.Equal(t, SilenceStateActive, getState(sil6, s.nowUTC()))\n\n\t// Should not be able to replace with a silence that exceeds maximum size.\n\t// This is different from the previous assertion as unlike when adding or\n\t// updating a comment, changing the matchers for a silence should expire\n\t// the existing silence, unless the silence that is replacing it exceeds\n\t// limits, in which case the operation should fail and the existing silence\n\t// should still be active.\n\tsil9 := cloneSilence(sil8)\n\tsil9.Matchers = []*pb.Matcher{{Name: \"n\", Pattern: \"o\"}}\n\trequire.EqualError(t, s.Set(t.Context(), sil9), fmt.Sprintf(\"silence exceeded maximum size: %d bytes (limit: 4096 bytes)\", proto.Size(s.toMeshSilence(sil9))))\n\n\t// sil6 should not be expired because the update failed.\n\tsil6, err = s.QueryOne(t.Context(), QIDs(sil6.Id))\n\trequire.NoError(t, err)\n\trequire.Equal(t, SilenceStateActive, getState(sil6, s.nowUTC()))\n}\n\nfunc TestSilenceNoLimits(t *testing.T) {\n\ts, err := New(Options{\n\t\tLimits:  Limits{},\n\t\tMetrics: prometheus.NewRegistry(),\n\t})\n\trequire.NoError(t, err)\n\n\t// Insert sil should succeed without error.\n\tsil := &pb.Silence{\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{{Name: \"a\", Pattern: \"b\"}},\n\t\t}},\n\t\tStartsAt: timestamppb.New(time.Now()),\n\t\tEndsAt:   timestamppb.New(time.Now().Add(5 * time.Minute)),\n\t\tComment:  strings.Repeat(\"c\", 2<<9),\n\t}\n\trequire.NoError(t, s.Set(t.Context(), sil))\n\trequire.NotEmpty(t, sil.Id)\n}\n\nfunc TestSetActiveSilence(t *testing.T) {\n\ts, err := New(Options{\n\t\tMetrics:   prometheus.NewRegistry(),\n\t\tRetention: time.Hour,\n\t})\n\trequire.NoError(t, err)\n\n\tclock := quartz.NewMock(t)\n\ts.clock = clock\n\tnow := clock.Now()\n\n\tstartsAt := now.Add(-1 * time.Minute)\n\tendsAt := now.Add(5 * time.Minute)\n\t// Insert silence with fixed start time.\n\tsil1 := &pb.Silence{\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{{Name: \"a\", Pattern: \"b\"}},\n\t\t}},\n\t\tStartsAt: timestamppb.New(startsAt),\n\t\tEndsAt:   timestamppb.New(endsAt),\n\t}\n\trequire.NoError(t, s.Set(t.Context(), sil1))\n\n\t// Update silence with 2 extra nanoseconds so the \"seconds\" part should not change\n\n\tnewStartsAt := now.Add(2 * time.Nanosecond)\n\tnewEndsAt := endsAt.Add(2 * time.Minute)\n\n\tsil2 := cloneSilence(sil1)\n\tsil2.Id = sil1.Id\n\tsil2.StartsAt = timestamppb.New(newStartsAt)\n\tsil2.EndsAt = timestamppb.New(newEndsAt)\n\n\tclock.Advance(time.Minute)\n\tnow = s.nowUTC()\n\trequire.NoError(t, s.Set(t.Context(), sil2))\n\trequire.Equal(t, sil1.Id, sil2.Id)\n\n\twant := state{\n\t\tsil2.Id: &pb.MeshSilence{\n\t\t\tSilence: &pb.Silence{\n\t\t\t\tId: sil1.Id,\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{{Name: \"a\", Pattern: \"b\"}},\n\t\t\t\t}},\n\t\t\t\tStartsAt:  timestamppb.New(newStartsAt),\n\t\t\t\tEndsAt:    timestamppb.New(newEndsAt),\n\t\t\t\tUpdatedAt: timestamppb.New(now),\n\t\t\t},\n\t\t\tExpiresAt: timestamppb.New(newEndsAt.Add(s.retention)),\n\t\t},\n\t}\n\trequireStatesEqual(t, want, s.st, \"unexpected state after silence creation\")\n}\n\nfunc TestSilencesSetFail(t *testing.T) {\n\ts, err := New(Options{Metrics: prometheus.NewRegistry()})\n\trequire.NoError(t, err)\n\n\tclock := quartz.NewMock(t)\n\ts.clock = clock\n\n\tcases := []struct {\n\t\ts   *pb.Silence\n\t\terr string\n\t}{\n\t\t{\n\t\t\ts: &pb.Silence{\n\t\t\t\tId: \"some_id\",\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{{Name: \"a\", Pattern: \"b\"}},\n\t\t\t\t}},\n\t\t\t\tEndsAt: timestamppb.New(clock.Now().Add(5 * time.Minute)),\n\t\t\t},\n\t\t\terr: ErrNotFound.Error(),\n\t\t}, {\n\t\t\ts:   &pb.Silence{}, // Silence without matcher.\n\t\t\terr: \"invalid silence\",\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tcheckErr(t, c.err, s.Set(t.Context(), c.s))\n\t}\n}\n\nfunc TestQState(t *testing.T) {\n\tnow := time.Now().UTC()\n\n\tcases := []struct {\n\t\tsil    *pb.Silence\n\t\tstates []SilenceState\n\t\tkeep   bool\n\t}{\n\t\t{\n\t\t\tsil: &pb.Silence{\n\t\t\t\tStartsAt: timestamppb.New(now.Add(time.Minute)),\n\t\t\t\tEndsAt:   timestamppb.New(now.Add(time.Hour)),\n\t\t\t},\n\t\t\tstates: []SilenceState{SilenceStateActive, SilenceStateExpired},\n\t\t\tkeep:   false,\n\t\t},\n\t\t{\n\t\t\tsil: &pb.Silence{\n\t\t\t\tStartsAt: timestamppb.New(now.Add(time.Minute)),\n\t\t\t\tEndsAt:   timestamppb.New(now.Add(time.Hour)),\n\t\t\t},\n\t\t\tstates: []SilenceState{SilenceStatePending},\n\t\t\tkeep:   true,\n\t\t},\n\t\t{\n\t\t\tsil: &pb.Silence{\n\t\t\t\tStartsAt: timestamppb.New(now.Add(time.Minute)),\n\t\t\t\tEndsAt:   timestamppb.New(now.Add(time.Hour)),\n\t\t\t},\n\t\t\tstates: []SilenceState{SilenceStateExpired, SilenceStatePending},\n\t\t\tkeep:   true,\n\t\t},\n\t}\n\tfor i, c := range cases {\n\t\tq := &query{}\n\t\tQState(c.states...)(q)\n\t\tf := q.filters[0]\n\n\t\tkeep, err := f(c.sil, nil, now)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, c.keep, keep, \"unexpected filter result for case %d\", i)\n\t}\n}\n\nfunc TestQMatches(t *testing.T) {\n\tqp := QMatches(model.LabelSet{\n\t\t\"job\":      \"test\",\n\t\t\"instance\": \"web-1\",\n\t\t\"path\":     \"/user/profile\",\n\t\t\"method\":   \"GET\",\n\t})\n\n\tq := &query{}\n\tqp(q)\n\tf := q.filters[0]\n\n\tcases := []struct {\n\t\tsil  *pb.Silence\n\t\tdrop bool\n\t}{\n\t\t{\n\t\t\tsil: &pb.Silence{\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t{Name: \"job\", Pattern: \"test\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t\tdrop: true,\n\t\t},\n\t\t{\n\t\t\tsil: &pb.Silence{\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t{Name: \"job\", Pattern: \"test\", Type: pb.Matcher_NOT_EQUAL},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t\tdrop: false,\n\t\t},\n\t\t{\n\t\t\tsil: &pb.Silence{\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t{Name: \"job\", Pattern: \"test\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t{Name: \"method\", Pattern: \"POST\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t\tdrop: false,\n\t\t},\n\t\t{\n\t\t\tsil: &pb.Silence{\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t{Name: \"job\", Pattern: \"test\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t{Name: \"method\", Pattern: \"POST\", Type: pb.Matcher_NOT_EQUAL},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t\tdrop: true,\n\t\t},\n\t\t{\n\t\t\tsil: &pb.Silence{\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t{Name: \"path\", Pattern: \"/user/.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t\tdrop: true,\n\t\t},\n\t\t{\n\t\t\tsil: &pb.Silence{\n\t\t\t\tMatcherSets: []*pb.MatcherSet{\n\t\t\t\t\t{\n\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tName: \"path\", Pattern: \"/user/.+\", Type: pb.Matcher_NOT_REGEXP,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tdrop: false,\n\t\t},\n\t\t{\n\t\t\tsil: &pb.Silence{\n\t\t\t\tMatcherSets: []*pb.MatcherSet{\n\t\t\t\t\t{\n\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t{Name: \"path\", Pattern: \"/user/.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t\t{Name: \"path\", Pattern: \"/nothing/.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tdrop: false,\n\t\t},\n\t\t{\n\t\t\tsil: &pb.Silence{\n\t\t\t\tMatcherSets: []*pb.MatcherSet{\n\t\t\t\t\t{\n\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t{Name: \"method\", Pattern: \"GET\", Type: pb.Matcher_NOT_EQUAL},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t{Name: \"method\", Pattern: \"GET|POST\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t\t{Name: \"job\", Pattern: \"test\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tdrop: true,\n\t\t},\n\t\t{\n\t\t\tsil: &pb.Silence{\n\t\t\t\tMatcherSets: []*pb.MatcherSet{\n\t\t\t\t\t{\n\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t{Name: \"method\", Pattern: \"GET\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t{Name: \"method\", Pattern: \"GET|POST\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t\t{Name: \"job\", Pattern: \"test\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tdrop: true,\n\t\t},\n\t\t{\n\t\t\tsil: &pb.Silence{\n\t\t\t\tMatcherSets: []*pb.MatcherSet{\n\t\t\t\t\t{\n\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t{Name: \"method\", Pattern: \"GET\", Type: pb.Matcher_NOT_EQUAL},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t{Name: \"method\", Pattern: \"GET|POST\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t\t{Name: \"job\", Pattern: \"test\", Type: pb.Matcher_NOT_EQUAL},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tdrop: false,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tsilences := &Silences{mi: matcherIndex{}, st: state{}}\n\t\tsilences.mi.add(c.sil)\n\t\tdrop, err := f(c.sil, silences, time.Time{})\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, c.drop, drop, \"unexpected filter result\")\n\t}\n}\n\nfunc TestSilenceBackwardCompatibility(t *testing.T) {\n\tt.Run(\"postprocessUnmarshalledSilence converts old format to new\", func(t *testing.T) {\n\t\t// Create a silence with only the old Matchers field (simulating old format)\n\t\toldSilence := &pb.Silence{\n\t\t\tId: \"test-id\",\n\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t{Name: \"job\", Pattern: \"test\", Type: pb.Matcher_EQUAL},\n\t\t\t\t{Name: \"instance\", Pattern: \"web-1\", Type: pb.Matcher_EQUAL},\n\t\t\t},\n\t\t\tStartsAt: timestamppb.New(time.Now()),\n\t\t\tEndsAt:   timestamppb.New(time.Now().Add(time.Hour)),\n\t\t}\n\n\t\t// Process as if unmarshalled from old version\n\t\tpostprocessUnmarshalledSilence(oldSilence)\n\n\t\t// Verify conversion to MatcherSets\n\t\trequire.Len(t, oldSilence.MatcherSets, 1, \"should have exactly one matcher set\")\n\t\trequire.Len(t, oldSilence.MatcherSets[0].Matchers, 2, \"matcher set should have 2 matchers\")\n\t\trequire.Equal(t, \"job\", oldSilence.MatcherSets[0].Matchers[0].Name)\n\t\trequire.Equal(t, \"test\", oldSilence.MatcherSets[0].Matchers[0].Pattern)\n\t\trequire.Equal(t, \"instance\", oldSilence.MatcherSets[0].Matchers[1].Name)\n\t\trequire.Equal(t, \"web-1\", oldSilence.MatcherSets[0].Matchers[1].Pattern)\n\n\t\t// Verify old Matchers field is cleared\n\t\trequire.Nil(t, oldSilence.Matchers, \"old Matchers field should be cleared\")\n\t})\n\n\tt.Run(\"prepareSilenceForMarshalling populates old format from new\", func(t *testing.T) {\n\t\t// Create a silence with new MatcherSets field\n\t\tnewSilence := &pb.Silence{\n\t\t\tId: \"test-id\",\n\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t{Name: \"job\", Pattern: \"test\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t{Name: \"instance\", Pattern: \"web-1\", Type: pb.Matcher_EQUAL},\n\t\t\t\t},\n\t\t\t}},\n\t\t\tStartsAt: timestamppb.New(time.Now()),\n\t\t\tEndsAt:   timestamppb.New(time.Now().Add(time.Hour)),\n\t\t}\n\n\t\t// Prepare for marshalling (for backward compatibility)\n\t\tprepareSilenceForMarshalling(newSilence)\n\n\t\t// Verify old Matchers field is populated from first matcher set\n\t\trequire.Len(t, newSilence.Matchers, 2, \"old Matchers field should be populated\")\n\t\trequire.Equal(t, \"job\", newSilence.Matchers[0].Name)\n\t\trequire.Equal(t, \"test\", newSilence.Matchers[0].Pattern)\n\t\trequire.Equal(t, \"instance\", newSilence.Matchers[1].Name)\n\t\trequire.Equal(t, \"web-1\", newSilence.Matchers[1].Pattern)\n\n\t\t// Verify MatcherSets is still intact\n\t\trequire.Len(t, newSilence.MatcherSets, 1)\n\t})\n\n\tt.Run(\"round-trip conversion preserves data\", func(t *testing.T) {\n\t\t// Start with new format\n\t\toriginal := &pb.Silence{\n\t\t\tId: \"test-id\",\n\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t{Name: \"job\", Pattern: \"test\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t{Name: \"method\", Pattern: \"GET\", Type: pb.Matcher_REGEXP},\n\t\t\t\t},\n\t\t\t}},\n\t\t\tStartsAt:  timestamppb.New(time.Now().Truncate(time.Second)),\n\t\t\tEndsAt:    timestamppb.New(time.Now().Add(time.Hour).Truncate(time.Second)),\n\t\t\tCreatedBy: \"test-user\",\n\t\t\tComment:   \"test comment\",\n\t\t}\n\n\t\t// Marshal (prepare for backward compatibility)\n\t\tprepareSilenceForMarshalling(original)\n\t\trequire.Len(t, original.Matchers, 2, \"should populate old Matchers field\")\n\n\t\t// Simulate round-trip by creating a new silence with only Matchers field\n\t\t// (as if received from old client)\n\t\treceived := &pb.Silence{\n\t\t\tId:        original.Id,\n\t\t\tMatchers:  original.Matchers,\n\t\t\tStartsAt:  original.StartsAt,\n\t\t\tEndsAt:    original.EndsAt,\n\t\t\tCreatedBy: original.CreatedBy,\n\t\t\tComment:   original.Comment,\n\t\t}\n\n\t\t// Unmarshal (convert to new format)\n\t\tpostprocessUnmarshalledSilence(received)\n\n\t\t// Verify data is preserved\n\t\trequire.Len(t, received.MatcherSets, 1)\n\t\trequire.Len(t, received.MatcherSets[0].Matchers, 2)\n\t\trequire.Equal(t, original.MatcherSets[0].Matchers[0].Name, received.MatcherSets[0].Matchers[0].Name)\n\t\trequire.Equal(t, original.MatcherSets[0].Matchers[0].Pattern, received.MatcherSets[0].Matchers[0].Pattern)\n\t\trequire.Equal(t, original.MatcherSets[0].Matchers[0].Type, received.MatcherSets[0].Matchers[0].Type)\n\t\trequire.Nil(t, received.Matchers, \"old Matchers field should be cleared after postprocess\")\n\t})\n\n\tt.Run(\"postprocess handles empty Matchers gracefully\", func(t *testing.T) {\n\t\t// Silence with no matchers at all\n\t\tsilence := &pb.Silence{\n\t\t\tId:       \"test-id\",\n\t\t\tStartsAt: timestamppb.New(time.Now()),\n\t\t\tEndsAt:   timestamppb.New(time.Now().Add(time.Hour)),\n\t\t}\n\n\t\tpostprocessUnmarshalledSilence(silence)\n\n\t\trequire.Nil(t, silence.Matchers)\n\t\trequire.Nil(t, silence.MatcherSets)\n\t})\n\n\tt.Run(\"postprocess prefers MatcherSets when both fields set\", func(t *testing.T) {\n\t\t// Silence with both old and new fields (can happen during migration)\n\t\tsilence := &pb.Silence{\n\t\t\tId: \"test-id\",\n\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t{Name: \"job\", Pattern: \"old-value\", Type: pb.Matcher_EQUAL},\n\t\t\t},\n\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t{Name: \"job\", Pattern: \"new-value\", Type: pb.Matcher_EQUAL},\n\t\t\t\t},\n\t\t\t}},\n\t\t\tStartsAt: timestamppb.New(time.Now()),\n\t\t\tEndsAt:   timestamppb.New(time.Now().Add(time.Hour)),\n\t\t}\n\n\t\tpostprocessUnmarshalledSilence(silence)\n\n\t\t// MatcherSets field should be preserved when already set\n\t\trequire.Len(t, silence.MatcherSets, 1)\n\t\trequire.Equal(t, \"new-value\", silence.MatcherSets[0].Matchers[0].Pattern)\n\t\trequire.Nil(t, silence.Matchers)\n\t})\n\n\tt.Run(\"multi-matcher silence backward compat populates only first set\", func(t *testing.T) {\n\t\t// Create a silence with multiple matcher sets\n\t\tmultiSilence := &pb.Silence{\n\t\t\tId: \"test-id\",\n\t\t\tMatcherSets: []*pb.MatcherSet{\n\t\t\t\t{\n\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t{Name: \"job\", Pattern: \"test\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t{Name: \"method\", Pattern: \"GET\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tStartsAt: timestamppb.New(time.Now()),\n\t\t\tEndsAt:   timestamppb.New(time.Now().Add(time.Hour)),\n\t\t}\n\n\t\t// Prepare for marshalling\n\t\tprepareSilenceForMarshalling(multiSilence)\n\n\t\t// Only first matcher set should be in old Matchers field\n\t\trequire.Len(t, multiSilence.Matchers, 1, \"should only populate first matcher set\")\n\t\trequire.Equal(t, \"job\", multiSilence.Matchers[0].Name)\n\t\trequire.Equal(t, \"test\", multiSilence.Matchers[0].Pattern)\n\n\t\t// All matcher sets should still be intact\n\t\trequire.Len(t, multiSilence.MatcherSets, 2)\n\t})\n}\n\nfunc TestStateUnmarshalling(t *testing.T) {\n\t// test that we can decode silences with the old format (without MatcherSets field)\n\tnow := time.Now().UTC()\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tsilence  *pb.MeshSilence\n\t\texpected *pb.MeshSilence\n\t}{\n\t\t{\n\t\t\tname: \"empty silence\",\n\t\t\tsilence: &pb.MeshSilence{\n\t\t\t\tSilence: &pb.Silence{\n\t\t\t\t\tId: \"silence1\",\n\t\t\t\t},\n\t\t\t\tExpiresAt: timestamppb.New(now.Add(time.Hour)),\n\t\t\t},\n\t\t\texpected: &pb.MeshSilence{\n\t\t\t\tSilence: &pb.Silence{\n\t\t\t\t\tId: \"silence1\",\n\t\t\t\t},\n\t\t\t\tExpiresAt: timestamppb.New(now.Add(time.Hour)),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"silence with matcher sets\",\n\t\t\tsilence: &pb.MeshSilence{\n\t\t\t\tSilence: &pb.Silence{\n\t\t\t\t\tId: \"silence1\",\n\t\t\t\t\tMatcherSets: []*pb.MatcherSet{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val1\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t\t\t{Name: \"label2\", Pattern: \"val.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tExpiresAt: timestamppb.New(now.Add(time.Hour)),\n\t\t\t},\n\t\t\texpected: &pb.MeshSilence{\n\t\t\t\tSilence: &pb.Silence{\n\t\t\t\t\tId: \"silence1\",\n\t\t\t\t\tMatcherSets: []*pb.MatcherSet{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val1\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t\t\t{Name: \"label2\", Pattern: \"val.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tExpiresAt: timestamppb.New(now.Add(time.Hour)),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"silence with multiple matcher sets\",\n\t\t\tsilence: &pb.MeshSilence{\n\t\t\t\tSilence: &pb.Silence{\n\t\t\t\t\tId: \"silence1\",\n\t\t\t\t\tMatcherSets: []*pb.MatcherSet{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val1\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t\t\t{Name: \"label2\", Pattern: \"val.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val2\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t\t\t{Name: \"label2\", Pattern: \"val2.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tExpiresAt: timestamppb.New(now.Add(time.Hour)),\n\t\t\t},\n\t\t\texpected: &pb.MeshSilence{\n\t\t\t\tSilence: &pb.Silence{\n\t\t\t\t\tId: \"silence1\",\n\t\t\t\t\tMatcherSets: []*pb.MatcherSet{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val1\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t\t\t{Name: \"label2\", Pattern: \"val.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val2\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t\t\t{Name: \"label2\", Pattern: \"val2.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tExpiresAt: timestamppb.New(now.Add(time.Hour)),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"silence with both classic matchers and matcher sets\",\n\t\t\tsilence: &pb.MeshSilence{\n\t\t\t\tSilence: &pb.Silence{\n\t\t\t\t\tId: \"silence1\",\n\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val1\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t{Name: \"label2\", Pattern: \"val.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t},\n\t\t\t\t\tMatcherSets: []*pb.MatcherSet{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val1\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t\t\t{Name: \"label2\", Pattern: \"val.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val2\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t\t\t{Name: \"label2\", Pattern: \"val2.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tExpiresAt: timestamppb.New(now.Add(time.Hour)),\n\t\t\t},\n\t\t\texpected: &pb.MeshSilence{\n\t\t\t\tSilence: &pb.Silence{\n\t\t\t\t\tId: \"silence1\",\n\t\t\t\t\tMatcherSets: []*pb.MatcherSet{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val1\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t\t\t{Name: \"label2\", Pattern: \"val.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val2\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t\t\t{Name: \"label2\", Pattern: \"val2.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tExpiresAt: timestamppb.New(now.Add(time.Hour)),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"silence with classic matchers\",\n\t\t\tsilence: &pb.MeshSilence{\n\t\t\t\tSilence: &pb.Silence{\n\t\t\t\t\tId: \"silence1\",\n\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val1\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t{Name: \"label2\", Pattern: \"val.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tExpiresAt: timestamppb.New(now.Add(time.Hour)),\n\t\t\t},\n\t\t\texpected: &pb.MeshSilence{\n\t\t\t\tSilence: &pb.Silence{\n\t\t\t\t\tId: \"silence1\",\n\t\t\t\t\tMatcherSets: []*pb.MatcherSet{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val1\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t\t\t{Name: \"label2\", Pattern: \"val.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tExpiresAt: timestamppb.New(now.Add(time.Hour)),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Marshal the silence to binary format\n\t\t\tin := state{\n\t\t\t\ttt.silence.Silence.Id: tt.silence,\n\t\t\t}\n\n\t\t\tmsg, err := in.MarshalBinary()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tdecoded, err := decodeState(bytes.NewReader(msg))\n\t\t\trequire.NoError(t, err, \"decoding message failed\")\n\n\t\t\trequire.True(t, proto.Equal(tt.expected, decoded[tt.silence.Silence.Id]), \"decoded data doesn't match encoded data\")\n\t\t})\n\t}\n}\n\nfunc TestQSince(t *testing.T) {\n\ttype testCase struct {\n\t\tindex versionIndex\n\n\t\tsince   int\n\t\tresults []string\n\t}\n\n\tcases := map[string]testCase{\n\t\t\"skips current version\": {\n\t\t\tindex: versionIndex{\n\t\t\t\t{id: \"1\", version: 1},\n\t\t\t\t{id: \"2\", version: 2},\n\t\t\t},\n\n\t\t\tsince:   1,\n\t\t\tresults: []string{\"2\"},\n\t\t},\n\t\t\"skips any number of old versions\": {\n\t\t\tindex: versionIndex{\n\t\t\t\t{id: \"1\", version: 1},\n\t\t\t\t{id: \"2\", version: 2},\n\t\t\t\t{id: \"3\", version: 2},\n\t\t\t\t{id: \"4\", version: 3},\n\t\t\t\t{id: \"5\", version: 4},\n\t\t\t},\n\n\t\t\tsince:   3,\n\t\t\tresults: []string{\"5\"},\n\t\t},\n\t\t\"since 0 returns everything\": {\n\t\t\tindex: versionIndex{\n\t\t\t\t{id: \"1\", version: 1},\n\t\t\t\t{id: \"2\", version: 2},\n\t\t\t},\n\n\t\t\tsince:   0,\n\t\t\tresults: []string{\"1\", \"2\"},\n\t\t},\n\t\t\"returns all elements of a group with the same version\": {\n\t\t\tindex: versionIndex{\n\t\t\t\t{id: \"1\", version: 1},\n\t\t\t\t{id: \"2\", version: 2},\n\t\t\t\t{id: \"3\", version: 3},\n\t\t\t\t{id: \"4\", version: 3},\n\t\t\t},\n\n\t\t\tsince:   2,\n\t\t\tresults: []string{\"3\", \"4\"},\n\t\t},\n\t\t\"returns everything after the provided version\": {\n\t\t\tindex: versionIndex{\n\t\t\t\t{id: \"1\", version: 1},\n\t\t\t\t{id: \"2\", version: 2},\n\t\t\t\t{id: \"3\", version: 3},\n\t\t\t\t{id: \"4\", version: 3},\n\t\t\t\t{id: \"5\", version: 4},\n\t\t\t\t{id: \"6\", version: 5},\n\t\t\t},\n\n\t\t\tsince:   2,\n\t\t\tresults: []string{\"3\", \"4\", \"5\", \"6\"},\n\t\t},\n\t}\n\n\tfor name, c := range cases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tsilences, err := New(Options{Metrics: prometheus.NewRegistry()})\n\t\t\trequire.NoError(t, err)\n\t\t\t// build state from index so test cases are easier to write\n\t\t\tst := state{}\n\t\t\tfor _, mapping := range c.index {\n\t\t\t\tst[mapping.id] = &pb.MeshSilence{Silence: &pb.Silence{Id: mapping.id}}\n\t\t\t}\n\t\t\tsilences.st = st\n\t\t\tsilences.vi = c.index\n\n\t\t\tres, _, err := silences.Query(t.Context(), QSince(c.since))\n\t\t\trequire.NoError(t, err)\n\t\t\tresultIds := []string{}\n\t\t\tfor _, sil := range res {\n\t\t\t\tresultIds = append(resultIds, sil.Id)\n\t\t\t}\n\n\t\t\tsort.StringSlice(c.results).Sort()\n\t\t\tsort.StringSlice(resultIds).Sort()\n\n\t\t\trequire.Equal(t, c.results, resultIds)\n\t\t})\n\t}\n}\n\nfunc TestSilencesQuery(t *testing.T) {\n\ts, err := New(Options{Metrics: prometheus.NewRegistry()})\n\trequire.NoError(t, err)\n\n\ts.st = state{\n\t\t\"1\": &pb.MeshSilence{Silence: &pb.Silence{Id: \"1\"}},\n\t\t\"2\": &pb.MeshSilence{Silence: &pb.Silence{Id: \"2\"}},\n\t\t\"3\": &pb.MeshSilence{Silence: &pb.Silence{Id: \"3\"}},\n\t\t\"4\": &pb.MeshSilence{Silence: &pb.Silence{Id: \"4\"}},\n\t\t\"5\": &pb.MeshSilence{Silence: &pb.Silence{Id: \"5\"}},\n\t}\n\ts.vi = versionIndex{\n\t\t{id: \"1\"},\n\t\t{id: \"2\"},\n\t\t{id: \"3\"},\n\t\t{id: \"4\"},\n\t\t{id: \"5\"},\n\t}\n\tcases := []struct {\n\t\tq   *query\n\t\texp []*pb.Silence\n\t}{\n\t\t{\n\t\t\t// Default query of retrieving all silences.\n\t\t\tq: &query{},\n\t\t\texp: []*pb.Silence{\n\t\t\t\t{Id: \"1\"},\n\t\t\t\t{Id: \"2\"},\n\t\t\t\t{Id: \"3\"},\n\t\t\t\t{Id: \"4\"},\n\t\t\t\t{Id: \"5\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// Retrieve by IDs.\n\t\t\tq: &query{\n\t\t\t\tids: []string{\"2\", \"5\"},\n\t\t\t},\n\t\t\texp: []*pb.Silence{\n\t\t\t\t{Id: \"2\"},\n\t\t\t\t{Id: \"5\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// Retrieve all and filter\n\t\t\tq: &query{\n\t\t\t\tfilters: []silenceFilter{\n\t\t\t\t\tfunc(sil *pb.Silence, _ *Silences, _ time.Time) (bool, error) {\n\t\t\t\t\t\treturn sil.Id == \"1\" || sil.Id == \"2\", nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: []*pb.Silence{\n\t\t\t\t{Id: \"1\"},\n\t\t\t\t{Id: \"2\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// Retrieve by IDs and filter\n\t\t\tq: &query{\n\t\t\t\tids: []string{\"2\", \"5\"},\n\t\t\t\tfilters: []silenceFilter{\n\t\t\t\t\tfunc(sil *pb.Silence, _ *Silences, _ time.Time) (bool, error) {\n\t\t\t\t\t\treturn sil.Id == \"1\" || sil.Id == \"2\", nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: []*pb.Silence{\n\t\t\t\t{Id: \"2\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\t// Run default query of retrieving all silences.\n\t\tres, _, err := s.query(c.q, time.Time{})\n\t\trequire.NoError(t, err, \"unexpected error on querying\")\n\n\t\t// Currently there are no sorting guarantees in the querying API.\n\t\tsort.Sort(silencesByID(c.exp))\n\t\tsort.Sort(silencesByID(res))\n\t\tfor i := range c.exp {\n\t\t\trequire.True(t, proto.Equal(c.exp[i], res[i]), \"unexpected silence in result\")\n\t\t}\n\t}\n}\n\nfunc TestQIDs(t *testing.T) {\n\ts, err := New(Options{Metrics: prometheus.NewRegistry()})\n\trequire.NoError(t, err)\n\n\ts.st = state{\n\t\t\"1\": &pb.MeshSilence{Silence: &pb.Silence{Id: \"1\"}},\n\t\t\"2\": &pb.MeshSilence{Silence: &pb.Silence{Id: \"2\"}},\n\t\t\"3\": &pb.MeshSilence{Silence: &pb.Silence{Id: \"3\"}},\n\t\t\"4\": &pb.MeshSilence{Silence: &pb.Silence{Id: \"4\"}},\n\t}\n\n\t// Test QIDs with empty arguments returns an error\n\t_, _, err = s.Query(t.Context(), QIDs())\n\trequire.Error(t, err, \"expected error when QIDs is called with no arguments\")\n\trequire.Contains(t, err.Error(), \"QIDs filter must have at least one id\")\n\n\t// Test QIDs with empty arguments returns an error via QueryOne\n\t_, err = s.QueryOne(t.Context(), QIDs())\n\trequire.Error(t, err, \"expected error when QIDs is called with no arguments\")\n\trequire.Contains(t, err.Error(), \"QIDs filter must have at least one id\")\n\n\t// Test QIDs with single ID works\n\tres, _, err := s.Query(t.Context(), QIDs(\"1\"))\n\trequire.NoError(t, err)\n\trequire.Len(t, res, 1)\n\trequire.Equal(t, \"1\", res[0].Id)\n\n\t// Test QIDs with multiple IDs works\n\tres, _, err = s.Query(t.Context(), QIDs(\"1\", \"2\"))\n\trequire.NoError(t, err)\n\trequire.Len(t, res, 2)\n\n\t// Test QueryOne with single ID works\n\tsil, err := s.QueryOne(t.Context(), QIDs(\"1\"))\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"1\", sil.Id)\n}\n\ntype silencesByID []*pb.Silence\n\nfunc (s silencesByID) Len() int           { return len(s) }\nfunc (s silencesByID) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }\nfunc (s silencesByID) Less(i, j int) bool { return s[i].Id < s[j].Id }\n\nfunc TestSilenceCanUpdate(t *testing.T) {\n\tnow := time.Now().UTC()\n\n\tcases := []struct {\n\t\ta, b *pb.Silence\n\t\tok   bool\n\t}{\n\t\t// Bad arguments.\n\t\t{\n\t\t\ta: &pb.Silence{},\n\t\t\tb: &pb.Silence{\n\t\t\t\tStartsAt: timestamppb.New(now),\n\t\t\t\tEndsAt:   timestamppb.New(now.Add(-time.Minute)),\n\t\t\t},\n\t\t\tok: false,\n\t\t},\n\t\t// Expired silence.\n\t\t{\n\t\t\ta: &pb.Silence{\n\t\t\t\tStartsAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t\t\tEndsAt:   timestamppb.New(now.Add(-time.Second)),\n\t\t\t},\n\t\t\tb: &pb.Silence{\n\t\t\t\tStartsAt: timestamppb.New(now),\n\t\t\t\tEndsAt:   timestamppb.New(now),\n\t\t\t},\n\t\t\tok: false,\n\t\t},\n\t\t// Pending silences.\n\t\t{\n\t\t\ta: &pb.Silence{\n\t\t\t\tStartsAt:  timestamppb.New(now.Add(time.Hour)),\n\t\t\t\tEndsAt:    timestamppb.New(now.Add(2 * time.Hour)),\n\t\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t\t},\n\t\t\tb: &pb.Silence{\n\t\t\t\tStartsAt: timestamppb.New(now.Add(-time.Minute)),\n\t\t\t\tEndsAt:   timestamppb.New(now.Add(time.Hour)),\n\t\t\t},\n\t\t\tok: false,\n\t\t},\n\t\t{\n\t\t\ta: &pb.Silence{\n\t\t\t\tStartsAt:  timestamppb.New(now.Add(time.Hour)),\n\t\t\t\tEndsAt:    timestamppb.New(now.Add(2 * time.Hour)),\n\t\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t\t},\n\t\t\tb: &pb.Silence{\n\t\t\t\tStartsAt: timestamppb.New(now.Add(time.Minute)),\n\t\t\t\tEndsAt:   timestamppb.New(now.Add(time.Minute)),\n\t\t\t},\n\t\t\tok: true,\n\t\t},\n\t\t{\n\t\t\ta: &pb.Silence{\n\t\t\t\tStartsAt:  timestamppb.New(now.Add(time.Hour)),\n\t\t\t\tEndsAt:    timestamppb.New(now.Add(2 * time.Hour)),\n\t\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t\t},\n\t\t\tb: &pb.Silence{\n\t\t\t\tStartsAt: timestamppb.New(now), // set to exactly start now.\n\t\t\t\tEndsAt:   timestamppb.New(now.Add(2 * time.Hour)),\n\t\t\t},\n\t\t\tok: true,\n\t\t},\n\t\t// Active silences.\n\t\t{\n\t\t\ta: &pb.Silence{\n\t\t\t\tStartsAt:  timestamppb.New(now.Add(-time.Hour)),\n\t\t\t\tEndsAt:    timestamppb.New(now.Add(2 * time.Hour)),\n\t\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t\t},\n\t\t\tb: &pb.Silence{\n\t\t\t\tStartsAt: timestamppb.New(now.Add(-time.Minute)),\n\t\t\t\tEndsAt:   timestamppb.New(now.Add(2 * time.Hour)),\n\t\t\t},\n\t\t\tok: false,\n\t\t},\n\t\t{\n\t\t\ta: &pb.Silence{\n\t\t\t\tStartsAt:  timestamppb.New(now.Add(-time.Hour)),\n\t\t\t\tEndsAt:    timestamppb.New(now.Add(2 * time.Hour)),\n\t\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t\t},\n\t\t\tb: &pb.Silence{\n\t\t\t\tStartsAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t\t\tEndsAt:   timestamppb.New(now.Add(-time.Second)),\n\t\t\t},\n\t\t\tok: false,\n\t\t},\n\t\t{\n\t\t\ta: &pb.Silence{\n\t\t\t\tStartsAt:  timestamppb.New(now.Add(-time.Hour)),\n\t\t\t\tEndsAt:    timestamppb.New(now.Add(2 * time.Hour)),\n\t\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t\t},\n\t\t\tb: &pb.Silence{\n\t\t\t\tStartsAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t\t\tEndsAt:   timestamppb.New(now),\n\t\t\t},\n\t\t\tok: true,\n\t\t},\n\t\t{\n\t\t\ta: &pb.Silence{\n\t\t\t\tStartsAt:  timestamppb.New(now.Add(-time.Hour)),\n\t\t\t\tEndsAt:    timestamppb.New(now.Add(2 * time.Hour)),\n\t\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t\t},\n\t\t\tb: &pb.Silence{\n\t\t\t\tStartsAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t\t\tEndsAt:   timestamppb.New(now.Add(3 * time.Hour)),\n\t\t\t},\n\t\t\tok: true,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tok := canUpdate(c.a, c.b, now)\n\t\tif ok && !c.ok {\n\t\t\tt.Errorf(\"expected not-updateable but was: %v, %v\", c.a, c.b)\n\t\t}\n\t\tif ok && !c.ok {\n\t\t\tt.Errorf(\"expected updateable but was not: %v, %v\", c.a, c.b)\n\t\t}\n\t}\n}\n\nfunc TestSilenceExpire(t *testing.T) {\n\ts, err := New(Options{Metrics: prometheus.NewRegistry(), Retention: time.Hour})\n\trequire.NoError(t, err)\n\n\tclock := quartz.NewMock(t)\n\ts.clock = clock\n\tnow := s.nowUTC()\n\n\tm := &pb.Matcher{Type: pb.Matcher_EQUAL, Name: \"a\", Pattern: \"b\"}\n\n\ts.st = state{\n\t\t\"pending\": &pb.MeshSilence{Silence: &pb.Silence{\n\t\t\tId: \"pending\",\n\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\tMatchers: []*pb.Matcher{m},\n\t\t\t}},\n\t\t\tStartsAt:  timestamppb.New(now.Add(time.Minute)),\n\t\t\tEndsAt:    timestamppb.New(now.Add(time.Hour)),\n\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t}},\n\t\t\"active\": &pb.MeshSilence{Silence: &pb.Silence{\n\t\t\tId: \"active\",\n\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\tMatchers: []*pb.Matcher{m},\n\t\t\t}},\n\t\t\tStartsAt:  timestamppb.New(now.Add(-time.Minute)),\n\t\t\tEndsAt:    timestamppb.New(now.Add(time.Hour)),\n\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t}},\n\t\t\"expired\": &pb.MeshSilence{Silence: &pb.Silence{\n\t\t\tId: \"expired\",\n\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\tMatchers: []*pb.Matcher{m},\n\t\t\t}},\n\t\t\tStartsAt:  timestamppb.New(now.Add(-time.Hour)),\n\t\t\tEndsAt:    timestamppb.New(now.Add(-time.Minute)),\n\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t}},\n\t}\n\ts.vi = versionIndex{\n\t\tsilenceVersion{id: \"pending\"},\n\t\tsilenceVersion{id: \"active\"},\n\t\tsilenceVersion{id: \"expired\"},\n\t}\n\tcount, err := s.CountState(t.Context(), SilenceStatePending)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, count)\n\n\tcount, err = s.CountState(t.Context(), SilenceStateExpired)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, count)\n\n\trequire.NoError(t, s.Expire(t.Context(), \"pending\"))\n\trequire.NoError(t, s.Expire(t.Context(), \"active\"))\n\n\trequire.NoError(t, s.Expire(t.Context(), \"expired\"))\n\n\tsil, err := s.QueryOne(t.Context(), QIDs(\"pending\"))\n\trequire.NoError(t, err)\n\texpectedPending := &pb.Silence{\n\t\tId: \"pending\",\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{m},\n\t\t}},\n\t\tStartsAt:  timestamppb.New(now),\n\t\tEndsAt:    timestamppb.New(now),\n\t\tUpdatedAt: timestamppb.New(now),\n\t}\n\trequire.True(t, proto.Equal(expectedPending, sil), \"pending silence mismatch\")\n\n\t// Let time pass...\n\tclock.Advance(time.Second)\n\n\tcount, err = s.CountState(t.Context(), SilenceStatePending)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 0, count)\n\n\tcount, err = s.CountState(t.Context(), SilenceStateExpired)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 3, count)\n\n\t// Expiring a pending Silence should make the API return the\n\t// SilenceStateExpired Silence state.\n\tsilenceState := CurrentState(sil.StartsAt.AsTime(), sil.EndsAt.AsTime())\n\trequire.Equal(t, SilenceStateExpired, silenceState)\n\n\tsil, err = s.QueryOne(t.Context(), QIDs(\"active\"))\n\trequire.NoError(t, err)\n\texpectedActive := &pb.Silence{\n\t\tId: \"active\",\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{m},\n\t\t}},\n\t\tStartsAt:  timestamppb.New(now.Add(-time.Minute)),\n\t\tEndsAt:    timestamppb.New(now),\n\t\tUpdatedAt: timestamppb.New(now),\n\t}\n\trequire.True(t, proto.Equal(expectedActive, sil), \"active silence mismatch\")\n\n\tsil, err = s.QueryOne(t.Context(), QIDs(\"expired\"))\n\trequire.NoError(t, err)\n\texpectedExpired := &pb.Silence{\n\t\tId: \"expired\",\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{m},\n\t\t}},\n\t\tStartsAt:  timestamppb.New(now.Add(-time.Hour)),\n\t\tEndsAt:    timestamppb.New(now.Add(-time.Minute)),\n\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t}\n\trequire.True(t, proto.Equal(expectedExpired, sil), \"expired silence mismatch\")\n}\n\n// TestSilenceExpireWithZeroRetention covers the problem that, with zero\n// retention time, a silence explicitly set to expired will also immediately\n// expire from the silence storage.\nfunc TestSilenceExpireWithZeroRetention(t *testing.T) {\n\ts, err := New(Options{Metrics: prometheus.NewRegistry(), Retention: 0})\n\trequire.NoError(t, err)\n\n\tclock := quartz.NewMock(t)\n\ts.clock = clock\n\tnow := s.nowUTC()\n\n\tm := &pb.Matcher{Type: pb.Matcher_EQUAL, Name: \"a\", Pattern: \"b\"}\n\n\ts.st = state{\n\t\t\"pending\": &pb.MeshSilence{Silence: &pb.Silence{\n\t\t\tId: \"pending\",\n\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\tMatchers: []*pb.Matcher{m},\n\t\t\t}},\n\t\t\tStartsAt:  timestamppb.New(now.Add(time.Minute)),\n\t\t\tEndsAt:    timestamppb.New(now.Add(time.Hour)),\n\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t}},\n\t\t\"active\": &pb.MeshSilence{Silence: &pb.Silence{\n\t\t\tId: \"active\",\n\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\tMatchers: []*pb.Matcher{m},\n\t\t\t}},\n\t\t\tStartsAt:  timestamppb.New(now.Add(-time.Minute)),\n\t\t\tEndsAt:    timestamppb.New(now.Add(time.Hour)),\n\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t}},\n\t\t\"expired\": &pb.MeshSilence{Silence: &pb.Silence{\n\t\t\tId: \"expired\",\n\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\tMatchers: []*pb.Matcher{m},\n\t\t\t}},\n\t\t\tStartsAt:  timestamppb.New(now.Add(-time.Hour)),\n\t\t\tEndsAt:    timestamppb.New(now.Add(-time.Minute)),\n\t\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t\t}},\n\t}\n\ts.vi = versionIndex{\n\t\tsilenceVersion{id: \"pending\"},\n\t\tsilenceVersion{id: \"active\"},\n\t\tsilenceVersion{id: \"expired\"},\n\t}\n\n\tcount, err := s.CountState(t.Context(), SilenceStatePending)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, count)\n\n\tcount, err = s.CountState(t.Context(), SilenceStateActive)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, count)\n\n\tcount, err = s.CountState(t.Context(), SilenceStateExpired)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, count)\n\n\t// Advance time. The silence state management code uses update time when\n\t// merging, and the logic is \"first write wins\". So we must advance the clock\n\t// one tick for updates to take effect.\n\tclock.Advance(1 * time.Millisecond)\n\n\trequire.NoError(t, s.Expire(t.Context(), \"pending\"))\n\trequire.NoError(t, s.Expire(t.Context(), \"active\"))\n\trequire.NoError(t, s.Expire(t.Context(), \"expired\"))\n\n\t// Advance time again. Despite what the function name says, s.Expire() does\n\t// not expire a silence. It sets the silence to EndAt the current time. This\n\t// means that the silence is active immediately after calling Expire.\n\tclock.Advance(1 * time.Millisecond)\n\n\t// Verify all silences have expired.\n\tcount, err = s.CountState(t.Context(), SilenceStatePending)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 0, count)\n\n\tcount, err = s.CountState(t.Context(), SilenceStateActive)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 0, count)\n\n\tcount, err = s.CountState(t.Context(), SilenceStateExpired)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 3, count)\n}\n\n// This test checks that invalid silences can be expired.\nfunc TestSilenceExpireInvalid(t *testing.T) {\n\ts, err := New(Options{Metrics: prometheus.NewRegistry(), Retention: time.Hour})\n\trequire.NoError(t, err)\n\n\tclock := quartz.NewMock(t)\n\ts.clock = clock\n\tnow := s.nowUTC()\n\n\t// In this test the matcher has an invalid type.\n\tsilence := pb.Silence{\n\t\tId: \"active\",\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{{Type: -1, Name: \"a\", Pattern: \"b\"}},\n\t\t}},\n\t\tStartsAt:  timestamppb.New(now.Add(-time.Minute)),\n\t\tEndsAt:    timestamppb.New(now.Add(time.Hour)),\n\t\tUpdatedAt: timestamppb.New(now.Add(-time.Hour)),\n\t}\n\t// Assert that this silence is invalid.\n\trequire.EqualError(t, validateSilence(&silence), \"invalid label matcher 0 in set 0: unknown matcher type \\\"-1\\\"\")\n\n\ts.st = state{\"active\": &pb.MeshSilence{Silence: &silence}}\n\ts.vi = versionIndex{silenceVersion{id: \"active\"}}\n\n\t// The silence should be active.\n\tcount, err := s.CountState(t.Context(), SilenceStateActive)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, count)\n\n\tclock.Advance(time.Millisecond)\n\trequire.NoError(t, s.Expire(t.Context(), \"active\"))\n\tclock.Advance(time.Millisecond)\n\n\t// The silence should be expired.\n\tcount, err = s.CountState(t.Context(), SilenceStateActive)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 0, count)\n\tcount, err = s.CountState(t.Context(), SilenceStateExpired)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, count)\n}\n\nfunc TestSilencer(t *testing.T) {\n\tss, err := New(Options{Metrics: prometheus.NewRegistry(), Retention: time.Hour})\n\trequire.NoError(t, err)\n\n\tclock := quartz.NewMock(t)\n\tss.clock = clock\n\tnow := ss.nowUTC()\n\n\tm := types.NewMarker(prometheus.NewRegistry())\n\ts := NewSilencer(ss, m, promslog.NewNopLogger())\n\n\trequire.False(t, s.Mutes(t.Context(), model.LabelSet{\"foo\": \"bar\"}), \"expected alert not silenced without any silences\")\n\n\tsil1 := &pb.Silence{\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{{Name: \"foo\", Pattern: \"baz\"}},\n\t\t}},\n\t\tStartsAt: timestamppb.New(now.Add(-time.Hour)),\n\t\tEndsAt:   timestamppb.New(now.Add(5 * time.Minute)),\n\t}\n\trequire.NoError(t, ss.Set(t.Context(), sil1))\n\n\trequire.False(t, s.Mutes(t.Context(), model.LabelSet{\"foo\": \"bar\"}), \"expected alert not silenced by non-matching silence\")\n\n\tsil2 := &pb.Silence{\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{{Name: \"foo\", Pattern: \"bar\"}},\n\t\t}},\n\t\tStartsAt: timestamppb.New(now.Add(-time.Hour)),\n\t\tEndsAt:   timestamppb.New(now.Add(5 * time.Minute)),\n\t}\n\trequire.NoError(t, ss.Set(t.Context(), sil2))\n\trequire.NotEmpty(t, sil2.Id)\n\n\trequire.True(t, s.Mutes(t.Context(), model.LabelSet{\"foo\": \"bar\"}), \"expected alert silenced by matching silence\")\n\n\t// One hour passes, silence expires.\n\tclock.Advance(time.Hour)\n\tnow = ss.nowUTC()\n\n\trequire.False(t, s.Mutes(t.Context(), model.LabelSet{\"foo\": \"bar\"}), \"expected alert not silenced by expired silence\")\n\n\t// Update silence to start in the future.\n\terr = ss.Set(t.Context(), &pb.Silence{\n\t\tId: sil2.Id,\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{{Name: \"foo\", Pattern: \"bar\"}},\n\t\t}},\n\t\tStartsAt: timestamppb.New(now.Add(time.Hour)),\n\t\tEndsAt:   timestamppb.New(now.Add(3 * time.Hour)),\n\t})\n\trequire.NoError(t, err)\n\n\trequire.False(t, s.Mutes(t.Context(), model.LabelSet{\"foo\": \"bar\"}), \"expected alert not silenced by future silence\")\n\n\t// Two hours pass, silence becomes active.\n\tclock.Advance(2 * time.Hour)\n\tnow = ss.nowUTC()\n\n\t// Exposes issue #2426.\n\trequire.True(t, s.Mutes(t.Context(), model.LabelSet{\"foo\": \"bar\"}), \"expected alert silenced by activated silence\")\n\n\terr = ss.Set(t.Context(), &pb.Silence{\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{{Name: \"foo\", Pattern: \"b..\", Type: pb.Matcher_REGEXP}},\n\t\t}},\n\t\tStartsAt: timestamppb.New(now.Add(time.Hour)),\n\t\tEndsAt:   timestamppb.New(now.Add(3 * time.Hour)),\n\t})\n\trequire.NoError(t, err)\n\n\t// Note that issue #2426 doesn't apply anymore because we added a new silence.\n\trequire.True(t, s.Mutes(t.Context(), model.LabelSet{\"foo\": \"bar\"}), \"expected alert still silenced by activated silence\")\n\n\t// Two hours pass, first silence expires, overlapping second silence becomes active.\n\tclock.Advance(2 * time.Hour)\n\n\t// Another variant of issue #2426 (overlapping silences).\n\trequire.True(t, s.Mutes(t.Context(), model.LabelSet{\"foo\": \"bar\"}), \"expected alert silenced by activated second silence\")\n}\n\nfunc TestSilencerPostDeleteEvictsCache(t *testing.T) {\n\tss, err := New(Options{Metrics: prometheus.NewRegistry(), Retention: time.Hour})\n\trequire.NoError(t, err)\n\n\tclock := quartz.NewMock(t)\n\tss.clock = clock\n\tnow := ss.nowUTC()\n\n\tm := types.NewMarker(prometheus.NewRegistry())\n\ts := NewSilencer(ss, m, promslog.NewNopLogger())\n\n\tlset := model.LabelSet{\"foo\": \"bar\"}\n\tfp := lset.Fingerprint()\n\n\t// Create a matching silence.\n\tsil := &pb.Silence{\n\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\tMatchers: []*pb.Matcher{{Name: \"foo\", Pattern: \"bar\"}},\n\t\t}},\n\t\tStartsAt: timestamppb.New(now.Add(-time.Hour)),\n\t\tEndsAt:   timestamppb.New(now.Add(5 * time.Minute)),\n\t}\n\trequire.NoError(t, ss.Set(t.Context(), sil))\n\n\t// Mutes populates the cache.\n\trequire.True(t, s.Mutes(t.Context(), lset))\n\tentry := s.cache.get(fp)\n\trequire.Positive(t, entry.count(), \"cache should have entries after Mutes()\")\n\n\t// PostGC evicts the cache entry for this fingerprint.\n\ts.PostGC(model.Fingerprints{fp})\n\tentry = s.cache.get(fp)\n\trequire.Equal(t, 0, entry.count(), \"cache should be empty after PostGC()\")\n\trequire.Equal(t, 0, entry.version, \"version should be zero for evicted entry\")\n\n\t// Mutes re-evaluates from scratch (cache miss) and still finds the silence.\n\trequire.True(t, s.Mutes(t.Context(), lset), \"expected alert still silenced after cache eviction\")\n\tentry = s.cache.get(fp)\n\trequire.Positive(t, entry.count(), \"cache should be repopulated after Mutes()\")\n\n\t// Expire the silence, advance time so it's truly expired.\n\tclock.Advance(time.Hour)\n\n\t// PostGC for a different fingerprint should not affect this entry.\n\totherLset := model.LabelSet{\"other\": \"alert\"}\n\ts.PostGC(model.Fingerprints{otherLset.Fingerprint()})\n\tentry = s.cache.get(fp)\n\trequire.Positive(t, entry.count(), \"unrelated PostGC should not evict other entries\")\n}\n\nfunc TestValidateClassicMatcher(t *testing.T) {\n\tcases := []struct {\n\t\tm   *pb.Matcher\n\t\terr string\n\t}{\n\t\t{\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"b\",\n\t\t\t\tType:    pb.Matcher_EQUAL,\n\t\t\t},\n\t\t\terr: \"\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"b\",\n\t\t\t\tType:    pb.Matcher_NOT_EQUAL,\n\t\t\t},\n\t\t\terr: \"\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"b\",\n\t\t\t\tType:    pb.Matcher_REGEXP,\n\t\t\t},\n\t\t\terr: \"\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"b\",\n\t\t\t\tType:    pb.Matcher_NOT_REGEXP,\n\t\t\t},\n\t\t\terr: \"\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"00\",\n\t\t\t\tPattern: \"a\",\n\t\t\t\tType:    pb.Matcher_EQUAL,\n\t\t\t},\n\t\t\terr: \"invalid label name\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"\\xf0\\x9f\\x99\\x82\", // U+1F642\n\t\t\t\tPattern: \"a\",\n\t\t\t\tType:    pb.Matcher_EQUAL,\n\t\t\t},\n\t\t\terr: \"invalid label name\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"((\",\n\t\t\t\tType:    pb.Matcher_REGEXP,\n\t\t\t},\n\t\t\terr: \"invalid regular expression\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"))\",\n\t\t\t\tType:    pb.Matcher_NOT_REGEXP,\n\t\t\t},\n\t\t\terr: \"invalid regular expression\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"\\xff\",\n\t\t\t\tType:    pb.Matcher_EQUAL,\n\t\t\t},\n\t\t\terr: \"invalid label value\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"\\xf0\\x9f\\x99\\x82\", // U+1F642\n\t\t\t\tType:    pb.Matcher_EQUAL,\n\t\t\t},\n\t\t\terr: \"\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"b\",\n\t\t\t\tType:    333,\n\t\t\t},\n\t\t\terr: \"unknown matcher type\",\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tcheckErr(t, c.err, validateMatcher(c.m))\n\t}\n}\n\nfunc TestValidateUTF8Matcher(t *testing.T) {\n\tcases := []struct {\n\t\tm   *pb.Matcher\n\t\terr string\n\t}{\n\t\t{\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"b\",\n\t\t\t\tType:    pb.Matcher_EQUAL,\n\t\t\t},\n\t\t\terr: \"\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"b\",\n\t\t\t\tType:    pb.Matcher_NOT_EQUAL,\n\t\t\t},\n\t\t\terr: \"\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"b\",\n\t\t\t\tType:    pb.Matcher_REGEXP,\n\t\t\t},\n\t\t\terr: \"\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"b\",\n\t\t\t\tType:    pb.Matcher_NOT_REGEXP,\n\t\t\t},\n\t\t\terr: \"\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"00\",\n\t\t\t\tPattern: \"a\",\n\t\t\t\tType:    pb.Matcher_EQUAL,\n\t\t\t},\n\t\t\terr: \"\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"\\xf0\\x9f\\x99\\x82\", // U+1F642\n\t\t\t\tPattern: \"a\",\n\t\t\t\tType:    pb.Matcher_EQUAL,\n\t\t\t},\n\t\t\terr: \"\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"((\",\n\t\t\t\tType:    pb.Matcher_REGEXP,\n\t\t\t},\n\t\t\terr: \"invalid regular expression\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"))\",\n\t\t\t\tType:    pb.Matcher_NOT_REGEXP,\n\t\t\t},\n\t\t\terr: \"invalid regular expression\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"\\xff\",\n\t\t\t\tType:    pb.Matcher_EQUAL,\n\t\t\t},\n\t\t\terr: \"invalid label value\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"\\xf0\\x9f\\x99\\x82\", // U+1F642\n\t\t\t\tType:    pb.Matcher_EQUAL,\n\t\t\t},\n\t\t\terr: \"\",\n\t\t}, {\n\t\t\tm: &pb.Matcher{\n\t\t\t\tName:    \"a\",\n\t\t\t\tPattern: \"b\",\n\t\t\t\tType:    333,\n\t\t\t},\n\t\t\terr: \"unknown matcher type\",\n\t\t},\n\t}\n\n\t// Change the mode to UTF-8 mode.\n\tff, err := featurecontrol.NewFlags(promslog.NewNopLogger(), featurecontrol.FeatureUTF8StrictMode)\n\trequire.NoError(t, err)\n\tcompat.InitFromFlags(promslog.NewNopLogger(), ff)\n\n\t// Restore the mode to classic at the end of the test.\n\tff, err = featurecontrol.NewFlags(promslog.NewNopLogger(), featurecontrol.FeatureClassicMode)\n\trequire.NoError(t, err)\n\tdefer compat.InitFromFlags(promslog.NewNopLogger(), ff)\n\n\tfor _, c := range cases {\n\t\tcheckErr(t, c.err, validateMatcher(c.m))\n\t}\n}\n\nfunc TestValidateSilence(t *testing.T) {\n\tvar (\n\t\tnow            = time.Now().UTC()\n\t\tzeroTimestamp  *timestamppb.Timestamp // nil represents zero timestamp\n\t\tvalidTimestamp = timestamppb.New(now)\n\t)\n\tcases := []struct {\n\t\ts   *pb.Silence\n\t\terr string\n\t}{\n\t\t{\n\t\t\ts: &pb.Silence{\n\t\t\t\tId: \"some_id\",\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t{Name: \"a\", Pattern: \"b\"},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t\tStartsAt:  validTimestamp,\n\t\t\t\tEndsAt:    validTimestamp,\n\t\t\t\tUpdatedAt: validTimestamp,\n\t\t\t},\n\t\t\terr: \"\",\n\t\t},\n\t\t{\n\t\t\ts: &pb.Silence{\n\t\t\t\tId: \"some_id\",\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{},\n\t\t\t\t}},\n\t\t\t\tStartsAt:  validTimestamp,\n\t\t\t\tEndsAt:    validTimestamp,\n\t\t\t\tUpdatedAt: validTimestamp,\n\t\t\t},\n\t\t\terr: \"matcher set 0 is empty\",\n\t\t},\n\t\t{\n\t\t\ts: &pb.Silence{\n\t\t\t\tId: \"some_id\",\n\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t{Name: \"a\", Pattern: \"b\"},\n\t\t\t\t\t{Name: \"00\", Pattern: \"b\"},\n\t\t\t\t},\n\t\t\t\tStartsAt:  validTimestamp,\n\t\t\t\tEndsAt:    validTimestamp,\n\t\t\t\tUpdatedAt: validTimestamp,\n\t\t\t},\n\t\t\terr: \"invalid label matcher\",\n\t\t},\n\t\t{\n\t\t\ts: &pb.Silence{\n\t\t\t\tId: \"some_id\",\n\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t{Name: \"a\", Pattern: \"\"},\n\t\t\t\t\t{Name: \"b\", Pattern: \".*\", Type: pb.Matcher_REGEXP},\n\t\t\t\t},\n\t\t\t\tStartsAt:  validTimestamp,\n\t\t\t\tEndsAt:    validTimestamp,\n\t\t\t\tUpdatedAt: validTimestamp,\n\t\t\t},\n\t\t\terr: \"at least one matcher must not match the empty string\",\n\t\t},\n\t\t{\n\t\t\ts: &pb.Silence{\n\t\t\t\tId: \"some_id\",\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t{Name: \"a\", Pattern: \"b\"},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t\tStartsAt:  timestamppb.New(now),\n\t\t\t\tEndsAt:    timestamppb.New(now.Add(-time.Second)),\n\t\t\t\tUpdatedAt: validTimestamp,\n\t\t\t},\n\t\t\terr: \"end time must not be before start time\",\n\t\t},\n\t\t{\n\t\t\ts: &pb.Silence{\n\t\t\t\tId: \"some_id\",\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t{Name: \"a\", Pattern: \"b\"},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t\tStartsAt:  zeroTimestamp,\n\t\t\t\tEndsAt:    validTimestamp,\n\t\t\t\tUpdatedAt: validTimestamp,\n\t\t\t},\n\t\t\terr: \"invalid zero start timestamp\",\n\t\t},\n\t\t{\n\t\t\ts: &pb.Silence{\n\t\t\t\tId: \"some_id\",\n\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t{Name: \"a\", Pattern: \"b\"},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t\tStartsAt:  validTimestamp,\n\t\t\t\tEndsAt:    zeroTimestamp,\n\t\t\t\tUpdatedAt: validTimestamp,\n\t\t\t},\n\t\t\terr: \"invalid zero end timestamp\",\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tcheckErr(t, c.err, validateSilence(c.s))\n\t}\n}\n\nfunc TestStateMerge(t *testing.T) {\n\tnow := time.Now().UTC()\n\n\t// We only care about key names and timestamps for the\n\t// merging logic.\n\tnewSilence := func(id string, ts, exp time.Time) *pb.MeshSilence {\n\t\treturn &pb.MeshSilence{\n\t\t\tSilence:   &pb.Silence{Id: id, UpdatedAt: timestamppb.New(ts)},\n\t\t\tExpiresAt: timestamppb.New(exp),\n\t\t}\n\t}\n\n\texp := now.Add(time.Minute)\n\n\tcases := []struct {\n\t\ta, b  state\n\t\tfinal state\n\t}{\n\t\t{\n\t\t\ta: state{\n\t\t\t\t\"a1\": newSilence(\"a1\", now, exp),\n\t\t\t\t\"a2\": newSilence(\"a2\", now, exp),\n\t\t\t\t\"a3\": newSilence(\"a3\", now, exp),\n\t\t\t},\n\t\t\tb: state{\n\t\t\t\t\"b1\": newSilence(\"b1\", now, exp),                                          // new key, should be added\n\t\t\t\t\"a2\": newSilence(\"a2\", now.Add(-time.Minute), exp),                        // older timestamp, should be dropped\n\t\t\t\t\"a3\": newSilence(\"a3\", now.Add(time.Minute), exp),                         // newer timestamp, should overwrite\n\t\t\t\t\"a4\": newSilence(\"a4\", now.Add(-time.Minute), now.Add(-time.Millisecond)), // new key, expired, should not be added\n\t\t\t},\n\t\t\tfinal: state{\n\t\t\t\t\"a1\": newSilence(\"a1\", now, exp),\n\t\t\t\t\"a2\": newSilence(\"a2\", now, exp),\n\t\t\t\t\"a3\": newSilence(\"a3\", now.Add(time.Minute), exp),\n\t\t\t\t\"b1\": newSilence(\"b1\", now, exp),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tfor _, e := range c.b {\n\t\t\tc.a.merge(e, now)\n\t\t}\n\n\t\trequire.Equal(t, c.final, c.a, \"Merge result should match expectation\")\n\t}\n}\n\nfunc TestStateCoding(t *testing.T) {\n\t// Check whether encoding and decoding the data is symmetric.\n\tnow := time.Now().UTC()\n\n\tcases := []struct {\n\t\tentries []*pb.MeshSilence\n\t}{\n\t\t{\n\t\t\tentries: []*pb.MeshSilence{\n\t\t\t\t{\n\t\t\t\t\tSilence: &pb.Silence{\n\t\t\t\t\t\tId: \"3be80475-e219-4ee7-b6fc-4b65114e362f\",\n\t\t\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val1\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t\t\t{Name: \"label2\", Pattern: \"val.+\", Type: pb.Matcher_REGEXP},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}},\n\t\t\t\t\t\tStartsAt:  timestamppb.New(now),\n\t\t\t\t\t\tEndsAt:    timestamppb.New(now),\n\t\t\t\t\t\tUpdatedAt: timestamppb.New(now),\n\t\t\t\t\t},\n\t\t\t\t\tExpiresAt: timestamppb.New(now),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tSilence: &pb.Silence{\n\t\t\t\t\t\tId: \"4b1e760d-182c-4980-b873-c1a6827c9817\",\n\t\t\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val1\", Type: pb.Matcher_EQUAL},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}},\n\t\t\t\t\t\tStartsAt:  timestamppb.New(now.Add(time.Hour)),\n\t\t\t\t\t\tEndsAt:    timestamppb.New(now.Add(2 * time.Hour)),\n\t\t\t\t\t\tUpdatedAt: timestamppb.New(now),\n\t\t\t\t\t},\n\t\t\t\t\tExpiresAt: timestamppb.New(now.Add(24 * time.Hour)),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tSilence: &pb.Silence{\n\t\t\t\t\t\tId: \"3dfb2528-59ce-41eb-b465-f875a4e744a4\",\n\t\t\t\t\t\tMatcherSets: []*pb.MatcherSet{{\n\t\t\t\t\t\t\tMatchers: []*pb.Matcher{\n\t\t\t\t\t\t\t\t{Name: \"label1\", Pattern: \"val1\", Type: pb.Matcher_NOT_EQUAL},\n\t\t\t\t\t\t\t\t{Name: \"label2\", Pattern: \"val.+\", Type: pb.Matcher_NOT_REGEXP},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}},\n\t\t\t\t\t\tStartsAt:  timestamppb.New(now),\n\t\t\t\t\t\tEndsAt:    timestamppb.New(now),\n\t\t\t\t\t\tUpdatedAt: timestamppb.New(now),\n\t\t\t\t\t},\n\t\t\t\t\tExpiresAt: timestamppb.New(now),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\t// Create gossip data from input.\n\t\tin := state{}\n\t\tfor _, e := range c.entries {\n\t\t\tin[e.Silence.Id] = e\n\t\t}\n\t\tmsg, err := in.MarshalBinary()\n\t\trequire.NoError(t, err)\n\n\t\tout, err := decodeState(bytes.NewReader(msg))\n\t\trequire.NoError(t, err, \"decoding message failed\")\n\n\t\trequire.Len(t, out, len(in), \"decoded state length mismatch\")\n\t\tfor id, expected := range in {\n\t\t\tactual, ok := out[id]\n\t\t\trequire.True(t, ok, \"silence %s missing from decoded state\", id)\n\t\t\trequire.True(t, proto.Equal(expected, actual), \"silence %s mismatch after decoding\", id)\n\t\t}\n\t}\n}\n\nfunc TestStateDecodingError(t *testing.T) {\n\t// Check whether decoding copes with erroneous data.\n\ts := state{\"\": &pb.MeshSilence{}}\n\n\tmsg, err := s.MarshalBinary()\n\trequire.NoError(t, err)\n\n\t_, err = decodeState(bytes.NewReader(msg))\n\trequire.Equal(t, ErrInvalidState, err)\n}\n\n// runtime.Gosched() does not \"suspend\" the current goroutine so there's no guarantee that the main goroutine won't\n// be able to continue. For more see https://pkg.go.dev/runtime#Gosched.\nfunc gosched() {\n\ttime.Sleep(1 * time.Millisecond)\n}\n\nfunc TestSilenceAnnotations(t *testing.T) {\n\ts, err := New(Options{\n\t\tMetrics:   prometheus.NewRegistry(),\n\t\tRetention: time.Hour,\n\t})\n\trequire.NoError(t, err)\n\n\tclock := quartz.NewMock(t)\n\ts.clock = clock\n\tnow := s.nowUTC()\n\n\t// Create a silence with annotations\n\tsil1 := &pb.Silence{\n\t\tMatchers: []*pb.Matcher{{Name: \"job\", Pattern: \"test\"}},\n\t\tStartsAt: timestamppb.New(now),\n\t\tEndsAt:   timestamppb.New(now.Add(time.Hour)),\n\t\tAnnotations: map[string]string{\n\t\t\t\"ticket\": \"JIRA-123\",\n\t\t\t\"type\":   \"planned\",\n\t\t\t\"test\":   \"integration\",\n\t\t},\n\t}\n\n\t// Set the silence via the API\n\trequire.NoError(t, s.Set(t.Context(), sil1))\n\trequire.NotEmpty(t, sil1.Id)\n\n\t// Query the silence back by ID\n\tqueriedSil, err := s.QueryOne(t.Context(), QIDs(sil1.Id))\n\trequire.NoError(t, err)\n\n\t// Verify all annotations are returned correctly\n\trequire.NotNil(t, queriedSil.Annotations)\n\trequire.Equal(t, \"JIRA-123\", queriedSil.Annotations[\"ticket\"])\n\trequire.Equal(t, \"planned\", queriedSil.Annotations[\"type\"])\n\trequire.Equal(t, \"integration\", queriedSil.Annotations[\"test\"])\n\n\t// Test querying all silences\n\tallSils, _, err := s.Query(t.Context())\n\trequire.NoError(t, err)\n\trequire.Len(t, allSils, 1)\n\trequire.Equal(t, queriedSil.Annotations, allSils[0].Annotations)\n\n\t// Create a second silence with different annotations\n\tsil2 := &pb.Silence{\n\t\tMatchers: []*pb.Matcher{{Name: \"job\", Pattern: \"frontend\"}},\n\t\tStartsAt: timestamppb.New(now),\n\t\tEndsAt:   timestamppb.New(now.Add(time.Hour)),\n\t\tAnnotations: map[string]string{\n\t\t\t\"ticket\": \"JIRA-456\",\n\t\t},\n\t}\n\trequire.NoError(t, s.Set(t.Context(), sil2))\n\n\t// Query by state and verify both silences have their annotations\n\tactiveSils, _, err := s.Query(t.Context(), QState(SilenceStateActive))\n\trequire.NoError(t, err)\n\trequire.Len(t, activeSils, 2)\n\n\tfor _, sil := range activeSils {\n\t\trequire.NotNil(t, sil.Annotations)\n\t\tswitch sil.Id {\n\t\tcase sil1.Id:\n\t\t\trequire.Len(t, sil.Annotations, 3)\n\t\t\trequire.Equal(t, \"JIRA-123\", sil.Annotations[\"ticket\"])\n\t\tcase sil2.Id:\n\t\t\trequire.Len(t, sil.Annotations, 1)\n\t\t\trequire.Equal(t, \"JIRA-456\", sil.Annotations[\"ticket\"])\n\t\tdefault:\n\t\t\tt.Fatalf(\"unexpected silence ID: %s\", sil.Id)\n\t\t}\n\t}\n\n\t// Test updating a silence with new annotations\n\tclock.Advance(time.Minute)\n\tsil1Updated := &pb.Silence{\n\t\tId:       sil1.Id,\n\t\tMatchers: []*pb.Matcher{{Name: \"job\", Pattern: \"test\"}},\n\t\tStartsAt: sil1.StartsAt,\n\t\tEndsAt:   sil1.EndsAt,\n\t\tAnnotations: map[string]string{\n\t\t\t\"ticket\": \"JIRA-123\",\n\t\t\t\"type\":   \"emergency\", // changed\n\t\t\t\"test\":   \"load\",      // changed\n\t\t},\n\t}\n\trequire.NoError(t, s.Set(t.Context(), sil1Updated))\n\n\t// Query back and verify annotations were updated\n\tqueriedUpdated, err := s.QueryOne(t.Context(), QIDs(sil1.Id))\n\trequire.NoError(t, err)\n\trequire.Len(t, queriedUpdated.Annotations, 3)\n\trequire.Equal(t, \"emergency\", queriedUpdated.Annotations[\"type\"])\n\trequire.Equal(t, \"load\", queriedUpdated.Annotations[\"test\"])\n\n\t// Test silence with nil annotations\n\tsil3 := &pb.Silence{\n\t\tMatchers:    []*pb.Matcher{{Name: \"job\", Pattern: \"backend\"}},\n\t\tStartsAt:    timestamppb.New(now),\n\t\tEndsAt:      timestamppb.New(now.Add(time.Hour)),\n\t\tAnnotations: nil,\n\t}\n\trequire.NoError(t, s.Set(t.Context(), sil3))\n\tqueriedSil3, err := s.QueryOne(t.Context(), QIDs(sil3.Id))\n\trequire.NoError(t, err)\n\t// nil annotations should be preserved or converted to empty map\n\tif queriedSil3.Annotations != nil {\n\t\trequire.Empty(t, queriedSil3.Annotations)\n\t}\n\n\t// Test silence with empty annotations map\n\tsil4 := &pb.Silence{\n\t\tMatchers:    []*pb.Matcher{{Name: \"job\", Pattern: \"database\"}},\n\t\tStartsAt:    timestamppb.New(now),\n\t\tEndsAt:      timestamppb.New(now.Add(time.Hour)),\n\t\tAnnotations: map[string]string{},\n\t}\n\trequire.NoError(t, s.Set(t.Context(), sil4))\n\tqueriedSil4, err := s.QueryOne(t.Context(), QIDs(sil4.Id))\n\trequire.NoError(t, err)\n\tif queriedSil4.Annotations != nil {\n\t\trequire.Empty(t, queriedSil4.Annotations)\n\t}\n}\n"
  },
  {
    "path": "silence/silencepb/silence.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        (unknown)\n// source: silence.proto\n\npackage silencepb\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\n// Type specifies how the given name and pattern are matched\n// against a label set.\ntype Matcher_Type int32\n\nconst (\n\tMatcher_EQUAL      Matcher_Type = 0\n\tMatcher_REGEXP     Matcher_Type = 1\n\tMatcher_NOT_EQUAL  Matcher_Type = 2\n\tMatcher_NOT_REGEXP Matcher_Type = 3\n)\n\n// Enum value maps for Matcher_Type.\nvar (\n\tMatcher_Type_name = map[int32]string{\n\t\t0: \"EQUAL\",\n\t\t1: \"REGEXP\",\n\t\t2: \"NOT_EQUAL\",\n\t\t3: \"NOT_REGEXP\",\n\t}\n\tMatcher_Type_value = map[string]int32{\n\t\t\"EQUAL\":      0,\n\t\t\"REGEXP\":     1,\n\t\t\"NOT_EQUAL\":  2,\n\t\t\"NOT_REGEXP\": 3,\n\t}\n)\n\nfunc (x Matcher_Type) Enum() *Matcher_Type {\n\tp := new(Matcher_Type)\n\t*p = x\n\treturn p\n}\n\nfunc (x Matcher_Type) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (Matcher_Type) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_silence_proto_enumTypes[0].Descriptor()\n}\n\nfunc (Matcher_Type) Type() protoreflect.EnumType {\n\treturn &file_silence_proto_enumTypes[0]\n}\n\nfunc (x Matcher_Type) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use Matcher_Type.Descriptor instead.\nfunc (Matcher_Type) EnumDescriptor() ([]byte, []int) {\n\treturn file_silence_proto_rawDescGZIP(), []int{0, 0}\n}\n\n// Matcher specifies a rule, which can match or set of labels or not.\ntype Matcher struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\tType  Matcher_Type           `protobuf:\"varint,1,opt,name=type,proto3,enum=silencepb.Matcher_Type\" json:\"type,omitempty\"`\n\t// The label name in a label set to against which the matcher\n\t// checks the pattern.\n\tName string `protobuf:\"bytes,2,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// The pattern being checked according to the matcher's type.\n\tPattern       string `protobuf:\"bytes,3,opt,name=pattern,proto3\" json:\"pattern,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Matcher) Reset() {\n\t*x = Matcher{}\n\tmi := &file_silence_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Matcher) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Matcher) ProtoMessage() {}\n\nfunc (x *Matcher) ProtoReflect() protoreflect.Message {\n\tmi := &file_silence_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Matcher.ProtoReflect.Descriptor instead.\nfunc (*Matcher) Descriptor() ([]byte, []int) {\n\treturn file_silence_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Matcher) GetType() Matcher_Type {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn Matcher_EQUAL\n}\n\nfunc (x *Matcher) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *Matcher) GetPattern() string {\n\tif x != nil {\n\t\treturn x.Pattern\n\t}\n\treturn \"\"\n}\n\n// DEPRECATED: A comment can be attached to a silence.\ntype Comment struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tAuthor        string                 `protobuf:\"bytes,1,opt,name=author,proto3\" json:\"author,omitempty\"`\n\tComment       string                 `protobuf:\"bytes,2,opt,name=comment,proto3\" json:\"comment,omitempty\"`\n\tTimestamp     *timestamppb.Timestamp `protobuf:\"bytes,3,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Comment) Reset() {\n\t*x = Comment{}\n\tmi := &file_silence_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Comment) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Comment) ProtoMessage() {}\n\nfunc (x *Comment) ProtoReflect() protoreflect.Message {\n\tmi := &file_silence_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Comment.ProtoReflect.Descriptor instead.\nfunc (*Comment) Descriptor() ([]byte, []int) {\n\treturn file_silence_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *Comment) GetAuthor() string {\n\tif x != nil {\n\t\treturn x.Author\n\t}\n\treturn \"\"\n}\n\nfunc (x *Comment) GetComment() string {\n\tif x != nil {\n\t\treturn x.Comment\n\t}\n\treturn \"\"\n}\n\nfunc (x *Comment) GetTimestamp() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn nil\n}\n\n// MatcherSet is a set of matchers all of which have to be true\n// for a silence to affect a given label set.\ntype MatcherSet struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tMatchers      []*Matcher             `protobuf:\"bytes,1,rep,name=matchers,proto3\" json:\"matchers,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MatcherSet) Reset() {\n\t*x = MatcherSet{}\n\tmi := &file_silence_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MatcherSet) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MatcherSet) ProtoMessage() {}\n\nfunc (x *MatcherSet) ProtoReflect() protoreflect.Message {\n\tmi := &file_silence_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MatcherSet.ProtoReflect.Descriptor instead.\nfunc (*MatcherSet) Descriptor() ([]byte, []int) {\n\treturn file_silence_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *MatcherSet) GetMatchers() []*Matcher {\n\tif x != nil {\n\t\treturn x.Matchers\n\t}\n\treturn nil\n}\n\n// Silence specifies an object that ignores alerts based\n// on a set of matchers during a given time frame.\ntype Silence struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// A globally unique identifier.\n\tId string `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\t// A set of matchers all of which have to be true for a silence\n\t// to affect a given label set. For silences with matcher_sets,\n\t// this is expected to be equal to the first entry in matcher_sets\n\tMatchers []*Matcher `protobuf:\"bytes,2,rep,name=matchers,proto3\" json:\"matchers,omitempty\"`\n\t// The time range during which the silence is active.\n\tStartsAt *timestamppb.Timestamp `protobuf:\"bytes,3,opt,name=starts_at,json=startsAt,proto3\" json:\"starts_at,omitempty\"`\n\tEndsAt   *timestamppb.Timestamp `protobuf:\"bytes,4,opt,name=ends_at,json=endsAt,proto3\" json:\"ends_at,omitempty\"`\n\t// The last notification made to the silence.\n\tUpdatedAt *timestamppb.Timestamp `protobuf:\"bytes,5,opt,name=updated_at,json=updatedAt,proto3\" json:\"updated_at,omitempty\"`\n\t// DEPRECATED: A set of comments made on the silence.\n\tComments []*Comment `protobuf:\"bytes,7,rep,name=comments,proto3\" json:\"comments,omitempty\"`\n\t// Comment for the silence.\n\tCreatedBy string `protobuf:\"bytes,8,opt,name=created_by,json=createdBy,proto3\" json:\"created_by,omitempty\"`\n\tComment   string `protobuf:\"bytes,9,opt,name=comment,proto3\" json:\"comment,omitempty\"`\n\t// Additional structured information about the silence\n\tAnnotations map[string]string `protobuf:\"bytes,10,rep,name=annotations,proto3\" json:\"annotations,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\t// Multiple matcher sets with OR logic between them.\n\t// At least one matcher set must match for the silence to apply.\n\tMatcherSets   []*MatcherSet `protobuf:\"bytes,11,rep,name=matcher_sets,json=matcherSets,proto3\" json:\"matcher_sets,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Silence) Reset() {\n\t*x = Silence{}\n\tmi := &file_silence_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Silence) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Silence) ProtoMessage() {}\n\nfunc (x *Silence) ProtoReflect() protoreflect.Message {\n\tmi := &file_silence_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Silence.ProtoReflect.Descriptor instead.\nfunc (*Silence) Descriptor() ([]byte, []int) {\n\treturn file_silence_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *Silence) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *Silence) GetMatchers() []*Matcher {\n\tif x != nil {\n\t\treturn x.Matchers\n\t}\n\treturn nil\n}\n\nfunc (x *Silence) GetStartsAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.StartsAt\n\t}\n\treturn nil\n}\n\nfunc (x *Silence) GetEndsAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.EndsAt\n\t}\n\treturn nil\n}\n\nfunc (x *Silence) GetUpdatedAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.UpdatedAt\n\t}\n\treturn nil\n}\n\nfunc (x *Silence) GetComments() []*Comment {\n\tif x != nil {\n\t\treturn x.Comments\n\t}\n\treturn nil\n}\n\nfunc (x *Silence) GetCreatedBy() string {\n\tif x != nil {\n\t\treturn x.CreatedBy\n\t}\n\treturn \"\"\n}\n\nfunc (x *Silence) GetComment() string {\n\tif x != nil {\n\t\treturn x.Comment\n\t}\n\treturn \"\"\n}\n\nfunc (x *Silence) GetAnnotations() map[string]string {\n\tif x != nil {\n\t\treturn x.Annotations\n\t}\n\treturn nil\n}\n\nfunc (x *Silence) GetMatcherSets() []*MatcherSet {\n\tif x != nil {\n\t\treturn x.MatcherSets\n\t}\n\treturn nil\n}\n\n// MeshSilence wraps a regular silence with an expiration timestamp\n// after which the silence may be garbage collected.\ntype MeshSilence struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tSilence       *Silence               `protobuf:\"bytes,1,opt,name=silence,proto3\" json:\"silence,omitempty\"`\n\tExpiresAt     *timestamppb.Timestamp `protobuf:\"bytes,2,opt,name=expires_at,json=expiresAt,proto3\" json:\"expires_at,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MeshSilence) Reset() {\n\t*x = MeshSilence{}\n\tmi := &file_silence_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MeshSilence) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MeshSilence) ProtoMessage() {}\n\nfunc (x *MeshSilence) ProtoReflect() protoreflect.Message {\n\tmi := &file_silence_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MeshSilence.ProtoReflect.Descriptor instead.\nfunc (*MeshSilence) Descriptor() ([]byte, []int) {\n\treturn file_silence_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *MeshSilence) GetSilence() *Silence {\n\tif x != nil {\n\t\treturn x.Silence\n\t}\n\treturn nil\n}\n\nfunc (x *MeshSilence) GetExpiresAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.ExpiresAt\n\t}\n\treturn nil\n}\n\nvar File_silence_proto protoreflect.FileDescriptor\n\nconst file_silence_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\rsilence.proto\\x12\\tsilencepb\\x1a\\x1fgoogle/protobuf/timestamp.proto\\\"\\xa2\\x01\\n\" +\n\t\"\\aMatcher\\x12+\\n\" +\n\t\"\\x04type\\x18\\x01 \\x01(\\x0e2\\x17.silencepb.Matcher.TypeR\\x04type\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x02 \\x01(\\tR\\x04name\\x12\\x18\\n\" +\n\t\"\\apattern\\x18\\x03 \\x01(\\tR\\apattern\\\"<\\n\" +\n\t\"\\x04Type\\x12\\t\\n\" +\n\t\"\\x05EQUAL\\x10\\x00\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06REGEXP\\x10\\x01\\x12\\r\\n\" +\n\t\"\\tNOT_EQUAL\\x10\\x02\\x12\\x0e\\n\" +\n\t\"\\n\" +\n\t\"NOT_REGEXP\\x10\\x03\\\"u\\n\" +\n\t\"\\aComment\\x12\\x16\\n\" +\n\t\"\\x06author\\x18\\x01 \\x01(\\tR\\x06author\\x12\\x18\\n\" +\n\t\"\\acomment\\x18\\x02 \\x01(\\tR\\acomment\\x128\\n\" +\n\t\"\\ttimestamp\\x18\\x03 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\ttimestamp\\\"<\\n\" +\n\t\"\\n\" +\n\t\"MatcherSet\\x12.\\n\" +\n\t\"\\bmatchers\\x18\\x01 \\x03(\\v2\\x12.silencepb.MatcherR\\bmatchers\\\"\\x9c\\x04\\n\" +\n\t\"\\aSilence\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12.\\n\" +\n\t\"\\bmatchers\\x18\\x02 \\x03(\\v2\\x12.silencepb.MatcherR\\bmatchers\\x127\\n\" +\n\t\"\\tstarts_at\\x18\\x03 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\bstartsAt\\x123\\n\" +\n\t\"\\aends_at\\x18\\x04 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\x06endsAt\\x129\\n\" +\n\t\"\\n\" +\n\t\"updated_at\\x18\\x05 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\tupdatedAt\\x12.\\n\" +\n\t\"\\bcomments\\x18\\a \\x03(\\v2\\x12.silencepb.CommentR\\bcomments\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"created_by\\x18\\b \\x01(\\tR\\tcreatedBy\\x12\\x18\\n\" +\n\t\"\\acomment\\x18\\t \\x01(\\tR\\acomment\\x12E\\n\" +\n\t\"\\vannotations\\x18\\n\" +\n\t\" \\x03(\\v2#.silencepb.Silence.AnnotationsEntryR\\vannotations\\x128\\n\" +\n\t\"\\fmatcher_sets\\x18\\v \\x03(\\v2\\x15.silencepb.MatcherSetR\\vmatcherSets\\x1a>\\n\" +\n\t\"\\x10AnnotationsEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\tR\\x05value:\\x028\\x01\\\"v\\n\" +\n\t\"\\vMeshSilence\\x12,\\n\" +\n\t\"\\asilence\\x18\\x01 \\x01(\\v2\\x12.silencepb.SilenceR\\asilence\\x129\\n\" +\n\t\"\\n\" +\n\t\"expires_at\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\texpiresAtB6Z4github.com/prometheus/alertmanager/silence/silencepbb\\x06proto3\"\n\nvar (\n\tfile_silence_proto_rawDescOnce sync.Once\n\tfile_silence_proto_rawDescData []byte\n)\n\nfunc file_silence_proto_rawDescGZIP() []byte {\n\tfile_silence_proto_rawDescOnce.Do(func() {\n\t\tfile_silence_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_silence_proto_rawDesc), len(file_silence_proto_rawDesc)))\n\t})\n\treturn file_silence_proto_rawDescData\n}\n\nvar file_silence_proto_enumTypes = make([]protoimpl.EnumInfo, 1)\nvar file_silence_proto_msgTypes = make([]protoimpl.MessageInfo, 6)\nvar file_silence_proto_goTypes = []any{\n\t(Matcher_Type)(0),             // 0: silencepb.Matcher.Type\n\t(*Matcher)(nil),               // 1: silencepb.Matcher\n\t(*Comment)(nil),               // 2: silencepb.Comment\n\t(*MatcherSet)(nil),            // 3: silencepb.MatcherSet\n\t(*Silence)(nil),               // 4: silencepb.Silence\n\t(*MeshSilence)(nil),           // 5: silencepb.MeshSilence\n\tnil,                           // 6: silencepb.Silence.AnnotationsEntry\n\t(*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp\n}\nvar file_silence_proto_depIdxs = []int32{\n\t0,  // 0: silencepb.Matcher.type:type_name -> silencepb.Matcher.Type\n\t7,  // 1: silencepb.Comment.timestamp:type_name -> google.protobuf.Timestamp\n\t1,  // 2: silencepb.MatcherSet.matchers:type_name -> silencepb.Matcher\n\t1,  // 3: silencepb.Silence.matchers:type_name -> silencepb.Matcher\n\t7,  // 4: silencepb.Silence.starts_at:type_name -> google.protobuf.Timestamp\n\t7,  // 5: silencepb.Silence.ends_at:type_name -> google.protobuf.Timestamp\n\t7,  // 6: silencepb.Silence.updated_at:type_name -> google.protobuf.Timestamp\n\t2,  // 7: silencepb.Silence.comments:type_name -> silencepb.Comment\n\t6,  // 8: silencepb.Silence.annotations:type_name -> silencepb.Silence.AnnotationsEntry\n\t3,  // 9: silencepb.Silence.matcher_sets:type_name -> silencepb.MatcherSet\n\t4,  // 10: silencepb.MeshSilence.silence:type_name -> silencepb.Silence\n\t7,  // 11: silencepb.MeshSilence.expires_at:type_name -> google.protobuf.Timestamp\n\t12, // [12:12] is the sub-list for method output_type\n\t12, // [12:12] is the sub-list for method input_type\n\t12, // [12:12] is the sub-list for extension type_name\n\t12, // [12:12] is the sub-list for extension extendee\n\t0,  // [0:12] is the sub-list for field type_name\n}\n\nfunc init() { file_silence_proto_init() }\nfunc file_silence_proto_init() {\n\tif File_silence_proto != nil {\n\t\treturn\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_silence_proto_rawDesc), len(file_silence_proto_rawDesc)),\n\t\t\tNumEnums:      1,\n\t\t\tNumMessages:   6,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_silence_proto_goTypes,\n\t\tDependencyIndexes: file_silence_proto_depIdxs,\n\t\tEnumInfos:         file_silence_proto_enumTypes,\n\t\tMessageInfos:      file_silence_proto_msgTypes,\n\t}.Build()\n\tFile_silence_proto = out.File\n\tfile_silence_proto_goTypes = nil\n\tfile_silence_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "silence/silencepb/silence.proto",
    "content": "syntax = \"proto3\";\n\npackage silencepb;\n\noption go_package = \"github.com/prometheus/alertmanager/silence/silencepb\";\n\nimport \"google/protobuf/timestamp.proto\";\n\n// Matcher specifies a rule, which can match or set of labels or not.\nmessage Matcher {\n  // Type specifies how the given name and pattern are matched\n  // against a label set.\n  enum Type {\n    EQUAL = 0;\n    REGEXP = 1;\n    NOT_EQUAL = 2;\n    NOT_REGEXP = 3;\n  };\n  Type type = 1;\n\n  // The label name in a label set to against which the matcher\n  // checks the pattern.\n  string name = 2;\n  // The pattern being checked according to the matcher's type.\n  string pattern = 3;\n}\n\n// DEPRECATED: A comment can be attached to a silence.\nmessage Comment {\n  string author = 1;\n  string comment = 2;\n  google.protobuf.Timestamp timestamp = 3;\n}\n\n// MatcherSet is a set of matchers all of which have to be true\n// for a silence to affect a given label set.\nmessage MatcherSet {\n  repeated Matcher matchers = 1;\n}\n\n// Silence specifies an object that ignores alerts based\n// on a set of matchers during a given time frame.\nmessage Silence {\n  // A globally unique identifier.\n  string id = 1;\n\n  // A set of matchers all of which have to be true for a silence\n  // to affect a given label set. For silences with matcher_sets,\n  // this is expected to be equal to the first entry in matcher_sets\n  repeated Matcher matchers = 2;\n\n  // The time range during which the silence is active.\n  google.protobuf.Timestamp starts_at = 3;\n  google.protobuf.Timestamp ends_at = 4;\n\n  // The last notification made to the silence.\n  google.protobuf.Timestamp updated_at = 5;\n\n  // DEPRECATED: A set of comments made on the silence.\n  repeated Comment comments = 7;\n  // Comment for the silence.\n  string created_by = 8;\n  string comment = 9;\n  \n  // Additional structured information about the silence\n  map<string, string> annotations = 10;\n\n  // Multiple matcher sets with OR logic between them.\n  // At least one matcher set must match for the silence to apply.\n  repeated MatcherSet matcher_sets = 11;\n}\n\n// MeshSilence wraps a regular silence with an expiration timestamp\n// after which the silence may be garbage collected.\nmessage MeshSilence {\n  Silence silence = 1;\n  google.protobuf.Timestamp expires_at = 2;\n}\n"
  },
  {
    "path": "silence/state.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage silence\n\nimport \"time\"\n\ntype SilenceState string\n\nconst (\n\tSilenceStateExpired SilenceState = \"expired\"\n\tSilenceStateActive  SilenceState = \"active\"\n\tSilenceStatePending SilenceState = \"pending\"\n)\n\n// CurrentState returns the SilenceState that a silence with the given start\n// and end time would have right now.\nfunc CurrentState(start, end time.Time) SilenceState {\n\tcurrent := time.Now()\n\tif current.Before(start) {\n\t\treturn SilenceStatePending\n\t}\n\tif current.Before(end) {\n\t\treturn SilenceStateActive\n\t}\n\treturn SilenceStateExpired\n}\n"
  },
  {
    "path": "silence/state_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\npackage silence\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCurrentState(t *testing.T) {\n\tvar (\n\t\tpastStartTime = time.Now()\n\t\tpastEndTime   = time.Now()\n\n\t\tfutureStartTime = time.Now().Add(time.Hour)\n\t\tfutureEndTime   = time.Now().Add(time.Hour)\n\t)\n\n\texpected := CurrentState(futureStartTime, futureEndTime)\n\trequire.Equal(t, SilenceStatePending, expected)\n\n\texpected = CurrentState(pastStartTime, futureEndTime)\n\trequire.Equal(t, SilenceStateActive, expected)\n\n\texpected = CurrentState(pastStartTime, pastEndTime)\n\trequire.Equal(t, SilenceStateExpired, expected)\n}\n"
  },
  {
    "path": "store/store.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage store\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/prometheus/common/model\"\n\n\t\"github.com/prometheus/alertmanager/limit\"\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n// ErrLimited is returned if a Store has reached the per-alert limit.\nvar ErrLimited = errors.New(\"alert limited\")\n\n// ErrNotFound is returned if a Store cannot find the Alert.\nvar ErrNotFound = errors.New(\"alert not found\")\n\n// ErrDestroyed is returned if a Store has been destroyed.\nvar ErrDestroyed = errors.New(\"alert store destroyed\")\n\n// Alerts provides lock-coordinated to an in-memory map of alerts, keyed by\n// their fingerprint. Resolved alerts are removed from the map based on\n// gcInterval. An optional callback can be set which receives a slice of all\n// resolved alerts that have been removed.\ntype Alerts struct {\n\tsync.Mutex\n\talerts        map[model.Fingerprint]*types.Alert\n\tgcCallback    func([]*types.Alert)\n\tlimits        map[string]*limit.Bucket[model.Fingerprint]\n\tperAlertLimit int\n\tdestroyed     bool\n}\n\n// NewAlerts returns a new Alerts struct.\nfunc NewAlerts() *Alerts {\n\ta := &Alerts{\n\t\talerts:        make(map[model.Fingerprint]*types.Alert),\n\t\tgcCallback:    func(_ []*types.Alert) {},\n\t\tperAlertLimit: 0,\n\t}\n\n\treturn a\n}\n\n// WithPerAlertLimit sets the per-alert limit for the Alerts struct.\nfunc (a *Alerts) WithPerAlertLimit(lim int) *Alerts {\n\ta.Lock()\n\tdefer a.Unlock()\n\n\ta.limits = make(map[string]*limit.Bucket[model.Fingerprint])\n\ta.perAlertLimit = lim\n\n\treturn a\n}\n\n// SetGCCallback sets a GC callback to be executed after each GC.\nfunc (a *Alerts) SetGCCallback(cb func([]*types.Alert)) {\n\ta.Lock()\n\tdefer a.Unlock()\n\n\ta.gcCallback = cb\n}\n\n// Run starts the GC loop. The interval must be greater than zero; if not, the function will panic.\n// Note: This is only used by inhibitor currently and potentially can be removed later.\nfunc (a *Alerts) Run(ctx context.Context, interval time.Duration) {\n\tt := time.NewTicker(interval)\n\tdefer t.Stop()\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-t.C:\n\t\t\ta.GC()\n\t\t}\n\t}\n}\n\n// GC deletes resolved alerts and returns them.\nfunc (a *Alerts) GC() (deleted []*types.Alert) {\n\t// Remove stale alert limit buckets.\n\ta.gcLimitBuckets()\n\n\t// Delete resolved alerts.\n\tdeleted = a.gcAlerts()\n\n\t// Execute GC callback if needed.\n\tif len(deleted) > 0 {\n\t\ta.gcCallback(deleted)\n\t}\n\n\treturn deleted\n}\n\n// gcAlerts deletes resolved alerts and returns a copy of them.\nfunc (a *Alerts) gcAlerts() (deleted []*types.Alert) {\n\ta.Lock()\n\tdefer a.Unlock()\n\tfor fp, alert := range a.alerts {\n\t\tif alert.Resolved() {\n\t\t\tdeleted = append(deleted, alert)\n\t\t\tdelete(a.alerts, fp)\n\t\t}\n\t}\n\treturn deleted\n}\n\n// gcLimitBuckets removes stale alert limit buckets.\nfunc (a *Alerts) gcLimitBuckets() {\n\ta.Lock()\n\tdefer a.Unlock()\n\n\tfor alertName, bucket := range a.limits {\n\t\tif bucket.IsStale() {\n\t\t\tdelete(a.limits, alertName)\n\t\t}\n\t}\n}\n\n// Get returns the Alert with the matching fingerprint, or an error if it is\n// not found.\nfunc (a *Alerts) Get(fp model.Fingerprint) (*types.Alert, error) {\n\ta.Lock()\n\tdefer a.Unlock()\n\n\talert, prs := a.alerts[fp]\n\tif !prs {\n\t\treturn nil, ErrNotFound\n\t}\n\treturn alert, nil\n}\n\n// Set unconditionally sets the alert in memory.\nfunc (a *Alerts) Set(alert *types.Alert) error {\n\ta.Lock()\n\tdefer a.Unlock()\n\n\tif a.destroyed {\n\t\treturn ErrDestroyed\n\t}\n\n\tfp := alert.Fingerprint()\n\tname := alert.Name()\n\n\t// Apply per alert limits if necessary\n\tif a.perAlertLimit > 0 {\n\t\tbucket, ok := a.limits[name]\n\t\tif !ok {\n\t\t\tbucket = limit.NewBucket[model.Fingerprint](a.perAlertLimit)\n\t\t\ta.limits[name] = bucket\n\t\t}\n\t\tif !bucket.Upsert(fp, alert.EndsAt) {\n\t\t\treturn ErrLimited\n\t\t}\n\t}\n\n\ta.alerts[fp] = alert\n\treturn nil\n}\n\n// DeleteIfNotModified deletes the slice of Alerts from the store if not\n// modified.\nfunc (a *Alerts) DeleteIfNotModified(alerts types.AlertSlice, destroyIfEmpty bool) error {\n\ta.Lock()\n\tdefer a.Unlock()\n\tfor _, alert := range alerts {\n\t\tfp := alert.Fingerprint()\n\t\tif other, ok := a.alerts[fp]; ok && alert.UpdatedAt.Equal(other.UpdatedAt) {\n\t\t\tdelete(a.alerts, fp)\n\t\t}\n\t}\n\n\t// If the store is now empty, mark it as destroyed\n\tif len(a.alerts) == 0 && destroyIfEmpty {\n\t\ta.destroyed = true\n\t}\n\n\treturn nil\n}\n\n// List returns a slice of Alerts currently held in memory.\nfunc (a *Alerts) List() []*types.Alert {\n\ta.Lock()\n\tdefer a.Unlock()\n\n\talerts := make([]*types.Alert, 0, len(a.alerts))\n\tfor _, alert := range a.alerts {\n\t\talerts = append(alerts, alert)\n\t}\n\n\treturn alerts\n}\n\n// Empty returns true if the store is empty.\nfunc (a *Alerts) Empty() bool {\n\ta.Lock()\n\tdefer a.Unlock()\n\n\treturn len(a.alerts) == 0\n}\n\n// Empty returns true if the store is empty.\nfunc (a *Alerts) Destroyed() bool {\n\ta.Lock()\n\tdefer a.Unlock()\n\n\treturn a.destroyed\n}\n\n// Len returns the number of alerts in the store.\nfunc (a *Alerts) Len() int {\n\ta.Lock()\n\tdefer a.Unlock()\n\n\treturn len(a.alerts)\n}\n"
  },
  {
    "path": "store/store_test.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage store\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nfunc TestSetGet(t *testing.T) {\n\ta := NewAlerts()\n\talert := &types.Alert{\n\t\tUpdatedAt: time.Now(),\n\t}\n\trequire.NoError(t, a.Set(alert))\n\twant := alert.Fingerprint()\n\tgot, err := a.Get(want)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, want, got.Fingerprint())\n}\n\nfunc TestDeleteIfNotModified(t *testing.T) {\n\tt.Run(\"unmodified alert should be deleted\", func(t *testing.T) {\n\t\ta := NewAlerts()\n\t\ta1 := &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tUpdatedAt: time.Now().Add(-time.Second),\n\t\t}\n\t\trequire.NoError(t, a.Set(a1))\n\n\t\t// a1 should be deleted as it has not been modified.\n\t\ta.DeleteIfNotModified(types.AlertSlice{a1}, false)\n\t\tgot, err := a.Get(a1.Fingerprint())\n\t\trequire.Equal(t, ErrNotFound, err)\n\t\trequire.Nil(t, got)\n\t})\n\n\tt.Run(\"modified alert should not be deleted\", func(t *testing.T) {\n\t\ta := NewAlerts()\n\t\ta1 := &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tUpdatedAt: time.Now(),\n\t\t}\n\t\trequire.NoError(t, a.Set(a1))\n\n\t\t// Make a copy of a1 that is older, but do not put it.\n\t\t// We want to make sure a1 is not deleted.\n\t\ta2 := &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tUpdatedAt: time.Now().Add(-time.Second),\n\t\t}\n\t\trequire.True(t, a2.UpdatedAt.Before(a1.UpdatedAt))\n\t\ta.DeleteIfNotModified(types.AlertSlice{a2}, false)\n\t\t// a1 should not be deleted.\n\t\tgot, err := a.Get(a1.Fingerprint())\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, a1, got)\n\n\t\t// Make another copy of a1 that is older, but do not put it.\n\t\t// We want to make sure a2 is not deleted here either.\n\t\ta3 := &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tUpdatedAt: time.Now().Add(time.Second),\n\t\t}\n\t\trequire.True(t, a3.UpdatedAt.After(a1.UpdatedAt))\n\t\ta.DeleteIfNotModified(types.AlertSlice{a3}, false)\n\t\t// a1 should not be deleted.\n\t\tgot, err = a.Get(a1.Fingerprint())\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, a1, got)\n\t})\n\n\tt.Run(\"should not delete other alerts\", func(t *testing.T) {\n\t\ta := NewAlerts()\n\t\ta1 := &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tUpdatedAt: time.Now(),\n\t\t}\n\t\ta2 := &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\"bar\": \"baz\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tUpdatedAt: time.Now(),\n\t\t}\n\t\trequire.NoError(t, a.Set(a1))\n\t\trequire.NoError(t, a.Set(a2))\n\n\t\t// Deleting a1 should not delete a2.\n\t\trequire.NoError(t, a.DeleteIfNotModified(types.AlertSlice{a1}, true))\n\t\t// a1 should be deleted.\n\t\tgot, err := a.Get(a1.Fingerprint())\n\t\trequire.Equal(t, ErrNotFound, err)\n\t\trequire.False(t, a.Destroyed())\n\t\trequire.Nil(t, got)\n\t\t// a2 should not be deleted.\n\t\tgot, err = a.Get(a2.Fingerprint())\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, a2, got)\n\t})\n}\n\nfunc TestGC(t *testing.T) {\n\tnow := time.Now()\n\tnewAlert := func(key string, start, end time.Duration) *types.Alert {\n\t\treturn &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels:   model.LabelSet{model.LabelName(key): \"b\"},\n\t\t\t\tStartsAt: now.Add(start * time.Minute),\n\t\t\t\tEndsAt:   now.Add(end * time.Minute),\n\t\t\t},\n\t\t}\n\t}\n\tactive := []*types.Alert{\n\t\tnewAlert(\"b\", 10, 20),\n\t\tnewAlert(\"c\", -10, 10),\n\t}\n\tresolved := []*types.Alert{\n\t\tnewAlert(\"a\", -10, -5),\n\t\tnewAlert(\"d\", -10, -1),\n\t}\n\ts := NewAlerts()\n\tvar (\n\t\tn           int\n\t\tdone        = make(chan struct{})\n\t\tctx, cancel = context.WithCancel(context.Background())\n\t)\n\ts.SetGCCallback(func(a []*types.Alert) {\n\t\tn += len(a)\n\t\tif n >= len(resolved) {\n\t\t\tcancel()\n\t\t}\n\t})\n\tfor _, alert := range append(active, resolved...) {\n\t\trequire.NoError(t, s.Set(alert))\n\t}\n\tgo func() {\n\t\ts.Run(ctx, 10*time.Millisecond)\n\t\tclose(done)\n\t}()\n\tselect {\n\tcase <-done:\n\t\tbreak\n\tcase <-time.After(1 * time.Second):\n\t\tt.Fatal(\"garbage collection didn't complete in time\")\n\t}\n\n\tfor _, alert := range active {\n\t\tif _, err := s.Get(alert.Fingerprint()); err != nil {\n\t\t\tt.Errorf(\"alert %v should not have been gc'd\", alert)\n\t\t}\n\t}\n\tfor _, alert := range resolved {\n\t\tif _, err := s.Get(alert.Fingerprint()); err == nil {\n\t\t\tt.Errorf(\"alert %v should have been gc'd\", alert)\n\t\t}\n\t}\n\trequire.Len(t, resolved, n)\n}\n"
  },
  {
    "path": "template/Dockerfile",
    "content": "FROM node:20-alpine\n\nENV NODE_PATH=\"/usr/local/lib/node_modules\"\n\nRUN npm install juice@10.0.1 -g\n\nENTRYPOINT [\"\"]\n"
  },
  {
    "path": "template/Makefile",
    "content": "DOCKER_IMG := alertmanager-template\nDOCKER_RUN_CURRENT_USER := docker run --user=$(shell id -u $(USER)):$(shell id -g $(USER))\nDOCKER_CMD := $(DOCKER_RUN_CURRENT_USER) --rm -t -v $(PWD):/app -w /app $(DOCKER_IMG)\n\nifeq ($(NO_DOCKER), true)\n\tDOCKER_CMD=\nendif\n\ntemplate-image:\n\t@(if [ \"$(NO_DOCKER)\" != \"true\" ] ; then \\\n\t\techo \">> build template docker image\"; \\\n\t\tdocker build -t $(DOCKER_IMG) . > /dev/null; \\\n\tfi; )\n\nemail.tmpl: template-image email.html\n\t@echo \">> inline css for html email template\"\n\t$(DOCKER_CMD) ./inline-css.js\n"
  },
  {
    "path": "template/default.tmpl",
    "content": "{{ define \"__alertmanager\" }}Alertmanager{{ end }}\n{{ define \"__alertmanagerURL\" }}{{ .ExternalURL }}/#/alerts?receiver={{ .Receiver | urlquery }}{{ end }}\n\n{{ define \"__subject\" }}[{{ .Status | toUpper }}{{ if eq .Status \"firing\" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .GroupLabels.SortedPairs.Values | join \" \" }} {{ if gt (len .CommonLabels) (len .GroupLabels) }}({{ with .CommonLabels.Remove .GroupLabels.Names }}{{ .Values | join \" \" }}{{ end }}){{ end }}{{ end }}\n{{ define \"__description\" }}{{ end }}\n\n{{ define \"__text_alert_list\" }}{{ range . }}Labels:\n{{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }}\n{{ end }}Annotations:\n{{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }}\n{{ end }}Source: {{ .GeneratorURL }}\n{{ end }}{{ end }}\n\n{{ define \"__text_alert_list_markdown\" }}{{ range . }}\nLabels:\n{{ range .Labels.SortedPairs }}  - {{ .Name }} = {{ .Value }}\n{{ end }}\nAnnotations:\n{{ range .Annotations.SortedPairs }}  - {{ .Name }} = {{ .Value }}\n{{ end }}\nSource: {{ .GeneratorURL }}\n{{ end }}\n{{ end }}\n\n{{ define \"slack.default.title\" }}{{ template \"__subject\" . }}{{ end }}\n{{ define \"slack.default.username\" }}{{ template \"__alertmanager\" . }}{{ end }}\n{{ define \"slack.default.fallback\" }}{{ template \"slack.default.title\" . }} | {{ template \"slack.default.titlelink\" . }}{{ end }}\n{{ define \"slack.default.callbackid\" }}{{ end }}\n{{ define \"slack.default.pretext\" }}{{ end }}\n{{ define \"slack.default.titlelink\" }}{{ template \"__alertmanagerURL\" . }}{{ end }}\n{{ define \"slack.default.iconemoji\" }}{{ end }}\n{{ define \"slack.default.iconurl\" }}{{ end }}\n{{ define \"slack.default.text\" }}{{ end }}\n{{ define \"slack.default.footer\" }}{{ end }}\n{{ define \"slack.default.color\" }}{{ if eq .Status \"firing\" }}danger{{ else }}good{{ end }}{{ end }}\n\n\n{{ define \"pagerduty.default.description\" }}{{ template \"__subject\" . }}{{ end }}\n{{ define \"pagerduty.default.client\" }}{{ template \"__alertmanager\" . }}{{ end }}\n{{ define \"pagerduty.default.clientURL\" }}{{ template \"__alertmanagerURL\" . }}{{ end }}\n{{ define \"pagerduty.default.instances\" }}{{ template \"__text_alert_list\" . }}{{ end }}\n\n\n{{ define \"opsgenie.default.message\" }}{{ template \"__subject\" . }}{{ end }}\n{{ define \"opsgenie.default.description\" }}{{ .CommonAnnotations.SortedPairs.Values | join \" \" }}\n{{ if gt (len .Alerts.Firing) 0 -}}\nAlerts Firing:\n{{ template \"__text_alert_list\" .Alerts.Firing }}\n{{- end }}\n{{ if gt (len .Alerts.Resolved) 0 -}}\nAlerts Resolved:\n{{ template \"__text_alert_list\" .Alerts.Resolved }}\n{{- end }}\n{{- end }}\n{{ define \"opsgenie.default.source\" }}{{ template \"__alertmanagerURL\" . }}{{ end }}\n\n\n{{ define \"wechat.default.message\" }}{{ template \"__subject\" . }}\n{{ .CommonAnnotations.SortedPairs.Values | join \" \" }}\n{{ if gt (len .Alerts.Firing) 0 -}}\nAlerts Firing:\n{{ template \"__text_alert_list\" .Alerts.Firing }}\n{{- end }}\n{{ if gt (len .Alerts.Resolved) 0 -}}\nAlerts Resolved:\n{{ template \"__text_alert_list\" .Alerts.Resolved }}\n{{- end }}\nAlertmanagerUrl:\n{{ template \"__alertmanagerURL\" . }}\n{{- end }}\n{{ define \"wechat.default.to_user\" }}{{ end }}\n{{ define \"wechat.default.to_party\" }}{{ end }}\n{{ define \"wechat.default.to_tag\" }}{{ end }}\n{{ define \"wechat.default.agent_id\" }}{{ end }}\n\n\n\n{{ define \"victorops.default.state_message\" }}{{ .CommonAnnotations.SortedPairs.Values | join \" \" }}\n{{ if gt (len .Alerts.Firing) 0 -}}\nAlerts Firing:\n{{ template \"__text_alert_list\" .Alerts.Firing }}\n{{- end }}\n{{ if gt (len .Alerts.Resolved) 0 -}}\nAlerts Resolved:\n{{ template \"__text_alert_list\" .Alerts.Resolved }}\n{{- end }}\n{{- end }}\n{{ define \"victorops.default.entity_display_name\" }}{{ template \"__subject\" . }}{{ end }}\n{{ define \"victorops.default.monitoring_tool\" }}{{ template \"__alertmanager\" . }}{{ end }}\n\n{{ define \"pushover.default.title\" }}{{ template \"__subject\" . }}{{ end }}\n{{ define \"pushover.default.message\" }}{{ .CommonAnnotations.SortedPairs.Values | join \" \" }}\n{{ if gt (len .Alerts.Firing) 0 }}\nAlerts Firing:\n{{ template \"__text_alert_list\" .Alerts.Firing }}\n{{ end }}\n{{ if gt (len .Alerts.Resolved) 0 }}\nAlerts Resolved:\n{{ template \"__text_alert_list\" .Alerts.Resolved }}\n{{ end }}\n{{ end }}\n{{ define \"pushover.default.url\" }}{{ template \"__alertmanagerURL\" . }}{{ end }}\n\n{{ define \"sns.default.subject\" }}{{ template \"__subject\" . }}{{ end }}\n{{ define \"sns.default.message\" }}{{ .CommonAnnotations.SortedPairs.Values | join \" \" }}\n{{ if gt (len .Alerts.Firing) 0 }}\nAlerts Firing:\n{{ template \"__text_alert_list\" .Alerts.Firing }}\n{{ end }}\n{{ if gt (len .Alerts.Resolved) 0 }}\nAlerts Resolved:\n{{ template \"__text_alert_list\" .Alerts.Resolved }}\n{{ end }}\n{{ end }}\n\n{{ define \"telegram.default.message\" }}\n{{ if gt (len .Alerts.Firing) 0 }}\nAlerts Firing:\n{{ template \"__text_alert_list\" .Alerts.Firing }}\n{{ end }}\n{{ if gt (len .Alerts.Resolved) 0 }}\nAlerts Resolved:\n{{ template \"__text_alert_list\" .Alerts.Resolved }}\n{{ end }}\n{{ end }}\n\n{{ define \"discord.default.content\" }}{{ end }}\n{{ define \"discord.default.title\" }}{{ template \"__subject\" . }}{{ end }}\n{{ define \"discord.default.message\" }}\n{{ if gt (len .Alerts.Firing) 0 }}\nAlerts Firing:\n{{ template \"__text_alert_list\" .Alerts.Firing }}\n{{ end }}\n{{ if gt (len .Alerts.Resolved) 0 }}\nAlerts Resolved:\n{{ template \"__text_alert_list\" .Alerts.Resolved }}\n{{ end }}\n{{ end }}\n\n{{ define \"webex.default.message\" }}{{ .CommonAnnotations.SortedPairs.Values | join \" \" }}\n{{ if gt (len .Alerts.Firing) 0 }}\nAlerts Firing:\n{{ template \"__text_alert_list\" .Alerts.Firing }}\n{{ end }}\n{{ if gt (len .Alerts.Resolved) 0 }}\nAlerts Resolved:\n{{ template \"__text_alert_list\" .Alerts.Resolved }}\n{{ end }}\n{{ end }}\n\n{{ define \"msteams.default.summary\" }}{{ template \"__subject\" . }}{{ end }}\n{{ define \"msteams.default.title\" }}{{ template \"__subject\" . }}{{ end }}\n{{ define \"msteams.default.text\" }}\n{{ if gt (len .Alerts.Firing) 0 }}\n# Alerts Firing:\n{{ template \"__text_alert_list_markdown\" .Alerts.Firing }}\n{{ end }}\n{{ if gt (len .Alerts.Resolved) 0 }}\n# Alerts Resolved:\n{{ template \"__text_alert_list_markdown\" .Alerts.Resolved }}\n{{ end }}\n{{ end }}\n\n{{ define \"msteamsv2.default.title\" }}{{ template \"__subject\" . }}{{ end }}\n{{ define \"msteamsv2.default.text\" }}\n{{ if gt (len .Alerts.Firing) 0 }}\n# Alerts Firing:\n{{ template \"__text_alert_list_markdown\" .Alerts.Firing }}\n{{ end }}\n{{ if gt (len .Alerts.Resolved) 0 }}\n# Alerts Resolved:\n{{ template \"__text_alert_list_markdown\" .Alerts.Resolved }}\n{{ end }}\n{{ end }}\n\n{{ define \"jira.default.summary\" }}{{ template \"__subject\" . }}{{ end }}\n{{ define \"jira.default.description\" }}\n{{ if gt (len .Alerts.Firing) 0 }}\n# Alerts Firing:\n{{ template \"__text_alert_list_markdown\" .Alerts.Firing }}\n{{ end }}\n{{ if gt (len .Alerts.Resolved) 0 }}\n# Alerts Resolved:\n{{ template \"__text_alert_list_markdown\" .Alerts.Resolved }}\n{{ end }}\n{{ end }}\n\n{{- define \"jira.default.priority\" -}}\n{{- $priority := \"\" }}\n{{- range .Alerts.Firing -}}\n    {{- $severity := index .Labels \"severity\" -}}\n    {{- if (eq $severity \"critical\") -}}\n        {{- $priority = \"High\" -}}\n    {{- else if (and (eq $severity \"warning\") (ne $priority \"High\")) -}}\n        {{- $priority = \"Medium\" -}}\n    {{- else if (and (eq $severity \"info\") (eq $priority \"\")) -}}\n        {{- $priority = \"Low\" -}}\n    {{- end -}}\n{{- end -}}\n{{- if eq $priority \"\" -}}\n    {{- range .Alerts.Resolved -}}\n        {{- $severity := index .Labels \"severity\" -}}\n        {{- if (eq $severity \"critical\") -}}\n            {{- $priority = \"High\" -}}\n        {{- else if (and (eq $severity \"warning\") (ne $priority \"High\")) -}}\n            {{- $priority = \"Medium\" -}}\n        {{- else if (and (eq $severity \"info\") (eq $priority \"\")) -}}\n            {{- $priority = \"Low\" -}}\n        {{- end -}}\n    {{- end -}}\n{{- end -}}\n{{- $priority -}}\n{{- end -}}\n\n{{ define \"rocketchat.default.title\" }}{{ template \"__subject\" . }}{{ end }}\n{{ define \"rocketchat.default.alias\" }}{{ template \"__alertmanager\" . }}{{ end }}\n{{ define \"rocketchat.default.titlelink\" }}{{ template \"__alertmanagerURL\" . }}{{ end }}\n{{ define \"rocketchat.default.emoji\" }}{{ end }}\n{{ define \"rocketchat.default.iconurl\" }}{{ end }}\n{{ define \"rocketchat.default.text\" }}{{ end }}\n\n{{ define \"mattermost.default.color\" }}{{ if eq .Status \"firing\" }}danger{{ else }}good{{ end }}{{ end }}\n{{ define \"mattermost.default.username\" }}{{ template \"__alertmanager\" . }}{{ end }}\n{{ define \"mattermost.default.title\" }}{{ template \"__subject\" . }}{{ end }}\n{{ define \"mattermost.default.titlelink\" }}{{ template \"__alertmanagerURL\" . }}{{ end }}\n{{ define \"mattermost.default.fallback\" }}{{ template \"mattermost.default.title\" . }} | {{ template \"mattermost.default.titlelink\" . }}{{ end }}\n{{ define \"mattermost.default.text\" }}\n{{ if gt (len .Alerts.Firing) 0 }}\n# Alerts Firing:\n{{ template \"__text_alert_list_markdown\" .Alerts.Firing }}\n{{ end }}\n{{ if gt (len .Alerts.Resolved) 0 }}\n# Alerts Resolved:\n{{ template \"__text_alert_list_markdown\" .Alerts.Resolved }}\n{{ end }}\n{{ end }}"
  },
  {
    "path": "template/email.html",
    "content": "<!--\nStyle and HTML derived from https://github.com/mailgun/transactional-email-templates\n\n\nThe MIT License (MIT)\n\nCopyright (c) 2014 Mailgun\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n-->\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta name=\"viewport\" content=\"width=device-width\" />\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>{{ template \"__subject\" . }}</title>\n<style>\n/* -------------------------------------\n    GLOBAL\n    A very basic CSS reset\n------------------------------------- */\n* {\n  margin: 0;\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  box-sizing: border-box;\n  font-size: 14px;\n}\n\nimg {\n  max-width: 100%;\n}\n\nbody {\n  -webkit-font-smoothing: antialiased;\n  -webkit-text-size-adjust: none;\n  width: 100% !important;\n  height: 100%;\n  line-height: 1.6em;\n  /* 1.6em * 14px = 22.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */\n  /*line-height: 22px;*/\n}\n\n/* Let's make sure all tables have defaults */\ntable td {\n  vertical-align: top;\n}\n\n/* -------------------------------------\n    BODY & CONTAINER\n------------------------------------- */\nbody {\n  background-color: #f6f6f6;\n}\n\n.body-wrap {\n  background-color: #f6f6f6;\n  width: 100%;\n}\n\n.container {\n  display: block !important;\n  max-width: 600px !important;\n  margin: 0 auto !important;\n  /* makes it centered */\n  clear: both !important;\n}\n\n.content {\n  max-width: 600px;\n  margin: 0 auto;\n  display: block;\n  padding: 20px;\n}\n\n/* -------------------------------------\n    HEADER, FOOTER, MAIN\n------------------------------------- */\n.main {\n  background-color: #fff;\n  border: 1px solid #e9e9e9;\n  border-radius: 3px;\n}\n\n.content-wrap {\n  padding: 30px;\n}\n\n.content-block {\n  padding: 0 0 20px;\n}\n\n.header {\n  width: 100%;\n  margin-bottom: 20px;\n}\n\n.footer {\n  width: 100%;\n  clear: both;\n  color: #999;\n  padding: 20px;\n}\n.footer p, .footer a, .footer td {\n  color: #999;\n  font-size: 12px;\n}\n\n/* -------------------------------------\n    TYPOGRAPHY\n------------------------------------- */\nh1, h2, h3 {\n  font-family: \"Helvetica Neue\", Helvetica, Arial, \"Lucida Grande\", sans-serif;\n  color: #000;\n  margin: 40px 0 0;\n  line-height: 1.2em;\n  font-weight: 400;\n}\n\nh1 {\n  font-size: 32px;\n  font-weight: 500;\n  /* 1.2em * 32px = 38.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */\n  /*line-height: 38px;*/\n}\n\nh2 {\n  font-size: 24px;\n  /* 1.2em * 24px = 28.8px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */\n  /*line-height: 29px;*/\n}\n\nh3 {\n  font-size: 18px;\n  /* 1.2em * 18px = 21.6px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */\n  /*line-height: 22px;*/\n}\n\nh4 {\n  font-size: 14px;\n  font-weight: 600;\n}\n\np, ul, ol {\n  margin-bottom: 10px;\n  font-weight: normal;\n}\np li, ul li, ol li {\n  margin-left: 5px;\n  list-style-position: inside;\n}\n\n/* -------------------------------------\n    LINKS & BUTTONS\n------------------------------------- */\na {\n  color: #348eda;\n  text-decoration: underline;\n}\n\n.btn-primary {\n  text-decoration: none;\n  color: #FFF;\n  background-color: #348eda;\n  border: solid #348eda;\n  border-width: 10px 20px;\n  line-height: 2em;\n  /* 2em * 14px = 28px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */\n  /*line-height: 28px;*/\n  font-weight: bold;\n  text-align: center;\n  cursor: pointer;\n  display: inline-block;\n  border-radius: 5px;\n  text-transform: capitalize;\n}\n\n/* -------------------------------------\n    OTHER STYLES THAT MIGHT BE USEFUL\n------------------------------------- */\n.last {\n  margin-bottom: 0;\n}\n\n.first {\n  margin-top: 0;\n}\n\n.aligncenter {\n  text-align: center;\n}\n\n.alignright {\n  text-align: right;\n}\n\n.alignleft {\n  text-align: left;\n}\n\n.clear {\n  clear: both;\n}\n\n/* -------------------------------------\n    ALERTS\n    Change the class depending on warning email, good email or bad email\n------------------------------------- */\n.alert {\n  font-size: 16px;\n  color: #fff;\n  font-weight: 500;\n  padding: 20px;\n  text-align: center;\n  border-radius: 3px 3px 0 0;\n}\n.alert a {\n  color: #fff;\n  text-decoration: none;\n  font-weight: 500;\n  font-size: 16px;\n}\n.alert.alert-warning {\n  background-color: #E6522C;\n}\n.alert.alert-bad {\n  background-color: #D0021B;\n}\n.alert.alert-good {\n  background-color: #68B90F;\n}\n\n/* -------------------------------------\n    INVOICE\n    Styles for the billing table\n------------------------------------- */\n.invoice {\n  margin: 40px auto;\n  text-align: left;\n  width: 80%;\n}\n.invoice td {\n  padding: 5px 0;\n}\n.invoice .invoice-items {\n  width: 100%;\n}\n.invoice .invoice-items td {\n  border-top: #eee 1px solid;\n}\n.invoice .invoice-items .total td {\n  border-top: 2px solid #333;\n  border-bottom: 2px solid #333;\n  font-weight: 700;\n}\n\n/* -------------------------------------\n    RESPONSIVE AND MOBILE FRIENDLY STYLES\n------------------------------------- */\n@media only screen and (max-width: 640px) {\n  body {\n    padding: 0 !important;\n  }\n\n  h1, h2, h3, h4 {\n    font-weight: 800 !important;\n    margin: 20px 0 5px !important;\n  }\n\n  h1 {\n    font-size: 22px !important;\n  }\n\n  h2 {\n    font-size: 18px !important;\n  }\n\n  h3 {\n    font-size: 16px !important;\n  }\n\n  .container {\n    padding: 0 !important;\n    width: 100% !important;\n  }\n\n  .content {\n    padding: 0 !important;\n  }\n\n  .content-wrap {\n    padding: 10px !important;\n  }\n\n  .invoice {\n    width: 100% !important;\n  }\n}\n</style>\n</head>\n\n<body itemscope itemtype=\"https://schema.org/EmailMessage\">\n\n<table class=\"body-wrap\">\n  <tr>\n    <td></td>\n    <td class=\"container\" width=\"600\">\n      <div class=\"content\">\n        <table class=\"main\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n          <tr>\n            {{ if gt (len .Alerts.Firing) 0 }}\n            <td class=\"alert alert-warning\">\n              {{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}\n                {{ .Name }}={{ .Value }}\n              {{ end }}\n            </td>\n            {{ else }}\n            <td class=\"alert alert-good\">\n              {{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}\n                {{ .Name }}={{ .Value }} \n              {{ end }}\n            </td>\n            {{ end }}\n          </tr>\n          <tr>\n            <td class=\"content-wrap\">\n              <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n                <tr>\n                  <td class=\"content-block\">\n                    <a href='{{ template \"__alertmanagerURL\" . }}' class=\"btn-primary\">View in {{ template \"__alertmanager\" . }}</a>\n                  </td>\n                </tr>\n                {{ if gt (len .Alerts.Firing) 0 }}\n                <tr>\n                  <td class=\"content-block\">\n                    <strong>[{{ .Alerts.Firing | len }}] Firing</strong>\n                  </td>\n                </tr>\n                {{ end }}\n                {{ range .Alerts.Firing }}\n                <tr>\n                  <td class=\"content-block\">\n                    <strong>Labels</strong><br />\n                    {{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}\n                    {{ if gt (len .Annotations) 0 }}<strong>Annotations</strong><br />{{ end }}\n                    {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}\n                    <a href=\"{{ .GeneratorURL }}\">Source</a><br />\n                  </td>\n                </tr>\n                {{ end }}\n\n                {{ if gt (len .Alerts.Resolved) 0 }}\n                  {{ if gt (len .Alerts.Firing) 0 }}\n                <tr>\n                  <td class=\"content-block\">\n                    <br />\n                    <hr />\n                    <br />\n                  </td>\n                </tr>\n                  {{ end }}\n                <tr>\n                  <td class=\"content-block\">\n                    <strong>[{{ .Alerts.Resolved | len }}] Resolved</strong>\n                  </td>\n                </tr>\n                {{ end }}\n                {{ range .Alerts.Resolved }}\n                <tr>\n                  <td class=\"content-block\">\n                    <strong>Labels</strong><br />\n                    {{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}\n                    {{ if gt (len .Annotations) 0 }}<strong>Annotations</strong><br />{{ end }}\n                    {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}\n                    <a href=\"{{ .GeneratorURL }}\">Source</a><br />\n                  </td>\n                </tr>\n                {{ end }}\n              </table>\n            </td>\n          </tr>\n        </table>\n\n        <div class=\"footer\">\n          <table width=\"100%\">\n            <tr>\n              <td class=\"aligncenter content-block\"><a href='{{ .ExternalURL }}'>Sent by {{ template \"__alertmanager\" . }}</a></td>\n            </tr>\n          </table>\n        </div></div>\n    </td>\n    <td></td>\n  </tr>\n</table>\n\n</body>\n</html>\n"
  },
  {
    "path": "template/email.tmpl",
    "content": "\n{{ define \"email.default.subject\" }}{{ template \"__subject\" . }}{{ end }}\n{{ define \"email.default.html\" }}\n<!--\nStyle and HTML derived from https://github.com/mailgun/transactional-email-templates\n\n\nThe MIT License (MIT)\n\nCopyright (c) 2014 Mailgun\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n-->\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n<head>\n<meta name=\"viewport\" content=\"width=device-width\">\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n<title>{{ template \"__subject\" . }}</title>\n<style>\n@media only screen and (max-width: 640px) {\n  body {\n    padding: 0 !important;\n  }\n\n  h1,\nh2,\nh3,\nh4 {\n    font-weight: 800 !important;\n    margin: 20px 0 5px !important;\n  }\n\n  h1 {\n    font-size: 22px !important;\n  }\n\n  h2 {\n    font-size: 18px !important;\n  }\n\n  h3 {\n    font-size: 16px !important;\n  }\n\n  .container {\n    padding: 0 !important;\n    width: 100% !important;\n  }\n\n  .content {\n    padding: 0 !important;\n  }\n\n  .content-wrap {\n    padding: 10px !important;\n  }\n\n  .invoice {\n    width: 100% !important;\n  }\n}\n</style>\n</head>\n\n<body itemscope itemtype=\"https://schema.org/EmailMessage\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 1.6em; background-color: #f6f6f6; width: 100%;\">\n\n<table class=\"body-wrap\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; background-color: #f6f6f6; width: 100%;\" width=\"100%\" bgcolor=\"#f6f6f6\">\n  <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n    <td style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top;\" valign=\"top\"></td>\n    <td class=\"container\" width=\"600\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block; max-width: 600px; margin: 0 auto; clear: both;\" valign=\"top\">\n      <div class=\"content\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; margin: 0 auto; display: block; padding: 20px;\">\n        <table class=\"main\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; background-color: #fff; border: 1px solid #e9e9e9; border-radius: 3px;\" bgcolor=\"#fff\">\n          <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n            {{ if gt (len .Alerts.Firing) 0 }}\n            <td class=\"alert alert-warning\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; vertical-align: top; font-size: 16px; color: #fff; font-weight: 500; padding: 20px; text-align: center; border-radius: 3px 3px 0 0; background-color: #E6522C;\" valign=\"top\" align=\"center\" bgcolor=\"#E6522C\">\n              {{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}\n                {{ .Name }}={{ .Value }}\n              {{ end }}\n            </td>\n            {{ else }}\n            <td class=\"alert alert-good\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; vertical-align: top; font-size: 16px; color: #fff; font-weight: 500; padding: 20px; text-align: center; border-radius: 3px 3px 0 0; background-color: #68B90F;\" valign=\"top\" align=\"center\" bgcolor=\"#68B90F\">\n              {{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}\n                {{ .Name }}={{ .Value }} \n              {{ end }}\n            </td>\n            {{ end }}\n          </tr>\n          <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n            <td class=\"content-wrap\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 30px;\" valign=\"top\">\n              <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n                <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n                  <td class=\"content-block\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;\" valign=\"top\">\n                    <a href=\"{{ template \"__alertmanagerURL\" . }}\" class=\"btn-primary\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; text-decoration: none; color: #FFF; background-color: #348eda; border: solid #348eda; border-width: 10px 20px; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize;\">View in {{ template \"__alertmanager\" . }}</a>\n                  </td>\n                </tr>\n                {{ if gt (len .Alerts.Firing) 0 }}\n                <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n                  <td class=\"content-block\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;\" valign=\"top\">\n                    <strong style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">[{{ .Alerts.Firing | len }}] Firing</strong>\n                  </td>\n                </tr>\n                {{ end }}\n                {{ range .Alerts.Firing }}\n                <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n                  <td class=\"content-block\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;\" valign=\"top\">\n                    <strong style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">Labels</strong><br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n                    {{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">{{ end }}\n                    {{ if gt (len .Annotations) 0 }}<strong style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">Annotations</strong><br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">{{ end }}\n                    {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">{{ end }}\n                    <a href=\"{{ .GeneratorURL }}\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #348eda; text-decoration: underline;\">Source</a><br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n                  </td>\n                </tr>\n                {{ end }}\n\n                {{ if gt (len .Alerts.Resolved) 0 }}\n                  {{ if gt (len .Alerts.Firing) 0 }}\n                <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n                  <td class=\"content-block\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;\" valign=\"top\">\n                    <br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n                    <hr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n                    <br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n                  </td>\n                </tr>\n                  {{ end }}\n                <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n                  <td class=\"content-block\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;\" valign=\"top\">\n                    <strong style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">[{{ .Alerts.Resolved | len }}] Resolved</strong>\n                  </td>\n                </tr>\n                {{ end }}\n                {{ range .Alerts.Resolved }}\n                <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n                  <td class=\"content-block\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;\" valign=\"top\">\n                    <strong style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">Labels</strong><br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n                    {{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">{{ end }}\n                    {{ if gt (len .Annotations) 0 }}<strong style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">Annotations</strong><br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">{{ end }}\n                    {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">{{ end }}\n                    <a href=\"{{ .GeneratorURL }}\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #348eda; text-decoration: underline;\">Source</a><br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n                  </td>\n                </tr>\n                {{ end }}\n              </table>\n            </td>\n          </tr>\n        </table>\n\n        <div class=\"footer\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; padding: 20px;\">\n          <table width=\"100%\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n            <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;\">\n              <td class=\"aligncenter content-block\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; vertical-align: top; padding: 0 0 20px; text-align: center; color: #999; font-size: 12px;\" valign=\"top\" align=\"center\"><a href=\"{{ .ExternalURL }}\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; text-decoration: underline; color: #999; font-size: 12px;\">Sent by {{ template \"__alertmanager\" . }}</a></td>\n            </tr>\n          </table>\n        </div></div>\n    </td>\n    <td style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top;\" valign=\"top\"></td>\n  </tr>\n</table>\n\n</body>\n</html>\n\n{{ end }}\n"
  },
  {
    "path": "template/inline-css.js",
    "content": "#!/usr/bin/env node\n\n// Copyright 2021 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nconst juice = require('juice')\nconst fs = require('fs')\n\nconst inputFile = 'email.html'\nconst outputFile = 'email.tmpl'\n\nvar inputData = ''\n\ntry {\n\tinputData = fs.readFileSync(inputFile, 'utf8')\n} catch (err) {\n\tconsole.error(err)\n\tprocess.exit(1)\n}\n\nvar templateData = juice(inputData)\n\nconst outputData = `\n{{ define \"email.default.subject\" }}{{ template \"__subject\" . }}{{ end }}\n{{ define \"email.default.html\" }}\n${templateData}\n{{ end }}\n`\n\nfs.writeFileSync(outputFile, outputData)\n"
  },
  {
    "path": "template/template.go",
    "content": "// Copyright 2015 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage template\n\nimport (\n\t\"bytes\"\n\t\"embed\"\n\t\"encoding/json\"\n\ttmplhtml \"html/template\"\n\t\"io\"\n\t\"net/url\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\ttmpltext \"text/template\"\n\t\"time\"\n\n\tcommonTemplates \"github.com/prometheus/common/helpers/templates\"\n\t\"github.com/prometheus/common/model\"\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n\t\"gopkg.in/yaml.v2\"\n\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\n//go:embed default.tmpl email.tmpl\nvar asset embed.FS\n\n// Template bundles a text and a html template instance.\ntype Template struct {\n\ttext *tmpltext.Template\n\thtml *tmplhtml.Template\n\n\tExternalURL *url.URL\n}\n\n// Option is generic modifier of the text and html templates used by a Template.\ntype Option func(text *tmpltext.Template, html *tmplhtml.Template)\n\n// New returns a new Template with the DefaultFuncs added. The DefaultFuncs\n// have precedence over any added custom functions. Options allow customization\n// of the text and html templates in given order.\nfunc New(options ...Option) (*Template, error) {\n\tt := &Template{\n\t\ttext: tmpltext.New(\"\").Option(\"missingkey=zero\"),\n\t\thtml: tmplhtml.New(\"\").Option(\"missingkey=zero\"),\n\t}\n\n\tfor _, o := range options {\n\t\to(t.text, t.html)\n\t}\n\n\tt.text.Funcs(tmpltext.FuncMap(DefaultFuncs))\n\tt.html.Funcs(tmplhtml.FuncMap(DefaultFuncs))\n\n\treturn t, nil\n}\n\n// FromGlobs calls ParseGlob on all path globs provided and returns the\n// resulting Template.\nfunc FromGlobs(paths []string, options ...Option) (*Template, error) {\n\tt, err := New(options...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefaultTemplates := []string{\"default.tmpl\", \"email.tmpl\"}\n\n\tfor _, file := range defaultTemplates {\n\t\tf, err := asset.Open(file)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := t.Parse(f); err != nil {\n\t\t\tf.Close()\n\t\t\treturn nil, err\n\t\t}\n\t\tf.Close()\n\t}\n\n\tfor _, tp := range paths {\n\t\tif err := t.FromGlob(tp); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn t, nil\n}\n\n// Parse parses the given text into the template.\nfunc (t *Template) Parse(r io.Reader) error {\n\tb, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif t.text, err = t.text.Parse(string(b)); err != nil {\n\t\treturn err\n\t}\n\tif t.html, err = t.html.Parse(string(b)); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// FromGlob calls ParseGlob on given path glob provided and parses into the\n// template.\nfunc (t *Template) FromGlob(path string) error {\n\t// ParseGlob in the template packages errors if not at least one file is\n\t// matched. We want to allow empty matches that may be populated later on.\n\tp, err := filepath.Glob(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(p) > 0 {\n\t\tif t.text, err = t.text.ParseGlob(path); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif t.html, err = t.html.ParseGlob(path); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// ExecuteTextString needs a meaningful doc comment (TODO(fabxc)).\nfunc (t *Template) ExecuteTextString(text string, data any) (string, error) {\n\tif text == \"\" {\n\t\treturn \"\", nil\n\t}\n\ttmpl, err := t.text.Clone()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\ttmpl, err = tmpl.New(\"\").Option(\"missingkey=zero\").Parse(text)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tvar buf bytes.Buffer\n\terr = tmpl.Execute(&buf, data)\n\treturn buf.String(), err\n}\n\n// ExecuteHTMLString needs a meaningful doc comment (TODO(fabxc)).\nfunc (t *Template) ExecuteHTMLString(html string, data any) (string, error) {\n\tif html == \"\" {\n\t\treturn \"\", nil\n\t}\n\ttmpl, err := t.html.Clone()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\ttmpl, err = tmpl.New(\"\").Option(\"missingkey=zero\").Parse(html)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tvar buf bytes.Buffer\n\terr = tmpl.Execute(&buf, data)\n\treturn buf.String(), err\n}\n\ntype FuncMap map[string]any\n\nvar DefaultFuncs = FuncMap{\n\t\"toUpper\": strings.ToUpper,\n\t\"toLower\": strings.ToLower,\n\t\"title\": func(text string) string {\n\t\t// Casers should not be shared between goroutines, instead\n\t\t// create a new caser each time this function is called.\n\t\treturn cases.Title(language.AmericanEnglish).String(text)\n\t},\n\t\"trimSpace\": strings.TrimSpace,\n\t// join is equal to strings.Join but inverts the argument order\n\t// for easier pipelining in templates.\n\t\"join\": func(sep string, s []string) string {\n\t\treturn strings.Join(s, sep)\n\t},\n\t\"match\": regexp.MatchString,\n\t\"safeHtml\": func(text string) tmplhtml.HTML {\n\t\treturn tmplhtml.HTML(text)\n\t},\n\t\"safeUrl\": func(text string) tmplhtml.URL {\n\t\treturn tmplhtml.URL(text)\n\t},\n\t\"urlUnescape\": url.QueryUnescape,\n\t\"reReplaceAll\": func(pattern, repl, text string) string {\n\t\tre := regexp.MustCompile(pattern)\n\t\treturn re.ReplaceAllString(text, repl)\n\t},\n\t\"stringSlice\": func(s ...string) []string {\n\t\treturn s\n\t},\n\t// date returns the text representation of the time in the specified format.\n\t\"date\": func(fmt string, t time.Time) string {\n\t\treturn t.Format(fmt)\n\t},\n\t// tz returns the time in the timezone.\n\t\"tz\": func(name string, t time.Time) (time.Time, error) {\n\t\tloc, err := time.LoadLocation(name)\n\t\tif err != nil {\n\t\t\treturn time.Time{}, err\n\t\t}\n\t\treturn t.In(loc), nil\n\t},\n\t\"since\":            time.Since,\n\t\"humanizeDuration\": commonTemplates.HumanizeDuration,\n\t\"toJson\": func(v any) (string, error) {\n\t\tbytes, err := json.Marshal(v)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn string(bytes), nil\n\t},\n}\n\n// Pair is a key/value string pair.\ntype Pair struct {\n\tName, Value string\n}\n\n// Pairs is a list of key/value string pairs.\ntype Pairs []Pair\n\n// Names returns a list of names of the pairs.\nfunc (ps Pairs) Names() []string {\n\tns := make([]string, 0, len(ps))\n\tfor _, p := range ps {\n\t\tns = append(ns, p.Name)\n\t}\n\treturn ns\n}\n\n// Values returns a list of values of the pairs.\nfunc (ps Pairs) Values() []string {\n\tvs := make([]string, 0, len(ps))\n\tfor _, p := range ps {\n\t\tvs = append(vs, p.Value)\n\t}\n\treturn vs\n}\n\nfunc (ps Pairs) String() string {\n\tb := strings.Builder{}\n\tfor i, p := range ps {\n\t\tb.WriteString(p.Name)\n\t\tb.WriteRune('=')\n\t\tb.WriteString(p.Value)\n\t\tif i < len(ps)-1 {\n\t\t\tb.WriteString(\", \")\n\t\t}\n\t}\n\treturn b.String()\n}\n\n// KV is a set of key/value string pairs.\ntype KV map[string]string\n\n// SortedPairs returns a sorted list of key/value pairs.\nfunc (kv KV) SortedPairs() Pairs {\n\tvar (\n\t\tpairs     = make([]Pair, 0, len(kv))\n\t\tkeys      = make([]string, 0, len(kv))\n\t\tsortStart = 0\n\t)\n\tfor k := range kv {\n\t\tif k == string(model.AlertNameLabel) {\n\t\t\tkeys = append([]string{k}, keys...)\n\t\t\tsortStart = 1\n\t\t} else {\n\t\t\tkeys = append(keys, k)\n\t\t}\n\t}\n\tsort.Strings(keys[sortStart:])\n\n\tfor _, k := range keys {\n\t\tpairs = append(pairs, Pair{k, kv[k]})\n\t}\n\treturn pairs\n}\n\n// Remove returns a copy of the key/value set without the given keys.\nfunc (kv KV) Remove(keys []string) KV {\n\tkeySet := make(map[string]struct{}, len(keys))\n\tfor _, k := range keys {\n\t\tkeySet[k] = struct{}{}\n\t}\n\n\tres := KV{}\n\tfor k, v := range kv {\n\t\tif _, ok := keySet[k]; !ok {\n\t\t\tres[k] = v\n\t\t}\n\t}\n\treturn res\n}\n\n// Names returns the names of the label names in the LabelSet.\nfunc (kv KV) Names() []string {\n\treturn kv.SortedPairs().Names()\n}\n\n// Values returns a list of the values in the LabelSet.\nfunc (kv KV) Values() []string {\n\treturn kv.SortedPairs().Values()\n}\n\nfunc (kv KV) String() string {\n\treturn kv.SortedPairs().String()\n}\n\n// Data is the data passed to notification templates and webhook pushes.\n//\n// End-users should not be exposed to Go's type system, as this will confuse them and prevent\n// simple things like simple equality checks to fail. Map everything to float64/string.\ntype Data struct {\n\tReceiver string `json:\"receiver\"`\n\tStatus   string `json:\"status\"`\n\tAlerts   Alerts `json:\"alerts\"`\n\n\tNotificationReason string `json:\"notification_reason\"`\n\n\tGroupLabels       KV `json:\"groupLabels\"`\n\tCommonLabels      KV `json:\"commonLabels\"`\n\tCommonAnnotations KV `json:\"commonAnnotations\"`\n\n\tExternalURL string `json:\"externalURL\"`\n}\n\n// Alert holds one alert for notification templates.\ntype Alert struct {\n\tStatus       string    `json:\"status\"`\n\tLabels       KV        `json:\"labels\"`\n\tAnnotations  KV        `json:\"annotations\"`\n\tStartsAt     time.Time `json:\"startsAt\"`\n\tEndsAt       time.Time `json:\"endsAt\"`\n\tGeneratorURL string    `json:\"generatorURL\"`\n\tFingerprint  string    `json:\"fingerprint\"`\n}\n\n// Alerts is a list of Alert objects.\ntype Alerts []Alert\n\n// Firing returns the subset of alerts that are firing.\nfunc (as Alerts) Firing() []Alert {\n\tres := []Alert{}\n\tfor _, a := range as {\n\t\tif a.Status == string(model.AlertFiring) {\n\t\t\tres = append(res, a)\n\t\t}\n\t}\n\treturn res\n}\n\n// Resolved returns the subset of alerts that are resolved.\nfunc (as Alerts) Resolved() []Alert {\n\tres := []Alert{}\n\tfor _, a := range as {\n\t\tif a.Status == string(model.AlertResolved) {\n\t\t\tres = append(res, a)\n\t\t}\n\t}\n\treturn res\n}\n\n// Data assembles data for template expansion.\nfunc (t *Template) Data(recv string, groupLabels model.LabelSet, notificationReason string, alerts ...*types.Alert) *Data {\n\ttypedAlerts := types.Alerts(alerts...)\n\n\tdata := &Data{\n\t\tReceiver:           regexp.QuoteMeta(recv),\n\t\tStatus:             string(typedAlerts.Status()),\n\t\tAlerts:             make(Alerts, 0, len(alerts)),\n\t\tNotificationReason: notificationReason,\n\t\tGroupLabels:        KV{},\n\t\tCommonLabels:       KV{},\n\t\tCommonAnnotations:  KV{},\n\t\tExternalURL:        t.ExternalURL.String(),\n\t}\n\n\t// The call to types.Alert is necessary to correctly resolve the internal\n\t// representation to the user representation.\n\tfor _, a := range typedAlerts {\n\t\talert := Alert{\n\t\t\tStatus:       string(a.Status()),\n\t\t\tLabels:       make(KV, len(a.Labels)),\n\t\t\tAnnotations:  make(KV, len(a.Annotations)),\n\t\t\tStartsAt:     a.StartsAt,\n\t\t\tEndsAt:       a.EndsAt,\n\t\t\tGeneratorURL: a.GeneratorURL,\n\t\t\tFingerprint:  a.Fingerprint().String(),\n\t\t}\n\t\tfor k, v := range a.Labels {\n\t\t\talert.Labels[string(k)] = string(v)\n\t\t}\n\t\tfor k, v := range a.Annotations {\n\t\t\talert.Annotations[string(k)] = string(v)\n\t\t}\n\t\tdata.Alerts = append(data.Alerts, alert)\n\t}\n\n\tfor k, v := range groupLabels {\n\t\tdata.GroupLabels[string(k)] = string(v)\n\t}\n\n\tif len(alerts) >= 1 {\n\t\tvar (\n\t\t\tcommonLabels      = alerts[0].Labels.Clone()\n\t\t\tcommonAnnotations = alerts[0].Annotations.Clone()\n\t\t)\n\t\tfor _, a := range alerts[1:] {\n\t\t\tif len(commonLabels) == 0 && len(commonAnnotations) == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tfor ln, lv := range commonLabels {\n\t\t\t\tif a.Labels[ln] != lv {\n\t\t\t\t\tdelete(commonLabels, ln)\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor an, av := range commonAnnotations {\n\t\t\t\tif a.Annotations[an] != av {\n\t\t\t\t\tdelete(commonAnnotations, an)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor k, v := range commonLabels {\n\t\t\tdata.CommonLabels[string(k)] = string(v)\n\t\t}\n\t\tfor k, v := range commonAnnotations {\n\t\t\tdata.CommonAnnotations[string(k)] = string(v)\n\t\t}\n\t}\n\n\treturn data\n}\n\ntype TemplateFunc func(string) (string, error)\n\n// DeepCopyWithTemplate returns a deep copy of a map/slice/array/string/int/bool or combination thereof, executing the\n// provided template (with the provided data) on all string keys or values. All maps are connverted to\n// map[string]any, with all non-string keys discarded.\nfunc DeepCopyWithTemplate(value any, tmplTextFunc TemplateFunc) (any, error) {\n\tif value == nil {\n\t\treturn value, nil\n\t}\n\n\tvalueMeta := reflect.ValueOf(value)\n\tswitch valueMeta.Kind() {\n\n\tcase reflect.String:\n\t\tparsed, ok := tmplTextFunc(value.(string))\n\t\tif ok == nil {\n\t\t\tvar inlineType any\n\t\t\terr := yaml.Unmarshal([]byte(parsed), &inlineType)\n\t\t\tif err != nil || (inlineType != nil && reflect.TypeOf(inlineType).Kind() == reflect.String) {\n\t\t\t\t// ignore error, thus the string is not an interface\n\t\t\t\treturn parsed, ok\n\t\t\t}\n\t\t\treturn DeepCopyWithTemplate(inlineType, tmplTextFunc)\n\t\t}\n\t\treturn parsed, ok\n\n\tcase reflect.Array, reflect.Slice:\n\t\tarrayLen := valueMeta.Len()\n\t\tconverted := make([]any, arrayLen)\n\t\tfor i := range arrayLen {\n\t\t\tvar err error\n\t\t\tconverted[i], err = DeepCopyWithTemplate(valueMeta.Index(i).Interface(), tmplTextFunc)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\treturn converted, nil\n\n\tcase reflect.Map:\n\t\tkeys := valueMeta.MapKeys()\n\t\tconverted := make(map[string]any, len(keys))\n\n\t\tfor _, keyMeta := range keys {\n\t\t\tvar err error\n\t\t\tstrKey, isString := keyMeta.Interface().(string)\n\t\t\tif !isString {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tstrKey, err = tmplTextFunc(strKey)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tconverted[strKey], err = DeepCopyWithTemplate(valueMeta.MapIndex(keyMeta).Interface(), tmplTextFunc)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\treturn converted, nil\n\tdefault:\n\t\treturn value, nil\n\t}\n}\n"
  },
  {
    "path": "template/template_test.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage template\n\nimport (\n\ttmplhtml \"html/template\"\n\t\"net/url\"\n\t\"sync\"\n\t\"testing\"\n\ttmpltext \"text/template\"\n\t\"time\"\n\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/prometheus/alertmanager/types\"\n)\n\nfunc TestPairNames(t *testing.T) {\n\tpairs := Pairs{\n\t\t{\"name1\", \"value1\"},\n\t\t{\"name2\", \"value2\"},\n\t\t{\"name3\", \"value3\"},\n\t}\n\n\texpected := []string{\"name1\", \"name2\", \"name3\"}\n\trequire.Equal(t, expected, pairs.Names())\n}\n\nfunc TestPairValues(t *testing.T) {\n\tpairs := Pairs{\n\t\t{\"name1\", \"value1\"},\n\t\t{\"name2\", \"value2\"},\n\t\t{\"name3\", \"value3\"},\n\t}\n\n\texpected := []string{\"value1\", \"value2\", \"value3\"}\n\trequire.Equal(t, expected, pairs.Values())\n}\n\nfunc TestPairsString(t *testing.T) {\n\tpairs := Pairs{{\"name1\", \"value1\"}}\n\trequire.Equal(t, \"name1=value1\", pairs.String())\n\tpairs = append(pairs, Pair{\"name2\", \"value2\"})\n\trequire.Equal(t, \"name1=value1, name2=value2\", pairs.String())\n}\n\nfunc TestKVSortedPairs(t *testing.T) {\n\tkv := KV{\"d\": \"dVal\", \"b\": \"bVal\", \"c\": \"cVal\"}\n\n\texpectedPairs := Pairs{\n\t\t{\"b\", \"bVal\"},\n\t\t{\"c\", \"cVal\"},\n\t\t{\"d\", \"dVal\"},\n\t}\n\n\tfor i, p := range kv.SortedPairs() {\n\t\trequire.Equal(t, p.Name, expectedPairs[i].Name)\n\t\trequire.Equal(t, p.Value, expectedPairs[i].Value)\n\t}\n\n\t// validates alertname always comes first\n\tkv = KV{\"d\": \"dVal\", \"b\": \"bVal\", \"c\": \"cVal\", \"alertname\": \"alert\", \"a\": \"aVal\"}\n\n\texpectedPairs = Pairs{\n\t\t{\"alertname\", \"alert\"},\n\t\t{\"a\", \"aVal\"},\n\t\t{\"b\", \"bVal\"},\n\t\t{\"c\", \"cVal\"},\n\t\t{\"d\", \"dVal\"},\n\t}\n\n\tfor i, p := range kv.SortedPairs() {\n\t\trequire.Equal(t, p.Name, expectedPairs[i].Name)\n\t\trequire.Equal(t, p.Value, expectedPairs[i].Value)\n\t}\n}\n\nfunc TestKVRemove(t *testing.T) {\n\tkv := KV{\n\t\t\"key1\": \"val1\",\n\t\t\"key2\": \"val2\",\n\t\t\"key3\": \"val3\",\n\t\t\"key4\": \"val4\",\n\t}\n\n\tkv = kv.Remove([]string{\"key2\", \"key4\"})\n\n\texpected := []string{\"key1\", \"key3\"}\n\trequire.Equal(t, expected, kv.Names())\n}\n\nfunc TestAlertsFiring(t *testing.T) {\n\talerts := Alerts{\n\t\t{Status: string(model.AlertFiring)},\n\t\t{Status: string(model.AlertResolved)},\n\t\t{Status: string(model.AlertFiring)},\n\t\t{Status: string(model.AlertResolved)},\n\t\t{Status: string(model.AlertResolved)},\n\t}\n\n\tfor _, alert := range alerts.Firing() {\n\t\tif alert.Status != string(model.AlertFiring) {\n\t\t\tt.Errorf(\"unexpected status %q\", alert.Status)\n\t\t}\n\t}\n}\n\nfunc TestAlertsResolved(t *testing.T) {\n\talerts := Alerts{\n\t\t{Status: string(model.AlertFiring)},\n\t\t{Status: string(model.AlertResolved)},\n\t\t{Status: string(model.AlertFiring)},\n\t\t{Status: string(model.AlertResolved)},\n\t\t{Status: string(model.AlertResolved)},\n\t}\n\n\tfor _, alert := range alerts.Resolved() {\n\t\tif alert.Status != string(model.AlertResolved) {\n\t\t\tt.Errorf(\"unexpected status %q\", alert.Status)\n\t\t}\n\t}\n}\n\nfunc TestData(t *testing.T) {\n\tu, err := url.Parse(\"http://example.com/\")\n\trequire.NoError(t, err)\n\ttmpl := &Template{ExternalURL: u}\n\tstartTime := time.Time{}.Add(1 * time.Second)\n\tendTime := time.Time{}.Add(2 * time.Second)\n\n\tfor _, tc := range []struct {\n\t\treceiver    string\n\t\tgroupLabels model.LabelSet\n\t\talerts      []*types.Alert\n\n\t\texp *Data\n\t}{\n\t\t{\n\t\t\treceiver: \"webhook\",\n\t\t\texp: &Data{\n\t\t\t\tReceiver:           \"webhook\",\n\t\t\t\tStatus:             \"resolved\",\n\t\t\t\tAlerts:             Alerts{},\n\t\t\t\tNotificationReason: \"first notification\",\n\t\t\t\tGroupLabels:        KV{},\n\t\t\t\tCommonLabels:       KV{},\n\t\t\t\tCommonAnnotations:  KV{},\n\t\t\t\tExternalURL:        u.String(),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\treceiver: \"webhook\",\n\t\t\tgroupLabels: model.LabelSet{\n\t\t\t\tmodel.LabelName(\"job\"): model.LabelValue(\"foo\"),\n\t\t\t},\n\t\t\talerts: []*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tStartsAt: startTime,\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\tmodel.LabelName(\"severity\"): model.LabelValue(\"warning\"),\n\t\t\t\t\t\t\tmodel.LabelName(\"job\"):      model.LabelValue(\"foo\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAnnotations: model.LabelSet{\n\t\t\t\t\t\t\tmodel.LabelName(\"description\"): model.LabelValue(\"something happened\"),\n\t\t\t\t\t\t\tmodel.LabelName(\"runbook\"):     model.LabelValue(\"foo\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tStartsAt: startTime,\n\t\t\t\t\t\tEndsAt:   endTime,\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\tmodel.LabelName(\"severity\"): model.LabelValue(\"critical\"),\n\t\t\t\t\t\t\tmodel.LabelName(\"job\"):      model.LabelValue(\"foo\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAnnotations: model.LabelSet{\n\t\t\t\t\t\t\tmodel.LabelName(\"description\"): model.LabelValue(\"something else happened\"),\n\t\t\t\t\t\t\tmodel.LabelName(\"runbook\"):     model.LabelValue(\"foo\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: &Data{\n\t\t\t\tReceiver: \"webhook\",\n\t\t\t\tStatus:   \"firing\",\n\t\t\t\tAlerts: Alerts{\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus:      \"firing\",\n\t\t\t\t\t\tLabels:      KV{\"severity\": \"warning\", \"job\": \"foo\"},\n\t\t\t\t\t\tAnnotations: KV{\"description\": \"something happened\", \"runbook\": \"foo\"},\n\t\t\t\t\t\tStartsAt:    startTime,\n\t\t\t\t\t\tFingerprint: \"9266ef3da838ad95\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus:      \"resolved\",\n\t\t\t\t\t\tLabels:      KV{\"severity\": \"critical\", \"job\": \"foo\"},\n\t\t\t\t\t\tAnnotations: KV{\"description\": \"something else happened\", \"runbook\": \"foo\"},\n\t\t\t\t\t\tStartsAt:    startTime,\n\t\t\t\t\t\tEndsAt:      endTime,\n\t\t\t\t\t\tFingerprint: \"3b15fd163d36582e\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tNotificationReason: \"first notification\",\n\t\t\t\tGroupLabels:        KV{\"job\": \"foo\"},\n\t\t\t\tCommonLabels:       KV{\"job\": \"foo\"},\n\t\t\t\tCommonAnnotations:  KV{\"runbook\": \"foo\"},\n\t\t\t\tExternalURL:        u.String(),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\treceiver:    \"webhook\",\n\t\t\tgroupLabels: model.LabelSet{},\n\t\t\talerts: []*types.Alert{\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tStartsAt: startTime,\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\tmodel.LabelName(\"severity\"): model.LabelValue(\"warning\"),\n\t\t\t\t\t\t\tmodel.LabelName(\"job\"):      model.LabelValue(\"foo\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAnnotations: model.LabelSet{\n\t\t\t\t\t\t\tmodel.LabelName(\"description\"): model.LabelValue(\"something happened\"),\n\t\t\t\t\t\t\tmodel.LabelName(\"runbook\"):     model.LabelValue(\"foo\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAlert: model.Alert{\n\t\t\t\t\t\tStartsAt: startTime,\n\t\t\t\t\t\tEndsAt:   endTime,\n\t\t\t\t\t\tLabels: model.LabelSet{\n\t\t\t\t\t\t\tmodel.LabelName(\"severity\"): model.LabelValue(\"critical\"),\n\t\t\t\t\t\t\tmodel.LabelName(\"job\"):      model.LabelValue(\"bar\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAnnotations: model.LabelSet{\n\t\t\t\t\t\t\tmodel.LabelName(\"description\"): model.LabelValue(\"something else happened\"),\n\t\t\t\t\t\t\tmodel.LabelName(\"runbook\"):     model.LabelValue(\"bar\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: &Data{\n\t\t\t\tReceiver: \"webhook\",\n\t\t\t\tStatus:   \"firing\",\n\t\t\t\tAlerts: Alerts{\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus:      \"firing\",\n\t\t\t\t\t\tLabels:      KV{\"severity\": \"warning\", \"job\": \"foo\"},\n\t\t\t\t\t\tAnnotations: KV{\"description\": \"something happened\", \"runbook\": \"foo\"},\n\t\t\t\t\t\tStartsAt:    startTime,\n\t\t\t\t\t\tFingerprint: \"9266ef3da838ad95\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus:      \"resolved\",\n\t\t\t\t\t\tLabels:      KV{\"severity\": \"critical\", \"job\": \"bar\"},\n\t\t\t\t\t\tAnnotations: KV{\"description\": \"something else happened\", \"runbook\": \"bar\"},\n\t\t\t\t\t\tStartsAt:    startTime,\n\t\t\t\t\t\tEndsAt:      endTime,\n\t\t\t\t\t\tFingerprint: \"c7e68cb08e3e67f9\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tNotificationReason: \"first notification\",\n\t\t\t\tGroupLabels:        KV{},\n\t\t\t\tCommonLabels:       KV{},\n\t\t\t\tCommonAnnotations:  KV{},\n\t\t\t\tExternalURL:        u.String(),\n\t\t\t},\n\t\t},\n\t} {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tgot := tmpl.Data(tc.receiver, tc.groupLabels, \"first notification\", tc.alerts...)\n\t\t\trequire.Equal(t, tc.exp, got)\n\t\t})\n\t}\n}\n\nfunc TestTemplateExpansion(t *testing.T) {\n\ttmpl, err := FromGlobs([]string{})\n\trequire.NoError(t, err)\n\n\tfor _, tc := range []struct {\n\t\ttitle string\n\t\tin    string\n\t\tdata  any\n\t\thtml  bool\n\n\t\texp  string\n\t\tfail bool\n\t}{\n\t\t{\n\t\t\ttitle: \"Template without action\",\n\t\t\tin:    `abc`,\n\t\t\texp:   \"abc\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template with simple action\",\n\t\t\tin:    `{{ \"abc\" }}`,\n\t\t\texp:   \"abc\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template with invalid syntax\",\n\t\t\tin:    `{{ `,\n\t\t\tfail:  true,\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using toUpper\",\n\t\t\tin:    `{{ \"abc\" | toUpper }}`,\n\t\t\texp:   \"ABC\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using toLower\",\n\t\t\tin:    `{{ \"ABC\" | toLower }}`,\n\t\t\texp:   \"abc\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using title\",\n\t\t\tin:    `{{ \"abc\" | title }}`,\n\t\t\texp:   \"Abc\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using TrimSpace\",\n\t\t\tin:    `{{ \" a b c \" | trimSpace }}`,\n\t\t\texp:   \"a b c\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using positive match\",\n\t\t\tin:    `{{ if match \"^a\" \"abc\"}}abc{{ end }}`,\n\t\t\texp:   \"abc\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using negative match\",\n\t\t\tin:    `{{ if match \"abcd\" \"abc\" }}abc{{ end }}`,\n\t\t\texp:   \"\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using join\",\n\t\t\tin:    `{{ . | join \",\" }}`,\n\t\t\tdata:  []string{\"a\", \"b\", \"c\"},\n\t\t\texp:   \"a,b,c\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"Text template without HTML escaping\",\n\t\t\tin:    `{{ \"<b>\" }}`,\n\t\t\texp:   \"<b>\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"HTML template with escaping\",\n\t\t\tin:    `{{ \"<b>\" }}`,\n\t\t\thtml:  true,\n\t\t\texp:   \"&lt;b&gt;\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"HTML template using safeHTML\",\n\t\t\tin:    `{{ \"<b>\" | safeHtml }}`,\n\t\t\thtml:  true,\n\t\t\texp:   \"<b>\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"URL template with escaping\",\n\t\t\tin:    `<a href=\"/search?{{ \"q=test%20foo\" }}\"></a>`,\n\t\t\thtml:  true,\n\t\t\texp:   `<a href=\"/search?q%3dtest%2520foo\"></a>`,\n\t\t},\n\t\t{\n\t\t\ttitle: \"URL template using safeUrl\",\n\t\t\tin:    `<a href=\"/search?{{ \"q=test%20foo\" | safeUrl }}\"></a>`,\n\t\t\thtml:  true,\n\t\t\texp:   `<a href=\"/search?q=test%20foo\"></a>`,\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using reReplaceAll\",\n\t\t\tin:    `{{ reReplaceAll \"ab\" \"AB\" \"abcdabcda\"}}`,\n\t\t\texp:   \"ABcdABcda\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using urlUnescape\",\n\t\t\tin:    `{{ \"search?q=test%20foo\" | urlUnescape }}`,\n\t\t\texp:   \"search?q=test foo\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using stringSlice\",\n\t\t\tin:    `{{ with .GroupLabels }}{{ with .Remove (stringSlice \"key1\" \"key3\") }}{{ .SortedPairs.Values }}{{ end }}{{ end }}`,\n\t\t\tdata: Data{\n\t\t\t\tGroupLabels: KV{\n\t\t\t\t\t\"key1\": \"key1\",\n\t\t\t\t\t\"key2\": \"key2\",\n\t\t\t\t\t\"key3\": \"key3\",\n\t\t\t\t\t\"key4\": \"key4\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: \"[key2 key4]\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using toJson with string\",\n\t\t\tin:    `{{ \"test\" | toJson }}`,\n\t\t\texp:   `\"test\"`,\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using toJson with number\",\n\t\t\tin:    `{{ 42 | toJson }}`,\n\t\t\texp:   `42`,\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using toJson with boolean\",\n\t\t\tin:    `{{ true | toJson }}`,\n\t\t\texp:   `true`,\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using toJson with map\",\n\t\t\tin:    `{{ . | toJson }}`,\n\t\t\tdata:  map[string]any{\"key\": \"value\", \"number\": 123},\n\t\t\texp:   `{\"key\":\"value\",\"number\":123}`,\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using toJson with slice\",\n\t\t\tin:    `{{ . | toJson }}`,\n\t\t\tdata:  []string{\"a\", \"b\", \"c\"},\n\t\t\texp:   `[\"a\",\"b\",\"c\"]`,\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using toJson with KV\",\n\t\t\tin:    `{{ .CommonLabels | toJson }}`,\n\t\t\tdata: Data{\n\t\t\t\tCommonLabels: KV{\"severity\": \"critical\", \"job\": \"foo\"},\n\t\t\t},\n\t\t\texp: `{\"job\":\"foo\",\"severity\":\"critical\"}`,\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using toJson with Alerts\",\n\t\t\tin:    `{{ .Alerts | toJson }}`,\n\t\t\tdata: Data{\n\t\t\t\tAlerts: Alerts{\n\t\t\t\t\t{\n\t\t\t\t\t\tStatus: \"firing\",\n\t\t\t\t\t\tLabels: KV{\"alertname\": \"test\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: `[{\"status\":\"firing\",\"labels\":{\"alertname\":\"test\"},\"annotations\":null,\"startsAt\":\"0001-01-01T00:00:00Z\",\"endsAt\":\"0001-01-01T00:00:00Z\",\"generatorURL\":\"\",\"fingerprint\":\"\"}]`,\n\t\t},\n\t\t{\n\t\t\ttitle: \"Template using toJson with Alerts.Firing()\",\n\t\t\tin:    `{{ .Alerts.Firing | toJson }}`,\n\t\t\tdata: Data{\n\t\t\t\tAlerts: Alerts{\n\t\t\t\t\t{Status: \"firing\"},\n\t\t\t\t\t{Status: \"resolved\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texp: `[{\"status\":\"firing\",\"labels\":null,\"annotations\":null,\"startsAt\":\"0001-01-01T00:00:00Z\",\"endsAt\":\"0001-01-01T00:00:00Z\",\"generatorURL\":\"\",\"fingerprint\":\"\"}]`,\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\tf := tmpl.ExecuteTextString\n\t\t\tif tc.html {\n\t\t\t\tf = tmpl.ExecuteHTMLString\n\t\t\t}\n\t\t\tgot, err := f(tc.in, tc.data)\n\t\t\tif tc.fail {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.exp, got)\n\t\t})\n\t}\n}\n\nfunc TestTemplateExpansionWithOptions(t *testing.T) {\n\ttestOptionWithAdditionalFuncs := func(funcs FuncMap) Option {\n\t\treturn func(text *tmpltext.Template, html *tmplhtml.Template) {\n\t\t\ttext.Funcs(tmpltext.FuncMap(funcs))\n\t\t\thtml.Funcs(tmplhtml.FuncMap(funcs))\n\t\t}\n\t}\n\tfor _, tc := range []struct {\n\t\toptions []Option\n\t\ttitle   string\n\t\tin      string\n\t\tdata    any\n\t\thtml    bool\n\n\t\texp  string\n\t\tfail bool\n\t}{\n\t\t{\n\t\t\ttitle:   \"Test custom function\",\n\t\t\toptions: []Option{testOptionWithAdditionalFuncs(FuncMap{\"printFoo\": func() string { return \"foo\" }})},\n\t\t\tin:      `{{ printFoo }}`,\n\t\t\texp:     \"foo\",\n\t\t},\n\t\t{\n\t\t\ttitle:   \"Test Default function with additional function added\",\n\t\t\toptions: []Option{testOptionWithAdditionalFuncs(FuncMap{\"printFoo\": func() string { return \"foo\" }})},\n\t\t\tin:      `{{ toUpper \"test\" }}`,\n\t\t\texp:     \"TEST\",\n\t\t},\n\t\t{\n\t\t\ttitle:   \"Test custom function is overridden by the DefaultFuncs\",\n\t\t\toptions: []Option{testOptionWithAdditionalFuncs(FuncMap{\"toUpper\": func(s string) string { return \"foo\" }})},\n\t\t\tin:      `{{ toUpper \"test\" }}`,\n\t\t\texp:     \"TEST\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"Test later Option overrides the previous\",\n\t\t\toptions: []Option{\n\t\t\t\ttestOptionWithAdditionalFuncs(FuncMap{\"printFoo\": func() string { return \"foo\" }}),\n\t\t\t\ttestOptionWithAdditionalFuncs(FuncMap{\"printFoo\": func() string { return \"bar\" }}),\n\t\t\t},\n\t\t\tin:  `{{ printFoo }}`,\n\t\t\texp: \"bar\",\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\ttmpl, err := FromGlobs([]string{}, tc.options...)\n\t\t\trequire.NoError(t, err)\n\t\t\tf := tmpl.ExecuteTextString\n\t\t\tif tc.html {\n\t\t\t\tf = tmpl.ExecuteHTMLString\n\t\t\t}\n\t\t\tgot, err := f(tc.in, tc.data)\n\t\t\tif tc.fail {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.exp, got)\n\t\t})\n\t}\n}\n\n// This test asserts that template functions are thread-safe.\nfunc TestTemplateFuncs(t *testing.T) {\n\ttmpl, err := FromGlobs([]string{})\n\trequire.NoError(t, err)\n\n\tfor _, tc := range []struct {\n\t\ttitle  string\n\t\tin     string\n\t\tdata   any\n\t\texp    string\n\t\texpErr string\n\t}{{\n\t\ttitle: \"Template using toUpper\",\n\t\tin:    `{{ \"abc\" | toUpper }}`,\n\t\texp:   \"ABC\",\n\t}, {\n\t\ttitle: \"Template using toLower\",\n\t\tin:    `{{ \"ABC\" | toLower }}`,\n\t\texp:   \"abc\",\n\t}, {\n\t\ttitle: \"Template using title\",\n\t\tin:    `{{ \"abc\" | title }}`,\n\t\texp:   \"Abc\",\n\t}, {\n\t\ttitle: \"Template using trimSpace\",\n\t\tin:    `{{ \" abc \" | trimSpace }}`,\n\t\texp:   \"abc\",\n\t}, {\n\t\ttitle: \"Template using join\",\n\t\tin:    `{{ . | join \",\" }}`,\n\t\tdata:  []string{\"abc\", \"def\"},\n\t\texp:   \"abc,def\",\n\t}, {\n\t\ttitle: \"Template using match\",\n\t\tin:    `{{ match \"[a-z]+\" \"abc\" }}`,\n\t\texp:   \"true\",\n\t}, {\n\t\ttitle: \"Template using reReplaceAll\",\n\t\tin:    `{{ reReplaceAll \"ab\" \"AB\" \"abc\" }}`,\n\t\texp:   \"ABc\",\n\t}, {\n\t\ttitle: \"Template using date\",\n\t\tin:    `{{ . | date \"2006-01-02\" }}`,\n\t\tdata:  time.Date(2024, 1, 1, 8, 15, 30, 0, time.UTC),\n\t\texp:   \"2024-01-01\",\n\t}, {\n\t\ttitle: \"Template using tz\",\n\t\tin:    `{{ . | tz \"Europe/Paris\" }}`,\n\t\tdata:  time.Date(2024, 1, 1, 8, 15, 30, 0, time.UTC),\n\t\texp:   \"2024-01-01 09:15:30 +0100 CET\",\n\t}, {\n\t\ttitle:  \"Template using invalid tz\",\n\t\tin:     `{{ . | tz \"Invalid/Timezone\" }}`,\n\t\tdata:   time.Date(2024, 1, 1, 8, 15, 30, 0, time.UTC),\n\t\texpErr: \"template: :1:7: executing \\\"\\\" at <tz \\\"Invalid/Timezone\\\">: error calling tz: unknown time zone Invalid/Timezone\",\n\t}, {\n\t\ttitle: \"Template using HumanizeDuration - seconds - float64\",\n\t\tin:    \"{{ range . }}{{ humanizeDuration . }}:{{ end }}\",\n\t\tdata:  []float64{0, 1, 60, 3600, 86400, 86400 + 3600, -(86400*2 + 3600*3 + 60*4 + 5), 899.99},\n\t\texp:   \"0s:1s:1m 0s:1h 0m 0s:1d 0h 0m 0s:1d 1h 0m 0s:-2d 3h 4m 5s:14m 59s:\",\n\t}, {\n\t\ttitle: \"Template using HumanizeDuration - seconds - string.\",\n\t\tin:    \"{{ range . }}{{ humanizeDuration . }}:{{ end }}\",\n\t\tdata:  []string{\"0\", \"1\", \"60\", \"3600\", \"86400\"},\n\t\texp:   \"0s:1s:1m 0s:1h 0m 0s:1d 0h 0m 0s:\",\n\t}, {\n\t\ttitle: \"Template using HumanizeDuration - subsecond and fractional seconds - float64.\",\n\t\tin:    \"{{ range . }}{{ humanizeDuration . }}:{{ end }}\",\n\t\tdata:  []float64{.1, .0001, .12345, 60.1, 60.5, 1.2345, 12.345},\n\t\texp:   \"100ms:100us:123.5ms:1m 0s:1m 0s:1.234s:12.35s:\",\n\t}, {\n\t\ttitle: \"Template using HumanizeDuration - subsecond and fractional seconds - string.\",\n\t\tin:    \"{{ range . }}{{ humanizeDuration . }}:{{ end }}\",\n\t\tdata:  []string{\".1\", \".0001\", \".12345\", \"60.1\", \"60.5\", \"1.2345\", \"12.345\"},\n\t\texp:   \"100ms:100us:123.5ms:1m 0s:1m 0s:1.234s:12.35s:\",\n\t}, {\n\t\ttitle:  \"Template using HumanizeDuration - string with error.\",\n\t\tin:     `{{ humanizeDuration \"one\" }}`,\n\t\texpErr: \"template: :1:3: executing \\\"\\\" at <humanizeDuration \\\"one\\\">: error calling humanizeDuration: strconv.ParseFloat: parsing \\\"one\\\": invalid syntax\",\n\t}, {\n\t\ttitle: \"Template using HumanizeDuration - int.\",\n\t\tin:    \"{{ range . }}{{ humanizeDuration . }}:{{ end }}\",\n\t\tdata:  []int{0, -1, 1, 1234567},\n\t\texp:   \"0s:-1s:1s:14d 6h 56m 7s:\",\n\t}, {\n\t\ttitle: \"Template using HumanizeDuration - uint.\",\n\t\tin:    \"{{ range . }}{{ humanizeDuration . }}:{{ end }}\",\n\t\tdata:  []uint{0, 1, 1234567},\n\t\texp:   \"0s:1s:14d 6h 56m 7s:\",\n\t}, {\n\t\ttitle: \"Template using since\",\n\t\tin:    \"{{ . | since | humanizeDuration }}\",\n\t\tdata:  time.Now().Add(-1 * time.Hour),\n\t\texp:   \"1h 0m 0s\",\n\t}, {\n\t\ttitle: \"Template using toJson with string\",\n\t\tin:    `{{ \"hello\" | toJson }}`,\n\t\texp:   `\"hello\"`,\n\t}, {\n\t\ttitle: \"Template using toJson with map\",\n\t\tin:    `{{ . | toJson }}`,\n\t\tdata:  map[string]string{\"key\": \"value\"},\n\t\texp:   `{\"key\":\"value\"}`,\n\t}, {\n\t\ttitle: \"Template using toJson with Alerts.Firing()\",\n\t\tin:    `{{ .Alerts.Firing | toJson }}`,\n\t\tdata: Data{\n\t\t\tAlerts: Alerts{\n\t\t\t\t{Status: \"firing\", Labels: KV{\"alertname\": \"test\"}},\n\t\t\t\t{Status: \"resolved\"},\n\t\t\t},\n\t\t},\n\t\texp: `[{\"status\":\"firing\",\"labels\":{\"alertname\":\"test\"},\"annotations\":null,\"startsAt\":\"0001-01-01T00:00:00Z\",\"endsAt\":\"0001-01-01T00:00:00Z\",\"generatorURL\":\"\",\"fingerprint\":\"\"}]`,\n\t}} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\twg := sync.WaitGroup{}\n\t\t\tfor range 10 {\n\t\t\t\twg.Go(func() {\n\t\t\t\t\tgot, err := tmpl.ExecuteTextString(tc.in, tc.data)\n\t\t\t\t\tif tc.expErr == \"\" {\n\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\trequire.Equal(t, tc.exp, got)\n\t\t\t\t\t} else {\n\t\t\t\t\t\trequire.EqualError(t, err, tc.expErr)\n\t\t\t\t\t\trequire.Empty(t, got)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t\twg.Wait()\n\t\t})\n\t}\n}\n\nfunc TestDeepCopyWithTemplate(t *testing.T) {\n\tidentity := TemplateFunc(func(s string) (string, error) { return s, nil })\n\twithSuffix := TemplateFunc(func(s string) (string, error) { return s + \"-templated\", nil })\n\n\tfor _, tc := range []struct {\n\t\ttitle   string\n\t\tinput   any\n\t\tfn      TemplateFunc\n\t\twant    any\n\t\twantErr string\n\t}{\n\t\t{\n\t\t\ttitle: \"string keeps templated value\",\n\t\t\tinput: \"hello\",\n\t\t\tfn:    withSuffix,\n\t\t\twant:  \"hello-templated\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"string parsed as YAML map\",\n\t\t\tinput: \"foo: bar\",\n\t\t\tfn:    identity,\n\t\t\twant:  map[string]any{\"foo\": \"bar\"},\n\t\t},\n\t\t{\n\t\t\ttitle: \"slice templating applied recursively\",\n\t\t\tinput: []any{\"foo\", 42},\n\t\t\tfn:    withSuffix,\n\t\t\twant:  []any{\"foo-templated\", 42},\n\t\t},\n\t\t{\n\t\t\ttitle: \"map converts keys and drops non-string\",\n\t\t\tinput: map[any]any{\n\t\t\t\t\"foo\":    \"bar\",\n\t\t\t\t42:       \"ignore\",\n\t\t\t\t\"nested\": []any{\"baz\"},\n\t\t\t},\n\t\t\tfn: withSuffix,\n\t\t\twant: map[string]any{\n\t\t\t\t\"foo-templated\":    \"bar-templated\",\n\t\t\t\t\"nested-templated\": []any{\"baz-templated\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttitle: \"non string value returned as-is\",\n\t\t\tinput: 123,\n\t\t\tfn:    identity,\n\t\t\twant:  123,\n\t\t},\n\t\t{\n\t\t\ttitle: \"nil input\",\n\t\t\tinput: nil,\n\t\t\tfn:    identity,\n\t\t\twant:  nil,\n\t\t},\n\t} {\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\tgot, err := DeepCopyWithTemplate(tc.input, tc.fn)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.want, got)\n\t\t})\n\t}\n}\n\nfunc BenchmarkTemplateData(b *testing.B) {\n\tu, _ := url.Parse(\"http://example.com/\")\n\ttmpl := &Template{ExternalURL: u}\n\n\tnow := time.Now()\n\talerts := make([]*types.Alert, 50)\n\tfor i := range alerts {\n\t\talerts[i] = &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels:      model.LabelSet{\"alertname\": \"test\", \"job\": \"bench\"},\n\t\t\t\tAnnotations: model.LabelSet{\"summary\": \"test alert\"},\n\t\t\t\tStartsAt:    now,\n\t\t\t\tEndsAt:      now.Add(time.Hour),\n\t\t\t},\n\t\t}\n\t}\n\tgroupLabels := model.LabelSet{\"alertname\": \"test\"}\n\n\tb.ResetTimer()\n\tfor b.Loop() {\n\t\ttmpl.Data(\"receiver\", groupLabels, \"firing\", alerts...)\n\t}\n}\n\nfunc BenchmarkTypesAlerts(b *testing.B) {\n\tnow := time.Now()\n\talerts := make([]*types.Alert, 50)\n\tfor i := range alerts {\n\t\talerts[i] = &types.Alert{\n\t\t\tAlert: model.Alert{\n\t\t\t\tLabels:      model.LabelSet{\"alertname\": \"test\", \"job\": \"bench\"},\n\t\t\t\tAnnotations: model.LabelSet{\"summary\": \"test alert\"},\n\t\t\t\tStartsAt:    now,\n\t\t\t\tEndsAt:      now.Add(time.Hour),\n\t\t\t},\n\t\t}\n\t}\n\n\tb.Run(\"SingleCall\", func(b *testing.B) {\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\ttyped := types.Alerts(alerts...)\n\t\t\t_ = typed.Status()\n\t\t\tfor range typed {\n\t\t\t}\n\t\t}\n\t})\n\n\tb.Run(\"DuplicateCall\", func(b *testing.B) {\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\t_ = types.Alerts(alerts...).Status()\n\t\t\tfor range types.Alerts(alerts...) {\n\t\t\t}\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "test/cli/acceptance/cli_test.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage test\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t. \"github.com/prometheus/alertmanager/test/cli\"\n)\n\nfunc TestMain(m *testing.M) {\n\tif ok, err := AmtoolOk(); !ok {\n\t\tpanic(\"unable to access amtool binary: \" + err.Error())\n\t}\n\tos.Exit(m.Run())\n}\n\n// TestAmtoolVersion checks that amtool is executable and\n// is reporting valid version info.\nfunc TestAmtoolVersion(t *testing.T) {\n\tt.Parallel()\n\tversion, err := Version()\n\tif err != nil {\n\t\tt.Fatal(\"Unable to get amtool version\", err)\n\t}\n\tt.Logf(\"testing amtool version: %v\", version)\n}\n\nfunc TestAddAlert(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: [alertname]\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1ms\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n    send_resolved: true\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 150 * time.Millisecond,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\n\tam := amc.Members()[0]\n\n\talert1 := Alert(\"alertname\", \"test1\").Active(1, 2)\n\tam.AddAlertsAt(false, 0, alert1)\n\tco.Want(Between(1, 2), Alert(\"alertname\", \"test1\").Active(1))\n\n\tat.Run()\n\n\tt.Log(co.Check())\n}\n\nfunc TestQueryAlert(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: [alertname]\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1ms\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n    send_resolved: true\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 1 * time.Second,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\trequire.NoError(t, amc.Start())\n\tdefer amc.Terminate()\n\n\tam := amc.Members()[0]\n\n\talert1 := Alert(\"alertname\", \"test1\", \"severity\", \"warning\").Active(1)\n\talert2 := Alert(\"alertname\", \"alertname=test2\", \"severity\", \"info\").Active(1)\n\talert3 := Alert(\"alertname\", \"{alertname=test3}\", \"severity\", \"info\").Active(1)\n\tam.AddAlerts(true, alert1, alert2, alert3)\n\n\talerts, err := am.QueryAlerts()\n\trequire.NoError(t, err)\n\trequire.Len(t, alerts, 3)\n\n\t// Get the first alert using the alertname heuristic\n\talerts, err = am.QueryAlerts(\"test1\")\n\trequire.NoError(t, err)\n\trequire.Len(t, alerts, 1)\n\n\t// QueryAlerts uses the simple output option, which means just the alertname\n\t// label is printed. We can assert that querying works as expected as we know\n\t// there are two alerts called \"test1\" and \"test2\".\n\texpectedLabels := models.LabelSet{\"alertname\": \"test1\"}\n\trequire.True(t, alerts[0].HasLabels(expectedLabels))\n\n\t// Get the second alert\n\talerts, err = am.QueryAlerts(\"alertname=test2\")\n\trequire.NoError(t, err)\n\trequire.Len(t, alerts, 1)\n\texpectedLabels = models.LabelSet{\"alertname\": \"test2\"}\n\trequire.True(t, alerts[0].HasLabels(expectedLabels))\n\n\t// Get the third alert\n\talerts, err = am.QueryAlerts(\"{alertname=test3}\")\n\trequire.NoError(t, err)\n\trequire.Len(t, alerts, 1)\n\texpectedLabels = models.LabelSet{\"alertname\": \"{alertname=test3}\"}\n\trequire.True(t, alerts[0].HasLabels(expectedLabels))\n}\n\nfunc TestQuerySilence(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: [alertname]\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1ms\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n    send_resolved: true\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 1 * time.Second,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\trequire.NoError(t, amc.Start())\n\tdefer amc.Terminate()\n\n\tam := amc.Members()[0]\n\n\tsilence1 := Silence(0, 4).Match(\"test1\", \"severity=warn\").Comment(\"test1\")\n\tsilence2 := Silence(0, 4).Match(\"alertname=test2\", \"severity=warn\").Comment(\"test2\")\n\tsilence3 := Silence(0, 4).Match(\"{alertname=test3}\", \"severity=warn\").Comment(\"test3\")\n\n\tam.SetSilence(0, silence1)\n\tam.SetSilence(0, silence2)\n\tam.SetSilence(0, silence3)\n\n\t// Get all silences\n\tsils, err := am.QuerySilence()\n\trequire.NoError(t, err)\n\trequire.Len(t, sils, 3)\n\texpected1 := []string{\"alertname=\\\"test1\\\"\", \"severity=\\\"warn\\\"\"}\n\trequire.Equal(t, expected1, sils[0].GetMatches())\n\texpected2 := []string{\"alertname=\\\"test2\\\"\", \"severity=\\\"warn\\\"\"}\n\trequire.Equal(t, expected2, sils[1].GetMatches())\n\texpected3 := []string{\"alertname=\\\"{alertname=test3}\\\"\", \"severity=\\\"warn\\\"\"}\n\trequire.Equal(t, expected3, sils[2].GetMatches())\n\n\t// Get the first silence using the alertname heuristic\n\tsils, err = am.QuerySilence(\"test1\")\n\trequire.NoError(t, err)\n\trequire.Len(t, sils, 1)\n\trequire.Equal(t, expected1, sils[0].GetMatches())\n}\n\nfunc TestRoutesShow(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: [alertname]\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1ms\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n    send_resolved: true\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 1 * time.Second,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\trequire.NoError(t, amc.Start())\n\tdefer amc.Terminate()\n\n\tam := amc.Members()[0]\n\t_, err := am.ShowRoute()\n\trequire.NoError(t, err)\n}\n\nfunc TestRoutesTest(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: [alertname]\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1ms\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n    send_resolved: true\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 1 * time.Second,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\trequire.NoError(t, amc.Start())\n\tdefer amc.Terminate()\n\n\tam := amc.Members()[0]\n\t_, err := am.TestRoute()\n\trequire.NoError(t, err)\n\n\t// Bad labels should return error\n\tout, err := am.TestRoute(\"{foo=bar}\")\n\trequire.EqualError(t, err, \"exit status 1\")\n\trequire.Equal(t, \"amtool: error: Failed to parse labels: unexpected open or close brace: {foo=bar}\\n\\n\", string(out))\n}\n\nfunc TestSilenceImport(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: [alertname]\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1ms\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n    send_resolved: true\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 1 * time.Second,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\trequire.NoError(t, amc.Start())\n\tdefer amc.Terminate()\n\n\tam := amc.Members()[0]\n\n\t// Add some test silences\n\tsilence1 := Silence(0, 4).Match(\"alertname=test1\", \"severity=warning\").Comment(\"test silence 1\")\n\tsilence2 := Silence(0, 4).Match(\"alertname=test2\", \"severity=critical\").Comment(\"test silence 2\")\n\n\tam.SetSilence(0, silence1)\n\tam.SetSilence(0, silence2)\n\n\t// Export silences to JSON file\n\ttmpDir := t.TempDir()\n\texportFile := tmpDir + \"/silences.json\"\n\n\texportOut, err := am.ExportSilences()\n\trequire.NoError(t, err)\n\n\t// Write to file\n\terr = os.WriteFile(exportFile, exportOut, 0o644)\n\trequire.NoError(t, err)\n\n\t// Query current silences to get their IDs, then expire them\n\tsils, err := am.QuerySilence()\n\trequire.NoError(t, err)\n\trequire.Len(t, sils, 2)\n\tsilIDs := make([]string, 0, len(sils))\n\n\t// Expire all silences by ID\n\tfor _, sil := range sils {\n\t\tid := sil.ID()\n\t\t_, err := am.ExpireSilenceByID(id)\n\t\trequire.NoError(t, err)\n\t\tsilIDs = append(silIDs, id)\n\t}\n\n\t// Verify silences show as expired\n\tsils, err = am.QueryExpiredSilence()\n\trequire.NoError(t, err)\n\t// Silences should still be queryable but in expired state\n\trequire.Len(t, sils, 2, \"expired silences should still be queryable\")\n\t// Check that the silences are actually expired (endsAt is in the past or equal to now)\n\tnow := float64(time.Now().Unix())\n\tfor _, sil := range sils {\n\t\trequire.Contains(t, silIDs, sil.ID(), \"silence ID should be in the expired list\")\n\t\trequire.LessOrEqual(t, sil.EndsAt(), now, \"silence %s should be expired\", sil.ID())\n\t}\n\n\t// Import silences back\n\timportOut, err := am.ImportSilences(exportFile)\n\trequire.NoError(t, err, \"import failed: %s\", string(importOut))\n\n\t// Verify silences were imported\n\tsils, err = am.QuerySilence()\n\trequire.NoError(t, err)\n\trequire.GreaterOrEqual(t, len(sils), 2, \"expected at least 2 silences after import\")\n}\n\nfunc TestSilenceImportInvalidJSON(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: [alertname]\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1ms\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n    send_resolved: true\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 1 * time.Second,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\trequire.NoError(t, amc.Start())\n\tdefer amc.Terminate()\n\n\tam := amc.Members()[0]\n\n\t// Create file with invalid JSON\n\ttmpDir := t.TempDir()\n\tinvalidFile := tmpDir + \"/invalid.json\"\n\terr := os.WriteFile(invalidFile, []byte(`[{\"broken\": \"json\"`), 0o644)\n\trequire.NoError(t, err)\n\n\t// Try to import - should fail\n\tout, err := am.ImportSilences(invalidFile)\n\trequire.Error(t, err, \"import should fail with invalid JSON\")\n\trequire.Contains(t, string(out), \"couldn't unmarshal\", \"error message should mention JSON parsing\")\n}\n\nfunc TestSilenceImportInvalidSilence(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: [alertname]\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1ms\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n    send_resolved: true\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 1 * time.Second,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\trequire.NoError(t, amc.Start())\n\tdefer amc.Terminate()\n\n\tam := amc.Members()[0]\n\n\t// Create file with valid JSON but invalid silence (zero timestamps)\n\ttmpDir := t.TempDir()\n\tinvalidFile := tmpDir + \"/invalid_silence.json\"\n\tinvalidSilence := `[\n\t{\n\t\t\"matchers\": [\n\t\t\t{\"name\": \"alertname\", \"value\": \"test\", \"isRegex\": false}\n\t\t],\n\t\t\"startsAt\": \"0001-01-01T00:00:00.000Z\",\n\t\t\"endsAt\": \"0001-01-01T00:00:00.000Z\",\n\t\t\"createdBy\": \"test\",\n\t\t\"comment\": \"invalid silence with zero timestamps\"\n\t}\n]`\n\terr := os.WriteFile(invalidFile, []byte(invalidSilence), 0o644)\n\trequire.NoError(t, err)\n\n\t// Try to import - should fail with error from addSilenceWorker\n\tout, err := am.ImportSilences(invalidFile)\n\trequire.Error(t, err, \"import should fail with invalid silence\")\n\trequire.Contains(t, string(out), \"couldn't import 1 out of 1 silences\", \"error message should report exact count\")\n}\n\nfunc TestSilenceImportPartialFailure(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: [alertname]\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1ms\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n    send_resolved: true\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 1 * time.Second,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\trequire.NoError(t, amc.Start())\n\tdefer amc.Terminate()\n\n\tam := amc.Members()[0]\n\n\t// Create array of PostableSilence directly\n\tnow := time.Now()\n\tfuture := now.Add(4 * time.Hour)\n\tsilences := []models.PostableSilence{\n\t\t// Valid silence 1\n\t\t{\n\t\t\tSilence: models.Silence{\n\t\t\t\tMatchers: models.Matchers{\n\t\t\t\t\t&models.Matcher{Name: ptrString(\"alertname\"), Value: ptrString(\"test1\"), IsRegex: ptrBool(false)},\n\t\t\t\t},\n\t\t\t\tStartsAt:  ptrTime(now),\n\t\t\t\tEndsAt:    ptrTime(future),\n\t\t\t\tCreatedBy: ptrString(\"test\"),\n\t\t\t\tComment:   ptrString(\"valid silence 1\"),\n\t\t\t},\n\t\t},\n\t\t// Invalid silence 2 (endsAt before startsAt)\n\t\t{\n\t\t\tSilence: models.Silence{\n\t\t\t\tMatchers: models.Matchers{\n\t\t\t\t\t&models.Matcher{Name: ptrString(\"alertname\"), Value: ptrString(\"test2\"), IsRegex: ptrBool(false)},\n\t\t\t\t},\n\t\t\t\tStartsAt:  ptrTime(future), // Swapped!\n\t\t\t\tEndsAt:    ptrTime(now),    // Swapped!\n\t\t\t\tCreatedBy: ptrString(\"test\"),\n\t\t\t\tComment:   ptrString(\"invalid silence 2\"),\n\t\t\t},\n\t\t},\n\t\t// Valid silence 3\n\t\t{\n\t\t\tSilence: models.Silence{\n\t\t\t\tMatchers: models.Matchers{\n\t\t\t\t\t&models.Matcher{Name: ptrString(\"alertname\"), Value: ptrString(\"test3\"), IsRegex: ptrBool(false)},\n\t\t\t\t},\n\t\t\t\tStartsAt:  ptrTime(now),\n\t\t\t\tEndsAt:    ptrTime(future),\n\t\t\t\tCreatedBy: ptrString(\"test\"),\n\t\t\t\tComment:   ptrString(\"valid silence 3\"),\n\t\t\t},\n\t\t},\n\t\t// Invalid silence 4 (endsAt before startsAt)\n\t\t{\n\t\t\tSilence: models.Silence{\n\t\t\t\tMatchers: models.Matchers{\n\t\t\t\t\t&models.Matcher{Name: ptrString(\"alertname\"), Value: ptrString(\"test4\"), IsRegex: ptrBool(false)},\n\t\t\t\t},\n\t\t\t\tStartsAt:  ptrTime(future), // Swapped!\n\t\t\t\tEndsAt:    ptrTime(now),    // Swapped!\n\t\t\t\tCreatedBy: ptrString(\"test\"),\n\t\t\t\tComment:   ptrString(\"invalid silence 4\"),\n\t\t\t},\n\t\t},\n\t\t// Valid silence 5\n\t\t{\n\t\t\tSilence: models.Silence{\n\t\t\t\tMatchers: models.Matchers{\n\t\t\t\t\t&models.Matcher{Name: ptrString(\"alertname\"), Value: ptrString(\"test5\"), IsRegex: ptrBool(false)},\n\t\t\t\t},\n\t\t\t\tStartsAt:  ptrTime(now),\n\t\t\t\tEndsAt:    ptrTime(future),\n\t\t\t\tCreatedBy: ptrString(\"test\"),\n\t\t\t\tComment:   ptrString(\"valid silence 5\"),\n\t\t\t},\n\t\t},\n\t}\n\n\t// Serialize to JSON\n\tjsonData, err := json.Marshal(silences)\n\trequire.NoError(t, err)\n\n\t// Write to file\n\ttmpDir := t.TempDir()\n\tmixedFile := tmpDir + \"/mixed_silences.json\"\n\terr = os.WriteFile(mixedFile, jsonData, 0o644)\n\trequire.NoError(t, err)\n\n\t// Try to import - should partially succeed\n\tout, err := am.ImportSilences(mixedFile)\n\trequire.Error(t, err, \"import should fail with partial import\")\n\trequire.Contains(t, string(out), \"couldn't import 2 out of 5 silences\", \"error message should report 2 failures out of 5\")\n}\n\nfunc ptrString(s string) *string { return &s }\nfunc ptrBool(b bool) *bool       { return &b }\nfunc ptrTime(t time.Time) *strfmt.DateTime {\n\tst := strfmt.DateTime(t)\n\treturn &st\n}\n"
  },
  {
    "path": "test/cli/acceptance.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"maps\"\n\t\"os\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-openapi/strfmt\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/cli/format\"\n\t\"github.com/prometheus/alertmanager/test/testutils\"\n)\n\nconst (\n\t// nolint:godot\n\t// amtool is the relative path to local amtool binary.\n\tamtool = \"../../../amtool\"\n)\n\n// Re-export common types from testutils.\ntype (\n\tCollector      = testutils.Collector\n\tAcceptanceOpts = testutils.AcceptanceOpts\n)\n\nvar CompareCollectors = testutils.CompareCollectors\n\n// AcceptanceTest wraps testutils.AcceptanceTest for CLI-based testing.\ntype AcceptanceTest struct {\n\t*testutils.AcceptanceTest\n}\n\n// NewAcceptanceTest returns a new acceptance test.\nfunc NewAcceptanceTest(t *testing.T, opts *AcceptanceOpts) *AcceptanceTest {\n\treturn &AcceptanceTest{\n\t\tAcceptanceTest: testutils.NewAcceptanceTest(t, opts),\n\t}\n}\n\n// AmtoolOk verifies that the \"amtool\" file exists in the correct location for testing,\n// and is a regular file.\nfunc AmtoolOk() (bool, error) {\n\tstat, err := os.Stat(amtool)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"error accessing amtool command, try 'make build' to generate the file. %w\", err)\n\t} else if stat.IsDir() {\n\t\treturn false, fmt.Errorf(\"file %s is a directory, expecting a binary executable file\", amtool)\n\t}\n\treturn true, nil\n}\n\n// Alertmanager wraps testutils.Alertmanager and adds CLI-specific methods.\ntype Alertmanager struct {\n\t*testutils.Alertmanager\n}\n\n// AlertmanagerCluster wraps testutils.AlertmanagerCluster and adds CLI-specific methods.\ntype AlertmanagerCluster struct {\n\t*testutils.AlertmanagerCluster\n}\n\n// AlertmanagerCluster returns a new AlertmanagerCluster.\nfunc (t *AcceptanceTest) AlertmanagerCluster(conf string, size int) *AlertmanagerCluster {\n\treturn &AlertmanagerCluster{\n\t\tAlertmanagerCluster: t.AcceptanceTest.AlertmanagerCluster(conf, size),\n\t}\n}\n\n// Members returns the underlying Alertmanager instances wrapped for CLI testing.\nfunc (amc *AlertmanagerCluster) Members() []*Alertmanager {\n\tbaseMembers := amc.AlertmanagerCluster.Members()\n\twrapped := make([]*Alertmanager, len(baseMembers))\n\tfor i, am := range baseMembers {\n\t\twrapped[i] = &Alertmanager{Alertmanager: am}\n\t}\n\treturn wrapped\n}\n\n// AddAlertsAt declares alerts that are to be added to the Alertmanager server\n// at a relative point in time.\nfunc (am *Alertmanager) AddAlertsAt(omitEquals bool, at float64, alerts ...*TestAlert) {\n\tam.T.Do(at, func() {\n\t\tam.AddAlerts(omitEquals, alerts...)\n\t})\n}\n\n// AddAlerts declares alerts that are to be added to the Alertmanager server.\n// The omitEquals option omits alertname= from the command line args passed to\n// amtool and instead uses the alertname value as the first argument to the command.\n// For example `amtool alert add foo` instead of `amtool alert add alertname=foo`.\n// This has been added to allow certain tests to test adding alerts both with and\n// without alertname=. All other tests that use AddAlerts as a fixture can set this\n// to false.\nfunc (am *Alertmanager) AddAlerts(omitEquals bool, alerts ...*TestAlert) {\n\tfor _, alert := range alerts {\n\t\tout, err := am.addAlertCommand(omitEquals, alert)\n\t\tif err != nil {\n\t\t\tam.T.Errorf(\"Error adding alert: %v\\nOutput: %s\", err, string(out))\n\t\t}\n\t}\n}\n\nfunc (am *Alertmanager) addAlertCommand(omitEquals bool, alert *TestAlert) ([]byte, error) {\n\tamURLFlag := \"--alertmanager.url=\" + am.getURL(\"/\")\n\targs := []string{amURLFlag, \"alert\", \"add\"}\n\t// Make a copy of the labels\n\tlabels := make(models.LabelSet, len(alert.Labels))\n\tmaps.Copy(labels, alert.Labels)\n\tif omitEquals {\n\t\t// If alertname is present and omitEquals is true then the command should\n\t\t// be `amtool alert add foo ...` and not `amtool alert add alertname=foo ...`.\n\t\tif alertname, ok := labels[\"alertname\"]; ok {\n\t\t\targs = append(args, alertname)\n\t\t\tdelete(labels, \"alertname\")\n\t\t}\n\t}\n\tfor k, v := range labels {\n\t\targs = append(args, k+\"=\"+v)\n\t}\n\tstartsAt := strfmt.DateTime(am.Opts.ExpandTime(alert.StartsAt))\n\targs = append(args, \"--start=\"+startsAt.String())\n\tif alert.EndsAt > alert.StartsAt {\n\t\tendsAt := strfmt.DateTime(am.Opts.ExpandTime(alert.EndsAt))\n\t\targs = append(args, \"--end=\"+endsAt.String())\n\t}\n\tcmd := exec.Command(amtool, args...)\n\treturn cmd.CombinedOutput()\n}\n\n// QueryAlerts uses the amtool cli to query alerts.\nfunc (am *Alertmanager) QueryAlerts(match ...string) ([]TestAlert, error) {\n\tamURLFlag := \"--alertmanager.url=\" + am.getURL(\"/\")\n\targs := append([]string{amURLFlag, \"alert\", \"query\"}, match...)\n\tcmd := exec.Command(amtool, args...)\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn parseAlertQueryResponse(output)\n}\n\nfunc parseAlertQueryResponse(data []byte) ([]TestAlert, error) {\n\talerts := []TestAlert{}\n\tlines := strings.Split(string(data), \"\\n\")\n\theader, lines := lines[0], lines[1:len(lines)-1]\n\tstartTimePos := strings.Index(header, \"Starts At\")\n\tif startTimePos == -1 {\n\t\treturn alerts, errors.New(\"Invalid header: \" + header)\n\t}\n\tsummPos := strings.Index(header, \"Summary\")\n\tif summPos == -1 {\n\t\treturn alerts, errors.New(\"Invalid header: \" + header)\n\t}\n\tfor _, line := range lines {\n\t\talertName := strings.TrimSpace(line[0:startTimePos])\n\t\tstartTime := strings.TrimSpace(line[startTimePos:summPos])\n\t\tstartsAt, err := time.Parse(format.DefaultDateFormat, startTime)\n\t\tif err != nil {\n\t\t\treturn alerts, err\n\t\t}\n\t\tsummary := strings.TrimSpace(line[summPos:])\n\t\talert := TestAlert{\n\t\t\tLabels:   models.LabelSet{\"alertname\": alertName},\n\t\t\tStartsAt: float64(startsAt.Unix()),\n\t\t\tSummary:  summary,\n\t\t}\n\t\talerts = append(alerts, alert)\n\t}\n\treturn alerts, nil\n}\n\n// SetSilence updates or creates the given Silence.\nfunc (amc *AlertmanagerCluster) SetSilence(at float64, sil *TestSilence) {\n\tfor _, am := range amc.Members() {\n\t\tam.SetSilence(at, sil)\n\t}\n}\n\n// SetSilence updates or creates the given Silence.\nfunc (am *Alertmanager) SetSilence(at float64, sil *TestSilence) {\n\tout, err := am.addSilenceCommand(sil)\n\tif err != nil {\n\t\tam.T.Errorf(\"Unable to set silence %v %v\", err, string(out))\n\t}\n}\n\n// addSilenceCommand adds a silence using the 'amtool silence add' command.\nfunc (am *Alertmanager) addSilenceCommand(sil *TestSilence) ([]byte, error) {\n\tamURLFlag := \"--alertmanager.url=\" + am.getURL(\"/\")\n\targs := []string{amURLFlag, \"silence\", \"add\"}\n\tif sil.comment != \"\" {\n\t\targs = append(args, \"--comment=\"+sil.comment)\n\t}\n\targs = append(args, sil.match...)\n\tcmd := exec.Command(amtool, args...)\n\treturn cmd.CombinedOutput()\n}\n\n// QuerySilence queries the current silences using the 'amtool silence query' command.\nfunc (am *Alertmanager) QuerySilence(match ...string) ([]TestSilence, error) {\n\tamURLFlag := \"--alertmanager.url=\" + am.getURL(\"/\")\n\targs := append([]string{amURLFlag, \"silence\", \"query\"}, match...)\n\tcmd := exec.Command(amtool, args...)\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tam.T.Error(\"Silence query command failed: \", err)\n\t}\n\treturn parseSilenceQueryResponse(out)\n}\n\n// QueryExpiredSilence queries expired silences using the 'amtool silence query --expired --within' command.\nfunc (am *Alertmanager) QueryExpiredSilence(match ...string) ([]TestSilence, error) {\n\tamURLFlag := \"--alertmanager.url=\" + am.getURL(\"/\")\n\targs := append([]string{amURLFlag, \"silence\", \"query\", \"--expired\", \"--within=1h\"}, match...)\n\tcmd := exec.Command(amtool, args...)\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tam.T.Error(\"Silence query command failed: \", err)\n\t}\n\treturn parseSilenceQueryResponse(out)\n}\n\nvar silenceHeaderFields = []string{\"ID\", \"Matchers\", \"Ends At\", \"Created By\", \"Comment\"}\n\nfunc parseSilenceQueryResponse(data []byte) ([]TestSilence, error) {\n\tsils := []TestSilence{}\n\tlines := strings.Split(string(data), \"\\n\")\n\theader, lines := lines[0], lines[1:len(lines)-1]\n\tmatchersPos := strings.Index(header, silenceHeaderFields[1])\n\tif matchersPos == -1 {\n\t\treturn sils, errors.New(\"Invalid header: \" + header)\n\t}\n\tendsAtPos := strings.Index(header, silenceHeaderFields[2])\n\tif endsAtPos == -1 {\n\t\treturn sils, errors.New(\"Invalid header: \" + header)\n\t}\n\tcreatedByPos := strings.Index(header, silenceHeaderFields[3])\n\tif createdByPos == -1 {\n\t\treturn sils, errors.New(\"Invalid header: \" + header)\n\t}\n\tcommentPos := strings.Index(header, silenceHeaderFields[4])\n\tif commentPos == -1 {\n\t\treturn sils, errors.New(\"Invalid header: \" + header)\n\t}\n\tfor _, line := range lines {\n\t\tid := strings.TrimSpace(line[0:matchersPos])\n\t\tmatchers := strings.TrimSpace(line[matchersPos:endsAtPos])\n\t\tendsAtString := strings.TrimSpace(line[endsAtPos:createdByPos])\n\t\tendsAt, err := time.Parse(format.DefaultDateFormat, endsAtString)\n\t\tif err != nil {\n\t\t\treturn sils, err\n\t\t}\n\t\tcreatedBy := strings.TrimSpace(line[createdByPos:commentPos])\n\t\tcomment := strings.TrimSpace(line[commentPos:])\n\t\tsilence := TestSilence{\n\t\t\tid:        id,\n\t\t\tendsAt:    float64(endsAt.Unix()),\n\t\t\tmatch:     strings.Split(matchers, \" \"),\n\t\t\tcreatedBy: createdBy,\n\t\t\tcomment:   comment,\n\t\t}\n\t\tsils = append(sils, silence)\n\t}\n\treturn sils, nil\n}\n\n// DelSilence deletes the silence with the sid at the given time.\nfunc (amc *AlertmanagerCluster) DelSilence(at float64, sil *TestSilence) {\n\tfor _, am := range amc.Members() {\n\t\tam.DelSilence(at, sil)\n\t}\n}\n\n// DelSilence deletes the silence with the sid at the given time.\nfunc (am *Alertmanager) DelSilence(at float64, sil *TestSilence) {\n\toutput, err := am.expireSilenceCommand(sil)\n\tif err != nil {\n\t\tam.T.Errorf(\"Error expiring silence %v: %s\", string(output), err)\n\t\treturn\n\t}\n}\n\n// expireSilenceCommand expires a silence using the 'amtool silence expire' command.\nfunc (am *Alertmanager) expireSilenceCommand(sil *TestSilence) ([]byte, error) {\n\tamURLFlag := \"--alertmanager.url=\" + am.getURL(\"/\")\n\targs := []string{amURLFlag, \"silence\", \"expire\", sil.ID()}\n\tcmd := exec.Command(amtool, args...)\n\treturn cmd.CombinedOutput()\n}\n\n// ExportSilences exports all silences to JSON format using 'amtool silence query -o json'.\nfunc (am *Alertmanager) ExportSilences() ([]byte, error) {\n\tamURLFlag := \"--alertmanager.url=\" + am.getURL(\"/\")\n\targs := []string{amURLFlag, \"silence\", \"query\", \"-o\", \"json\"}\n\tcmd := exec.Command(amtool, args...)\n\treturn cmd.Output()\n}\n\n// ImportSilences imports silences from a JSON file using 'amtool silence import'.\nfunc (am *Alertmanager) ImportSilences(filename string) ([]byte, error) {\n\tamURLFlag := \"--alertmanager.url=\" + am.getURL(\"/\")\n\targs := []string{amURLFlag, \"silence\", \"import\", filename}\n\tcmd := exec.Command(amtool, args...)\n\treturn cmd.CombinedOutput()\n}\n\n// ExpireSilenceByID expires a silence by its ID using 'amtool silence expire'.\nfunc (am *Alertmanager) ExpireSilenceByID(id string) ([]byte, error) {\n\tamURLFlag := \"--alertmanager.url=\" + am.getURL(\"/\")\n\targs := []string{amURLFlag, \"silence\", \"expire\", id}\n\tcmd := exec.Command(amtool, args...)\n\treturn cmd.CombinedOutput()\n}\n\n// ShowRoute shows the routing tree using 'amtool config routes show'.\nfunc (am *Alertmanager) ShowRoute() ([]byte, error) {\n\treturn am.showRouteCommand()\n}\n\nfunc (am *Alertmanager) showRouteCommand() ([]byte, error) {\n\tamURLFlag := \"--alertmanager.url=\" + am.getURL(\"/\")\n\targs := []string{amURLFlag, \"config\", \"routes\", \"show\"}\n\tcmd := exec.Command(amtool, args...)\n\treturn cmd.CombinedOutput()\n}\n\n// TestRoute tests label matching against the routing tree using 'amtool config routes test'.\nfunc (am *Alertmanager) TestRoute(labels ...string) ([]byte, error) {\n\treturn am.testRouteCommand(labels...)\n}\n\nfunc (am *Alertmanager) testRouteCommand(labels ...string) ([]byte, error) {\n\tamURLFlag := \"--alertmanager.url=\" + am.getURL(\"/\")\n\targs := append([]string{amURLFlag, \"config\", \"routes\", \"test\"}, labels...)\n\tcmd := exec.Command(amtool, args...)\n\treturn cmd.CombinedOutput()\n}\n\nfunc (am *Alertmanager) getURL(path string) string {\n\treturn fmt.Sprintf(\"http://%s%s%s\", am.APIAddr(), am.Opts.RoutePrefix, path)\n}\n\n// Version runs the 'amtool' command with the --version option and checks\n// for appropriate output.\nfunc Version() (string, error) {\n\tcmd := exec.Command(amtool, \"--version\")\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tversionRE := regexp.MustCompile(`^amtool, version (\\d+\\.\\d+\\.\\d+) *`)\n\tmatched := versionRE.FindStringSubmatch(string(out))\n\tif len(matched) != 2 {\n\t\treturn \"\", errors.New(\"Unable to match version info regex: \" + string(out))\n\t}\n\treturn matched[1], nil\n}\n"
  },
  {
    "path": "test/cli/mock.go",
    "content": "// Copyright 2019 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage test\n\nimport (\n\t\"github.com/prometheus/alertmanager/test/testutils\"\n)\n\n// Re-export common types and functions from testutils.\ntype (\n\tInterval    = testutils.Interval\n\tTestAlert   = testutils.TestAlert\n\tMockWebhook = testutils.MockWebhook\n)\n\nvar (\n\tAt         = testutils.At\n\tBetween    = testutils.Between\n\tAlert      = testutils.Alert\n\tNewWebhook = testutils.NewWebhook\n)\n\n// TestSilence models a model.Silence with relative times.\n// This is the CLI-specific version with additional fields.\ntype TestSilence struct {\n\tid               string\n\tcreatedBy        string\n\tmatch            []string\n\tmatchRE          []string\n\tstartsAt, endsAt float64\n\tcomment          string\n}\n\n// Silence creates a new TestSilence active for the relative interval given\n// by start and end.\nfunc Silence(start, end float64) *TestSilence {\n\treturn &TestSilence{\n\t\tstartsAt: start,\n\t\tendsAt:   end,\n\t}\n}\n\n// Match adds a new plain matcher to the silence.\nfunc (s *TestSilence) Match(v ...string) *TestSilence {\n\ts.match = append(s.match, v...)\n\treturn s\n}\n\n// GetMatches returns the plain matchers for the silence.\nfunc (s TestSilence) GetMatches() []string {\n\treturn s.match\n}\n\n// MatchRE adds a new regex matcher to the silence.\nfunc (s *TestSilence) MatchRE(v ...string) *TestSilence {\n\tif len(v)%2 == 1 {\n\t\tpanic(\"bad key/values\")\n\t}\n\ts.matchRE = append(s.matchRE, v...)\n\treturn s\n}\n\n// GetMatchREs returns the regex matchers for the silence.\nfunc (s *TestSilence) GetMatchREs() []string {\n\treturn s.matchRE\n}\n\n// Comment sets the comment to the silence.\nfunc (s *TestSilence) Comment(c string) *TestSilence {\n\ts.comment = c\n\treturn s\n}\n\n// SetID sets the silence ID.\nfunc (s *TestSilence) SetID(ID string) {\n\ts.id = ID\n}\n\n// ID gets the silence ID.\nfunc (s *TestSilence) ID() string {\n\treturn s.id\n}\n\n// EndsAt gets the silence end time.\nfunc (s *TestSilence) EndsAt() float64 {\n\treturn s.endsAt\n}\n"
  },
  {
    "path": "test/testutils/acceptance.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage testutils\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"testing\"\n\t\"time\"\n\n\tapiclient \"github.com/prometheus/alertmanager/api/v2/client\"\n\t\"github.com/prometheus/alertmanager/api/v2/client/general\"\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\n\thttptransport \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n)\n\n// AcceptanceOpts defines configuration parameters for an acceptance test.\ntype AcceptanceOpts struct {\n\tFeatureFlags []string\n\tRoutePrefix  string\n\tTolerance    time.Duration\n\tbaseTime     time.Time\n}\n\n// AlertString formats an alert for display with relative times.\nfunc (opts *AcceptanceOpts) AlertString(a *models.GettableAlert) string {\n\tif a.EndsAt == nil || time.Time(*a.EndsAt).IsZero() {\n\t\treturn fmt.Sprintf(\"%v[%v:]\", a, opts.RelativeTime(time.Time(*a.StartsAt)))\n\t}\n\treturn fmt.Sprintf(\"%v[%v:%v]\", a, opts.RelativeTime(time.Time(*a.StartsAt)), opts.RelativeTime(time.Time(*a.EndsAt)))\n}\n\n// ExpandTime returns the absolute time for the relative time\n// calculated from the test's base time.\nfunc (opts *AcceptanceOpts) ExpandTime(rel float64) time.Time {\n\treturn opts.baseTime.Add(time.Duration(rel * float64(time.Second)))\n}\n\n// RelativeTime returns the relative time for the given time\n// calculated from the test's base time.\nfunc (opts *AcceptanceOpts) RelativeTime(act time.Time) float64 {\n\treturn float64(act.Sub(opts.baseTime)) / float64(time.Second)\n}\n\n// SetBaseTime sets the base time for relative time calculations.\nfunc (opts *AcceptanceOpts) SetBaseTime(t time.Time) {\n\topts.baseTime = t\n}\n\n// AcceptanceTest provides declarative definition of given inputs and expected\n// output of an Alertmanager setup.\ntype AcceptanceTest struct {\n\t*testing.T\n\n\topts *AcceptanceOpts\n\n\tamc        *AlertmanagerCluster\n\tcollectors []*Collector\n\n\tactions map[float64][]func()\n}\n\n// NewAcceptanceTest returns a new acceptance test with the base time\n// set to the current time.\nfunc NewAcceptanceTest(t *testing.T, opts *AcceptanceOpts) *AcceptanceTest {\n\ttest := &AcceptanceTest{\n\t\tT:       t,\n\t\topts:    opts,\n\t\tactions: map[float64][]func(){},\n\t}\n\treturn test\n}\n\n// Do sets the given function to be executed at the given time.\nfunc (t *AcceptanceTest) Do(at float64, f func()) {\n\tt.actions[at] = append(t.actions[at], f)\n}\n\n// AlertmanagerCluster returns a new AlertmanagerCluster that allows starting a\n// cluster of Alertmanager instances on random ports.\nfunc (t *AcceptanceTest) AlertmanagerCluster(conf string, size int) *AlertmanagerCluster {\n\tamc := AlertmanagerCluster{}\n\n\tfor range size {\n\t\tam := &Alertmanager{\n\t\t\tT:    t,\n\t\t\tOpts: t.opts,\n\t\t}\n\n\t\tdir, err := os.MkdirTemp(\"\", \"am_test\")\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tam.dir = dir\n\n\t\tcf, err := os.Create(filepath.Join(dir, \"config.yml\"))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tam.confFile = cf\n\t\tam.UpdateConfig(conf)\n\n\t\t// apiAddr and clusterAddr will be discovered during Start()\n\t\t// clientV2 will be created during Start() with the discovered address\n\n\t\tamc.ams = append(amc.ams, am)\n\t}\n\n\tt.amc = &amc\n\n\treturn &amc\n}\n\n// Collector returns a new collector bound to the test instance.\nfunc (t *AcceptanceTest) Collector(name string) *Collector {\n\tco := NewCollector(t.T, name, t.opts)\n\tt.collectors = append(t.collectors, co)\n\n\treturn co\n}\n\n// Run starts all Alertmanagers and runs queries against them. It then checks\n// whether all expected notifications have arrived at the expected receiver.\nfunc (t *AcceptanceTest) Run(additionalArgs ...string) {\n\terrc := make(chan error)\n\n\tfor _, am := range t.amc.ams {\n\t\tam.errc = errc\n\t\tt.Cleanup(am.Terminate)\n\t\tt.Cleanup(am.cleanup)\n\t}\n\n\terr := t.amc.Start(additionalArgs...)\n\tif err != nil {\n\t\tt.Log(err)\n\t\tt.Fail()\n\t\treturn\n\t}\n\n\t// Set the reference time right before running the test actions to avoid\n\t// test failures due to slow setup of the test environment.\n\tt.opts.SetBaseTime(time.Now())\n\n\tgo t.runActions()\n\n\tvar latest float64\n\tfor _, coll := range t.collectors {\n\t\tif l := coll.Latest(); l > latest {\n\t\t\tlatest = l\n\t\t}\n\t}\n\n\tdeadline := t.opts.ExpandTime(latest)\n\n\tselect {\n\tcase <-time.After(time.Until(deadline)):\n\t\t// continue\n\tcase err := <-errc:\n\t\tt.Error(err)\n\t}\n}\n\n// runActions performs the stored actions at the defined times.\nfunc (t *AcceptanceTest) runActions() {\n\tvar wg sync.WaitGroup\n\n\tfor at, fs := range t.actions {\n\t\tts := t.opts.ExpandTime(at)\n\t\twg.Add(len(fs))\n\n\t\tfor _, f := range fs {\n\t\t\tgo func(f func()) {\n\t\t\t\ttime.Sleep(time.Until(ts))\n\t\t\t\tf()\n\t\t\t\twg.Done()\n\t\t\t}(f)\n\t\t}\n\t}\n\n\twg.Wait()\n}\n\ntype buffer struct {\n\tb   bytes.Buffer\n\tmtx sync.Mutex\n}\n\nfunc (b *buffer) Write(p []byte) (int, error) {\n\tb.mtx.Lock()\n\tdefer b.mtx.Unlock()\n\treturn b.b.Write(p)\n}\n\nfunc (b *buffer) String() string {\n\tb.mtx.Lock()\n\tdefer b.mtx.Unlock()\n\treturn b.b.String()\n}\n\n// Alertmanager encapsulates an Alertmanager process and allows\n// declaring alerts being pushed to it at fixed points in time.\ntype Alertmanager struct {\n\tT    *AcceptanceTest\n\tOpts *AcceptanceOpts\n\n\tapiAddr     string // the API address of this instance, discovered after start\n\tclusterAddr string // the cluster address can be the address of any peer\n\n\tclientV2 *apiclient.AlertmanagerAPI\n\tconfFile *os.File\n\tdir      string\n\n\tcmd  *exec.Cmd\n\terrc chan<- error\n}\n\n// ClusterAddr returns an address for the cluster.\nfunc (am *Alertmanager) ClusterAddr() string {\n\treturn am.clusterAddr\n}\n\n// APIAddr returns the API address for the instance.\nfunc (am *Alertmanager) APIAddr() string {\n\treturn am.apiAddr\n}\n\n// AlertmanagerCluster represents a group of Alertmanager instances\n// acting as a cluster.\ntype AlertmanagerCluster struct {\n\tams []*Alertmanager\n}\n\n// Start the Alertmanager cluster and wait until it is ready to receive.\nfunc (amc *AlertmanagerCluster) Start(additionalArgs ...string) error {\n\targs := make([]string, 0, len(additionalArgs)+1)\n\targs = append(args, additionalArgs...)\n\tclusterAdded := false\n\n\tfor i, am := range amc.ams {\n\t\tam.T.Logf(\"Starting cluster member %d/%d\", i+1, len(amc.ams))\n\n\t\t// Start this instance (it will discover its own ports)\n\t\tif err := am.Start(args); err != nil {\n\t\t\treturn fmt.Errorf(\"starting cluster member %d: %w\", i, err)\n\t\t}\n\n\t\t// From the second instance onwards, append the cluster.peer argument\n\t\t// so the subsequent ones join up.\n\t\tif !clusterAdded {\n\t\t\targs = append(args, \"--cluster.peer=\"+am.ClusterAddr())\n\t\t\tclusterAdded = true\n\t\t}\n\t}\n\n\t// Wait for cluster to converge\n\tfor _, am := range amc.ams {\n\t\tif err := am.WaitForCluster(len(amc.ams)); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to wait for Alertmanager instance %q to join cluster: %w\", am.APIAddr(), err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Members returns the underlying slice of cluster members.\nfunc (amc *AlertmanagerCluster) Members() []*Alertmanager {\n\treturn amc.ams\n}\n\n// discoverWebAddress parses stderr for \"Listening on\" log message and updates am.apiAddr.\nfunc (am *Alertmanager) discoverWebAddress(timeout time.Duration) error {\n\tam.T.Helper()\n\tdeadline := time.Now().Add(timeout)\n\tstderrBuf, ok := am.cmd.Stderr.(*buffer)\n\tif !ok {\n\t\treturn fmt.Errorf(\"stderr is not a buffer\")\n\t}\n\n\t// Compile regex once outside the loop\n\tre := regexp.MustCompile(`address=([^\\s]+)`)\n\tlastPos := 0\n\n\tfor time.Now().Before(deadline) {\n\t\ttime.Sleep(10 * time.Millisecond)\n\t\tstderr := stderrBuf.String()\n\t\t// Only process new content since last check\n\t\tif len(stderr) <= lastPos {\n\t\t\tcontinue\n\t\t}\n\t\tnewContent := stderr[lastPos:]\n\t\tlastPos = len(stderr)\n\n\t\t// Look for: msg=\"Listening on\" address=127.0.0.1:PORT\n\t\tfor line := range strings.SplitSeq(newContent, \"\\n\") {\n\t\t\tif !strings.Contains(line, \"Listening on\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Extract address using regex: address=IP:PORT\n\t\t\tmatches := re.FindStringSubmatch(line)\n\t\t\tif len(matches) == 2 {\n\t\t\t\tam.apiAddr = matches[1]\n\t\t\t\tam.T.Logf(\"Discovered web address: %s\", am.apiAddr)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\treturn fmt.Errorf(\"timeout waiting for web address in logs\")\n}\n\n// discoverClusterAddress queries /api/v2/status for cluster address and updates am.clusterAddr.\nfunc (am *Alertmanager) discoverClusterAddress(timeout time.Duration) error {\n\tam.T.Helper()\n\tdeadline := time.Now().Add(timeout)\n\tparams := general.NewGetStatusParams()\n\tparams.WithContext(context.Background())\n\n\tfor time.Now().Before(deadline) {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\tstatus, err := am.clientV2.General.GetStatus(params)\n\t\tif err != nil || status.Payload == nil || status.Payload.Cluster == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif len(status.Payload.Cluster.Peers) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tpeer := status.Payload.Cluster.Peers[0]\n\t\tif peer != nil && peer.Address != nil {\n\t\t\tam.clusterAddr = *peer.Address\n\t\t\tam.T.Logf(\"Discovered cluster address: %s\", am.clusterAddr)\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn fmt.Errorf(\"timeout waiting for cluster address from API\")\n}\n\n// Start the alertmanager and wait until it is ready to receive.\nfunc (am *Alertmanager) Start(additionalArg []string) error {\n\tam.T.Helper()\n\targs := []string{\n\t\t\"--config.file\", am.confFile.Name(),\n\t\t\"--log.level\", \"debug\",\n\t\t\"--web.listen-address\", \"127.0.0.1:0\",\n\t\t\"--storage.path\", am.dir,\n\t\t\"--cluster.listen-address\", \"127.0.0.1:0\",\n\t\t\"--cluster.settle-timeout\", \"0s\",\n\t}\n\tif len(am.Opts.FeatureFlags) > 0 {\n\t\targs = append(args, \"--enable-feature\", strings.Join(am.Opts.FeatureFlags, \",\"))\n\t}\n\tif am.Opts.RoutePrefix != \"\" {\n\t\targs = append(args, \"--web.route-prefix\", am.Opts.RoutePrefix)\n\t}\n\targs = append(args, additionalArg...)\n\n\tcmd := exec.Command(\"../../../alertmanager\", args...)\n\n\tif am.cmd == nil {\n\t\tvar outb, errb buffer\n\t\tcmd.Stdout = &outb\n\t\tcmd.Stderr = &errb\n\t} else {\n\t\tcmd.Stdout = am.cmd.Stdout\n\t\tcmd.Stderr = am.cmd.Stderr\n\t}\n\tam.cmd = cmd\n\n\tif err := am.cmd.Start(); err != nil {\n\t\treturn err\n\t}\n\n\tgo func() {\n\t\tif err := am.cmd.Wait(); err != nil {\n\t\t\tam.errc <- err\n\t\t}\n\t}()\n\n\t// Discover web address from logs\n\tif err := am.discoverWebAddress(5 * time.Second); err != nil {\n\t\treturn fmt.Errorf(\"failed to discover web address: %w\", err)\n\t}\n\n\t// Update API client with discovered address\n\ttransport := httptransport.New(am.apiAddr, am.Opts.RoutePrefix+\"/api/v2/\", nil)\n\tam.clientV2 = apiclient.New(transport, strfmt.Default)\n\n\t// Discover cluster address from API (also serves as readiness check)\n\tif err := am.discoverClusterAddress(5 * time.Second); err != nil {\n\t\treturn fmt.Errorf(\"failed to discover cluster address: %w\", err)\n\t}\n\n\tam.T.Logf(\"Alertmanager started - web: %s, cluster: %s\", am.apiAddr, am.clusterAddr)\n\treturn nil\n}\n\n// WaitForCluster waits for the Alertmanager instance to join a cluster with the\n// given size.\nfunc (am *Alertmanager) WaitForCluster(size int) error {\n\tparams := general.NewGetStatusParams()\n\tparams.WithContext(context.Background())\n\tvar status *general.GetStatusOK\n\n\t// Poll for 2s\n\tfor range 20 {\n\t\tvar err error\n\t\tstatus, err = am.clientV2.General.GetStatus(params)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif len(status.Payload.Cluster.Peers) == size {\n\t\t\treturn nil\n\t\t}\n\t\ttime.Sleep(100 * time.Millisecond)\n\t}\n\n\treturn fmt.Errorf(\n\t\t\"expected %v peers, but got %v\",\n\t\tsize,\n\t\tlen(status.Payload.Cluster.Peers),\n\t)\n}\n\n// Terminate kills the underlying Alertmanager cluster processes and removes intermediate\n// data.\nfunc (amc *AlertmanagerCluster) Terminate() {\n\tfor _, am := range amc.ams {\n\t\tam.Terminate()\n\t}\n}\n\n// Terminate kills the underlying Alertmanager process and remove intermediate\n// data.\nfunc (am *Alertmanager) Terminate() {\n\tam.T.Helper()\n\tif am.cmd != nil && am.cmd.Process != nil {\n\t\tif err := syscall.Kill(am.cmd.Process.Pid, syscall.SIGTERM); err != nil {\n\t\t\tam.T.Logf(\"Error sending SIGTERM to Alertmanager process: %v\", err)\n\t\t}\n\t\tam.T.Logf(\"stdout:\\n%v\", am.cmd.Stdout)\n\t\tam.T.Logf(\"stderr:\\n%v\", am.cmd.Stderr)\n\t}\n}\n\n// Reload sends the reloading signal to the Alertmanager instances.\nfunc (amc *AlertmanagerCluster) Reload() {\n\tfor _, am := range amc.ams {\n\t\tam.Reload()\n\t}\n}\n\n// Reload sends the reloading signal to the Alertmanager process.\nfunc (am *Alertmanager) Reload() {\n\tam.T.Helper()\n\tif am.cmd.Process != nil {\n\t\tif err := syscall.Kill(am.cmd.Process.Pid, syscall.SIGHUP); err != nil {\n\t\t\tam.T.Fatalf(\"Error sending SIGHUP to Alertmanager process: %v\", err)\n\t\t}\n\t}\n}\n\nfunc (am *Alertmanager) cleanup() {\n\tam.T.Helper()\n\tif err := os.RemoveAll(am.confFile.Name()); err != nil {\n\t\tam.T.Errorf(\"Error removing test config file %q: %v\", am.confFile.Name(), err)\n\t}\n}\n\n// UpdateConfig rewrites the configuration file for the Alertmanager cluster. It\n// does not initiate config reloading.\nfunc (amc *AlertmanagerCluster) UpdateConfig(conf string) {\n\tfor _, am := range amc.ams {\n\t\tam.UpdateConfig(conf)\n\t}\n}\n\n// UpdateConfig rewrites the configuration file for the Alertmanager. It does not\n// initiate config reloading.\nfunc (am *Alertmanager) UpdateConfig(conf string) {\n\tif _, err := am.confFile.WriteString(conf); err != nil {\n\t\tam.T.Fatal(err)\n\t}\n\tif err := am.confFile.Sync(); err != nil {\n\t\tam.T.Fatal(err)\n\t}\n}\n\n// Client returns a client to interact with the API v2 endpoint.\nfunc (am *Alertmanager) Client() *apiclient.AlertmanagerAPI {\n\tif am.clientV2 == nil {\n\t\tpanic(\"Client not available. Start() was not called or failed.\")\n\t}\n\treturn am.clientV2\n}\n"
  },
  {
    "path": "test/testutils/collector.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage testutils\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\n// Collector gathers alerts received by a notification receiver\n// and verifies whether all arrived and within the correct time boundaries.\ntype Collector struct {\n\tt    *testing.T\n\tname string\n\topts *AcceptanceOpts\n\n\tcollected map[float64][]models.GettableAlerts\n\texpected  map[Interval][]models.GettableAlerts\n\n\tmtx sync.RWMutex\n}\n\n// NewCollector creates a new Collector with the given parameters.\nfunc NewCollector(t *testing.T, name string, opts *AcceptanceOpts) *Collector {\n\treturn &Collector{\n\t\tt:         t,\n\t\tname:      name,\n\t\topts:      opts,\n\t\tcollected: map[float64][]models.GettableAlerts{},\n\t\texpected:  map[Interval][]models.GettableAlerts{},\n\t}\n}\n\nfunc (c *Collector) String() string {\n\treturn c.name\n}\n\n// Opts returns the acceptance options for this collector.\nfunc (c *Collector) Opts() *AcceptanceOpts {\n\treturn c.opts\n}\n\n// Collected returns a map of alerts collected by the collector indexed with the\n// receive timestamp.\nfunc (c *Collector) Collected() map[float64][]models.GettableAlerts {\n\tc.mtx.RLock()\n\tdefer c.mtx.RUnlock()\n\treturn c.collected\n}\n\nfunc batchesEqual(as, bs models.GettableAlerts, opts *AcceptanceOpts) bool {\n\tif len(as) != len(bs) {\n\t\treturn false\n\t}\n\n\tfor _, a := range as {\n\t\tfound := false\n\t\tfor _, b := range bs {\n\t\t\tif EqualAlerts(a, b, opts) {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// Latest returns the latest relative point in time where a notification is\n// expected.\nfunc (c *Collector) Latest() float64 {\n\tc.mtx.RLock()\n\tdefer c.mtx.RUnlock()\n\tvar latest float64\n\tfor iv := range c.expected {\n\t\tif iv.end > latest {\n\t\t\tlatest = iv.end\n\t\t}\n\t}\n\treturn latest\n}\n\n// Want declares that the Collector expects to receive the given alerts\n// within the given time boundaries.\nfunc (c *Collector) Want(iv Interval, alerts ...*TestAlert) {\n\tc.mtx.Lock()\n\tdefer c.mtx.Unlock()\n\tvar nas models.GettableAlerts\n\tfor _, a := range alerts {\n\t\tnas = append(nas, a.NativeAlert(c.opts))\n\t}\n\n\tc.expected[iv] = append(c.expected[iv], nas)\n}\n\n// Add the given alerts to the collected alerts.\n// This is exported so it can be used by MockWebhook implementations.\nfunc (c *Collector) Add(alerts ...*models.GettableAlert) {\n\tc.mtx.Lock()\n\tdefer c.mtx.Unlock()\n\tarrival := c.opts.RelativeTime(time.Now())\n\n\tc.collected[arrival] = append(c.collected[arrival], models.GettableAlerts(alerts))\n}\n\nfunc (c *Collector) Check() string {\n\tvar report strings.Builder\n\tfmt.Fprintf(&report, \"\\ncollector %q:\\n\\n\", c)\n\n\tc.mtx.RLock()\n\tdefer c.mtx.RUnlock()\n\tfor iv, expected := range c.expected {\n\t\tfmt.Fprintf(&report, \"interval %v\\n\", iv)\n\n\t\tvar alerts []models.GettableAlerts\n\t\tfor at, got := range c.collected {\n\t\t\tif iv.contains(at) {\n\t\t\t\talerts = append(alerts, got...)\n\t\t\t}\n\t\t}\n\n\t\tfor _, exp := range expected {\n\t\t\tfound := len(exp) == 0 && len(alerts) == 0\n\n\t\t\treport.WriteString(\"---\\n\")\n\n\t\t\tfor _, e := range exp {\n\t\t\t\tfmt.Fprintf(&report, \"- %v\\n\", c.opts.AlertString(e))\n\t\t\t}\n\n\t\t\tfor _, a := range alerts {\n\t\t\t\tif batchesEqual(exp, a, c.opts) {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif found {\n\t\t\t\treport.WriteString(\"  [ ✓ ]\\n\")\n\t\t\t} else {\n\t\t\t\tc.t.Fail()\n\t\t\t\treport.WriteString(\"  [ ✗ ]\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Detect unexpected notifications.\n\tvar totalExp, totalAct int\n\tfor _, exp := range c.expected {\n\t\tfor _, e := range exp {\n\t\t\ttotalExp += len(e)\n\t\t}\n\t}\n\tfor _, act := range c.collected {\n\t\tfor _, a := range act {\n\t\t\tif len(a) == 0 {\n\t\t\t\tc.t.Error(\"received empty notifications\")\n\t\t\t}\n\t\t\ttotalAct += len(a)\n\t\t}\n\t}\n\tif totalExp != totalAct {\n\t\tc.t.Fail()\n\t\tfmt.Fprintf(&report, \"\\nExpected total of %d alerts, got %d\", totalExp, totalAct)\n\t}\n\n\tif c.t.Failed() {\n\t\treport.WriteString(\"\\nreceived:\\n\")\n\n\t\tfor at, col := range c.collected {\n\t\t\tfor _, alerts := range col {\n\t\t\t\tfmt.Fprintf(&report, \"@ %v\\n\", at)\n\t\t\t\tfor _, a := range alerts {\n\t\t\t\t\tfmt.Fprintf(&report, \"- %v\\n\", c.opts.AlertString(a))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn report.String()\n}\n\n// alertsToString returns a string representation of the given Alerts. Use for\n// debugging.\nfunc alertsToString(as []*models.GettableAlert) (string, error) {\n\tb, err := json.Marshal(as)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(b), nil\n}\n\n// CompareCollectors compares two collectors based on their collected alerts.\nfunc CompareCollectors(a, b *Collector, opts *AcceptanceOpts) (bool, error) {\n\tf := func(collected map[float64][]models.GettableAlerts) []*models.GettableAlert {\n\t\tresult := []*models.GettableAlert{}\n\t\tfor _, batches := range collected {\n\t\t\tfor _, batch := range batches {\n\t\t\t\tfor _, alert := range batch {\n\t\t\t\t\tresult = append(result, alert)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn result\n\t}\n\n\taAlerts := f(a.Collected())\n\tbAlerts := f(b.Collected())\n\n\tif len(aAlerts) != len(bAlerts) {\n\t\taAsString, err := alertsToString(aAlerts)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tbAsString, err := alertsToString(bAlerts)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\terr = fmt.Errorf(\n\t\t\t\"first collector has %v alerts, second collector has %v alerts\\n%v\\n%v\",\n\t\t\tlen(aAlerts), len(bAlerts),\n\t\t\taAsString, bAsString,\n\t\t)\n\t\treturn false, err\n\t}\n\n\tfor _, aAlert := range aAlerts {\n\t\tfound := false\n\t\tfor _, bAlert := range bAlerts {\n\t\t\tif EqualAlerts(aAlert, bAlert, opts) {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !found {\n\t\t\taAsString, err := alertsToString([]*models.GettableAlert{aAlert})\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t\tbAsString, err := alertsToString(bAlerts)\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\n\t\t\terr = fmt.Errorf(\n\t\t\t\t\"could not find matching alert for alert from first collector\\n%v\\nin alerts of second collector\\n%v\",\n\t\t\t\taAsString, bAsString,\n\t\t\t)\n\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\treturn true, nil\n}\n"
  },
  {
    "path": "test/testutils/mock.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage testutils\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-openapi/strfmt\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/notify/webhook\"\n)\n\n// At is a convenience method to allow for declarative syntax of Acceptance\n// test definitions.\nfunc At(ts float64) float64 {\n\treturn ts\n}\n\ntype Interval struct {\n\tstart, end float64\n}\n\nfunc (iv Interval) String() string {\n\treturn fmt.Sprintf(\"[%v,%v]\", iv.start, iv.end)\n}\n\nfunc (iv Interval) contains(f float64) bool {\n\treturn f >= iv.start && f <= iv.end\n}\n\n// Between is a convenience constructor for an interval for declarative syntax\n// of Acceptance test definitions.\nfunc Between(start, end float64) Interval {\n\treturn Interval{start: start, end: end}\n}\n\n// TestAlert models a model.Alert with relative times.\ntype TestAlert struct {\n\tLabels           models.LabelSet\n\tAnnotations      models.LabelSet\n\tStartsAt, EndsAt float64\n\tSummary          string // CLI-specific field, unused in with_api_v2\n}\n\n// Alert creates a new alert declaration with the given key/value pairs\n// as identifying labels.\nfunc Alert(keyval ...any) *TestAlert {\n\tif len(keyval)%2 == 1 {\n\t\tpanic(\"bad key/values\")\n\t}\n\ta := &TestAlert{\n\t\tLabels:      models.LabelSet{},\n\t\tAnnotations: models.LabelSet{},\n\t}\n\n\tfor i := 0; i < len(keyval); i += 2 {\n\t\tln := keyval[i].(string)\n\t\tlv := keyval[i+1].(string)\n\n\t\ta.Labels[ln] = lv\n\t}\n\n\treturn a\n}\n\n// NativeAlert converts the declared test alert into a full alert based\n// on the given parameters.\nfunc (a *TestAlert) NativeAlert(opts *AcceptanceOpts) *models.GettableAlert {\n\tna := &models.GettableAlert{\n\t\tAlert: models.Alert{\n\t\t\tLabels: a.Labels,\n\t\t},\n\t\tAnnotations: a.Annotations,\n\t\tStartsAt:    &strfmt.DateTime{},\n\t\tEndsAt:      &strfmt.DateTime{},\n\t}\n\n\tif a.StartsAt > 0 {\n\t\tstart := strfmt.DateTime(opts.ExpandTime(a.StartsAt))\n\t\tna.StartsAt = &start\n\t}\n\tif a.EndsAt > 0 {\n\t\tend := strfmt.DateTime(opts.ExpandTime(a.EndsAt))\n\t\tna.EndsAt = &end\n\t}\n\n\treturn na\n}\n\n// Annotate the alert with the given key/value pairs.\nfunc (a *TestAlert) Annotate(keyval ...any) *TestAlert {\n\tif len(keyval)%2 == 1 {\n\t\tpanic(\"bad key/values\")\n\t}\n\n\tfor i := 0; i < len(keyval); i += 2 {\n\t\tln := keyval[i].(string)\n\t\tlv := keyval[i+1].(string)\n\n\t\ta.Annotations[ln] = lv\n\t}\n\n\treturn a\n}\n\n// Active declares the relative activity time for this alert. It\n// must be a single starting value or two values where the second value\n// declares the resolved time.\nfunc (a *TestAlert) Active(tss ...float64) *TestAlert {\n\tif len(tss) > 2 || len(tss) == 0 {\n\t\tpanic(\"only one or two timestamps allowed\")\n\t}\n\tif len(tss) == 2 {\n\t\ta.EndsAt = tss[1]\n\t}\n\ta.StartsAt = tss[0]\n\n\treturn a\n}\n\n// HasLabels returns true if the two label sets are equivalent, otherwise false.\n// CLI-specific method, unused in with_api_v2.\nfunc (a *TestAlert) HasLabels(labels models.LabelSet) bool {\n\treturn reflect.DeepEqual(a.Labels, labels)\n}\n\n// EqualAlerts compares two alerts for equality, considering the tolerance.\nfunc EqualAlerts(a, b *models.GettableAlert, opts *AcceptanceOpts) bool {\n\tif !reflect.DeepEqual(a.Labels, b.Labels) {\n\t\treturn false\n\t}\n\tif !reflect.DeepEqual(a.Annotations, b.Annotations) {\n\t\treturn false\n\t}\n\n\tif !EqualTime(time.Time(*a.StartsAt), time.Time(*b.StartsAt), opts) {\n\t\treturn false\n\t}\n\tif (a.EndsAt == nil) != (b.EndsAt == nil) {\n\t\treturn false\n\t}\n\tif (a.EndsAt != nil) && (b.EndsAt != nil) && !EqualTime(time.Time(*a.EndsAt), time.Time(*b.EndsAt), opts) {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// EqualTime compares two times for equality within the tolerance.\nfunc EqualTime(a, b time.Time, opts *AcceptanceOpts) bool {\n\tif a.IsZero() != b.IsZero() {\n\t\treturn false\n\t}\n\n\tdiff := a.Sub(b)\n\tif diff < 0 {\n\t\tdiff = -diff\n\t}\n\treturn diff <= opts.Tolerance\n}\n\n// MockWebhook provides a mock HTTP webhook receiver for testing.\ntype MockWebhook struct {\n\topts      *AcceptanceOpts\n\tcollector *Collector\n\taddr      string\n\tclosing   atomic.Bool\n\n\t// Func is called early on when retrieving a notification by an\n\t// Alertmanager. If Func returns true, the given notification is dropped.\n\t// See sample usage in `send_test.go/TestRetry()`.\n\tFunc func(timestamp float64) bool\n}\n\n// NewWebhook creates a new MockWebhook that collects alerts via HTTP.\nfunc NewWebhook(t *testing.T, c *Collector) *MockWebhook {\n\tt.Helper()\n\n\twh := &MockWebhook{\n\t\tcollector: c,\n\t\topts:      c.Opts(),\n\t}\n\n\tserver := httptest.NewServer(wh)\n\twh.addr = server.Listener.Addr().String()\n\n\tt.Cleanup(func() {\n\t\twh.closing.Store(true)\n\t\tserver.Close()\n\t})\n\n\treturn wh\n}\n\n// ServeHTTP handles incoming webhook requests.\nfunc (ws *MockWebhook) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\t// Inject drop function if it exists.\n\tif ws.Func != nil {\n\t\tif ws.Func(ws.opts.RelativeTime(time.Now())) {\n\t\t\treturn\n\t\t}\n\t}\n\n\tdec := json.NewDecoder(req.Body)\n\tdefer req.Body.Close()\n\n\tvar v webhook.Message\n\tif err := dec.Decode(&v); err != nil {\n\t\t// During shutdown, ignore EOF errors from interrupted connections\n\t\tif ws.closing.Load() && (err == io.EOF || err.Error() == \"EOF\") {\n\t\t\treturn\n\t\t}\n\t\tpanic(err)\n\t}\n\n\t// Transform the webhook message alerts back into model.Alerts.\n\tvar alerts models.GettableAlerts\n\tfor _, a := range v.Alerts {\n\t\tvar (\n\t\t\tlabels      = models.LabelSet{}\n\t\t\tannotations = models.LabelSet{}\n\t\t)\n\t\tmaps.Copy(labels, a.Labels)\n\t\tmaps.Copy(annotations, a.Annotations)\n\n\t\tstart := strfmt.DateTime(a.StartsAt)\n\t\tend := strfmt.DateTime(a.EndsAt)\n\n\t\talerts = append(alerts, &models.GettableAlert{\n\t\t\tAlert: models.Alert{\n\t\t\t\tLabels:       labels,\n\t\t\t\tGeneratorURL: strfmt.URI(a.GeneratorURL),\n\t\t\t},\n\t\t\tAnnotations: annotations,\n\t\t\tStartsAt:    &start,\n\t\t\tEndsAt:      &end,\n\t\t})\n\t}\n\n\tws.collector.Add(alerts...)\n}\n\n// Address returns the address of the mock webhook server.\nfunc (ws *MockWebhook) Address() string {\n\treturn ws.addr\n}\n"
  },
  {
    "path": "test/with_api_v2/acceptance/api_test.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/client/alert\"\n\t\"github.com/prometheus/alertmanager/api/v2/client/silence\"\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/featurecontrol\"\n\ta \"github.com/prometheus/alertmanager/test/with_api_v2\"\n)\n\nfunc TestAddAlerts(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  10m\n  repeat_interval: 1h\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\tat := a.NewAcceptanceTest(t, &a.AcceptanceOpts{\n\t\tFeatureFlags: []string{featurecontrol.FeatureClassicMode},\n\t\tTolerance:    1 * time.Second,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := a.NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\trequire.NoError(t, amc.Start())\n\tdefer amc.Terminate()\n\n\tam := amc.Members()[0]\n\n\tnow := time.Now()\n\tpa := &models.PostableAlert{\n\t\tStartsAt: strfmt.DateTime(now),\n\t\tEndsAt:   strfmt.DateTime(now.Add(5 * time.Minute)),\n\t\tAlert: models.Alert{\n\t\t\tLabels: models.LabelSet{\n\t\t\t\t\"a\": \"b\",\n\t\t\t\t\"b\": \"Σ\",\n\t\t\t\t\"c\": \"\\xf0\\x9f\\x99\\x82\",\n\t\t\t\t\"d\": \"eΘ\",\n\t\t\t},\n\t\t},\n\t}\n\talertParams := alert.NewPostAlertsParams()\n\talertParams.Alerts = models.PostableAlerts{pa}\n\n\t_, err := am.Client().Alert.PostAlerts(alertParams)\n\trequire.NoError(t, err)\n}\n\n// TestAlertGetReturnsCurrentStatus checks that querying the API returns the\n// current status of each alert, i.e. if it is silenced or inhibited.\nfunc TestAlertGetReturnsCurrentAlertStatus(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  10m\n  repeat_interval: 1h\n\ninhibit_rules:\n  - source_match:\n      severity: 'critical'\n    target_match:\n      severity: 'warning'\n    equal: ['alertname']\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\tat := a.NewAcceptanceTest(t, &a.AcceptanceOpts{\n\t\tTolerance: 1 * time.Second,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := a.NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\trequire.NoError(t, amc.Start())\n\tdefer amc.Terminate()\n\n\tam := amc.Members()[0]\n\n\tlabelName := \"alertname\"\n\tlabelValue := \"test1\"\n\n\tnow := time.Now()\n\tstartsAt := strfmt.DateTime(now)\n\tendsAt := strfmt.DateTime(now.Add(5 * time.Minute))\n\n\tlabels := models.LabelSet(map[string]string{labelName: labelValue, \"severity\": \"warning\"})\n\tfp := model.LabelSet{model.LabelName(labelName): model.LabelValue(labelValue), \"severity\": \"warning\"}.Fingerprint()\n\tpa := &models.PostableAlert{\n\t\tStartsAt: startsAt,\n\t\tEndsAt:   endsAt,\n\t\tAlert:    models.Alert{Labels: labels},\n\t}\n\talertParams := alert.NewPostAlertsParams()\n\talertParams.Alerts = models.PostableAlerts{pa}\n\t_, err := am.Client().Alert.PostAlerts(alertParams)\n\trequire.NoError(t, err)\n\n\tresp, err := am.Client().Alert.GetAlerts(nil)\n\trequire.NoError(t, err)\n\t// No silence has been created or inhibiting alert sent, alert should\n\t// be active.\n\tfor _, al := range resp.Payload {\n\t\trequire.Equal(t, models.AlertStatusStateActive, *al.Status.State)\n\t}\n\n\t// Wait for group_wait, so that we are in the group_interval period,\n\t// when the pipeline won't update the alert's status.\n\ttime.Sleep(2 * time.Second)\n\n\t// Create silence and verify that the alert is immediately marked\n\t// silenced via the API.\n\tsilenceParams := silence.NewPostSilencesParams()\n\n\tcm := \"a\"\n\tisRegex := false\n\tps := &models.PostableSilence{\n\t\tSilence: models.Silence{\n\t\t\tStartsAt:  &startsAt,\n\t\t\tEndsAt:    &endsAt,\n\t\t\tComment:   &cm,\n\t\t\tCreatedBy: &cm,\n\t\t\tMatchers: models.Matchers{\n\t\t\t\t&models.Matcher{Name: &labelName, Value: &labelValue, IsRegex: &isRegex},\n\t\t\t},\n\t\t},\n\t}\n\tsilenceParams.Silence = ps\n\tsilenceResp, err := am.Client().Silence.PostSilences(silenceParams)\n\trequire.NoError(t, err)\n\tsilenceID := silenceResp.Payload.SilenceID\n\n\tresp, err = am.Client().Alert.GetAlerts(nil)\n\trequire.NoError(t, err)\n\tfor _, al := range resp.Payload {\n\t\trequire.Equal(t, models.AlertStatusStateSuppressed, *al.Status.State)\n\t\trequire.Equal(t, fp.String(), *al.Fingerprint)\n\t\trequire.Len(t, al.Status.SilencedBy, 1)\n\t\trequire.Equal(t, silenceID, al.Status.SilencedBy[0])\n\t}\n\n\t// Create inhibiting alert and verify that original alert is\n\t// immediately marked as inhibited.\n\tlabels[\"severity\"] = \"critical\"\n\t_, err = am.Client().Alert.PostAlerts(alertParams)\n\trequire.NoError(t, err)\n\n\tinhibitingFP := model.LabelSet{model.LabelName(labelName): model.LabelValue(labelValue), \"severity\": \"critical\"}.Fingerprint()\n\n\tresp, err = am.Client().Alert.GetAlerts(nil)\n\trequire.NoError(t, err)\n\tfor _, al := range resp.Payload {\n\t\trequire.Len(t, al.Status.SilencedBy, 1)\n\t\trequire.Equal(t, silenceID, al.Status.SilencedBy[0])\n\t\tif fp.String() == *al.Fingerprint {\n\t\t\trequire.Equal(t, models.AlertStatusStateSuppressed, *al.Status.State)\n\t\t\trequire.Equal(t, fp.String(), *al.Fingerprint)\n\t\t\trequire.Len(t, al.Status.InhibitedBy, 1)\n\t\t\trequire.Equal(t, inhibitingFP.String(), al.Status.InhibitedBy[0])\n\t\t}\n\t}\n\n\tdeleteParams := silence.NewDeleteSilenceParams().WithSilenceID(strfmt.UUID(silenceID))\n\t_, err = am.Client().Silence.DeleteSilence(deleteParams)\n\trequire.NoError(t, err)\n\n\tresp, err = am.Client().Alert.GetAlerts(nil)\n\trequire.NoError(t, err)\n\t// Silence has been deleted, inhibiting alert should be active.\n\t// Original alert should still be inhibited.\n\tfor _, al := range resp.Payload {\n\t\trequire.Empty(t, al.Status.SilencedBy)\n\t\tif inhibitingFP.String() == *al.Fingerprint {\n\t\t\trequire.Equal(t, models.AlertStatusStateActive, *al.Status.State)\n\t\t} else {\n\t\t\trequire.Equal(t, models.AlertStatusStateSuppressed, *al.Status.State)\n\t\t}\n\t}\n}\n\nfunc TestFilterAlertRequest(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  10m\n  repeat_interval: 1h\n\ninhibit_rules:\n  - source_match:\n      severity: 'critical'\n    target_match:\n      severity: 'warning'\n    equal: ['alertname']\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\tat := a.NewAcceptanceTest(t, &a.AcceptanceOpts{\n\t\tTolerance: 1 * time.Second,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := a.NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\trequire.NoError(t, amc.Start())\n\tdefer amc.Terminate()\n\n\tam := amc.Members()[0]\n\n\tnow := time.Now()\n\tstartsAt := strfmt.DateTime(now)\n\tendsAt := strfmt.DateTime(now.Add(5 * time.Minute))\n\n\tlabels := models.LabelSet(map[string]string{\"alertname\": \"test1\", \"severity\": \"warning\"})\n\tpa1 := &models.PostableAlert{\n\t\tStartsAt: startsAt,\n\t\tEndsAt:   endsAt,\n\t\tAlert:    models.Alert{Labels: labels},\n\t}\n\tlabels = models.LabelSet(map[string]string{\"system\": \"foo\", \"severity\": \"critical\"})\n\tpa2 := &models.PostableAlert{\n\t\tStartsAt: startsAt,\n\t\tEndsAt:   endsAt,\n\t\tAlert:    models.Alert{Labels: labels},\n\t}\n\talertParams := alert.NewPostAlertsParams()\n\talertParams.Alerts = models.PostableAlerts{pa1, pa2}\n\t_, err := am.Client().Alert.PostAlerts(alertParams)\n\trequire.NoError(t, err)\n\n\tfilter := []string{\"alertname=test1\", \"severity=warning\"}\n\tresp, err := am.Client().Alert.GetAlerts(alert.NewGetAlertsParams().WithFilter(filter))\n\trequire.NoError(t, err)\n\trequire.Len(t, resp.Payload, 1)\n\tfor _, al := range resp.Payload {\n\t\trequire.Equal(t, models.AlertStatusStateActive, *al.Status.State)\n\t}\n}\n"
  },
  {
    "path": "test/with_api_v2/acceptance/cluster_test.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\ta \"github.com/prometheus/alertmanager/test/with_api_v2\"\n)\n\n// TestClusterDeduplication tests, that in an Alertmanager cluster of 3\n// instances, only one should send a notification for a given alert.\nfunc TestClusterDeduplication(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1h\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\tat := a.NewAcceptanceTest(t, &a.AcceptanceOpts{\n\t\tTolerance: 1 * time.Second,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := a.NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 3)\n\n\tamc.Push(a.At(1), a.Alert(\"alertname\", \"test1\"))\n\n\tco.Want(a.Between(2, 3), a.Alert(\"alertname\", \"test1\").Active(1))\n\n\tat.Run()\n\n\tt.Log(co.Check())\n}\n\n// TestClusterVSInstance compares notifications sent by Alertmanager cluster to\n// notifications sent by single instance.\nfunc TestClusterVSInstance(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: [ \"alertname\" ]\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1h\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\tacceptanceOpts := func() *a.AcceptanceOpts {\n\t\treturn &a.AcceptanceOpts{\n\t\t\tTolerance: 2 * time.Second,\n\t\t}\n\t}\n\n\tclusterSizes := []int{1, 3}\n\n\ttests := []*a.AcceptanceTest{\n\t\ta.NewAcceptanceTest(t, acceptanceOpts()),\n\t\ta.NewAcceptanceTest(t, acceptanceOpts()),\n\t}\n\n\tcollectors := []*a.Collector{}\n\tamClusters := []*a.AlertmanagerCluster{}\n\twg := sync.WaitGroup{}\n\n\tfor i, tc := range tests {\n\t\tcollectors = append(collectors, tc.Collector(\"webhook\"))\n\t\twebhook := a.NewWebhook(t, collectors[i])\n\n\t\tamClusters = append(amClusters, tc.AlertmanagerCluster(fmt.Sprintf(conf, webhook.Address()), clusterSizes[i]))\n\n\t\twg.Add(1)\n\t}\n\n\tfor _, alertTime := range []float64{0, 2, 4, 6, 8} {\n\t\tfor i, amc := range amClusters {\n\t\t\talert := a.Alert(\"alertname\", fmt.Sprintf(\"test1-%v\", alertTime))\n\t\t\tamc.Push(a.At(alertTime), alert)\n\t\t\tcollectors[i].Want(a.Between(alertTime, alertTime+5), alert.Active(alertTime))\n\t\t}\n\t}\n\n\tfor _, t := range tests {\n\t\tgo func(t *a.AcceptanceTest) {\n\t\t\tt.Run()\n\t\t\twg.Done()\n\t\t}(t)\n\t}\n\n\twg.Wait()\n\n\t_, err := a.CompareCollectors(collectors[0], collectors[1], acceptanceOpts())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "test/with_api_v2/acceptance/inhibit_test.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t. \"github.com/prometheus/alertmanager/test/with_api_v2\"\n)\n\nfunc TestInhibiting(t *testing.T) {\n\tt.Parallel()\n\n\t// This integration test checks that alerts can be inhibited and that an\n\t// inhibited alert will be notified again as soon as the inhibiting alert\n\t// gets resolved.\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1s\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n\ninhibit_rules:\n- source_match:\n    alertname: JobDown\n  target_match:\n    alertname: InstanceDown\n  equal:\n    - job\n    - zone\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 150 * time.Millisecond,\n\t})\n\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\n\tamc.Push(At(1), Alert(\"alertname\", \"test1\", \"job\", \"testjob\", \"zone\", \"aa\"))\n\tamc.Push(At(1), Alert(\"alertname\", \"InstanceDown\", \"job\", \"testjob\", \"zone\", \"aa\"))\n\tamc.Push(At(1), Alert(\"alertname\", \"InstanceDown\", \"job\", \"testjob\", \"zone\", \"ab\"))\n\n\t// This JobDown in zone aa should inhibit InstanceDown in zone aa in the\n\t// second batch of notifications.\n\tamc.Push(At(2.2), Alert(\"alertname\", \"JobDown\", \"job\", \"testjob\", \"zone\", \"aa\"))\n\n\t// InstanceDown in zone aa should fire again in the third batch of\n\t// notifications once JobDown in zone aa gets resolved.\n\tamc.Push(At(3.6), Alert(\"alertname\", \"JobDown\", \"job\", \"testjob\", \"zone\", \"aa\").Active(2.2, 3.6))\n\n\tco.Want(Between(2, 2.5),\n\t\tAlert(\"alertname\", \"test1\", \"job\", \"testjob\", \"zone\", \"aa\").Active(1),\n\t\tAlert(\"alertname\", \"InstanceDown\", \"job\", \"testjob\", \"zone\", \"aa\").Active(1),\n\t\tAlert(\"alertname\", \"InstanceDown\", \"job\", \"testjob\", \"zone\", \"ab\").Active(1),\n\t)\n\n\tco.Want(Between(3, 3.5),\n\t\tAlert(\"alertname\", \"test1\", \"job\", \"testjob\", \"zone\", \"aa\").Active(1),\n\t\tAlert(\"alertname\", \"InstanceDown\", \"job\", \"testjob\", \"zone\", \"ab\").Active(1),\n\t\tAlert(\"alertname\", \"JobDown\", \"job\", \"testjob\", \"zone\", \"aa\").Active(2.2),\n\t)\n\n\tco.Want(Between(4, 4.5),\n\t\tAlert(\"alertname\", \"test1\", \"job\", \"testjob\", \"zone\", \"aa\").Active(1),\n\t\tAlert(\"alertname\", \"InstanceDown\", \"job\", \"testjob\", \"zone\", \"aa\").Active(1),\n\t\tAlert(\"alertname\", \"InstanceDown\", \"job\", \"testjob\", \"zone\", \"ab\").Active(1),\n\t\tAlert(\"alertname\", \"JobDown\", \"job\", \"testjob\", \"zone\", \"aa\").Active(2.2, 3.6),\n\t)\n\n\tat.Run()\n\n\tt.Log(co.Check())\n}\n\nfunc TestAlwaysInhibiting(t *testing.T) {\n\tt.Parallel()\n\n\t// This integration test checks that when inhibited and inhibiting alerts\n\t// gets resolved at the same time, the final notification contains both\n\t// alerts.\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1s\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n\ninhibit_rules:\n- source_match:\n    alertname: JobDown\n  target_match:\n    alertname: InstanceDown\n  equal:\n    - job\n    - zone\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 150 * time.Millisecond,\n\t})\n\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\n\tamc.Push(At(1), Alert(\"alertname\", \"InstanceDown\", \"job\", \"testjob\", \"zone\", \"aa\"))\n\tamc.Push(At(1), Alert(\"alertname\", \"JobDown\", \"job\", \"testjob\", \"zone\", \"aa\"))\n\n\tamc.Push(At(2.6), Alert(\"alertname\", \"JobDown\", \"job\", \"testjob\", \"zone\", \"aa\").Active(1, 2.6))\n\tamc.Push(At(2.6), Alert(\"alertname\", \"InstanceDown\", \"job\", \"testjob\", \"zone\", \"aa\").Active(1, 2.6))\n\n\tco.Want(Between(2, 2.5),\n\t\tAlert(\"alertname\", \"JobDown\", \"job\", \"testjob\", \"zone\", \"aa\").Active(1),\n\t)\n\n\tco.Want(Between(3, 3.5),\n\t\tAlert(\"alertname\", \"InstanceDown\", \"job\", \"testjob\", \"zone\", \"aa\").Active(1, 2.6),\n\t\tAlert(\"alertname\", \"JobDown\", \"job\", \"testjob\", \"zone\", \"aa\").Active(1, 2.6),\n\t)\n\n\tat.Run()\n\n\tt.Log(co.Check())\n}\n\nfunc TestEmptyInhibitionRule(t *testing.T) {\n\tt.Parallel()\n\n\t// This integration test checks that when we have empty inhibition rules,\n\t// there is no panic caused by null-pointer references.\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1s\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n\ninhibit_rules:\n-\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 150 * time.Millisecond,\n\t})\n\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tat.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\tat.Run()\n\n\tt.Log(co.Check())\n}\n"
  },
  {
    "path": "test/with_api_v2/acceptance/send_test.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t. \"github.com/prometheus/alertmanager/test/with_api_v2\"\n)\n\n// This file contains acceptance tests around the basic sending logic\n// for notifications, which includes batching and ensuring that each\n// notification is eventually sent at least once and ideally exactly\n// once.\n\nfunc testMergeAlerts(t *testing.T, endsAt bool) {\n\tt.Parallel()\n\n\ttimerange := func(ts float64) []float64 {\n\t\tif !endsAt {\n\t\t\treturn []float64{ts}\n\t\t}\n\t\treturn []float64{ts, ts + 3.0}\n\t}\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: [alertname]\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1ms\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n    send_resolved: true\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 150 * time.Millisecond,\n\t})\n\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tam := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\n\t// Refresh an alert several times. The starting time must remain at the earliest\n\t// point in time.\n\tam.Push(At(1), Alert(\"alertname\", \"test\").Active(timerange(1.1)...))\n\t// Another Prometheus server might be sending later but with an earlier start time.\n\tam.Push(At(1.2), Alert(\"alertname\", \"test\").Active(1))\n\n\tco.Want(Between(2, 2.5), Alert(\"alertname\", \"test\").Active(1))\n\n\tam.Push(At(2.1), Alert(\"alertname\", \"test\").Annotate(\"ann\", \"v1\").Active(timerange(2)...))\n\n\tco.Want(Between(3, 3.5), Alert(\"alertname\", \"test\").Annotate(\"ann\", \"v1\").Active(1))\n\n\t// Annotations are always overwritten by the alert that arrived most recently.\n\tam.Push(At(3.6), Alert(\"alertname\", \"test\").Annotate(\"ann\", \"v2\").Active(timerange(1.5)...))\n\n\tco.Want(Between(4, 4.5), Alert(\"alertname\", \"test\").Annotate(\"ann\", \"v2\").Active(1))\n\n\t// If an alert is marked resolved twice, the latest point in time must be\n\t// set as the eventual resolve time.\n\tam.Push(At(4.6), Alert(\"alertname\", \"test\").Annotate(\"ann\", \"v2\").Active(3, 4.5))\n\tam.Push(At(4.8), Alert(\"alertname\", \"test\").Annotate(\"ann\", \"v3\").Active(2.9, 4.8))\n\tam.Push(At(4.8), Alert(\"alertname\", \"test\").Annotate(\"ann\", \"v3\").Active(2.9, 4.1))\n\n\tco.Want(Between(5, 5.5), Alert(\"alertname\", \"test\").Annotate(\"ann\", \"v3\").Active(1, 4.8))\n\n\t// Reactivate an alert after a previous occurrence has been resolved.\n\t// No overlap, no merge must occur.\n\tam.Push(At(5.3), Alert(\"alertname\", \"test\").Active(timerange(5)...))\n\n\tco.Want(Between(6, 6.5), Alert(\"alertname\", \"test\").Active(5))\n\n\tat.Run()\n\n\tt.Log(co.Check())\n}\n\nfunc TestMergeAlerts(t *testing.T) {\n\ttestMergeAlerts(t, false)\n}\n\n// This test is similar to TestMergeAlerts except that the firing alerts have\n// the EndsAt field set to StartsAt + 3s. This is what Prometheus starting from\n// version 2.4.0 sends to AlertManager.\nfunc TestMergeAlertsWithEndsAt(t *testing.T) {\n\ttestMergeAlerts(t, true)\n}\n\nfunc TestRepeat(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: [alertname]\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1ms\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\t// Create a new acceptance test that instantiates new Alertmanagers\n\t// with the given configuration and verifies times with the given\n\t// tolerance.\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 150 * time.Millisecond,\n\t})\n\n\t// Create a collector to which alerts can be written and verified\n\t// against a set of expected alert notifications.\n\tco := at.Collector(\"webhook\")\n\t// Run something that satisfies the webhook interface to which the\n\t// Alertmanager pushes as defined by its configuration.\n\twh := NewWebhook(t, co)\n\n\t// Create a new Alertmanager process listening to a random port\n\tam := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\n\t// Declare pushes to be made to the Alertmanager at the given time.\n\t// Times are provided in fractions of seconds.\n\tam.Push(At(1), Alert(\"alertname\", \"test\").Active(1))\n\n\t// XXX(fabxc): disabled as long as alerts are not persisted.\n\t// at.Do(At(1.2), func() {\n\t//\tam.Terminate()\n\t//\tam.Start()\n\t// })\n\tam.Push(At(3.5), Alert(\"alertname\", \"test\").Active(1, 3))\n\n\t// Declare which alerts are expected to arrive at the collector within\n\t// the defined time intervals.\n\tco.Want(Between(2, 2.5), Alert(\"alertname\", \"test\").Active(1))\n\tco.Want(Between(3, 3.5), Alert(\"alertname\", \"test\").Active(1))\n\tco.Want(Between(4, 4.5), Alert(\"alertname\", \"test\").Active(1, 3))\n\n\t// Start the flow as defined above and run the checks afterwards.\n\tat.Run()\n\n\tt.Log(co.Check())\n}\n\nfunc TestRetry(t *testing.T) {\n\tt.Parallel()\n\n\t// We create a notification config that fans out into two different\n\t// webhooks.\n\t// The succeeding one must still only receive the first successful\n\t// notifications. Sending to the succeeding one must eventually succeed.\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: [alertname]\n  group_wait:      1s\n  group_interval:  2s\n  repeat_interval: 3s\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n  - url: 'http://%s'\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 150 * time.Millisecond,\n\t})\n\n\tco1 := at.Collector(\"webhook\")\n\twh1 := NewWebhook(t, co1)\n\n\tco2 := at.Collector(\"webhook_failing\")\n\twh2 := NewWebhook(t, co2)\n\n\twh2.Func = func(ts float64) bool {\n\t\t// Fail the first interval period but eventually succeed in the third\n\t\t// interval after a few failed attempts.\n\t\treturn ts < 4.5\n\t}\n\n\tam := at.AlertmanagerCluster(fmt.Sprintf(conf, wh1.Address(), wh2.Address()), 1)\n\n\tam.Push(At(1), Alert(\"alertname\", \"test1\"))\n\n\tco1.Want(Between(2, 2.5), Alert(\"alertname\", \"test1\").Active(1))\n\tco1.Want(Between(6, 6.5), Alert(\"alertname\", \"test1\").Active(1))\n\n\tco2.Want(Between(6, 6.5), Alert(\"alertname\", \"test1\").Active(1))\n\n\tat.Run()\n\n\tfor _, c := range []*Collector{co1, co2} {\n\t\tt.Log(c.Check())\n\t}\n}\n\nfunc TestBatching(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  1s\n  # use a value slightly below the 5s interval to avoid timing issues\n  repeat_interval: 4900ms\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 150 * time.Millisecond,\n\t})\n\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tam := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\n\tam.Push(At(1.1), Alert(\"alertname\", \"test1\").Active(1))\n\tam.Push(At(1.7), Alert(\"alertname\", \"test5\").Active(1))\n\n\tco.Want(Between(2.0, 2.5),\n\t\tAlert(\"alertname\", \"test1\").Active(1),\n\t\tAlert(\"alertname\", \"test5\").Active(1),\n\t)\n\n\tam.Push(At(3.3),\n\t\tAlert(\"alertname\", \"test2\").Active(1.5),\n\t\tAlert(\"alertname\", \"test3\").Active(1.5),\n\t\tAlert(\"alertname\", \"test4\").Active(1.6),\n\t)\n\n\tco.Want(Between(4.1, 4.5),\n\t\tAlert(\"alertname\", \"test1\").Active(1),\n\t\tAlert(\"alertname\", \"test5\").Active(1),\n\t\tAlert(\"alertname\", \"test2\").Active(1.5),\n\t\tAlert(\"alertname\", \"test3\").Active(1.5),\n\t\tAlert(\"alertname\", \"test4\").Active(1.6),\n\t)\n\n\t// While no changes happen expect no additional notifications\n\t// until the 5s repeat interval has ended.\n\n\tco.Want(Between(9.1, 9.5),\n\t\tAlert(\"alertname\", \"test1\").Active(1),\n\t\tAlert(\"alertname\", \"test5\").Active(1),\n\t\tAlert(\"alertname\", \"test2\").Active(1.5),\n\t\tAlert(\"alertname\", \"test3\").Active(1.5),\n\t\tAlert(\"alertname\", \"test4\").Active(1.6),\n\t)\n\n\tat.Run()\n\n\tt.Log(co.Check())\n}\n\nfunc TestResolved(t *testing.T) {\n\tt.Parallel()\n\n\tfor range 2 {\n\t\tconf := `\nglobal:\n  resolve_timeout: 10s\n\nroute:\n  receiver: \"default\"\n  group_by: [alertname]\n  group_wait: 1s\n  group_interval: 5s\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\t\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\t\tTolerance: 150 * time.Millisecond,\n\t\t})\n\n\t\tco := at.Collector(\"webhook\")\n\t\twh := NewWebhook(t, co)\n\n\t\tam := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\n\t\tam.Push(At(1),\n\t\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v1\"),\n\t\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v2\"),\n\t\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v3\"),\n\t\t)\n\n\t\tco.Want(Between(2, 2.5),\n\t\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v1\").Active(1),\n\t\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v2\").Active(1),\n\t\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v3\").Active(1),\n\t\t)\n\t\tco.Want(Between(12, 13),\n\t\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v1\").Active(1, 11),\n\t\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v2\").Active(1, 11),\n\t\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v3\").Active(1, 11),\n\t\t)\n\n\t\tat.Run()\n\n\t\tt.Log(co.Check())\n\t}\n}\n\nfunc TestResolvedFilter(t *testing.T) {\n\tt.Parallel()\n\n\t// This integration test ensures that even though resolved alerts may not be\n\t// notified about, they must be set as notified. Resolved alerts, even when\n\t// filtered, have to end up in the SetNotifiesStage, otherwise when an alert\n\t// fires again it is ambiguous whether it was resolved in between or not.\n\n\tconf := `\nglobal:\n  resolve_timeout: 10s\n\nroute:\n  receiver: \"default\"\n  group_by: [alertname]\n  group_wait: 1s\n  group_interval: 5s\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n    send_resolved: true\n  - url: 'http://%s'\n    send_resolved: false\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 150 * time.Millisecond,\n\t})\n\n\tco1 := at.Collector(\"webhook1\")\n\twh1 := NewWebhook(t, co1)\n\n\tco2 := at.Collector(\"webhook2\")\n\twh2 := NewWebhook(t, co2)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh1.Address(), wh2.Address()), 1)\n\n\tamc.Push(At(1),\n\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v1\"),\n\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v2\"),\n\t)\n\tamc.Push(At(3),\n\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v1\").Active(1, 4),\n\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v3\"),\n\t)\n\tamc.Push(At(8),\n\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v3\").Active(3),\n\t)\n\n\tco1.Want(Between(2, 2.5),\n\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v1\").Active(1),\n\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v2\").Active(1),\n\t)\n\tco1.Want(Between(7, 7.5),\n\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v1\").Active(1, 4),\n\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v2\").Active(1),\n\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v3\").Active(3),\n\t)\n\t// Notification should be sent because the v2 alert is resolved due to the time-out.\n\tco1.Want(Between(12, 12.5),\n\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v2\").Active(1, 11),\n\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v3\").Active(3),\n\t)\n\n\tco2.Want(Between(2, 2.5),\n\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v1\").Active(1),\n\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v2\").Active(1),\n\t)\n\tco2.Want(Between(7, 7.5),\n\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v2\").Active(1),\n\t\tAlert(\"alertname\", \"test\", \"lbl\", \"v3\").Active(3),\n\t)\n\t// No notification should be sent after group_interval because no new alert has been fired.\n\tco2.Want(Between(12, 12.5))\n\n\tat.Run()\n\n\tfor _, c := range []*Collector{co1, co2} {\n\t\tt.Log(c.Check())\n\t}\n}\n\nfunc TestColdStart(t *testing.T) {\n\tt.Parallel()\n\n\t// This integration test ensures that the first alert isn't notified before\n\t// the AlertManager process has started considering the resend delay.\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  6s\n  repeat_interval: 10m\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 150 * time.Millisecond,\n\t})\n\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\n\tamc.Push(At(1), Alert(\"alertname\", \"test1\").Active(-100))\n\tamc.Push(At(2), Alert(\"alertname\", \"test2\"))\n\n\t// Alerts are dispatched 5 seconds after the AlertManager process has started.\n\t// start delay: 5s\n\t// first alert received at: 1s\n\t// first alert dispatched at: 5s - 1s = 4s\n\tco.Want(Between(4, 5),\n\t\tAlert(\"alertname\", \"test1\").Active(1),\n\t\tAlert(\"alertname\", \"test2\").Active(4),\n\t)\n\n\t// Reload AlertManager process.\n\tat.Do(At(5), amc.Reload)\n\n\tamc.Push(At(6), Alert(\"alertname\", \"test3\").Active(-100))\n\tamc.Push(At(7), Alert(\"alertname\", \"test4\"))\n\n\t// Group interval is applied on top of start delay.\n\t// start delay: 5s\n\t// group interval: 6s\n\t// alerts dispatched at: 5s + 6s = 11s\n\tco.Want(Between(11, 11.5),\n\t\tAlert(\"alertname\", \"test1\").Active(1),\n\t\tAlert(\"alertname\", \"test2\").Active(4),\n\t\tAlert(\"alertname\", \"test3\").Active(6),\n\t\tAlert(\"alertname\", \"test4\").Active(7),\n\t)\n\n\tat.Run(\"--dispatch.start-delay\", \"5s\")\n\n\tt.Log(co.Check())\n}\n\nfunc TestReload(t *testing.T) {\n\tt.Parallel()\n\n\t// This integration test ensures that the first alert isn't notified twice\n\t// and repeat_interval applies after the AlertManager process has been\n\t// reloaded.\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  6s\n  repeat_interval: 10m\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 150 * time.Millisecond,\n\t})\n\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\n\tamc.Push(At(1), Alert(\"alertname\", \"test1\"))\n\tat.Do(At(3), amc.Reload)\n\tamc.Push(At(4), Alert(\"alertname\", \"test2\"))\n\n\tco.Want(Between(2, 2.5), Alert(\"alertname\", \"test1\").Active(1))\n\t// Timers are reset on reload regardless, so we count the 6 second group\n\t// interval from 3 onwards.\n\tco.Want(Between(9, 9.5),\n\t\tAlert(\"alertname\", \"test1\").Active(1),\n\t\tAlert(\"alertname\", \"test2\").Active(4),\n\t)\n\n\tat.Run()\n\n\tt.Log(co.Check())\n}\n\nfunc TestWebhookTimeout(t *testing.T) {\n\tt.Parallel()\n\n\t// This integration test uses an extended group_interval to check that\n\t// the webhook level timeout has the desired effect, and that notification\n\t// sending is retried in this case.\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: [alertname]\n  group_wait:      1s\n  group_interval:  1m\n  repeat_interval: 1m\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n    timeout: 500ms\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 150 * time.Millisecond,\n\t})\n\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\twh.Func = func(ts float64) bool {\n\t\t// Make some webhook requests slow enough to hit the webhook\n\t\t// timeout, but not so slow as to hit the dispatcher timeout.\n\t\tif ts < 3 {\n\t\t\ttime.Sleep(time.Second)\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\n\tam := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\n\tam.Push(At(1), Alert(\"alertname\", \"test1\"))\n\n\tco.Want(Between(3, 4), Alert(\"alertname\", \"test1\").Active(1))\n\n\tat.Run()\n\n\tt.Log(co.Check())\n}\n"
  },
  {
    "path": "test/with_api_v2/acceptance/silence_test.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/client/silence\"\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/featurecontrol\"\n\t. \"github.com/prometheus/alertmanager/test/with_api_v2\"\n)\n\nfunc TestAddSilence(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1ms\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tFeatureFlags: []string{featurecontrol.FeatureClassicMode},\n\t\tTolerance:    150 * time.Millisecond,\n\t})\n\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\trequire.NoError(t, amc.Start())\n\tdefer amc.Terminate()\n\n\tam := amc.Members()[0]\n\n\tnow := time.Now()\n\tps := models.PostableSilence{\n\t\tSilence: models.Silence{\n\t\t\tComment:   stringPtr(\"test\"),\n\t\t\tCreatedBy: stringPtr(\"test\"),\n\t\t\tMatchers: models.Matchers{{\n\t\t\t\tName:    stringPtr(\"foo\"),\n\t\t\t\tIsEqual: boolPtr(true),\n\t\t\t\tIsRegex: boolPtr(false),\n\t\t\t\tValue:   stringPtr(\"bar\"),\n\t\t\t}},\n\t\t\tStartsAt: dateTimePtr(strfmt.DateTime(now)),\n\t\t\tEndsAt:   dateTimePtr(strfmt.DateTime(now.Add(24 * time.Hour))),\n\t\t},\n\t}\n\tsilenceParams := silence.NewPostSilencesParams()\n\tsilenceParams.Silence = &ps\n\n\t_, err := am.Client().Silence.PostSilences(silenceParams)\n\trequire.NoError(t, err)\n}\n\nfunc TestSilencing(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1ms\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 150 * time.Millisecond,\n\t})\n\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\n\t// No repeat interval is configured. Thus, we receive an alert\n\t// notification every second.\n\tamc.Push(At(1), Alert(\"alertname\", \"test1\").Active(1))\n\tamc.Push(At(1), Alert(\"alertname\", \"test2\").Active(1))\n\n\tco.Want(Between(2, 2.5),\n\t\tAlert(\"alertname\", \"test1\").Active(1),\n\t\tAlert(\"alertname\", \"test2\").Active(1),\n\t)\n\n\t// Add a silence that affects the first alert.\n\tamc.SetSilence(At(2.3), Silence(2.5, 4.5).Match(\"alertname\", \"test1\"))\n\n\tco.Want(Between(3, 3.5), Alert(\"alertname\", \"test2\").Active(1))\n\tco.Want(Between(4, 4.5), Alert(\"alertname\", \"test2\").Active(1))\n\n\t// Silence should be over now and we receive both alerts again.\n\n\tco.Want(Between(5, 5.5),\n\t\tAlert(\"alertname\", \"test1\").Active(1),\n\t\tAlert(\"alertname\", \"test2\").Active(1),\n\t)\n\n\tat.Run()\n\n\tt.Log(co.Check())\n}\n\nfunc TestSilenceDelete(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1ms\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 150 * time.Millisecond,\n\t})\n\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\n\t// No repeat interval is configured. Thus, we receive an alert\n\t// notification every second.\n\tamc.Push(At(1), Alert(\"alertname\", \"test1\").Active(1))\n\tamc.Push(At(1), Alert(\"alertname\", \"test2\").Active(1))\n\n\t// Silence everything for a long time and delete the silence after\n\t// two iterations.\n\tsil := Silence(1.5, 100).MatchRE(\"alertname\", \".+\")\n\n\tamc.SetSilence(At(1.3), sil)\n\tamc.DelSilence(At(3.5), sil)\n\n\tco.Want(Between(3.5, 4.5),\n\t\tAlert(\"alertname\", \"test1\").Active(1),\n\t\tAlert(\"alertname\", \"test2\").Active(1),\n\t)\n\n\tat.Run()\n\n\tt.Log(co.Check())\n}\n\nfunc boolPtr(b bool) *bool {\n\treturn &b\n}\n\nfunc stringPtr(s string) *string {\n\treturn &s\n}\n\nfunc dateTimePtr(t strfmt.DateTime) *strfmt.DateTime {\n\treturn &t\n}\n"
  },
  {
    "path": "test/with_api_v2/acceptance/utf8_test.go",
    "content": "// Copyright 2023 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/client/alert\"\n\t\"github.com/prometheus/alertmanager/api/v2/client/alertgroup\"\n\t\"github.com/prometheus/alertmanager/api/v2/client/silence\"\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/featurecontrol\"\n\t. \"github.com/prometheus/alertmanager/test/with_api_v2\"\n)\n\nfunc TestAddUTF8Alerts(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  10m\n  repeat_interval: 1h\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 1 * time.Second,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\trequire.NoError(t, amc.Start())\n\tdefer amc.Terminate()\n\tam := amc.Members()[0]\n\n\t// Add an alert with UTF-8 labels.\n\tnow := time.Now()\n\tlabels := models.LabelSet{\n\t\t\"a\":                \"a\",\n\t\t\"00\":               \"b\",\n\t\t\"Σ\":                \"c\",\n\t\t\"\\xf0\\x9f\\x99\\x82\": \"dΘ\",\n\t}\n\tpa := &models.PostableAlert{\n\t\tStartsAt: strfmt.DateTime(now),\n\t\tEndsAt:   strfmt.DateTime(now.Add(5 * time.Minute)),\n\t\tAlert:    models.Alert{Labels: labels},\n\t}\n\tpostAlertParams := alert.NewPostAlertsParams()\n\tpostAlertParams.Alerts = models.PostableAlerts{pa}\n\t_, err := am.Client().Alert.PostAlerts(postAlertParams)\n\trequire.NoError(t, err)\n\n\t// Can get same alert from the API.\n\tresp, err := am.Client().Alert.GetAlerts(nil)\n\trequire.NoError(t, err)\n\trequire.Len(t, resp.Payload, 1)\n\trequire.Equal(t, labels, resp.Payload[0].Labels)\n\n\t// Can filter alerts on UTF-8 labels.\n\tgetAlertParams := alert.NewGetAlertsParams()\n\tgetAlertParams = getAlertParams.WithFilter([]string{\"00=b\", \"Σ=c\", \"\\\"\\\\xf0\\\\x9f\\\\x99\\\\x82\\\"=dΘ\"})\n\tresp, err = am.Client().Alert.GetAlerts(getAlertParams)\n\trequire.NoError(t, err)\n\trequire.Len(t, resp.Payload, 1)\n\trequire.Equal(t, labels, resp.Payload[0].Labels)\n\n\t// Can get same alert in alert group from the API.\n\talertGroupResp, err := am.Client().Alertgroup.GetAlertGroups(nil)\n\trequire.NoError(t, err)\n\trequire.Len(t, alertGroupResp.Payload, 1)\n\trequire.Len(t, alertGroupResp.Payload[0].Alerts, 1)\n\trequire.Equal(t, labels, alertGroupResp.Payload[0].Alerts[0].Labels)\n\n\t// Can filter alertGroups on UTF-8 labels.\n\tgetAlertGroupsParams := alertgroup.NewGetAlertGroupsParams()\n\tgetAlertGroupsParams.Filter = []string{\"00=b\", \"Σ=c\", \"\\\"\\\\xf0\\\\x9f\\\\x99\\\\x82\\\"=dΘ\"}\n\talertGroupResp, err = am.Client().Alertgroup.GetAlertGroups(getAlertGroupsParams)\n\trequire.NoError(t, err)\n\trequire.Len(t, alertGroupResp.Payload, 1)\n\trequire.Len(t, alertGroupResp.Payload[0].Alerts, 1)\n\trequire.Equal(t, labels, alertGroupResp.Payload[0].Alerts[0].Labels)\n}\n\nfunc TestCannotAddUTF8AlertsInClassicMode(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  10m\n  repeat_interval: 1h\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tFeatureFlags: []string{featurecontrol.FeatureClassicMode},\n\t\tTolerance:    1 * time.Second,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\trequire.NoError(t, amc.Start())\n\tdefer amc.Terminate()\n\tam := amc.Members()[0]\n\n\t// Cannot add an alert with UTF-8 labels.\n\tnow := time.Now()\n\tpa := &models.PostableAlert{\n\t\tStartsAt: strfmt.DateTime(now),\n\t\tEndsAt:   strfmt.DateTime(now.Add(5 * time.Minute)),\n\t\tAlert: models.Alert{\n\t\t\tLabels: models.LabelSet{\n\t\t\t\t\"a\":                \"a\",\n\t\t\t\t\"00\":               \"b\",\n\t\t\t\t\"Σ\":                \"c\",\n\t\t\t\t\"\\xf0\\x9f\\x99\\x82\": \"dΘ\",\n\t\t\t},\n\t\t},\n\t}\n\talertParams := alert.NewPostAlertsParams()\n\talertParams.Alerts = models.PostableAlerts{pa}\n\n\t_, err := am.Client().Alert.PostAlerts(alertParams)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"invalid label set\")\n}\n\nfunc TestAddUTF8Silences(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1ms\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 150 * time.Millisecond,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\trequire.NoError(t, amc.Start())\n\tdefer amc.Terminate()\n\tam := amc.Members()[0]\n\n\t// Add a silence with UTF-8 label matchers.\n\tnow := time.Now()\n\tmatchers := models.Matchers{{\n\t\tName:    stringPtr(\"fooΣ\"),\n\t\tIsEqual: boolPtr(true),\n\t\tIsRegex: boolPtr(false),\n\t\tValue:   stringPtr(\"bar🙂\"),\n\t}}\n\tps := models.PostableSilence{\n\t\tSilence: models.Silence{\n\t\t\tComment:   stringPtr(\"test\"),\n\t\t\tCreatedBy: stringPtr(\"test\"),\n\t\t\tMatchers:  matchers,\n\t\t\tStartsAt:  dateTimePtr(strfmt.DateTime(now)),\n\t\t\tEndsAt:    dateTimePtr(strfmt.DateTime(now.Add(24 * time.Hour))),\n\t\t},\n\t}\n\tpostSilenceParams := silence.NewPostSilencesParams()\n\tpostSilenceParams.Silence = &ps\n\t_, err := am.Client().Silence.PostSilences(postSilenceParams)\n\trequire.NoError(t, err)\n\n\t// Can get the same silence from the API.\n\tresp, err := am.Client().Silence.GetSilences(nil)\n\trequire.NoError(t, err)\n\trequire.Len(t, resp.Payload, 1)\n\trequire.Equal(t, matchers, resp.Payload[0].Matchers)\n\n\t// Can filter silences on UTF-8 label matchers.\n\tgetSilenceParams := silence.NewGetSilencesParams()\n\tgetSilenceParams = getSilenceParams.WithFilter([]string{\"fooΣ=bar🙂\"})\n\tresp, err = am.Client().Silence.GetSilences(getSilenceParams)\n\trequire.NoError(t, err)\n\trequire.Len(t, resp.Payload, 1)\n\trequire.Equal(t, matchers, resp.Payload[0].Matchers)\n}\n\nfunc TestCannotAddUTF8SilencesInClassicMode(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1ms\n\nreceivers:\n- name: \"default\"\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tFeatureFlags: []string{featurecontrol.FeatureClassicMode},\n\t\tTolerance:    150 * time.Millisecond,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\tamc := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\trequire.NoError(t, amc.Start())\n\tdefer amc.Terminate()\n\tam := amc.Members()[0]\n\n\t// Cannot create a silence with UTF-8 matchers.\n\tnow := time.Now()\n\tps := models.PostableSilence{\n\t\tSilence: models.Silence{\n\t\t\tComment:   stringPtr(\"test\"),\n\t\t\tCreatedBy: stringPtr(\"test\"),\n\t\t\tMatchers: models.Matchers{{\n\t\t\t\tName:    stringPtr(\"fooΣ\"),\n\t\t\t\tIsEqual: boolPtr(true),\n\t\t\t\tIsRegex: boolPtr(false),\n\t\t\t\tValue:   stringPtr(\"bar🙂\"),\n\t\t\t}},\n\t\t\tStartsAt: dateTimePtr(strfmt.DateTime(now)),\n\t\t\tEndsAt:   dateTimePtr(strfmt.DateTime(now.Add(24 * time.Hour))),\n\t\t},\n\t}\n\tsilenceParams := silence.NewPostSilencesParams()\n\tsilenceParams.Silence = &ps\n\n\t_, err := am.Client().Silence.PostSilences(silenceParams)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"invalid silence: invalid label matcher\")\n}\n\nfunc TestSendAlertsToUTF8Route(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: default\n  routes:\n    - receiver: webhook\n      matchers:\n        - foo🙂=bar\n      group_by:\n        - foo🙂\n      group_wait: 1s\nreceivers:\n- name: default\n- name: webhook\n  webhook_configs:\n  - url: 'http://%s'\n`\n\n\tat := NewAcceptanceTest(t, &AcceptanceOpts{\n\t\tTolerance: 150 * time.Millisecond,\n\t})\n\tco := at.Collector(\"webhook\")\n\twh := NewWebhook(t, co)\n\tam := at.AlertmanagerCluster(fmt.Sprintf(conf, wh.Address()), 1)\n\n\tam.Push(At(1), Alert(\"foo🙂\", \"bar\").Active(1))\n\tco.Want(Between(2, 2.5), Alert(\"foo🙂\", \"bar\").Active(1))\n\tat.Run()\n\tt.Log(co.Check())\n}\n"
  },
  {
    "path": "test/with_api_v2/acceptance/web_test.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage test\n\nimport (\n\t\"testing\"\n\n\ta \"github.com/prometheus/alertmanager/test/with_api_v2\"\n)\n\nfunc TestWebWithPrefix(t *testing.T) {\n\tt.Parallel()\n\n\tconf := `\nroute:\n  receiver: \"default\"\n  group_by: []\n  group_wait:      1s\n  group_interval:  1s\n  repeat_interval: 1h\n\nreceivers:\n- name: \"default\"\n`\n\n\t// The test framework polls the API with the given prefix during\n\t// Alertmanager startup and thereby ensures proper configuration.\n\tat := a.NewAcceptanceTest(t, &a.AcceptanceOpts{RoutePrefix: \"/foo\"})\n\tat.AlertmanagerCluster(conf, 1)\n\tat.Run()\n}\n"
  },
  {
    "path": "test/with_api_v2/acceptance.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/go-openapi/strfmt\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/client/alert\"\n\t\"github.com/prometheus/alertmanager/api/v2/client/silence\"\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\n\t\"github.com/prometheus/alertmanager/test/testutils\"\n)\n\n// Re-export common types and functions from testutils.\ntype (\n\tCollector      = testutils.Collector\n\tAcceptanceOpts = testutils.AcceptanceOpts\n)\n\nvar CompareCollectors = testutils.CompareCollectors\n\n// AcceptanceTest wraps testutils.AcceptanceTest for API-based testing.\ntype AcceptanceTest struct {\n\t*testutils.AcceptanceTest\n}\n\n// NewAcceptanceTest returns a new acceptance test.\nfunc NewAcceptanceTest(t *testing.T, opts *AcceptanceOpts) *AcceptanceTest {\n\treturn &AcceptanceTest{\n\t\tAcceptanceTest: testutils.NewAcceptanceTest(t, opts),\n\t}\n}\n\n// Alertmanager wraps testutils.Alertmanager and adds API-specific methods.\ntype Alertmanager struct {\n\t*testutils.Alertmanager\n}\n\n// AlertmanagerCluster wraps testutils.AlertmanagerCluster and adds API-specific methods.\ntype AlertmanagerCluster struct {\n\t*testutils.AlertmanagerCluster\n}\n\n// AlertmanagerCluster returns a new AlertmanagerCluster.\nfunc (t *AcceptanceTest) AlertmanagerCluster(conf string, size int) *AlertmanagerCluster {\n\treturn &AlertmanagerCluster{\n\t\tAlertmanagerCluster: t.AcceptanceTest.AlertmanagerCluster(conf, size),\n\t}\n}\n\n// Members returns the underlying Alertmanager instances wrapped for API testing.\nfunc (amc *AlertmanagerCluster) Members() []*Alertmanager {\n\tbaseMembers := amc.AlertmanagerCluster.Members()\n\twrapped := make([]*Alertmanager, len(baseMembers))\n\tfor i, am := range baseMembers {\n\t\twrapped[i] = &Alertmanager{Alertmanager: am}\n\t}\n\treturn wrapped\n}\n\n// Push declares alerts that are to be pushed to the Alertmanager\n// servers at a relative point in time.\nfunc (amc *AlertmanagerCluster) Push(at float64, alerts ...*TestAlert) {\n\tfor _, am := range amc.Members() {\n\t\tam.Push(at, alerts...)\n\t}\n}\n\n// Push declares alerts that are to be pushed to the Alertmanager\n// server at a relative point in time.\nfunc (am *Alertmanager) Push(at float64, alerts ...*TestAlert) {\n\tam.T.Do(at, func() {\n\t\tvar cas models.PostableAlerts\n\t\tfor i := range alerts {\n\t\t\ta := alerts[i].NativeAlert(am.Opts)\n\t\t\talert := &models.PostableAlert{\n\t\t\t\tAlert: models.Alert{\n\t\t\t\t\tLabels:       a.Labels,\n\t\t\t\t\tGeneratorURL: a.GeneratorURL,\n\t\t\t\t},\n\t\t\t\tAnnotations: a.Annotations,\n\t\t\t}\n\t\t\tif a.StartsAt != nil {\n\t\t\t\talert.StartsAt = *a.StartsAt\n\t\t\t}\n\t\t\tif a.EndsAt != nil {\n\t\t\t\talert.EndsAt = *a.EndsAt\n\t\t\t}\n\t\t\tcas = append(cas, alert)\n\t\t}\n\n\t\tparams := alert.PostAlertsParams{}\n\t\tparams.WithContext(context.Background()).WithAlerts(cas)\n\n\t\t_, err := am.Client().Alert.PostAlerts(&params)\n\t\tif err != nil {\n\t\t\tam.T.Errorf(\"Error pushing %v: %v\", cas, err)\n\t\t}\n\t})\n}\n\n// SetSilence updates or creates the given Silence.\nfunc (amc *AlertmanagerCluster) SetSilence(at float64, sil *TestSilence) {\n\tfor _, am := range amc.Members() {\n\t\tam.SetSilence(at, sil)\n\t}\n}\n\n// SetSilence updates or creates the given Silence.\nfunc (am *Alertmanager) SetSilence(at float64, sil *TestSilence) {\n\tam.T.Do(at, func() {\n\t\tresp, err := am.Client().Silence.PostSilences(\n\t\t\tsilence.NewPostSilencesParams().WithSilence(\n\t\t\t\t&models.PostableSilence{\n\t\t\t\t\tSilence: *sil.nativeSilence(am.Opts),\n\t\t\t\t},\n\t\t\t),\n\t\t)\n\t\tif err != nil {\n\t\t\tam.T.Errorf(\"Error setting silence %v: %s\", sil, err)\n\t\t\treturn\n\t\t}\n\t\tsil.SetID(resp.Payload.SilenceID)\n\t})\n}\n\n// DelSilence deletes the silence with the sid at the given time.\nfunc (amc *AlertmanagerCluster) DelSilence(at float64, sil *TestSilence) {\n\tfor _, am := range amc.Members() {\n\t\tam.DelSilence(at, sil)\n\t}\n}\n\n// DelSilence deletes the silence with the sid at the given time.\nfunc (am *Alertmanager) DelSilence(at float64, sil *TestSilence) {\n\tam.T.Do(at, func() {\n\t\t_, err := am.Client().Silence.DeleteSilence(\n\t\t\tsilence.NewDeleteSilenceParams().WithSilenceID(strfmt.UUID(sil.ID())),\n\t\t)\n\t\tif err != nil {\n\t\t\tam.T.Errorf(\"Error deleting silence %v: %s\", sil, err)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "test/with_api_v2/mock.go",
    "content": "// Copyright 2018 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage test\n\nimport (\n\t\"sync\"\n\n\t\"github.com/go-openapi/strfmt\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/test/testutils\"\n)\n\n// Re-export common types and functions from testutils.\ntype (\n\tInterval    = testutils.Interval\n\tTestAlert   = testutils.TestAlert\n\tMockWebhook = testutils.MockWebhook\n)\n\nvar (\n\tAt         = testutils.At\n\tBetween    = testutils.Between\n\tAlert      = testutils.Alert\n\tNewWebhook = testutils.NewWebhook\n)\n\n// TestSilence models a model.Silence with relative times.\ntype TestSilence struct {\n\tid               string\n\tmatch            []string\n\tmatchRE          []string\n\tstartsAt, endsAt float64\n\n\tmtx sync.RWMutex\n}\n\n// Silence creates a new TestSilence active for the relative interval given\n// by start and end.\nfunc Silence(start, end float64) *TestSilence {\n\treturn &TestSilence{\n\t\tstartsAt: start,\n\t\tendsAt:   end,\n\t}\n}\n\n// Match adds a new plain matcher to the silence.\nfunc (s *TestSilence) Match(v ...string) *TestSilence {\n\ts.match = append(s.match, v...)\n\treturn s\n}\n\n// MatchRE adds a new regex matcher to the silence.\nfunc (s *TestSilence) MatchRE(v ...string) *TestSilence {\n\tif len(v)%2 == 1 {\n\t\tpanic(\"bad key/values\")\n\t}\n\ts.matchRE = append(s.matchRE, v...)\n\treturn s\n}\n\n// SetID sets the silence ID.\nfunc (s *TestSilence) SetID(ID string) {\n\ts.mtx.Lock()\n\tdefer s.mtx.Unlock()\n\ts.id = ID\n}\n\n// ID gets the silence ID.\nfunc (s *TestSilence) ID() string {\n\ts.mtx.RLock()\n\tdefer s.mtx.RUnlock()\n\treturn s.id\n}\n\n// nativeSilence converts the declared test silence into a regular\n// silence with resolved times.\nfunc (s *TestSilence) nativeSilence(opts *AcceptanceOpts) *models.Silence {\n\tnsil := &models.Silence{}\n\n\tt := false\n\tfor i := 0; i < len(s.match); i += 2 {\n\t\tnsil.Matchers = append(nsil.Matchers, &models.Matcher{\n\t\t\tName:    &s.match[i],\n\t\t\tValue:   &s.match[i+1],\n\t\t\tIsRegex: &t,\n\t\t})\n\t}\n\tt = true\n\tfor i := 0; i < len(s.matchRE); i += 2 {\n\t\tnsil.Matchers = append(nsil.Matchers, &models.Matcher{\n\t\t\tName:    &s.matchRE[i],\n\t\t\tValue:   &s.matchRE[i+1],\n\t\t\tIsRegex: &t,\n\t\t})\n\t}\n\n\tif s.startsAt > 0 {\n\t\tstart := strfmt.DateTime(opts.ExpandTime(s.startsAt))\n\t\tnsil.StartsAt = &start\n\t}\n\tif s.endsAt > 0 {\n\t\tend := strfmt.DateTime(opts.ExpandTime(s.endsAt))\n\t\tnsil.EndsAt = &end\n\t}\n\tcomment := \"some comment\"\n\tcreatedBy := \"admin@example.com\"\n\tnsil.Comment = &comment\n\tnsil.CreatedBy = &createdBy\n\n\treturn nsil\n}\n"
  },
  {
    "path": "timeinterval/timeinterval.go",
    "content": "// Copyright 2020 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage timeinterval\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gopkg.in/yaml.v2\"\n)\n\n// Intervener determines whether a given time and active route time interval should mute outgoing notifications.\n// It implements the TimeMuter interface.\ntype Intervener struct {\n\tintervals map[string][]TimeInterval\n}\n\n// Mutes implements the TimeMuter interface.\nfunc (i *Intervener) Mutes(names []string, now time.Time) (bool, []string, error) {\n\tvar in []string\n\tfor _, name := range names {\n\t\tinterval, ok := i.intervals[name]\n\t\tif !ok {\n\t\t\treturn false, nil, fmt.Errorf(\"time interval %s doesn't exist in config\", name)\n\t\t}\n\n\t\tfor _, ti := range interval {\n\t\t\tif ti.ContainsTime(now.UTC()) {\n\t\t\t\tin = append(in, name)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn len(in) > 0, in, nil\n}\n\nfunc NewIntervener(ti map[string][]TimeInterval) *Intervener {\n\treturn &Intervener{\n\t\tintervals: ti,\n\t}\n}\n\n// TimeInterval describes intervals of time. ContainsTime will tell you if a golang time is contained\n// within the interval.\ntype TimeInterval struct {\n\tTimes       []TimeRange       `yaml:\"times,omitempty\" json:\"times,omitempty\"`\n\tWeekdays    []WeekdayRange    `yaml:\"weekdays,flow,omitempty\" json:\"weekdays,omitempty\"`\n\tDaysOfMonth []DayOfMonthRange `yaml:\"days_of_month,flow,omitempty\" json:\"days_of_month,omitempty\"`\n\tMonths      []MonthRange      `yaml:\"months,flow,omitempty\" json:\"months,omitempty\"`\n\tYears       []YearRange       `yaml:\"years,flow,omitempty\" json:\"years,omitempty\"`\n\tLocation    *Location         `yaml:\"location,flow,omitempty\" json:\"location,omitempty\"`\n}\n\n// TimeRange represents a range of minutes within a 1440 minute day, exclusive of the End minute. A day consists of 1440 minutes.\n// For example, 4:00PM to End of the day would Begin at 1020 and End at 1440.\ntype TimeRange struct {\n\tStartMinute int\n\tEndMinute   int\n}\n\n// InclusiveRange is used to hold the Beginning and End values of many time interval components.\ntype InclusiveRange struct {\n\tBegin int\n\tEnd   int\n}\n\n// A WeekdayRange is an inclusive range between [0, 6] where 0 = Sunday.\ntype WeekdayRange struct {\n\tInclusiveRange\n}\n\n// A DayOfMonthRange is an inclusive range that may have negative Beginning/End values that represent distance from the End of the month Beginning at -1.\ntype DayOfMonthRange struct {\n\tInclusiveRange\n}\n\n// A MonthRange is an inclusive range between [1, 12] where 1 = January.\ntype MonthRange struct {\n\tInclusiveRange\n}\n\n// A YearRange is a positive inclusive range.\ntype YearRange struct {\n\tInclusiveRange\n}\n\n// A Location is a container for a time.Location, used for custom unmarshalling/validation logic.\ntype Location struct {\n\t*time.Location\n}\n\ntype yamlTimeRange struct {\n\tStartTime string `yaml:\"start_time\" json:\"start_time\"`\n\tEndTime   string `yaml:\"end_time\" json:\"end_time\"`\n}\n\n// A range with a Beginning and End that can be represented as strings.\ntype stringableRange interface {\n\tsetBegin(int)\n\tsetEnd(int)\n\t// Try to map a member of the range into an integer.\n\tmemberFromString(string) (int, error)\n}\n\nfunc (ir *InclusiveRange) setBegin(n int) {\n\tir.Begin = n\n}\n\nfunc (ir *InclusiveRange) setEnd(n int) {\n\tir.End = n\n}\n\nfunc (ir *InclusiveRange) memberFromString(in string) (out int, err error) {\n\tout, err = strconv.Atoi(in)\n\tif err != nil {\n\t\treturn -1, err\n\t}\n\treturn out, nil\n}\n\nfunc (r *WeekdayRange) memberFromString(in string) (out int, err error) {\n\tout, ok := daysOfWeek[in]\n\tif !ok {\n\t\treturn -1, fmt.Errorf(\"%s is not a valid weekday\", in)\n\t}\n\treturn out, nil\n}\n\nfunc (r *MonthRange) memberFromString(in string) (out int, err error) {\n\tout, ok := months[in]\n\tif !ok {\n\t\tout, err = strconv.Atoi(in)\n\t\tif err != nil {\n\t\t\treturn -1, fmt.Errorf(\"%s is not a valid month\", in)\n\t\t}\n\t}\n\treturn out, nil\n}\n\nvar daysOfWeek = map[string]int{\n\t\"sunday\":    0,\n\t\"monday\":    1,\n\t\"tuesday\":   2,\n\t\"wednesday\": 3,\n\t\"thursday\":  4,\n\t\"friday\":    5,\n\t\"saturday\":  6,\n}\n\nvar daysOfWeekInv = map[int]string{\n\t0: \"sunday\",\n\t1: \"monday\",\n\t2: \"tuesday\",\n\t3: \"wednesday\",\n\t4: \"thursday\",\n\t5: \"friday\",\n\t6: \"saturday\",\n}\n\nvar months = map[string]int{\n\t\"january\":   1,\n\t\"february\":  2,\n\t\"march\":     3,\n\t\"april\":     4,\n\t\"may\":       5,\n\t\"june\":      6,\n\t\"july\":      7,\n\t\"august\":    8,\n\t\"september\": 9,\n\t\"october\":   10,\n\t\"november\":  11,\n\t\"december\":  12,\n}\n\nvar monthsInv = map[int]string{\n\t1:  \"january\",\n\t2:  \"february\",\n\t3:  \"march\",\n\t4:  \"april\",\n\t5:  \"may\",\n\t6:  \"june\",\n\t7:  \"july\",\n\t8:  \"august\",\n\t9:  \"september\",\n\t10: \"october\",\n\t11: \"november\",\n\t12: \"december\",\n}\n\n// UnmarshalYAML implements the Unmarshaller interface for Location.\nfunc (tz *Location) UnmarshalYAML(unmarshal func(any) error) error {\n\tvar str string\n\tif err := unmarshal(&str); err != nil {\n\t\treturn err\n\t}\n\n\tloc, err := time.LoadLocation(str)\n\tif err != nil {\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tif zoneinfo := os.Getenv(\"ZONEINFO\"); zoneinfo != \"\" {\n\t\t\t\treturn fmt.Errorf(\"%w (ZONEINFO=%q)\", err, zoneinfo)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"%w (on Windows platforms, you may have to pass the time zone database using the ZONEINFO environment variable, see https://pkg.go.dev/time#LoadLocation for details)\", err)\n\t\t}\n\t\treturn err\n\t}\n\n\t*tz = Location{loc}\n\treturn nil\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface for Location.\n// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.\nfunc (tz *Location) UnmarshalJSON(in []byte) error {\n\treturn yaml.Unmarshal(in, tz)\n}\n\n// UnmarshalYAML implements the Unmarshaller interface for WeekdayRange.\nfunc (r *WeekdayRange) UnmarshalYAML(unmarshal func(any) error) error {\n\tvar str string\n\tif err := unmarshal(&str); err != nil {\n\t\treturn err\n\t}\n\tif err := stringableRangeFromString(str, r); err != nil {\n\t\treturn err\n\t}\n\tif r.Begin > r.End {\n\t\treturn errors.New(\"start day cannot be before end day\")\n\t}\n\tif r.Begin < 0 || r.Begin > 6 {\n\t\treturn fmt.Errorf(\"%s is not a valid day of the week: out of range\", str)\n\t}\n\tif r.End < 0 || r.End > 6 {\n\t\treturn fmt.Errorf(\"%s is not a valid day of the week: out of range\", str)\n\t}\n\treturn nil\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface for WeekdayRange.\n// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.\nfunc (r *WeekdayRange) UnmarshalJSON(in []byte) error {\n\treturn yaml.Unmarshal(in, r)\n}\n\n// UnmarshalYAML implements the Unmarshaller interface for DayOfMonthRange.\nfunc (r *DayOfMonthRange) UnmarshalYAML(unmarshal func(any) error) error {\n\tvar str string\n\tif err := unmarshal(&str); err != nil {\n\t\treturn err\n\t}\n\tif err := stringableRangeFromString(str, r); err != nil {\n\t\treturn err\n\t}\n\t// Check beginning <= end accounting for negatives day of month indices as well.\n\t// Months != 31 days can't be addressed here and are clamped, but at least we can catch blatant errors.\n\tif r.Begin == 0 || r.Begin < -31 || r.Begin > 31 {\n\t\treturn fmt.Errorf(\"%d is not a valid day of the month: out of range\", r.Begin)\n\t}\n\tif r.End == 0 || r.End < -31 || r.End > 31 {\n\t\treturn fmt.Errorf(\"%d is not a valid day of the month: out of range\", r.End)\n\t}\n\t// Restricting here prevents errors where begin > end in longer months but not shorter months.\n\tif r.Begin < 0 && r.End > 0 {\n\t\treturn fmt.Errorf(\"end day must be negative if start day is negative\")\n\t}\n\t// Check begin <= end. We can't know this for sure when using negative indices\n\t// but we can prevent cases where its always invalid (using 28 day minimum length).\n\tcheckBegin := r.Begin\n\tcheckEnd := r.End\n\tif r.Begin < 0 {\n\t\tcheckBegin = 28 + r.Begin\n\t}\n\tif r.End < 0 {\n\t\tcheckEnd = 28 + r.End\n\t}\n\tif checkBegin > checkEnd {\n\t\treturn fmt.Errorf(\"end day %d is always before start day %d\", r.End, r.Begin)\n\t}\n\treturn nil\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface for DayOfMonthRange.\n// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.\nfunc (r *DayOfMonthRange) UnmarshalJSON(in []byte) error {\n\treturn yaml.Unmarshal(in, r)\n}\n\n// UnmarshalYAML implements the Unmarshaller interface for MonthRange.\nfunc (r *MonthRange) UnmarshalYAML(unmarshal func(any) error) error {\n\tvar str string\n\tif err := unmarshal(&str); err != nil {\n\t\treturn err\n\t}\n\tif err := stringableRangeFromString(str, r); err != nil {\n\t\treturn err\n\t}\n\tif r.Begin > r.End {\n\t\tbegin := monthsInv[r.Begin]\n\t\tend := monthsInv[r.End]\n\t\treturn fmt.Errorf(\"end month %s is before start month %s\", end, begin)\n\t}\n\treturn nil\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface for MonthRange.\n// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.\nfunc (r *MonthRange) UnmarshalJSON(in []byte) error {\n\treturn yaml.Unmarshal(in, r)\n}\n\n// UnmarshalYAML implements the Unmarshaller interface for YearRange.\nfunc (r *YearRange) UnmarshalYAML(unmarshal func(any) error) error {\n\tvar str string\n\tif err := unmarshal(&str); err != nil {\n\t\treturn err\n\t}\n\tif err := stringableRangeFromString(str, r); err != nil {\n\t\treturn err\n\t}\n\tif r.Begin > r.End {\n\t\treturn fmt.Errorf(\"end year %d is before start year %d\", r.End, r.Begin)\n\t}\n\treturn nil\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface for YearRange.\n// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.\nfunc (r *YearRange) UnmarshalJSON(in []byte) error {\n\treturn yaml.Unmarshal(in, r)\n}\n\n// UnmarshalYAML implements the Unmarshaller interface for TimeRanges.\nfunc (tr *TimeRange) UnmarshalYAML(unmarshal func(any) error) error {\n\tvar y yamlTimeRange\n\tif err := unmarshal(&y); err != nil {\n\t\treturn err\n\t}\n\tif y.EndTime == \"\" || y.StartTime == \"\" {\n\t\treturn errors.New(\"both start and end times must be provided\")\n\t}\n\tstart, err := parseTime(y.StartTime)\n\tif err != nil {\n\t\treturn err\n\t}\n\tend, err := parseTime(y.EndTime)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif start >= end {\n\t\treturn errors.New(\"start time cannot be equal or greater than end time\")\n\t}\n\ttr.StartMinute, tr.EndMinute = start, end\n\treturn nil\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface for Timerange.\n// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.\nfunc (tr *TimeRange) UnmarshalJSON(in []byte) error {\n\treturn yaml.Unmarshal(in, tr)\n}\n\n// MarshalYAML implements the yaml.Marshaler interface for WeekdayRange.\nfunc (r WeekdayRange) MarshalYAML() (any, error) {\n\tbytes, err := r.MarshalText()\n\treturn string(bytes), err\n}\n\n// MarshalText implements the econding.TextMarshaler interface for WeekdayRange.\n// It converts the range into a colon-separated string, or a single weekday if possible.\n// E.g. \"monday:friday\" or \"saturday\".\nfunc (r WeekdayRange) MarshalText() ([]byte, error) {\n\tbeginStr, ok := daysOfWeekInv[r.Begin]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unable to convert %d into weekday string\", r.Begin)\n\t}\n\tif r.Begin == r.End {\n\t\treturn []byte(beginStr), nil\n\t}\n\tendStr, ok := daysOfWeekInv[r.End]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unable to convert %d into weekday string\", r.End)\n\t}\n\trangeStr := fmt.Sprintf(\"%s:%s\", beginStr, endStr)\n\treturn []byte(rangeStr), nil\n}\n\n// MarshalYAML implements the yaml.Marshaler interface for TimeRange.\nfunc (tr TimeRange) MarshalYAML() (out any, err error) {\n\tstartHr := tr.StartMinute / 60\n\tendHr := tr.EndMinute / 60\n\tstartMin := tr.StartMinute % 60\n\tendMin := tr.EndMinute % 60\n\n\tstartStr := fmt.Sprintf(\"%02d:%02d\", startHr, startMin)\n\tendStr := fmt.Sprintf(\"%02d:%02d\", endHr, endMin)\n\n\tyTr := yamlTimeRange{startStr, endStr}\n\treturn any(yTr), err\n}\n\n// MarshalJSON implements the json.Marshaler interface for TimeRange.\nfunc (tr TimeRange) MarshalJSON() (out []byte, err error) {\n\tstartHr := tr.StartMinute / 60\n\tendHr := tr.EndMinute / 60\n\tstartMin := tr.StartMinute % 60\n\tendMin := tr.EndMinute % 60\n\n\tstartStr := fmt.Sprintf(\"%02d:%02d\", startHr, startMin)\n\tendStr := fmt.Sprintf(\"%02d:%02d\", endHr, endMin)\n\n\tyTr := yamlTimeRange{startStr, endStr}\n\treturn json.Marshal(yTr)\n}\n\n// MarshalText implements the econding.TextMarshaler interface for Location.\n// It marshals a Location back into a string that represents a time.Location.\nfunc (tz Location) MarshalText() ([]byte, error) {\n\tif tz.Location == nil {\n\t\treturn nil, fmt.Errorf(\"unable to convert nil location into string\")\n\t}\n\treturn []byte(tz.String()), nil\n}\n\n// MarshalYAML implements the yaml.Marshaler interface for Location.\nfunc (tz Location) MarshalYAML() (any, error) {\n\tbytes, err := tz.MarshalText()\n\treturn string(bytes), err\n}\n\n// MarshalJSON implements the json.Marshaler interface for Location.\nfunc (tz Location) MarshalJSON() (out []byte, err error) {\n\treturn json.Marshal(tz.String())\n}\n\n// MarshalText implements the encoding.TextMarshaler interface for InclusiveRange.\n// It converts the struct into a colon-separated string, or a single element if\n// appropriate. E.g. \"monday:friday\" or \"monday\".\nfunc (ir InclusiveRange) MarshalText() ([]byte, error) {\n\tif ir.Begin == ir.End {\n\t\treturn []byte(strconv.Itoa(ir.Begin)), nil\n\t}\n\tout := fmt.Sprintf(\"%d:%d\", ir.Begin, ir.End)\n\treturn []byte(out), nil\n}\n\n// MarshalYAML implements the yaml.Marshaler interface for InclusiveRange.\nfunc (ir InclusiveRange) MarshalYAML() (any, error) {\n\tbytes, err := ir.MarshalText()\n\treturn string(bytes), err\n}\n\nvar (\n\tvalidTime   = \"^((([01][0-9])|(2[0-3])):[0-5][0-9])$|(^24:00$)\"\n\tvalidTimeRE = regexp.MustCompile(validTime)\n)\n\n// Given a time, determines the number of days in the month that time occurs in.\nfunc daysInMonth(t time.Time) int {\n\tmonthStart := time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())\n\tmonthEnd := monthStart.AddDate(0, 1, 0)\n\tdiff := monthEnd.Sub(monthStart)\n\treturn int(diff.Hours() / 24)\n}\n\nfunc clamp(n, min, max int) int {\n\tif n <= min {\n\t\treturn min\n\t}\n\tif n >= max {\n\t\treturn max\n\t}\n\treturn n\n}\n\n// ContainsTime returns true if the TimeInterval contains the given time, otherwise returns false.\nfunc (tp TimeInterval) ContainsTime(t time.Time) bool {\n\tif tp.Location != nil {\n\t\tt = t.In(tp.Location.Location)\n\t}\n\tif tp.Times != nil {\n\t\tin := false\n\t\tfor _, validMinutes := range tp.Times {\n\t\t\tif (t.Hour()*60+t.Minute()) >= validMinutes.StartMinute && (t.Hour()*60+t.Minute()) < validMinutes.EndMinute {\n\t\t\t\tin = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !in {\n\t\t\treturn false\n\t\t}\n\t}\n\tif tp.DaysOfMonth != nil {\n\t\tin := false\n\t\tfor _, validDates := range tp.DaysOfMonth {\n\t\t\tvar begin, end int\n\t\t\tdaysInMonth := daysInMonth(t)\n\t\t\tif validDates.Begin < 0 {\n\t\t\t\tbegin = daysInMonth + validDates.Begin + 1\n\t\t\t} else {\n\t\t\t\tbegin = validDates.Begin\n\t\t\t}\n\t\t\tif validDates.End < 0 {\n\t\t\t\tend = daysInMonth + validDates.End + 1\n\t\t\t} else {\n\t\t\t\tend = validDates.End\n\t\t\t}\n\t\t\t// Skip clamping if the beginning date is after the end of the month.\n\t\t\tif begin > daysInMonth {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Clamp to the boundaries of the month to prevent crossing into other months.\n\t\t\tbegin = clamp(begin, -1*daysInMonth, daysInMonth)\n\t\t\tend = clamp(end, -1*daysInMonth, daysInMonth)\n\t\t\tif t.Day() >= begin && t.Day() <= end {\n\t\t\t\tin = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !in {\n\t\t\treturn false\n\t\t}\n\t}\n\tif tp.Months != nil {\n\t\tin := false\n\t\tfor _, validMonths := range tp.Months {\n\t\t\tif t.Month() >= time.Month(validMonths.Begin) && t.Month() <= time.Month(validMonths.End) {\n\t\t\t\tin = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !in {\n\t\t\treturn false\n\t\t}\n\t}\n\tif tp.Weekdays != nil {\n\t\tin := false\n\t\tfor _, validDays := range tp.Weekdays {\n\t\t\tif t.Weekday() >= time.Weekday(validDays.Begin) && t.Weekday() <= time.Weekday(validDays.End) {\n\t\t\t\tin = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !in {\n\t\t\treturn false\n\t\t}\n\t}\n\tif tp.Years != nil {\n\t\tin := false\n\t\tfor _, validYears := range tp.Years {\n\t\t\tif t.Year() >= validYears.Begin && t.Year() <= validYears.End {\n\t\t\t\tin = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !in {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// Converts a string of the form \"HH:MM\" into the number of minutes elapsed in the day.\nfunc parseTime(in string) (mins int, err error) {\n\tif !validTimeRE.MatchString(in) {\n\t\treturn 0, fmt.Errorf(\"couldn't parse timestamp %s, invalid format\", in)\n\t}\n\ttimestampComponents := strings.Split(in, \":\")\n\tif len(timestampComponents) != 2 {\n\t\treturn 0, fmt.Errorf(\"invalid timestamp format: %s\", in)\n\t}\n\ttimeStampHours, err := strconv.Atoi(timestampComponents[0])\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\ttimeStampMinutes, err := strconv.Atoi(timestampComponents[1])\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tif timeStampHours < 0 || timeStampHours > 24 || timeStampMinutes < 0 || timeStampMinutes > 60 {\n\t\treturn 0, fmt.Errorf(\"timestamp %s out of range\", in)\n\t}\n\t// Timestamps are stored as minutes elapsed in the day, so multiply hours by 60.\n\tmins = timeStampHours*60 + timeStampMinutes\n\treturn mins, nil\n}\n\n// Converts a range that can be represented as strings (e.g. monday:wednesday) into an equivalent integer-represented range.\nfunc stringableRangeFromString(in string, r stringableRange) (err error) {\n\tin = strings.ToLower(in)\n\tif strings.ContainsRune(in, ':') {\n\t\tcomponents := strings.Split(in, \":\")\n\t\tif len(components) != 2 {\n\t\t\treturn fmt.Errorf(\"couldn't parse range %s, invalid format\", in)\n\t\t}\n\t\tstart, err := r.memberFromString(components[0])\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tEnd, err := r.memberFromString(components[1])\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tr.setBegin(start)\n\t\tr.setEnd(End)\n\t\treturn nil\n\t}\n\tval, err := r.memberFromString(in)\n\tif err != nil {\n\t\treturn err\n\t}\n\tr.setBegin(val)\n\tr.setEnd(val)\n\treturn nil\n}\n"
  },
  {
    "path": "timeinterval/timeinterval_test.go",
    "content": "// Copyright 2020 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage timeinterval\n\nimport (\n\t\"encoding/json\"\n\t\"reflect\"\n\t\"sort\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"gopkg.in/yaml.v2\"\n)\n\nvar timeIntervalTestCases = []struct {\n\tvalidTimeStrings   []string\n\tinvalidTimeStrings []string\n\ttimeInterval       TimeInterval\n}{\n\t{\n\t\ttimeInterval: TimeInterval{},\n\t\tvalidTimeStrings: []string{\n\t\t\t\"02 Jan 06 15:04 +0000\",\n\t\t\t\"03 Jan 07 10:04 +0000\",\n\t\t\t\"04 Jan 06 09:04 +0000\",\n\t\t},\n\t\tinvalidTimeStrings: []string{},\n\t},\n\t{\n\t\t// 9am to 5pm, monday to friday\n\t\ttimeInterval: TimeInterval{\n\t\t\tTimes:    []TimeRange{{StartMinute: 540, EndMinute: 1020}},\n\t\t\tWeekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}},\n\t\t},\n\t\tvalidTimeStrings: []string{\n\t\t\t\"04 May 20 15:04 +0000\",\n\t\t\t\"05 May 20 10:04 +0000\",\n\t\t\t\"09 Jun 20 09:04 +0000\",\n\t\t},\n\t\tinvalidTimeStrings: []string{\n\t\t\t\"03 May 20 15:04 +0000\",\n\t\t\t\"04 May 20 08:59 +0000\",\n\t\t\t\"05 May 20 05:00 +0000\",\n\t\t},\n\t},\n\t{\n\t\t// Easter 2020\n\t\ttimeInterval: TimeInterval{\n\t\t\tDaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: 4, End: 6}}},\n\t\t\tMonths:      []MonthRange{{InclusiveRange{Begin: 4, End: 4}}},\n\t\t\tYears:       []YearRange{{InclusiveRange{Begin: 2020, End: 2020}}},\n\t\t},\n\t\tvalidTimeStrings: []string{\n\t\t\t\"04 Apr 20 15:04 +0000\",\n\t\t\t\"05 Apr 20 00:00 +0000\",\n\t\t\t\"06 Apr 20 23:05 +0000\",\n\t\t},\n\t\tinvalidTimeStrings: []string{\n\t\t\t\"03 May 18 15:04 +0000\",\n\t\t\t\"03 Apr 20 23:59 +0000\",\n\t\t\t\"04 Jun 20 23:59 +0000\",\n\t\t\t\"06 Apr 19 23:59 +0000\",\n\t\t\t\"07 Apr 20 00:00 +0000\",\n\t\t},\n\t},\n\t{\n\t\t// Check negative days of month, last 3 days of each month\n\t\ttimeInterval: TimeInterval{\n\t\t\tDaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: -3, End: -1}}},\n\t\t},\n\t\tvalidTimeStrings: []string{\n\t\t\t\"31 Jan 20 15:04 +0000\",\n\t\t\t\"30 Jan 20 15:04 +0000\",\n\t\t\t\"29 Jan 20 15:04 +0000\",\n\t\t\t\"30 Jun 20 00:00 +0000\",\n\t\t\t\"29 Feb 20 23:05 +0000\",\n\t\t},\n\t\tinvalidTimeStrings: []string{\n\t\t\t\"03 May 18 15:04 +0000\",\n\t\t\t\"27 Jan 20 15:04 +0000\",\n\t\t\t\"03 Apr 20 23:59 +0000\",\n\t\t\t\"04 Jun 20 23:59 +0000\",\n\t\t\t\"06 Apr 19 23:59 +0000\",\n\t\t\t\"07 Apr 20 00:00 +0000\",\n\t\t\t\"01 Mar 20 00:00 +0000\",\n\t\t},\n\t},\n\t{\n\t\t// Check out of bound days are clamped to month boundaries\n\t\ttimeInterval: TimeInterval{\n\t\t\tMonths:      []MonthRange{{InclusiveRange{Begin: 6, End: 6}}},\n\t\t\tDaysOfMonth: []DayOfMonthRange{{InclusiveRange{Begin: -31, End: 31}}},\n\t\t},\n\t\tvalidTimeStrings: []string{\n\t\t\t\"30 Jun 20 00:00 +0000\",\n\t\t\t\"01 Jun 20 00:00 +0000\",\n\t\t},\n\t\tinvalidTimeStrings: []string{\n\t\t\t\"31 May 20 00:00 +0000\",\n\t\t\t\"1 Jul 20 00:00 +0000\",\n\t\t},\n\t},\n\t{\n\t\t// Check alternative timezones can be used to compare times.\n\t\t// AEST 9AM to 5PM, Monday to Friday.\n\t\ttimeInterval: TimeInterval{\n\t\t\tTimes:    []TimeRange{{StartMinute: 540, EndMinute: 1020}},\n\t\t\tWeekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}},\n\t\t\tLocation: &Location{mustLoadLocation(\"Australia/Sydney\")},\n\t\t},\n\t\tvalidTimeStrings: []string{\n\t\t\t\"06 Apr 21 13:00 +1000\",\n\t\t},\n\t\tinvalidTimeStrings: []string{\n\t\t\t\"06 Apr 21 13:00 +0000\",\n\t\t},\n\t},\n\t{\n\t\t// Check an alternative timezone during daylight savings time.\n\t\ttimeInterval: TimeInterval{\n\t\t\tTimes:    []TimeRange{{StartMinute: 540, EndMinute: 1020}},\n\t\t\tWeekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}},\n\t\t\tMonths:   []MonthRange{{InclusiveRange{Begin: 11, End: 11}}},\n\t\t\tLocation: &Location{mustLoadLocation(\"Australia/Sydney\")},\n\t\t},\n\t\tvalidTimeStrings: []string{\n\t\t\t\"01 Nov 21 09:00 +1100\",\n\t\t\t\"31 Oct 21 22:00 +0000\",\n\t\t},\n\t\tinvalidTimeStrings: []string{\n\t\t\t\"31 Oct 21 21:00 +0000\",\n\t\t},\n\t},\n}\n\nvar timeStringTestCases = []struct {\n\ttimeString  string\n\tTimeRange   TimeRange\n\texpectError bool\n}{\n\t{\n\t\ttimeString:  \"{'start_time': '00:00', 'end_time': '24:00'}\",\n\t\tTimeRange:   TimeRange{StartMinute: 0, EndMinute: 1440},\n\t\texpectError: false,\n\t},\n\t{\n\t\ttimeString:  \"{'start_time': '01:35', 'end_time': '17:39'}\",\n\t\tTimeRange:   TimeRange{StartMinute: 95, EndMinute: 1059},\n\t\texpectError: false,\n\t},\n\t{\n\t\ttimeString:  \"{'start_time': '09:35', 'end_time': '09:39'}\",\n\t\tTimeRange:   TimeRange{StartMinute: 575, EndMinute: 579},\n\t\texpectError: false,\n\t},\n\t{\n\t\t// Error: Begin and End times are the same\n\t\ttimeString:  \"{'start_time': '17:31', 'end_time': '17:31'}\",\n\t\tTimeRange:   TimeRange{},\n\t\texpectError: true,\n\t},\n\t{\n\t\t// Error: End time out of range\n\t\ttimeString:  \"{'start_time': '12:30', 'end_time': '24:01'}\",\n\t\tTimeRange:   TimeRange{},\n\t\texpectError: true,\n\t},\n\t{\n\t\t// Error: Start time greater than End time\n\t\ttimeString:  \"{'start_time': '09:30', 'end_time': '07:41'}\",\n\t\tTimeRange:   TimeRange{},\n\t\texpectError: true,\n\t},\n\t{\n\t\t// Error: Start time out of range and greater than End time\n\t\ttimeString:  \"{'start_time': '24:00', 'end_time': '17:41'}\",\n\t\tTimeRange:   TimeRange{},\n\t\texpectError: true,\n\t},\n\t{\n\t\t// Error: No range specified\n\t\ttimeString:  \"{'start_time': '14:03'}\",\n\t\tTimeRange:   TimeRange{},\n\t\texpectError: true,\n\t},\n}\n\nvar yamlUnmarshalTestCases = []struct {\n\tin          string\n\tintervals   []TimeInterval\n\tcontains    []string\n\texcludes    []string\n\texpectError bool\n\terr         string\n}{\n\t{\n\t\t// Simple business hours test\n\t\tin: `\n---\n- weekdays: ['monday:friday']\n  times:\n    - start_time: '09:00'\n      end_time: '17:00'\n`,\n\t\tintervals: []TimeInterval{\n\t\t\t{\n\t\t\t\tWeekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}},\n\t\t\t\tTimes:    []TimeRange{{StartMinute: 540, EndMinute: 1020}},\n\t\t\t},\n\t\t},\n\t\tcontains: []string{\n\t\t\t\"08 Jul 20 09:00 +0000\",\n\t\t\t\"08 Jul 20 16:59 +0000\",\n\t\t},\n\t\texcludes: []string{\n\t\t\t\"08 Jul 20 05:00 +0000\",\n\t\t\t\"08 Jul 20 08:59 +0000\",\n\t\t},\n\t\texpectError: false,\n\t},\n\t{\n\t\t// More advanced test with negative indices and ranges\n\t\tin: `\n---\n  # Last week, excluding Saturday, of the first quarter of the year during business hours from 2020 to 2025 and 2030-2035\n- weekdays: ['monday:friday', 'sunday']\n  months: ['january:march']\n  days_of_month: ['-7:-1']\n  years: ['2020:2025', '2030:2035']\n  times:\n    - start_time: '09:00'\n      end_time: '17:00'\n`,\n\t\tintervals: []TimeInterval{\n\t\t\t{\n\t\t\t\tWeekdays:    []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}, {InclusiveRange{Begin: 0, End: 0}}},\n\t\t\t\tTimes:       []TimeRange{{StartMinute: 540, EndMinute: 1020}},\n\t\t\t\tMonths:      []MonthRange{{InclusiveRange{1, 3}}},\n\t\t\t\tDaysOfMonth: []DayOfMonthRange{{InclusiveRange{-7, -1}}},\n\t\t\t\tYears:       []YearRange{{InclusiveRange{2020, 2025}}, {InclusiveRange{2030, 2035}}},\n\t\t\t},\n\t\t},\n\t\tcontains: []string{\n\t\t\t\"27 Jan 21 09:00 +0000\",\n\t\t\t\"28 Jan 21 16:59 +0000\",\n\t\t\t\"29 Jan 21 13:00 +0000\",\n\t\t\t\"31 Mar 25 13:00 +0000\",\n\t\t\t\"31 Mar 25 13:00 +0000\",\n\t\t\t\"31 Jan 35 13:00 +0000\",\n\t\t},\n\t\texcludes: []string{\n\t\t\t\"30 Jan 21 13:00 +0000\", // Saturday\n\t\t\t\"01 Apr 21 13:00 +0000\", // 4th month\n\t\t\t\"30 Jan 26 13:00 +0000\", // 2026\n\t\t\t\"31 Jan 35 17:01 +0000\", // After 5pm\n\t\t},\n\t\texpectError: false,\n\t},\n\t{\n\t\tin: `\n---\n- weekdays: ['monday:friday']\n  times:\n    - start_time: '09:00'\n      end_time: '17:00'`,\n\t\tintervals: []TimeInterval{\n\t\t\t{\n\t\t\t\tWeekdays: []WeekdayRange{{InclusiveRange{Begin: 1, End: 5}}},\n\t\t\t\tTimes:    []TimeRange{{StartMinute: 540, EndMinute: 1020}},\n\t\t\t},\n\t\t},\n\t\tcontains: []string{\n\t\t\t\"01 Apr 21 13:00 +0000\",\n\t\t},\n\t},\n\t{\n\t\t// Invalid start time.\n\t\tin: `\n---\n- times:\n    - start_time: '01:99'\n      end_time: '23:59'`,\n\t\texpectError: true,\n\t\terr:         \"couldn't parse timestamp 01:99, invalid format\",\n\t},\n\t{\n\t\t// Invalid end time.\n\t\tin: `\n---\n- times:\n    - start_time: '00:00'\n      end_time: '99:99'`,\n\t\texpectError: true,\n\t\terr:         \"couldn't parse timestamp 99:99, invalid format\",\n\t},\n\t{\n\t\t// Start day before end day.\n\t\tin: `\n---\n- weekdays: ['friday:monday']`,\n\t\texpectError: true,\n\t\terr:         \"start day cannot be before end day\",\n\t},\n\t{\n\t\t// Invalid weekdays.\n\t\tin: `\n---\n- weekdays: ['blurgsday:flurgsday']\n`,\n\t\texpectError: true,\n\t\terr:         \"blurgsday is not a valid weekday\",\n\t},\n\t{\n\t\t// Numeric weekdays aren't allowed.\n\t\tin: `\n---\n- weekdays: ['1:3']\n`,\n\t\texpectError: true,\n\t\terr:         \"1 is not a valid weekday\",\n\t},\n\t{\n\t\t// Negative numeric weekdays aren't allowed.\n\t\tin: `\n---\n- weekdays: ['-2:-1']\n`,\n\t\texpectError: true,\n\t\terr:         \"-2 is not a valid weekday\",\n\t},\n\t{\n\t\t// 0 day of month.\n\t\tin: `\n---\n- days_of_month: ['0']\n`,\n\t\texpectError: true,\n\t\terr:         \"0 is not a valid day of the month: out of range\",\n\t},\n\t{\n\t\t// Start day of month < 0.\n\t\tin: `\n---\n- days_of_month: ['-50:-20']\n`,\n\t\texpectError: true,\n\t\terr:         \"-50 is not a valid day of the month: out of range\",\n\t},\n\t{\n\t\t// End day of month > 31.\n\t\tin: `\n---\n- days_of_month: ['1:50']\n`,\n\t\texpectError: true,\n\t\terr:         \"50 is not a valid day of the month: out of range\",\n\t},\n\t{\n\t\t// Negative indices should work.\n\t\tin: `\n---\n- days_of_month: ['1:-1']\n`,\n\t\tintervals: []TimeInterval{\n\t\t\t{\n\t\t\t\tDaysOfMonth: []DayOfMonthRange{{InclusiveRange{1, -1}}},\n\t\t\t},\n\t\t},\n\t\texpectError: false,\n\t},\n\t{\n\t\t// End day must be negative if begin day is negative.\n\t\tin: `\n---\n- days_of_month: ['-15:5']\n`,\n\t\texpectError: true,\n\t\terr:         \"end day must be negative if start day is negative\",\n\t},\n\t{\n\t\t// Negative end date before positive positive start date.\n\t\tin: `\n---\n- days_of_month: ['10:-25']\n`,\n\t\texpectError: true,\n\t\terr:         \"end day -25 is always before start day 10\",\n\t},\n\t{\n\t\t// Months should work regardless of case\n\t\tin: `\n---\n- months: ['January:december']\n`,\n\t\texpectError: false,\n\t\tintervals: []TimeInterval{\n\t\t\t{\n\t\t\t\tMonths: []MonthRange{{InclusiveRange{1, 12}}},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\t// Time zones may be specified by location.\n\t\tin: `\n---\n- years: ['2020:2022']\n  location: 'Australia/Sydney'\n`,\n\t\texpectError: false,\n\t\tintervals: []TimeInterval{\n\t\t\t{\n\t\t\t\tYears:    []YearRange{{InclusiveRange{2020, 2022}}},\n\t\t\t\tLocation: &Location{mustLoadLocation(\"Australia/Sydney\")},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\t// Invalid start month.\n\t\tin: `\n---\n- months: ['martius:june']\n`,\n\t\texpectError: true,\n\t\terr:         \"martius is not a valid month\",\n\t},\n\t{\n\t\t// Invalid end month.\n\t\tin: `\n---\n- months: ['march:junius']\n`,\n\t\texpectError: true,\n\t\terr:         \"junius is not a valid month\",\n\t},\n\t{\n\t\t// Start month after end month.\n\t\tin: `\n---\n- months: ['december:january']\n`,\n\t\texpectError: true,\n\t\terr:         \"end month january is before start month december\",\n\t},\n\t{\n\t\t// Start year after end year.\n\t\tin: `\n---\n- years: ['2022:2020']\n`,\n\t\texpectError: true,\n\t\terr:         \"end year 2020 is before start year 2022\",\n\t},\n}\n\nfunc TestYamlUnmarshal(t *testing.T) {\n\tfor _, tc := range yamlUnmarshalTestCases {\n\t\tvar ti []TimeInterval\n\t\terr := yaml.Unmarshal([]byte(tc.in), &ti)\n\t\tif err != nil && !tc.expectError {\n\t\t\tt.Errorf(\"Received unexpected error: %v when parsing %v\", err, tc.in)\n\t\t} else if err == nil && tc.expectError {\n\t\t\tt.Errorf(\"Expected error when unmarshalling %s but didn't receive one\", tc.in)\n\t\t} else if err != nil && tc.expectError {\n\t\t\tif err.Error() != tc.err {\n\t\t\t\tt.Errorf(\"Incorrect error: Want %s, got %s\", tc.err, err.Error())\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif !reflect.DeepEqual(ti, tc.intervals) {\n\t\t\tt.Errorf(\"Error unmarshalling %s: Want %+v, got %+v\", tc.in, tc.intervals, ti)\n\t\t}\n\t\tfor _, ts := range tc.contains {\n\t\t\t_t, _ := time.Parse(time.RFC822Z, ts)\n\t\t\tisContained := false\n\t\t\tfor _, interval := range ti {\n\t\t\t\tif interval.ContainsTime(_t) {\n\t\t\t\t\tisContained = true\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !isContained {\n\t\t\t\tt.Errorf(\"Expected intervals to contain time %s\", _t)\n\t\t\t}\n\t\t}\n\t\tfor _, ts := range tc.excludes {\n\t\t\t_t, _ := time.Parse(time.RFC822Z, ts)\n\t\t\tisContained := false\n\t\t\tfor _, interval := range ti {\n\t\t\t\tif interval.ContainsTime(_t) {\n\t\t\t\t\tisContained = true\n\t\t\t\t}\n\t\t\t}\n\t\t\tif isContained {\n\t\t\t\tt.Errorf(\"Expected intervals to exclude time %s\", _t)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestContainsTime(t *testing.T) {\n\tfor _, tc := range timeIntervalTestCases {\n\t\tfor _, ts := range tc.validTimeStrings {\n\t\t\t_t, _ := time.Parse(time.RFC822Z, ts)\n\t\t\tif !tc.timeInterval.ContainsTime(_t) {\n\t\t\t\tt.Errorf(\"Expected period %+v to contain %+v\", tc.timeInterval, _t)\n\t\t\t}\n\t\t}\n\t\tfor _, ts := range tc.invalidTimeStrings {\n\t\t\t_t, _ := time.Parse(time.RFC822Z, ts)\n\t\t\tif tc.timeInterval.ContainsTime(_t) {\n\t\t\t\tt.Errorf(\"Period %+v not expected to contain %+v\", tc.timeInterval, _t)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestParseTimeString(t *testing.T) {\n\tfor _, tc := range timeStringTestCases {\n\t\tvar tr TimeRange\n\t\terr := yaml.Unmarshal([]byte(tc.timeString), &tr)\n\t\tif err != nil && !tc.expectError {\n\t\t\tt.Errorf(\"Received unexpected error: %v when parsing %v\", err, tc.timeString)\n\t\t} else if err == nil && tc.expectError {\n\t\t\tt.Errorf(\"Expected error for invalid string %s but didn't receive one\", tc.timeString)\n\t\t} else if !reflect.DeepEqual(tr, tc.TimeRange) {\n\t\t\tt.Errorf(\"Error parsing time string %s: Want %+v, got %+v\", tc.timeString, tc.TimeRange, tr)\n\t\t}\n\t}\n}\n\nfunc TestYamlMarshal(t *testing.T) {\n\tfor _, tc := range yamlUnmarshalTestCases {\n\t\tif tc.expectError {\n\t\t\tcontinue\n\t\t}\n\t\tvar ti []TimeInterval\n\t\terr := yaml.Unmarshal([]byte(tc.in), &ti)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tout, err := yaml.Marshal(&ti)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tvar ti2 []TimeInterval\n\t\tyaml.Unmarshal(out, &ti2)\n\t\tif !reflect.DeepEqual(ti, ti2) {\n\t\t\tt.Errorf(\"Re-marshalling %s produced a different TimeInterval.\", tc.in)\n\t\t}\n\t}\n}\n\n// Test JSON marshalling by marshalling a time interval\n// and then unmarshalling to ensure they're identical.\nfunc TestJsonMarshal(t *testing.T) {\n\tfor _, tc := range yamlUnmarshalTestCases {\n\t\tif tc.expectError {\n\t\t\tcontinue\n\t\t}\n\t\tvar ti []TimeInterval\n\t\terr := yaml.Unmarshal([]byte(tc.in), &ti)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tout, err := json.Marshal(&ti)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tvar ti2 []TimeInterval\n\t\tjson.Unmarshal(out, &ti2)\n\t\tif !reflect.DeepEqual(ti, ti2) {\n\t\t\tt.Errorf(\"Re-marshalling %s produced a different TimeInterval. Used:\\n%s and got:\\n%v\", tc.in, out, ti2)\n\t\t}\n\t}\n}\n\nvar completeTestCases = []struct {\n\tin       string\n\tcontains []string\n\texcludes []string\n}{\n\t{\n\t\tin: `\n---\nweekdays: ['monday:wednesday', 'saturday', 'sunday']\ntimes:\n  - start_time: '13:00'\n    end_time: '15:00'\ndays_of_month: ['1', '10', '20:-1']\nyears: ['2020:2023']\nmonths: ['january:march']\n`,\n\t\tcontains: []string{\n\t\t\t\"10 Jan 21 13:00 +0000\",\n\t\t\t\"30 Jan 21 14:24 +0000\",\n\t\t},\n\t\texcludes: []string{\n\t\t\t\"09 Jan 21 13:00 +0000\",\n\t\t\t\"20 Jan 21 12:59 +0000\",\n\t\t\t\"02 Feb 21 13:00 +0000\",\n\t\t},\n\t},\n\t{\n\t\t// Check for broken clamping (clamping begin date after end of month to the end of the month)\n\t\tin: `\n---\ndays_of_month: ['30:31']\nyears: ['2020:2023']\nmonths: ['february']\n`,\n\t\texcludes: []string{\n\t\t\t\"28 Feb 21 13:00 +0000\",\n\t\t},\n\t},\n}\n\n// Tests the entire flow from unmarshalling to containing a time.\nfunc TestTimeIntervalComplete(t *testing.T) {\n\tfor _, tc := range completeTestCases {\n\t\tvar ti TimeInterval\n\t\tif err := yaml.Unmarshal([]byte(tc.in), &ti); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tfor _, ts := range tc.contains {\n\t\t\ttt, err := time.Parse(time.RFC822Z, ts)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t\tif !ti.ContainsTime(tt) {\n\t\t\t\tt.Errorf(\"Expected %s to contain %s\", tc.in, ts)\n\t\t\t}\n\t\t}\n\t\tfor _, ts := range tc.excludes {\n\t\t\ttt, err := time.Parse(time.RFC822Z, ts)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t\tif ti.ContainsTime(tt) {\n\t\t\t\tt.Errorf(\"Expected %s to exclude %s\", tc.in, ts)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Utility function for declaring time locations in test cases. Panic if the location can't be loaded.\nfunc mustLoadLocation(name string) *time.Location {\n\tloc, err := time.LoadLocation(name)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn loc\n}\n\nfunc TestIntervener_Mutes(t *testing.T) {\n\tsydney, err := time.LoadLocation(\"Australia/Sydney\")\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to load location Australia/Sydney: %s\", err)\n\t}\n\teveningsAndWeekends := map[string][]TimeInterval{\n\t\t\"evenings\": {{\n\t\t\tTimes: []TimeRange{{\n\t\t\t\tStartMinute: 0,   // 00:00\n\t\t\t\tEndMinute:   540, // 09:00\n\t\t\t}, {\n\t\t\t\tStartMinute: 1020, // 17:00\n\t\t\t\tEndMinute:   1440, // 24:00\n\t\t\t}},\n\t\t\tLocation: &Location{Location: sydney},\n\t\t}},\n\t\t\"weekends\": {{\n\t\t\tWeekdays: []WeekdayRange{{\n\t\t\t\tInclusiveRange: InclusiveRange{Begin: 6, End: 6}, // Saturday\n\t\t\t}, {\n\t\t\t\tInclusiveRange: InclusiveRange{Begin: 0, End: 0}, // Sunday\n\t\t\t}},\n\t\t\tLocation: &Location{Location: sydney},\n\t\t}},\n\t}\n\n\ttests := []struct {\n\t\tname      string\n\t\tintervals map[string][]TimeInterval\n\t\tnow       time.Time\n\t\tmutedBy   []string\n\t}{{\n\t\tname:      \"Should be muted outside working hours\",\n\t\tintervals: eveningsAndWeekends,\n\t\tnow:       time.Date(2024, 1, 1, 0, 0, 0, 0, sydney),\n\t\tmutedBy:   []string{\"evenings\"},\n\t}, {\n\t\tname:      \"Should not be muted during working hours\",\n\t\tintervals: eveningsAndWeekends,\n\t\tnow:       time.Date(2024, 1, 1, 9, 0, 0, 0, sydney),\n\t\tmutedBy:   nil,\n\t}, {\n\t\tname:      \"Should be muted during weekends\",\n\t\tintervals: eveningsAndWeekends,\n\t\tnow:       time.Date(2024, 1, 6, 10, 0, 0, 0, sydney),\n\t\tmutedBy:   []string{\"weekends\"},\n\t}, {\n\t\tname:      \"Should be muted during weekend evenings\",\n\t\tintervals: eveningsAndWeekends,\n\t\tnow:       time.Date(2024, 1, 6, 17, 0, 0, 0, sydney),\n\t\tmutedBy:   []string{\"evenings\", \"weekends\"},\n\t}, {\n\t\tname:      \"Should be muted at 12pm UTC on a weekday\",\n\t\tintervals: eveningsAndWeekends,\n\t\tnow:       time.Date(2024, 1, 1, 10, 0, 0, 0, time.UTC),\n\t\tmutedBy:   []string{\"evenings\"},\n\t}, {\n\t\tname:      \"Should be muted at 12pm UTC on a weekend\",\n\t\tintervals: eveningsAndWeekends,\n\t\tnow:       time.Date(2024, 1, 6, 10, 0, 0, 0, time.UTC),\n\t\tmutedBy:   []string{\"evenings\", \"weekends\"},\n\t}}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tintervener := NewIntervener(test.intervals)\n\n\t\t\t// Get the names of all time intervals for the context.\n\t\t\ttimeIntervalNames := make([]string, 0, len(test.intervals))\n\t\t\tfor name := range test.intervals {\n\t\t\t\ttimeIntervalNames = append(timeIntervalNames, name)\n\t\t\t}\n\t\t\t// Sort the names so we can compare mutedBy with test.mutedBy.\n\t\t\tsort.Strings(timeIntervalNames)\n\n\t\t\tisMuted, mutedBy, err := intervener.Mutes(timeIntervalNames, test.now)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif len(test.mutedBy) == 0 {\n\t\t\t\trequire.False(t, isMuted)\n\t\t\t\trequire.Empty(t, mutedBy)\n\t\t\t} else {\n\t\t\t\trequire.True(t, isMuted)\n\t\t\t\trequire.Equal(t, test.mutedBy, mutedBy)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "tracing/config.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage tracing\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/model\"\n)\n\n// TODO: probably move these into prometheus/common since they're copied from\n// prometheus/prometheus?\n\ntype TracingClientType string\n\nconst (\n\tTracingClientHTTP TracingClientType = \"http\"\n\tTracingClientGRPC TracingClientType = \"grpc\"\n\n\tGzipCompression = \"gzip\"\n)\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (t *TracingClientType) UnmarshalYAML(unmarshal func(any) error) error {\n\t*t = TracingClientType(\"\")\n\ttype plain TracingClientType\n\tif err := unmarshal((*plain)(t)); err != nil {\n\t\treturn err\n\t}\n\n\tswitch *t {\n\tcase TracingClientHTTP, TracingClientGRPC:\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"expected tracing client type to be to be %s or %s, but got %s\",\n\t\t\tTracingClientHTTP, TracingClientGRPC, *t,\n\t\t)\n\t}\n}\n\n// TracingConfig configures the tracing options.\ntype TracingConfig struct {\n\tClientType       TracingClientType    `yaml:\"client_type,omitempty\"`\n\tEndpoint         string               `yaml:\"endpoint,omitempty\"`\n\tSamplingFraction float64              `yaml:\"sampling_fraction,omitempty\"`\n\tInsecure         bool                 `yaml:\"insecure,omitempty\"`\n\tTLSConfig        *commoncfg.TLSConfig `yaml:\"tls_config,omitempty\"`\n\tHeaders          *commoncfg.Headers   `yaml:\"headers,omitempty\"`\n\tCompression      string               `yaml:\"compression,omitempty\"`\n\tTimeout          model.Duration       `yaml:\"timeout,omitempty\"`\n}\n\n// SetDirectory joins any relative file paths with dir.\nfunc (t *TracingConfig) SetDirectory(dir string) {\n\tt.TLSConfig.SetDirectory(dir)\n\tt.Headers.SetDirectory(dir)\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (t *TracingConfig) UnmarshalYAML(unmarshal func(any) error) error {\n\t*t = TracingConfig{\n\t\tClientType: TracingClientGRPC,\n\t}\n\ttype plain TracingConfig\n\tif err := unmarshal((*plain)(t)); err != nil {\n\t\treturn err\n\t}\n\n\tif t.Endpoint == \"\" {\n\t\treturn errors.New(\"tracing endpoint must be set\")\n\t}\n\n\tif t.Compression != \"\" && t.Compression != GzipCompression {\n\t\treturn fmt.Errorf(\"invalid compression type %s provided, valid options: %s\",\n\t\t\tt.Compression, GzipCompression)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "tracing/http.go",
    "content": "// Copyright 2024 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage tracing\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptrace\"\n\n\t\"go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace\"\n\t\"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\"\n)\n\n// TODO: maybe move these into prometheus/common?\n\n// Transport wraps the provided http.RoundTripper with one that starts a span\n// and injects the span context into the outbound request headers. If the\n// provided http.RoundTripper is nil, http.DefaultTransport will be used as the\n// base http.RoundTripper.\nfunc Transport(rt http.RoundTripper) http.RoundTripper {\n\trt = otelhttp.NewTransport(rt,\n\t\totelhttp.WithClientTrace(func(ctx context.Context) *httptrace.ClientTrace {\n\t\t\treturn otelhttptrace.NewClientTrace(ctx)\n\t\t}),\n\t)\n\n\treturn rt\n}\n\n// Middleware returns a new HTTP handler that will trace all requests with the\n// HTTP method and path as the span name.\nfunc Middleware(handler http.Handler) http.Handler {\n\treturn otelhttp.NewHandler(handler, \"\",\n\t\totelhttp.WithSpanNameFormatter(func(_ string, r *http.Request) string {\n\t\t\treturn fmt.Sprintf(\"%s %s\", r.Method, r.URL.Path)\n\t\t}),\n\t)\n}\n"
  },
  {
    "path": "tracing/testdata/ca.cer",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDkTCCAnmgAwIBAgIJAJNsnimNN3tmMA0GCSqGSIb3DQEBCwUAMF8xCzAJBgNV\nBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg\nQ29tcGFueSBMdGQxGzAZBgNVBAMMElByb21ldGhldXMgVGVzdCBDQTAeFw0xNTA4\nMDQxNDA5MjFaFw0yNTA4MDExNDA5MjFaMF8xCzAJBgNVBAYTAlhYMRUwEwYDVQQH\nDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQxGzAZ\nBgNVBAMMElByb21ldGhldXMgVGVzdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP\nADCCAQoCggEBAOlSBU3yWpUELbhzizznR0hnAL7dbEHzfEtEc6N3PoSvMNcqrUVq\nt4kjBRWzqkZ5uJVkzBPERKEBoOI9pWcrqtMTBkMzHJY2Ep7GHTab10e9KC2IFQT6\nFKP/jCYixaIVx3azEfajRJooD8r79FGoagWUfHdHyCFWJb/iLt8z8+S91kelSRMS\nyB9M1ypWomzBz1UFXZp1oiNO5o7/dgXW4MgLUfC2obJ9j5xqpc6GkhWMW4ZFwEr/\nVLjuzxG9B8tLfQuhnXKGn1W8+WzZVWCWMD/sLfZfmjKaWlwcXzL51g8E+IEIBJqV\nw51aMI6lDkcvAM7gLq1auLZMVXyKWSKw7XMCAwEAAaNQME4wHQYDVR0OBBYEFMz1\nBZnlqxJp2HiJSjHK8IsLrWYbMB8GA1UdIwQYMBaAFMz1BZnlqxJp2HiJSjHK8IsL\nrWYbMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAI2iA3w3TK5J15Pu\ne4fPFB4jxQqsbUwuyXbCCv/jKLeFNCD4BjM181WZEYjPMumeTBVzU3aF45LWQIG1\n0DJcrCL4mjMz9qgAoGqA7aDDXiJGbukMgYYsn7vrnVmrZH8T3E8ySlltr7+W578k\npJ5FxnbCroQwn0zLyVB3sFbS8E3vpBr3L8oy8PwPHhIScexcNVc3V6/m4vTZsXTH\nU+vUm1XhDgpDcFMTg2QQiJbfpOYUkwIgnRDAT7t282t2KQWtnlqc3zwPQ1F/6Cpx\nj19JeNsaF1DArkD7YlyKj/GhZLtHwFHG5cxznH0mLDJTW7bQvqqh2iQTeXmBk1lU\nmM5lH/s=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tracing/tracing.go",
    "content": "// Copyright 2021 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage tracing\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"reflect\"\n\t\"time\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/version\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp\"\n\t\"go.opentelemetry.io/otel/propagation\"\n\t\"go.opentelemetry.io/otel/sdk/resource\"\n\ttracesdk \"go.opentelemetry.io/otel/sdk/trace\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.39.0\"\n\t\"go.opentelemetry.io/otel/trace\"\n\t\"go.opentelemetry.io/otel/trace/noop\"\n\t\"google.golang.org/grpc/credentials\"\n)\n\nconst serviceName = \"alertmanager\"\n\n// Manager is capable of building, (re)installing and shutting down\n// the tracer provider.\ntype Manager struct {\n\tlogger       *slog.Logger\n\tdone         chan struct{}\n\tconfig       TracingConfig\n\tshutdownFunc func() error\n}\n\n// NewManager creates a new tracing manager.\nfunc NewManager(logger *slog.Logger) *Manager {\n\treturn &Manager{\n\t\tlogger: logger,\n\t\tdone:   make(chan struct{}),\n\t}\n}\n\n// Run starts the tracing manager. It registers the global text map propagator and error handler.\n// It is blocking.\nfunc (m *Manager) Run() {\n\totel.SetTextMapPropagator(propagation.TraceContext{})\n\totel.SetErrorHandler(otelErrHandler(func(err error) {\n\t\tm.logger.Error(\"OpenTelemetry handler returned an error\", \"err\", err)\n\t}))\n\t<-m.done\n}\n\n// ApplyConfig takes care of refreshing the tracing configuration by shutting down\n// the current tracer provider (if any is registered) and installing a new one.\nfunc (m *Manager) ApplyConfig(cfg TracingConfig) error {\n\t// Update only if a config change is detected. If TLS configuration is\n\t// set, we have to restart the manager to make sure that new TLS\n\t// certificates are picked up.\n\tvar blankTLSConfig commoncfg.TLSConfig\n\tif reflect.DeepEqual(m.config, cfg) && (m.config.TLSConfig == nil || *m.config.TLSConfig == blankTLSConfig) {\n\t\treturn nil\n\t}\n\n\tif m.shutdownFunc != nil {\n\t\tif err := m.shutdownFunc(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to shut down the tracer provider: %w\", err)\n\t\t}\n\t}\n\n\t// If no endpoint is set, assume tracing should be disabled.\n\tif cfg.Endpoint == \"\" {\n\t\tm.config = cfg\n\t\tm.shutdownFunc = nil\n\t\totel.SetTracerProvider(noop.NewTracerProvider())\n\t\tm.logger.Info(\"Tracing provider uninstalled.\")\n\t\treturn nil\n\t}\n\n\ttp, shutdownFunc, err := buildTracerProvider(context.Background(), cfg)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to install a new tracer provider: %w\", err)\n\t}\n\n\tm.shutdownFunc = shutdownFunc\n\tm.config = cfg\n\totel.SetTracerProvider(tp)\n\n\tm.logger.Info(\"Successfully installed a new tracer provider.\")\n\treturn nil\n}\n\n// Stop gracefully shuts down the tracer provider and stops the tracing manager.\nfunc (m *Manager) Stop() {\n\tdefer close(m.done)\n\n\tif m.shutdownFunc == nil {\n\t\treturn\n\t}\n\n\tif err := m.shutdownFunc(); err != nil {\n\t\tm.logger.Error(\"failed to shut down the tracer provider\", \"err\", err)\n\t}\n\n\tm.logger.Info(\"Tracing manager stopped\")\n}\n\ntype otelErrHandler func(err error)\n\nfunc (o otelErrHandler) Handle(err error) {\n\to(err)\n}\n\n// buildTracerProvider return a new tracer provider ready for installation, together\n// with a shutdown function.\nfunc buildTracerProvider(ctx context.Context, tracingCfg TracingConfig) (trace.TracerProvider, func() error, error) {\n\tclient, err := getClient(tracingCfg)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\texp, err := otlptrace.New(ctx, client)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// Create a resource describing the service and the runtime.\n\tres, err := resource.New(\n\t\tctx,\n\t\tresource.WithSchemaURL(semconv.SchemaURL),\n\t\tresource.WithAttributes(\n\t\t\tsemconv.ServiceNameKey.String(serviceName),\n\t\t\tsemconv.ServiceVersionKey.String(version.Version),\n\t\t),\n\t\tresource.WithProcessRuntimeDescription(),\n\t\tresource.WithTelemetrySDK(),\n\t)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\ttp := tracesdk.NewTracerProvider(\n\t\ttracesdk.WithBatcher(exp),\n\t\ttracesdk.WithSampler(tracesdk.ParentBased(\n\t\t\ttracesdk.TraceIDRatioBased(tracingCfg.SamplingFraction),\n\t\t)),\n\t\ttracesdk.WithResource(res),\n\t)\n\n\treturn tp, func() error {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\t\tdefer cancel()\n\t\terr := tp.Shutdown(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t}, nil\n}\n\n// headersToMap converts prometheus/common Headers to a simple map[string]string.\n// It takes the first value from Values, Secrets, or Files for each header.\nfunc headersToMap(headers *commoncfg.Headers) (map[string]string, error) {\n\tif headers == nil || len(headers.Headers) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tresult := make(map[string]string)\n\tfor name, header := range headers.Headers {\n\t\tif len(header.Values) > 0 {\n\t\t\tresult[name] = header.Values[0]\n\t\t} else if len(header.Secrets) > 0 {\n\t\t\tresult[name] = string(header.Secrets[0])\n\t\t} else if len(header.Files) > 0 {\n\t\t\t// Note: Files would need to be read at runtime. For tracing config,\n\t\t\t// we only support direct values and secrets.\n\t\t\treturn nil, fmt.Errorf(\"header files are not supported for tracing configuration\")\n\t\t}\n\t}\n\treturn result, nil\n}\n\n// getClient returns an appropriate OTLP client (either gRPC or HTTP), based\n// on the provided tracing configuration.\nfunc getClient(tracingCfg TracingConfig) (otlptrace.Client, error) {\n\tvar client otlptrace.Client\n\tswitch tracingCfg.ClientType {\n\tcase TracingClientGRPC:\n\t\topts := []otlptracegrpc.Option{otlptracegrpc.WithEndpoint(tracingCfg.Endpoint)}\n\n\t\tswitch {\n\t\tcase tracingCfg.Insecure:\n\t\t\topts = append(opts, otlptracegrpc.WithInsecure())\n\t\tcase tracingCfg.TLSConfig != nil:\n\t\t\t// Use of TLS Credentials forces the use of TLS. Therefore it can\n\t\t\t// only be set when `insecure` is set to false.\n\t\t\ttlsConf, err := commoncfg.NewTLSConfig(tracingCfg.TLSConfig)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\topts = append(opts, otlptracegrpc.WithTLSCredentials(credentials.NewTLS(tlsConf)))\n\t\t}\n\n\t\tif tracingCfg.Compression != \"\" {\n\t\t\topts = append(opts, otlptracegrpc.WithCompressor(tracingCfg.Compression))\n\t\t}\n\n\t\theaders, err := headersToMap(tracingCfg.Headers)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(headers) > 0 {\n\t\t\topts = append(opts, otlptracegrpc.WithHeaders(headers))\n\t\t}\n\n\t\tif tracingCfg.Timeout != 0 {\n\t\t\topts = append(opts, otlptracegrpc.WithTimeout(time.Duration(tracingCfg.Timeout)))\n\t\t}\n\n\t\tclient = otlptracegrpc.NewClient(opts...)\n\tcase TracingClientHTTP:\n\t\topts := []otlptracehttp.Option{otlptracehttp.WithEndpoint(tracingCfg.Endpoint)}\n\n\t\tswitch {\n\t\tcase tracingCfg.Insecure:\n\t\t\topts = append(opts, otlptracehttp.WithInsecure())\n\t\tcase tracingCfg.TLSConfig != nil:\n\t\t\ttlsConf, err := commoncfg.NewTLSConfig(tracingCfg.TLSConfig)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\topts = append(opts, otlptracehttp.WithTLSClientConfig(tlsConf))\n\t\t}\n\n\t\tif tracingCfg.Compression == GzipCompression {\n\t\t\topts = append(opts, otlptracehttp.WithCompression(otlptracehttp.GzipCompression))\n\t\t}\n\n\t\theaders, err := headersToMap(tracingCfg.Headers)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(headers) > 0 {\n\t\t\topts = append(opts, otlptracehttp.WithHeaders(headers))\n\t\t}\n\n\t\tif tracingCfg.Timeout != 0 {\n\t\t\topts = append(opts, otlptracehttp.WithTimeout(time.Duration(tracingCfg.Timeout)))\n\t\t}\n\n\t\tclient = otlptracehttp.NewClient(opts...)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown tracing client type: %s\", tracingCfg.ClientType)\n\t}\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "tracing/tracing_test.go",
    "content": "// Copyright 2021 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage tracing\n\nimport (\n\t\"testing\"\n\n\tcommoncfg \"github.com/prometheus/common/config\"\n\t\"github.com/prometheus/common/promslog\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/trace/noop\"\n)\n\nfunc TestInstallingNewTracerProvider(t *testing.T) {\n\ttpBefore := otel.GetTracerProvider()\n\n\tm := NewManager(promslog.NewNopLogger())\n\tcfg := TracingConfig{\n\t\tEndpoint:   \"localhost:1234\",\n\t\tClientType: TracingClientGRPC,\n\t}\n\n\trequire.NoError(t, m.ApplyConfig(cfg))\n\trequire.NotEqual(t, tpBefore, otel.GetTracerProvider())\n}\n\nfunc TestReinstallingTracerProvider(t *testing.T) {\n\tm := NewManager(promslog.NewNopLogger())\n\tcfg := TracingConfig{\n\t\tEndpoint:   \"localhost:1234\",\n\t\tClientType: TracingClientGRPC,\n\t\tHeaders: &commoncfg.Headers{\n\t\t\tHeaders: map[string]commoncfg.Header{\n\t\t\t\t\"foo\": {Values: []string{\"bar\"}},\n\t\t\t},\n\t\t},\n\t}\n\n\trequire.NoError(t, m.ApplyConfig(cfg))\n\ttpFirstConfig := otel.GetTracerProvider()\n\n\t// Trying to apply the same config should not reinstall provider.\n\trequire.NoError(t, m.ApplyConfig(cfg))\n\trequire.Equal(t, tpFirstConfig, otel.GetTracerProvider())\n\n\tcfg2 := TracingConfig{\n\t\tEndpoint:   \"localhost:1234\",\n\t\tClientType: TracingClientHTTP,\n\t\tHeaders: &commoncfg.Headers{\n\t\t\tHeaders: map[string]commoncfg.Header{\n\t\t\t\t\"bar\": {Values: []string{\"foo\"}},\n\t\t\t},\n\t\t},\n\t}\n\n\trequire.NoError(t, m.ApplyConfig(cfg2))\n\trequire.NotEqual(t, tpFirstConfig, otel.GetTracerProvider())\n\ttpSecondConfig := otel.GetTracerProvider()\n\n\t// Setting previously unset option should reinstall provider.\n\tcfg2.Compression = \"gzip\"\n\trequire.NoError(t, m.ApplyConfig(cfg2))\n\trequire.NotEqual(t, tpSecondConfig, otel.GetTracerProvider())\n}\n\nfunc TestReinstallingTracerProviderWithTLS(t *testing.T) {\n\tm := NewManager(promslog.NewNopLogger())\n\tcfg := TracingConfig{\n\t\tEndpoint:   \"localhost:1234\",\n\t\tClientType: TracingClientGRPC,\n\t\tTLSConfig: &commoncfg.TLSConfig{\n\t\t\tCAFile: \"testdata/ca.cer\",\n\t\t},\n\t}\n\n\trequire.NoError(t, m.ApplyConfig(cfg))\n\ttpFirstConfig := otel.GetTracerProvider()\n\n\t// Trying to apply the same config with TLS should reinstall provider.\n\trequire.NoError(t, m.ApplyConfig(cfg))\n\trequire.NotEqual(t, tpFirstConfig, otel.GetTracerProvider())\n}\n\nfunc TestUninstallingTracerProvider(t *testing.T) {\n\tm := NewManager(promslog.NewNopLogger())\n\tcfg := TracingConfig{\n\t\tEndpoint:   \"localhost:1234\",\n\t\tClientType: TracingClientGRPC,\n\t}\n\n\trequire.NoError(t, m.ApplyConfig(cfg))\n\trequire.NotEqual(t, noop.NewTracerProvider(), otel.GetTracerProvider())\n\n\t// Uninstall by passing empty config.\n\tcfg2 := TracingConfig{}\n\n\trequire.NoError(t, m.ApplyConfig(cfg2))\n\t// Make sure we get a no-op tracer provider after uninstallation.\n\trequire.Equal(t, noop.NewTracerProvider(), otel.GetTracerProvider())\n}\n\nfunc TestTracerProviderShutdown(t *testing.T) {\n\tm := NewManager(promslog.NewNopLogger())\n\tcfg := TracingConfig{\n\t\tEndpoint:   \"localhost:1234\",\n\t\tClientType: TracingClientGRPC,\n\t}\n\n\trequire.NoError(t, m.ApplyConfig(cfg))\n\tm.Stop()\n\n\t// Check if we closed the done channel.\n\t_, ok := <-m.done\n\trequire.False(t, ok)\n}\n"
  },
  {
    "path": "types/types.go",
    "content": "// Copyright 2015 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage types\n\nimport (\n\t\"sync\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/common/model\"\n\n\t\"github.com/prometheus/alertmanager/alert\"\n)\n\n// Deprecated: Use alert.Alert directly.\ntype Alert = alert.Alert\n\n// Deprecated: Use alert.AlertSlice directly.\ntype AlertSlice = alert.AlertSlice\n\n// Deprecated: Use alert.Alerts directly.\nvar Alerts = alert.Alerts\n\n// Deprecated: Use alert.AlertState constants directly.\ntype AlertState = alert.AlertState\n\n// Deprecated: Use alert.AlertStateActive directly.\nconst AlertStateActive AlertState = alert.AlertStateActive\n\n// Deprecated: Use alert.AlertStateSuppressed directly.\nconst AlertStateSuppressed AlertState = alert.AlertStateSuppressed\n\n// Deprecated: Use alert.AlertStateUnprocessed directly.\nconst AlertStateUnprocessed AlertState = alert.AlertStateUnprocessed\n\n// Deprecated: Use alert.AlertStatus directly.\ntype AlertStatus = alert.AlertStatus\n\n// groupStatus stores the state of the group, and, as applicable, the names\n// of all active and mute time intervals that are muting it.\ntype groupStatus struct {\n\t// mutedBy contains the names of all active and mute time intervals that\n\t// are muting it.\n\tmutedBy []string\n}\n\n// AlertMarker helps to mark alerts as silenced and/or inhibited.\n// All methods are goroutine-safe.\ntype AlertMarker interface {\n\t// SetActiveOrSilenced replaces the previous SilencedBy by the provided IDs of\n\t// active silences. The set of provided IDs is supposed to represent the\n\t// complete set of relevant silences. If no active silence IDs are provided and\n\t// InhibitedBy is already empty, it sets the provided alert to AlertStateActive.\n\t// Otherwise, it sets the provided alert to AlertStateSuppressed.\n\tSetActiveOrSilenced(alert model.Fingerprint, activeSilenceIDs []string)\n\t// SetInhibited replaces the previous InhibitedBy by the provided IDs of\n\t// alerts. In contrast to SetActiveOrSilenced, the set of provided IDs is not\n\t// expected to represent the complete set of inhibiting alerts. (In\n\t// practice, this method is only called with one or zero IDs. However,\n\t// this expectation might change in the future. If no IDs are provided\n\t// and InhibitedBy is already empty, it sets the provided alert to\n\t// AlertStateActive. Otherwise, it sets the provided alert to\n\t// AlertStateSuppressed.\n\tSetInhibited(alert model.Fingerprint, alertIDs ...string)\n\n\t// Count alerts of the given state(s). With no state provided, count all\n\t// alerts.\n\tCount(...AlertState) int\n\n\t// Status of the given alert.\n\tStatus(model.Fingerprint) AlertStatus\n\t// Delete the given alert.\n\tDelete(...model.Fingerprint)\n\n\t// Various methods to inquire if the given alert is in a certain\n\t// AlertState. Silenced also returns all the active silences,\n\t// while Inhibited may return only a subset of inhibiting alerts.\n\tUnprocessed(model.Fingerprint) bool\n\tActive(model.Fingerprint) bool\n\tSilenced(model.Fingerprint) (activeIDs []string, silenced bool)\n\tInhibited(model.Fingerprint) ([]string, bool)\n}\n\n// GroupMarker helps to mark groups as active or muted.\n// All methods are goroutine-safe.\n//\n// TODO(grobinson): routeID is used in Muted and SetMuted because groupKey\n// is not unique (see #3817). Once groupKey uniqueness is fixed routeID can\n// be removed from the GroupMarker interface.\ntype GroupMarker interface {\n\t// Muted returns true if the group is muted, otherwise false. If the group\n\t// is muted then it also returns the names of the time intervals that muted\n\t// it.\n\tMuted(routeID, groupKey string) ([]string, bool)\n\n\t// SetMuted marks the group as muted, and sets the names of the time\n\t// intervals that mute it. If the list of names is nil or the empty slice\n\t// then the muted marker is removed.\n\tSetMuted(routeID, groupKey string, timeIntervalNames []string)\n\n\t// DeleteByGroupKey removes all markers for the GroupKey.\n\tDeleteByGroupKey(routeID, groupKey string)\n}\n\n// NewMarker returns an instance of a AlertMarker implementation.\nfunc NewMarker(r prometheus.Registerer) *MemMarker {\n\tm := &MemMarker{\n\t\talerts: map[model.Fingerprint]*AlertStatus{},\n\t\tgroups: map[string]*groupStatus{},\n\t}\n\tm.registerMetrics(r)\n\treturn m\n}\n\ntype MemMarker struct {\n\talerts map[model.Fingerprint]*AlertStatus\n\tgroups map[string]*groupStatus\n\n\tmtx sync.RWMutex\n}\n\n// Muted implements GroupMarker.\nfunc (m *MemMarker) Muted(routeID, groupKey string) ([]string, bool) {\n\tm.mtx.Lock()\n\tdefer m.mtx.Unlock()\n\tstatus, ok := m.groups[routeID+groupKey]\n\tif !ok {\n\t\treturn nil, false\n\t}\n\treturn status.mutedBy, len(status.mutedBy) > 0\n}\n\n// SetMuted implements GroupMarker.\nfunc (m *MemMarker) SetMuted(routeID, groupKey string, timeIntervalNames []string) {\n\tm.mtx.Lock()\n\tdefer m.mtx.Unlock()\n\tstatus, ok := m.groups[routeID+groupKey]\n\tif !ok {\n\t\tstatus = &groupStatus{}\n\t\tm.groups[routeID+groupKey] = status\n\t}\n\tstatus.mutedBy = timeIntervalNames\n}\n\nfunc (m *MemMarker) DeleteByGroupKey(routeID, groupKey string) {\n\tm.mtx.Lock()\n\tdefer m.mtx.Unlock()\n\tdelete(m.groups, routeID+groupKey)\n}\n\nfunc (m *MemMarker) registerMetrics(r prometheus.Registerer) {\n\tnewMarkedAlertMetricByState := func(st AlertState) prometheus.GaugeFunc {\n\t\treturn prometheus.NewGaugeFunc(\n\t\t\tprometheus.GaugeOpts{\n\t\t\t\tName:        \"alertmanager_marked_alerts\",\n\t\t\t\tHelp:        \"How many alerts by state are currently marked in the Alertmanager regardless of their expiry.\",\n\t\t\t\tConstLabels: prometheus.Labels{\"state\": string(st)},\n\t\t\t},\n\t\t\tfunc() float64 {\n\t\t\t\treturn float64(m.Count(st))\n\t\t\t},\n\t\t)\n\t}\n\n\talertsActive := newMarkedAlertMetricByState(AlertStateActive)\n\talertsSuppressed := newMarkedAlertMetricByState(AlertStateSuppressed)\n\talertStateUnprocessed := newMarkedAlertMetricByState(AlertStateUnprocessed)\n\n\tr.MustRegister(alertsActive)\n\tr.MustRegister(alertsSuppressed)\n\tr.MustRegister(alertStateUnprocessed)\n}\n\n// Count implements AlertMarker.\nfunc (m *MemMarker) Count(states ...AlertState) int {\n\tm.mtx.RLock()\n\tdefer m.mtx.RUnlock()\n\n\tif len(states) == 0 {\n\t\treturn len(m.alerts)\n\t}\n\n\tvar count int\n\tfor _, status := range m.alerts {\n\t\tfor _, state := range states {\n\t\t\tif status.State == state {\n\t\t\t\tcount++\n\t\t\t}\n\t\t}\n\t}\n\treturn count\n}\n\n// SetActiveOrSilenced implements AlertMarker.\nfunc (m *MemMarker) SetActiveOrSilenced(alert model.Fingerprint, activeIDs []string) {\n\tm.mtx.Lock()\n\tdefer m.mtx.Unlock()\n\n\ts, found := m.alerts[alert]\n\tif !found {\n\t\ts = &AlertStatus{}\n\t\tm.alerts[alert] = s\n\t}\n\ts.SilencedBy = activeIDs\n\n\t// If there are any silence or alert IDs associated with the\n\t// fingerprint, it is suppressed. Otherwise, set it to\n\t// AlertStateActive.\n\tif len(activeIDs) == 0 && len(s.InhibitedBy) == 0 {\n\t\ts.State = AlertStateActive\n\t\treturn\n\t}\n\n\ts.State = AlertStateSuppressed\n}\n\n// SetInhibited implements AlertMarker.\nfunc (m *MemMarker) SetInhibited(alert model.Fingerprint, ids ...string) {\n\tm.mtx.Lock()\n\tdefer m.mtx.Unlock()\n\n\ts, found := m.alerts[alert]\n\tif !found {\n\t\ts = &AlertStatus{}\n\t\tm.alerts[alert] = s\n\t}\n\ts.InhibitedBy = ids\n\n\t// If there are any silence or alert IDs associated with the\n\t// fingerprint, it is suppressed. Otherwise, set it to\n\t// AlertStateActive.\n\tif len(ids) == 0 && len(s.SilencedBy) == 0 {\n\t\ts.State = AlertStateActive\n\t\treturn\n\t}\n\n\ts.State = AlertStateSuppressed\n}\n\n// Status implements AlertMarker.\nfunc (m *MemMarker) Status(alert model.Fingerprint) AlertStatus {\n\tm.mtx.RLock()\n\tdefer m.mtx.RUnlock()\n\n\tif s, found := m.alerts[alert]; found {\n\t\treturn *s\n\t}\n\treturn AlertStatus{\n\t\tState:       AlertStateUnprocessed,\n\t\tSilencedBy:  []string{},\n\t\tInhibitedBy: []string{},\n\t}\n}\n\n// Delete implements AlertMarker.\nfunc (m *MemMarker) Delete(alerts ...model.Fingerprint) {\n\tm.mtx.Lock()\n\tdefer m.mtx.Unlock()\n\n\tfor _, alert := range alerts {\n\t\tdelete(m.alerts, alert)\n\t}\n}\n\n// Unprocessed implements AlertMarker.\nfunc (m *MemMarker) Unprocessed(alert model.Fingerprint) bool {\n\treturn m.Status(alert).State == AlertStateUnprocessed\n}\n\n// Active implements AlertMarker.\nfunc (m *MemMarker) Active(alert model.Fingerprint) bool {\n\treturn m.Status(alert).State == AlertStateActive\n}\n\n// Inhibited implements AlertMarker.\nfunc (m *MemMarker) Inhibited(alert model.Fingerprint) ([]string, bool) {\n\ts := m.Status(alert)\n\treturn s.InhibitedBy,\n\t\ts.State == AlertStateSuppressed && len(s.InhibitedBy) > 0\n}\n\n// Silenced returns whether the alert for the given Fingerprint is in the\n// Silenced state, any associated silence IDs, and the silences state version\n// the result is based on.\nfunc (m *MemMarker) Silenced(alert model.Fingerprint) (activeIDs []string, silenced bool) {\n\ts := m.Status(alert)\n\treturn s.SilencedBy,\n\t\ts.State == AlertStateSuppressed && len(s.SilencedBy) > 0\n}\n"
  },
  {
    "path": "types/types_test.go",
    "content": "// Copyright 2015 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage types //nolint:revive\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/common/model\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMemMarker_Muted(t *testing.T) {\n\tr := prometheus.NewRegistry()\n\tmarker := NewMarker(r)\n\n\t// No groups should be muted.\n\ttimeIntervalNames, isMuted := marker.Muted(\"route1\", \"group1\")\n\trequire.False(t, isMuted)\n\trequire.Empty(t, timeIntervalNames)\n\n\t// Mark the group as muted because it's the weekend.\n\tmarker.SetMuted(\"route1\", \"group1\", []string{\"weekends\"})\n\ttimeIntervalNames, isMuted = marker.Muted(\"route1\", \"group1\")\n\trequire.True(t, isMuted)\n\trequire.Equal(t, []string{\"weekends\"}, timeIntervalNames)\n\n\t// Other groups should not be marked as muted.\n\ttimeIntervalNames, isMuted = marker.Muted(\"route1\", \"group2\")\n\trequire.False(t, isMuted)\n\trequire.Empty(t, timeIntervalNames)\n\n\t// Other routes should not be marked as muted either.\n\ttimeIntervalNames, isMuted = marker.Muted(\"route2\", \"group1\")\n\trequire.False(t, isMuted)\n\trequire.Empty(t, timeIntervalNames)\n\n\t// The group is no longer muted.\n\tmarker.SetMuted(\"route1\", \"group1\", nil)\n\ttimeIntervalNames, isMuted = marker.Muted(\"route1\", \"group1\")\n\trequire.False(t, isMuted)\n\trequire.Empty(t, timeIntervalNames)\n}\n\nfunc TestMemMarker_DeleteByGroupKey(t *testing.T) {\n\tr := prometheus.NewRegistry()\n\tmarker := NewMarker(r)\n\n\t// Mark the group and check that it is muted.\n\tmarker.SetMuted(\"route1\", \"group1\", []string{\"weekends\"})\n\ttimeIntervalNames, isMuted := marker.Muted(\"route1\", \"group1\")\n\trequire.True(t, isMuted)\n\trequire.Equal(t, []string{\"weekends\"}, timeIntervalNames)\n\n\t// Delete the markers for a different group key. The group should\n\t// still be muted.\n\tmarker.DeleteByGroupKey(\"route1\", \"group2\")\n\ttimeIntervalNames, isMuted = marker.Muted(\"route1\", \"group1\")\n\trequire.True(t, isMuted)\n\trequire.Equal(t, []string{\"weekends\"}, timeIntervalNames)\n\n\t// Delete the markers for the correct group key. The group should\n\t// no longer be muted.\n\tmarker.DeleteByGroupKey(\"route1\", \"group1\")\n\ttimeIntervalNames, isMuted = marker.Muted(\"route1\", \"group1\")\n\trequire.False(t, isMuted)\n\trequire.Empty(t, timeIntervalNames)\n}\n\nfunc TestMemMarker_Count(t *testing.T) {\n\tr := prometheus.NewRegistry()\n\tmarker := NewMarker(r)\n\tnow := time.Now()\n\n\tstates := []AlertState{AlertStateSuppressed, AlertStateActive, AlertStateUnprocessed}\n\tcountByState := func(state AlertState) int {\n\t\treturn marker.Count(state)\n\t}\n\n\tcountTotal := func() int {\n\t\tvar count int\n\t\tfor _, s := range states {\n\t\t\tcount += countByState(s)\n\t\t}\n\t\treturn count\n\t}\n\n\trequire.Equal(t, 0, countTotal())\n\n\ta1 := model.Alert{\n\t\tStartsAt: now.Add(-2 * time.Minute),\n\t\tEndsAt:   now.Add(2 * time.Minute),\n\t\tLabels:   model.LabelSet{\"test\": \"active\"},\n\t}\n\ta2 := model.Alert{\n\t\tStartsAt: now.Add(-2 * time.Minute),\n\t\tEndsAt:   now.Add(2 * time.Minute),\n\t\tLabels:   model.LabelSet{\"test\": \"suppressed\"},\n\t}\n\ta3 := model.Alert{\n\t\tStartsAt: now.Add(-2 * time.Minute),\n\t\tEndsAt:   now.Add(-1 * time.Minute),\n\t\tLabels:   model.LabelSet{\"test\": \"resolved\"},\n\t}\n\n\t// Insert an active alert.\n\tmarker.SetActiveOrSilenced(a1.Fingerprint(), nil)\n\trequire.Equal(t, 1, countByState(AlertStateActive))\n\trequire.Equal(t, 1, countTotal())\n\n\t// Insert a silenced alert.\n\tmarker.SetActiveOrSilenced(a2.Fingerprint(), []string{\"1\"})\n\trequire.Equal(t, 1, countByState(AlertStateSuppressed))\n\trequire.Equal(t, 2, countTotal())\n\n\t// Insert a resolved silenced alert - it'll count as suppressed.\n\tmarker.SetActiveOrSilenced(a3.Fingerprint(), []string{\"1\"})\n\trequire.Equal(t, 2, countByState(AlertStateSuppressed))\n\trequire.Equal(t, 3, countTotal())\n\n\t// Remove the silence from a3 - it'll count as active.\n\tmarker.SetActiveOrSilenced(a3.Fingerprint(), nil)\n\trequire.Equal(t, 2, countByState(AlertStateActive))\n\trequire.Equal(t, 3, countTotal())\n}\n\ntype fakeRegisterer struct {\n\tregisteredCollectors []prometheus.Collector\n}\n\nfunc (r *fakeRegisterer) Register(prometheus.Collector) error {\n\treturn nil\n}\n\nfunc (r *fakeRegisterer) MustRegister(c ...prometheus.Collector) {\n\tr.registeredCollectors = append(r.registeredCollectors, c...)\n}\n\nfunc (r *fakeRegisterer) Unregister(prometheus.Collector) bool {\n\treturn false\n}\n\nfunc TestNewMarkerRegistersMetrics(t *testing.T) {\n\tfr := fakeRegisterer{}\n\tNewMarker(&fr)\n\n\tif len(fr.registeredCollectors) == 0 {\n\t\tt.Error(\"expected NewMarker to register metrics on the given registerer\")\n\t}\n}\n"
  },
  {
    "path": "ui/Dockerfile",
    "content": "FROM node:22-bookworm\n\nENV NPM_CONFIG_PREFIX=/home/node/.npm-global\nENV PATH=$PATH:/home/node/.npm-global/bin\n\nRUN mkdir -p $NPM_CONFIG_PREFIX; yarn global add \\\n  elm@0.19.1 \\\n  elm-format@0.8.7 \\\n  elm-test@0.19.1-revision6 \\\n  uglify-js@3.13.4 \\\n  elm-review@2.5.0\n"
  },
  {
    "path": "ui/app/.gitignore",
    "content": "dist/\nelm-stuff/\n.elm\n/elm-*\n/openapi-*\n/build\n"
  },
  {
    "path": "ui/app/CONTRIBUTING.md",
    "content": "# Contributing\n\nThis document describes how to:\n\n- Set up your dev environment\n- Become familiar with [Elm](http://elm-lang.org/)\n- Develop against AlertManager\n\n## Dev Environment Setup\n\nYou can either use our default Docker setup or install all dev dependencies\nlocally. For the former you only need Docker installed, for the latter you need\nto set the environment flag `NO_DOCKER` to `true` and have the following\ndependencies installed:\n\n- [Elm](https://guide.elm-lang.org/install.html#install)\n- [Elm-Format](https://github.com/avh4/elm-format) is installed\n\nIn addition for easier development you\ncan [configure](https://guide.elm-lang.org/install.html#configure-your-editor)\nyour editor.\n\n**All submitted elm code must be formatted with `elm-format`**. Install and\nexecute it however works best for you. We recommend having formatting the file\non save, similar to how many developers use `gofmt`.\n\nIf you prefer, there's a make target available to format all elm source files:\n\n```\n# make format\n```\n\n## Elm Resources\n\n- The [Official Elm Guide](https://guide.elm-lang.org/) is a great place to\n  start. Going through the entire guide takes about an hour, and is a good\n  primer to get involved in our codebase. Once you've worked through it, you\n  should be able to start writing your feature with the help of the compiler.\n- Check the [syntax reference](http://elm-lang.org/docs/syntax) when you need a\n  reminder of how the language works.\n- Read up on [how to write elm code](http://elm-lang.org/docs/style-guide).\n- Watch videos from the\n  latest [elm-conf](https://www.youtube.com/channel/UCOpGiN9AkczVjlpGDaBwQrQ)\n- Learn how to use the debugger! Elm comes packaged with an excellent\n  [debugger](http://elm-lang.org/blog/the-perfect-bug-report). We've found this\n  tool to be invaluable in understanding how the app is working as we're\n  debugging behavior.\n\n## Local development workflow\n\nAt the top level of this repo, follow the HA AlertManager instructions. Compile\nthe binary, then run with `goreman`. Add example alerts with the file provided\nin the HA example folder. Then start the development server:\n\n```\n# cd ui/app\n# make dev-server\n```\n\nYour app should be available at `http://localhost:<port>`. Navigate to\n`src/Main.elm`. Any changes to the file system are detected automatically,\ntriggering a recompile of the project.\n\n## Committing changes\n\nBefore you commit changes, please run `make build-all` on the root level\nMakefile.\n"
  },
  {
    "path": "ui/app/Makefile",
    "content": "# Use `=` instead of `:=` expanding variable lazely, not at beginning. Needed as\n# elm files change during execution.\nELM_FILES = $(shell find src -iname *.elm)\nDOCKER_IMG := elm-env\nDOCKER_RUN_CURRENT_USER := docker run --user=$(shell id -u $(USER)):$(shell id -g $(USER))\nDOCKER_CMD := $(DOCKER_RUN_CURRENT_USER) --rm -t -v $(PWD):/app -w /app -e \"ELM_HOME=/app/.elm\" $(DOCKER_IMG)\n# If JUNIT_DIR is set, the tests are executed with the JUnit reporter and the result is stored in the given directory.\nJUNIT_DIR ?=\n\nifeq ($(NO_DOCKER), true)\n  DOCKER_CMD=\nendif\n\nall: script.js test\n\nelm-env:\n\t@(if [ \"$(NO_DOCKER)\" != \"true\" ] ; then \\\n\t\techo \">> building elm-env docker image\"; \\\n\t\tdocker build --platform=linux/amd64 -t $(DOCKER_IMG) ../. > /dev/null; \\\n\tfi; )\n\nformat: elm-env $(ELM_FILES)\n\t@echo \">> format front-end code\"\n\t@$(DOCKER_CMD) elm-format --yes $(ELM_FILES)\n\nreview: src/Data elm-env\n\t@$(DOCKER_CMD) elm-review --fix\n\ntest: src/Data elm-env\n\t@$(DOCKER_CMD) rm -rf elm-stuff/generated-code\n\t@$(DOCKER_CMD) elm-format $(ELM_FILES) --validate\n\t@$(DOCKER_CMD) elm-review\nifneq ($(JUNIT_DIR),)\n\tmkdir -p $(JUNIT_DIR)\n\t@$(DOCKER_CMD) elm-test --report=junit | tee $(JUNIT_DIR)/junit.xml\nelse\n\t@$(DOCKER_CMD) elm-test\nendif\n\ndev-server:\n\telm reactor\n\n# macOS requires mktemp template to be at the end of the filename,\n# however --output flag for elm make must end in .js or .html.\nscript.js: export TEMPFILE := \"$(shell mktemp ./elm-XXXXXXXXXX)\"\nscript.js: export TEMPFILE_JS := \"$(TEMPFILE).js\"\nscript.js: src/Data elm-env format $(ELM_FILES)\n\t@echo \">> building script.js\"\n\t@$(DOCKER_CMD) rm -rf elm-stuff\n\t@$(DOCKER_CMD) elm make src/Main.elm --optimize --output $(TEMPFILE_JS)\n\t@$(DOCKER_CMD) uglifyjs $(TEMPFILE_JS) --compress 'pure_funcs=\"F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9\",pure_getters,keep_fargs=false,unsafe_comps,unsafe' --mangle --output $(@)\n\t@rm -rf $(TEMPFILE_JS)\n\t@rm -rf $(TEMPFILE)\n\nsrc/Data: export TEMPOPENAPI := $(shell mktemp -d ./openapi-XXXXXXXXXX)\nsrc/Data: ../../api/v2/openapi.yaml\n\t-rm -r src/Data\n\t$(DOCKER_RUN_CURRENT_USER) --rm -v ${PWD}/../..:/local openapitools/openapi-generator-cli:v3.3.4 generate \\\n\t\t-i /local/api/v2/openapi.yaml \\\n\t  -g elm \\\n\t\t-o /local/ui/app/$(TEMPOPENAPI)\n\t# We only want data directory & DateTime package.\n\tcp -r $(TEMPOPENAPI)/src/Data src/Data\n\tcp -r $(TEMPOPENAPI)/src/DateTime.elm src/DateTime.elm\n\trm -rf $(TEMPOPENAPI)\n\n\nclean:\n\t- @rm -rf script.js elm-stuff src/Data src/DateTime.elm openapi-* elm-*\n\t- @if [ ! -z \"$(docker images -q $(DOCKER_IMG))\" ]; then \\\n             docker rmi $(DOCKER_IMG); fi\n"
  },
  {
    "path": "ui/app/README.md",
    "content": "# Alertmanager UI\n\nThis is a re-write of the Alertmanager UI in [elm-lang](http://elm-lang.org/).\n\n## Usage\n\n### Filtering on the alerts page\n\nBy default, the alerts page only shows active (not silenced) alerts. Adding a\nquery string containing the following will additionally show silenced alerts.\n\n```\nhttp://alertmanager/#/alerts?silenced=true\n```\n\nIn order to show _only_ silenced alerts, update the query string to hide active alerts.\n```\nhttp://alertmanager/#/alerts?silenced=true&active=false\n```\n\nThe alerts page can also be filtered by the receivers for a page. Receivers are\nconfigured in Alertmanager's yaml configuration file.\n\n```\nhttp://alertmanager/#/alerts?receiver=backend\n```\n\nFiltering based on label matchers is available. They can easily be added and\nmodified through the UI.\n\n```\nhttp://alertmanager/#/alerts?filter=%7Bseverity%3D%22warning%22%2C%20owner%3D%22backend%22%7D\n```\n\nThese filters can be used in conjunction.\n\n### Filtering on the silences page\n\nFiltering based on label matchers is available. They can easily be added and\nmodified through the UI.\n\n```\nhttp://alertmanager/#/silences?filter=%7Bseverity%3D%22warning%22%2C%20owner%3D%22backend%22%7D\n```\n\n### Note on filtering via label matchers\n\nFiltering via label matchers follows the same syntax and semantics as Prometheus.\n\nA properly formatted filter is a set of label matchers joined by accepted\nmatching operators, surrounded by curly braces:\n\n```\n{foo=\"bar\", baz=~\"quu.*\"}\n```\n\nOperators include:\n\n- `=`\n- `!=`\n- `=~`\n- `!~`\n\nSee the official documentation for additional information: https://prometheus.io/docs/querying/basics/#instant-vector-selectors\n"
  },
  {
    "path": "ui/app/elm.json",
    "content": "{\n    \"type\": \"application\",\n    \"source-directories\": [\n        \"src\"\n    ],\n    \"elm-version\": \"0.19.1\",\n    \"dependencies\": {\n        \"direct\": {\n            \"elm/browser\": \"1.0.0\",\n            \"elm/core\": \"1.0.0\",\n            \"elm/html\": \"1.0.0\",\n            \"elm/http\": \"1.0.0\",\n            \"elm/json\": \"1.0.0\",\n            \"elm/parser\": \"1.1.0\",\n            \"elm/regex\": \"1.0.0\",\n            \"elm/time\": \"1.0.0\",\n            \"elm/url\": \"1.0.0\",\n            \"rtfeldman/elm-iso8601-date-strings\": \"1.1.2\",\n            \"NoRedInk/elm-json-decode-pipeline\": \"1.0.0\",\n            \"justinmimbs/time-extra\": \"1.1.0\"\n        },\n        \"indirect\": {\n            \"elm/virtual-dom\": \"1.0.0\",\n            \"elm/random\": \"1.0.0\",\n            \"justinmimbs/date\": \"3.2.0\"\n        }\n    },\n    \"test-dependencies\": {\n        \"direct\": {\n            \"elm-explorations/test\": \"1.0.0\"\n        },\n        \"indirect\": {}\n    }\n}\n"
  },
  {
    "path": "ui/app/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n        <link rel=\"icon\" type=\"image/x-icon\" href=\"favicon.ico\" />\n        <title>Alertmanager</title>\n    </head>\n    <body>\n        <script>\n            // If there is no trailing slash at the end of the path in the url,\n            // add one. This ensures assets like script.js are loaded properly\n            if (location.pathname.substr(-1) != '/') {\n                location.pathname = location.pathname + '/';\n                console.log('added slash');\n            }\n        </script>\n        <script src=\"script.js\"></script>\n        <script>\n            var app = Elm.Main.init({\n                flags: {\n                    production: true,\n                    firstDayOfWeek: JSON.parse(localStorage.getItem('firstDayOfWeek')),\n                    defaultCreator: localStorage.getItem('defaultCreator'),\n                    groupExpandAll: JSON.parse(localStorage.getItem('groupExpandAll'))\n                }\n            });\n            app.ports.persistDefaultCreator.subscribe(function(name) {\n                localStorage.setItem('defaultCreator', name);\n            });\n            app.ports.persistGroupExpandAll.subscribe(function(expanded) {\n                localStorage.setItem('groupExpandAll', JSON.stringify(expanded));\n            });\n            app.ports.persistFirstDayOfWeek.subscribe(function(firstDayOfWeek) {\n                localStorage.setItem('firstDayOfWeek', JSON.stringify(firstDayOfWeek));\n            });\n        </script>\n    </body>\n</html>\n"
  },
  {
    "path": "ui/app/lib/elm-datepicker/css/elm-datepicker.css",
    "content": ".cursor-pointer {cursor: pointer;}\n\n.month {\n    height: 270px;\n}\n\n.calendar_ .date-container {\n}\n\n.calendar_ .weekheader {\n    margin-top: 5px;\n}\n\n.calendar_ .date {\n    color: #C0C0C0;\n    cursor: pointer;\n    height: 30px;\n    width: 40px;\n    display: inline-flex;\n    justify-content: center;\n    align-items: center;\n    font-size: .75rem;\n    background-color: #fff;\n}\n\n.calendar_ .date.thismonth {\n    color: #22292f;\n}\n\n.calendar_ .date.front {\n    background-color: rgba(0,0,0,0);\n}\n\n.calendar_ .date.back {\n    margin-bottom: 5px;\n}\n\n.calendar_ .date.front.mouseover {\n    background-color: #EEEEEE;\n    border-radius: 50%;\n}\n\n.calendar_ .date.front.start {\n    background-color: #b0c4de;\n    border-radius: 50%;\n}\n\n.calendar_ .date.front.end {\n    background-color: #b0c4de;\n    border-radius: 50%;\n}\n\n.calendar_ .date.back.start {\n    background-color: #b0c4de;\n    border-top-left-radius: 50%;\n    border-bottom-left-radius: 50%;\n}\n\n.calendar_ .date.back.end {\n    background-color: #b0c4de;\n    border-top-right-radius: 50%;\n    border-bottom-right-radius: 50%;\n}\n\n.calendar_ .date.back.between {\n    background-color: #b0c4de;\n}\n\n.timepicker {\n    height:60px;\n    width:80%;\n    margin-top: 10px;\n}\n\n.timepicker .subject {\n    padding:10px;\n    width:20%;\n    vertical-align:middle;\n}\n\n.timepicker .hour {\n    width:10%;\n}\n\n.timepicker .minute {\n    width:10%;\n}\n\n.timepicker .view {\n    width:100%;\n    height:50%;\n    text-align:center;\n    border: 0px none;\n}\n\n.timepicker .up-button {\n    width:100%;\n    height:25%;\n    border: 0px none;\n}\n\n.timepicker .down-button {\n    width:100%;\n    height:25%;\n    border: 0px none;\n}\n\n.timepicker .colon {\n    width: 5%;\n}\n\n.timepicker .timeview {\n    width:50%;\n}\n\n.month-header {\n    width:70%;\n    margin: 0 auto;\n}\n\n.month-header .prev-month {\n    width:20%;\n}\n\n.month-header .month-text {\n    width:60%;\n    font-weight: bold;\n    font-size: 12px;\n    text-align: center;\n}\n\n.month-header .next-month {\n    width:20%;\n}\n\n.d-flex-center {\n    display:flex;\n    align-items: center;\n    justify-content: space-around;\n}\n"
  },
  {
    "path": "ui/app/lib/font-awesome-4.7.0/css/font-awesome.css",
    "content": "/*!\n *  Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome\n *  License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)\n */\n/* FONT PATH\n * -------------------------- */\n@font-face {\n  font-family: 'FontAwesome';\n  src: url('../fonts/fontawesome-webfont.eot?v=4.7.0');\n  src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');\n  font-weight: normal;\n  font-style: normal;\n}\n.fa {\n  display: inline-block;\n  font: normal normal normal 14px/1 FontAwesome;\n  font-size: inherit;\n  text-rendering: auto;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n/* makes the font 33% larger relative to the icon container */\n.fa-lg {\n  font-size: 1.33333333em;\n  line-height: 0.75em;\n  vertical-align: -15%;\n}\n.fa-2x {\n  font-size: 2em;\n}\n.fa-3x {\n  font-size: 3em;\n}\n.fa-4x {\n  font-size: 4em;\n}\n.fa-5x {\n  font-size: 5em;\n}\n.fa-fw {\n  width: 1.28571429em;\n  text-align: center;\n}\n.fa-ul {\n  padding-left: 0;\n  margin-left: 2.14285714em;\n  list-style-type: none;\n}\n.fa-ul > li {\n  position: relative;\n}\n.fa-li {\n  position: absolute;\n  left: -2.14285714em;\n  width: 2.14285714em;\n  top: 0.14285714em;\n  text-align: center;\n}\n.fa-li.fa-lg {\n  left: -1.85714286em;\n}\n.fa-border {\n  padding: .2em .25em .15em;\n  border: solid 0.08em #eeeeee;\n  border-radius: .1em;\n}\n.fa-pull-left {\n  float: left;\n}\n.fa-pull-right {\n  float: right;\n}\n.fa.fa-pull-left {\n  margin-right: .3em;\n}\n.fa.fa-pull-right {\n  margin-left: .3em;\n}\n/* Deprecated as of 4.4.0 */\n.pull-right {\n  float: right;\n}\n.pull-left {\n  float: left;\n}\n.fa.pull-left {\n  margin-right: .3em;\n}\n.fa.pull-right {\n  margin-left: .3em;\n}\n.fa-spin {\n  -webkit-animation: fa-spin 2s infinite linear;\n  animation: fa-spin 2s infinite linear;\n}\n.fa-pulse {\n  -webkit-animation: fa-spin 1s infinite steps(8);\n  animation: fa-spin 1s infinite steps(8);\n}\n@-webkit-keyframes fa-spin {\n  0% {\n    -webkit-transform: rotate(0deg);\n    transform: rotate(0deg);\n  }\n  100% {\n    -webkit-transform: rotate(359deg);\n    transform: rotate(359deg);\n  }\n}\n@keyframes fa-spin {\n  0% {\n    -webkit-transform: rotate(0deg);\n    transform: rotate(0deg);\n  }\n  100% {\n    -webkit-transform: rotate(359deg);\n    transform: rotate(359deg);\n  }\n}\n.fa-rotate-90 {\n  -ms-filter: \"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)\";\n  -webkit-transform: rotate(90deg);\n  -ms-transform: rotate(90deg);\n  transform: rotate(90deg);\n}\n.fa-rotate-180 {\n  -ms-filter: \"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)\";\n  -webkit-transform: rotate(180deg);\n  -ms-transform: rotate(180deg);\n  transform: rotate(180deg);\n}\n.fa-rotate-270 {\n  -ms-filter: \"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)\";\n  -webkit-transform: rotate(270deg);\n  -ms-transform: rotate(270deg);\n  transform: rotate(270deg);\n}\n.fa-flip-horizontal {\n  -ms-filter: \"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)\";\n  -webkit-transform: scale(-1, 1);\n  -ms-transform: scale(-1, 1);\n  transform: scale(-1, 1);\n}\n.fa-flip-vertical {\n  -ms-filter: \"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)\";\n  -webkit-transform: scale(1, -1);\n  -ms-transform: scale(1, -1);\n  transform: scale(1, -1);\n}\n:root .fa-rotate-90,\n:root .fa-rotate-180,\n:root .fa-rotate-270,\n:root .fa-flip-horizontal,\n:root .fa-flip-vertical {\n  filter: none;\n}\n.fa-stack {\n  position: relative;\n  display: inline-block;\n  width: 2em;\n  height: 2em;\n  line-height: 2em;\n  vertical-align: middle;\n}\n.fa-stack-1x,\n.fa-stack-2x {\n  position: absolute;\n  left: 0;\n  width: 100%;\n  text-align: center;\n}\n.fa-stack-1x {\n  line-height: inherit;\n}\n.fa-stack-2x {\n  font-size: 2em;\n}\n.fa-inverse {\n  color: #ffffff;\n}\n/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen\n   readers do not read off random characters that represent icons */\n.fa-glass:before {\n  content: \"\\f000\";\n}\n.fa-music:before {\n  content: \"\\f001\";\n}\n.fa-search:before {\n  content: \"\\f002\";\n}\n.fa-envelope-o:before {\n  content: \"\\f003\";\n}\n.fa-heart:before {\n  content: \"\\f004\";\n}\n.fa-star:before {\n  content: \"\\f005\";\n}\n.fa-star-o:before {\n  content: \"\\f006\";\n}\n.fa-user:before {\n  content: \"\\f007\";\n}\n.fa-film:before {\n  content: \"\\f008\";\n}\n.fa-th-large:before {\n  content: \"\\f009\";\n}\n.fa-th:before {\n  content: \"\\f00a\";\n}\n.fa-th-list:before {\n  content: \"\\f00b\";\n}\n.fa-check:before {\n  content: \"\\f00c\";\n}\n.fa-remove:before,\n.fa-close:before,\n.fa-times:before {\n  content: \"\\f00d\";\n}\n.fa-search-plus:before {\n  content: \"\\f00e\";\n}\n.fa-search-minus:before {\n  content: \"\\f010\";\n}\n.fa-power-off:before {\n  content: \"\\f011\";\n}\n.fa-signal:before {\n  content: \"\\f012\";\n}\n.fa-gear:before,\n.fa-cog:before {\n  content: \"\\f013\";\n}\n.fa-trash-o:before {\n  content: \"\\f014\";\n}\n.fa-home:before {\n  content: \"\\f015\";\n}\n.fa-file-o:before {\n  content: \"\\f016\";\n}\n.fa-clock-o:before {\n  content: \"\\f017\";\n}\n.fa-road:before {\n  content: \"\\f018\";\n}\n.fa-download:before {\n  content: \"\\f019\";\n}\n.fa-arrow-circle-o-down:before {\n  content: \"\\f01a\";\n}\n.fa-arrow-circle-o-up:before {\n  content: \"\\f01b\";\n}\n.fa-inbox:before {\n  content: \"\\f01c\";\n}\n.fa-play-circle-o:before {\n  content: \"\\f01d\";\n}\n.fa-rotate-right:before,\n.fa-repeat:before {\n  content: \"\\f01e\";\n}\n.fa-refresh:before {\n  content: \"\\f021\";\n}\n.fa-list-alt:before {\n  content: \"\\f022\";\n}\n.fa-lock:before {\n  content: \"\\f023\";\n}\n.fa-flag:before {\n  content: \"\\f024\";\n}\n.fa-headphones:before {\n  content: \"\\f025\";\n}\n.fa-volume-off:before {\n  content: \"\\f026\";\n}\n.fa-volume-down:before {\n  content: \"\\f027\";\n}\n.fa-volume-up:before {\n  content: \"\\f028\";\n}\n.fa-qrcode:before {\n  content: \"\\f029\";\n}\n.fa-barcode:before {\n  content: \"\\f02a\";\n}\n.fa-tag:before {\n  content: \"\\f02b\";\n}\n.fa-tags:before {\n  content: \"\\f02c\";\n}\n.fa-book:before {\n  content: \"\\f02d\";\n}\n.fa-bookmark:before {\n  content: \"\\f02e\";\n}\n.fa-print:before {\n  content: \"\\f02f\";\n}\n.fa-camera:before {\n  content: \"\\f030\";\n}\n.fa-font:before {\n  content: \"\\f031\";\n}\n.fa-bold:before {\n  content: \"\\f032\";\n}\n.fa-italic:before {\n  content: \"\\f033\";\n}\n.fa-text-height:before {\n  content: \"\\f034\";\n}\n.fa-text-width:before {\n  content: \"\\f035\";\n}\n.fa-align-left:before {\n  content: \"\\f036\";\n}\n.fa-align-center:before {\n  content: \"\\f037\";\n}\n.fa-align-right:before {\n  content: \"\\f038\";\n}\n.fa-align-justify:before {\n  content: \"\\f039\";\n}\n.fa-list:before {\n  content: \"\\f03a\";\n}\n.fa-dedent:before,\n.fa-outdent:before {\n  content: \"\\f03b\";\n}\n.fa-indent:before {\n  content: \"\\f03c\";\n}\n.fa-video-camera:before {\n  content: \"\\f03d\";\n}\n.fa-photo:before,\n.fa-image:before,\n.fa-picture-o:before {\n  content: \"\\f03e\";\n}\n.fa-pencil:before {\n  content: \"\\f040\";\n}\n.fa-map-marker:before {\n  content: \"\\f041\";\n}\n.fa-adjust:before {\n  content: \"\\f042\";\n}\n.fa-tint:before {\n  content: \"\\f043\";\n}\n.fa-edit:before,\n.fa-pencil-square-o:before {\n  content: \"\\f044\";\n}\n.fa-share-square-o:before {\n  content: \"\\f045\";\n}\n.fa-check-square-o:before {\n  content: \"\\f046\";\n}\n.fa-arrows:before {\n  content: \"\\f047\";\n}\n.fa-step-backward:before {\n  content: \"\\f048\";\n}\n.fa-fast-backward:before {\n  content: \"\\f049\";\n}\n.fa-backward:before {\n  content: \"\\f04a\";\n}\n.fa-play:before {\n  content: \"\\f04b\";\n}\n.fa-pause:before {\n  content: \"\\f04c\";\n}\n.fa-stop:before {\n  content: \"\\f04d\";\n}\n.fa-forward:before {\n  content: \"\\f04e\";\n}\n.fa-fast-forward:before {\n  content: \"\\f050\";\n}\n.fa-step-forward:before {\n  content: \"\\f051\";\n}\n.fa-eject:before {\n  content: \"\\f052\";\n}\n.fa-chevron-left:before {\n  content: \"\\f053\";\n}\n.fa-chevron-right:before {\n  content: \"\\f054\";\n}\n.fa-plus-circle:before {\n  content: \"\\f055\";\n}\n.fa-minus-circle:before {\n  content: \"\\f056\";\n}\n.fa-times-circle:before {\n  content: \"\\f057\";\n}\n.fa-check-circle:before {\n  content: \"\\f058\";\n}\n.fa-question-circle:before {\n  content: \"\\f059\";\n}\n.fa-info-circle:before {\n  content: \"\\f05a\";\n}\n.fa-crosshairs:before {\n  content: \"\\f05b\";\n}\n.fa-times-circle-o:before {\n  content: \"\\f05c\";\n}\n.fa-check-circle-o:before {\n  content: \"\\f05d\";\n}\n.fa-ban:before {\n  content: \"\\f05e\";\n}\n.fa-arrow-left:before {\n  content: \"\\f060\";\n}\n.fa-arrow-right:before {\n  content: \"\\f061\";\n}\n.fa-arrow-up:before {\n  content: \"\\f062\";\n}\n.fa-arrow-down:before {\n  content: \"\\f063\";\n}\n.fa-mail-forward:before,\n.fa-share:before {\n  content: \"\\f064\";\n}\n.fa-expand:before {\n  content: \"\\f065\";\n}\n.fa-compress:before {\n  content: \"\\f066\";\n}\n.fa-plus:before {\n  content: \"\\f067\";\n}\n.fa-minus:before {\n  content: \"\\f068\";\n}\n.fa-asterisk:before {\n  content: \"\\f069\";\n}\n.fa-exclamation-circle:before {\n  content: \"\\f06a\";\n}\n.fa-gift:before {\n  content: \"\\f06b\";\n}\n.fa-leaf:before {\n  content: \"\\f06c\";\n}\n.fa-fire:before {\n  content: \"\\f06d\";\n}\n.fa-eye:before {\n  content: \"\\f06e\";\n}\n.fa-eye-slash:before {\n  content: \"\\f070\";\n}\n.fa-warning:before,\n.fa-exclamation-triangle:before {\n  content: \"\\f071\";\n}\n.fa-plane:before {\n  content: \"\\f072\";\n}\n.fa-calendar:before {\n  content: \"\\f073\";\n}\n.fa-random:before {\n  content: \"\\f074\";\n}\n.fa-comment:before {\n  content: \"\\f075\";\n}\n.fa-magnet:before {\n  content: \"\\f076\";\n}\n.fa-chevron-up:before {\n  content: \"\\f077\";\n}\n.fa-chevron-down:before {\n  content: \"\\f078\";\n}\n.fa-retweet:before {\n  content: \"\\f079\";\n}\n.fa-shopping-cart:before {\n  content: \"\\f07a\";\n}\n.fa-folder:before {\n  content: \"\\f07b\";\n}\n.fa-folder-open:before {\n  content: \"\\f07c\";\n}\n.fa-arrows-v:before {\n  content: \"\\f07d\";\n}\n.fa-arrows-h:before {\n  content: \"\\f07e\";\n}\n.fa-bar-chart-o:before,\n.fa-bar-chart:before {\n  content: \"\\f080\";\n}\n.fa-twitter-square:before {\n  content: \"\\f081\";\n}\n.fa-facebook-square:before {\n  content: \"\\f082\";\n}\n.fa-camera-retro:before {\n  content: \"\\f083\";\n}\n.fa-key:before {\n  content: \"\\f084\";\n}\n.fa-gears:before,\n.fa-cogs:before {\n  content: \"\\f085\";\n}\n.fa-comments:before {\n  content: \"\\f086\";\n}\n.fa-thumbs-o-up:before {\n  content: \"\\f087\";\n}\n.fa-thumbs-o-down:before {\n  content: \"\\f088\";\n}\n.fa-star-half:before {\n  content: \"\\f089\";\n}\n.fa-heart-o:before {\n  content: \"\\f08a\";\n}\n.fa-sign-out:before {\n  content: \"\\f08b\";\n}\n.fa-linkedin-square:before {\n  content: \"\\f08c\";\n}\n.fa-thumb-tack:before {\n  content: \"\\f08d\";\n}\n.fa-external-link:before {\n  content: \"\\f08e\";\n}\n.fa-sign-in:before {\n  content: \"\\f090\";\n}\n.fa-trophy:before {\n  content: \"\\f091\";\n}\n.fa-github-square:before {\n  content: \"\\f092\";\n}\n.fa-upload:before {\n  content: \"\\f093\";\n}\n.fa-lemon-o:before {\n  content: \"\\f094\";\n}\n.fa-phone:before {\n  content: \"\\f095\";\n}\n.fa-square-o:before {\n  content: \"\\f096\";\n}\n.fa-bookmark-o:before {\n  content: \"\\f097\";\n}\n.fa-phone-square:before {\n  content: \"\\f098\";\n}\n.fa-twitter:before {\n  content: \"\\f099\";\n}\n.fa-facebook-f:before,\n.fa-facebook:before {\n  content: \"\\f09a\";\n}\n.fa-github:before {\n  content: \"\\f09b\";\n}\n.fa-unlock:before {\n  content: \"\\f09c\";\n}\n.fa-credit-card:before {\n  content: \"\\f09d\";\n}\n.fa-feed:before,\n.fa-rss:before {\n  content: \"\\f09e\";\n}\n.fa-hdd-o:before {\n  content: \"\\f0a0\";\n}\n.fa-bullhorn:before {\n  content: \"\\f0a1\";\n}\n.fa-bell:before {\n  content: \"\\f0f3\";\n}\n.fa-certificate:before {\n  content: \"\\f0a3\";\n}\n.fa-hand-o-right:before {\n  content: \"\\f0a4\";\n}\n.fa-hand-o-left:before {\n  content: \"\\f0a5\";\n}\n.fa-hand-o-up:before {\n  content: \"\\f0a6\";\n}\n.fa-hand-o-down:before {\n  content: \"\\f0a7\";\n}\n.fa-arrow-circle-left:before {\n  content: \"\\f0a8\";\n}\n.fa-arrow-circle-right:before {\n  content: \"\\f0a9\";\n}\n.fa-arrow-circle-up:before {\n  content: \"\\f0aa\";\n}\n.fa-arrow-circle-down:before {\n  content: \"\\f0ab\";\n}\n.fa-globe:before {\n  content: \"\\f0ac\";\n}\n.fa-wrench:before {\n  content: \"\\f0ad\";\n}\n.fa-tasks:before {\n  content: \"\\f0ae\";\n}\n.fa-filter:before {\n  content: \"\\f0b0\";\n}\n.fa-briefcase:before {\n  content: \"\\f0b1\";\n}\n.fa-arrows-alt:before {\n  content: \"\\f0b2\";\n}\n.fa-group:before,\n.fa-users:before {\n  content: \"\\f0c0\";\n}\n.fa-chain:before,\n.fa-link:before {\n  content: \"\\f0c1\";\n}\n.fa-cloud:before {\n  content: \"\\f0c2\";\n}\n.fa-flask:before {\n  content: \"\\f0c3\";\n}\n.fa-cut:before,\n.fa-scissors:before {\n  content: \"\\f0c4\";\n}\n.fa-copy:before,\n.fa-files-o:before {\n  content: \"\\f0c5\";\n}\n.fa-paperclip:before {\n  content: \"\\f0c6\";\n}\n.fa-save:before,\n.fa-floppy-o:before {\n  content: \"\\f0c7\";\n}\n.fa-square:before {\n  content: \"\\f0c8\";\n}\n.fa-navicon:before,\n.fa-reorder:before,\n.fa-bars:before {\n  content: \"\\f0c9\";\n}\n.fa-list-ul:before {\n  content: \"\\f0ca\";\n}\n.fa-list-ol:before {\n  content: \"\\f0cb\";\n}\n.fa-strikethrough:before {\n  content: \"\\f0cc\";\n}\n.fa-underline:before {\n  content: \"\\f0cd\";\n}\n.fa-table:before {\n  content: \"\\f0ce\";\n}\n.fa-magic:before {\n  content: \"\\f0d0\";\n}\n.fa-truck:before {\n  content: \"\\f0d1\";\n}\n.fa-pinterest:before {\n  content: \"\\f0d2\";\n}\n.fa-pinterest-square:before {\n  content: \"\\f0d3\";\n}\n.fa-google-plus-square:before {\n  content: \"\\f0d4\";\n}\n.fa-google-plus:before {\n  content: \"\\f0d5\";\n}\n.fa-money:before {\n  content: \"\\f0d6\";\n}\n.fa-caret-down:before {\n  content: \"\\f0d7\";\n}\n.fa-caret-up:before {\n  content: \"\\f0d8\";\n}\n.fa-caret-left:before {\n  content: \"\\f0d9\";\n}\n.fa-caret-right:before {\n  content: \"\\f0da\";\n}\n.fa-columns:before {\n  content: \"\\f0db\";\n}\n.fa-unsorted:before,\n.fa-sort:before {\n  content: \"\\f0dc\";\n}\n.fa-sort-down:before,\n.fa-sort-desc:before {\n  content: \"\\f0dd\";\n}\n.fa-sort-up:before,\n.fa-sort-asc:before {\n  content: \"\\f0de\";\n}\n.fa-envelope:before {\n  content: \"\\f0e0\";\n}\n.fa-linkedin:before {\n  content: \"\\f0e1\";\n}\n.fa-rotate-left:before,\n.fa-undo:before {\n  content: \"\\f0e2\";\n}\n.fa-legal:before,\n.fa-gavel:before {\n  content: \"\\f0e3\";\n}\n.fa-dashboard:before,\n.fa-tachometer:before {\n  content: \"\\f0e4\";\n}\n.fa-comment-o:before {\n  content: \"\\f0e5\";\n}\n.fa-comments-o:before {\n  content: \"\\f0e6\";\n}\n.fa-flash:before,\n.fa-bolt:before {\n  content: \"\\f0e7\";\n}\n.fa-sitemap:before {\n  content: \"\\f0e8\";\n}\n.fa-umbrella:before {\n  content: \"\\f0e9\";\n}\n.fa-paste:before,\n.fa-clipboard:before {\n  content: \"\\f0ea\";\n}\n.fa-lightbulb-o:before {\n  content: \"\\f0eb\";\n}\n.fa-exchange:before {\n  content: \"\\f0ec\";\n}\n.fa-cloud-download:before {\n  content: \"\\f0ed\";\n}\n.fa-cloud-upload:before {\n  content: \"\\f0ee\";\n}\n.fa-user-md:before {\n  content: \"\\f0f0\";\n}\n.fa-stethoscope:before {\n  content: \"\\f0f1\";\n}\n.fa-suitcase:before {\n  content: \"\\f0f2\";\n}\n.fa-bell-o:before {\n  content: \"\\f0a2\";\n}\n.fa-coffee:before {\n  content: \"\\f0f4\";\n}\n.fa-cutlery:before {\n  content: \"\\f0f5\";\n}\n.fa-file-text-o:before {\n  content: \"\\f0f6\";\n}\n.fa-building-o:before {\n  content: \"\\f0f7\";\n}\n.fa-hospital-o:before {\n  content: \"\\f0f8\";\n}\n.fa-ambulance:before {\n  content: \"\\f0f9\";\n}\n.fa-medkit:before {\n  content: \"\\f0fa\";\n}\n.fa-fighter-jet:before {\n  content: \"\\f0fb\";\n}\n.fa-beer:before {\n  content: \"\\f0fc\";\n}\n.fa-h-square:before {\n  content: \"\\f0fd\";\n}\n.fa-plus-square:before {\n  content: \"\\f0fe\";\n}\n.fa-angle-double-left:before {\n  content: \"\\f100\";\n}\n.fa-angle-double-right:before {\n  content: \"\\f101\";\n}\n.fa-angle-double-up:before {\n  content: \"\\f102\";\n}\n.fa-angle-double-down:before {\n  content: \"\\f103\";\n}\n.fa-angle-left:before {\n  content: \"\\f104\";\n}\n.fa-angle-right:before {\n  content: \"\\f105\";\n}\n.fa-angle-up:before {\n  content: \"\\f106\";\n}\n.fa-angle-down:before {\n  content: \"\\f107\";\n}\n.fa-desktop:before {\n  content: \"\\f108\";\n}\n.fa-laptop:before {\n  content: \"\\f109\";\n}\n.fa-tablet:before {\n  content: \"\\f10a\";\n}\n.fa-mobile-phone:before,\n.fa-mobile:before {\n  content: \"\\f10b\";\n}\n.fa-circle-o:before {\n  content: \"\\f10c\";\n}\n.fa-quote-left:before {\n  content: \"\\f10d\";\n}\n.fa-quote-right:before {\n  content: \"\\f10e\";\n}\n.fa-spinner:before {\n  content: \"\\f110\";\n}\n.fa-circle:before {\n  content: \"\\f111\";\n}\n.fa-mail-reply:before,\n.fa-reply:before {\n  content: \"\\f112\";\n}\n.fa-github-alt:before {\n  content: \"\\f113\";\n}\n.fa-folder-o:before {\n  content: \"\\f114\";\n}\n.fa-folder-open-o:before {\n  content: \"\\f115\";\n}\n.fa-smile-o:before {\n  content: \"\\f118\";\n}\n.fa-frown-o:before {\n  content: \"\\f119\";\n}\n.fa-meh-o:before {\n  content: \"\\f11a\";\n}\n.fa-gamepad:before {\n  content: \"\\f11b\";\n}\n.fa-keyboard-o:before {\n  content: \"\\f11c\";\n}\n.fa-flag-o:before {\n  content: \"\\f11d\";\n}\n.fa-flag-checkered:before {\n  content: \"\\f11e\";\n}\n.fa-terminal:before {\n  content: \"\\f120\";\n}\n.fa-code:before {\n  content: \"\\f121\";\n}\n.fa-mail-reply-all:before,\n.fa-reply-all:before {\n  content: \"\\f122\";\n}\n.fa-star-half-empty:before,\n.fa-star-half-full:before,\n.fa-star-half-o:before {\n  content: \"\\f123\";\n}\n.fa-location-arrow:before {\n  content: \"\\f124\";\n}\n.fa-crop:before {\n  content: \"\\f125\";\n}\n.fa-code-fork:before {\n  content: \"\\f126\";\n}\n.fa-unlink:before,\n.fa-chain-broken:before {\n  content: \"\\f127\";\n}\n.fa-question:before {\n  content: \"\\f128\";\n}\n.fa-info:before {\n  content: \"\\f129\";\n}\n.fa-exclamation:before {\n  content: \"\\f12a\";\n}\n.fa-superscript:before {\n  content: \"\\f12b\";\n}\n.fa-subscript:before {\n  content: \"\\f12c\";\n}\n.fa-eraser:before {\n  content: \"\\f12d\";\n}\n.fa-puzzle-piece:before {\n  content: \"\\f12e\";\n}\n.fa-microphone:before {\n  content: \"\\f130\";\n}\n.fa-microphone-slash:before {\n  content: \"\\f131\";\n}\n.fa-shield:before {\n  content: \"\\f132\";\n}\n.fa-calendar-o:before {\n  content: \"\\f133\";\n}\n.fa-fire-extinguisher:before {\n  content: \"\\f134\";\n}\n.fa-rocket:before {\n  content: \"\\f135\";\n}\n.fa-maxcdn:before {\n  content: \"\\f136\";\n}\n.fa-chevron-circle-left:before {\n  content: \"\\f137\";\n}\n.fa-chevron-circle-right:before {\n  content: \"\\f138\";\n}\n.fa-chevron-circle-up:before {\n  content: \"\\f139\";\n}\n.fa-chevron-circle-down:before {\n  content: \"\\f13a\";\n}\n.fa-html5:before {\n  content: \"\\f13b\";\n}\n.fa-css3:before {\n  content: \"\\f13c\";\n}\n.fa-anchor:before {\n  content: \"\\f13d\";\n}\n.fa-unlock-alt:before {\n  content: \"\\f13e\";\n}\n.fa-bullseye:before {\n  content: \"\\f140\";\n}\n.fa-ellipsis-h:before {\n  content: \"\\f141\";\n}\n.fa-ellipsis-v:before {\n  content: \"\\f142\";\n}\n.fa-rss-square:before {\n  content: \"\\f143\";\n}\n.fa-play-circle:before {\n  content: \"\\f144\";\n}\n.fa-ticket:before {\n  content: \"\\f145\";\n}\n.fa-minus-square:before {\n  content: \"\\f146\";\n}\n.fa-minus-square-o:before {\n  content: \"\\f147\";\n}\n.fa-level-up:before {\n  content: \"\\f148\";\n}\n.fa-level-down:before {\n  content: \"\\f149\";\n}\n.fa-check-square:before {\n  content: \"\\f14a\";\n}\n.fa-pencil-square:before {\n  content: \"\\f14b\";\n}\n.fa-external-link-square:before {\n  content: \"\\f14c\";\n}\n.fa-share-square:before {\n  content: \"\\f14d\";\n}\n.fa-compass:before {\n  content: \"\\f14e\";\n}\n.fa-toggle-down:before,\n.fa-caret-square-o-down:before {\n  content: \"\\f150\";\n}\n.fa-toggle-up:before,\n.fa-caret-square-o-up:before {\n  content: \"\\f151\";\n}\n.fa-toggle-right:before,\n.fa-caret-square-o-right:before {\n  content: \"\\f152\";\n}\n.fa-euro:before,\n.fa-eur:before {\n  content: \"\\f153\";\n}\n.fa-gbp:before {\n  content: \"\\f154\";\n}\n.fa-dollar:before,\n.fa-usd:before {\n  content: \"\\f155\";\n}\n.fa-rupee:before,\n.fa-inr:before {\n  content: \"\\f156\";\n}\n.fa-cny:before,\n.fa-rmb:before,\n.fa-yen:before,\n.fa-jpy:before {\n  content: \"\\f157\";\n}\n.fa-ruble:before,\n.fa-rouble:before,\n.fa-rub:before {\n  content: \"\\f158\";\n}\n.fa-won:before,\n.fa-krw:before {\n  content: \"\\f159\";\n}\n.fa-bitcoin:before,\n.fa-btc:before {\n  content: \"\\f15a\";\n}\n.fa-file:before {\n  content: \"\\f15b\";\n}\n.fa-file-text:before {\n  content: \"\\f15c\";\n}\n.fa-sort-alpha-asc:before {\n  content: \"\\f15d\";\n}\n.fa-sort-alpha-desc:before {\n  content: \"\\f15e\";\n}\n.fa-sort-amount-asc:before {\n  content: \"\\f160\";\n}\n.fa-sort-amount-desc:before {\n  content: \"\\f161\";\n}\n.fa-sort-numeric-asc:before {\n  content: \"\\f162\";\n}\n.fa-sort-numeric-desc:before {\n  content: \"\\f163\";\n}\n.fa-thumbs-up:before {\n  content: \"\\f164\";\n}\n.fa-thumbs-down:before {\n  content: \"\\f165\";\n}\n.fa-youtube-square:before {\n  content: \"\\f166\";\n}\n.fa-youtube:before {\n  content: \"\\f167\";\n}\n.fa-xing:before {\n  content: \"\\f168\";\n}\n.fa-xing-square:before {\n  content: \"\\f169\";\n}\n.fa-youtube-play:before {\n  content: \"\\f16a\";\n}\n.fa-dropbox:before {\n  content: \"\\f16b\";\n}\n.fa-stack-overflow:before {\n  content: \"\\f16c\";\n}\n.fa-instagram:before {\n  content: \"\\f16d\";\n}\n.fa-flickr:before {\n  content: \"\\f16e\";\n}\n.fa-adn:before {\n  content: \"\\f170\";\n}\n.fa-bitbucket:before {\n  content: \"\\f171\";\n}\n.fa-bitbucket-square:before {\n  content: \"\\f172\";\n}\n.fa-tumblr:before {\n  content: \"\\f173\";\n}\n.fa-tumblr-square:before {\n  content: \"\\f174\";\n}\n.fa-long-arrow-down:before {\n  content: \"\\f175\";\n}\n.fa-long-arrow-up:before {\n  content: \"\\f176\";\n}\n.fa-long-arrow-left:before {\n  content: \"\\f177\";\n}\n.fa-long-arrow-right:before {\n  content: \"\\f178\";\n}\n.fa-apple:before {\n  content: \"\\f179\";\n}\n.fa-windows:before {\n  content: \"\\f17a\";\n}\n.fa-android:before {\n  content: \"\\f17b\";\n}\n.fa-linux:before {\n  content: \"\\f17c\";\n}\n.fa-dribbble:before {\n  content: \"\\f17d\";\n}\n.fa-skype:before {\n  content: \"\\f17e\";\n}\n.fa-foursquare:before {\n  content: \"\\f180\";\n}\n.fa-trello:before {\n  content: \"\\f181\";\n}\n.fa-female:before {\n  content: \"\\f182\";\n}\n.fa-male:before {\n  content: \"\\f183\";\n}\n.fa-gittip:before,\n.fa-gratipay:before {\n  content: \"\\f184\";\n}\n.fa-sun-o:before {\n  content: \"\\f185\";\n}\n.fa-moon-o:before {\n  content: \"\\f186\";\n}\n.fa-archive:before {\n  content: \"\\f187\";\n}\n.fa-bug:before {\n  content: \"\\f188\";\n}\n.fa-vk:before {\n  content: \"\\f189\";\n}\n.fa-weibo:before {\n  content: \"\\f18a\";\n}\n.fa-renren:before {\n  content: \"\\f18b\";\n}\n.fa-pagelines:before {\n  content: \"\\f18c\";\n}\n.fa-stack-exchange:before {\n  content: \"\\f18d\";\n}\n.fa-arrow-circle-o-right:before {\n  content: \"\\f18e\";\n}\n.fa-arrow-circle-o-left:before {\n  content: \"\\f190\";\n}\n.fa-toggle-left:before,\n.fa-caret-square-o-left:before {\n  content: \"\\f191\";\n}\n.fa-dot-circle-o:before {\n  content: \"\\f192\";\n}\n.fa-wheelchair:before {\n  content: \"\\f193\";\n}\n.fa-vimeo-square:before {\n  content: \"\\f194\";\n}\n.fa-turkish-lira:before,\n.fa-try:before {\n  content: \"\\f195\";\n}\n.fa-plus-square-o:before {\n  content: \"\\f196\";\n}\n.fa-space-shuttle:before {\n  content: \"\\f197\";\n}\n.fa-slack:before {\n  content: \"\\f198\";\n}\n.fa-envelope-square:before {\n  content: \"\\f199\";\n}\n.fa-wordpress:before {\n  content: \"\\f19a\";\n}\n.fa-openid:before {\n  content: \"\\f19b\";\n}\n.fa-institution:before,\n.fa-bank:before,\n.fa-university:before {\n  content: \"\\f19c\";\n}\n.fa-mortar-board:before,\n.fa-graduation-cap:before {\n  content: \"\\f19d\";\n}\n.fa-yahoo:before {\n  content: \"\\f19e\";\n}\n.fa-google:before {\n  content: \"\\f1a0\";\n}\n.fa-reddit:before {\n  content: \"\\f1a1\";\n}\n.fa-reddit-square:before {\n  content: \"\\f1a2\";\n}\n.fa-stumbleupon-circle:before {\n  content: \"\\f1a3\";\n}\n.fa-stumbleupon:before {\n  content: \"\\f1a4\";\n}\n.fa-delicious:before {\n  content: \"\\f1a5\";\n}\n.fa-digg:before {\n  content: \"\\f1a6\";\n}\n.fa-pied-piper-pp:before {\n  content: \"\\f1a7\";\n}\n.fa-pied-piper-alt:before {\n  content: \"\\f1a8\";\n}\n.fa-drupal:before {\n  content: \"\\f1a9\";\n}\n.fa-joomla:before {\n  content: \"\\f1aa\";\n}\n.fa-language:before {\n  content: \"\\f1ab\";\n}\n.fa-fax:before {\n  content: \"\\f1ac\";\n}\n.fa-building:before {\n  content: \"\\f1ad\";\n}\n.fa-child:before {\n  content: \"\\f1ae\";\n}\n.fa-paw:before {\n  content: \"\\f1b0\";\n}\n.fa-spoon:before {\n  content: \"\\f1b1\";\n}\n.fa-cube:before {\n  content: \"\\f1b2\";\n}\n.fa-cubes:before {\n  content: \"\\f1b3\";\n}\n.fa-behance:before {\n  content: \"\\f1b4\";\n}\n.fa-behance-square:before {\n  content: \"\\f1b5\";\n}\n.fa-steam:before {\n  content: \"\\f1b6\";\n}\n.fa-steam-square:before {\n  content: \"\\f1b7\";\n}\n.fa-recycle:before {\n  content: \"\\f1b8\";\n}\n.fa-automobile:before,\n.fa-car:before {\n  content: \"\\f1b9\";\n}\n.fa-cab:before,\n.fa-taxi:before {\n  content: \"\\f1ba\";\n}\n.fa-tree:before {\n  content: \"\\f1bb\";\n}\n.fa-spotify:before {\n  content: \"\\f1bc\";\n}\n.fa-deviantart:before {\n  content: \"\\f1bd\";\n}\n.fa-soundcloud:before {\n  content: \"\\f1be\";\n}\n.fa-database:before {\n  content: \"\\f1c0\";\n}\n.fa-file-pdf-o:before {\n  content: \"\\f1c1\";\n}\n.fa-file-word-o:before {\n  content: \"\\f1c2\";\n}\n.fa-file-excel-o:before {\n  content: \"\\f1c3\";\n}\n.fa-file-powerpoint-o:before {\n  content: \"\\f1c4\";\n}\n.fa-file-photo-o:before,\n.fa-file-picture-o:before,\n.fa-file-image-o:before {\n  content: \"\\f1c5\";\n}\n.fa-file-zip-o:before,\n.fa-file-archive-o:before {\n  content: \"\\f1c6\";\n}\n.fa-file-sound-o:before,\n.fa-file-audio-o:before {\n  content: \"\\f1c7\";\n}\n.fa-file-movie-o:before,\n.fa-file-video-o:before {\n  content: \"\\f1c8\";\n}\n.fa-file-code-o:before {\n  content: \"\\f1c9\";\n}\n.fa-vine:before {\n  content: \"\\f1ca\";\n}\n.fa-codepen:before {\n  content: \"\\f1cb\";\n}\n.fa-jsfiddle:before {\n  content: \"\\f1cc\";\n}\n.fa-life-bouy:before,\n.fa-life-buoy:before,\n.fa-life-saver:before,\n.fa-support:before,\n.fa-life-ring:before {\n  content: \"\\f1cd\";\n}\n.fa-circle-o-notch:before {\n  content: \"\\f1ce\";\n}\n.fa-ra:before,\n.fa-resistance:before,\n.fa-rebel:before {\n  content: \"\\f1d0\";\n}\n.fa-ge:before,\n.fa-empire:before {\n  content: \"\\f1d1\";\n}\n.fa-git-square:before {\n  content: \"\\f1d2\";\n}\n.fa-git:before {\n  content: \"\\f1d3\";\n}\n.fa-y-combinator-square:before,\n.fa-yc-square:before,\n.fa-hacker-news:before {\n  content: \"\\f1d4\";\n}\n.fa-tencent-weibo:before {\n  content: \"\\f1d5\";\n}\n.fa-qq:before {\n  content: \"\\f1d6\";\n}\n.fa-wechat:before,\n.fa-weixin:before {\n  content: \"\\f1d7\";\n}\n.fa-send:before,\n.fa-paper-plane:before {\n  content: \"\\f1d8\";\n}\n.fa-send-o:before,\n.fa-paper-plane-o:before {\n  content: \"\\f1d9\";\n}\n.fa-history:before {\n  content: \"\\f1da\";\n}\n.fa-circle-thin:before {\n  content: \"\\f1db\";\n}\n.fa-header:before {\n  content: \"\\f1dc\";\n}\n.fa-paragraph:before {\n  content: \"\\f1dd\";\n}\n.fa-sliders:before {\n  content: \"\\f1de\";\n}\n.fa-share-alt:before {\n  content: \"\\f1e0\";\n}\n.fa-share-alt-square:before {\n  content: \"\\f1e1\";\n}\n.fa-bomb:before {\n  content: \"\\f1e2\";\n}\n.fa-soccer-ball-o:before,\n.fa-futbol-o:before {\n  content: \"\\f1e3\";\n}\n.fa-tty:before {\n  content: \"\\f1e4\";\n}\n.fa-binoculars:before {\n  content: \"\\f1e5\";\n}\n.fa-plug:before {\n  content: \"\\f1e6\";\n}\n.fa-slideshare:before {\n  content: \"\\f1e7\";\n}\n.fa-twitch:before {\n  content: \"\\f1e8\";\n}\n.fa-yelp:before {\n  content: \"\\f1e9\";\n}\n.fa-newspaper-o:before {\n  content: \"\\f1ea\";\n}\n.fa-wifi:before {\n  content: \"\\f1eb\";\n}\n.fa-calculator:before {\n  content: \"\\f1ec\";\n}\n.fa-paypal:before {\n  content: \"\\f1ed\";\n}\n.fa-google-wallet:before {\n  content: \"\\f1ee\";\n}\n.fa-cc-visa:before {\n  content: \"\\f1f0\";\n}\n.fa-cc-mastercard:before {\n  content: \"\\f1f1\";\n}\n.fa-cc-discover:before {\n  content: \"\\f1f2\";\n}\n.fa-cc-amex:before {\n  content: \"\\f1f3\";\n}\n.fa-cc-paypal:before {\n  content: \"\\f1f4\";\n}\n.fa-cc-stripe:before {\n  content: \"\\f1f5\";\n}\n.fa-bell-slash:before {\n  content: \"\\f1f6\";\n}\n.fa-bell-slash-o:before {\n  content: \"\\f1f7\";\n}\n.fa-trash:before {\n  content: \"\\f1f8\";\n}\n.fa-copyright:before {\n  content: \"\\f1f9\";\n}\n.fa-at:before {\n  content: \"\\f1fa\";\n}\n.fa-eyedropper:before {\n  content: \"\\f1fb\";\n}\n.fa-paint-brush:before {\n  content: \"\\f1fc\";\n}\n.fa-birthday-cake:before {\n  content: \"\\f1fd\";\n}\n.fa-area-chart:before {\n  content: \"\\f1fe\";\n}\n.fa-pie-chart:before {\n  content: \"\\f200\";\n}\n.fa-line-chart:before {\n  content: \"\\f201\";\n}\n.fa-lastfm:before {\n  content: \"\\f202\";\n}\n.fa-lastfm-square:before {\n  content: \"\\f203\";\n}\n.fa-toggle-off:before {\n  content: \"\\f204\";\n}\n.fa-toggle-on:before {\n  content: \"\\f205\";\n}\n.fa-bicycle:before {\n  content: \"\\f206\";\n}\n.fa-bus:before {\n  content: \"\\f207\";\n}\n.fa-ioxhost:before {\n  content: \"\\f208\";\n}\n.fa-angellist:before {\n  content: \"\\f209\";\n}\n.fa-cc:before {\n  content: \"\\f20a\";\n}\n.fa-shekel:before,\n.fa-sheqel:before,\n.fa-ils:before {\n  content: \"\\f20b\";\n}\n.fa-meanpath:before {\n  content: \"\\f20c\";\n}\n.fa-buysellads:before {\n  content: \"\\f20d\";\n}\n.fa-connectdevelop:before {\n  content: \"\\f20e\";\n}\n.fa-dashcube:before {\n  content: \"\\f210\";\n}\n.fa-forumbee:before {\n  content: \"\\f211\";\n}\n.fa-leanpub:before {\n  content: \"\\f212\";\n}\n.fa-sellsy:before {\n  content: \"\\f213\";\n}\n.fa-shirtsinbulk:before {\n  content: \"\\f214\";\n}\n.fa-simplybuilt:before {\n  content: \"\\f215\";\n}\n.fa-skyatlas:before {\n  content: \"\\f216\";\n}\n.fa-cart-plus:before {\n  content: \"\\f217\";\n}\n.fa-cart-arrow-down:before {\n  content: \"\\f218\";\n}\n.fa-diamond:before {\n  content: \"\\f219\";\n}\n.fa-ship:before {\n  content: \"\\f21a\";\n}\n.fa-user-secret:before {\n  content: \"\\f21b\";\n}\n.fa-motorcycle:before {\n  content: \"\\f21c\";\n}\n.fa-street-view:before {\n  content: \"\\f21d\";\n}\n.fa-heartbeat:before {\n  content: \"\\f21e\";\n}\n.fa-venus:before {\n  content: \"\\f221\";\n}\n.fa-mars:before {\n  content: \"\\f222\";\n}\n.fa-mercury:before {\n  content: \"\\f223\";\n}\n.fa-intersex:before,\n.fa-transgender:before {\n  content: \"\\f224\";\n}\n.fa-transgender-alt:before {\n  content: \"\\f225\";\n}\n.fa-venus-double:before {\n  content: \"\\f226\";\n}\n.fa-mars-double:before {\n  content: \"\\f227\";\n}\n.fa-venus-mars:before {\n  content: \"\\f228\";\n}\n.fa-mars-stroke:before {\n  content: \"\\f229\";\n}\n.fa-mars-stroke-v:before {\n  content: \"\\f22a\";\n}\n.fa-mars-stroke-h:before {\n  content: \"\\f22b\";\n}\n.fa-neuter:before {\n  content: \"\\f22c\";\n}\n.fa-genderless:before {\n  content: \"\\f22d\";\n}\n.fa-facebook-official:before {\n  content: \"\\f230\";\n}\n.fa-pinterest-p:before {\n  content: \"\\f231\";\n}\n.fa-whatsapp:before {\n  content: \"\\f232\";\n}\n.fa-server:before {\n  content: \"\\f233\";\n}\n.fa-user-plus:before {\n  content: \"\\f234\";\n}\n.fa-user-times:before {\n  content: \"\\f235\";\n}\n.fa-hotel:before,\n.fa-bed:before {\n  content: \"\\f236\";\n}\n.fa-viacoin:before {\n  content: \"\\f237\";\n}\n.fa-train:before {\n  content: \"\\f238\";\n}\n.fa-subway:before {\n  content: \"\\f239\";\n}\n.fa-medium:before {\n  content: \"\\f23a\";\n}\n.fa-yc:before,\n.fa-y-combinator:before {\n  content: \"\\f23b\";\n}\n.fa-optin-monster:before {\n  content: \"\\f23c\";\n}\n.fa-opencart:before {\n  content: \"\\f23d\";\n}\n.fa-expeditedssl:before {\n  content: \"\\f23e\";\n}\n.fa-battery-4:before,\n.fa-battery:before,\n.fa-battery-full:before {\n  content: \"\\f240\";\n}\n.fa-battery-3:before,\n.fa-battery-three-quarters:before {\n  content: \"\\f241\";\n}\n.fa-battery-2:before,\n.fa-battery-half:before {\n  content: \"\\f242\";\n}\n.fa-battery-1:before,\n.fa-battery-quarter:before {\n  content: \"\\f243\";\n}\n.fa-battery-0:before,\n.fa-battery-empty:before {\n  content: \"\\f244\";\n}\n.fa-mouse-pointer:before {\n  content: \"\\f245\";\n}\n.fa-i-cursor:before {\n  content: \"\\f246\";\n}\n.fa-object-group:before {\n  content: \"\\f247\";\n}\n.fa-object-ungroup:before {\n  content: \"\\f248\";\n}\n.fa-sticky-note:before {\n  content: \"\\f249\";\n}\n.fa-sticky-note-o:before {\n  content: \"\\f24a\";\n}\n.fa-cc-jcb:before {\n  content: \"\\f24b\";\n}\n.fa-cc-diners-club:before {\n  content: \"\\f24c\";\n}\n.fa-clone:before {\n  content: \"\\f24d\";\n}\n.fa-balance-scale:before {\n  content: \"\\f24e\";\n}\n.fa-hourglass-o:before {\n  content: \"\\f250\";\n}\n.fa-hourglass-1:before,\n.fa-hourglass-start:before {\n  content: \"\\f251\";\n}\n.fa-hourglass-2:before,\n.fa-hourglass-half:before {\n  content: \"\\f252\";\n}\n.fa-hourglass-3:before,\n.fa-hourglass-end:before {\n  content: \"\\f253\";\n}\n.fa-hourglass:before {\n  content: \"\\f254\";\n}\n.fa-hand-grab-o:before,\n.fa-hand-rock-o:before {\n  content: \"\\f255\";\n}\n.fa-hand-stop-o:before,\n.fa-hand-paper-o:before {\n  content: \"\\f256\";\n}\n.fa-hand-scissors-o:before {\n  content: \"\\f257\";\n}\n.fa-hand-lizard-o:before {\n  content: \"\\f258\";\n}\n.fa-hand-spock-o:before {\n  content: \"\\f259\";\n}\n.fa-hand-pointer-o:before {\n  content: \"\\f25a\";\n}\n.fa-hand-peace-o:before {\n  content: \"\\f25b\";\n}\n.fa-trademark:before {\n  content: \"\\f25c\";\n}\n.fa-registered:before {\n  content: \"\\f25d\";\n}\n.fa-creative-commons:before {\n  content: \"\\f25e\";\n}\n.fa-gg:before {\n  content: \"\\f260\";\n}\n.fa-gg-circle:before {\n  content: \"\\f261\";\n}\n.fa-tripadvisor:before {\n  content: \"\\f262\";\n}\n.fa-odnoklassniki:before {\n  content: \"\\f263\";\n}\n.fa-odnoklassniki-square:before {\n  content: \"\\f264\";\n}\n.fa-get-pocket:before {\n  content: \"\\f265\";\n}\n.fa-wikipedia-w:before {\n  content: \"\\f266\";\n}\n.fa-safari:before {\n  content: \"\\f267\";\n}\n.fa-chrome:before {\n  content: \"\\f268\";\n}\n.fa-firefox:before {\n  content: \"\\f269\";\n}\n.fa-opera:before {\n  content: \"\\f26a\";\n}\n.fa-internet-explorer:before {\n  content: \"\\f26b\";\n}\n.fa-tv:before,\n.fa-television:before {\n  content: \"\\f26c\";\n}\n.fa-contao:before {\n  content: \"\\f26d\";\n}\n.fa-500px:before {\n  content: \"\\f26e\";\n}\n.fa-amazon:before {\n  content: \"\\f270\";\n}\n.fa-calendar-plus-o:before {\n  content: \"\\f271\";\n}\n.fa-calendar-minus-o:before {\n  content: \"\\f272\";\n}\n.fa-calendar-times-o:before {\n  content: \"\\f273\";\n}\n.fa-calendar-check-o:before {\n  content: \"\\f274\";\n}\n.fa-industry:before {\n  content: \"\\f275\";\n}\n.fa-map-pin:before {\n  content: \"\\f276\";\n}\n.fa-map-signs:before {\n  content: \"\\f277\";\n}\n.fa-map-o:before {\n  content: \"\\f278\";\n}\n.fa-map:before {\n  content: \"\\f279\";\n}\n.fa-commenting:before {\n  content: \"\\f27a\";\n}\n.fa-commenting-o:before {\n  content: \"\\f27b\";\n}\n.fa-houzz:before {\n  content: \"\\f27c\";\n}\n.fa-vimeo:before {\n  content: \"\\f27d\";\n}\n.fa-black-tie:before {\n  content: \"\\f27e\";\n}\n.fa-fonticons:before {\n  content: \"\\f280\";\n}\n.fa-reddit-alien:before {\n  content: \"\\f281\";\n}\n.fa-edge:before {\n  content: \"\\f282\";\n}\n.fa-credit-card-alt:before {\n  content: \"\\f283\";\n}\n.fa-codiepie:before {\n  content: \"\\f284\";\n}\n.fa-modx:before {\n  content: \"\\f285\";\n}\n.fa-fort-awesome:before {\n  content: \"\\f286\";\n}\n.fa-usb:before {\n  content: \"\\f287\";\n}\n.fa-product-hunt:before {\n  content: \"\\f288\";\n}\n.fa-mixcloud:before {\n  content: \"\\f289\";\n}\n.fa-scribd:before {\n  content: \"\\f28a\";\n}\n.fa-pause-circle:before {\n  content: \"\\f28b\";\n}\n.fa-pause-circle-o:before {\n  content: \"\\f28c\";\n}\n.fa-stop-circle:before {\n  content: \"\\f28d\";\n}\n.fa-stop-circle-o:before {\n  content: \"\\f28e\";\n}\n.fa-shopping-bag:before {\n  content: \"\\f290\";\n}\n.fa-shopping-basket:before {\n  content: \"\\f291\";\n}\n.fa-hashtag:before {\n  content: \"\\f292\";\n}\n.fa-bluetooth:before {\n  content: \"\\f293\";\n}\n.fa-bluetooth-b:before {\n  content: \"\\f294\";\n}\n.fa-percent:before {\n  content: \"\\f295\";\n}\n.fa-gitlab:before {\n  content: \"\\f296\";\n}\n.fa-wpbeginner:before {\n  content: \"\\f297\";\n}\n.fa-wpforms:before {\n  content: \"\\f298\";\n}\n.fa-envira:before {\n  content: \"\\f299\";\n}\n.fa-universal-access:before {\n  content: \"\\f29a\";\n}\n.fa-wheelchair-alt:before {\n  content: \"\\f29b\";\n}\n.fa-question-circle-o:before {\n  content: \"\\f29c\";\n}\n.fa-blind:before {\n  content: \"\\f29d\";\n}\n.fa-audio-description:before {\n  content: \"\\f29e\";\n}\n.fa-volume-control-phone:before {\n  content: \"\\f2a0\";\n}\n.fa-braille:before {\n  content: \"\\f2a1\";\n}\n.fa-assistive-listening-systems:before {\n  content: \"\\f2a2\";\n}\n.fa-asl-interpreting:before,\n.fa-american-sign-language-interpreting:before {\n  content: \"\\f2a3\";\n}\n.fa-deafness:before,\n.fa-hard-of-hearing:before,\n.fa-deaf:before {\n  content: \"\\f2a4\";\n}\n.fa-glide:before {\n  content: \"\\f2a5\";\n}\n.fa-glide-g:before {\n  content: \"\\f2a6\";\n}\n.fa-signing:before,\n.fa-sign-language:before {\n  content: \"\\f2a7\";\n}\n.fa-low-vision:before {\n  content: \"\\f2a8\";\n}\n.fa-viadeo:before {\n  content: \"\\f2a9\";\n}\n.fa-viadeo-square:before {\n  content: \"\\f2aa\";\n}\n.fa-snapchat:before {\n  content: \"\\f2ab\";\n}\n.fa-snapchat-ghost:before {\n  content: \"\\f2ac\";\n}\n.fa-snapchat-square:before {\n  content: \"\\f2ad\";\n}\n.fa-pied-piper:before {\n  content: \"\\f2ae\";\n}\n.fa-first-order:before {\n  content: \"\\f2b0\";\n}\n.fa-yoast:before {\n  content: \"\\f2b1\";\n}\n.fa-themeisle:before {\n  content: \"\\f2b2\";\n}\n.fa-google-plus-circle:before,\n.fa-google-plus-official:before {\n  content: \"\\f2b3\";\n}\n.fa-fa:before,\n.fa-font-awesome:before {\n  content: \"\\f2b4\";\n}\n.fa-handshake-o:before {\n  content: \"\\f2b5\";\n}\n.fa-envelope-open:before {\n  content: \"\\f2b6\";\n}\n.fa-envelope-open-o:before {\n  content: \"\\f2b7\";\n}\n.fa-linode:before {\n  content: \"\\f2b8\";\n}\n.fa-address-book:before {\n  content: \"\\f2b9\";\n}\n.fa-address-book-o:before {\n  content: \"\\f2ba\";\n}\n.fa-vcard:before,\n.fa-address-card:before {\n  content: \"\\f2bb\";\n}\n.fa-vcard-o:before,\n.fa-address-card-o:before {\n  content: \"\\f2bc\";\n}\n.fa-user-circle:before {\n  content: \"\\f2bd\";\n}\n.fa-user-circle-o:before {\n  content: \"\\f2be\";\n}\n.fa-user-o:before {\n  content: \"\\f2c0\";\n}\n.fa-id-badge:before {\n  content: \"\\f2c1\";\n}\n.fa-drivers-license:before,\n.fa-id-card:before {\n  content: \"\\f2c2\";\n}\n.fa-drivers-license-o:before,\n.fa-id-card-o:before {\n  content: \"\\f2c3\";\n}\n.fa-quora:before {\n  content: \"\\f2c4\";\n}\n.fa-free-code-camp:before {\n  content: \"\\f2c5\";\n}\n.fa-telegram:before {\n  content: \"\\f2c6\";\n}\n.fa-thermometer-4:before,\n.fa-thermometer:before,\n.fa-thermometer-full:before {\n  content: \"\\f2c7\";\n}\n.fa-thermometer-3:before,\n.fa-thermometer-three-quarters:before {\n  content: \"\\f2c8\";\n}\n.fa-thermometer-2:before,\n.fa-thermometer-half:before {\n  content: \"\\f2c9\";\n}\n.fa-thermometer-1:before,\n.fa-thermometer-quarter:before {\n  content: \"\\f2ca\";\n}\n.fa-thermometer-0:before,\n.fa-thermometer-empty:before {\n  content: \"\\f2cb\";\n}\n.fa-shower:before {\n  content: \"\\f2cc\";\n}\n.fa-bathtub:before,\n.fa-s15:before,\n.fa-bath:before {\n  content: \"\\f2cd\";\n}\n.fa-podcast:before {\n  content: \"\\f2ce\";\n}\n.fa-window-maximize:before {\n  content: \"\\f2d0\";\n}\n.fa-window-minimize:before {\n  content: \"\\f2d1\";\n}\n.fa-window-restore:before {\n  content: \"\\f2d2\";\n}\n.fa-times-rectangle:before,\n.fa-window-close:before {\n  content: \"\\f2d3\";\n}\n.fa-times-rectangle-o:before,\n.fa-window-close-o:before {\n  content: \"\\f2d4\";\n}\n.fa-bandcamp:before {\n  content: \"\\f2d5\";\n}\n.fa-grav:before {\n  content: \"\\f2d6\";\n}\n.fa-etsy:before {\n  content: \"\\f2d7\";\n}\n.fa-imdb:before {\n  content: \"\\f2d8\";\n}\n.fa-ravelry:before {\n  content: \"\\f2d9\";\n}\n.fa-eercast:before {\n  content: \"\\f2da\";\n}\n.fa-microchip:before {\n  content: \"\\f2db\";\n}\n.fa-snowflake-o:before {\n  content: \"\\f2dc\";\n}\n.fa-superpowers:before {\n  content: \"\\f2dd\";\n}\n.fa-wpexplorer:before {\n  content: \"\\f2de\";\n}\n.fa-meetup:before {\n  content: \"\\f2e0\";\n}\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n  position: static;\n  width: auto;\n  height: auto;\n  margin: 0;\n  overflow: visible;\n  clip: auto;\n}\n"
  },
  {
    "path": "ui/app/review/elm.json",
    "content": "{\n    \"type\": \"application\",\n    \"source-directories\": [\n        \"src\"\n    ],\n    \"elm-version\": \"0.19.1\",\n    \"dependencies\": {\n        \"direct\": {\n            \"elm/core\": \"1.0.5\",\n            \"elm/json\": \"1.1.3\",\n            \"elm/project-metadata-utils\": \"1.0.1\",\n            \"jfmengels/elm-review\": \"2.4.1\",\n            \"jfmengels/elm-review-simplify\": \"1.0.1\",\n            \"jfmengels/elm-review-unused\": \"1.1.9\",\n            \"stil4m/elm-syntax\": \"7.2.2\"\n        },\n        \"indirect\": {\n            \"elm/html\": \"1.0.0\",\n            \"elm/parser\": \"1.1.0\",\n            \"elm/random\": \"1.0.0\",\n            \"elm/time\": \"1.0.0\",\n            \"elm/virtual-dom\": \"1.0.2\",\n            \"elm-community/list-extra\": \"8.3.0\",\n            \"elm-explorations/test\": \"1.2.2\",\n            \"rtfeldman/elm-hex\": \"1.0.0\",\n            \"stil4m/structured-writer\": \"1.0.3\"\n        }\n    },\n    \"test-dependencies\": {\n        \"direct\": {\n            \"elm-explorations/test\": \"1.2.2\"\n        },\n        \"indirect\": {}\n    }\n}\n"
  },
  {
    "path": "ui/app/review/src/ReviewConfig.elm",
    "content": "module ReviewConfig exposing (config)\n\n{-| Do not rename the ReviewConfig module or the config function, because\n`elm-review` will look for these.\n\nTo add packages that contain rules, add them to this review project using\n\n    `elm install author/packagename`\n\nwhen inside the directory containing this file.\n\n-}\n\nimport NoUnused.CustomTypeConstructorArgs\nimport NoUnused.CustomTypeConstructors\nimport NoUnused.Dependencies\nimport NoUnused.Exports\nimport NoUnused.Modules\nimport NoUnused.Parameters\nimport NoUnused.Patterns\nimport NoUnused.Variables\nimport Simplify\nimport Review.Rule exposing (Rule)\n\n\nconfig : List Rule\nconfig =\n    List.map\n        (Review.Rule.ignoreErrorsForDirectories [ \"src/Data/\" ])\n        [ NoUnused.CustomTypeConstructors.rule []\n        , NoUnused.CustomTypeConstructorArgs.rule\n        , NoUnused.Dependencies.rule\n        , NoUnused.Exports.rule\n        , NoUnused.Modules.rule\n        , NoUnused.Parameters.rule\n        , NoUnused.Patterns.rule\n        , NoUnused.Variables.rule\n        , Simplify.rule\n        ]\n"
  },
  {
    "path": "ui/app/script.js",
    "content": "!function(n){\"use strict\";function r(n,r,t){return t.a=n,t.f=r,t}function l(t){return r(2,t,function(r){return function(n){return t(r,n)}})}function d(e){return r(3,e,function(t){return function(r){return function(n){return e(t,r,n)}}})}function t(u){return r(4,u,function(e){return function(t){return function(r){return function(n){return u(e,t,r,n)}}}})}function c(a){return r(5,a,function(u){return function(e){return function(t){return function(r){return function(n){return a(u,e,t,r,n)}}}}})}function e(c){return r(6,c,function(a){return function(u){return function(e){return function(t){return function(r){return function(n){return c(a,u,e,t,r,n)}}}}}})}function u(o){return r(7,o,function(c){return function(a){return function(u){return function(e){return function(t){return function(r){return function(n){return o(c,a,u,e,t,r,n)}}}}}}})}function a(i){return r(8,i,function(o){return function(c){return function(a){return function(u){return function(e){return function(t){return function(r){return function(n){return i(o,c,a,u,e,t,r,n)}}}}}}}})}function o(f){return r(9,f,function(i){return function(o){return function(c){return function(a){return function(u){return function(e){return function(t){return function(r){return function(n){return f(i,o,c,a,u,e,t,r,n)}}}}}}}}})}function w(n,r,t){return 2===n.a?n.f(r,t):n(r)(t)}function y(n,r,t,e){return 3===n.a?n.f(r,t,e):n(r)(t)(e)}function x(n,r,t,e,u){return 4===n.a?n.f(r,t,e,u):n(r)(t)(e)(u)}function v(n,r,t,e,u,a){return 5===n.a?n.f(r,t,e,u,a):n(r)(t)(e)(u)(a)}function f(n,r,t,e,u,a,c){return 6===n.a?n.f(r,t,e,u,a,c):n(r)(t)(e)(u)(a)(c)}function b(n,r,t,e,u,a,c,o){return 7===n.a?n.f(r,t,e,u,a,c,o):n(r)(t)(e)(u)(a)(c)(o)}function s(n,r,t,e,u,a,c,o,i){return 8===n.a?n.f(r,t,e,u,a,c,o,i):n(r)(t)(e)(u)(a)(c)(o)(i)}var i=d(function(n,r,t){for(var e=Array(n),u=0;u<n;u++)e[u]=t(r+u);return e}),$=l(function(n,r){for(var t=Array(n),e=0;e<n&&r.b;e++)t[e]=r.a,r=r.b;return t.length=e,_(t,r)}),m=(l(function(n,r){return r[n]}),d(function(n,r,t){for(var e=t.length,u=Array(e),a=0;a<e;a++)u[a]=t[a];return u[n]=r,u}),l(function(n,r){for(var t=r.length,e=Array(t+1),u=0;u<t;u++)e[u]=r[u];return e[t]=n,e}),d(function(n,r,t){for(var e=t.length,u=0;u<e;u++)r=w(n,t[u],r);return r}),d(function(n,r,t){for(var e=t.length-1;0<=e;e--)r=w(n,t[e],r);return r}));l(function(n,r){for(var t=r.length,e=Array(t),u=0;u<t;u++)e[u]=n(r[u]);return e}),d(function(n,r,t){for(var e=t.length,u=Array(e),a=0;a<e;a++)u[a]=w(n,r+a,t[a]);return u}),d(function(n,r,t){return t.slice(n,r)}),d(function(n,r,t){for(var e=r.length,u=n-e,a=Array(e+(u=t.length<u?t.length:u)),c=0;c<e;c++)a[c]=r[c];for(c=0;c<u;c++)a[c+e]=t[c];return a}),l(function(n,r){return r}),l(function(n,r){return console.log(n+\": \"+h()),r});function h(){return\"<internals>\"}function p(n){throw Error(\"https://github.com/elm/core/blob/1.0.0/hints/\"+n+\".md\")}function g(n,r){for(var t,e=[],u=k(n,r,0,e);u&&(t=e.pop());u=k(t.a,t.b,0,e));return u}function k(n,r,t,e){if(100<t)return e.push(_(n,r)),!0;if(n===r)return!0;if(\"object\"!=typeof n||null===n||null===r)return\"function\"==typeof n&&p(5),!1;for(var u in n.$<0&&(n=bt(n),r=bt(r)),n)if(!k(n[u],r[u],t+1,e))return!1;return!0}var A=l(g),S=l(function(n,r){return!g(n,r)});function T(n,r,t){if(\"object\"!=typeof n)return n===r?0:n<r?-1:1;if(!n.$)return(t=T(n.a,r.a))||(t=T(n.b,r.b))?t:T(n.c,r.c);for(;n.b&&r.b&&!(t=T(n.a,r.a));n=n.b,r=r.b);return t||(n.b?1:r.b?-1:0)}var E=l(function(n,r){return T(n,r)<0}),Z=(l(function(n,r){return T(n,r)<1}),l(function(n,r){return 0<T(n,r)}),l(function(n,r){return 0<=T(n,r)})),j=l(function(n,r){r=T(n,r);return r<0?vt:r?dt:lt}),C=0;function _(n,r){return{a:n,b:r}}function J(n,r,t){return{a:n,b:r,c:t}}function L(n,r){var t,e={};for(t in n)e[t]=n[t];for(t in r)e[t]=r[t];return e}var N=l(X);function X(n,r){if(\"string\"==typeof n)return n+r;if(!n.b)return r;var t=I(n.a,r);n=n.b;for(var e=t;n.b;n=n.b)e=e.b=I(n.a,r);return t}var H={$:0};function I(n,r){return{$:1,a:n,b:r}}var O=l(I);function R(n){for(var r=H,t=n.length;t--;)r=I(n[t],r);return r}function V(n){for(var r=[];n.b;n=n.b)r.push(n.a);return r}var z=d(function(n,r,t){for(var e=[];r.b&&t.b;r=r.b,t=t.b)e.push(w(n,r.a,t.a));return R(e)}),D=(t(function(n,r,t,e){for(var u=[];r.b&&t.b&&e.b;r=r.b,t=t.b,e=e.b)u.push(y(n,r.a,t.a,e.a));return R(u)}),c(function(n,r,t,e,u){for(var a=[];r.b&&t.b&&e.b&&u.b;r=r.b,t=t.b,e=e.b,u=u.b)a.push(x(n,r.a,t.a,e.a,u.a));return R(a)}),e(function(n,r,t,e,u,a){for(var c=[];r.b&&t.b&&e.b&&u.b&&a.b;r=r.b,t=t.b,e=e.b,u=u.b,a=a.b)c.push(v(n,r.a,t.a,e.a,u.a,a.a));return R(c)}),l(function(t,n){return R(V(n).sort(function(n,r){return T(t(n),t(r))}))})),G=(l(function(t,n){return R(V(n).sort(function(n,r){r=w(t,n,r);return r===lt?0:r===vt?-1:1}))}),l(function(n,r){return n+r})),F=(l(function(n,r){return n-r}),l(function(n,r){return n*r})),M=(l(function(n,r){return n/r}),l(function(n,r){return n/r|0}),l(Math.pow),l(function(n,r){return r%n}),l(function(n,r){r%=n;return 0===n?p(11):0<r&&n<0||r<0&&0<n?r+n:r}));l(Math.atan2);var Q=Math.ceil,U=Math.floor,B=Math.round,q=Math.log;l(function(n,r){return n&&r}),l(function(n,r){return n||r}),l(function(n,r){return n!==r});var K=l(function(n,r){return n+r});l(function(n,r){return n+r});l(function(n,r){for(var t=r.length,e=Array(t),u=0;u<t;){var a=r.charCodeAt(u);a<55296||56319<a?(e[u]=n(r[u]),u++):(e[u]=n(r[u]+r[u+1]),u+=2)}return e.join(\"\")}),l(function(n,r){for(var t=[],e=r.length,u=0;u<e;){var a=r[u],c=r.charCodeAt(u);u++,c<55296||56319<c||(a+=r[u],u++),n(a)&&t.push(a)}return t.join(\"\")});d(function(n,r,t){for(var e=t.length,u=0;u<e;){var a=t[u],c=t.charCodeAt(u);u++,c<55296||56319<c||(a+=t[u],u++),r=w(n,a,r)}return r});var P=d(function(n,r,t){for(var e=t.length;e--;){var u=t[e],a=t.charCodeAt(e);r=w(n,u=a>=56320&&57343>=a?t[--e]+u:u,r)}return r}),W=l(function(n,r){return r.split(n)}),Y=l(function(n,r){return r.join(n)}),nn=d(function(n,r,t){return t.slice(n,r)});l(function(n,r){for(var t=r.length;t--;){var e=r[t],u=r.charCodeAt(t);if(n(e=u>=56320&&57343>=u?r[--t]+e:e))return!0}return!1});var rn=l(function(n,r){for(var t=r.length;t--;){var e=r[t],u=r.charCodeAt(t);if(!n(e=u>=56320&&57343>=u?r[--t]+e:e))return!1}return!0}),tn=l(function(n,r){return!!~r.indexOf(n)}),en=l(function(n,r){return 0==r.indexOf(n)}),un=l(function(n,r){return n.length<=r.length&&r.lastIndexOf(n)==r.length-n.length}),an=l(function(n,r){var t=n.length;if(t<1)return H;for(var e=0,u=[];-1<(e=r.indexOf(n,e));)u.push(e),e+=t;return R(u)});var cn=l(function(n,r){return{$:10,d:n,b:r}});l(function(n,r){return{$:11,e:n,b:r}});function on(n,r){return{$:13,f:n,g:r}}var fn=l(function(n,r){return{$:14,b:r,h:n}});var bn=l(function(n,r){return on(n,[r])}),sn=d(function(n,r,t){return on(n,[r,t])}),ln=(t(function(n,r,t,e){return on(n,[r,t,e])}),c(function(n,r,t,e,u){return on(n,[r,t,e,u])}),e(function(n,r,t,e,u,a){return on(n,[r,t,e,u,a])}),u(function(n,r,t,e,u,a,c){return on(n,[r,t,e,u,a,c])}),a(function(n,r,t,e,u,a,c,o){return on(n,[r,t,e,u,a,c,o])}),o(function(n,r,t,e,u,a,c,o,i){return on(n,[r,t,e,u,a,c,o,i])}),l(function(n,r){try{return vn(n,JSON.parse(r))}catch(n){return mt(w(ht,\"This is not valid JSON! \"+n.message,r))}})),dn=l(vn);function vn(n,r){switch(n.$){case 3:return\"boolean\"==typeof r?wt(r):hn(\"a BOOL\",r);case 2:return\"number\"!=typeof r?hn(\"an INT\",r):(r<=-2147483647||2147483647<=r||(0|r)!==r)&&(!isFinite(r)||r%1)?hn(\"an INT\",r):wt(r);case 4:return\"number\"==typeof r?wt(r):hn(\"a FLOAT\",r);case 6:return\"string\"==typeof r?wt(r):r instanceof String?wt(r+\"\"):hn(\"a STRING\",r);case 9:return null===r?wt(n.c):hn(\"null\",r);case 5:return wt(r);case 7:return Array.isArray(r)?$n(n.b,r,R):hn(\"a LIST\",r);case 8:return Array.isArray(r)?$n(n.b,r,mn):hn(\"an ARRAY\",r);case 10:var t=n.d;if(\"object\"!=typeof r||null===r||!(t in r))return hn(\"an OBJECT with a field named `\"+t+\"`\",r);var e=vn(n.b,r[t]);return ie(e)?e:mt(w(pt,t,e.a));case 11:t=n.e;if(!Array.isArray(r))return hn(\"an ARRAY\",r);if(r.length<=t)return hn(\"a LONGER array. Need index \"+t+\" but only see \"+r.length+\" entries\",r);e=vn(n.b,r[t]);return ie(e)?e:mt(w(gt,t,e.a));case 12:if(\"object\"!=typeof r||null===r||Array.isArray(r))return hn(\"an OBJECT\",r);var u,a=H;for(u in r)if(r.hasOwnProperty(u)){e=vn(n.b,r[u]);if(!ie(e))return mt(w(pt,u,e.a));a=I(_(u,e.a),a)}return wt(It(a));case 13:for(var c=n.f,o=n.g,i=0;i<o.length;i++){e=vn(o[i],r);if(!ie(e))return e;c=c(e.a)}return wt(c);case 14:e=vn(n.b,r);return ie(e)?vn(n.h(e.a),r):e;case 15:for(var f=H,b=n.g;b.b;b=b.b){e=vn(b.a,r);if(ie(e))return e;f=I(e.a,f)}return mt(yt(It(f)));case 1:return mt(w(ht,n.a,r));case 0:return wt(n.a)}}function $n(n,r,t){for(var e=r.length,u=Array(e),a=0;a<e;a++){var c=vn(n,r[a]);if(!ie(c))return mt(w(gt,a,c.a));u[a]=c.a}return wt(t(u))}function mn(r){return w(oe,r.length,function(n){return r[n]})}function hn(n,r){return mt(w(ht,\"Expecting \"+n,r))}function pn(n,r){if(n===r)return!0;if(n.$!==r.$)return!1;switch(n.$){case 0:case 1:return n.a===r.a;case 3:case 2:case 4:case 6:case 5:return!0;case 9:return n.c===r.c;case 7:case 8:case 12:return pn(n.b,r.b);case 10:return n.d===r.d&&pn(n.b,r.b);case 11:return n.e===r.e&&pn(n.b,r.b);case 13:return n.f===r.f&&gn(n.g,r.g);case 14:return n.h===r.h&&pn(n.b,r.b);case 15:return gn(n.g,r.g)}}function gn(n,r){var t=n.length;if(t!==r.length)return!1;for(var e=0;e<t;e++)if(!pn(n[e],r[e]))return!1;return!0}var wn=l(function(n,r){return JSON.stringify(r,null,n)+\"\"});function yn(n){return n}var xn=d(function(n,r,t){return t[n]=r,t});var kn=null;function An(n){return{$:0,a:n}}function Sn(n){return{$:1,a:n}}function Tn(n){return{$:2,b:n,c:null}}var En=l(function(n,r){return{$:3,b:n,d:r}}),Zn=l(function(n,r){return{$:4,b:n,d:r}});var jn=0;function Cn(n){n={$:0,e:jn++,f:n,g:null,h:[]};return Hn(n),n}function _n(r){return Tn(function(n){n(An(Cn(r)))})}function Jn(n,r){n.h.push(r),Hn(n)}var Ln=l(function(r,t){return Tn(function(n){Jn(r,t),n(An(C))})});var Nn=!1,Xn=[];function Hn(n){if(Xn.push(n),!Nn){for(Nn=!0;n=Xn.shift();)!function(r){for(;r.f;){var n=r.f.$;if(0===n||1===n){for(;r.g&&r.g.$!==n;)r.g=r.g.i;if(!r.g)return;r.f=r.g.b(r.f.a),r.g=r.g.i}else{if(2===n)return r.f.c=r.f.b(function(n){r.f=n,Hn(r)});if(5===n){if(0===r.h.length)return;r.f=r.f.b(r.h.shift())}else r.g={$:3===n?0:1,b:r.f.b,i:r.g},r.f=r.f.d}}}(n);Nn=!1}}t(function(n,r,t,e){return In(r,e,n.cD,n.c9,n.c4,function(){return function(){}})});function In(n,r,t,e,u,a){var c=w(dn,n,r?r.flags:void 0);ie(c)||p(2);var o={},i=(c=t(c.a)).a,f=a(b,i),a=function(n,r){var t,e;for(e in On){var u=On[e];u.a&&((t=t||{})[e]=u.a(e,r)),n[e]=function(n,r){var e={g:r,h:void 0},u=n.c,a=n.d,c=n.e,o=n.f;return e.h=Cn(w(En,function n(t){return w(En,n,{$:5,b:function(n){var r=n.a;return 0===n.$?y(a,e,r,t):c&&o?x(u,e,r.i,r.j,t):y(u,e,c?r.i:r.j,t)}})},n.b))}(u,r)}return t}(o,b);function b(n,r){c=w(e,n,i),f(i=c.a,r),Gn(o,c.b,u(i))}return Gn(o,c.b,u(i)),a?{ports:a}:{}}var On={};var Rn=l(function(r,t){return Tn(function(n){r.g(t),n(An(C))})});l(function(n,r){return w(Ln,n.h,{$:0,a:r})});function Vn(r){return function(n){return{$:1,k:r,l:n}}}function zn(n){return{$:2,m:n}}var Dn=l(function(n,r){return{$:3,n:n,o:r}});function Gn(n,r,t){var e,u={};for(e in Fn(!0,r,u,null),Fn(!1,t,u,null),n)Jn(n[e],{$:\"fx\",a:u[e]||{i:H,j:H}})}function Fn(n,r,t,e){switch(r.$){case 1:var u=r.k,a=(a=u,o=e,w(n?On[a].e:On[a].f,function(n){for(var r=o;r;r=r.q)n=r.p(n);return n},r.l));return void(t[u]=(a=a,u=(u=t[u])||{i:H,j:H},n?u.i=I(a,u.i):u.j=I(a,u.j),u));case 2:for(var c=r.m;c.b;c=c.b)Fn(n,c.a,t,e);return;case 3:return void Fn(n,r.o,t,{p:r.n,q:e})}var a,o}function Mn(n){On[n]&&p(3)}function Qn(n,r){return Mn(n),On[n]={e:Un,r:r,a:Bn},Vn(n)}var Un=l(function(n,r){return r});function Bn(n){var t,c=[],o=On[n].r,i=(t=0,Tn(function(n){var r=setTimeout(function(){n(An(C))},t);return function(){clearTimeout(r)}}));return On[n].b=i,On[n].c=d(function(n,r,t){for(;r.b;r=r.b)for(var e=c,u=o(r.a),a=0;a<e.length;a++)e[a](u);return i}),{subscribe:function(n){c.push(n)},unsubscribe:function(n){(n=(c=c.slice()).indexOf(n))<0||c.splice(n,1)}}}var qn;l(function(r,t){return function(n){return r(t(n))}});var Kn=\"undefined\"!=typeof document?document:{};function Pn(n,r){n.appendChild(r)}t(function(n,r,t,e){e=e.node;return e.parentNode.replaceChild($r(n,function(){}),e),{}});function Wn(n){return{$:0,a:n}}var Yn=l(function(a,c){return l(function(n,r){for(var t=[],e=0;r.b;r=r.b){var u=r.a;e+=u.b||0,t.push(u)}return e+=t.length,{$:1,c:c,d:dr(n),e:t,f:a,b:e}})})(void 0),nr=l(function(a,c){return l(function(n,r){for(var t=[],e=0;r.b;r=r.b){var u=r.a;e+=u.b.b||0,t.push(u)}return e+=t.length,{$:2,c:c,d:dr(n),e:t,f:a,b:e}})})(void 0);var rr=l(function(n,r){return{$:4,j:n,k:r,b:1+(r.b||0)}});function tr(n,r){return{$:5,l:n,m:r,k:void 0}}l(function(n,r){return tr([n,r],function(){return n(r)})});var er=d(function(n,r,t){return tr([n,r,t],function(){return w(n,r,t)})}),ur=t(function(n,r,t,e){return tr([n,r,t,e],function(){return y(n,r,t,e)})}),ar=(c(function(n,r,t,e,u){return tr([n,r,t,e,u],function(){return x(n,r,t,e,u)})}),e(function(n,r,t,e,u,a){return tr([n,r,t,e,u,a],function(){return v(n,r,t,e,u,a)})}),u(function(n,r,t,e,u,a,c){return tr([n,r,t,e,u,a,c],function(){return f(n,r,t,e,u,a,c)})}),a(function(n,r,t,e,u,a,c,o){return tr([n,r,t,e,u,a,c,o],function(){return b(n,r,t,e,u,a,c,o)})}),o(function(n,r,t,e,u,a,c,o,i){return tr([n,r,t,e,u,a,c,o,i],function(){return s(n,r,t,e,u,a,c,o,i)})}),l(function(n,r){return{$:\"a0\",n:n,o:r}})),cr=l(function(n,r){return{$:\"a1\",n:n,o:r}}),or=l(function(n,r){return{$:\"a2\",n:n,o:r}}),ir=l(function(n,r){return{$:\"a3\",n:n,o:r}});d(function(n,r,t){return{$:\"a4\",n:r,o:{f:n,o:t}}});function fr(n){return\"script\"==n?\"p\":n}l(function(n,r){return\"a0\"===r.$?w(ar,r.n,(t=n,n=le(e=r.o),{$:e.$,a:n?y(be,n<3?sr:lr,se(t),e.a):w(fe,t,e.a)})):r;var t,e});var br,sr=l(function(n,r){return _(n(r.a),r.b)}),lr=l(function(n,r){return{cL:n(r.cL),aB:r.aB,av:r.av}});function dr(n){for(var r={};n.b;n=n.b){var t=n.a,e=t.$,u=t.n,a=t.o;\"a2\"!==e?(t=r[e]||(r[e]={}),\"a3\"===e&&\"class\"===u?vr(t,u,a):t[u]=a):\"className\"===u?vr(r,u,a):r[u]=a}return r}function vr(n,r,t){var e=n[r];n[r]=e?e+\" \"+t:t}function $r(n,r){var t=n.$;if(5===t)return $r(n.k||(n.k=n.m()),r);if(0===t)return Kn.createTextNode(n.a);if(4===t){for(var e=n.k,u=n.j;4===e.$;)\"object\"!=typeof u?u=[u,e.j]:u.push(e.j),e=e.k;var a={j:u,p:r};return(c=$r(e,a)).elm_event_node_ref=a,c}if(3===t)return mr(c=n.h(n.g),r,n.d),c;var c=n.f?Kn.createElementNS(n.f,n.c):Kn.createElement(n.c);qn&&\"a\"==n.c&&c.addEventListener(\"click\",qn(c)),mr(c,r,n.d);for(var o=n.e,i=0;i<o.length;i++)Pn(c,$r(1===t?o[i]:o[i].b,r));return c}function mr(n,r,t){for(var e in t){var u=t[e];\"a1\"===e?function(n,r){var t,e=n.style;for(t in r)e[t]=r[t]}(n,u):\"a0\"===e?function(n,r,t){var e,u=n.elmFs||(n.elmFs={});for(e in t){var a=t[e],c=u[e];if(a){if(c){if(c.q.$===a.$){c.q=a;continue}n.removeEventListener(e,c)}c=function(i,n){function f(n){var r=f.q,t=vn(r.a,n);if(ie(t)){for(var e,u=le(r),r=t.a,a=u?u<3?r.a:r.cL:r,t=1==u?r.b:3==u&&r.aB,c=(t&&n.stopPropagation(),(2==u?r.b:3==u&&r.av)&&n.preventDefault(),i);e=c.j;){if(\"function\"==typeof e)a=e(a);else for(var o=e.length;o--;)a=e[o](a);c=c.p}c(a,t)}}return f.q=n,f}(r,a),n.addEventListener(e,c,br&&{passive:le(a)<2}),u[e]=c}else n.removeEventListener(e,c),u[e]=void 0}}(n,r,u):\"a3\"===e?function(n,r){for(var t in r){var e=r[t];e?n.setAttribute(t,e):n.removeAttribute(t)}}(n,u):\"a4\"===e?function(n,r){for(var t in r){var e=r[t],u=e.f,e=e.o;e?n.setAttributeNS(u,t,e):n.removeAttributeNS(u,t)}}(n,u):\"value\"===e&&\"checked\"===e&&n[e]===u||(n[e]=u)}}try{window.addEventListener(\"t\",null,Object.defineProperty({},\"passive\",{get:function(){br=!0}}))}catch(n){}function hr(n,r){var t=[];return gr(n,r,t,0),t}function pr(n,r,t,e){e={$:r,r:t,s:e,t:void 0,u:void 0};return n.push(e),e}function gr(n,r,t,e){if(n!==r){var u=n.$,a=r.$;if(u!==a){if(1!==u||2!==a)return void pr(t,0,e,r);r=function(n){for(var r=n.e,t=r.length,e=Array(t),u=0;u<t;u++)e[u]=r[u].b;return{$:1,c:n.c,d:n.d,e:e,f:n.f,b:n.b}}(r),a=1}switch(a){case 5:for(var c=n.l,o=r.l,i=c.length,f=i===o.length;f&&i--;)f=c[i]===o[i];if(f)return void(r.k=n.k);r.k=r.m();var b=[];return gr(n.k,r.k,b,0),void(0<b.length&&pr(t,1,e,b));case 4:for(var s=n.j,l=r.j,d=!1,v=n.k;4===v.$;)d=!0,\"object\"!=typeof s?s=[s,v.j]:s.push(v.j),v=v.k;for(var $=r.k;4===$.$;)d=!0,\"object\"!=typeof l?l=[l,$.j]:l.push($.j),$=$.k;return d&&s.length!==l.length?void pr(t,0,e,r):((d?function(n,r){for(var t=0;t<n.length;t++)if(n[t]!==r[t])return!1;return!0}(s,l):s===l)||pr(t,2,e,l),void gr(v,$,t,e+1));case 0:return void(n.a!==r.a&&pr(t,3,e,r.a));case 1:return void wr(n,r,t,e,xr);case 2:return void wr(n,r,t,e,kr);case 3:if(n.h!==r.h)return void pr(t,0,e,r);b=yr(n.d,r.d);b&&pr(t,4,e,b);b=r.i(n.g,r.g);return void(b&&pr(t,5,e,b))}}}function wr(n,r,t,e,u){var a;n.c===r.c&&n.f===r.f?((a=yr(n.d,r.d))&&pr(t,4,e,a),u(n,r,t,e)):pr(t,0,e,r)}function yr(n,r,t){var e,u,a,c,o,i;for(u in n)\"a1\"!==u&&\"a0\"!==u&&\"a3\"!==u&&\"a4\"!==u?u in r?(o=n[u])===(a=r[u])&&\"value\"!==u&&\"checked\"!==u||\"a0\"===t&&((c=o).$==(o=a).$&&pn(c.a,o.a))||((e=e||{})[u]=a):(e=e||{})[u]=t?\"a1\"===t?\"\":\"a0\"===t||\"a3\"===t?void 0:{f:n[u].f,o:void 0}:\"string\"==typeof n[u]?\"\":null:(a=yr(n[u],r[u]||{},u))&&((e=e||{})[u]=a);for(i in r)i in n||((e=e||{})[i]=r[i]);return e}function xr(n,r,t,e){var u=n.e,a=r.e,n=u.length,r=a.length;r<n?pr(t,6,e,{v:r,i:n-r}):n<r&&pr(t,7,e,{v:n,e:a});for(var c=n<r?n:r,o=0;o<c;o++){var i=u[o];gr(i,a[o],t,++e),e+=i.b||0}}function kr(n,r,t,e){for(var u=[],a={},c=[],o=n.e,i=r.e,f=o.length,b=i.length,s=0,l=0,d=e;s<f&&l<b;){var v,$=(v=o[s]).a,m=(E=i[l]).a,h=v.b,p=E.b;if($!==m){var g,w,y,x,k,A,S=o[s+1],T=i[l+1];if(S&&(w=S.b,y=m===(g=S.a)),T&&(k=T.b,A=$===(x=T.a)),A&&y)gr(h,k,u,++d),Sr(a,u,$,p,l,c),d+=h.b||0,Tr(a,u,$,w,++d),d+=w.b||0,s+=2,l+=2;else if(A)d++,Sr(a,u,m,p,l,c),gr(h,k,u,d),d+=h.b||0,s+=1,l+=2;else if(y)Tr(a,u,$,h,++d),d+=h.b||0,gr(w,p,u,++d),d+=w.b||0,s+=2,l+=1;else{if(!S||g!==x)break;Tr(a,u,$,h,++d),Sr(a,u,m,p,l,c),d+=h.b||0,gr(w,k,u,++d),d+=w.b||0,s+=2,l+=2}}else gr(h,p,u,++d),d+=h.b||0,s++,l++}for(;s<f;)Tr(a,u,(v=o[s]).a,h=v.b,++d),d+=h.b||0,s++;for(;l<b;){var E,Z=Z||[];Sr(a,u,(E=i[l]).a,E.b,void 0,Z),l++}(0<u.length||0<c.length||Z)&&pr(t,8,e,{w:u,x:c,y:Z})}var Ar=\"_elmW6BL\";function Sr(n,r,t,e,u,a){var c=n[t];if(!c)return a.push({r:u,A:c={c:0,z:e,r:u,s:void 0}}),void(n[t]=c);if(1===c.c){a.push({r:u,A:c}),c.c=2;var o=[];return gr(c.z,e,o,c.r),c.r=u,void(c.s.s={w:o,A:c})}Sr(n,r,t+Ar,e,u,a)}function Tr(n,r,t,e,u){var a=n[t];if(a){if(0===a.c){a.c=2;var c=[];return gr(e,a.z,c,u),void pr(r,9,u,{w:c,A:a})}Tr(n,r,t+Ar,e,u)}else{r=pr(r,9,u,void 0);n[t]={c:1,z:e,r:u,s:r}}}function Er(n,r,t,e){!function n(r,t,e,u,a,c,o){var i=e[u];var f=i.r;for(;f===a;){var b,s=i.$;if(1===s?Er(r,t.k,i.s,o):8===s?(i.t=r,i.u=o,0<(b=i.s.w).length&&n(r,t,b,0,a,c,o)):9===s?(i.t=r,i.u=o,(s=i.s)&&(s.A.s=r,0<(b=s.w).length&&n(r,t,b,0,a,c,o))):(i.t=r,i.u=o),!(i=e[++u])||(f=i.r)>c)return u}var l=t.$;if(4===l){for(var d=t.k;4===d.$;)d=d.k;return n(r,d,e,u,a+1,c,r.elm_event_node_ref)}var v=t.e;var $=r.childNodes;for(var m=0;m<v.length;m++){var h=1===l?v[m]:v[m].b,p=++a+(h.b||0);if(a<=f&&f<=p&&(u=n($[m],h,e,u,a,p,o),!(i=e[u])||(f=i.r)>c))return u;a=p}return u}(n,r,t,0,0,r.b,e)}function Zr(n,r,t,e){return 0===t.length?n:(Er(n,r,t,e),jr(n,t))}function jr(n,r){for(var t=0;t<r.length;t++){var e=r[t],u=e.t,e=function(n,r){switch(r.$){case 0:return function(n,r,t){var e=n.parentNode,t=$r(r,t);t.elm_event_node_ref||(t.elm_event_node_ref=n.elm_event_node_ref);e&&t!==n&&e.replaceChild(t,n);return t}(n,r.s,r.u);case 4:return mr(n,r.u,r.s),n;case 3:return n.replaceData(0,n.length,r.s),n;case 1:return jr(n,r.s);case 2:return n.elm_event_node_ref?n.elm_event_node_ref.j=r.s:n.elm_event_node_ref={j:r.s,p:r.u},n;case 6:for(var t=r.s,e=0;e<t.i;e++)n.removeChild(n.childNodes[t.v]);return n;case 7:for(var u=(t=r.s).e,a=n.childNodes[e=t.v];e<u.length;e++)n.insertBefore($r(u[e],r.u),a);return n;case 9:if(!(t=r.s))return n.parentNode.removeChild(n),n;var c=t.A;return void 0!==c.r&&n.parentNode.removeChild(n),c.s=jr(n,t.w),n;case 8:return function(n,r){var t=r.s,e=function(n,r){if(n){for(var t=Kn.createDocumentFragment(),e=0;e<n.length;e++){var u=n[e].A;Pn(t,2===u.c?u.s:$r(u.z,r.u))}return t}}(t.y,r);n=jr(n,t.w);for(var u=t.x,a=0;a<u.length;a++){var c=u[a],o=c.A,o=2===o.c?o.s:$r(o.z,r.u);n.insertBefore(o,n.childNodes[c.r])}e&&Pn(n,e);return n}(n,r);case 5:return r.s(n);default:p(10)}}(u,e);u===n&&(n=e)}return n}function Cr(n){if(3===n.nodeType)return Wn(n.textContent);if(1!==n.nodeType)return Wn(\"\");for(var r=H,t=n.attributes,e=t.length;e--;)var u=t[e],r=I(w(ir,u.name,u.value),r);for(var a=n.tagName.toLowerCase(),c=H,o=n.childNodes,e=o.length;e--;)c=I(Cr(o[e]),c);return y(Yn,a,r,c)}t(function(r,n,t,c){return In(n,c,r.cD,r.c9,r.c4,function(t,n){var e=r.db,u=c.node,a=Cr(u);return Lr(n,function(n){var r=e(n),n=hr(a,r);u=Zr(u,a,n,t),a=r})})});var _r=t(function(r,n,t,e){return In(n,e,r.cD,r.c9,r.c4,function(e,n){var u=r.ax&&r.ax(e),a=r.db,c=Kn.title,o=Kn.body,i=Cr(o);return Lr(n,function(n){qn=u;var r=a(n),t=Yn(\"body\")(H)(r.ca),n=hr(i,t);o=Zr(o,i,n,e),i=t,qn=0,c!==r.c6&&(Kn.title=c=r.c6)})})}),Jr=\"undefined\"!=typeof requestAnimationFrame?requestAnimationFrame:function(n){setTimeout(n,1e3/60)};function Lr(t,e){e(t);var u=0;function a(){u=1===u?0:(Jr(a),e(t),1)}return function(n,r){t=n,r?(e(t),2===u&&(u=1)):(0===u&&Jr(a),u=2)}}function Nr(){return Ee(Kn.location.href).a||p(1)}l(function(n,r){return w(oo,Ze,Tn(function(){r&&history.go(r),n()}))});var Xr=l(function(n,r){return w(oo,Ze,Tn(function(){history.pushState({},\"\",r),n()}))}),Hr=(l(function(n,r){return w(oo,Ze,Tn(function(){history.replaceState({},\"\",r),n()}))}),{addEventListener:function(){},removeEventListener:function(){}}),Ir=\"undefined\"!=typeof window?window:Hr;d(function(t,e,u){return _n(Tn(function(n){function r(n){Cn(u(n))}return t.addEventListener(e,r,br&&{passive:!0}),function(){t.removeEventListener(e,r)}}))}),l(function(n,r){r=vn(n,r);return ie(r)?kt(r.a):At});function Or(t,e){return Tn(function(r){Jr(function(){var n=document.getElementById(t);r(n?An(e(n)):Sn(de(t)))})})}var Rr=l(function(r,n){return Or(n,function(n){return n[r](),C})});l(function(n,r){return t=function(){return Ir.scroll(n,r),C},Tn(function(n){Jr(function(){n(An(t()))})});var t});d(function(n,r,t){return Or(n,function(n){return n.scrollLeft=r,n.scrollTop=t,C})});var Vr=c(function(n,r,t,e,u){for(var a=n.length,c=r+a<=u.length,o=0;c&&o<a;)var i=u.charCodeAt(r),c=n[o++]===u[r++]&&(10==i?(t++,e=1):(e++,55296==(63488&i)?n[o++]===u[r++]:1));return J(c?r:-1,t,e)}),zr=d(function(n,r,t){return r<t.length?55296==(63488&t.charCodeAt(r))?n(t.substr(r,2))?r+2:-1:n(t[r])?\"\\n\"===t[r]?-2:r+1:-1:-1}),Dr=d(function(n,r,t){return t.charCodeAt(r)===n}),Gr=l(function(n,r){for(;n<r.length;n++){var t=r.charCodeAt(n);if(t<48||57<t)return n}return n}),Fr=d(function(n,r,t){for(var e=0;r<t.length;r++){var u=t.charCodeAt(r)-48;if(u<0||n<=u)break;e=n*e+u}return _(r,e)}),Mr=l(function(n,r){for(var t=0;n<r.length;n++){var e=r.charCodeAt(n);if(48>e||e>57)if(65>e||e>70){if(e<97||102<e)break;t=16*t+e-87}else t=16*t+e-55;else t=16*t+e-48}return _(n,t)});c(function(n,r,t,e,u){for(var a=u.indexOf(n,r),c=a<0?u.length:a+n.length;r<c;){var o=u.charCodeAt(r++);10==o?(e=1,t++):(e++,55296==(63488&o)&&r++)}return J(a,t,e)});var Qr=l(function(u,a){return Tn(function(r){var t,n=new XMLHttpRequest;e=n,Ef(t=a)&&e.addEventListener(\"progress\",function(n){n.lengthComputable&&Cn(t.a({cc:n.loaded,cd:n.total}))}),n.addEventListener(\"error\",function(){r(Sn(Sf))}),n.addEventListener(\"timeout\",function(){r(Sn(Tf))}),n.addEventListener(\"load\",function(){r(function(n,r){var t=function(n){return{da:n.responseURL,bS:{ch:n.status,cL:n.statusText},cz:function(n){var r=$o;if(!n)return r;for(var t=n.split(\"\\r\\n\"),e=t.length;e--;){var u,a,c=t[e],o=c.indexOf(\": \");0<o&&(u=c.substring(0,o),a=c.substring(2+o),r=y(Xf,u,function(n){return kt(Ef(n)?a+\", \"+n.a:a)},r))}return r}(n.getAllResponseHeaders()),ca:n.response}}(n);if(n.status<200||300<=n.status)return t.body=n.responseText,Sn(kf(t));r=r(t);return ie(r)?An(r.a):(t.body=n.responseText,Sn(w(xf,r.a,t)))}(n,u.cr.a))});try{n.open(u.cM,u.da,!0)}catch(n){return r(Sn(Af(u.da)))}!function(n,r){for(var t=r.cz;t.b;t=t.b)n.setRequestHeader(t.a.a,t.a.b);n.responseType=r.cr.b,n.withCredentials=r.dd,Ef(r.c5)&&(n.timeout=r.c5.a)}(n,u);var e=u.ca;return n.send(Zf(e)?(n.setRequestHeader(\"Content-Type\",e.a),e.b):e.a),function(){n.abort()}})});l(function(r,t){return{$:0,b:t.b,a:function(n){n=t.a(n);return w(jf,r,n)}}});var Ur=l(function(n,r){var t=\"g\";n.cO&&(t+=\"m\"),n.ce&&(t+=\"i\");try{return kt(RegExp(r,t))}catch(n){return At}}),Br=(l(function(n,r){return null!==r.match(n)}),d(function(n,r,t){for(var e,u=[],a=0,c=t,t=r.lastIndex,o=-1;a++<n&&(e=r.exec(c))&&o!=r.lastIndex;){for(var i=e.length-1,f=Array(i);0<i;){var b=e[i];f[--i]=b?kt(b):At}u.push(x(Cb,e[0],e.index,a,R(f))),o=r.lastIndex}return r.lastIndex=t,R(u)}),t(function(u,n,a,r){var c=0;return r.replace(n,function(n){if(c++>=u)return n;for(var r=arguments.length-3,t=Array(r);0<r;){var e=arguments[r];t[--r]=e?kt(e):At}return a(x(Cb,n,arguments[arguments.length-2],c,R(t)))})})),qr=d(function(n,r,t){for(var e=t,u=[],a=r.lastIndex,t=r.lastIndex;n--;){var c=r.exec(e);if(!c)break;u.push(e.slice(a,c.index)),a=r.lastIndex}return u.push(e.slice(a)),r.lastIndex=t,R(u)}),Kr=l(function(n,r){return n&r});l(function(n,r){return n|r}),l(function(n,r){return n^r});l(function(n,r){return r<<n});Hr=l(function(n,r){return r>>n}),l(function(n,r){return r>>>n});l(function(t,e){return Tn(function(n){var r=setInterval(function(){Cn(e)},t);return function(){clearInterval(r)}})});function Pr(n){return w(jt,\"\\n    \",w(Ct,\"\\n\",n))}function Wr(n){return y(_t,l(function(n,r){return r+1}),0,n)}function Yr(n){return 97<=(n=Ht(n))&&n<=122}function nt(n){return(n=Ht(n))<=90&&65<=n}function rt(n){return(n=Ht(n))<=57&&48<=n}function tt(n){return Yr(n)||nt(n)||rt(n)}function et(n){return n}function ut(n){return n.a}function at(n){return n}function ct(n){return\"\"===n}var ot=O,it=m,ft=(d(function(t,n,r){var e=r.c,r=r.d,u=l(function(n,r){return y(it,n.$?t:u,r,n.a)});return y(it,u,y(it,t,n,r),e)}),d(function(n,r,t){for(;;){if(-2===t.$)return r;var e=t.d,u=n,a=y(n,t.b,t.c,y(ft,n,r,t.e));n=u,r=a,t=e}})),bt=function(n){return y(ft,d(function(n,r,t){return w(ot,_(n,r),t)}),H,n)},st=function(n){return n=n,y(ft,d(function(n,r,t){return w(ot,n,t)}),H,n)},lt=1,dt=2,vt=0,$t=l(function(n,r){return n}),mt=function(n){return{$:1,a:n}},ht=l(function(n,r){return{$:3,a:n,b:r}}),pt=l(function(n,r){return{$:0,a:n,b:r}}),gt=l(function(n,r){return{$:1,a:n,b:r}}),wt=function(n){return{$:0,a:n}},yt=function(n){return{$:2,a:n}},xt=G,kt=function(n){return{$:0,a:n}},At={$:1},St=rn,Tt=N,Et=wn,Zt=function(n){return n+\"\"},jt=l(function(n,r){return w(Y,n,V(r))}),Ct=l(function(n,r){return R(w(W,n,r))}),_t=d(function(n,r,t){for(;;){if(!t.b)return r;var e=t.b,u=n,a=w(n,t.a,r);n=u,r=a,t=e}}),Jt=z,Lt=d(function(n,r,t){for(;;){if(1<=T(n,r))return t;var e=n,u=r-1,a=w(ot,r,t);n=e,r=u,t=a}}),Nt=l(function(n,r){return y(Lt,n,r,H)}),Xt=l(function(n,r){return y(Jt,n,w(Nt,0,Wr(r)-1),r)}),Ht=function(n){var r=n.charCodeAt(0);return r<55296||56319<r?r:1024*(r-55296)+n.charCodeAt(1)-56320+65536},It=function(n){return y(_t,ot,H,n)},Ot=function(n){var r=n.charCodeAt(0);return r?kt(r<55296||56319<r?_(n[0],n.slice(1)):_(n[0]+n[1],n.slice(2))):At},Rt=l(function(n,r){return\"\\n\\n(\"+Zt(n+1)+(\") \"+Pr(Vt(r)))}),Vt=function(n){return w(zt,n,H)},zt=l(function(n,r){n:for(;;)switch(n.$){case 0:var t=n.a,e=n.b,u=function(){var n=Ot(t);if(1===n.$)return!1;var r=n.a,n=r.b;return(Yr(r=r.a)||nt(r))&&w(St,tt,n)}(),a=e,u=w(ot,u?\".\"+t:\"['\"+t+\"']\",r);n=a,r=u;continue n;case 1:var e=n.b,c=\"[\"+Zt(n.a)+\"]\",a=e,u=w(ot,c,r);n=a,r=u;continue n;case 2:var o=n.a;if(o.b){if(o.b.b){var i=(r.b?\"The Json.Decode.oneOf at json\"+w(jt,\"\",It(r)):\"Json.Decode.oneOf\")+\" failed in the following \"+Zt(Wr(o))+\" ways:\";return w(jt,\"\\n\\n\",w(ot,i,w(Xt,Rt,o)))}n=a=e=o.a,r=u=r;continue n}return\"Ran into a Json.Decode.oneOf with no possibilities\"+(r.b?\" at json\"+w(jt,\"\",It(r)):\"!\");default:c=n.a,o=n.b;return(i=r.b?\"Problem with the value at json\"+w(jt,\"\",It(r))+\":\\n\\n    \":\"Problem with the given value:\\n\\n\")+(Pr(w(Et,4,o))+\"\\n\\n\")+c}}),Dt=t(function(n,r,t,e){return{$:0,a:n,b:r,c:t,d:e}}),Gt=[],Ft=Q,Mt=l(function(n,r){return q(r)/q(n)}),Qt=Ft(w(Mt,2,32)),Ut=x(Dt,0,Qt,Gt,Gt),Bt=i,qt=l(function(n,r){return n(r)}),i=l(function(n,r){return r(n)}),Kt=A,Pt=U,Wt=function(n){return n.length},Yt=l(function(n,r){return 0<T(n,r)?n:r}),ne=F,re=$,te=l(function(n,r){for(;;){var t=w(re,32,n),e=t.b,t=w(ot,{$:0,a:t.a},r);if(!e.b)return It(t);n=e,r=t}}),ee=l(function(n,r){for(;;){var t=Ft(r/32);if(1===t)return w(re,32,n).a;n=w(te,n,H),r=t}}),ue=l(function(n,r){if(r.e){var t=32*r.e,e=Pt(w(Mt,32,t-1)),n=n?It(r.g):r.g,n=w(ee,n,r.e);return x(Dt,Wt(r.f)+t,w(Yt,5,e*Qt),n,r.f)}return x(Dt,Wt(r.f),Qt,Gt,r.f)}),ae=E,ce=c(function(n,r,t,e,u){for(;;){if(r<0)return w(ue,!1,{g:e,e:t/32|0,f:u});var a={$:1,a:y(Bt,32,r,n)};n=n,r=r-32,t=t,e=w(ot,a,e),u=u}}),oe=l(function(n,r){if(0<n){var t=n%32,e=y(Bt,t,n-t,r);return v(ce,r,n-t-32,n,H,e)}return Ut}),ie=function(n){return!n.$},fe=bn,be=sn,se=function(n){return{$:0,a:n}},le=function(n){switch(n.$){case 0:return 0;case 1:return 1;case 2:return 2;default:return 3}},de=at,ve=e(function(n,r,t,e,u,a){return{a6:a,bc:r,au:e,bu:t,by:n,cT:u}}),$e=tn,me=function(n){return n.length},he=nn,pe=l(function(n,r){return n<1?r:y(he,n,me(r),r)}),ge=an,we=l(function(n,r){return n<1?\"\":y(he,0,n,r)}),ye=function(n){for(var r=0,t=n.charCodeAt(0),e=43==t||45==t?1:0,u=e;u<n.length;++u){var a=n.charCodeAt(u);if(a<48||57<a)return At;r=10*r+a-48}return u==e?At:kt(45==t?-r:r)},xe=c(function(n,r,t,e,u){if(ct(u)||w($e,\"@\",u))return At;var a=w(ge,\":\",u);if(a.b){if(a.b.b)return At;var c=a.a,a=ye(w(pe,c+1,u));if(1===a.$)return At;a=a;return kt(f(ve,n,w(we,c,u),a,r,t,e))}return kt(f(ve,n,u,At,r,t,e))}),ke=t(function(n,r,t,e){if(ct(e))return At;var u=w(ge,\"/\",e);if(u.b){u=u.a;return v(xe,n,w(pe,u,e),r,t,w(we,u,e))}return v(xe,n,\"/\",r,t,e)}),Ae=d(function(n,r,t){if(ct(t))return At;var e=w(ge,\"?\",t);if(e.b){e=e.a;return x(ke,n,kt(w(pe,e+1,t)),r,w(we,e,t))}return x(ke,n,At,r,t)}),Se=l(function(n,r){if(ct(r))return At;var t=w(ge,\"#\",r);if(t.b){t=t.a;return y(Ae,n,kt(w(pe,t+1,r)),w(we,t,r))}return y(Ae,n,At,r)}),Te=en,Ee=function(n){return w(Te,\"http://\",n)?w(Se,0,w(pe,7,n)):w(Te,\"https://\",n)?w(Se,1,w(pe,8,n)):At},Ze=function(n){for(;;)0},je=An,nn=je(0),Ce=t(function(n,r,t,e){if(e.b){var u=e.a,a=e.b;if(a.b){var c=a.a,o=a.b;if(o.b){e=o.a,a=o.b;if(a.b){o=a.b;return w(n,u,w(n,c,w(n,e,w(n,a.a,500<t?y(_t,n,r,It(o)):x(Ce,n,r,t+1,o)))))}return w(n,u,w(n,c,w(n,e,r)))}return w(n,u,w(n,c,r))}return w(n,u,r)}return r}),_e=d(function(n,r,t){return x(Ce,n,r,0,t)}),Je=l(function(t,n){return y(_e,l(function(n,r){return w(ot,t(n),r)}),H,n)}),Le=En,Ne=l(function(r,n){return w(Le,function(n){return je(r(n))},n)}),Xe=d(function(t,n,e){return w(Le,function(r){return w(Le,function(n){return je(w(t,r,n))},e)},n)}),He=Rn,Ie=l(function(n,r){return _n(w(Le,He(n),r))}),an=d(function(n,r,t){return w(Ne,function(n){return 0},(r=w(Je,Ie(n),r),y(_e,Xe(ot),je(H),r)))}),en=d(function(n,r,t){return je(0)}),Rn=l(function(n,r){return w(Ne,n,r)});On.Task={b:nn,c:an,d:en,e:Rn,f:void 0};function Oe(n){return{aL:!1,bj:\"\",bk:n}}function Re(n){return{ae:po,bZ:n}}function Ve(n){return{aT:Re(\"\"),aV:Re(\"\"),v:{aj:At,ak:At,cx:n,ap:At,ar:At,az:At,aA:At},s:Re(\"\"),aX:Re(\"\"),bd:At,bQ:Re(\"\"),J:!1}}function ze(n){return(w(xo,\"/\",n)?w(yo,1,n):n)+\"/api/v2\"}function De(n){return{$:0,a:n}}function Ge(n){return{$:5,a:n}}function Fe(n){return{$:2,a:n}}function Me(n){return{$:3,a:n}}function Qe(n){return{$:1,a:n}}function Ue(n){return{$:4,a:n}}function Be(n){return{$:2,a:n}}function qe(n){return{$:3,a:n}}function Ke(n){return{$:4,a:n}}function Pe(n){return{$:5,a:n}}function We(n){return{$:1,a:n}}function Ye(n){return{$:0,a:n}}function nu(n){return{$:2,a:n}}function ru(n){return{$:0,a:n}}function tu(n){return{$:4,a:n}}function eu(n){return{$:5,a:n}}function uu(n){return{$:3,a:n}}function au(n){return{$:21,a:n}}function cu(n){return{$:3,a:n}}function ou(n){return r={$:12,a:n},function(n){return w(Mo,!1,w(Wo,n,r))};var r}function iu(r){return function(n){return y(Qo,!1,r,n)}}function fu(t){return w(Bo,function(n){if(g(me(n),t)){var r=ye(n);return r.$?ou('Invalid integer: \"'+n+'\"'):ci(r.a)}return ou(\"Expected \"+Zt(t)+(\" digits, but got \"+Zt(me(n))))},ui(ti(rt)))}function bu(n){return!n}function su(n){var u=n.a,a=n.b,c=!ct(u);return function(n){var r=v(pi,u,n.b,n.bG,n.aS,n.a),t=r.a,e=r.b,r=r.c;return g(t,-1)?w(Mo,!1,w(Wo,n,a)):y(Qo,c,0,{aS:r,c:n.c,d:n.d,b:t,bG:e,a:n.a})}}function lu(n){return gi(w(hi,n,{$:8,a:n}))}function du(n){return ou(\"Invalid day: \"+Zt(n))}function vu(n){return!(w(wi,4,n)||!w(wi,100,n)&&w(wi,400,n))}function $u(n){return((n-=1)/4|0)-(n/100|0)+(n/400|0)}function mu(r){return function(n){return y(ki,n,Po,r)}}function hu(n){return y(Ei,n.bG,n.aS,n.bv)}function pu(n){return w(Ci,Ti,n)}function gu(n){return y(_t,l(function(n,r){return y(Xi,n.a,n.b,r)}),$o,n)}function wu(n){return n?\"true\":\"false\"}function yu(n){return n.$||\"\"!==n.a?n:At}function xu(n){return Mi(w(_o,Qi,Tt(n+\"=\")))}function ku(n){return Yr(n)||nt(n)||\"_\"===n||rt(n)}function Au(n){return n=w(hi,n,{$:9,a:n}),a=n.b,c=!ct(u=n.a),function(n){var r=v(pi,u,n.b,n.bG,n.aS,n.a),t=r.a,e=r.b,r=r.c;return g(t,-1)||0<=y(ni,function(n){return tt(n)||\"_\"===n},t,n.a)?w(Mo,!1,w(Wo,n,a)):y(Qo,c,0,{aS:r,c:n.c,d:n.d,b:t,bG:e,a:n.a})};var u,a,c}function Su(n){return w(Pi,n,\"\")}function Tu(n){return{$:1,a:n}}function Eu(n){return{$:0,a:n}}function Zu(n){return(n.$?Tu:Eu)(n.a)}function ju(n){return{$:1,a:n}}function Cu(n){return{$:0,a:n}}function _u(n){return w(tf,n,rf)}function Ju(n){return w(hi,n,{$:0,a:n})}function Lu(n){return su(Ju(n))}function Nu(r){return Ai(R([w(di,ci(ju(0)),Lu(Su(r))),w(di,w(di,ci(Cu(r)),_u(function(n){return\"\\\\\"===n})),_u(function(n){return!0})),w(di,ci(Cu(r)),_u(function(n){return\"\\\\\"!==n&&!g(n,r)}))]))}function Xu(n){return n.$?At:kt(n.a)}function Hu(n){return n.b?kt(n.a):At}function Iu(n){return n.b}function Ou(n){var r=n.bZ;return X(n.cH,X(w(wf,\"\",w(Mi,ut,Hu(w(pf,w(_o,Iu,Kt(n.O)),qi)))),w(Et,0,gf(r))))}function Ru(n){var r=n.bz,t=n.U,e=n.S,u=n.T,a=n.R,c=n.H,o=n.aa,n=function(){var n=hf(w(wf,\"\",c));if(n.$)return H;n=n.a;return w(Je,w(_o,Ou,w(_o,kt,Ui(\"filter\"))),n)}(),o=w(Fi,function(n){return w(xu,n.a,n.b)},X(n,R([_(\"silenced\",kt(wu(w(wf,!1,t)))),_(\"inhibited\",kt(wu(w(wf,!1,e)))),_(\"muted\",kt(wu(w(wf,!1,u)))),_(\"active\",kt(wu(w(wf,!0,a)))),_(\"receiver\",yu(r)),_(\"group\",o)])));return 0<Wr(o)?\"?\"+w(jt,\"&\",o):\"\"}function Vu(n){if(-1!==n.$||-1!==n.d.$||-1!==n.e.$)return n;if(-1!==n.e.d.$||n.e.d.a){var r=n.d,t=n.e,e=t.b,u=t.c,a=t.d,c=t.e;return v(_i,1,n.b,n.c,v(_i,0,r.b,r.c,r.d,r.e),v(_i,0,e,u,a,c))}var a,o=n.d,i=n.e,e=i.b,u=i.c,t=(a=i.d).d,r=a.e,c=i.e;return v(_i,0,a.b,a.c,v(_i,1,n.b,n.c,v(_i,0,o.b,o.c,o.d,o.e),t),v(_i,1,e,u,r,c))}function zu(n){if(-1!==n.$||-1!==n.d.$||-1!==n.e.$)return n;if(-1!==n.d.d.$||n.d.d.a){var r=n.d,t=r.d,e=n.e,u=e.b,a=e.c,c=e.d,o=e.e;return v(_i,1,i=n.b,f=n.c,v(_i,0,r.b,r.c,t,e=r.e),v(_i,0,u,a,c,o))}var i=n.b,f=n.c,e=(t=n.d).e,u=(n=n.e).b,a=n.c,c=n.d,o=n.e;return v(_i,0,t.b,t.c,v(_i,1,(r=t.d).b,r.c,r.d,r.e),v(_i,1,i,f,e,v(_i,0,u,a,c,o)))}function Du(n){return y(_t,Bf,mo,n)}function Gu(n){return y(hb,ot,H,n)}function Fu(n){return w(_b,{ce:!1,cO:!1},n)}function Mu(n){return w(jb,n.bo,(r=n.bo,n=w(wf,Jb,Fu(\"[-[\\\\]{}()*+?.,\\\\\\\\^$|#\\\\s]\")),y(Lb,n,w(_o,function(n){return n.cK},Tt(\"\\\\\")),r)));var r}function Qu(n){return n.b?\"{\"+w(jt,\", \",w(Je,Ou,n))+\"}\":\"\"}function Uu(n){return{$:3,a:n}}function Bu(n){return{$:11,a:n}}function qu(n){return{$:20,a:n}}function Ku(n){return{$:9,a:n}}function Pu(n){return{$:8,a:n}}function Wu(n){return y(_t,l(function(n,r){return y(xn,n.a,n.b,r)}),{},n)}function Yu(n){return Wu(R([_(\"name\",gf(n.bo)),_(\"value\",gf(n.bZ)),_(\"isRegex\",Wf(n.bh)),_(\"isEqual\",w(wf,Fb,w(Mi,Wf,n.bg)))]))}function na(n){return n}function ra(n){var r=w(Mb,n,1440)+719468,t=(r<0?r-146096:r)/146097|0,e=r-146097*t,u=(e-(e/1460|0)+(e/36524|0)-(e/146096|0))/365|0;return{aW:(n=e-(365*u+(u/4|0)-(u/100|0)))-((153*(r=(5*n+2)/153|0)+2)/5|0)+1,ap:e=r+(r<10?3:-9),b3:u+400*t+(2<e?0:1)}}function ta(n){return w(ts,4,w(us,as,n))+(\"-\"+w(ts,2,function(n){switch(n){case 0:return 1;case 1:return 2;case 2:return 3;case 3:return 4;case 4:return 5;case 5:return 6;case 6:return 7;case 7:return 8;case 8:return 9;case 9:return 10;case 10:return 11;default:return 12}}(w(Wb,as,n)))+(\"-\"+w(ts,2,w(Bb,as,n))+(\"T\"+w(ts,2,w(qb,as,n))+(\":\"+w(ts,2,w(Pb,as,n))+(\":\"+w(ts,2,w(es,as,n))+(\".\"+w(ts,3,w(Kb,as,n))))))))+\"Z\"}function ea(n){var r=n.bh,t=1===(t=n.bg).$||t.a;return{cH:n.bo,O:!r&&t?0:r||t?r&&t?2:3:1,bZ:n.bZ}}function ua(n){return n<0?At:kt(vs(y(_t,l(function(n,r){var t=n.a,e=n.b,n=r.a,r=r.b;return _(r/e|0?n+(Zt(r/e|0)+(t+\" \")):n,w(wi,e,r))}),_(\"\",ai(n)),$s).a))}function aa(n){return!w(wi,4,n)&&!!w(wi,100,n)||!w(wi,400,n)}function ca(n){return 365*(n-=1)+(w(ps,n,4)-w(ps,n,100)+w(ps,n,400))}function oa(n){switch(n){case 0:return 1;case 1:return 2;case 2:return 3;case 3:return 4;case 4:return 5;case 5:return 6;case 6:return 7;case 7:return 8;case 8:return 9;case 9:return 10;case 10:return 11;default:return 12}}function ia(n){switch(w(Yt,1,n)){case 1:return 0;case 2:return 1;case 3:return 2;case 4:return 3;case 5:return 4;case 6:return 5;case 7:return 6;case 8:return 7;case 9:return 8;case 10:return 9;case 11:return 10;default:return 11}}function fa(n){var r=(e=w(xs,n,146097)).a,n=(t=w(xs,e.b,36524)).a,t=(e=w(xs,t.b,1461)).a,e=w(xs,e.b,365);return 400*r+100*n+4*t+e.a+(e.b?1:0)}function ba(n){var r=(r=fa(n=r=n),{at:n-ca(r),b3:r});return y(ys,r.b3,0,r.at)}function sa(n){return 864e5*(n-719163)}function la(n){return y(Ns,13,as,n)}function da(n){return\"\"===n?mt(\"Should not be empty\"):w(Rs,$t(\"Wrong ISO8601 format\"),pu(n))}function va(n){return L(ko,{R:kt(!0),S:kt(!0),T:kt(!0),U:kt(!0),H:kt(Qu(w(Je,ea,n)))})}function $a(n){return ct(vs(n))?mt(\"Should not be empty\"):wt(n)}function ma(n){var r=n.cH,t=n.O,e=n.bZ,u=function(){switch(t){case 0:return _(!1,!0);case 1:return _(!1,!1);case 2:return _(!0,!0);default:return _(!0,!1)}}(),n=u.a;return{bg:kt(u.b),bh:n,bo:r,bZ:e}}function ha(n){var r=n.bk;return\"\"!==n.bj?mt(\"Please complete adding the matcher\"):r.b?wt(w(Je,ma,r)):mt(\"Matchers are required\")}function pa(n){return y(nl,2,as,x(Ps,11,1,as,y(Ns,2,as,n)))}function ga(n){return y(Ns,11,as,n)}function wa(n){return{$:2,a:n}}function ya(n){var r=n.bd,t=n.aV,e=n.bQ,u=n.aX,a=n.s,c=n.v;return{aT:w(gl,$a,n.aT),aV:w(gl,$a,t),v:c,s:w(gl,ml,a),aX:w(gl,Us(e.bZ),u),bd:r,bQ:w(gl,da,e),J:!1}}function xa(n){return 1!==(n=ha(n)).$?pl:wa(n.a)}function ka(n){return{$:4,a:n}}function Aa(n){return{$:2,a:n}}function Sa(n){return pf(Tl(n))}function Ta(n){return{$:2,a:n}}function Ea(n){return{$:0,a:n}}function Za(n){return{$:0,a:n}}function ja(n){return n.toLowerCase()}function Ca(n){var r=(r=w(wf,H,w(Mi,w(Ul,1,w(wf,Jb,Fu(\"\\\\?\"))),n.a6))).b?_(r.a,r.b.b?kt(w(jt,\"\",r.b)):At):_(\"/\",At);return(r=w(zl,Ql,L(n,{a6:At,au:r.a,cT:r.b}))).$?Zo:r.a}function _a(n){var r=Ca(n);switch(r.$){case 4:return{$:11,a:r.a};case 5:return{$:8,a:r.a};case 2:return{$:9,a:r.a};case 3:return{$:10,a:r.a};case 0:return{$:6,a:r.a};case 6:return Kl;case 8:return ql;case 7:return Pl;default:return Bl}}function Ja(n){return{$:5,a:n}}function La(n){return{$:11,a:n}}function Na(n){return{$:7,a:n}}function Xa(n){return{$:8,a:n}}function Ha(n){return{$:6,a:n}}function Ia(n){var r=Ot(n);return 1===r.$?n:(r=(n=r.a).b,w(Pi,fd(n.a),r))}function Oa(n){return w(ud,R([td(\"alert alert-warning\")]),R([cd(Ia(n))]))}function Ra(n){return ru(tu(w(ld,!1,{cH:n.a,O:0,bZ:n.b})))}function Va(n){return w($d,\"click\",se(n))}function za(n){return w(rd,\"href\",/^javascript:/i.test((n=n).replace(/\\s/g,\"\"))?\"\":n)}function Da(n){return w(Je,function(n){if(n.$)return cd(n.a);n=n.a;return w(wd,R([za(n),Ad(\"_blank\")]),R([cd(n)]))},It(w(xd,kd(n),H)))}function Ga(n){var r=n.a,n=n.b;return w(Ed,H,R([w(Td,R([td(\"text-nowrap\")]),R([cd(r+\":\")])),w(Sd,R([td(\"w-100\")]),Da(n))]))}function Fa(n){return w(Te,\"http://\",n)||w(Te,\"https://\",n)?w(wd,R([td(\"btn btn-outline-info border-0\"),za(n)]),R([w(dd,R([td(\"fa fa-line-chart mr-2\")]),H),cd(\"Source\")])):cd(\"\")}function Ma(n){var r=n.a,n=n.b;return w(ud,R([td(\"btn-group mr-2 mb-2\")]),R([w(ad,R([td(\"btn btn-sm border-right-0 text-muted\"),w(md,\"user-select\",\"initial\"),w(md,\"-moz-user-select\",\"initial\"),w(md,\"-webkit-user-select\",\"initial\"),w(md,\"border-color\",\"#ccc\")]),R([cd(r+'=\"'+n+'\"')])),w(sd,R([td(\"btn btn-sm bg-light btn-outline-secondary\"),Va(Ra(_(r,n))),pd(\"Filter by this label\")]),R([cd(\"+\")]))]))}function Qa(n){return\"#/silences/new?filter=\"+Qi(Qu(n))}function Ua(n){var r=Hu(n.bS.bO);return r.$?w(wd,R([td(\"btn btn-outline-info border-0\"),za(Qa(w(Je,function(n){return y(Bi,n.a,0,n.b)},bt(n.bi))))]),R([w(dd,R([td(\"fa fa-bell-slash-o mr-2\")]),H),cd(\"Silence\")])):(r=r.a,w(wd,R([td(\"btn btn-outline-danger border-0\"),za(\"#/silences/\"+r)]),R([w(dd,R([td(\"fa fa-bell-slash mr-2\")]),H),cd(\"Silenced\")])))}function Ba(n){return w(_d,0,n)}function qa(n){return w(ad,R([td(\"align-self-center mr-2\")]),R([cd(Ld(n.bQ))]))}function Ka(n){return w($d,\"change\",w(fe,n,Gd))}function Pa(n){return{$:2,a:n}}function Wa(n){return{$:3,a:n}}function Ya(n){var r=n.cH,t=n.O,n=n.bZ;return{bg:kt(!t||2===t),bh:2===t||3===t,bo:r,bZ:n}}function nc(n){return Qa(w(Je,function(n){var r=n.bh,t=n.bg,t=1===t.$||t.a;return y(Bi,n.bo,!r&&t?0:r||t?r&&t?2:3:1,n.bZ)},n))}function rc(n){return _(n,!0)}function tc(n){return w(Yd,\"input\",w(fe,rc,w(fe,n,nv)))}function ec(n){return w($d,\"keydown\",w(fe,n,rv))}function uc(n){return w($d,\"keyup\",w(fe,n,rv))}function ac(n){return w(ud,R([td(\"col col-auto\")]),R([w(ud,R([td(\"btn-group mr-2 mb-2\")]),R([w(sd,R([td(\"btn btn-outline-info\"),Va(w(Ud,!0,n))]),R([cd(Ou(n))])),w(sd,R([td(\"btn btn-outline-danger\"),Va(w(Ud,!1,n))]),R([cd(\"×\")]))]))]))}function cc(n){return{$:7,a:n}}function oc(n){return{$:5,a:n}}function ic(n){return w($d,\"mouseenter\",se(n))}function fc(n){return w($d,\"mouseleave\",se(n))}function bc(n){return{$:6,a:n}}function sc(n){return{$:4,a:n}}function lc(n){return{$:3,a:n}}function dc(n){return{$:2,a:n}}function vc(n){return w($d,\"blur\",se(n))}function $c(n){return w(ud,R([td(\"col col-auto\")]),R([w(ud,R([td(\"btn-group mr-2 mb-2\")]),R([w(sd,R([td(\"btn btn-outline-info\"),Va(w(bv,!0,n))]),R([cd(n)])),w(sd,R([td(\"btn btn-outline-danger\"),Va(w(bv,!1,n))]),R([cd(\"×\")]))]))]))}function mc(n){return{$:3,a:n}}function hc(n){return{$:5,a:n}}function pc(n){return{$:4,a:n}}function gc(n){return{$:1,a:n}}function wc(n){return{$:6,a:n}}function yc(n){return{$:4,a:n}}function xc(n){return{$:0,a:n}}function kc(n){return{$:10,a:n}}function Ac(n){switch(n){case 0:return 1;case 1:return 2;case 2:return 3;case 3:return 4;case 4:return 5;case 5:return 6;default:return 7}}function Sc(n){return y(Ns,2,as,n)}function Tc(n){return w(ud,R([td(\"date text-muted\")]),R([cd(n)]))}function Ec(n){var r,t=w(wf,fi(0),n.ap);return w(ud,R([td(\"calendar_ month\")]),R([(r=t,w(ud,R([td(\"row month-header\")]),R([w(ud,R([td(\"prev-month d-flex-center\"),Va(Fv)]),R([w(Qv,R([td(\"arrow\")]),R([w(dd,R([td(\"fa fa-angle-left fa-3x cursor-pointer\")]),H)]))])),w(ud,R([td(\"month-text d-flex-center\")]),R([cd(Zt(w(us,as,r))),w(Mv,H,H),cd(function(n){switch(n){case 0:return\"January\";case 1:return\"February\";case 2:return\"March\";case 3:return\"April\";case 4:return\"May\";case 5:return\"June\";case 6:return\"July\";case 7:return\"August\";case 8:return\"September\";case 9:return\"October\";case 10:return\"November\";default:return\"December\"}}(w(Wb,as,r)))])),w(ud,R([td(\"next-month d-flex-center\"),Va(Gv)]),R([w(Qv,R([td(\"arrow\")]),R([w(dd,R([td(\"fa fa-angle-right fa-3x cursor-pointer\")]),H)]))]))]))),w(Dv,n,t)]))}function Zc(n){return w(ir,\"maxlength\",Zt(n))}function jc(n){return n.J?w(ud,H,R([w(ud,R([td(\"modal fade show\"),w(md,\"display\",\"block\")]),R([w(ud,R([td(\"modal-dialog modal-dialog-centered\")]),R([w(ud,R([td(\"modal-content\")]),R([w(ud,R([td(\"modal-header\")]),R([w(sd,R([td(\"close ml-auto\"),Va(xc(Cv))]),R([cd(\"x\")]))])),w(ud,R([td(\"modal-body\")]),R([w(id,kc,(n=n.v,w(ud,R([td(\"w-100 container\")]),R([Ec(n),w(ud,R([td(\"pt-4 row justify-content-center\")]),R([w(Pv,n,0),w(Pv,n,1)]))]))))])),w(ud,R([td(\"modal-footer\")]),R([w(sd,R([td(\"ml-2 btn btn-outline-success mr-auto\"),Va(xc(Cv))]),R([cd(\"Cancel\")])),w(sd,R([td(\"ml-2 btn btn-primary\"),Va(xc(_v))]),R([cd(\"Set Date/Time\")]))]))]))]))])),w(ud,R([td(\"modal-backdrop fade show\")]),H)])):w(ud,R([w(md,\"clip\",\"rect(0,0,0,0)\"),w(md,\"position\",\"fixed\")]),R([w(ud,R([td(\"modal fade\")]),H),w(ud,R([td(\"modal-backdrop fade\")]),H)]))}function Cc(n){return{$:4,a:n}}function _c(n){return{$:2,a:n}}function Jc(n){return{$:1,a:n}}function Lc(n){return{$:0,a:n}}function Nc(n){return w(ir,\"rows\",Zt(n))}function Xc(n){return{$:0,a:n}}function Hc(n){var r=w(jt,\"/\",R([\"#/silences\",n.bd,\"edit\"])),r=w(wd,R([td(\"btn btn-outline-info border-0\"),za(r)]),R([cd(\"Edit\")]));return n.bS.bR?r:w(wd,R([td(\"btn btn-outline-info border-0\"),za(w(k$,n.bk,n.aT))]),R([cd(\"Recreate\")]))}function Ic(n){var r=1===(r=n.bg).$||r.a;return w(jt,!n.bh&&r?\"=\":n.bh||r?n.bh&&r?\"=~\":\"!~\":\"!=\",R([n.bo,w(Et,0,gf(n.bZ))]))}function Oc(n){var r=1===(r=n.bg).$||r.a,r=Me(ka(w(ld,!1,{cH:n.bo,O:!n.bh&&r?0:n.bh||r?n.bh&&r?2:3:1,bZ:n.bZ})));return w(Wv,kt(r),Ic(n))}function Rc(n){if(1===n.$)return w(ud,R([w(md,\"clip\",\"rect(0,0,0,0)\"),w(md,\"position\",\"fixed\")]),R([w(ud,R([td(\"modal fade\")]),H),w(ud,R([td(\"modal-backdrop fade\")]),H)]));var r=n.a.bq,t=n.a.ca,e=n.a.a4,n=n.a.c6;return w(ud,H,R([w(ud,R([td(\"modal fade show\"),w(md,\"display\",\"block\")]),R([w(ud,R([td(\"modal-dialog modal-dialog-centered\")]),R([w(ud,R([td(\"modal-content\")]),R([w(ud,R([td(\"modal-header\")]),R([w(A$,R([td(\"modal-title\")]),R([cd(n)])),w(sd,R([td(\"close\"),Va(r)]),R([cd(\"×\")]))])),w(ud,R([td(\"modal-body\")]),R([t])),w(ud,R([td(\"modal-footer\")]),R([e]))]))]))])),w(ud,R([td(\"modal-backdrop fade show\")]),H)]))}function Vc(n){return{$:5,a:n}}function zc(n){switch(n){case 1:return\"active\";case 2:return\"pending\";default:return\"expired\"}}function Dc(n){return w(jd,H,R([w(ud,R([td(\"\")]),R([w(J$,R([td(\"\")]),R([cd(\"Name: \")])),cd(n.bo)])),w(ud,R([td(\"\")]),R([w(J$,R([td(\"\")]),R([cd(\"Address: \")])),cd(n.aH)]))]))}function Gc(n){var r=n.bo,t=n.bS,e=n.bs;return w(ad,H,R([w(X$,H,R([cd(\"Cluster Status\")])),function(){if(r.$)return cd(\"\");var n=r.a;return w(ud,R([td(\"form-group row\")]),R([w(J$,R([td(\"col-sm-2\")]),R([cd(\"Name:\")])),w(ud,R([td(\"col-sm-10\")]),R([cd(n)]))]))}(),w(ud,R([td(\"form-group row\")]),R([w(J$,R([td(\"col-sm-2\")]),R([cd(\"Status:\")])),w(ud,R([td(\"col-sm-10\")]),R([w(ad,R([(n=R([_(\"badge\",!0),function(){switch(t){case 0:return _(\"badge-success\",!0);case 1:return _(\"badge-warning\",!0);default:return _(\"badge-danger\",!0)}}()]),td(w(jt,\" \",w(Je,ut,w(pf,Iu,n)))))]),R([cd(function(n){switch(n){case 0:return\"ready\";case 1:return\"settling\";default:return\"disabled\"}}(t))]))]))])),function(){if(e.$)return cd(\"\");var n=e.a;return w(ud,R([td(\"form-group row\")]),R([w(J$,R([td(\"col-sm-2\")]),R([cd(\"Peers:\")])),w(gd,R([td(\"col-sm-10\")]),w(Je,Dc,n))]))}()]))}function Fc(n){return w(ud,H,R([w(Av,H,R([cd(\"Status\")])),w(ud,R([td(\"form-group row\")]),R([w(J$,R([td(\"col-sm-2\")]),R([cd(\"Uptime:\")])),w(ud,R([td(\"col-sm-10\")]),R([cd(Hs(n.bY))]))])),Gc(n.aR),(r=n.b$,w(ad,H,R([w(X$,H,R([cd(\"Version Information\")])),w(ud,R([td(\"form-group row\")]),R([w(J$,R([td(\"col-sm-2\")]),R([cd(\"Branch:\")])),w(ud,R([td(\"col-sm-10\")]),R([cd(r.aN)]))])),w(ud,R([td(\"form-group row\")]),R([w(J$,R([td(\"col-sm-2\")]),R([cd(\"BuildDate:\")])),w(ud,R([td(\"col-sm-10\")]),R([cd(r.aO)]))])),w(ud,R([td(\"form-group row\")]),R([w(J$,R([td(\"col-sm-2\")]),R([cd(\"BuildUser:\")])),w(ud,R([td(\"col-sm-10\")]),R([cd(r.aP)]))])),w(ud,R([td(\"form-group row\")]),R([w(J$,R([td(\"col-sm-2\")]),R([cd(\"GoVersion:\")])),w(ud,R([td(\"col-sm-10\")]),R([cd(r.a9)]))])),w(ud,R([td(\"form-group row\")]),R([w(J$,R([td(\"col-sm-2\")]),R([cd(\"Revision:\")])),w(ud,R([td(\"col-sm-10\")]),R([cd(r.bF)]))])),w(ud,R([td(\"form-group row\")]),R([w(J$,R([td(\"col-sm-2\")]),R([cd(\"Version:\")])),w(ud,R([td(\"col-sm-10\")]),R([cd(r.b_)]))]))]))),(n=n.aU.br,w(ud,H,R([w(X$,H,R([cd(\"Config\")])),w(I$,R([td(\"p-4\"),w(md,\"background\",\"#f7f7f9\"),w(md,\"font-family\",\"monospace\")]),R([w(H$,H,R([cd(n)]))]))])))]));var r}function Mc(n){var r,t,e,u,a=n.cX;switch(a.$){case 8:return w(id,Ja,(u=n.cZ,w(ud,H,R([w(ud,R([td(\"no-gutters\")]),R([w(Dd,R([Rd(\"fieldset\")]),R([cd(\"First day of the week:\")])),w(Tv,R([Vd(\"fieldset\")]),R([y(Ev,\"Monday\",!u.cx,at),y(Ev,\"Sunday\",1===u.cx,at),y(Ev,\"Saturday\",2===u.cx,at)])),w(ev,R([td(\"form-text text-muted\")]),R([cd(\"Note: This setting is saved in local storage of your browser\")]))]))]))));case 6:return w(bd,Fc,n.bS.bT);case 5:return function(n){var r=n.bM,t=n.aJ,e=n.aD,u=n.bK;switch(r.$){case 3:var a=r.a;return x(N$,e,t,a,!!u);case 0:case 1:return od;default:return Oa(r.a)}}(n.c1);case 0:return w(kv,n.b5,a.a);case 4:return t=(r=n.c0).a$,e=r.bV,u=r.ay,r=r.bK,w(ud,H,R([w(ud,R([td(\"mb-4\")]),R([w(Dd,R([td(\"mb-2\"),Rd(\"filter-bar-matcher\")]),R([cd(\"Filter\")])),w(id,w(_o,ka,Me),w(cv,{c_:!1},t))])),y(h$,j$,e,u),x(p$,T$,r,e,u)]));case 3:return w(id,Fe,x(m$,At,a.a,n.ck,n.c$));case 2:return w(id,Fe,x(m$,kt(a.a),ed,\"\",n.c$));case 7:return od;default:return Sv}}function Qc(n){return w(O$,R([td(\"navbar navbar-expand-md navbar-light bg-light mb-5 pt-3 pb-3\"),w(md,\"border-bottom\",\"1px solid rgba(0, 0, 0, .125)\")]),R([w(R$,R([td(\"container\")]),R([w(wd,R([td(\"navbar-brand\"),za(\"#\")]),R([cd(\"Alertmanager\")])),w(gd,R([td(\"navbar-nav\")]),w(Je,Q$(n),U$)),function(){switch(n.$){case 2:case 3:return cd(\"\");default:return w(ud,R([td(\"form-inline ml-auto\")]),R([w(wd,R([td(\"btn btn-outline-info\"),za(\"#/silences/new\")]),R([cd(\"New Silence\")]))]))}}()]))]))}function Uc(n){return{$:17,a:n}}function Bc(n){return{$:19,a:n}}function qc(n){return{$:18,a:n}}function Kc(n){return Yn(fr(n))}function Pc(r){return w(ud,H,R([w(ud,H,R([w(K$,(n=r.cI)+\"lib/bootstrap-4.6.2-dist/css/bootstrap.min.css\",Uc),w(K$,n+\"lib/font-awesome-4.7.0/css/font-awesome.min.css\",qc),w(K$,n+\"lib/elm-datepicker/css/elm-datepicker.css\",Bc)])),function(){var n=J(r.cb,r.cy,r.cn);n:for(;;){r:for(;;){t:for(;;)switch(n.a.$){case 3:switch(n.b.$){case 3:switch(n.c.$){case 3:return w(ud,H,R([Qc(r.cX),w(ud,R([td(\"container pb-4\")]),R([Mc(r)]))]));case 2:break r;default:break t}case 2:break n;default:if(2===n.c.$)break r;break t}case 2:return w(B$,r,n.a.a);default:if(2===n.b.$)break n;if(2===n.c.$)break r;break t}return cd(\"\")}return w(B$,r,n.c.a)}return w(B$,r,n.b.a)}()]));var n}var Wc,Yc,no,ro,to,eo,uo,ao,co=Vn(\"Task\"),oo=l(function(n,r){return co(w(Ne,n,r))}),Rn=function(r){var n=r.cQ,u=r.cR,t=function(){t.a(n(Nr()))};return _r({ax:function(e){return t.a=e,Ir.addEventListener(\"popstate\",t),~Ir.navigator.userAgent.indexOf(\"Trident\")&&Ir.addEventListener(\"hashchange\",t),l(function(n,r){var t;r.ctrlKey||r.metaKey||r.shiftKey||1<=r.button||n.target||n.download||(r.preventDefault(),t=n.href,r=Nr(),n=Ee(t).a,e(u(n&&r.by===n.by&&r.bc===n.bc&&r.bu.a===n.bu.a?{$:0,a:n}:{$:1,a:t})))})},cD:function(n){return y(r.cD,n,Nr(),t)},db:r.db,c9:r.c9,c4:r.c4})},io={$:1},fo={$:3},bo=dn,so=cn,lo={$:0},vo={$:-2},$o=vo,mo=$o,ho=l(function(n,r){return{aE:mo,aF:At,aI:lo,aJ:lo,cq:r,a$:Oe(H),ba:{aL:!1,a_:\"\",cv:H,a3:!1,cH:n,cJ:mo,bl:H,bm:At,bE:!1},cH:n,bA:{a_:\"\",cH:n,bl:H,bB:H,bE:!1,bJ:At,bL:!1},bV:0}}),po={$:0},go=l(function(n,r){return{aD:At,aJ:lo,a$:Oe(H),a0:po,cx:r,a5:Ve(r),cH:n,bN:lo}}),wo={bT:lo},yo=l(function(n,r){return n<1?r:y(he,0,-n,r)}),xo=un,ko={ah:!1,aa:At,bz:At,R:At,S:At,T:At,U:At,H:At},Ao={$:6},So={$:2},To={$:3},Eo=l(function(n,r){return{$:6,a:n,b:r}}),Zo={$:1},jo={$:8},Co={$:6},_o=d(function(n,r,t){return r(n(t))}),Jo=function(r){return w(oo,Ze,Tn(function(n){try{Ir.location=r}catch(n){Kn.location.reload(!1)}}))},Lo=Dn,No=zn,Xo=No(H),Ho=Xr,Io=d(function(n,r,t){return{aJ:t,bi:n,bz:r}}),Oo=l(function(n,r){return r.b?y(_e,ot,r,n):n}),Ro=l(function(n,r){return r=w(Je,n,r),y(_e,Oo,H,r)}),Dn=o(function(n,r,t,e,u,a,c,o,i){return{aK:t,aX:o,a1:u,a7:r,bi:n,bB:e,bQ:a,bS:i,bX:c}}),Xr=t(function(n,r,t,e){return{be:t,bn:e,bO:r,bR:n}}),Vo=function(n){return{$:7,b:n}},zo=be(i),i=d(function(n,r,t){return w(zo,w(so,n,r),t)}),Do=fn,Go=function(n){return{$:1,a:n}},fn=w(Do,function(n){switch(n){case\"unprocessed\":return se(0);case\"active\":return se(1);case\"suppressed\":return se(2);default:return Go(\"Unknown type: \"+n)}},Ao),Xr=y(i,\"mutedBy\",Vo(Ao),y(i,\"inhibitedBy\",Vo(Ao),y(i,\"silencedBy\",Vo(Ao),y(i,\"state\",fn,se(Xr))))),Fo=y(i,\"name\",Ao,se(function(n){return{bo:n}})),Mo=l(function(n,r){return{$:1,a:n,b:r}}),Qo=d(function(n,r,t){return{$:0,a:n,b:r,c:t}}),Uo=l(function(u,n){var a=n;return function(n){var r=a(n);if(1===r.$)return w(Mo,r.a,r.b);var t=r.a,n=r.c,n=u(r.b)(n);if(1===n.$){var e=n.a;return w(Mo,t||e,n.b)}e=n.a;return y(Qo,t||e,n.b,n.c)}}),Bo=Uo,qo=l(function(n,r){return{$:1,a:n,b:r}}),Ko=t(function(n,r,t,e){return{aS:r,ci:e,bv:t,bG:n}}),Po={$:0},Wo=l(function(n,r){return w(qo,Po,x(Ko,n.bG,n.aS,r,n.c))}),Yo=(Wc={$:10},function(n){return g(me(n.a),n.b)?y(Qo,!1,0,n):w(Mo,!1,w(Wo,n,Wc))}),ni=zr,ri=c(function(n,r,t,e,u){for(;;){var a=y(ni,n,r,u.a);if(g(a,-1))return y(Qo,T(u.b,r)<0,0,{aS:e,c:u.c,d:u.d,b:r,bG:t,a:u.a});u=(e=g(a,-2)?(n=n,r=r+1,t=t+1,1):(n=n,r=a,t=t,e+1),u)}}),zr=function(r){return function(n){return v(ri,r,n.b,n.bG,n.aS,n)}},ti=zr,ei=l(function(u,n){var a=n;return function(n){var r=a(n);if(1===r.$)return w(Mo,r.a,r.b);var t=r.b,e=r.c;return y(Qo,r.a,w(u,y(he,n.b,e.b,n.a),t),e)}}),ui=function(n){return w(ei,$t,n)},ai=B,ci=iu,oi=function(n){return 0!==n.length&&!/[\\sxbo]/.test(n)&&(n=+n)==n?kt(n):At},ii=w(Bo,function(n){if(9<me(n))return ou(\"Expected at most 9 digits, but got \"+Zt(me(n)));var r=oi(\"0.\"+n);return r.$?ou('Invalid float: \"'+n+'\"'):ci(ai(1e3*r.a))},ui(ti(rt))),fi=at,bi=e(function(n,r,t,e,u,a){return fi(n+60*r*60*1e3+60*(t-a)*1e3+1e3*e+u)}),si=d(function(a,n,r){var c=n,o=r;return function(n){var r=c(n);if(1===r.$)return w(Mo,r.a,r.b);var t=r.a,e=r.b,n=o(r.c);if(1===n.$){var u=n.a;return w(Mo,t||u,n.b)}u=n.a,r=n.c;return y(Qo,t||u,w(a,e,n.b),r)}}),li=l(function(n,r){return y(si,$t,n,r)}),di=li,vi=l(function(n,r){return y(si,qt,n,r)}),$i=l(function(t,n){var e=n;return function(n){var r=e(n);if(r.$)return w(Mo,r.a,r.b);n=r.c;return y(Qo,r.a,t(r.b),n)}}),mi=$i,hi=l(function(n,r){return{$:0,a:n,b:r}}),pi=Vr,gi=su,wi=M,yi=S,Vr=w(Bo,function(n){var e=n.a,u=n.b,a=n.c;if(a<0)return du(a);function r(n){var r=31536e6*(e-1970),t=864e5*((u<3||!vu(e)?a-1:a)+($u(e)-$u(1970)));return ci(n+r+t)}switch(u){case 1:return 31<a?du(a):r(0);case 2:return 29<a||29===a&&!vu(e)?du(a):r(26784e5);case 3:return 31<a?du(a):r(50976e5);case 4:return 30<a?du(a):r(7776e6);case 5:return 31<a?du(a):r(10368e6);case 6:return 30<a?du(a):r(130464e5);case 7:return 31<a?du(a):r(156384e5);case 8:return 31<a?du(a):r(183168e5);case 9:return 30<a?du(a):r(209952e5);case 10:return 31<a?du(a):r(235872e5);case 11:return 30<a?du(a):r(262656e5);case 12:return 31<a?du(a):r(288576e5);default:return ou('Invalid month: \"'+Zt(u)+'\"')}},w(vi,w(vi,w(vi,ci(d(J)),w(di,fu(4),lu(\"-\"))),w(di,fu(2),lu(\"-\"))),fu(2))),xi=l(function(n,r){return{$:2,a:n,b:r}}),ki=d(function(n,r,t){for(;;){if(!t.b)return w(Mo,!1,r);var e,u=t.b,a=(0,t.a)(n);if(!a.$)return e=a;if((e=a).a)return e;n=n,r=w(xi,r,e.b),t=u}}),Ai=mu,Si=d(function(n,r,t){return n*(60*r+t)}),Ti=w(Bo,function(n){return Ai(R([w(vi,w(vi,w(vi,w(vi,w(vi,w(di,ci(bi(n)),lu(\"T\")),w(di,fu(2),lu(\":\"))),w(di,fu(2),lu(\":\"))),fu(2)),Ai(R([w(vi,w(di,ci(at),lu(\".\")),ii),ci(0)]))),Ai(R([w(mi,function(n){return 0},lu(\"Z\")),w(vi,w(vi,w(vi,ci(Si),Ai(R([w(mi,function(n){return 1},lu(\"+\")),w(mi,function(n){return-1},lu(\"-\"))]))),w(di,fu(2),lu(\":\"))),fu(2))]))),w(di,ci(f(bi,n,0,0,0,0,0)),Yo)]))},Vr),Ei=d(function(n,r,t){return{aS:r,bv:t,bG:n}}),Zi=l(function(n,r){n:for(;;)switch(n.$){case 0:return r;case 1:var t=n.b;n=n.a,r=w(ot,t,r);continue n;default:t=n.b;n=n.a,r=w(Zi,t,r);continue n}}),ji=l(function(n,r){r=n({aS:1,c:H,d:1,b:0,bG:1,a:r});return r.$?mt(w(Zi,r.b,H)):wt(r.b)}),Ci=l(function(n,r){r=w(ji,n,r);return r.$?mt(w(Je,hu,r.a)):wt(r.a)}),M=w(Do,function(n){var r=pu(n);return r.$?Go(\"Invalid date: \"+n):se(r.a)},Ao),_i=c(function(n,r,t,e,u){return{$:-1,a:n,b:r,c:t,d:e,e:u}}),Ji=c(function(n,r,t,e,u){if(-1!==u.$||u.a){if(-1!==e.$||e.a||-1!==e.d.$||e.d.a)return v(_i,n,r,t,e,u);var a=e.d,c=e.e;return v(_i,0,e.b,e.c,v(_i,1,a.b,a.c,a.d,a.e),v(_i,1,r,t,c,u))}var o=u.b,i=u.c,a=u.d,u=u.e;if(-1!==e.$||e.a)return v(_i,n,o,i,v(_i,0,r,t,e,a),u);var c;return v(_i,0,r,t,v(_i,1,e.b,e.c,e.d,c=e.e),v(_i,1,o,i,a,u))}),Li=j,Ni=d(function(n,r,t){if(-2===t.$)return v(_i,0,n,r,vo,vo);var e=t.a,u=t.b,a=t.c,c=t.d,o=t.e;switch(w(Li,n,u)){case 0:return v(Ji,e,u,a,y(Ni,n,r,c),o);case 1:return v(_i,e,u,r,c,o);default:return v(Ji,e,u,a,c,y(Ni,n,r,o))}}),Xi=d(function(n,r,t){t=y(Ni,n,r,t);if(-1!==t.$||t.a)return t;return v(_i,1,t.b,t.c,t.d,t.e)}),Hi=function(n){return{$:12,b:n}},S=function(n){return w(fe,gu,Hi(n))},Ii=function(n){return{$:9,c:n}},Oi=function(n){return{$:15,g:n}},Vr=function(n){return Oi(R([Ii(At),w(fe,kt,n)]))},Ri={$:5},Vi=d(function(r,t,e){return w(Do,function(n){n=w(bo,r,n);if(n.$)return se(e);n=n.a,n=w(bo,Oi(R([t,Ii(e)])),n);return n.$?Go(Vt(n.a)):se(n.a)},Ri)}),j=t(function(n,r,t,e){return w(zo,y(Vi,w(so,n,Ri),r,t),e)}),zi=y(i,\"status\",Xr,y(i,\"endsAt\",M,y(i,\"updatedAt\",M,y(i,\"startsAt\",M,y(i,\"fingerprint\",Ao,y(i,\"receivers\",Vo(Fo),y(i,\"annotations\",S(Ao),x(j,\"generatorURL\",Vr(Ao),At,y(i,\"labels\",S(Ao),se(Dn)))))))))),Di=y(i,\"alerts\",Vo(zi),y(i,\"receiver\",Fo,y(i,\"labels\",S(Ao),se(Io)))),Gi=d(function(n,r,t){r=n(r);return r.$?t:w(ot,r.a,t)}),Fi=l(function(n,r){return y(_e,Gi(n),H,r)}),Mi=l(function(n,r){return r.$?At:kt(n(r.a))}),Qi=function(n){return encodeURIComponent(n)},Ui=l(_),Bi=d(function(n,r,t){return{cH:n,O:r,bZ:t}}),qi=R([_(\"=~\",2),_(\"!~\",3),_(\"=\",0),_(\"!=\",1)]),Ki=ln,Pi=K,Wi=t(function(n,r,t,e){for(;;){var u=t(r)(e);if(u.$){a=u.a;return w(Mo,n||a,u.b)}var a=u.a,c=u.b,u=u.c;if(c.$)return y(Qo,n||a,c.a,u);n=n||a,r=c.a,t=t,e=u}}),Yi=l(function(r,t){return function(n){return x(Wi,!1,r,t,n)}}),nf=l(function(n,r){return w(Yi,n,function(n){return w(mi,Zu,r(n))})}),rf={$:11},tf=l(function(t,e){return function(n){var r=y(ni,t,n.b,n.a);return g(r,-1)?w(Mo,!1,w(Wo,n,e)):g(r,-2)?y(Qo,!0,0,{aS:1,c:n.c,d:n.d,b:n.b+1,bG:n.bG+1,a:n.a}):y(Qo,!0,0,{aS:n.aS+1,c:n.c,d:n.d,b:r,bG:n.bG,a:n.a})}}),ef={$:7},uf=l(function(n,r){n:for(;;){if(-2===r.$)return At;var t=r.c,e=r.d,u=r.e;switch(w(Li,n,r.b)){case 0:n=n,r=e;continue n;case 1:return kt(t);default:n=n,r=u;continue n}}}),af=l(function(n,r){return!w(uf,n,r).$}),cf=l(function(n,r){return w(af,n,r)}),of=u(function(n,r,t,e,u,a,c){for(;;){var o=y(ni,n,r,u);if(g(o,-1))return{aS:e,c:c,d:a,b:r,bG:t,a:u};c=(a=(u=(e=g(o,-2)?(n=n,r=r+1,t=t+1,1):(n=n,r=o,t=t,e+1),u),a),c)}}),ff=w(vi,w(vi,w(vi,ci(Bi),(ro={aZ:ef,cE:(no={cE:ku,cV:mo,bP:ku}).cE,cV:no.cV,bP:no.bP},function(n){var r=y(ni,ro.bP,n.b,n.a);if(g(r,-1))return w(Mo,!1,w(Wo,n,ro.aZ));var t=g(r,-2)?b(of,ro.cE,n.b+1,n.bG+1,1,n.a,n.d,n.c):b(of,ro.cE,r,n.bG,n.aS+1,n.a,n.d,n.c),r=y(he,n.b,t.b,n.a);return w(cf,r,ro.cV)?w(Mo,!1,w(Wo,n,ro.aZ)):y(Qo,!0,r,t)})),Ai(w(Je,function(n){var r=n.a;return w(di,ci(n.b),Au(r))},qi))),(Yc='\"',w(Bo,function(n){n=w(Ki,Ao,n);return n.$?ou(\"Invalid string\"):ci(n.a)},ui(w(di,w(di,ci(0),Lu(Su(Yc))),w(nf,Yc,Nu)))))),bf=l(function(n,r){return r}),sf=l(function(n,r){return y(si,bf,n,r)}),lf=c(function(n,r,t,e,u){return w(sf,r,mu(R([w(sf,e,w(sf,r,w($i,function(n){return Eu(w(ot,n,u))},t))),w($i,function(n){return Tu(It(u))},n)])))}),df=t(function(n,r,t,e){return mu(R([w($i,function(n){return Eu(w(ot,n,e))},w(li,r,w(li,n,w(li,t,n)))),w($i,function(n){return Tu(It(e))},iu(0))]))}),vf=c(function(n,r,t,e,u){n=w($i,function(n){return Tu(It(u))},n);return w(sf,r,mu(R([w(sf,e,w(sf,r,mu(R([w($i,function(n){return Eu(w(ot,n,u))},t),n])))),n])))}),$f=c(function(r,t,e,u,a){return mu(R([w(Uo,function(n){switch(a){case 0:return w(Yi,R([n]),x(lf,r,t,e,u));case 1:return w(Yi,R([n]),x(vf,r,t,e,u));default:return w(li,w(sf,t,w(sf,u,w(sf,t,w(Yi,R([n]),y(df,t,e,u))))),r)}},e),w($i,function(n){return H},r)]))}),mf=zr(function(n){return\" \"===n||\"\\n\"===n||\"\\r\"===n}),zr=w(vi,ci(at),w(di,(to={co:Ju((to={co:\"}\",cG:ff,cY:\",\",c2:mf,bP:\"{\",c7:0}).co),cG:to.cG,cY:Ju(to.cY),c2:to.c2,bP:Ju(to.bP),c7:function(n){switch(n){case 0:return 0;case 1:return 1;default:return 2}}(to.c7)},w(sf,su(to.bP),w(sf,to.c2,v($f,su(to.co),to.c2,to.cG,su(to.cY),to.c7)))),Yo)),hf=w(_o,Ci(zr),Xu),pf=l(function(t,n){return y(_e,l(function(n,r){return t(n)?w(ot,n,r):r}),H,n)}),gf=yn,wf=l(function(n,r){return r.$?n:r.a}),yf={$:0},xf=l(function(n,r){return{$:4,a:n,b:r}}),kf=function(n){return{$:3,a:n}},Af=function(n){return{$:0,a:n}},Sf={$:2},Tf={$:1},Ef=function(n){return!n.$},Zf=function(n){return 1===n.$},jf=l(function(n,r){return r.$?mt(r.a):wt(n(r.a))}),Cf=u(function(n,r,t,e,u,a,c){if(-1!==a.$||a.a){n:for(;-1===c.$&&1===c.a;){if(-1===c.d.$){if(1!==c.d.a)break n;return zu(r)}return zu(r)}return r}return v(_i,t,a.b,a.c,a.d,v(_i,0,e,u,a.e,c))}),_f=function(n){if(-1!==n.$||-1!==n.d.$)return vo;var r=n.a,t=n.b,e=n.c,u=n.d,a=u.d,c=n.e;if(1!==u.a)return v(_i,r,t,e,_f(u),c);if(-1!==a.$||a.a){var o=Vu(n);if(-1!==o.$)return vo;n=o.e;return v(Ji,o.a,o.b,o.c,_f(o.d),n)}return v(_i,r,t,e,_f(u),c)},Jf=l(function(n,r){if(-2===r.$)return vo;var t=r.a,e=r.b,u=r.c,a=r.d,c=r.e;if(T(n,e)<0){if(-1!==a.$||1!==a.a)return v(_i,t,e,u,w(Jf,n,a),c);var o=a.d;if(-1!==o.$||o.a){var i=Vu(r);if(-1!==i.$)return vo;var f=i.e;return v(Ji,i.a,i.b,i.c,w(Jf,n,i.d),f)}return v(_i,t,e,u,w(Jf,n,a),c)}return w(Lf,n,b(Cf,n,r,t,e,u,a,c))}),Lf=l(function(n,r){if(-1!==r.$)return vo;var t=r.a,e=r.b,u=r.c,a=r.d,c=r.e;if(g(n,e)){r=function(n){for(;;){if(-1!==n.$||-1!==n.d.$)return n;n=n.d}}(c);return-1!==r.$?vo:v(Ji,t,r.b,r.c,a,_f(c))}return v(Ji,t,e,u,a,w(Jf,n,c))}),Nf=l(function(n,r){r=w(Jf,n,r);if(-1!==r.$||r.a)return r;return v(_i,1,r.b,r.c,r.d,r.e)}),Xf=d(function(n,r,t){r=r(w(uf,n,t));return r.$?w(Nf,n,t):y(Xi,n,r.a,t)}),Hf=function(n){return{$:0,b:\"text\",a:n}},If=at,Of=c(function(n,r,t,e,u){return If({ca:e,cr:(a=u,Hf(function(n){n=w(Ki,a,n.ca);return 1!==n.$?wt(n.a):mt(Vt(n.a))})),cz:r,cM:n,c5:At,da:t,dd:!1});var a}),Rf=l(function(n,r){return v(Of,\"GET\",H,n,yf,r)}),Vf=w(_o,Ki(w(so,\"error\",Ao)),Xu),zf=d(function(n,r,t){return n(r(t))}),Df=Zn,Gf=l(function(n,r){return co(w(Df,w(zf,w(zf,je,n),mt),w(Le,w(zf,w(zf,je,n),wt),r)))}),Ff=l(function(n,r){return w(Gf,n,w(Qr,r,At))})(function(n){return 1!==n.$?cu(n.a):nu(function(n){switch(n.$){case 1:return\"Timeout exceeded\";case 2:return\"Network error\";case 3:var r=n.a;return w(wf,Zt(r.bS.ch)+\" \"+r.bS.cL,Vf(r.ca));case 4:return\"Unexpected response from api: \"+n.a;default:return\"Malformed url: \"+n.a}}(n.a))}),Mf=l(function(n,r){r=w(jt,\"/\",R([n,\"alerts\",\"groups\"+Ru(r)]));return Ff(w(Rf,r,Vo(Di)))}),Qf=l(function(n,r){r=w(jt,\"/\",R([n,\"alerts\"+Ru(r)]));return Ff(w(Rf,r,Vo(zi)))}),Uf=w(_o,function(n){return Ff(w(Rf,n+\"/receivers\",Vo(Fo)))},Lo(function(n){return{$:0,a:n}})),Bf=l(function(n,r){return y(Xi,n,0,r)}),qf=l(function(t,n){return y(_e,l(function(n,r){return y(Xf,t(n),w(_o,Mi(ot(n)),w(_o,wf(R([n])),kt)),r)}),$o,n)}),Kf=l(function(n,r){for(;;){if(!r.b)return!1;var t=r.b;if(n(r.a))return!0;n=n,r=t}}),Pf=l(function(r,n){return w(Kf,function(n){return g(n,r)},n)}),Wf=yn,Yf=Qn(\"persistGroupExpandAll\",Wf),nb=l(function(n,r){return w(Nf,n,r)}),rb=l(function(n,r){return L(r,{cv:function(n){if(1===n.$)return R([\"alertname\"]);n=n.a;return w(pf,w(_o,me,ae(0)),w(Ct,\",\",n))}(n.aa)})}),tb=l(function(n,r){return r.$?At:n(r.a)}),eb=l(function(n,r){return L(r,{bk:w(wf,H,w(tb,hf,n.H))})}),ub=l(function(n,r){var t=r.bz,e=r.ah,u=r.S,a=r.T,c=r.R,o=r.H,i=r.aa,e=w(Fi,function(n){return w(xu,n.a,n.b)},R([_(\"silenced\",kt(wu(w(wf,!1,r.U)))),_(\"inhibited\",kt(wu(w(wf,!1,u)))),_(\"muted\",kt(wu(w(wf,!1,a)))),_(\"active\",kt(wu(w(wf,!0,c)))),_(\"filter\",yu(o)),_(\"receiver\",yu(t)),_(\"group\",i),_(\"customGrouping\",e?kt(\"true\"):At)]));return 0<Wr(e)?X(n,\"?\"+w(jt,\"&\",e)):n}),ab={$:4},cb=Rr(\"focus\"),ob=l(function(n,r){switch(n.$){case 0:return J(L(r,{bj:n.a?\"\":r.bj,bk:w(Pf,t=n.b,r.bk)?r.bk:X(r.bk,R([t]))}),!0,w(Gf,$t(ab),cb(\"filter-bar-matcher\")));case 1:var t=n.b;return J(L(r,{bj:n.a?Ou(t):r.bj,bk:w(pf,yi(t),r.bk)}),!0,w(Gf,$t(ab),cb(\"filter-bar-matcher\")));case 3:return J(L(r,{bj:n.a}),!1,Xo);case 2:return J(L(r,{aL:n.a}),!1,Xo);default:return J(r,!1,Xo)}}),ib={$:8},fb=d(function(n,r,t){r=L(r,{aa:(r=t.cv).b?g(r,R([\"alertname\"]))?At:kt(w(jt,\",\",r)):kt(\"\")});return _(t,No(R([w(Ho,t.cH,w(ub,n,r)),w(Gf,$t(ib),cb(\"group-by-field\"))])))}),bb=l(function(n,r){for(;;){if(n<=0)return r;if(!r.b)return r;n=n-1,r=r.b}}),sb=d(function(n,r,t){for(;;){if(n<=0)return t;if(!r.b)return t;var e=r.a;n=n-1,r=r.b,t=w(ot,e,t)}}),lb=l(function(n,r){return It(y(sb,n,r,H))}),db=d(function(n,r,t){if(0<r){var e=_(r,t);n:for(;;){r:for(;;){if(!e.b.b)return t;if(!e.b.b.b){if(1===e.a)break n;break r}switch(e.a){case 1:break n;case 2:var u=e.b;return R([u.a,u.b.a]);case 3:if(e.b.b.b.b){var a=e.b,c=a.b;return R([a.a,c.a,c.b.a])}break r;default:if(e.b.b.b.b&&e.b.b.b.b.b){var o=e.b,i=o.b,u=i.b,a=u.b,c=a.a,a=a.b;return w(ot,o.a,w(ot,i.a,w(ot,u.a,w(ot,c,1e3<n?w(lb,r-4,a):y(db,n+1,r-4,a)))))}break r}}return t}return R([e.b.a])}return H}),vb=l(function(n,r){return y(db,0,n,r)}),$b=d(function(n,r,t){var e=t.a;return w(pf,w(_o,Iu,Kt(t.b)),w(vb,e+n,w(bb,e-n-1,r)))}),mb=l(function(n,r){return _(r.a,n(r.b))}),hb=P,pb=l(function(n,r){var t=r.a,e=r.b;return Wr(w(pf,function(n){var r=n.b;return!g(t,n.a)&&g(e,r)},n))}),gb=l(function(n,r){return y(Jt,l(_),n,r)}),wb=l(function(n,r){if(g(n,r))return 1;var t=me(r),e=w(Je,mb(Ht),w(gb,w(Nt,1,t),Gu(r))),u=me(n),r=(w(Yt,u,t)/2|0)-1,r=w(Ro,w($b,r,e),w(Je,mb(Ht),w(gb,w(Nt,1,u),Gu(n)))),n=Wr(r),u=n/u,t=n/t,r=(r=w(Je,w(_o,pb(e),w(_o,et,ne(.5))),r),y(_t,xt,0,r));return n?1/3*(u+t+(n-r)/n):0}),yb=d(function(n,r,t){for(;;){var e=_(n,r);if(!e.a.b||!e.b.b)return t;var u=e.a,a=u.a,c=u.b,u=e.b,e=u.b;if(g(a,u.a))n=c,r=e,t=X(t,R([a]));else{if(0<Wr(t))return t;n=n,r=e,t=t}}}),xb=function(n){return V(n).join(\"\")},kb=l(function(n,r){return\"\"===n||\"\"===r?\"\":g(n,r)?n:xb(y(yb,Gu(n),Gu(r),H))}),Ab=d(function(n,r,t){if(\"\"===n||\"\"===r)return 0;if(g(n,r))return 1;return t+.25*me(w(kb,n,r))*(1-t)}),Sb=l(function(n,r){return\"\"===n||\"\"===r?0:g(n,r)?1:y(Ab,n,r,w(wb,n,r))}),Tb=D,Eb=t(function(n,r,t,e){switch(t.$){case 7:return _(e,No(R([w(Ho,e.cH,w(ub,n,L(r,{ah:t.a}))),w(Gf,$t(ib),cb(\"group-by-field\"))])));case 0:return y(fb,n,r,L(e,{a_:t.a?\"\":e.a_,cv:X(e.cv,R([u=t.b])),bl:H}));case 1:var u=t.b;return y(fb,n,r,L(e,{a_:t.a?u:e.a_,cv:w(pf,yi(u),e.cv),bl:H}));case 2:return _(L(e,{bm:t.a}),Xo);case 4:return _(L(e,{a3:t.a,bm:At}),Xo);case 5:return _(L(e,{bE:t.a}),Xo);case 3:return _(L(e,{aL:t.a}),Xo);case 6:return a=L(e,{a_:u=t.a}),_(L(a,{bl:ct(a.a_)?H:w($e,\" \",a.a_)?a.bl:w(vb,10,It(w(Tb,Sb(a.a_),w(pf,w(_o,function(n){return w(Pf,n,a.cv)},bu),st(a.cJ))))),bm:At}),Xo);default:return _(e,Xo)}var a}),Zb={$:7},jb=l(function(n,r){return{bo:n,cU:r}}),Cb=t(function(n,r,t,e){return{cC:r,cK:n,cP:t,c3:e}}),_b=Ur,Jb=/.^/,Lb=Br(1/0),Nb=t(function(n,r,t,e){switch(t.$){case 0:return _(3!==t.a.$?e:L(e,{bB:w(Je,Mu,t.a.a)}),Xo);case 2:return _(L(e,{a_:\"\",bl:w(ot,{bo:\"All\",cU:\"\"},w(vb,10,e.bB)),bJ:At,bL:!0}),w(Gf,$t(Zb),cb(\"receiver-field\")));case 5:return _(L(e,{bE:t.a}),Xo);case 1:var u=t.a;return _(L(e,{a_:u,bl:w(ot,{bo:\"All\",cU:\"\"},w(vb,10,It(w(Tb,w(_o,function(n){return n.bo},Sb(u)),e.bB))))}),Xo);case 6:return _(L(e,{bL:!1}),Xo);case 4:return _(L(e,{bJ:t.a}),Xo);case 3:u=t.a;return _(L(e,{bE:!1,bL:!1}),w(Ho,e.cH,w(ub,n,L(r,{bz:\"\"===u?At:kt(u)}))));default:return _(e,Xo)}}),Xb=l(function(n,r){return L(r,{H:kt(Qu(n))})}),Hb=c(function(n,r,t,e,u){var a=r.ba,c=r.aJ,o=r.a$,i=r.bA,f=r.aI,b=u+\"#/alerts\",s=ub(b);switch(n.$){case 1:return _(L(r,{aI:n.a}),Xo);case 0:var l=n.a,d=function(){switch(l.$){case 3:var n=l.a,r=L(a,{cJ:Du(w(Je,ut,w(Ro,w(_o,function(n){return n.bi},bt),n)))}),n=w(Je,function(n){var r=n.b;return y(Io,gu(n.a),{bo:\"unknown\"},r)},bt(w(qf,w(_o,function(n){return n.bi},w(_o,bt,pf(function(n){return w(Pf,n.a,a.cv)}))),n)));return _(cu(n),r);case 0:return _(lo,a);case 1:return _(io,a);default:return _(nu(l.a),a)}}();return _(L(r,{aI:d.a,aJ:l,ba:d.b}),Xo);case 2:var v=w(rb,t,a),$=w(eb,t,o);return _(L(r,{aE:mo,aF:At,aI:t.ah?f:io,aJ:t.ah?io:c,a$:$,ba:v}),No(R([t.ah?w(Lo,w(_o,Ye,ru),w(Qf,e,t)):w(Lo,w(_o,We,ru),w(Mf,e,t)),w(Lo,w(_o,uu,ru),Uf(e))])));case 6:return _(r,w(Ho,r.cH,s(L(t,{U:kt(n.a)}))));case 7:return _(r,w(Ho,r.cH,s(L(t,{S:kt(n.a)}))));case 8:return _(r,w(Ho,r.cH,s(L(t,{T:kt(n.a)}))));case 11:return _(L(r,{bV:n.a}),Xo);case 4:var m=w(ob,n.a,o),$=m.a,d=m.b,h=m.c,p=w(Lo,w(_o,tu,ru),h),m=s(w(Xb,$.bk,t)),p=d?No(R([w(Ho,r.cH,m),p])):p;return _(L(r,{a$:$,bV:0}),p);case 5:p=x(Eb,b,t,n.a,a),h=p.b;return _(L(r,{ba:v=p.a}),w(Lo,w(_o,eu,ru),h));case 3:v=x(Nb,b,t,n.a,i),h=v.b;return _(L(r,{bA:v.a}),w(Lo,w(_o,uu,ru),h));case 9:return _(L(r,{aF:n.a}),Xo);case 10:var g=n.a;return _(L(r,{aE:w(cf,g,r.aE)?w(nb,g,r.aE):w(Bf,g,r.aE),cq:!1}),Yf(!1));default:h=n.a,g=3===(g=_(f,h)).a.$&&g.b?Du(w(Nt,0,Wr(g.a.a))):mo;return _(L(r,{aE:g,cq:h}),No(R([Yf(h),w(oo,au,je(h))])))}}),Ib=Qn(\"persistFirstDayOfWeek\",gf),Ob=l(function(n,r){var t=n,e=function(){switch(t){case\"Monday\":return 0;case\"Sunday\":return 1;case\"Saturday\":return 2;default:return 0}}(),n=function(){switch(e){case 0:return\"Monday\";case 1:return\"Sunday\";default:return\"Saturday\"}}();return _(L(r,{cx:e}),No(R([w(oo,at,je(Fe({$:12,a:e}))),Ib(n)])))}),Rb=t(function(n,r,t,e){return{$:7,a:n,b:r,c:t,d:e}}),Vb={$:2},Ur=l(function(n,r){return y(_e,so,r,n)}),zb=w(Ur,R([\"silenceID\"]),Ao),Db=d(function(n,r,t){for(;;){if(-2===t.$)return r;var e=t.e,u=n,a=y(n,t.b,t.c,y(Db,n,r,t.d));n=u,r=a,t=e}}),Gb=d(function(e,u,n){return y(Db,d(function(n,r,t){return y(xn,e(n),u(r),t)}),{},n)}),Fb=kn,Mb=l(function(n,r){return Pt(n/r)}),Qb=d(function(n,r,t){for(;;){if(!t.b)return r+n;var e=t.a,u=t.b;if(T(e.bP,r)<0)return r+e.b;n=n,r=r,t=u}}),Ub=l(function(n,r){var t=n.b;return y(Qb,n.a,w(Mb,na(r),6e4),t)}),Bb=l(function(n,r){return ra(w(Ub,n,r)).aW}),qb=l(function(n,r){return w(wi,24,w(Mb,w(Ub,n,r),60))}),Kb=l(function(n,r){return w(wi,1e3,na(r))}),Pb=l(function(n,r){return w(wi,60,w(Ub,n,r))}),Wb=l(function(n,r){switch(ra(w(Ub,n,r)).ap){case 1:return 0;case 2:return 1;case 3:return 2;case 4:return 3;case 5:return 4;case 6:return 5;case 7:return 6;case 8:return 7;case 9:return 8;case 10:return 9;case 11:return 10;default:return 11}}),Yb=d(function(n,r,t){return 0<n?y(Yb,n>>1,X(r,r),1&n?X(t,r):t):t}),ns=l(function(n,r){return y(Yb,n,r,\"\")}),rs=d(function(n,r,t){return X(w(ns,n-me(t),Su(r)),t)}),ts=l(function(n,r){return y(rs,n,\"0\",Zt(r))}),es=l(function(n,r){return w(wi,60,w(Mb,na(r),1e3))}),us=l(function(n,r){return ra(w(Ub,n,r)).b3}),Br=l(function(n,r){return{$:0,a:n,b:r}}),as=w(Br,0,H),cs=ta,os=w(zf,gf,cs),is=l(function(n,r){return y(_t,(t=n,l(function(n,r){return r.push(t(n)),r})),[],r);var t}),fs=l(function(n,r){return{$:1,a:n,b:r}}),bs=d(function(n,r,t){return v(Of,\"POST\",H,n,r,t)}),ss=l(function(n,r){n=w(jt,\"/\",R([n,\"silences\"])),r=Wu(R([_(\"matchers\",w(is,Yu,(r=r).bk)),_(\"startsAt\",os(r.bQ)),_(\"endsAt\",os(r.aX)),_(\"createdBy\",gf(r.aV)),_(\"comment\",gf(r.aT)),_(\"annotations\",w(wf,Fb,w(Mi,w(Gb,at,gf),r.aK))),_(\"id\",w(wf,Fb,w(Mi,gf,r.bd)))])),r=w(fs,\"application/json\",w(Et,0,r));return Ff(y(bs,n,r,zb))}),ls=l(function(n,r){return{aT:n.aT,aV:n.aV,v:r,s:n.s,aX:n.aX,bd:n.bd,bQ:n.bQ,J:!0}}),ds=l(function(n,r){return fi(na(r)+ai(n))}),vs=function(n){return n.trim()},$s=R([_(\"w\",6048e5),_(\"d\",864e5),_(\"h\",36e5),_(\"m\",6e4),_(\"s\",1e3)]),ms=l(function(n,r){return w(wi,7,function(n){n=w(wi,7,n);return n||7}(r)+7-function(n){switch(n){case 0:return 1;case 1:return 2;case 2:return 3;case 3:return 4;case 4:return 5;case 5:return 6;default:return 7}}(n))}),hs=l(function(n,r){var t=aa(n)?1:0;switch(r){case 0:return 0;case 1:return 31;case 2:return 59+t;case 3:return 90+t;case 4:return 120+t;case 5:return 151+t;case 6:return 181+t;case 7:return 212+t;case 8:return 243+t;case 9:return 273+t;case 10:return 304+t;default:return 334+t}}),ps=l(function(n,r){return Pt(n/r)}),gs=l(function(n,r){return ca(n)+w(hs,n,r)+1}),ws=l(function(n,r){switch(r){case 0:return 31;case 1:return aa(n)?29:28;case 2:return 31;case 3:return 30;case 4:return 31;case 5:return 30;case 6:case 7:return 31;case 8:return 30;case 9:return 31;case 10:return 30;default:return 31}}),ys=d(function(n,r,t){for(;;){var e=w(ws,n,r),u=oa(r);if(12<=u||T(t,e)<=0)return{aW:t,ap:r,b3:n};n=n,r=ia(u+1),t=t-e}}),xs=l(function(n,r){return _(w(ps,n,r),w(wi,r,n))}),ks=w(_o,ba,function(n){return n.ap}),As=w(_o,ks,function(n){return(oa(n)+2)/3|0}),Ss=l(function(n,r){var t,e=r;switch(n){case 0:return t=fa(r),ca(t)+1;case 1:return w(gs,fa(r),(t=As(r),ia(3*t-2)));case 2:return w(gs,fa(r),ks(r));case 3:case 4:return e-w(ms,0,r);case 5:return e-w(ms,1,r);case 6:return e-w(ms,2,r);case 7:return e-w(ms,3,r);case 8:return e-w(ms,4,r);case 9:return e-w(ms,5,r);case 10:return e-w(ms,6,r);default:return r}}),Ts=d(function(n,r,t){return T(t,n)<0?n:0<T(t,r)?r:t}),Es=d(function(n,r,t){return ca(n)+w(hs,n,r)+y(Ts,1,w(ws,n,r),t)}),Zs=l(function(n,r){return y(Es,w(us,n,r),w(Wb,n,r),w(Bb,n,r))}),js=t(function(n,r,t,e){return 36e5*n+6e4*r+1e3*t+e}),Cs=l(function(n,r){return x(js,w(qb,n,r),w(Pb,n,r),w(es,n,r),w(Kb,n,r))}),_s=l(function(n,r){var t=na(r);return(sa(w(Zs,n,r))+w(Cs,n,r)-t)/6e4|0}),Js=d(function(n,r,t){var e=sa(r)+t,u=w(_s,n,fi(e)),r=fi(e-6e4*u),t=w(_s,n,r);if(g(u,t))return r;e=fi(e-6e4*t);return g(t,w(_s,n,e))?e:r}),Ls=d(function(n,r,t){return y(Js,r,w(Ss,n,w(Zs,r,t)),0)}),Ns=d(function(n,r,t){switch(n){case 15:return t;case 14:return y(Js,r,w(Zs,r,t),x(js,w(qb,r,t),w(Pb,r,t),w(es,r,t),0));case 13:return y(Js,r,w(Zs,r,t),x(js,w(qb,r,t),w(Pb,r,t),0,0));case 12:return y(Js,r,w(Zs,r,t),x(js,w(qb,r,t),0,0,0));case 11:return y(Ls,11,r,t);case 2:return y(Ls,2,r,t);case 0:return y(Ls,0,r,t);case 1:return y(Ls,1,r,t);case 3:return y(Ls,3,r,t);case 4:return y(Ls,4,r,t);case 5:return y(Ls,5,r,t);case 6:return y(Ls,6,r,t);case 7:return y(Ls,7,r,t);case 8:return y(Ls,8,r,t);case 9:return y(Ls,9,r,t);default:return y(Ls,10,r,t)}}),Xs=d(function(n,r,t){var e=w(Mi,la,n);return{aj:r,ak:w(Mi,la,r),cx:t,ap:n,ar:At,az:n,aA:e}}),Hs=ta,Is=t(function(n,r,t,e){return{aT:Re(r),aV:Re(n),v:y(Xs,kt(t),kt(w(ds,72e5,t)),e),s:Re(w(wf,\"\",ua(72e5))),aX:Re(Hs(w(ds,72e5,t))),bd:At,bQ:Re(Hs(t)),J:!1}}),Os=l(function(n,r){return na(r)-na(n)}),Rs=l(function(n,r){return r.$?mt(n(r.a)):wt(r.a)}),Vs=l(function(n,r){var t=n.bd,e=n.aV,u=n.aT,a=n.bQ,c=n.aX,o=Xu(da(cs(a))),n=Xu(da(cs(c)));return{aT:Re(u),aV:Re(e),v:y(Xs,o,n,r),s:Re(w(wf,\"\",ua(w(Os,a,c)))),aX:Re(Hs(c)),bd:kt(t),bQ:Re(Hs(a)),J:!1}}),kn=o(function(n,r,t,e,u,a,c,o,i){return{aK:a,aT:u,aV:e,aX:t,bd:c,bk:n,bQ:r,bS:o,bX:i}}),Z=t(function(n,r,t,e){return{bg:e,bh:t,bo:n,bZ:r}}),Kr=x(j,\"isEqual\",Vr(fo),kt(!0),y(i,\"isRegex\",fo,y(i,\"value\",Ao,y(i,\"name\",Ao,se(Z))))),Hr=y(i,\"state\",w(Do,function(n){switch(n){case\"expired\":return se(0);case\"active\":return se(1);case\"pending\":return se(2);default:return Go(\"Unknown type: \"+n)}},Ao),se(function(n){return{bR:n}})),zs=y(i,\"updatedAt\",M,y(i,\"status\",Hr,y(i,\"id\",Ao,x(j,\"annotations\",Vr(S(Ao)),At,y(i,\"comment\",Ao,y(i,\"createdBy\",Ao,y(i,\"endsAt\",M,y(i,\"startsAt\",M,y(i,\"matchers\",Vo(Kr),se(kn)))))))))),Ds=d(function(n,r,t){r=w(jt,\"/\",R([n,\"silence\",r]));return w(Lo,t,Ff(w(Rf,r,zs)))}),Gs=(eo=fi,Tn(function(n){n(An(eo(Date.now())))})),Fs=Qn(\"persistDefaultCreator\",gf),Ms=e(function(n,r,t,e,u,a){return 1!==r.$?1!==t.$?1!==e.$?1!==u.$?1!==a.$?wt(v(n,r.a,t.a,e.a,u.a,a.a)):mt(a.a):mt(u.a):mt(e.a):mt(t.a):mt(r.a)}),Br=R([x(Z,\"\",\"\",!1,kt(!0))]),Qs={aK:At,aT:\"\",aV:\"\",aX:fi(0),bd:At,bk:Br,bQ:fi(0)},Us=l(function(n,r){n=_(da(n),da(r));if(n.a.$||n.b.$)return n.b;r=n.b.a;return 0<T(na(n.a.a),na(r))?mt(\"Can't be in the past\"):wt(r)}),Bs=l(function(n,r){var a=r.bd,t=r.aT,e=r.aV,u=r.bQ,r=r.aX;return Xu(f(Ms,c(function(n,r,t,e,u){return L(Qs,{aT:r,aV:t,aX:u,bd:a,bk:n,bQ:e})}),ha(n),$a(t.bZ),$a(e.bZ),da(u.bZ),w(Us,u.bZ,r.bZ)))}),qs=l(function(n,r){return T(n,r)<0?n:r}),Ks=d(function(n,r,t){var e=t;switch(n){case 0:return y(Ks,1,12*r,e);case 1:var u=ba(e),a=12*(u.b3-1)+(oa(u.ap)-1)+r,c=ia(w(wi,12,a)+1),a=w(ps,a,12)+1;return ca(a)+w(hs,a,c)+w(qs,u.aW,w(ws,a,c));case 2:return e+7*r;default:return e+r}}),Ps=t(function(n,r,t,e){n:for(;;)switch(n){case 15:return fi(na(e)+r);case 14:n=15,r=1e3*r,t=t,e=e;continue n;case 13:n=15,r=6e4*r,t=t,e=e;continue n;case 12:n=15,r=36e5*r,t=t,e=e;continue n;case 11:return y(Js,t,y(Ks,3,r,w(Zs,t,e)),w(Cs,t,e));case 2:return y(Js,t,y(Ks,1,r,w(Zs,t,e)),w(Cs,t,e));case 0:n=2,r=12*r,t=t,e=e;continue n;case 1:n=2,r=3*r,t=t,e=e;continue n;case 3:n=11,r=7*r,t=t,e=e;continue n;default:n=11,r=7*r,t=t,e=e;continue n}}),Ws=l(function(n,r){return x(Ps,12,n,as,r)}),Ys=l(function(n,r){return x(Ps,13,n,as,r)}),nl=d(function(n,r,t){var e=y(Ns,n,r,t);return g(e,t)?t:x(Ps,n,1,r,e)}),rl=l(function(n,r){n-=w(qb,as,r);return x(Ps,12,n,as,r)}),tl=l(function(n,r){n-=w(Pb,as,r);return x(Ps,13,n,as,r)}),el=l(function(n,u){var r=d(function(n,r,t){function e(n){return w(Mi,function(n){return w(t,r,n)},n)}return n?_(u.aA,e(u.ak)):_(e(u.aA),u.ak)}),t=w(wf,fi(0),u.ap);switch(n.$){case 0:return L(u,{ap:kt(pa(t))});case 1:return L(u,{ap:kt(y(Ns,2,as,x(Ps,11,-1,as,y(Ns,2,as,t))))});case 2:return L(u,{ar:kt(n.a)});case 4:return L(u,{ar:At});case 3:var e=l(function(n,r){if(r.$)return ga(n);var t=r.a;return fi((r=na(ga(n)),t=na(fi((t=na(y(Ns,11,as,n=t)),na(n)-t))),r+t))}),a=l(function(n,r){return n.$?r:kt(w(e,n.a,r))}),c=function(){var n=u.ar;if(n.$)return _(u.az,u.aj);var r=n.a,n=_(u.az,u.aj);if(1===n.a.$){if(1===n.b.$)return _(kt(r),At);var t=n.b.a;return _(kt(r),kt(t))}if(1!==n.b.$)return _(kt(r),At);t=n.a.a;return w(Li,na(ga(r)),na(ga(t)))?_(kt(t),kt(r)):_(kt(r),kt(t))}(),o=c.a,c=c.b;return L(u,{aj:c,ak:w(a,c,u.ak),az:o,aA:w(a,o,u.aA)});case 5:var i=n.a,f=n.b,b=n.c,s=l(function(n,r){return r<0?0:w(wi,n,r)}),o=y(r,i,f,l(function(n,r){return n?w(tl,w(s,60,b),r):w(rl,w(s,24,b),r)}));return L(u,{ak:o.b,aA:o.a});default:b=n.c,f=y(r,i=n.a,f=n.b,l(function(n,r){function t(n){return g(na(ga(r)),na(ga(n)))?n:r}return t(w(n?Ys:Ws,b,r))}));return L(u,{ak:f.b,aA:f.a})}}),ul=d(function(n,r,t){return 1!==r.$?1!==t.$?wt(w(n,r.a,t.a)):mt(t.a):mt(r.a)}),Z={$:5},al=Fr,cl=Mr,ol=l(function(n,r){return{aS:r.aS+(n-r.b),c:r.c,d:r.d,b:n,bG:r.bG,a:r.a}}),il=Gr,fl=Dr,bl=l(function(n,r){if(y(fl,101,n,r)||y(fl,69,n,r)){var t=n+1,t=y(fl,43,t,r)||y(fl,45,t,r)?t+1:t,r=w(il,t,r);return g(t,r)?-r:r}return n}),sl=l(function(n,r){return y(fl,46,n,r)?w(bl,w(il,n+1,r),r):w(bl,n,r)}),ll=c(function(n,r,t,e,u){var a=e.a,e=e.b;if(1===r.$)return w(Mo,!0,w(Wo,u,r.a));r=r.a;return g(t,a)?w(Mo,T(u.b,t)<0,w(Wo,u,n)):y(Qo,!0,r(e),w(ol,a,u))}),dl=t(function(n,r,t,e){return w(qo,Po,x(Ko,n,r,t,e))}),vl=e(function(n,r,t,e,u,a){var c=u.a,o=w(sl,c,a.a);if(o<0)return w(Mo,!0,x(dl,a.bG,a.aS-(o+a.b),n,a.c));if(g(a.b,o))return w(Mo,!1,w(Wo,a,r));if(g(c,o))return v(ll,n,t,a.b,u,a);if(1===e.$)return w(Mo,!0,w(Wo,a,n));u=e.a,e=oi(y(he,a.b,o,a.a));return 1===e.$?w(Mo,!0,w(Wo,a,n)):y(Qo,!0,u(e.a),w(ol,o,a))}),Br=l(function(n,r){return e={aM:mt(r),aZ:n,a2:wt(at),bb:mt(r),bf:wt(et),cF:r,bp:mt(r)},function(n){if(y(fl,48,n.b,n.a)){var r=n.b+1,t=r+1;return y(fl,120,r,n.a)?v(ll,e.cF,e.bb,t,w(cl,t,n.a),n):y(fl,111,r,n.a)?v(ll,e.cF,e.bp,t,y(al,8,t,n.a),n):y(fl,98,r,n.a)?v(ll,e.cF,e.aM,t,y(al,2,t,n.a),n):f(vl,e.cF,e.aZ,e.bf,e.a2,_(r,0),n)}return f(vl,e.cF,e.aZ,e.bf,e.a2,y(al,10,n.b,n.a),n)};var e}),Fr=w(Br,Z,Z),$l=w(vi,w(vi,ci(ne),Fr),Ai(w(Je,function(n){var r=n.a;return w(di,ci(n.b),lu(r))},$s))),Mr=w(vi,ci(at),w(di,w(di,w(nf,0,function(r){return Ai(R([w(vi,ci(function(n){return Cu(n+r)}),w(di,$l,mf)),ci(ju(r))]))}),mf),Yo)),ml=w(_o,Ci(Mr),Rs($t(\"Wrong duration format\"))),hl=l(function(n,r){return L(r,{ae:po,bZ:n})}),pl={$:1},gl=l(function(n,r){return L(r,{ae:(r=n(r.bZ)).$?wa(r.a):pl})}),wl=l(function(n,t){switch(n.$){case 0:var r=da(a=n.a),e=da(t.aX.bZ),u=function(){var n=y(ul,Os,r,e);if(n.$)return t.s.bZ;n=ua(n.a);return n.$?t.s.bZ:n.a}();return L(t,{s:w(hl,u,t.s),bQ:w(hl,a,t.bQ)});case 1:var a=n.a,r=da(t.bQ.bZ),e=da(a),u=function(){var n=y(ul,Os,r,e);if(n.$)return t.s.bZ;n=ua(n.a);return n.$?t.s.bZ:n.a}();return L(t,{s:w(hl,u,t.s),aX:w(hl,a,t.aX)});case 2:var a=n.a,r=da(t.bQ.bZ),c=ml(a),u=(u=y(ul,ds,c,r)).$?t.aX.bZ:Hs(u.a);return L(t,{s:w(hl,a,t.s),aX:w(hl,u,t.aX)});case 3:return L(t,{s:w(gl,ml,t.s),aX:w(gl,Us(t.bQ.bZ),t.aX),bQ:w(gl,da,t.bQ)});case 4:return L(t,{aV:w(hl,n.a,t.aV)});case 5:return L(t,{aV:w(gl,$a,t.aV)});case 6:return L(t,{aT:w(hl,n.a,t.aT)});case 7:return L(t,{aT:w(gl,$a,t.aT)});case 8:var o=function(){var n=_(t.v.aA,t.v.ak);if(n.a.$||n.b.$)return J(t.bQ,t.aX,t.s);var r=n.a.a,n=n.b.a;return J(w(gl,da,Re(Hs(r))),w(gl,Us(Hs(r)),Re(Hs(n))),w(gl,ml,Re(w(wf,\"\",ua(w(Os,r,n))))))}();return L(t,{s:c=o.c,aX:e=o.b,bQ:r=o.a,J:!1});case 9:o=(c=da(t.bQ.bZ)).$?t.v.aA:kt(c.a),c=Xu(da(t.aX.bZ));return L(t,{v:y(Xs,o,c,t.v.cx),J:!0});default:return L(t,{J:!1})}}),yl=t(function(n,r,t,e){switch(n.$){case 1:var u=w(Bs,r.a$,r.a5);if(u.$)return _(L(r,{a0:xa(r.a$),a5:ya(r.a5),bN:nu(\"Could not submit the form, Silence is not yet valid.\")}),Xo);u=u.a;return _(L(r,{bN:io}),No(R([w(Lo,w(_o,Ku,Fe),w(ss,e,u)),Fs(u.aV),w(oo,qu,je(u.aV))])));case 9:var a=3!==(c=n.a).$?Xo:w(Ho,r.cH,t+\"#/silences/\"+c.a);return _(L(r,{bN:c}),a);case 6:a=n.b;return _(r,w(oo,w(_o,y(Rb,n.a,a.bk,a.aT),Fe),Gs));case 7:return _({aD:At,aJ:lo,a$:Oe(n.b),a0:po,cx:r.cx,a5:x(Is,n.a,n.c,n.d,r.cx),cH:r.cH,bN:lo},Xo);case 5:return _(r,y(Ds,e,c=n.a,w(_o,Pu,Fe)));case 8:return 3!==n.a.$?_(r,Xo):_({aD:At,aJ:lo,a$:Oe(w(Je,ea,(u=n.a.a).bk)),a0:po,cx:r.cx,a5:w(Vs,u,r.cx),cH:r.cH,bN:r.bN},w(oo,at,je(Fe(Vb))));case 2:var c=w(Bs,r.a$,r.a5);if(c.$)return _(L(r,{aJ:nu(\"Cannot display affected Alerts, Silence is not yet valid.\"),a0:xa(r.a$),a5:ya(r.a5)}),Xo);u=c.a;return _(L(r,{aJ:io}),w(Lo,w(_o,Uu,Fe),w(Qf,e,va(u.bk))));case 3:return _(L(r,{aJ:n.a}),Xo);case 4:return _(L(r,{aD:n.a}),Xo);case 0:return _(L(r,{aJ:lo,a5:w(wl,n.a,r.a5),bN:lo}),Xo);case 10:var o=w(el,n.a,r.a5.v);return _(L(r,{a5:w(ls,r.a5,o)}),Xo);case 11:u=w(ob,n.a,r.a$),o=u.c;return _(L(r,{a$:u.a,a0:po}),w(Lo,w(_o,Bu,Fe),o));default:return _(L(r,{cx:n.a}),Xo)}}),xl=l(function(n,r){return v(Of,\"DELETE\",H,n,yf,r)}),kl=w(Ur,R([\"status\"]),Ao),Al=d(function(n,r,t){r=w(jt,\"/\",R([n,\"silence\",r.bd]));return w(Lo,t,Ff(w(xl,r,kl)))}),Sl=d(function(n,r,t){r=w(jt,\"/\",R([n,\"silences\"+Ru(r)]));return w(Lo,t,Ff(w(Rf,r,Vo(zs))))}),Tl=l(function(n,r){return g(r.bS.bR,n)}),El=l(function(n,r){n=w(Sa,r,n);return{cj:Wr(n),ay:n,bV:r}}),Zl=l(function(n,r){switch(r.$){case 3:return cu(n(r.a));case 0:return lo;case 1:return io;default:return nu(r.a)}}),jl=R([1,2,0]),Cl=c(function(n,r,t,e,u){switch(n.$){case 2:return _(L(r,{ay:w(Zl,function(n){return w(Je,El(n),jl)},n.a)}),Xo);case 3:return _(L(r,{a$:w(eb,t,r.a$),bK:At,ay:io}),y(Sl,u,t,Aa));case 0:return _(L(r,{bK:kt((a=n.a).bd)}),Xo);case 1:var a=n.a,c=n.b;return _(L(r,{bK:At,ay:io}),No(R([y(Al,u,a,$t(To)),c?w(Ho,r.cH,e+\"#/silences\"):Xo])));case 4:var o=w(ob,n.a,r.a$),i=o.a,a=o.b,c=w(Lo,ka,o.c),o=w(ub,e+\"#/silences\",w(Xb,i.bk,t)),c=a?No(R([w(Ho,r.cH,o),c])):c;return _(L(r,{a$:i}),c);default:return _(L(r,{bV:n.a}),Xo)}}),_l=d(function(n,r,t){switch(n.$){case 2:return _(L(r,{aJ:n.a}),Xo);case 1:return _(L(r,{aD:n.a}),Xo);case 4:return _(L(r,{bK:!0}),Xo);case 0:return 3!==n.a.$?_(L(r,{aJ:lo,bM:e=n.a}),Xo):_(L(r,{aJ:io,bM:cu(e=n.a.a)}),w(Lo,Ta,w(Qf,t,va(e.bk))));var e;case 3:var u=n.a;return _(L(r,{bK:!1}),y(Ds,t,u,Ea));default:u=n.a;return _(L(r,{bK:!1}),w(Ho,r.cH,\"#/silences/\"+u))}}),Gr=t(function(n,r,t,e){return{aR:n,aU:t,bY:e,b$:r}}),Dr=y(i,\"original\",Ao,se(function(n){return{br:n}})),Br=d(function(n,r,t){return{bo:n,bs:t,bS:r}}),Z=l(function(n,r){return{aH:r,bo:n}}),Fr=y(i,\"address\",Ao,y(i,\"name\",Ao,se(Z))),Mr=w(Do,function(n){switch(n){case\"ready\":return se(0);case\"settling\":return se(1);case\"disabled\":return se(2);default:return Go(\"Unknown type: \"+n)}},Ao),Z=x(j,\"peers\",Vr(Vo(Fr)),At,y(i,\"status\",Mr,x(j,\"name\",Vr(Ao),At,se(Br)))),Fr=e(function(n,r,t,e,u,a){return{aN:t,aO:u,aP:e,a9:a,bF:r,b_:n}}),Mr=y(i,\"goVersion\",Ao,y(i,\"buildDate\",Ao,y(i,\"buildUser\",Ao,y(i,\"branch\",Ao,y(i,\"revision\",Ao,y(i,\"version\",Ao,se(Fr))))))),Jl=y(i,\"uptime\",M,y(i,\"config\",Dr,y(i,\"versionInfo\",Mr,y(i,\"cluster\",Z,se(Gr))))),Ll=l(function(n,r){n=w(jt,\"/\",R([n,\"status\"])),n=w(Rf,n,Jl);return w(Lo,r,Ff(n))}),Nl=l(function(n,r){return n.$?_(r,w(Ll,n.a,w(_o,Za,Ue))):_(L(r,{bS:{bT:n.a}}),Xo)}),Xl=l(function(n,r){var t=r.b9,e=r.b6;switch(n.$){case 6:var u=v(Hb,So,r.b5,c=n.a,e,t),a=u.b;return _(L(r,{b5:u.a,cw:c,cX:De(c)}),a);case 11:var c,o=v(Cl,To,r.c0,c=n.a,t,e),a=o.b;return _(L(r,{cw:c,cX:Ke(c),c0:o.a}),w(Lo,Me,a));case 12:return _(L(r,{cX:Co}),w(oo,at,je(Ue({$:1,a:e}))));case 8:var o=n.a,i=y(_l,{$:3,a:o},r.c1,e),a=i.b;return _(L(r,{cX:Pe(o),c1:i.a}),w(Lo,Qe,a));case 10:i=n.a;return _(L(r,{cX:qe(i)}),w(oo,w(_o,Eo(r.ck),Fe),je(i)));case 9:var f=n.a;return _(L(r,{cX:Be(f)}),w(oo,at,je(Fe(Ge(f)))));case 7:return _(L(r,{cX:Zo}),Xo);case 14:return _(r,w(Ho,r.cH,n.a));case 15:return _(r,Jo(n.a));case 16:return _(r,w(Ho,r.cH,t+\"#/alerts\"));case 13:return _(L(r,{cX:jo}),Xo);case 4:return w(Nl,n.a,r);case 0:f=v(Hb,n.a,r.b5,r.cw,e,t),a=f.b;return _(L(r,{b5:f.a}),a);case 3:var b=v(Cl,n.a,r.c0,r.cw,t,e),a=b.b;return _(L(r,{c0:b.a}),w(Lo,Me,a));case 5:b=w(Ob,n.a,r.cZ),a=b.b;return _(L(r,{cZ:b.a}),a);case 1:var s=y(_l,n.a,r.c1,e),a=s.b;return _(L(r,{c1:s.a}),w(Lo,Qe,a));case 2:s=x(yl,n.a,r.c$,t,e),a=s.b;return _(L(r,{c$:s.a}),a);case 17:return _(L(r,{cb:n.a}),Xo);case 18:return _(L(r,{cy:n.a}),Xo);case 19:return _(L(r,{cn:n.a}),Xo);case 20:return _(L(r,{ck:n.a}),Xo);default:return _(L(r,{cq:n.a}),Xo)}}),Hl=c(function(n,r,t,e,u){return{w:e,z:t,u:r,bZ:u,A:n}}),Il=function(n){return n.b&&(\"\"!==n.a||n.b.b)?w(ot,n.a,Il(n.b)):H},Ol=l(function(n,r){return kt(1===r.$?R([n]):w(ot,n,r.a))}),Rl=function(n){try{return kt(decodeURIComponent(n))}catch(n){return At}},Vl=l(function(n,r){var t=w(Ct,\"=\",n);if(t.b&&t.b.b&&!t.b.b.b){n=t.b.a,t=Rl(t.a);if(1===t.$)return r;t=t.a,n=Rl(n);return 1===n.$?r:y(Xf,t,Ol(n.a),r)}return r}),zl=l(function(n,r){return function(n){for(;;){if(!n.b)return At;var r=n.a,t=n.b,e=r.u;if(!e.b)return kt(r.bZ);if(\"\"===e.a&&!e.b.b)return kt(r.bZ);n=t}}(n(v(Hl,H,function(n){n=w(Ct,\"/\",n);return Il(n.b&&\"\"===n.a?n.b:n)}(r.au),1===(n=r.cT).$?$o:y(_e,Vl,$o,w(Ct,\"&\",n.a)),r.a6,at)))}),Dl=a(function(n,r,t,e,u,a,c,o){return{ah:t,aa:r,bz:e,R:o,S:a,T:c,U:u,H:n}}),Gl=l(function(r,t){return function(n){return t(w(wf,H,w(uf,r,n)))}}),Fl=l(function(n,r){return v(Hl,r.A,r.u,r.z,r.w,n(r.bZ))}),j=l(function(a,n){var c=n;return function(n){var r=n.A,t=n.u,e=n.z,u=n.w;return w(Je,Fl(n.bZ),c(v(Hl,r,t,e,u,a)))}}),Vr=function(n){return w(Gl,n,w(_o,Hu,Mi(w(_o,ja,yi(\"false\")))))},Ml=l(function(n,r){var t=n,e=r;return function(n){return w(Ro,e,t(n))}}),Br=l(function(n,r){return w(Ml,n,(t=r,function(n){var r=n.z;return R([v(Hl,n.A,n.u,r,n.w,(0,n.bZ)(t(r)))])}));var t}),Fr=function(c){return function(n){var r=n.A,t=n.u,e=n.z,u=n.w,a=n.bZ;if(t.b){n=t.a,t=t.b;return g(n,c)?R([v(Hl,w(ot,n,r),t,e,u,a)]):H}return H}},M=function(n){return w(Gl,n,function(n){return!n.b||n.b.b?At:kt(n.a)})},Dr=w(j,Dl,w(Br,w(Br,w(Br,w(Br,w(Br,w(Br,w(Br,w(Br,Fr(\"alerts\"),M(\"filter\")),M(\"group\")),w(Gl,\"customGrouping\",w(_o,Hu,yi(At)))),M(\"receiver\")),Vr(\"silenced\")),Vr(\"inhibited\")),Vr(\"muted\")),Vr(\"active\"))),Mr=Fr(\"settings\"),i=l(function(n,o){return function(n){var r=n.A,t=n.u,e=n.z,u=n.w,a=n.bZ;if(t.b){var c=t.a,n=t.b,t=o(c);if(t.$)return H;t=t.a;return R([v(Hl,w(ot,c,r),n,e,u,a(t))])}return H}}),Z=w(i,\"STRING\",kt),Gr=w(Ml,Fr(\"silences\"),w(Ml,Z,Fr(\"edit\"))),Vr=d(function(r,n,t){var e=n,u=t;return function(n){return w(r,e(n),u(n))}}),i=l(function(n,r){return{aT:w(wf,\"\",r),bk:y(_o,tb(hf),wf(H),n)}}),i=w(Ml,Fr(\"silences\"),w(Br,Fr(\"new\"),y(Vr,i,M(\"filter\"),M(\"comment\")))),M=w(j,function(n){return s(Dl,n,At,!1,At,At,At,At,At)},w(Br,Fr(\"silences\"),M(\"filter\"))),Z=w(Ml,Fr(\"silences\"),Z),Fr=Fr(\"status\"),Ql=(uo=R([w(j,Ke,M),w(j,Co,Fr),w(j,jo,Mr),w(j,qe,i),w(j,Pe,Z),w(j,Be,Gr),w(j,De,Dr),w(j,{$:7},function(n){return R([n])})]),function(r){return w(Ro,function(n){return n(r)},uo)}),Ul=qr,Bl={$:7},ql={$:13},Kl={$:12},Pl={$:16},Wl=l(function(n,r){return r.$?n:r.a}),j=d(function(n,r,t){var m,e=Ca(r),u=w(Wl,!1,w(bo,w(so,\"production\",fo),n)),a=u?r.au:\"/\",c=w(Wl,!1,w(bo,w(so,\"groupExpandAll\",fo),n)),o=function(){switch(w(Wl,\"Sunday\",w(bo,w(so,\"firstDayOfWeek\",Ao),n))){case\"Sunday\":return 1;case\"Saturday\":return 2;default:return 0}}(),i=function(){switch(e.$){case 0:case 4:return e.a;default:return ko}}(),f=w(Wl,\"\",w(bo,w(so,\"defaultCreator\",Ao),n)),u=ze(u?r.au:\"http://localhost:9093/\");return w(Xl,_a(r),(m={a$:Oe(H),cH:t,bK:At,ay:lo,bV:1},function($){return function(v){return function(d){return function(l){return function(s){return function(b){return function(f){return function(i){return function(o){return function(c){return function(a){return function(u){return function(e){return function(t){return function(r){return function(n){return{b5:d,b6:i,b9:f,cb:c,ck:e,cn:u,cq:t,cw:s,cy:a,cH:r,cI:o,cX:l,cZ:n,c$:v,c0:m,c1:$,bS:b}}}}}}}}}}}}}}}}}({aD:At,aJ:lo,cH:t,bK:!1,bM:lo})(w(go,t,o))(w(ho,t,c))(e)(i)(wo)(r.au)(u)(a)(io)(io)(io)(f)(c)(t)({cx:o})))}),qr=zn(H),Yl=l(function(n,r){return 1===n.$?r:r+(\":\"+Zt(n.a))}),nd=d(function(n,r,t){return 1===r.$?t:X(t,X(n,r.a))}),rd=l(function(n,r){return w(or,n,gf(r))}),td=rd(\"className\"),ed={aT:\"\",bk:H},ud=Yn(\"div\"),ad=Yn(\"span\"),cd=Wn,od=w(ud,H,R([w(ad,H,R([cd(\"Loading...\")]))])),id=rr,fd=function(n){return n.toUpperCase()},bd=l(function(n,r){switch(r.$){case 3:return n(r.a);case 1:case 0:return od;default:return Oa(r.a)}}),sd=Yn(\"button\"),ld=l(function(n,r){return{$:0,a:n,b:r}}),dd=Yn(\"i\"),vd=ar,$d=l(function(n,r){return w(vd,n,{$:0,a:r})}),md=cr,hd=d(function(n,r,t){n=n?\"fa-minus\":\"fa-plus\";return w(sd,R([Va(r),td(\"btn btn-outline-info border-0 mr-1 mb-1\"),w(md,\"margin-left\",\"-3rem\")]),R([w(dd,R([td(\"fa \"+n),td(\"mr-2\")]),H),cd(t.bo)]))}),pd=rd(\"title\"),gd=Yn(\"ul\"),wd=Yn(\"a\"),yd=w(_o,l(function(n,r){return w(Te,r,n)}),l(function(n,r){return w(Kf,r,n)})(R([\"http://\",\"https://\"]))),xd=l(function(n,r){for(;;){if(!n.b)return r;var t,e,u=n.a,a=n.b;r=yd(u)?r.b?1===r.a.$?(t=r.a.a,e=r.b,n=a,w(ot,wt(u),w(ot,mt(t+\" \"),e))):(n=a,w(ot,wt(u),w(ot,mt(\" \"),r))):(n=a,w(ot,wt(u),r)):r.b?1===r.a.$?(e=r.b,n=a,w(ot,mt((t=r.a.a)+\" \"+u),e)):(n=a,w(ot,mt(\" \"+u),r)):(n=a,w(ot,mt(u),r))}}),kd=function(n){return R(n.trim().split(/\\s+/g))},Ad=rd(\"target\"),Sd=Yn(\"td\"),Td=Yn(\"th\"),Ed=Yn(\"tr\"),Zd=l(function(n,r){return g(n,kt(r.a1))?w(sd,R([Va(At),td(\"btn btn-outline-info border-0 active\")]),R([w(dd,R([td(\"fa fa-minus mr-2\")]),H),cd(\"Info\")])):w(sd,R([td(\"btn btn-outline-info border-0\"),Va(kt(r.a1))]),R([w(dd,R([td(\"fa fa-plus mr-2\")]),H),cd(\"Info\")]))}),jd=Yn(\"li\"),Cd=l(function(e,n){var r=l(function(n,r){var t=r.a,r=r.b;return e(n)?_(w(ot,n,t),r):_(t,w(ot,n,r))});return y(_e,r,_(H,H),n)}),_d=l(function(n,r){for(;;){if(-2===r.$)return n;var t=r.d;n=w(_d,n+1,r.e),r=t}}),Jd=Yn(\"table\"),Ld=ta,Nd=d(function(n,r,t){var e,n=X((e=w(Cd,w(_o,ut,Kt(\"alertname\")),w(pf,w(_o,l(function(n,r){return w(Pf,r,n)})(n),bu),bt(t.bi)))).a,e.b);return w(jd,R([w(md,\"position\",\"static\"),td(\"align-items-start list-group-item border-0 p-0 mb-4\")]),R([w(ud,R([td(\"w-100 mb-2 d-flex align-items-start\")]),R([qa(t),0<Ba(t.aK)?w(id,function(n){return ru({$:9,a:n})},w(Zd,r,t)):cd(\"\"),(e=t.a7).$?cd(\"\"):Fa(e.a),Ua(t),Hu(t.bS.be).$?cd(\"\"):w(ad,R([td(\"btn btn-outline-danger border-0\")]),R([w(dd,R([td(\"fa fa-eye-slash mr-2\")]),H),cd(\"Inhibited\")])),Hu(t.bS.bn).$?cd(\"\"):w(ad,R([td(\"btn btn-outline-danger border-0\")]),R([w(dd,R([td(\"fa fa-bell-slash mr-2\")]),H),cd(\"Muted\")])),function(n){n=\"#/alerts?filter=\"+Qi(Qu(w(Je,function(n){return y(Bi,n.a,0,n.b)},bt(n.bi))));return w(wd,R([td(\"btn btn-outline-info border-0\"),za(n)]),R([w(dd,R([td(\"fa fa-link mr-2\")]),H),cd(\"Link\")]))}(t)])),g(r,kt(t.a1))?w(Jd,R([td(\"table w-100 mb-1\")]),w(Je,Ga,bt(t.aK))):cd(\"\"),w(ud,H,w(Je,Ma,n))]))}),Xd=u(function(n,r,t,e,u,a,c){var o=e.b?w(Je,function(n){var r=n.a,n=n.b;return w(ud,R([td(\"btn-group mr-1 mb-1\")]),R([w(ad,R([td(\"btn text-muted\"),w(md,\"user-select\",\"initial\"),w(md,\"-moz-user-select\",\"initial\"),w(md,\"-webkit-user-select\",\"initial\"),w(md,\"border-color\",\"#5bc0de\")]),R([cd(r+'=\"'+n+'\"')])),w(sd,R([td(\"btn btn-outline-info\"),Va(Ra(_(r,n))),pd(\"Filter by this label\")]),R([cd(\"+\")]))]))},e):R([w(ad,R([td(\"btn btn-secondary mr-1 mb-1\")]),R([cd(\"Not grouped\")]))]),r=c||w(cf,a,r),a=w(id,function(n){return ru({$:10,a:n})},y(hd,r,a,t)),t=Wr(u),t=1===t?Zt(t)+\" alert\":Zt(t)+\" alerts\",t=R([w(ad,R([td(\"ml-1 mb-0\"),w(md,\"white-space\",\"nowrap\")]),R([cd(t)]))]);return w(ud,H,R([w(ud,R([td(\"mb-3\")]),w(ot,a,X(o,t))),r?w(gd,R([td(\"list-group mb-0\")]),w(Je,w(Nd,e,n),u)):cd(\"\")]))}),Hd=l(function(n,r){return v(_i,1,n,r,vo,vo)}),Id=t(function(t,e,u,n){if(n.b){if(n.b.b)return w(ud,R([td(\"pl-5\")]),w(Xt,l(function(n,r){return b(Xd,t,e,r.bz,bt(r.bi),r.aJ,n,u)}),n));var r=n.a.bz,a=n.a.aJ,n=bt(n.a.bi);return b(Xd,t,w(Hd,0,0),r,n,a,0,u)}return Oa(\"No alert groups found\")}),cr=l(function(n,r){return w(or,n,Wf(r))}),Od=cr(\"checked\"),Rd=rd(\"htmlFor\"),Vd=rd(\"id\"),zd=Yn(\"input\"),Dd=Yn(\"label\"),Gd=w(Ur,R([\"target\",\"checked\"]),fo),Fd=rd(\"type\"),Md=d(function(n,r,t){return w(jd,R([td(\"nav-item\")]),R([w(ud,R([td(\"mt-1 ml-1 custom-control custom-checkbox\")]),R([w(zd,R([Fd(\"checkbox\"),Vd(n),td(\"custom-control-input\"),Od(w(wf,!1,r)),Ka(w(_o,t,ru))]),H),w(Dd,R([td(\"custom-control-label\"),Rd(n)]),R([cd(n)]))]))]))}),Qd=t(function(n,r,t,e){return w(jd,R([td(\"nav-item\")]),R([g(n,r)?w(ad,R([td(\"nav-link active\")]),e):w(sd,R([w(md,\"background\",\"transparent\"),w(md,\"font\",\"inherit\"),w(md,\"cursor\",\"pointer\"),w(md,\"outline\",\"none\"),td(\"nav-link\"),Va(t(n))]),e)]))}),Ud=l(function(n,r){return{$:1,a:n,b:r}}),Bd=cr(\"disabled\"),qd='env=\"production\"',Kd=8,Pd=13,Wd=w(_t,w(_o,kt,$t),At),Yd=l(function(n,r){return w(vd,n,{$:1,a:r})}),nv=w(Ur,R([\"target\",\"value\"]),Ao),rv=w(so,\"keyCode\",{$:2}),Ur=w(vi,w(di,ci(at),mf),w(di,w(di,ff,mf),Yo)),tv=w(_o,Ci(Ur),Xu),ev=Yn(\"small\"),uv=cr(\"spellcheck\"),av=rd(\"value\"),cv=l(function(n,r){var t=n.c_,e=r.bk,u=r.bj,a=r.aL,c=tv(u),o=Va(w(wf,ab,w(Mi,ld(!0),c))),i=Wd(e),f=g(c,At),n=w(Je,Ya,e),r=\"\"===u?\"\":c.$?\"has-danger\":\"has-success\";return w(ud,R([td(\"row no-gutters align-items-start\")]),X(w(Je,ac,e),R([w(ud,R([td(\"col \"+r),w(md,\"min-width\",t?\"300px\":\"200px\")]),R([w(ud,R([td(\"row no-gutters align-content-stretch\")]),R([w(ud,R([td(\"col input-group\")]),R([w(zd,R([Vd(\"filter-bar-matcher\"),td(\"form-control\"),uv(!1),av(u),ec(function(n){if(g(n,Pd))return w(wf,ab,w(Mi,ld(!0),c));if(g(n,Kd)){if(\"\"!==u)return Pa(!0);n=_(a,i);return n.a||n.b.$?ab:w(Ud,!0,n.b.a)}return ab}),uc(function(n){return g(n,Kd)?Pa(!1):ab}),tc(Wa)]),H),w(ud,R([td(\"input-group-append\")]),R([w(sd,R([td(\"btn btn-primary\"),Bd(f),o]),R([cd(\"+\")]))]))])),t?w(ud,R([td(\"col col-auto ml-2\")]),R([w(ud,R([td(\"input-group\")]),R([w(wd,R([td(\"btn btn-outline-info\"),za(nc(n))]),R([w(dd,R([td(\"fa fa-bell-slash-o mr-2\")]),H),cd(\"Silence\")]))]))])):cd(\"\")])),w(ev,R([td(\"form-text text-muted\")]),R([cd(\"Custom matcher, e.g.\"),w(sd,R([td(\"btn btn-link btn-sm align-baseline\"),Va(Wa(qd))]),R([cd(qd)]))]))]))])))}),ov=l(function(n,r){return{$:0,a:n,b:r}}),iv=l(function(n,r){n=g(n,kt(r))?\"active\":\"\";return w(sd,R([td(\"dropdown-item \"+n),Va(w(ov,!0,r))]),R([cd(r)]))}),fv=d(function(n,r,t){return w(Dd,R([td(\"f6 dib mb2 mr2 d-flex align-items-center\")]),R([w(zd,R([Fd(\"checkbox\"),Od(r),Ka(t)]),H),w(ad,R([td(\"pl-2\")]),R([cd(\" \"+n)]))]))}),bv=l(function(n,r){return{$:1,a:n,b:r}}),sv=8,lv=40,dv=13,vv=38,$v=l(function(n,r){for(;;){if(!r.b)return At;var t=r.b;if(g(r.a,n))return Hu(t);n=n,r=t}}),mv=l(function(r,n){var t=n.a_,e=n.bl,u=n.bm,a=n.cv,c=n.aL,o=w(wf,Wd(e),w(Mi,l(function(n,r){return w($v,r,n)})(It(e)),u)),n=r?ib:w(ov,!0,t),i=w(wf,Hu(e),w(Mi,l(function(n,r){return w($v,r,n)})(e),u));return w(ud,R([td(\"input-group\")]),R([w(zd,R([Vd(\"group-by-field\"),td(\"form-control\"),av(t),ec(function(n){if(g(n,lv))return dc(i);if(g(n,vv))return dc(o);if(g(n,dv))return r?w(wf,ib,w(Mi,ov(!0),u)):w(ov,!0,t);if(g(n,sv)){if(\"\"!==t)return lc(!0);n=_(Wd(a),c);return n.a.$||n.b?ib:w(bv,!0,n.a.a)}return ib}),uc(function(n){return g(n,sv)?lc(!1):ib}),tc(bc),(e=sc(!0),w($d,\"focus\",se(e))),vc(sc(!1))]),H),w(ud,R([td(\"input-group-append\")]),R([w(sd,R([td(\"btn btn-primary\"),Bd(r),Va(n)]),R([cd(\"+\")]))]))]))}),hv=l(function(n,r){var t=n.a_,e=n.cv,u=!w(cf,t,n.cJ)||w(Pf,t,e),a=ct(t)?\"\":u?\"has-danger\":\"has-success\",t=w(ud,R([td(\"mb-3\")]),R([y(fv,\"Enable custom grouping\",r,cc)]));return r?w(ud,H,R([t,w(ud,R([td(\"row no-gutters align-items-start\")]),X(w(Je,$c,e),R([w(ud,R([td(\"col \"+a),w(md,\"min-width\",\"200px\")]),R([w(mv,u,n),w(Pf,\"alertname\",e)?w(ev,R([td(\"form-text text-muted\")]),R([cd(\"Label key for grouping alerts\")])):w(ev,R([td(\"form-text text-muted\")]),R([cd(\"Label key for grouping alerts, e.g.\"),w(sd,R([td(\"btn btn-link btn-sm align-baseline\"),Va(bc(\"alertname\"))]),R([cd(\"alertname\")]))])),(e=(u=n).bm,n=u.bl,w(ud,R([td(\"autocomplete-menu \"+((u.a3||u.bE)&&n.b?\"show\":\"\")),ic(oc(!0)),fc(oc(!1))]),R([w(ud,R([td(\"dropdown-menu\")]),w(Je,iv(e),n))])))]))])))])):t}),pv={$:6},gv=l(function(n,r){n=g(n,kt(r))?R([td(\"dropdown-item active\")]):R([td(\"dropdown-item\"),w(md,\"cursor\",\"pointer\"),Va(mc(r.cU))]);return w(ud,n,R([cd(r.bo)]))}),wv={$:2},yv=l(function(n,r){r=w(wf,n,Hu(w(Je,w(_o,function(n){return n.bo},kt),w(pf,w(_o,function(n){return n.cU},w(_o,kt,Kt(n))),r))));return w(jd,R([td(\"nav-item ml-auto\"),w(ir,\"tabIndex\",Zt(1)),w(md,\"position\",\"relative\"),w(md,\"outline\",\"none\")]),R([w(ud,R([Va(wv),td(\"mt-1 mr-4\"),w(md,\"cursor\",\"pointer\")]),R([cd(\"Receiver: \"+w(wf,\"All\",r))]))]))}),xv=l(function(n,r){return r.bL||r.bE?(e=(t=r).a_,u=t.bJ,a=w(wf,Wd(t=t.bl),w(Mi,l(function(n,r){return w($v,r,n)})(It(t)),u)),c=w(wf,Hu(t),w(Mi,l(function(n,r){return w($v,r,n)})(t),u)),w(jd,R([td(\"nav-item ml-auto mr-4 autocomplete-menu show\"),ic(hc(!0)),fc(hc(!1)),w(md,\"position\",\"relative\"),w(md,\"outline\",\"none\")]),R([w(zd,R([Vd(\"receiver-field\"),av(e),vc(pv),tc(gc),ec(function(n){return g(n,lv)?pc(c):g(n,vv)?pc(a):g(n,dv)?mc(w(wf,e,w(Mi,function(n){return n.cU},u))):Zb}),td(\"mr-4\"),w(md,\"display\",\"block\"),w(md,\"width\",\"100%\")]),H),w(ud,R([td(\"dropdown-menu dropdown-menu-right\")]),w(Je,gv(u),t))]))):w(yv,n,r.bB);var t,e,u,a,c}),kv=l(function(n,r){var t=n.aI,e=n.ba,u=n.a$,a=n.bA,c=n.bV,o=n.aF,i=n.aE,n=n.cq;return w(ud,H,R([w(ud,R([td(\"card mb-3\")]),R([w(ud,R([td(\"card-header\")]),R([w(gd,R([td(\"nav nav-tabs card-header-tabs\")]),R([x(Qd,0,c,w(_o,La,ru),R([cd(\"Filter\")])),x(Qd,1,c,w(_o,La,ru),R([cd(r.ah?\"Group (custom)\":\"Group\")])),w(id,w(_o,uu,ru),w(xv,r.bz,a)),y(Md,\"Silenced\",r.U,Ha),y(Md,\"Inhibited\",r.S,Na),y(Md,\"Muted\",r.T,Xa)]))])),w(ud,R([td(\"card-body\")]),R([c?w(id,w(_o,eu,ru),w(hv,e,r.ah)):w(id,w(_o,tu,ru),w(cv,{c_:!0},u))]))])),w(ud,H,R([w(sd,R([td(\"btn btn-outline-secondary border-0 mr-1 mb-3\"),Va(ru({$:12,a:!n}))]),R(n?[w(dd,R([td(\"fa fa-minus mr-3\")]),H),cd(\"Collapse all groups\")]:[w(dd,R([td(\"fa fa-plus mr-3\")]),H),cd(\"Expand all groups\")]))])),w(bd,y(Id,o,i,n),t)]))}),Av=Yn(\"h1\"),Sv=w(ud,H,R([w(Av,H,R([cd(\"not found\")]))])),Tv=Yn(\"fieldset\"),Ev=d(function(n,r,t){return w(ud,R([td(\"mt-1 ml-1 custom-control custom-radio\")]),R([w(zd,R([Fd(\"radio\"),Vd(n),td(\"custom-control-input\"),Od(r),av(n),tc(t)]),H),w(Dd,R([td(\"custom-control-label\"),Rd(n)]),R([cd(n)]))]))}),Zv={$:7},jv={$:5},Cv={$:10},_v={$:8},Jv={$:4},Lv=e(function(n,r,t,e,u,a){for(;;){if(0<=T(na(a),na(e)))return It(u);var c=n,o=r,i=t,f=e,b=w(ot,a,u),s=x(Ps,n,r,t,a);n=c,r=o,t=i,e=f,u=b,a=s}}),Nv=c(function(n,r,t,e,u){return f(Lv,n,w(Yt,1,r),t,u,H,y(nl,n,t,e))}),Xv=l(function(n,r){switch(w(wi,7,w(Mb,w(Ub,n,r),1440))){case 0:return 3;case 1:return 4;case 2:return 5;case 3:return 6;case 4:return 0;case 5:return 1;default:return 2}}),Hv=l(function(n,r){var t=pa(n),e=v(Nv,11,1,as,t,(u=function(n){switch(r){case 1:return n;case 0:return 1===n?7:n-1;default:return 6===n?7:7===n?1:n+1}}(Ac(w(Xv,as,t))),x(Ps,11,7-u,as,t))),u=y(Ns,2,as,n),n=(n=function(n){switch(r){case 1:return 7===n?0:n;case 0:return 1===n?0:n-1;default:return 6===n?0:7===n?1:n+1}}(Ac(w(Xv,as,u))),n=x(Ps,11,-n,as,u),v(Nv,11,1,as,n,u));return t=v(Nv,11,1,as,u,t),X(n,X(t,e))}),Iv=l(function(n,r){return Wr(n)<7?r:w(Iv,w(bb,7,n),w(Oo,r,R([w(vb,7,n)])))}),Ov={$:3},Rv=d(function(n,r,t){return 1===r.$||1===t.$?At:kt(w(n,r.a,t.a))}),Vv=d(function(t,n,e){var r=g(Sc(n),Sc(e))?\" thismonth\":\"\",u=l(function(n,r){return w(Li,na(ga(n)),na(ga(r)))}),a=l(function(n,r){return!n.$&&1===w(u,n.a,e)?r:\"\"}),c=w(a,t.aj,\" end\"),o=w(a,t.ar,\" mouseover\"),i=w(a,t.az,\" start\"),n=function(){var n=_(t.az,t.aj);if(n.a.$||n.b.$)return\"\";var r=n.b.a,r=_(w(u,n.a.a,e),w(u,r,e));if(r.a||2!==r.b)return\"\";return\" between\"}(),a=w(wf,_(\"\",\"\"),y(Rv,l(function(n,r){return _(i,c)}),t.az,t.aj));return w(ud,R([td(\"date back\"+(a.a+(a.b+n)))]),R([w(ud,R([td(\"date front\"+o+(i+(c+r))),w($d,\"mouseover\",se({$:2,a:e})),Va(Ov)]),R([cd(Zt(w(Bb,as,e)))]))]))}),zv=d(function(n,r,t){return w(ud,H,R([w(ud,H,w(Je,w(Vv,n,r),t))]))}),Dv=l(function(n,r){var t=w(Hv,r,n.cx),t=w(Iv,t,H);return w(ud,R([td(\"row justify-content-center\")]),R([w(ud,R([td(\"weekheader\")]),function(){switch(n.cx){case 1:return w(Je,Tc,R([\"Sun\",\"Mon\",\"Tue\",\"Wed\",\"Thu\",\"Fri\",\"Sat\"]));case 0:return w(Je,Tc,R([\"Mon\",\"Tue\",\"Wed\",\"Thu\",\"Fri\",\"Sat\",\"Sun\"]));default:return w(Je,Tc,R([\"Sat\",\"Sun\",\"Mon\",\"Tue\",\"Wed\",\"Thu\",\"Fri\"]))}}()),w(ud,R([td(\"date-container\"),w($d,\"mouseout\",se(Jv))]),w(Je,w(zv,n,r),t))]))}),Gv={$:0},Fv={$:1},Mv=Yn(\"br\"),Qv=Yn(\"p\"),Uv=d(function(n,r,t){return{$:6,a:n,b:r,c:t}}),Bv=d(function(n,r,t){return{$:5,a:n,b:r,c:t}}),qv=Yn(\"strong\"),Ur=l(function(n,r){return w(Do,function(n){return(n.$?Go:se)(n.a)},w(fe,r,n))}),cr=l(function(n,r){return r.$?mt(n):wt(r.a)})(\"could not convert string\"),Kv=w(Ur,nv,w(_o,ye,cr)),Pv=l(function(r,t){return w(ud,R([td(\"row timepicker\")]),R([w(qv,R([td(\"subject\")]),R([cd(t?\"End\":\"Start\")])),w(ud,R([td(\"hour\")]),R([w(sd,R([td(\"up-button d-flex-center\"),Va(y(Uv,t,0,1))]),R([w(dd,R([td(\"fa fa-angle-up\")]),H)])),w(zd,R([w($d,\"blur\",w(fe,w(Bv,t,0),Kv)),av(function(){if(t){var n=r.ak;return n.$?\"0\":Zt(w(qb,as,n.a))}n=r.aA;return n.$?\"0\":Zt(w(qb,as,n.a))}()),Zc(2),td(\"view d-flex-center\")]),H),w(sd,R([td(\"down-button d-flex-center\"),Va(y(Uv,t,0,-1))]),R([w(dd,R([td(\"fa fa-angle-down\")]),H)]))])),w(ud,R([td(\"colon d-flex-center\")]),R([cd(\":\")])),w(ud,R([td(\"minute\")]),R([w(sd,R([td(\"up-button d-flex-center\"),Va(y(Uv,t,1,1))]),R([w(dd,R([td(\"fa fa-angle-up\")]),H)])),w(zd,R([w($d,\"blur\",w(fe,w(Bv,t,1),Kv)),av(function(){if(t){var n=r.ak;return n.$?\"0\":Zt(w(Pb,as,n.a))}n=r.aA;return n.$?\"0\":Zt(w(Pb,as,n.a))}()),Zc(2),td(\"view\")]),H),w(sd,R([td(\"down-button d-flex-center\"),Va(y(Uv,t,1,-1))]),R([w(dd,R([td(\"fa fa-angle-down\")]),H)]))])),w(ud,R([td(\"timeview d-flex-center\")]),R([cd((n=l(function(n,r){return w(wf,\"\",w(Mi,function(n){return r.$?\"\":w(yo,8,ta(n))},n))}),t?w(n,r.ak,r.aj):w(n,r.aA,r.az)))]))]));var n}),Wv=l(function(n,r){if(1===n.$)return w(ad,R([td(\"btn btn-sm btn-light border mr-2 mb-2\"),w(md,\"user-select\",\"text\"),w(md,\"-moz-user-select\",\"text\"),w(md,\"-webkit-user-select\",\"text\")]),R([cd(r)]));n=n.a;return w(sd,R([td(\"btn btn-sm btn-light border mr-2 mb-2\"),Va(n)]),R([w(ad,R([td(\"text-muted\")]),R([cd(r)]))]))}),Yv=l(function(n,r){var t,e=w(Je,function(n){return w(jt,\"=\",R([n.a,n.b]))},X((t=w(Cd,w(_o,ut,Kt(\"alertname\")),bt(r.bi))).a,t.b));return w(ud,R([w(md,\"position\",\"static\"),td(\"border-0 p-0 mb-4\")]),R([w(ud,R([td(\"w-100 mb-2 d-flex\")]),R([qa(r),0<Ba(r.aK)?w(Zd,n,r):cd(\"\"),(t=r.a7).$?cd(\"\"):Fa(t.a)])),g(n,kt(r.a1))?w(Jd,R([td(\"table w-100 mb-1\")]),w(Je,Ga,bt(r.aK))):cd(\"\"),w(ud,H,w(Je,Wv(At),e))]))}),n$=l(function(n,r){return w(ud,R([td(\"pa0 w-100\")]),w(Je,Yv(n),r))}),r$=l(function(n,r){switch(r.$){case 3:var t=r.a;return w(ud,R([td(\"w-100\")]),R(t.b?[w(Qv,H,R([w(qv,H,R([cd(\"Affected alerts: \"+Zt(Wr(t)))]))])),w(n$,n,t)]:[w(Qv,H,R([w(qv,H,R([cd(\"No affected alerts\")]))]))]));case 0:return cd(\"\");case 1:return od;default:t=r.a;return w(ud,R([td(\"alert alert-warning\")]),R([cd(t)]))}}),t$=d(function(n,r,t){switch(r.$){case 3:return cd(\"\");case 0:return w(id,Cc,w(r$,n,t));case 2:return Oa(r.a);default:return od}}),e$=\"mt-5\",u$=l(function(r,n){return w(ud,R([td(X(e$,2===r.$?\" has-danger\":\"\"))]),R([w(Dd,R([Rd(\"filter-bar-matcher\")]),R([w(qv,H,R([cd(\"Matchers \")])),cd(\"Alerts affected by this silence\")])),w(id,Bu,w(cv,{c_:!1},n)),function(){if(2!==r.$)return cd(\"\");var n=r.a;return w(ud,R([td(\"form-control-feedback\")]),R([cd(n)]))}()]))}),a$={$:1},c$=w(sd,R([td(\"btn btn-outline-success\"),Va(Vb)]),R([cd(\"Preview Alerts\")])),o$=l(function(n,r){return w(ud,R([td(\"mb-4 \"+e$)]),R([c$,function(n){n=n.$?\"Create\":\"Update\";return w(sd,R([td(\"ml-2 btn btn-primary\"),Va(a$)]),R([cd(n)]))}(n),w(sd,R([td(\"ml-2 btn btn-danger\"),Va(r)]),R([cd(\"Reset\")]))]))}),i$={$:9},f$={$:3},b$=e(function(n,r,t,e,u,a){var c=a.ae;switch(c.$){case 1:return w(ud,R([td(\"d-flex flex-column form-group has-success \"+t)]),R([w(Dd,H,R([w(qv,H,R([cd(r)]))])),w(n,R([av(a.bZ),tc(e),vc(u),td(\"form-control form-control-success\")]),H)]));case 0:return w(ud,R([td(\"d-flex flex-column form-group \"+t)]),R([w(Dd,H,R([w(qv,H,R([cd(r)]))])),w(n,R([av(a.bZ),tc(e),vc(u),td(\"form-control\")]),H)]));default:var o=c.a;return w(ud,R([td(\"d-flex flex-column form-group has-danger \"+t)]),R([w(Dd,H,R([w(qv,H,R([cd(r)]))])),w(n,R([av(a.bZ),tc(e),vc(u),td(\"form-control form-control-danger\")]),H),w(ud,R([td(\"form-control-feedback\")]),R([cd(o)]))]))}}),s$=d(function(n,r,t){return w(ud,R([td(\"row \"+e$)]),R([f(b$,zd,\"Start\",\"col-lg-4 col-6\",w(_o,Lc,xc),xc(f$),n),f(b$,zd,\"Duration\",\"col-lg-3 col-6\",w(_o,_c,xc),xc(f$),t),f(b$,zd,\"End\",\"col-lg-4 col-6\",w(_o,Jc,xc),xc(f$),r),w(ud,R([td(\"form-group col-lg-1 col-6\")]),R([w(Dd,H,R([cd(\" \")])),w(sd,R([td(\"form-control btn btn-outline-primary cursor-pointer\"),Va(xc(i$))]),R([w(dd,R([td(\"fa fa-calendar\")]),H)]))]))]))}),cr=l(function(n,r){return w(ir,/^(on|formAction$)/i.test(n=n)?\"data-\"+n:n,/^\\s*(javascript:|data:text\\/html)/i.test(r=r)?\"\":r)}),l$=w(cr,\"data-gramm_editor\",\"false\"),d$=function(n){return R(n.split(/\\r\\n|\\r|\\n/g))},v$=Yn(\"textarea\"),$$=c(function(n,r,t,e,u){var a=y(Ts,3,15,Wr(d$(u.bZ))),c=u.ae;switch(c.$){case 1:return w(ud,R([td(\"d-flex flex-column form-group has-success \"+r)]),R([w(Dd,H,R([w(qv,H,R([cd(n)]))])),w(v$,R([av(u.bZ),tc(t),vc(e),td(\"form-control form-control-success\"),Nc(a),l$]),H)]));case 0:return w(ud,R([td(\"d-flex flex-column form-group \"+r)]),R([w(Dd,H,R([w(qv,H,R([cd(n)]))])),w(v$,R([av(u.bZ),tc(t),vc(e),td(\"form-control\"),Nc(a),l$]),H)]));default:var o=c.a;return w(ud,R([td(\"d-flex flex-column form-group has-danger \"+r)]),R([w(Dd,H,R([w(qv,H,R([cd(n)]))])),w(v$,R([av(u.bZ),tc(t),vc(e),td(\"form-control form-control-danger\"),Nc(a),l$]),H),w(ud,R([td(\"form-control-feedback\")]),R([cd(o)]))]))}}),m$=t(function(n,r,t,e){var u=e.a5,a=e.a$,c=e.a0,o=e.bN,i=e.aJ,e=e.aD,t=n.$?_(\"New Silence\",w(Eo,t,r)):_(\"Edit Silence\",Ge(n.a)),r=t.b;return w(ud,H,R([w(Av,H,R([cd(t.a)])),y(s$,u.bQ,u.aX,u.s),w(u$,c,a),f(b$,zd,\"Creator\",e$,w(_o,yc,xc),xc(jv),u.aV),v($$,\"Comment\",e$,w(_o,wc,xc),xc(Zv),u.aT),w(ud,R([td(e$)]),R([y(t$,e,o,i),w(o$,n,r)])),jc(u)]))}),h$=er,p$=ur,g$=function(n){return nr(fr(n))}(\"ul\"),w$=l(function(n,r){return{$:1,a:n,b:r}}),y$=l(function(n,r){return{ca:cd(\"Are you sure you want to expire this silence?\"),a4:w(sd,R([td(\"btn btn-primary\"),Va(Me(w(w$,n,r)))]),R([cd(\"Confirm\")])),bq:Me(To),c6:\"Expire Silence\"}}),x$=l(function(n,r){return w(ad,R([td(\"text-muted align-self-center mr-2\")]),R([cd(n+(\" \"+Ld(r)))]))}),k$=l(function(n,r){return nc(n)+(\"&comment=\"+Qi(r))}),A$=Yn(\"h5\"),S$=l(function(n,r){return w(jd,R([w(md,\"position\",\"static\"),td(\"align-items-start list-group-item border-0 p-0 mb-4\")]),R([w(ud,R([td(\"w-100 mb-2 d-flex align-items-start\")]),R([function(){switch(r.bS.bR){case 1:return w(x$,\"Ends\",r.aX);case 2:return w(x$,\"Starts\",r.bQ);default:return w(x$,\"Expired\",r.aX)}}(),(t=r.bd,w(wd,R([td(\"btn btn-outline-info border-0\"),za(\"#/silences/\"+t)]),R([cd(\"View\")]))),Hc(r),function(n){switch(n.bS.bR){case 0:return cd(\"\");case 1:return w(sd,R([td(\"btn btn-outline-danger border-0\"),Va(Me(Xc(n)))]),R([cd(\"Expire\")]));default:return w(sd,R([td(\"btn btn-outline-danger border-0\"),Va(Me(Xc(n)))]),R([cd(\"Delete\")]))}}(r)])),w(ud,R([td(\"\")]),w(Je,Oc,r.bk)),Rc(n?kt(w(y$,r,!1)):At)]));var t}),T$=d(function(r,n,t){switch(t.$){case 3:var e=t.a;return(e=w(wf,H,w(Mi,function(n){return n.ay},Hu(w(pf,w(_o,function(n){return n.bV},Kt(n)),e))))).b?w(g$,R([td(\"list-group\")]),w(Je,function(n){return _(n.bd,w(S$,g(r,kt(n.bd)),n))},e)):Oa(\"No silences found\");case 2:return Oa(t.a);default:return od}}),E$=R([1,2,0]),Z$=d(function(n,r,t){return x(Qd,t,n,w(_o,Vc,Me),function(){if(r){var n=r;return R([cd(Ia(zc(t))),w(ad,R([td(\"badge badge-pillow badge-default align-text-top ml-2\")]),R([cd(Zt(n))]))])}return R([cd(Ia(zc(t)))])}())}),j$=l(function(r,n){if(3!==n.$)return w(gd,R([td(\"nav nav-tabs mb-4\")]),w(Je,w(Z$,r,0),E$));n=n.a;return w(gd,R([td(\"nav nav-tabs mb-4\")]),w(Je,function(n){return y(Z$,r,n.cj,n.bV)},n))}),C$=l(function(n,r){return{ca:cd(\"Are you sure you want to expire this silence?\"),a4:w(sd,R([td(\"btn btn-primary\"),Va(Me(w(w$,n,r)))]),R([cd(\"Confirm\")])),bq:Qe({$:5,a:n.bd}),c6:\"Expire Silence\"}}),_$={$:4},J$=Yn(\"b\"),L$=l(function(n,r){return w(ud,R([td(\"form-group row\")]),R([w(Dd,R([td(\"col-2 col-form-label\")]),R([w(J$,H,R([cd(n)]))])),w(ud,R([td(\"col-10 d-flex align-items-center\")]),R([r]))]))}),N$=t(function(n,r,t,e){r=w(id,function(n){return Qe({$:1,a:n})},w(r$,n,r));return w(ud,H,R([w(Av,H,R([cd(\"Silence\"),w(ad,R([td(\"ml-3\")]),R([Hc(t),function(n){switch(n.bS.bR){case 0:return cd(\"\");case 1:return w(sd,R([td(\"btn btn-outline-danger border-0\"),Va(Qe(_$))]),R([cd(\"Expire\")]));default:return w(sd,R([td(\"btn btn-outline-danger border-0\"),Va(Qe(_$))]),R([cd(\"Delete\")]))}}(t)]))])),w(L$,\"ID\",cd(t.bd)),w(L$,\"Starts at\",cd(Ld(t.bQ))),w(L$,\"Ends at\",cd(Ld(t.aX))),w(L$,\"Updated at\",cd(Ld(t.bX))),w(L$,\"Created by\",cd(t.aV)),w(L$,\"Comment\",cd(t.aT)),w(L$,\"State\",cd(zc(t.bS.bR))),w(L$,\"Matchers\",w(ud,H,w(Je,w(_o,Ic,Wv(At)),t.bk))),r,Rc(e?kt(w(C$,t,!0)):At)]))}),X$=Yn(\"h2\"),H$=Yn(\"code\"),I$=Yn(\"pre\"),O$=Yn(\"header\"),R$=Yn(\"nav\"),V$={x:\"#/alerts\",bo:\"Alerts\"},z$={x:\"\",bo:\"\"},D$={x:\"#/settings\",bo:\"Settings\"},G$={x:\"#/silences\",bo:\"Silences\"},F$={x:\"#/status\",bo:\"Status\"},M$=l(function(n,r){return g(function(n){switch(n.$){case 0:return V$;case 1:return z$;case 2:case 3:case 4:case 5:return G$;case 6:return F$;case 7:return z$;default:return D$}}(n),r)?\" active\":\"\"}),Q$=l(function(n,r){return w(jd,R([td(\"nav-item\"+w(M$,n,r))]),R([w(wd,R([td(\"nav-link\"),za(r.x),pd(r.bo)]),R([cd(r.bo)]))]))}),U$=R([V$,G$,F$,D$,{x:\"https://prometheus.io/docs/alerting/alertmanager/\",bo:\"Documentation\"}]),B$=l(function(n,r){return w(ud,H,R([w(ud,R([w(md,\"padding\",\"40px\"),w(md,\"color\",\"red\")]),R([cd(r)])),Qc(n.cX),w(ud,R([td(\"container pb-4\")]),R([Mc(n)]))]))}),q$=ir(\"rel\"),K$=l(function(n,r){return y(Kc,\"link\",R([za(n),q$(\"stylesheet\"),w($d,\"load\",se(r(cu(n)))),w($d,\"error\",se(r(nu(\"Failed to load CSS from: \"+n))))]),H)}),qr=Rn({cD:j,cQ:_a,cR:function(n){return n.$?{$:15,a:n.a}:{$:14,a:y(nd,\"#\",(n=n.a).a6,y(nd,\"?\",n.cT,X(w(Yl,n.bu,X(n.by?\"https://\":\"http://\",n.bc)),n.au)))}},c4:$t(qr),c9:Xl,db:function(n){return{ca:R([Pc(n)]),c6:\"Alertmanager\"}}});ao={Main:{init:qr(Ri)(0)}},n.Elm?function n(r,t){for(var e in t)e in r?\"init\"==e?p(6):n(r[e],t[e]):r[e]=t[e]}(n.Elm,ao):n.Elm=ao}(this);"
  },
  {
    "path": "ui/app/src/Alerts/Api.elm",
    "content": "module Alerts.Api exposing (fetchAlertGroups, fetchAlerts, fetchReceivers)\n\nimport Data.AlertGroup exposing (AlertGroup)\nimport Data.GettableAlert exposing (GettableAlert)\nimport Data.Receiver exposing (Receiver)\nimport Json.Decode\nimport Utils.Api\nimport Utils.Filter exposing (Filter, generateAPIQueryString)\nimport Utils.Types exposing (ApiData)\n\n\nfetchReceivers : String -> Cmd (ApiData (List Receiver))\nfetchReceivers apiUrl =\n    Utils.Api.send\n        (Utils.Api.get\n            (apiUrl ++ \"/receivers\")\n            (Json.Decode.list Data.Receiver.decoder)\n        )\n\n\nfetchAlertGroups : String -> Filter -> Cmd (ApiData (List AlertGroup))\nfetchAlertGroups apiUrl filter =\n    let\n        url =\n            String.join \"/\" [ apiUrl, \"alerts\", \"groups\" ++ generateAPIQueryString filter ]\n    in\n    Utils.Api.send (Utils.Api.get url (Json.Decode.list Data.AlertGroup.decoder))\n\n\nfetchAlerts : String -> Filter -> Cmd (ApiData (List GettableAlert))\nfetchAlerts apiUrl filter =\n    let\n        url =\n            String.join \"/\" [ apiUrl, \"alerts\" ++ generateAPIQueryString filter ]\n    in\n    Utils.Api.send (Utils.Api.get url (Json.Decode.list Data.GettableAlert.decoder))\n"
  },
  {
    "path": "ui/app/src/Data/Alert.elm",
    "content": "{-\n   Alertmanager API\n   API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n\n   OpenAPI spec version: 0.0.1\n\n   NOTE: This file is auto generated by the openapi-generator.\n   https://github.com/openapitools/openapi-generator.git\n   Do not edit this file manually.\n-}\n\n\nmodule Data.Alert exposing (Alert(..), decoder, encoder)\n\nimport Dict exposing (Dict)\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Decode.Pipeline exposing (optional, required)\nimport Json.Encode as Encode\n\n\ntype alias Alert =\n    { labels : Dict String String\n    , generatorURL : Maybe String\n    }\n\n\ndecoder : Decoder Alert\ndecoder =\n    Decode.succeed Alert\n        |> required \"labels\" (Decode.dict Decode.string)\n        |> optional \"generatorURL\" (Decode.nullable Decode.string) Nothing\n\n\nencoder : Alert -> Encode.Value\nencoder model =\n    Encode.object\n        [ ( \"labels\", Encode.dict identity Encode.string model.labels )\n        , ( \"generatorURL\", Maybe.withDefault Encode.null (Maybe.map Encode.string model.generatorURL) )\n        ]\n"
  },
  {
    "path": "ui/app/src/Data/AlertGroup.elm",
    "content": "{-\n   Alertmanager API\n   API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n\n   OpenAPI spec version: 0.0.1\n\n   NOTE: This file is auto generated by the openapi-generator.\n   https://github.com/openapitools/openapi-generator.git\n   Do not edit this file manually.\n-}\n\n\nmodule Data.AlertGroup exposing (AlertGroup, decoder, encoder)\n\nimport Data.GettableAlert as GettableAlert exposing (GettableAlert)\nimport Data.Receiver as Receiver exposing (Receiver)\nimport Dict exposing (Dict)\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Decode.Pipeline exposing (optional, required)\nimport Json.Encode as Encode\n\n\ntype alias AlertGroup =\n    { labels : Dict String String\n    , receiver : Receiver\n    , alerts : List GettableAlert\n    }\n\n\ndecoder : Decoder AlertGroup\ndecoder =\n    Decode.succeed AlertGroup\n        |> required \"labels\" (Decode.dict Decode.string)\n        |> required \"receiver\" Receiver.decoder\n        |> required \"alerts\" (Decode.list GettableAlert.decoder)\n\n\nencoder : AlertGroup -> Encode.Value\nencoder model =\n    Encode.object\n        [ ( \"labels\", Encode.dict identity Encode.string model.labels )\n        , ( \"receiver\", Receiver.encoder model.receiver )\n        , ( \"alerts\", Encode.list GettableAlert.encoder model.alerts )\n        ]\n"
  },
  {
    "path": "ui/app/src/Data/AlertStatus.elm",
    "content": "{-\n   Alertmanager API\n   API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n\n   OpenAPI spec version: 0.0.1\n\n   NOTE: This file is auto generated by the openapi-generator.\n   https://github.com/openapitools/openapi-generator.git\n   Do not edit this file manually.\n-}\n\n\nmodule Data.AlertStatus exposing (AlertStatus, State(..), decoder, encoder)\n\nimport Dict exposing (Dict)\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Decode.Pipeline exposing (optional, required)\nimport Json.Encode as Encode\n\n\ntype alias AlertStatus =\n    { state : State\n    , silencedBy : List String\n    , inhibitedBy : List String\n    , mutedBy : List String\n    }\n\n\ntype State\n    = Unprocessed\n    | Active\n    | Suppressed\n\n\ndecoder : Decoder AlertStatus\ndecoder =\n    Decode.succeed AlertStatus\n        |> required \"state\" stateDecoder\n        |> required \"silencedBy\" (Decode.list Decode.string)\n        |> required \"inhibitedBy\" (Decode.list Decode.string)\n        |> required \"mutedBy\" (Decode.list Decode.string)\n\n\nencoder : AlertStatus -> Encode.Value\nencoder model =\n    Encode.object\n        [ ( \"state\", stateEncoder model.state )\n        , ( \"silencedBy\", Encode.list Encode.string model.silencedBy )\n        , ( \"inhibitedBy\", Encode.list Encode.string model.inhibitedBy )\n        , ( \"mutedBy\", Encode.list Encode.string model.mutedBy )\n        ]\n\n\nstateDecoder : Decoder State\nstateDecoder =\n    Decode.string\n        |> Decode.andThen\n            (\\str ->\n                case str of\n                    \"unprocessed\" ->\n                        Decode.succeed Unprocessed\n\n                    \"active\" ->\n                        Decode.succeed Active\n\n                    \"suppressed\" ->\n                        Decode.succeed Suppressed\n\n                    other ->\n                        Decode.fail <| \"Unknown type: \" ++ other\n            )\n\n\nstateEncoder : State -> Encode.Value\nstateEncoder model =\n    case model of\n        Unprocessed ->\n            Encode.string \"unprocessed\"\n\n        Active ->\n            Encode.string \"active\"\n\n        Suppressed ->\n            Encode.string \"suppressed\"\n"
  },
  {
    "path": "ui/app/src/Data/AlertmanagerConfig.elm",
    "content": "{-\n   Alertmanager API\n   API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n\n   OpenAPI spec version: 0.0.1\n\n   NOTE: This file is auto generated by the openapi-generator.\n   https://github.com/openapitools/openapi-generator.git\n   Do not edit this file manually.\n-}\n\n\nmodule Data.AlertmanagerConfig exposing (AlertmanagerConfig, decoder, encoder)\n\nimport Dict exposing (Dict)\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Decode.Pipeline exposing (optional, required)\nimport Json.Encode as Encode\n\n\ntype alias AlertmanagerConfig =\n    { original : String\n    }\n\n\ndecoder : Decoder AlertmanagerConfig\ndecoder =\n    Decode.succeed AlertmanagerConfig\n        |> required \"original\" Decode.string\n\n\nencoder : AlertmanagerConfig -> Encode.Value\nencoder model =\n    Encode.object\n        [ ( \"original\", Encode.string model.original )\n        ]\n"
  },
  {
    "path": "ui/app/src/Data/AlertmanagerStatus.elm",
    "content": "{-\n   Alertmanager API\n   API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n\n   OpenAPI spec version: 0.0.1\n\n   NOTE: This file is auto generated by the openapi-generator.\n   https://github.com/openapitools/openapi-generator.git\n   Do not edit this file manually.\n-}\n\n\nmodule Data.AlertmanagerStatus exposing (AlertmanagerStatus, decoder, encoder)\n\nimport Data.AlertmanagerConfig as AlertmanagerConfig exposing (AlertmanagerConfig)\nimport Data.ClusterStatus as ClusterStatus exposing (ClusterStatus)\nimport Data.VersionInfo as VersionInfo exposing (VersionInfo)\nimport DateTime exposing (DateTime)\nimport Dict exposing (Dict)\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Decode.Pipeline exposing (optional, required)\nimport Json.Encode as Encode\n\n\ntype alias AlertmanagerStatus =\n    { cluster : ClusterStatus\n    , versionInfo : VersionInfo\n    , config : AlertmanagerConfig\n    , uptime : DateTime\n    }\n\n\ndecoder : Decoder AlertmanagerStatus\ndecoder =\n    Decode.succeed AlertmanagerStatus\n        |> required \"cluster\" ClusterStatus.decoder\n        |> required \"versionInfo\" VersionInfo.decoder\n        |> required \"config\" AlertmanagerConfig.decoder\n        |> required \"uptime\" DateTime.decoder\n\n\nencoder : AlertmanagerStatus -> Encode.Value\nencoder model =\n    Encode.object\n        [ ( \"cluster\", ClusterStatus.encoder model.cluster )\n        , ( \"versionInfo\", VersionInfo.encoder model.versionInfo )\n        , ( \"config\", AlertmanagerConfig.encoder model.config )\n        , ( \"uptime\", DateTime.encoder model.uptime )\n        ]\n"
  },
  {
    "path": "ui/app/src/Data/ClusterStatus.elm",
    "content": "{-\n   Alertmanager API\n   API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n\n   OpenAPI spec version: 0.0.1\n\n   NOTE: This file is auto generated by the openapi-generator.\n   https://github.com/openapitools/openapi-generator.git\n   Do not edit this file manually.\n-}\n\n\nmodule Data.ClusterStatus exposing (ClusterStatus, Status(..), decoder, encoder)\n\nimport Data.PeerStatus as PeerStatus exposing (PeerStatus)\nimport Dict exposing (Dict)\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Decode.Pipeline exposing (optional, required)\nimport Json.Encode as Encode\n\n\ntype alias ClusterStatus =\n    { name : Maybe String\n    , status : Status\n    , peers : Maybe (List PeerStatus)\n    }\n\n\ntype Status\n    = Ready\n    | Settling\n    | Disabled\n\n\ndecoder : Decoder ClusterStatus\ndecoder =\n    Decode.succeed ClusterStatus\n        |> optional \"name\" (Decode.nullable Decode.string) Nothing\n        |> required \"status\" statusDecoder\n        |> optional \"peers\" (Decode.nullable (Decode.list PeerStatus.decoder)) Nothing\n\n\nencoder : ClusterStatus -> Encode.Value\nencoder model =\n    Encode.object\n        [ ( \"name\", Maybe.withDefault Encode.null (Maybe.map Encode.string model.name) )\n        , ( \"status\", statusEncoder model.status )\n        , ( \"peers\", Maybe.withDefault Encode.null (Maybe.map (Encode.list PeerStatus.encoder) model.peers) )\n        ]\n\n\nstatusDecoder : Decoder Status\nstatusDecoder =\n    Decode.string\n        |> Decode.andThen\n            (\\str ->\n                case str of\n                    \"ready\" ->\n                        Decode.succeed Ready\n\n                    \"settling\" ->\n                        Decode.succeed Settling\n\n                    \"disabled\" ->\n                        Decode.succeed Disabled\n\n                    other ->\n                        Decode.fail <| \"Unknown type: \" ++ other\n            )\n\n\nstatusEncoder : Status -> Encode.Value\nstatusEncoder model =\n    case model of\n        Ready ->\n            Encode.string \"ready\"\n\n        Settling ->\n            Encode.string \"settling\"\n\n        Disabled ->\n            Encode.string \"disabled\"\n"
  },
  {
    "path": "ui/app/src/Data/GettableAlert.elm",
    "content": "{-\n   Alertmanager API\n   API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n\n   OpenAPI spec version: 0.0.1\n\n   NOTE: This file is auto generated by the openapi-generator.\n   https://github.com/openapitools/openapi-generator.git\n   Do not edit this file manually.\n-}\n\n\nmodule Data.GettableAlert exposing (GettableAlert, decoder, encoder)\n\nimport Data.AlertStatus as AlertStatus exposing (AlertStatus)\nimport Data.Receiver as Receiver exposing (Receiver)\nimport DateTime exposing (DateTime)\nimport Dict exposing (Dict)\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Decode.Pipeline exposing (optional, required)\nimport Json.Encode as Encode\n\n\ntype alias GettableAlert =\n    { labels : Dict String String\n    , generatorURL : Maybe String\n    , annotations : Dict String String\n    , receivers : List Receiver\n    , fingerprint : String\n    , startsAt : DateTime\n    , updatedAt : DateTime\n    , endsAt : DateTime\n    , status : AlertStatus\n    }\n\n\ndecoder : Decoder GettableAlert\ndecoder =\n    Decode.succeed GettableAlert\n        |> required \"labels\" (Decode.dict Decode.string)\n        |> optional \"generatorURL\" (Decode.nullable Decode.string) Nothing\n        |> required \"annotations\" (Decode.dict Decode.string)\n        |> required \"receivers\" (Decode.list Receiver.decoder)\n        |> required \"fingerprint\" Decode.string\n        |> required \"startsAt\" DateTime.decoder\n        |> required \"updatedAt\" DateTime.decoder\n        |> required \"endsAt\" DateTime.decoder\n        |> required \"status\" AlertStatus.decoder\n\n\nencoder : GettableAlert -> Encode.Value\nencoder model =\n    Encode.object\n        [ ( \"labels\", Encode.dict identity Encode.string model.labels )\n        , ( \"generatorURL\", Maybe.withDefault Encode.null (Maybe.map Encode.string model.generatorURL) )\n        , ( \"annotations\", Encode.dict identity Encode.string model.annotations )\n        , ( \"receivers\", Encode.list Receiver.encoder model.receivers )\n        , ( \"fingerprint\", Encode.string model.fingerprint )\n        , ( \"startsAt\", DateTime.encoder model.startsAt )\n        , ( \"updatedAt\", DateTime.encoder model.updatedAt )\n        , ( \"endsAt\", DateTime.encoder model.endsAt )\n        , ( \"status\", AlertStatus.encoder model.status )\n        ]\n"
  },
  {
    "path": "ui/app/src/Data/GettableSilence.elm",
    "content": "{-\n   Alertmanager API\n   API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n\n   OpenAPI spec version: 0.0.1\n\n   NOTE: This file is auto generated by the openapi-generator.\n   https://github.com/openapitools/openapi-generator.git\n   Do not edit this file manually.\n-}\n\n\nmodule Data.GettableSilence exposing (GettableSilence, decoder, encoder)\n\nimport Data.Matcher as Matcher exposing (Matcher)\nimport Data.SilenceStatus as SilenceStatus exposing (SilenceStatus)\nimport DateTime exposing (DateTime)\nimport Dict exposing (Dict)\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Decode.Pipeline exposing (optional, required)\nimport Json.Encode as Encode\n\n\ntype alias GettableSilence =\n    { matchers : List Matcher\n    , startsAt : DateTime\n    , endsAt : DateTime\n    , createdBy : String\n    , comment : String\n    , annotations : Maybe (Dict String String)\n    , id : String\n    , status : SilenceStatus\n    , updatedAt : DateTime\n    }\n\n\ndecoder : Decoder GettableSilence\ndecoder =\n    Decode.succeed GettableSilence\n        |> required \"matchers\" (Decode.list Matcher.decoder)\n        |> required \"startsAt\" DateTime.decoder\n        |> required \"endsAt\" DateTime.decoder\n        |> required \"createdBy\" Decode.string\n        |> required \"comment\" Decode.string\n        |> optional \"annotations\" (Decode.nullable (Decode.dict Decode.string)) Nothing\n        |> required \"id\" Decode.string\n        |> required \"status\" SilenceStatus.decoder\n        |> required \"updatedAt\" DateTime.decoder\n\n\nencoder : GettableSilence -> Encode.Value\nencoder model =\n    Encode.object\n        [ ( \"matchers\", Encode.list Matcher.encoder model.matchers )\n        , ( \"startsAt\", DateTime.encoder model.startsAt )\n        , ( \"endsAt\", DateTime.encoder model.endsAt )\n        , ( \"createdBy\", Encode.string model.createdBy )\n        , ( \"comment\", Encode.string model.comment )\n        , ( \"annotations\", Maybe.withDefault Encode.null (Maybe.map (Encode.dict identity Encode.string) model.annotations) )\n        , ( \"id\", Encode.string model.id )\n        , ( \"status\", SilenceStatus.encoder model.status )\n        , ( \"updatedAt\", DateTime.encoder model.updatedAt )\n        ]\n"
  },
  {
    "path": "ui/app/src/Data/InlineResponse200.elm",
    "content": "{-\n   Alertmanager API\n   API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n\n   OpenAPI spec version: 0.0.1\n\n   NOTE: This file is auto generated by the openapi-generator.\n   https://github.com/openapitools/openapi-generator.git\n   Do not edit this file manually.\n-}\n\n\nmodule Data.InlineResponse200 exposing (InlineResponse200, decoder, encoder)\n\nimport Dict exposing (Dict)\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Decode.Pipeline exposing (optional, required)\nimport Json.Encode as Encode\n\n\ntype alias InlineResponse200 =\n    { silenceID : Maybe String\n    }\n\n\ndecoder : Decoder InlineResponse200\ndecoder =\n    Decode.succeed InlineResponse200\n        |> optional \"silenceID\" (Decode.nullable Decode.string) Nothing\n\n\nencoder : InlineResponse200 -> Encode.Value\nencoder model =\n    Encode.object\n        [ ( \"silenceID\", Maybe.withDefault Encode.null (Maybe.map Encode.string model.silenceID) )\n        ]\n"
  },
  {
    "path": "ui/app/src/Data/Matcher.elm",
    "content": "{-\n   Alertmanager API\n   API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n\n   OpenAPI spec version: 0.0.1\n\n   NOTE: This file is auto generated by the openapi-generator.\n   https://github.com/openapitools/openapi-generator.git\n   Do not edit this file manually.\n-}\n\n\nmodule Data.Matcher exposing (Matcher, decoder, encoder)\n\nimport Dict exposing (Dict)\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Decode.Pipeline exposing (optional, required)\nimport Json.Encode as Encode\n\n\ntype alias Matcher =\n    { name : String\n    , value : String\n    , isRegex : Bool\n    , isEqual : Maybe Bool\n    }\n\n\ndecoder : Decoder Matcher\ndecoder =\n    Decode.succeed Matcher\n        |> required \"name\" Decode.string\n        |> required \"value\" Decode.string\n        |> required \"isRegex\" Decode.bool\n        |> optional \"isEqual\" (Decode.nullable Decode.bool) (Just True)\n\n\nencoder : Matcher -> Encode.Value\nencoder model =\n    Encode.object\n        [ ( \"name\", Encode.string model.name )\n        , ( \"value\", Encode.string model.value )\n        , ( \"isRegex\", Encode.bool model.isRegex )\n        , ( \"isEqual\", Maybe.withDefault Encode.null (Maybe.map Encode.bool model.isEqual) )\n        ]\n"
  },
  {
    "path": "ui/app/src/Data/PeerStatus.elm",
    "content": "{-\n   Alertmanager API\n   API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n\n   OpenAPI spec version: 0.0.1\n\n   NOTE: This file is auto generated by the openapi-generator.\n   https://github.com/openapitools/openapi-generator.git\n   Do not edit this file manually.\n-}\n\n\nmodule Data.PeerStatus exposing (PeerStatus, decoder, encoder)\n\nimport Dict exposing (Dict)\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Decode.Pipeline exposing (optional, required)\nimport Json.Encode as Encode\n\n\ntype alias PeerStatus =\n    { name : String\n    , address : String\n    }\n\n\ndecoder : Decoder PeerStatus\ndecoder =\n    Decode.succeed PeerStatus\n        |> required \"name\" Decode.string\n        |> required \"address\" Decode.string\n\n\nencoder : PeerStatus -> Encode.Value\nencoder model =\n    Encode.object\n        [ ( \"name\", Encode.string model.name )\n        , ( \"address\", Encode.string model.address )\n        ]\n"
  },
  {
    "path": "ui/app/src/Data/PostableAlert.elm",
    "content": "{-\n   Alertmanager API\n   API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n\n   OpenAPI spec version: 0.0.1\n\n   NOTE: This file is auto generated by the openapi-generator.\n   https://github.com/openapitools/openapi-generator.git\n   Do not edit this file manually.\n-}\n\n\nmodule Data.PostableAlert exposing (PostableAlert, decoder, encoder)\n\nimport DateTime exposing (DateTime)\nimport Dict exposing (Dict)\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Decode.Pipeline exposing (optional, required)\nimport Json.Encode as Encode\n\n\ntype alias PostableAlert =\n    { labels : Dict String String\n    , generatorURL : Maybe String\n    , startsAt : Maybe DateTime\n    , endsAt : Maybe DateTime\n    , annotations : Maybe (Dict String String)\n    }\n\n\ndecoder : Decoder PostableAlert\ndecoder =\n    Decode.succeed PostableAlert\n        |> required \"labels\" (Decode.dict Decode.string)\n        |> optional \"generatorURL\" (Decode.nullable Decode.string) Nothing\n        |> optional \"startsAt\" (Decode.nullable DateTime.decoder) Nothing\n        |> optional \"endsAt\" (Decode.nullable DateTime.decoder) Nothing\n        |> optional \"annotations\" (Decode.nullable (Decode.dict Decode.string)) Nothing\n\n\nencoder : PostableAlert -> Encode.Value\nencoder model =\n    Encode.object\n        [ ( \"labels\", Encode.dict identity Encode.string model.labels )\n        , ( \"generatorURL\", Maybe.withDefault Encode.null (Maybe.map Encode.string model.generatorURL) )\n        , ( \"startsAt\", Maybe.withDefault Encode.null (Maybe.map DateTime.encoder model.startsAt) )\n        , ( \"endsAt\", Maybe.withDefault Encode.null (Maybe.map DateTime.encoder model.endsAt) )\n        , ( \"annotations\", Maybe.withDefault Encode.null (Maybe.map (Encode.dict identity Encode.string) model.annotations) )\n        ]\n"
  },
  {
    "path": "ui/app/src/Data/PostableSilence.elm",
    "content": "{-\n   Alertmanager API\n   API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n\n   OpenAPI spec version: 0.0.1\n\n   NOTE: This file is auto generated by the openapi-generator.\n   https://github.com/openapitools/openapi-generator.git\n   Do not edit this file manually.\n-}\n\n\nmodule Data.PostableSilence exposing (PostableSilence, decoder, encoder)\n\nimport Data.Matcher as Matcher exposing (Matcher)\nimport DateTime exposing (DateTime)\nimport Dict exposing (Dict)\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Decode.Pipeline exposing (optional, required)\nimport Json.Encode as Encode\n\n\ntype alias PostableSilence =\n    { matchers : List Matcher\n    , startsAt : DateTime\n    , endsAt : DateTime\n    , createdBy : String\n    , comment : String\n    , annotations : Maybe (Dict String String)\n    , id : Maybe String\n    }\n\n\ndecoder : Decoder PostableSilence\ndecoder =\n    Decode.succeed PostableSilence\n        |> required \"matchers\" (Decode.list Matcher.decoder)\n        |> required \"startsAt\" DateTime.decoder\n        |> required \"endsAt\" DateTime.decoder\n        |> required \"createdBy\" Decode.string\n        |> required \"comment\" Decode.string\n        |> optional \"annotations\" (Decode.nullable (Decode.dict Decode.string)) Nothing\n        |> optional \"id\" (Decode.nullable Decode.string) Nothing\n\n\nencoder : PostableSilence -> Encode.Value\nencoder model =\n    Encode.object\n        [ ( \"matchers\", Encode.list Matcher.encoder model.matchers )\n        , ( \"startsAt\", DateTime.encoder model.startsAt )\n        , ( \"endsAt\", DateTime.encoder model.endsAt )\n        , ( \"createdBy\", Encode.string model.createdBy )\n        , ( \"comment\", Encode.string model.comment )\n        , ( \"annotations\", Maybe.withDefault Encode.null (Maybe.map (Encode.dict identity Encode.string) model.annotations) )\n        , ( \"id\", Maybe.withDefault Encode.null (Maybe.map Encode.string model.id) )\n        ]\n"
  },
  {
    "path": "ui/app/src/Data/Receiver.elm",
    "content": "{-\n   Alertmanager API\n   API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n\n   OpenAPI spec version: 0.0.1\n\n   NOTE: This file is auto generated by the openapi-generator.\n   https://github.com/openapitools/openapi-generator.git\n   Do not edit this file manually.\n-}\n\n\nmodule Data.Receiver exposing (Receiver, decoder, encoder)\n\nimport Dict exposing (Dict)\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Decode.Pipeline exposing (optional, required)\nimport Json.Encode as Encode\n\n\ntype alias Receiver =\n    { name : String\n    }\n\n\ndecoder : Decoder Receiver\ndecoder =\n    Decode.succeed Receiver\n        |> required \"name\" Decode.string\n\n\nencoder : Receiver -> Encode.Value\nencoder model =\n    Encode.object\n        [ ( \"name\", Encode.string model.name )\n        ]\n"
  },
  {
    "path": "ui/app/src/Data/Silence.elm",
    "content": "{-\n   Alertmanager API\n   API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n\n   OpenAPI spec version: 0.0.1\n\n   NOTE: This file is auto generated by the openapi-generator.\n   https://github.com/openapitools/openapi-generator.git\n   Do not edit this file manually.\n-}\n\n\nmodule Data.Silence exposing (Silence(..), decoder, encoder)\n\nimport Data.Matcher as Matcher exposing (Matcher)\nimport DateTime exposing (DateTime)\nimport Dict exposing (Dict)\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Decode.Pipeline exposing (optional, required)\nimport Json.Encode as Encode\n\n\ntype alias Silence =\n    { matchers : List Matcher\n    , startsAt : DateTime\n    , endsAt : DateTime\n    , createdBy : String\n    , comment : String\n    , annotations : Maybe (Dict String String)\n    }\n\n\ndecoder : Decoder Silence\ndecoder =\n    Decode.succeed Silence\n        |> required \"matchers\" (Decode.list Matcher.decoder)\n        |> required \"startsAt\" DateTime.decoder\n        |> required \"endsAt\" DateTime.decoder\n        |> required \"createdBy\" Decode.string\n        |> required \"comment\" Decode.string\n        |> optional \"annotations\" (Decode.nullable (Decode.dict Decode.string)) Nothing\n\n\nencoder : Silence -> Encode.Value\nencoder model =\n    Encode.object\n        [ ( \"matchers\", Encode.list Matcher.encoder model.matchers )\n        , ( \"startsAt\", DateTime.encoder model.startsAt )\n        , ( \"endsAt\", DateTime.encoder model.endsAt )\n        , ( \"createdBy\", Encode.string model.createdBy )\n        , ( \"comment\", Encode.string model.comment )\n        , ( \"annotations\", Maybe.withDefault Encode.null (Maybe.map (Encode.dict identity Encode.string) model.annotations) )\n        ]\n"
  },
  {
    "path": "ui/app/src/Data/SilenceStatus.elm",
    "content": "{-\n   Alertmanager API\n   API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n\n   OpenAPI spec version: 0.0.1\n\n   NOTE: This file is auto generated by the openapi-generator.\n   https://github.com/openapitools/openapi-generator.git\n   Do not edit this file manually.\n-}\n\n\nmodule Data.SilenceStatus exposing (SilenceStatus, State(..), decoder, encoder)\n\nimport Dict exposing (Dict)\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Decode.Pipeline exposing (optional, required)\nimport Json.Encode as Encode\n\n\ntype alias SilenceStatus =\n    { state : State\n    }\n\n\ntype State\n    = Expired\n    | Active\n    | Pending\n\n\ndecoder : Decoder SilenceStatus\ndecoder =\n    Decode.succeed SilenceStatus\n        |> required \"state\" stateDecoder\n\n\nencoder : SilenceStatus -> Encode.Value\nencoder model =\n    Encode.object\n        [ ( \"state\", stateEncoder model.state )\n        ]\n\n\nstateDecoder : Decoder State\nstateDecoder =\n    Decode.string\n        |> Decode.andThen\n            (\\str ->\n                case str of\n                    \"expired\" ->\n                        Decode.succeed Expired\n\n                    \"active\" ->\n                        Decode.succeed Active\n\n                    \"pending\" ->\n                        Decode.succeed Pending\n\n                    other ->\n                        Decode.fail <| \"Unknown type: \" ++ other\n            )\n\n\nstateEncoder : State -> Encode.Value\nstateEncoder model =\n    case model of\n        Expired ->\n            Encode.string \"expired\"\n\n        Active ->\n            Encode.string \"active\"\n\n        Pending ->\n            Encode.string \"pending\"\n"
  },
  {
    "path": "ui/app/src/Data/VersionInfo.elm",
    "content": "{-\n   Alertmanager API\n   API of the Prometheus Alertmanager (https://github.com/prometheus/alertmanager)\n\n   OpenAPI spec version: 0.0.1\n\n   NOTE: This file is auto generated by the openapi-generator.\n   https://github.com/openapitools/openapi-generator.git\n   Do not edit this file manually.\n-}\n\n\nmodule Data.VersionInfo exposing (VersionInfo, decoder, encoder)\n\nimport Dict exposing (Dict)\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Decode.Pipeline exposing (optional, required)\nimport Json.Encode as Encode\n\n\ntype alias VersionInfo =\n    { version : String\n    , revision : String\n    , branch : String\n    , buildUser : String\n    , buildDate : String\n    , goVersion : String\n    }\n\n\ndecoder : Decoder VersionInfo\ndecoder =\n    Decode.succeed VersionInfo\n        |> required \"version\" Decode.string\n        |> required \"revision\" Decode.string\n        |> required \"branch\" Decode.string\n        |> required \"buildUser\" Decode.string\n        |> required \"buildDate\" Decode.string\n        |> required \"goVersion\" Decode.string\n\n\nencoder : VersionInfo -> Encode.Value\nencoder model =\n    Encode.object\n        [ ( \"version\", Encode.string model.version )\n        , ( \"revision\", Encode.string model.revision )\n        , ( \"branch\", Encode.string model.branch )\n        , ( \"buildUser\", Encode.string model.buildUser )\n        , ( \"buildDate\", Encode.string model.buildDate )\n        , ( \"goVersion\", Encode.string model.goVersion )\n        ]\n"
  },
  {
    "path": "ui/app/src/DateTime.elm",
    "content": "module DateTime exposing (DateTime, decoder, encoder, toString)\n\nimport Iso8601\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Encode as Encode\nimport Result\nimport Time\n\n\ntype alias DateTime =\n    Time.Posix\n\n\ndecoder : Decoder DateTime\ndecoder =\n    Decode.string\n        |> Decode.andThen decodeIsoString\n\n\nencoder : DateTime -> Encode.Value\nencoder =\n    Encode.string << toString\n\n\ndecodeIsoString : String -> Decoder DateTime\ndecodeIsoString str =\n    case Iso8601.toTime str of\n        Result.Ok posix ->\n            Decode.succeed posix\n\n        Result.Err _ ->\n            Decode.fail <| \"Invalid date: \" ++ str\n\n\ntoString : DateTime -> String\ntoString =\n    Iso8601.fromTime\n"
  },
  {
    "path": "ui/app/src/Main.elm",
    "content": "module Main exposing (main)\n\nimport Browser exposing (UrlRequest(..))\nimport Browser.Navigation exposing (Key)\nimport Json.Decode as Json\nimport Parsing\nimport Types exposing (Model, Msg(..), Route(..))\nimport Updates exposing (update)\nimport Url exposing (Url)\nimport Utils.Api as Api\nimport Utils.DateTimePicker.Utils exposing (FirstDayOfWeek(..))\nimport Utils.Filter exposing (nullFilter)\nimport Utils.Types exposing (ApiData(..))\nimport Views\nimport Views.AlertList.Types exposing (initAlertList)\nimport Views.SilenceForm.Types exposing (initSilenceForm)\nimport Views.SilenceList.Types exposing (initSilenceList)\nimport Views.SilenceView.Types exposing (initSilenceView)\nimport Views.Status.Types exposing (initStatusModel)\n\n\nmain : Program Json.Value Model Msg\nmain =\n    Browser.application\n        { init = init\n        , update = update\n        , view =\n            \\model ->\n                { title = \"Alertmanager\"\n                , body = [ Views.view model ]\n                }\n        , subscriptions = always Sub.none\n        , onUrlRequest =\n            \\request ->\n                case request of\n                    Internal url ->\n                        NavigateToInternalUrl (Url.toString url)\n\n                    External url ->\n                        NavigateToExternalUrl url\n        , onUrlChange = urlUpdate\n        }\n\n\ninit : Json.Value -> Url -> Key -> ( Model, Cmd Msg )\ninit flags url key =\n    let\n        route =\n            Parsing.urlParser url\n\n        filter =\n            case route of\n                AlertsRoute filter_ ->\n                    filter_\n\n                SilenceListRoute filter_ ->\n                    filter_\n\n                _ ->\n                    nullFilter\n\n        prod =\n            flags\n                |> Json.decodeValue (Json.field \"production\" Json.bool)\n                |> Result.withDefault False\n\n        defaultCreator =\n            flags\n                |> Json.decodeValue (Json.field \"defaultCreator\" Json.string)\n                |> Result.withDefault \"\"\n\n        groupExpandAll =\n            flags\n                |> Json.decodeValue (Json.field \"groupExpandAll\" Json.bool)\n                |> Result.withDefault False\n\n        apiUrl =\n            if prod then\n                Api.makeApiUrl url.path\n\n            else\n                Api.makeApiUrl \"http://localhost:9093/\"\n\n        libUrl =\n            if prod then\n                url.path\n\n            else\n                \"/\"\n\n        firstDayOfWeek =\n            flags\n                |> Json.decodeValue (Json.field \"firstDayOfWeek\" Json.string)\n                |> Result.withDefault \"Sunday\"\n                |> (\\d ->\n                        case d of\n                            \"Sunday\" ->\n                                Sunday\n\n                            \"Saturday\" ->\n                                Saturday\n\n                            _ ->\n                                Monday\n                   )\n    in\n    update (urlUpdate url)\n        (Model\n            (initSilenceList key)\n            (initSilenceView key)\n            (initSilenceForm key firstDayOfWeek)\n            (initAlertList key groupExpandAll)\n            route\n            filter\n            initStatusModel\n            url.path\n            apiUrl\n            libUrl\n            Loading\n            Loading\n            Loading\n            defaultCreator\n            groupExpandAll\n            key\n            { firstDayOfWeek = firstDayOfWeek\n            }\n        )\n\n\nurlUpdate : Url -> Msg\nurlUpdate url =\n    let\n        route =\n            Parsing.urlParser url\n    in\n    case route of\n        SilenceListRoute maybeFilter ->\n            NavigateToSilenceList maybeFilter\n\n        SilenceViewRoute silenceId ->\n            NavigateToSilenceView silenceId\n\n        SilenceFormEditRoute silenceId ->\n            NavigateToSilenceFormEdit silenceId\n\n        SilenceFormNewRoute params ->\n            NavigateToSilenceFormNew params\n\n        AlertsRoute filter ->\n            NavigateToAlerts filter\n\n        StatusRoute ->\n            NavigateToStatus\n\n        SettingsRoute ->\n            NavigateToSettings\n\n        TopLevelRoute ->\n            RedirectAlerts\n\n        NotFoundRoute ->\n            NavigateToNotFound\n"
  },
  {
    "path": "ui/app/src/Parsing.elm",
    "content": "module Parsing exposing (urlParser)\n\nimport Regex\nimport Types exposing (Route(..))\nimport Url exposing (Url)\nimport Url.Parser exposing (Parser, map, oneOf, parse, top)\nimport Views.AlertList.Parsing exposing (alertsParser)\nimport Views.Settings.Parsing exposing (settingsViewParser)\nimport Views.SilenceForm.Parsing exposing (silenceFormEditParser, silenceFormNewParser)\nimport Views.SilenceList.Parsing exposing (silenceListParser)\nimport Views.SilenceView.Parsing exposing (silenceViewParser)\nimport Views.Status.Parsing exposing (statusParser)\n\n\nurlParser : Url -> Route\nurlParser url =\n    let\n        -- Parse a query string occurring after the hash if it exists, and use\n        -- it for routing.\n        hashAndQuery =\n            url.fragment\n                |> Maybe.map\n                    (Regex.splitAtMost 1 (Regex.fromString \"\\\\?\" |> Maybe.withDefault Regex.never))\n                |> Maybe.withDefault []\n\n        ( path, query ) =\n            case hashAndQuery of\n                [] ->\n                    ( \"/\", Nothing )\n\n                h :: [] ->\n                    ( h, Nothing )\n\n                h :: rest ->\n                    ( h, Just (String.concat rest) )\n    in\n    case parse routeParser { url | query = query, fragment = Nothing, path = path } of\n        Just route ->\n            route\n\n        Nothing ->\n            NotFoundRoute\n\n\nrouteParser : Parser (Route -> a) a\nrouteParser =\n    oneOf\n        [ map SilenceListRoute silenceListParser\n        , map StatusRoute statusParser\n        , map SettingsRoute settingsViewParser\n        , map SilenceFormNewRoute silenceFormNewParser\n        , map SilenceViewRoute silenceViewParser\n        , map SilenceFormEditRoute silenceFormEditParser\n        , map AlertsRoute alertsParser\n        , map TopLevelRoute top\n        ]\n"
  },
  {
    "path": "ui/app/src/Silences/Api.elm",
    "content": "module Silences.Api exposing (create, destroy, getSilence, getSilences)\n\nimport Data.GettableSilence exposing (GettableSilence)\nimport Data.PostableSilence exposing (PostableSilence)\nimport Http\nimport Json.Decode\nimport Silences.Decoders\nimport Utils.Api\nimport Utils.Filter exposing (Filter, generateAPIQueryString)\nimport Utils.Types exposing (ApiData(..))\n\n\ngetSilences : String -> Filter -> (ApiData (List GettableSilence) -> msg) -> Cmd msg\ngetSilences apiUrl filter msg =\n    let\n        url =\n            String.join \"/\" [ apiUrl, \"silences\" ++ generateAPIQueryString filter ]\n    in\n    Utils.Api.send (Utils.Api.get url (Json.Decode.list Data.GettableSilence.decoder))\n        |> Cmd.map msg\n\n\ngetSilence : String -> String -> (ApiData GettableSilence -> msg) -> Cmd msg\ngetSilence apiUrl uuid msg =\n    let\n        url =\n            String.join \"/\" [ apiUrl, \"silence\", uuid ]\n    in\n    Utils.Api.send (Utils.Api.get url Data.GettableSilence.decoder)\n        |> Cmd.map msg\n\n\ncreate : String -> PostableSilence -> Cmd (ApiData String)\ncreate apiUrl silence =\n    let\n        url =\n            String.join \"/\" [ apiUrl, \"silences\" ]\n\n        body =\n            Http.jsonBody <| Data.PostableSilence.encoder silence\n    in\n    -- TODO: This should return the silence, not just the ID, so that we can\n    -- redirect to the silence show page.\n    Utils.Api.send\n        (Utils.Api.post url body Silences.Decoders.create)\n\n\ndestroy : String -> GettableSilence -> (ApiData String -> msg) -> Cmd msg\ndestroy apiUrl silence msg =\n    -- The incorrect route using \"silences\" receives a 405. The route seems to\n    -- be matching on /silences and ignoring the :sid, should be getting a 404.\n    let\n        url =\n            String.join \"/\" [ apiUrl, \"silence\", silence.id ]\n\n        responseDecoder =\n            -- Silences.Encoders.silence silence\n            Silences.Decoders.destroy\n    in\n    Utils.Api.send (Utils.Api.delete url responseDecoder)\n        |> Cmd.map msg\n"
  },
  {
    "path": "ui/app/src/Silences/Decoders.elm",
    "content": "module Silences.Decoders exposing (create, destroy)\n\nimport Json.Decode as Json\nimport Utils.Types exposing (ApiData(..))\n\n\ncreate : Json.Decoder String\ncreate =\n    Json.at [ \"silenceID\" ] Json.string\n\n\ndestroy : Json.Decoder String\ndestroy =\n    Json.at [ \"status\" ] Json.string\n"
  },
  {
    "path": "ui/app/src/Silences/Types.elm",
    "content": "module Silences.Types exposing\n    ( nullSilence\n    , stateToString\n    )\n\nimport Data.Matcher exposing (Matcher)\nimport Data.PostableSilence exposing (PostableSilence)\nimport Data.SilenceStatus exposing (State(..))\nimport Time\n\n\nnullSilence : PostableSilence\nnullSilence =\n    { id = Nothing\n    , createdBy = \"\"\n    , comment = \"\"\n    , startsAt = Time.millisToPosix 0\n    , endsAt = Time.millisToPosix 0\n    , matchers = nullMatchers\n    , annotations = Nothing\n    }\n\n\nnullMatchers : List Matcher\nnullMatchers =\n    [ nullMatcher ]\n\n\nnullMatcher : Matcher\nnullMatcher =\n    Matcher \"\" \"\" False (Just True)\n\n\nstateToString : State -> String\nstateToString state =\n    case state of\n        Active ->\n            \"active\"\n\n        Pending ->\n            \"pending\"\n\n        Expired ->\n            \"expired\"\n"
  },
  {
    "path": "ui/app/src/Status/Api.elm",
    "content": "module Status.Api exposing (clusterStatusToString, getStatus)\n\nimport Data.AlertmanagerStatus exposing (AlertmanagerStatus)\nimport Data.ClusterStatus exposing (Status(..))\nimport Utils.Api exposing (get, send)\nimport Utils.Types exposing (ApiData)\n\n\ngetStatus : String -> (ApiData AlertmanagerStatus -> msg) -> Cmd msg\ngetStatus apiUrl msg =\n    let\n        url =\n            String.join \"/\" [ apiUrl, \"status\" ]\n\n        request =\n            get url Data.AlertmanagerStatus.decoder\n    in\n    Cmd.map msg <| send request\n\n\nclusterStatusToString : Status -> String\nclusterStatusToString status =\n    case status of\n        Ready ->\n            \"ready\"\n\n        Settling ->\n            \"settling\"\n\n        Disabled ->\n            \"disabled\"\n"
  },
  {
    "path": "ui/app/src/Status/Types.elm",
    "content": "module Status.Types exposing (ClusterPeer, ClusterStatus, VersionInfo)\n\n\ntype alias StatusResponse =\n    { config : String\n    , uptime : String\n    , versionInfo : VersionInfo\n    , clusterStatus : Maybe ClusterStatus\n    }\n\n\ntype alias VersionInfo =\n    { branch : String\n    , buildDate : String\n    , buildUser : String\n    , goVersion : String\n    , revision : String\n    , version : String\n    }\n\n\ntype alias ClusterStatus =\n    { name : String\n    , status : String\n    , peers : List ClusterPeer\n    }\n\n\ntype alias ClusterPeer =\n    { name : String\n    , address : String\n    }\n"
  },
  {
    "path": "ui/app/src/Types.elm",
    "content": "module Types exposing (Model, Msg(..), Route(..))\n\nimport Browser.Navigation exposing (Key)\nimport Utils.Filter exposing (Filter, SilenceFormGetParams)\nimport Utils.Types exposing (ApiData)\nimport Views.AlertList.Types as AlertList exposing (AlertListMsg)\nimport Views.Settings.Types as SettingsView exposing (SettingsMsg)\nimport Views.SilenceForm.Types as SilenceForm exposing (SilenceFormMsg)\nimport Views.SilenceList.Types as SilenceList exposing (SilenceListMsg)\nimport Views.SilenceView.Types as SilenceView exposing (SilenceViewMsg)\nimport Views.Status.Types exposing (StatusModel, StatusMsg)\n\n\ntype alias Model =\n    { silenceList : SilenceList.Model\n    , silenceView : SilenceView.Model\n    , silenceForm : SilenceForm.Model\n    , alertList : AlertList.Model\n    , route : Route\n    , filter : Filter\n    , status : StatusModel\n    , basePath : String\n    , apiUrl : String\n    , libUrl : String\n    , bootstrapCSS : ApiData String\n    , fontAwesomeCSS : ApiData String\n    , elmDatepickerCSS : ApiData String\n    , defaultCreator : String\n    , expandAll : Bool\n    , key : Key\n    , settings : SettingsView.Model\n    }\n\n\ntype Msg\n    = MsgForAlertList AlertListMsg\n    | MsgForSilenceView SilenceViewMsg\n    | MsgForSilenceForm SilenceFormMsg\n    | MsgForSilenceList SilenceListMsg\n    | MsgForStatus StatusMsg\n    | MsgForSettings SettingsMsg\n    | NavigateToAlerts Filter\n    | NavigateToNotFound\n    | NavigateToSilenceView String\n    | NavigateToSilenceFormEdit String\n    | NavigateToSilenceFormNew SilenceFormGetParams\n    | NavigateToSilenceList Filter\n    | NavigateToStatus\n    | NavigateToSettings\n    | NavigateToInternalUrl String\n    | NavigateToExternalUrl String\n    | RedirectAlerts\n    | BootstrapCSSLoaded (ApiData String)\n    | FontAwesomeCSSLoaded (ApiData String)\n    | ElmDatepickerCSSLoaded (ApiData String)\n    | SetDefaultCreator String\n    | SetGroupExpandAll Bool\n\n\ntype Route\n    = AlertsRoute Filter\n    | NotFoundRoute\n    | SilenceFormEditRoute String\n    | SilenceFormNewRoute SilenceFormGetParams\n    | SilenceListRoute Filter\n    | SilenceViewRoute String\n    | StatusRoute\n    | TopLevelRoute\n    | SettingsRoute\n"
  },
  {
    "path": "ui/app/src/Updates.elm",
    "content": "module Updates exposing (update)\n\nimport Browser.Navigation as Navigation\nimport Task\nimport Types exposing (Model, Msg(..), Route(..))\nimport Views.AlertList.Types exposing (AlertListMsg(..))\nimport Views.AlertList.Updates\nimport Views.Settings.Updates\nimport Views.SilenceForm.Types exposing (SilenceFormMsg(..))\nimport Views.SilenceForm.Updates\nimport Views.SilenceList.Types exposing (SilenceListMsg(..))\nimport Views.SilenceList.Updates\nimport Views.SilenceView.Types as SilenceViewTypes\nimport Views.SilenceView.Updates\nimport Views.Status.Types exposing (StatusMsg(..))\nimport Views.Status.Updates\n\n\nupdate : Msg -> Model -> ( Model, Cmd Msg )\nupdate msg ({ basePath, apiUrl } as model) =\n    case msg of\n        NavigateToAlerts filter ->\n            let\n                ( alertList, cmd ) =\n                    Views.AlertList.Updates.update FetchAlerts model.alertList filter apiUrl basePath\n            in\n            ( { model | alertList = alertList, route = AlertsRoute filter, filter = filter }, cmd )\n\n        NavigateToSilenceList filter ->\n            let\n                ( silenceList, cmd ) =\n                    Views.SilenceList.Updates.update FetchSilences model.silenceList filter basePath apiUrl\n            in\n            ( { model | silenceList = silenceList, route = SilenceListRoute filter, filter = filter }\n            , Cmd.map MsgForSilenceList cmd\n            )\n\n        NavigateToStatus ->\n            ( { model | route = StatusRoute }, Task.perform identity (Task.succeed <| MsgForStatus <| InitStatusView apiUrl) )\n\n        NavigateToSilenceView silenceId ->\n            let\n                ( silenceView, cmd ) =\n                    Views.SilenceView.Updates.update (SilenceViewTypes.InitSilenceView silenceId) model.silenceView apiUrl\n            in\n            ( { model | route = SilenceViewRoute silenceId, silenceView = silenceView }\n            , Cmd.map MsgForSilenceView cmd\n            )\n\n        NavigateToSilenceFormNew params ->\n            ( { model | route = SilenceFormNewRoute params }\n            , Task.perform (NewSilenceFromMatchersAndComment model.defaultCreator >> MsgForSilenceForm) (Task.succeed params)\n            )\n\n        NavigateToSilenceFormEdit uuid ->\n            ( { model | route = SilenceFormEditRoute uuid }, Task.perform identity (Task.succeed <| (FetchSilence uuid |> MsgForSilenceForm)) )\n\n        NavigateToNotFound ->\n            ( { model | route = NotFoundRoute }, Cmd.none )\n\n        NavigateToInternalUrl url ->\n            ( model, Navigation.pushUrl model.key url )\n\n        NavigateToExternalUrl url ->\n            ( model, Navigation.load url )\n\n        RedirectAlerts ->\n            ( model, Navigation.pushUrl model.key (basePath ++ \"#/alerts\") )\n\n        NavigateToSettings ->\n            ( { model | route = SettingsRoute }, Cmd.none )\n\n        MsgForStatus subMsg ->\n            Views.Status.Updates.update subMsg model\n\n        MsgForAlertList subMsg ->\n            let\n                ( alertList, cmd ) =\n                    Views.AlertList.Updates.update subMsg model.alertList model.filter apiUrl basePath\n            in\n            ( { model | alertList = alertList }, cmd )\n\n        MsgForSilenceList subMsg ->\n            let\n                ( silenceList, cmd ) =\n                    Views.SilenceList.Updates.update subMsg model.silenceList model.filter basePath apiUrl\n            in\n            ( { model | silenceList = silenceList }, Cmd.map MsgForSilenceList cmd )\n\n        MsgForSettings subMsg ->\n            let\n                ( settingsView, cmd ) =\n                    Views.Settings.Updates.update subMsg model.settings\n            in\n            ( { model | settings = settingsView }, cmd )\n\n        MsgForSilenceView subMsg ->\n            let\n                ( silenceView, cmd ) =\n                    Views.SilenceView.Updates.update subMsg model.silenceView apiUrl\n            in\n            ( { model | silenceView = silenceView }, Cmd.map MsgForSilenceView cmd )\n\n        MsgForSilenceForm subMsg ->\n            let\n                ( silenceForm, cmd ) =\n                    Views.SilenceForm.Updates.update subMsg model.silenceForm basePath apiUrl\n            in\n            ( { model | silenceForm = silenceForm }, cmd )\n\n        BootstrapCSSLoaded css ->\n            ( { model | bootstrapCSS = css }, Cmd.none )\n\n        FontAwesomeCSSLoaded css ->\n            ( { model | fontAwesomeCSS = css }, Cmd.none )\n\n        ElmDatepickerCSSLoaded css ->\n            ( { model | elmDatepickerCSS = css }, Cmd.none )\n\n        SetDefaultCreator name ->\n            ( { model | defaultCreator = name }, Cmd.none )\n\n        SetGroupExpandAll expanded ->\n            ( { model | expandAll = expanded }, Cmd.none )\n"
  },
  {
    "path": "ui/app/src/Utils/Api.elm",
    "content": "module Utils.Api exposing (delete, get, makeApiUrl, map, post, send)\n\nimport Http exposing (Error(..))\nimport Json.Decode as Json exposing (field)\nimport Utils.Types exposing (ApiData(..))\n\n\nmap : (a -> b) -> ApiData a -> ApiData b\nmap fn response =\n    case response of\n        Success value ->\n            Success (fn value)\n\n        Initial ->\n            Initial\n\n        Loading ->\n            Loading\n\n        Failure a ->\n            Failure a\n\n\nparseError : String -> Maybe String\nparseError =\n    Json.decodeString (field \"error\" Json.string) >> Result.toMaybe\n\n\nerrorToString : Http.Error -> String\nerrorToString err =\n    case err of\n        Timeout ->\n            \"Timeout exceeded\"\n\n        NetworkError ->\n            \"Network error\"\n\n        BadStatus resp ->\n            parseError resp.body\n                |> Maybe.withDefault (String.fromInt resp.status.code ++ \" \" ++ resp.status.message)\n\n        BadPayload err_ _ ->\n            -- OK status, unexpected payload\n            \"Unexpected response from api: \" ++ err_\n\n        BadUrl url ->\n            \"Malformed url: \" ++ url\n\n\nfromResult : Result Http.Error a -> ApiData a\nfromResult result =\n    case result of\n        Err e ->\n            Failure (errorToString e)\n\n        Ok x ->\n            Success x\n\n\nsend : Http.Request a -> Cmd (ApiData a)\nsend =\n    Http.send fromResult\n\n\nget : String -> Json.Decoder a -> Http.Request a\nget url decoder =\n    request \"GET\" [] url Http.emptyBody decoder\n\n\npost : String -> Http.Body -> Json.Decoder a -> Http.Request a\npost url body decoder =\n    request \"POST\" [] url body decoder\n\n\ndelete : String -> Json.Decoder a -> Http.Request a\ndelete url decoder =\n    request \"DELETE\" [] url Http.emptyBody decoder\n\n\nrequest : String -> List Http.Header -> String -> Http.Body -> Json.Decoder a -> Http.Request a\nrequest method headers url body decoder =\n    Http.request\n        { method = method\n        , headers = headers\n        , url = url\n        , body = body\n        , expect = Http.expectJson decoder\n        , timeout = Nothing\n        , withCredentials = False\n        }\n\n\nmakeApiUrl : String -> String\nmakeApiUrl externalUrl =\n    let\n        url =\n            if String.endsWith \"/\" externalUrl then\n                String.dropRight 1 externalUrl\n\n            else\n                externalUrl\n    in\n    url ++ \"/api/v2\"\n"
  },
  {
    "path": "ui/app/src/Utils/Date.elm",
    "content": "module Utils.Date exposing\n    ( addDuration\n    , dateTimeFormat\n    , durationFormat\n    , parseDuration\n    , timeDifference\n    , timeFromString\n    , timeToString\n    )\n\nimport Iso8601\nimport Parser exposing ((|.), (|=), Parser)\nimport Time exposing (Posix)\nimport Tuple\n\n\nparseDuration : String -> Result String Float\nparseDuration =\n    Parser.run durationParser >> Result.mapError (always \"Wrong duration format\")\n\n\ndurationParser : Parser Float\ndurationParser =\n    Parser.succeed identity\n        |= Parser.loop 0 durationHelp\n        |. Parser.spaces\n        |. Parser.end\n\n\ndurationHelp : Float -> Parser (Parser.Step Float Float)\ndurationHelp duration =\n    Parser.oneOf\n        [ Parser.succeed (\\d -> Parser.Loop (d + duration))\n            |= term\n            |. Parser.spaces\n        , Parser.succeed (Parser.Done duration)\n        ]\n\n\nunits : List ( String, number )\nunits =\n    [ ( \"w\", 604800000 )\n    , ( \"d\", 86400000 )\n    , ( \"h\", 3600000 )\n    , ( \"m\", 60000 )\n    , ( \"s\", 1000 )\n    ]\n\n\ntimeToString : Posix -> String\ntimeToString =\n    Iso8601.fromTime\n\n\nterm : Parser Float\nterm =\n    Parser.succeed (*)\n        |= Parser.float\n        |= (units\n                |> List.map (\\( unit, ms ) -> Parser.succeed ms |. Parser.symbol unit)\n                |> Parser.oneOf\n           )\n\n\naddDuration : Float -> Posix -> Posix\naddDuration duration time =\n    Time.millisToPosix <|\n        (Time.posixToMillis time + round duration)\n\n\ntimeDifference : Posix -> Posix -> Float\ntimeDifference startsAt endsAt =\n    toFloat <|\n        (Time.posixToMillis endsAt - Time.posixToMillis startsAt)\n\n\ndurationFormat : Float -> Maybe String\ndurationFormat duration =\n    if duration >= 0 then\n        List.foldl\n            (\\( unit, ms ) ( result, curr ) ->\n                ( if curr // ms == 0 then\n                    result\n\n                  else\n                    result ++ String.fromInt (curr // ms) ++ unit ++ \" \"\n                , modBy ms curr\n                )\n            )\n            ( \"\", round duration )\n            units\n            |> Tuple.first\n            |> String.trim\n            |> Just\n\n    else\n        Nothing\n\n\ndateTimeFormat : Posix -> String\ndateTimeFormat =\n    Iso8601.fromTime\n\n\ntimeFromString : String -> Result String Posix\ntimeFromString string =\n    if string == \"\" then\n        Err \"Should not be empty\"\n\n    else\n        Iso8601.toTime string\n            |> Result.mapError (always \"Wrong ISO8601 format\")\n"
  },
  {
    "path": "ui/app/src/Utils/DateTimePicker/Types.elm",
    "content": "module Utils.DateTimePicker.Types exposing\n    ( DateTimePicker\n    , InputHourOrMinute(..)\n    , Msg(..)\n    , StartOrEnd(..)\n    , initDateTimePicker\n    , initFromStartAndEndTime\n    )\n\nimport Time exposing (Posix)\nimport Utils.DateTimePicker.Utils exposing (FirstDayOfWeek, floorMinute)\n\n\ntype alias DateTimePicker =\n    { month : Maybe Posix\n    , mouseOverDay : Maybe Posix\n    , startDate : Maybe Posix\n    , endDate : Maybe Posix\n    , startTime : Maybe Posix\n    , endTime : Maybe Posix\n    , firstDayOfWeek : FirstDayOfWeek\n    }\n\n\ntype Msg\n    = NextMonth\n    | PrevMonth\n    | MouseOverDay Posix\n    | OnClickDay\n    | ClearMouseOverDay\n    | SetInputTime StartOrEnd InputHourOrMinute Int\n    | IncrementTime StartOrEnd InputHourOrMinute Int\n\n\ntype StartOrEnd\n    = Start\n    | End\n\n\ntype InputHourOrMinute\n    = InputHour\n    | InputMinute\n\n\ninitDateTimePicker : FirstDayOfWeek -> DateTimePicker\ninitDateTimePicker firstDayOfWeek =\n    { month = Nothing\n    , mouseOverDay = Nothing\n    , startDate = Nothing\n    , endDate = Nothing\n    , startTime = Nothing\n    , endTime = Nothing\n    , firstDayOfWeek = firstDayOfWeek\n    }\n\n\ninitFromStartAndEndTime : Maybe Posix -> Maybe Posix -> FirstDayOfWeek -> DateTimePicker\ninitFromStartAndEndTime start end firstDayOfWeek =\n    let\n        startTime =\n            Maybe.map (\\s -> floorMinute s) start\n\n        endTime =\n            Maybe.map (\\e -> floorMinute e) end\n    in\n    { month = start\n    , mouseOverDay = Nothing\n    , startDate = start\n    , endDate = end\n    , startTime = startTime\n    , endTime = endTime\n    , firstDayOfWeek = firstDayOfWeek\n    }\n"
  },
  {
    "path": "ui/app/src/Utils/DateTimePicker/Updates.elm",
    "content": "module Utils.DateTimePicker.Updates exposing (update)\n\nimport Time exposing (Posix)\nimport Utils.DateTimePicker.Types\n    exposing\n        ( DateTimePicker\n        , InputHourOrMinute(..)\n        , Msg(..)\n        , StartOrEnd(..)\n        )\nimport Utils.DateTimePicker.Utils\n    exposing\n        ( addHour\n        , addMinute\n        , firstDayOfNextMonth\n        , firstDayOfPrevMonth\n        , floorDate\n        , trimTime\n        , updateHour\n        , updateMinute\n        )\n\n\nupdate : Msg -> DateTimePicker -> DateTimePicker\nupdate msg dateTimePicker =\n    let\n        justMonth =\n            dateTimePicker.month\n                |> Maybe.withDefault (Time.millisToPosix 0)\n\n        setTime_ : StartOrEnd -> InputHourOrMinute -> (InputHourOrMinute -> Posix -> Posix) -> ( Maybe Posix, Maybe Posix )\n        setTime_ soe ihom updateTime =\n            let\n                set_ : Maybe Posix -> Maybe Posix\n                set_ a =\n                    Maybe.map (\\b -> updateTime ihom b) a\n            in\n            case soe of\n                Start ->\n                    ( set_ dateTimePicker.startTime, dateTimePicker.endTime )\n\n                End ->\n                    ( dateTimePicker.startTime, set_ dateTimePicker.endTime )\n    in\n    case msg of\n        NextMonth ->\n            { dateTimePicker | month = Just (firstDayOfNextMonth justMonth) }\n\n        PrevMonth ->\n            { dateTimePicker | month = Just (firstDayOfPrevMonth justMonth) }\n\n        MouseOverDay time ->\n            { dateTimePicker | mouseOverDay = Just time }\n\n        ClearMouseOverDay ->\n            { dateTimePicker | mouseOverDay = Nothing }\n\n        OnClickDay ->\n            let\n                addDateTime_ : Posix -> Maybe Posix -> Posix\n                addDateTime_ date maybeTime =\n                    case maybeTime of\n                        Just time ->\n                            floorDate date\n                                |> Time.posixToMillis\n                                |> (\\d ->\n                                        trimTime time\n                                            |> Time.posixToMillis\n                                            |> (\\t -> d + t)\n                                   )\n                                |> Time.millisToPosix\n\n                        Nothing ->\n                            floorDate date\n\n                updateTime_ : Maybe Posix -> Maybe Posix -> Maybe Posix\n                updateTime_ maybeDate maybeTime =\n                    case maybeDate of\n                        Just date ->\n                            Just <| addDateTime_ date maybeTime\n\n                        Nothing ->\n                            maybeTime\n\n                ( startDate, endDate ) =\n                    case dateTimePicker.mouseOverDay of\n                        Just m ->\n                            case ( dateTimePicker.startDate, dateTimePicker.endDate ) of\n                                ( Nothing, Nothing ) ->\n                                    ( Just m\n                                    , Nothing\n                                    )\n\n                                ( Just start, Nothing ) ->\n                                    case\n                                        compare (floorDate m |> Time.posixToMillis)\n                                            (floorDate start |> Time.posixToMillis)\n                                    of\n                                        LT ->\n                                            ( Just m\n                                            , Just start\n                                            )\n\n                                        _ ->\n                                            ( Just start\n                                            , Just m\n                                            )\n\n                                ( Nothing, Just end ) ->\n                                    ( Just m\n                                    , Just end\n                                    )\n\n                                ( Just _, Just _ ) ->\n                                    ( Just m\n                                    , Nothing\n                                    )\n\n                        _ ->\n                            ( dateTimePicker.startDate\n                            , dateTimePicker.endDate\n                            )\n            in\n            { dateTimePicker\n                | startDate = startDate\n                , endDate = endDate\n                , startTime = updateTime_ startDate dateTimePicker.startTime\n                , endTime = updateTime_ endDate dateTimePicker.endTime\n            }\n\n        SetInputTime startOrEnd inputHourOrMinute num ->\n            let\n                limit_ : Int -> Int -> Int\n                limit_ limit n =\n                    if n < 0 then\n                        0\n\n                    else\n                        modBy limit n\n\n                updateHourOrMinute_ : InputHourOrMinute -> Posix -> Posix\n                updateHourOrMinute_ ihom s =\n                    case ihom of\n                        InputHour ->\n                            updateHour (limit_ 24 num) s\n\n                        InputMinute ->\n                            updateMinute (limit_ 60 num) s\n\n                ( startTime, endTime ) =\n                    setTime_ startOrEnd inputHourOrMinute updateHourOrMinute_\n            in\n            { dateTimePicker | startTime = startTime, endTime = endTime }\n\n        IncrementTime startOrEnd inputHourOrMinute num ->\n            let\n                updateHourOrMinute_ : InputHourOrMinute -> Posix -> Posix\n                updateHourOrMinute_ ihom s =\n                    let\n                        compare_ : Posix -> Posix\n                        compare_ a =\n                            if\n                                (floorDate s |> Time.posixToMillis)\n                                    == (floorDate a |> Time.posixToMillis)\n                            then\n                                a\n\n                            else\n                                s\n                    in\n                    case ihom of\n                        InputHour ->\n                            addHour num s\n                                |> compare_\n\n                        InputMinute ->\n                            addMinute num s\n                                |> compare_\n\n                ( startTime, endTime ) =\n                    setTime_ startOrEnd inputHourOrMinute updateHourOrMinute_\n            in\n            { dateTimePicker | startTime = startTime, endTime = endTime }\n"
  },
  {
    "path": "ui/app/src/Utils/DateTimePicker/Utils.elm",
    "content": "module Utils.DateTimePicker.Utils exposing\n    ( FirstDayOfWeek(..)\n    , addHour\n    , addMinute\n    , firstDayOfNextMonth\n    , firstDayOfPrevMonth\n    , floorDate\n    , floorMinute\n    , floorMonth\n    , listDaysOfMonth\n    , monthToString\n    , splitWeek\n    , targetValueIntParse\n    , trimTime\n    , updateHour\n    , updateMinute\n    )\n\nimport Html.Events exposing (targetValue)\nimport Json.Decode as Decode\nimport Time exposing (Month(..), Posix, Weekday(..), utc)\nimport Time.Extra as Time exposing (Interval(..))\n\n\ntype FirstDayOfWeek\n    = Monday\n    | Sunday\n    | Saturday\n\n\nlistDaysOfMonth : Posix -> FirstDayOfWeek -> List Posix\nlistDaysOfMonth time firstDayOfWeek =\n    let\n        firstOfMonth =\n            Time.floor Time.Month utc time\n\n        firstOfNextMonth =\n            firstDayOfNextMonth time\n\n        padFront =\n            weekToInt (Time.toWeekday utc firstOfMonth)\n                |> (\\wd ->\n                        case firstDayOfWeek of\n                            Sunday ->\n                                if wd == 7 then\n                                    0\n\n                                else\n                                    wd\n\n                            Monday ->\n                                if wd == 1 then\n                                    0\n\n                                else\n                                    wd - 1\n\n                            Saturday ->\n                                if wd == 6 then\n                                    0\n\n                                else if wd == 7 then\n                                    1\n\n                                else\n                                    wd + 1\n                   )\n                |> (\\w -> Time.add Time.Day -w utc firstOfMonth)\n                |> (\\d -> Time.range Time.Day 1 utc d firstOfMonth)\n\n        padBack =\n            weekToInt (Time.toWeekday utc firstOfNextMonth)\n                |> (\\wd ->\n                        case firstDayOfWeek of\n                            Sunday ->\n                                wd\n\n                            Monday ->\n                                if wd == 1 then\n                                    7\n\n                                else\n                                    wd - 1\n\n                            Saturday ->\n                                if wd == 6 then\n                                    7\n\n                                else if wd == 7 then\n                                    1\n\n                                else\n                                    wd + 1\n                   )\n                |> (\\w -> Time.add Time.Day (7 - w) utc firstOfNextMonth)\n                |> Time.range Time.Day 1 utc firstOfNextMonth\n    in\n    Time.range Time.Day 1 utc firstOfMonth firstOfNextMonth\n        |> (\\m -> padFront ++ m ++ padBack)\n\n\nfirstDayOfNextMonth : Posix -> Posix\nfirstDayOfNextMonth time =\n    Time.floor Time.Month utc time\n        |> Time.add Time.Day 1 utc\n        |> Time.ceiling Time.Month utc\n\n\nfirstDayOfPrevMonth : Posix -> Posix\nfirstDayOfPrevMonth time =\n    Time.floor Time.Month utc time\n        |> Time.add Time.Day -1 utc\n        |> Time.floor Time.Month utc\n\n\nsplitWeek : List Posix -> List (List Posix) -> List (List Posix)\nsplitWeek days weeks =\n    if List.length days < 7 then\n        weeks\n\n    else\n        List.append weeks [ List.take 7 days ]\n            |> splitWeek (List.drop 7 days)\n\n\nfloorDate : Posix -> Posix\nfloorDate time =\n    Time.floor Time.Day utc time\n\n\nfloorMonth : Posix -> Posix\nfloorMonth time =\n    Time.floor Time.Month utc time\n\n\nfloorMinute : Posix -> Posix\nfloorMinute time =\n    Time.floor Time.Minute utc time\n\n\ntrimTime : Posix -> Posix\ntrimTime time =\n    Time.floor Time.Day utc time\n        |> Time.posixToMillis\n        |> (\\d ->\n                Time.posixToMillis time - d\n           )\n        |> Time.millisToPosix\n\n\nupdateHour : Int -> Posix -> Posix\nupdateHour n time =\n    let\n        diff =\n            n - Time.toHour utc time\n    in\n    Time.add Hour diff utc time\n\n\nupdateMinute : Int -> Posix -> Posix\nupdateMinute n time =\n    let\n        diff =\n            n - Time.toMinute utc time\n    in\n    Time.add Minute diff utc time\n\n\naddHour : Int -> Posix -> Posix\naddHour n time =\n    Time.add Hour n utc time\n\n\naddMinute : Int -> Posix -> Posix\naddMinute n time =\n    Time.add Minute n utc time\n\n\nweekToInt : Weekday -> Int\nweekToInt weekday =\n    case weekday of\n        Mon ->\n            1\n\n        Tue ->\n            2\n\n        Wed ->\n            3\n\n        Thu ->\n            4\n\n        Fri ->\n            5\n\n        Sat ->\n            6\n\n        Sun ->\n            7\n\n\nmonthToString : Month -> String\nmonthToString month =\n    case month of\n        Jan ->\n            \"January\"\n\n        Feb ->\n            \"February\"\n\n        Mar ->\n            \"March\"\n\n        Apr ->\n            \"April\"\n\n        May ->\n            \"May\"\n\n        Jun ->\n            \"June\"\n\n        Jul ->\n            \"July\"\n\n        Aug ->\n            \"August\"\n\n        Sep ->\n            \"September\"\n\n        Oct ->\n            \"October\"\n\n        Nov ->\n            \"November\"\n\n        Dec ->\n            \"December\"\n\n\ntargetValueIntParse : Decode.Decoder Int\ntargetValueIntParse =\n    customDecoder targetValue (String.toInt >> maybeStringToResult)\n\n\nmaybeStringToResult : Maybe a -> Result String a\nmaybeStringToResult =\n    Result.fromMaybe \"could not convert string\"\n\n\ncustomDecoder : Decode.Decoder a -> (a -> Result String b) -> Decode.Decoder b\ncustomDecoder d f =\n    let\n        resultDecoder x =\n            case x of\n                Ok a ->\n                    Decode.succeed a\n\n                Err e ->\n                    Decode.fail e\n    in\n    Decode.map f d |> Decode.andThen resultDecoder\n"
  },
  {
    "path": "ui/app/src/Utils/DateTimePicker/Views.elm",
    "content": "module Utils.DateTimePicker.Views exposing (viewDateTimePicker)\n\nimport Html exposing (Html, br, button, div, i, input, p, strong, text)\nimport Html.Attributes exposing (class, maxlength, value)\nimport Html.Events exposing (on, onClick, onMouseOut, onMouseOver)\nimport Iso8601\nimport Json.Decode as Decode\nimport Time exposing (Posix, utc)\nimport Utils.DateTimePicker.Types exposing (DateTimePicker, InputHourOrMinute(..), Msg(..), StartOrEnd(..))\nimport Utils.DateTimePicker.Utils\n    exposing\n        ( FirstDayOfWeek(..)\n        , floorDate\n        , floorMonth\n        , listDaysOfMonth\n        , monthToString\n        , splitWeek\n        , targetValueIntParse\n        )\n\n\nviewDateTimePicker : DateTimePicker -> Html Msg\nviewDateTimePicker dateTimePicker =\n    div [ class \"w-100 container\" ]\n        [ viewCalendar dateTimePicker\n        , div [ class \"pt-4 row justify-content-center\" ]\n            [ viewTimePicker dateTimePicker Start\n            , viewTimePicker dateTimePicker End\n            ]\n        ]\n\n\nviewCalendar : DateTimePicker -> Html Msg\nviewCalendar dateTimePicker =\n    let\n        justViewTime =\n            dateTimePicker.month\n                |> Maybe.withDefault (Time.millisToPosix 0)\n    in\n    div [ class \"calendar_ month\" ]\n        [ viewMonthHeader justViewTime\n        , viewMonth dateTimePicker justViewTime\n        ]\n\n\nviewMonthHeader : Posix -> Html Msg\nviewMonthHeader justViewTime =\n    div [ class \"row month-header\" ]\n        [ div\n            [ class \"prev-month d-flex-center\"\n            , onClick PrevMonth\n            ]\n            [ p\n                [ class \"arrow\" ]\n                [ i\n                    [ class \"fa fa-angle-left fa-3x cursor-pointer\" ]\n                    []\n                ]\n            ]\n        , div\n            [ class \"month-text d-flex-center\" ]\n            [ text (Time.toYear utc justViewTime |> String.fromInt)\n            , br [] []\n            , text (Time.toMonth utc justViewTime |> monthToString)\n            ]\n        , div\n            [ class \"next-month d-flex-center\"\n            , onClick NextMonth\n            ]\n            [ p\n                [ class \"arrow\" ]\n                [ i\n                    [ class \"fa fa-angle-right fa-3x cursor-pointer\" ]\n                    []\n                ]\n            ]\n        ]\n\n\nviewMonth : DateTimePicker -> Posix -> Html Msg\nviewMonth dateTimePicker justViewTime =\n    let\n        days =\n            listDaysOfMonth justViewTime dateTimePicker.firstDayOfWeek\n\n        weeks =\n            splitWeek days []\n    in\n    div [ class \"row justify-content-center\" ]\n        [ div [ class \"weekheader\" ]\n            (case dateTimePicker.firstDayOfWeek of\n                Sunday ->\n                    List.map viewWeekHeader [ \"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\" ]\n\n                Monday ->\n                    List.map viewWeekHeader [ \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\" ]\n\n                Saturday ->\n                    List.map viewWeekHeader [ \"Sat\", \"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\" ]\n            )\n        , div\n            [ class \"date-container\"\n            , onMouseOut ClearMouseOverDay\n            ]\n            (List.map (viewWeek dateTimePicker justViewTime) weeks)\n        ]\n\n\nviewWeekHeader : String -> Html Msg\nviewWeekHeader weekday =\n    div [ class \"date text-muted\" ]\n        [ text weekday ]\n\n\nviewWeek : DateTimePicker -> Posix -> List Posix -> Html Msg\nviewWeek dateTimePicker justViewTime days =\n    div []\n        [ div [] (List.map (viewDay dateTimePicker justViewTime) days) ]\n\n\nviewDay : DateTimePicker -> Posix -> Posix -> Html Msg\nviewDay dateTimePicker justViewTime day =\n    let\n        compareDate_ : Posix -> Posix -> Order\n        compareDate_ a b =\n            compare (floorDate a |> Time.posixToMillis)\n                (floorDate b |> Time.posixToMillis)\n\n        setClass_ : Maybe Posix -> String -> String\n        setClass_ d s =\n            case d of\n                Just m ->\n                    case compareDate_ m day of\n                        EQ ->\n                            s\n\n                        _ ->\n                            \"\"\n\n                Nothing ->\n                    \"\"\n\n        thisMonthClass =\n            if floorMonth justViewTime == floorMonth day then\n                \" thismonth\"\n\n            else\n                \"\"\n\n        mouseoverClass =\n            setClass_ dateTimePicker.mouseOverDay \" mouseover\"\n\n        startClass =\n            setClass_ dateTimePicker.startDate \" start\"\n\n        endClass =\n            setClass_ dateTimePicker.endDate \" end\"\n\n        ( startClassBack, endClassBack ) =\n            Maybe.map2 (\\_ _ -> ( startClass, endClass )) dateTimePicker.startDate dateTimePicker.endDate\n                |> Maybe.withDefault ( \"\", \"\" )\n\n        betweenClass =\n            case ( dateTimePicker.startDate, dateTimePicker.endDate ) of\n                ( Just start, Just end ) ->\n                    case ( compareDate_ start day, compareDate_ end day ) of\n                        ( LT, GT ) ->\n                            \" between\"\n\n                        _ ->\n                            \"\"\n\n                _ ->\n                    \"\"\n    in\n    div [ class (\"date back\" ++ startClassBack ++ endClassBack ++ betweenClass) ]\n        [ div\n            [ class (\"date front\" ++ mouseoverClass ++ startClass ++ endClass ++ thisMonthClass)\n            , onMouseOver <| MouseOverDay day\n            , onClick OnClickDay\n            ]\n            [ text (Time.toDay utc day |> String.fromInt) ]\n        ]\n\n\nviewTimePicker : DateTimePicker -> StartOrEnd -> Html Msg\nviewTimePicker dateTimePicker startOrEnd =\n    div\n        [ class \"row timepicker\" ]\n        [ strong [ class \"subject\" ]\n            [ text\n                (case startOrEnd of\n                    Start ->\n                        \"Start\"\n\n                    End ->\n                        \"End\"\n                )\n            ]\n        , div [ class \"hour\" ]\n            [ button\n                [ class \"up-button d-flex-center\"\n                , onClick <| IncrementTime startOrEnd InputHour 1\n                ]\n                [ i\n                    [ class \"fa fa-angle-up\" ]\n                    []\n                ]\n            , input\n                [ on \"blur\" <| Decode.map (SetInputTime startOrEnd InputHour) targetValueIntParse\n                , value\n                    (case startOrEnd of\n                        Start ->\n                            case dateTimePicker.startTime of\n                                Just t ->\n                                    Time.toHour utc t |> String.fromInt\n\n                                Nothing ->\n                                    \"0\"\n\n                        End ->\n                            case dateTimePicker.endTime of\n                                Just t ->\n                                    Time.toHour utc t |> String.fromInt\n\n                                Nothing ->\n                                    \"0\"\n                    )\n                , maxlength 2\n                , class \"view d-flex-center\"\n                ]\n                []\n            , button\n                [ class \"down-button d-flex-center\"\n                , onClick <| IncrementTime startOrEnd InputHour -1\n                ]\n                [ i\n                    [ class \"fa fa-angle-down\" ]\n                    []\n                ]\n            ]\n        , div [ class \"colon d-flex-center\" ] [ text \":\" ]\n        , div [ class \"minute\" ]\n            [ button\n                [ class \"up-button d-flex-center\"\n                , onClick <| IncrementTime startOrEnd InputMinute 1\n                ]\n                [ i\n                    [ class \"fa fa-angle-up\" ]\n                    []\n                ]\n            , input\n                [ on \"blur\" <| Decode.map (SetInputTime startOrEnd InputMinute) targetValueIntParse\n                , value\n                    (case startOrEnd of\n                        Start ->\n                            case dateTimePicker.startTime of\n                                Just t ->\n                                    Time.toMinute utc t |> String.fromInt\n\n                                Nothing ->\n                                    \"0\"\n\n                        End ->\n                            case dateTimePicker.endTime of\n                                Just t ->\n                                    Time.toMinute utc t |> String.fromInt\n\n                                Nothing ->\n                                    \"0\"\n                    )\n                , maxlength 2\n                , class \"view\"\n                ]\n                []\n            , button\n                [ class \"down-button d-flex-center\"\n                , onClick <| IncrementTime startOrEnd InputMinute -1\n                ]\n                [ i\n                    [ class \"fa fa-angle-down\" ]\n                    []\n                ]\n            ]\n        , div [ class \"timeview d-flex-center\" ]\n            [ text\n                (let\n                    toString_ : Maybe Posix -> Maybe Posix -> String\n                    toString_ maybeTime maybeDate =\n                        Maybe.map\n                            (\\t ->\n                                case maybeDate of\n                                    Just _ ->\n                                        Iso8601.fromTime t\n                                            |> String.dropRight 8\n\n                                    Nothing ->\n                                        \"\"\n                            )\n                            maybeTime\n                            |> Maybe.withDefault \"\"\n\n                    selectedTime =\n                        case startOrEnd of\n                            Start ->\n                                toString_ dateTimePicker.startTime dateTimePicker.startDate\n\n                            End ->\n                                toString_ dateTimePicker.endTime dateTimePicker.endDate\n                 in\n                 selectedTime\n                )\n            ]\n        ]\n"
  },
  {
    "path": "ui/app/src/Utils/Filter.elm",
    "content": "module Utils.Filter exposing\n    ( Filter\n    , MatchOperator(..)\n    , Matcher\n    , SilenceFormGetParams\n    , convertFilterMatcher\n    , emptySilenceFormGetParams\n    , fromApiMatcher\n    , generateAPIQueryString\n    , nullFilter\n    , parseFilter\n    , parseGroup\n    , parseMatcher\n    , silencePreviewFilter\n    , stringifyFilter\n    , stringifyGroup\n    , stringifyMatcher\n    , toApiMatcher\n    , toUrl\n    , withMatchers\n    )\n\nimport Char\nimport Data.Matcher\nimport Json.Decode as Decode\nimport Json.Encode as Encode\nimport Parser exposing ((|.), (|=), Parser, Trailing(..))\nimport Set\nimport Url exposing (percentEncode)\n\n\ntype alias Filter =\n    { text : Maybe String\n    , group : Maybe String\n    , customGrouping : Bool\n    , receiver : Maybe String\n    , showSilenced : Maybe Bool\n    , showInhibited : Maybe Bool\n    , showMuted : Maybe Bool\n    , showActive : Maybe Bool\n    }\n\n\nnullFilter : Filter\nnullFilter =\n    { text = Nothing\n    , group = Nothing\n    , customGrouping = False\n    , receiver = Nothing\n    , showSilenced = Nothing\n    , showInhibited = Nothing\n    , showMuted = Nothing\n    , showActive = Nothing\n    }\n\n\ngenerateQueryParam : String -> Maybe String -> Maybe String\ngenerateQueryParam name =\n    Maybe.map (percentEncode >> (++) (name ++ \"=\"))\n\n\ntoUrl : String -> Filter -> String\ntoUrl baseUrl { receiver, customGrouping, showSilenced, showInhibited, showMuted, showActive, text, group } =\n    let\n        parts =\n            [ ( \"silenced\", Maybe.withDefault False showSilenced |> boolToString |> Just )\n            , ( \"inhibited\", Maybe.withDefault False showInhibited |> boolToString |> Just )\n            , ( \"muted\", Maybe.withDefault False showMuted |> boolToString |> Just )\n            , ( \"active\", Maybe.withDefault True showActive |> boolToString |> Just )\n            , ( \"filter\", emptyToNothing text )\n            , ( \"receiver\", emptyToNothing receiver )\n            , ( \"group\", group )\n            , ( \"customGrouping\", boolToMaybeString customGrouping )\n            ]\n                |> List.filterMap (\\( a, b ) -> generateQueryParam a b)\n    in\n    if List.length parts > 0 then\n        baseUrl\n            ++ (parts\n                    |> String.join \"&\"\n                    |> (++) \"?\"\n               )\n\n    else\n        baseUrl\n\n\ngenerateAPIQueryString : Filter -> String\ngenerateAPIQueryString { receiver, showSilenced, showInhibited, showMuted, showActive, text, group } =\n    let\n        filter_ =\n            case parseFilter (Maybe.withDefault \"\" text) of\n                Just matchers_ ->\n                    List.map (stringifyMatcher >> Just >> Tuple.pair \"filter\") matchers_\n\n                Nothing ->\n                    []\n\n        parts =\n            filter_\n                ++ [ ( \"silenced\", Maybe.withDefault False showSilenced |> boolToString |> Just )\n                   , ( \"inhibited\", Maybe.withDefault False showInhibited |> boolToString |> Just )\n                   , ( \"muted\", Maybe.withDefault False showMuted |> boolToString |> Just )\n                   , ( \"active\", Maybe.withDefault True showActive |> boolToString |> Just )\n                   , ( \"receiver\", emptyToNothing receiver )\n                   , ( \"group\", group )\n                   ]\n                |> List.filterMap (\\( a, b ) -> generateQueryParam a b)\n    in\n    if List.length parts > 0 then\n        parts\n            |> String.join \"&\"\n            |> (++) \"?\"\n\n    else\n        \"\"\n\n\nboolToMaybeString : Bool -> Maybe String\nboolToMaybeString b =\n    if b then\n        Just \"true\"\n\n    else\n        Nothing\n\n\nboolToString : Bool -> String\nboolToString b =\n    if b then\n        \"true\"\n\n    else\n        \"false\"\n\n\nemptyToNothing : Maybe String -> Maybe String\nemptyToNothing str =\n    case str of\n        Just \"\" ->\n            Nothing\n\n        _ ->\n            str\n\n\ntype alias Matcher =\n    { key : String\n    , op : MatchOperator\n    , value : String\n    }\n\n\ntoApiMatcher : Matcher -> Data.Matcher.Matcher\ntoApiMatcher { key, op, value } =\n    let\n        ( isRegex, isEqual ) =\n            case op of\n                Eq ->\n                    ( False, True )\n\n                NotEq ->\n                    ( False, False )\n\n                RegexMatch ->\n                    ( True, True )\n\n                NotRegexMatch ->\n                    ( True, False )\n    in\n    { name = key\n    , isRegex = isRegex\n    , isEqual = Just isEqual\n    , value = value\n    }\n\n\nfromApiMatcher : Data.Matcher.Matcher -> Matcher\nfromApiMatcher { name, value, isRegex, isEqual } =\n    let\n        isEqualValue =\n            case isEqual of\n                Nothing ->\n                    True\n\n                Just justIsEqual ->\n                    justIsEqual\n\n        op =\n            if not isRegex && isEqualValue then\n                Eq\n\n            else if not isRegex && not isEqualValue then\n                NotEq\n\n            else if isRegex && isEqualValue then\n                RegexMatch\n\n            else\n                NotRegexMatch\n    in\n    { key = name\n    , value = value\n    , op = op\n    }\n\n\ntype MatchOperator\n    = Eq\n    | NotEq\n    | RegexMatch\n    | NotRegexMatch\n\n\nmatchers : List ( String, MatchOperator )\nmatchers =\n    [ ( \"=~\", RegexMatch )\n    , ( \"!~\", NotRegexMatch )\n    , ( \"=\", Eq )\n    , ( \"!=\", NotEq )\n    ]\n\n\nparseFilter : String -> Maybe (List Matcher)\nparseFilter =\n    Parser.run filter\n        >> Result.toMaybe\n\n\nparseMatcher : String -> Maybe Matcher\nparseMatcher =\n    Parser.run matcher\n        >> Result.toMaybe\n\n\nstringifyGroup : List String -> Maybe String\nstringifyGroup list =\n    if List.isEmpty list then\n        Just \"\"\n\n    else if list == [ \"alertname\" ] then\n        Nothing\n\n    else\n        Just (String.join \",\" list)\n\n\nparseGroup : Maybe String -> List String\nparseGroup maybeGroup =\n    case maybeGroup of\n        Nothing ->\n            [ \"alertname\" ]\n\n        Just something ->\n            String.split \",\" something\n                |> List.filter (String.length >> (<) 0)\n\n\nstringifyFilter : List Matcher -> String\nstringifyFilter matchers_ =\n    case matchers_ of\n        [] ->\n            \"\"\n\n        list ->\n            (list\n                |> List.map stringifyMatcher\n                |> String.join \", \"\n                |> (++) \"{\"\n            )\n                ++ \"}\"\n\n\nstringifyMatcher : Matcher -> String\nstringifyMatcher { key, op, value } =\n    key\n        ++ (matchers\n                |> List.filter (Tuple.second >> (==) op)\n                |> List.head\n                |> Maybe.map Tuple.first\n                |> Maybe.withDefault \"\"\n           )\n        ++ Encode.encode 0 (Encode.string value)\n\n\nconvertFilterMatcher : Matcher -> Data.Matcher.Matcher\nconvertFilterMatcher { key, op, value } =\n    { name = key\n    , value = value\n    , isRegex = (op == RegexMatch) || (op == NotRegexMatch)\n    , isEqual = Just ((op == Eq) || (op == RegexMatch))\n    }\n\n\nfilter : Parser (List Matcher)\nfilter =\n    Parser.succeed identity\n        |= Parser.sequence\n            { start = \"{\"\n            , separator = \",\"\n            , end = \"}\"\n            , spaces = Parser.spaces\n            , item = item\n            , trailing = Forbidden\n            }\n        |. Parser.end\n\n\nmatcher : Parser Matcher\nmatcher =\n    Parser.succeed identity\n        |. Parser.spaces\n        |= item\n        |. Parser.spaces\n        |. Parser.end\n\n\nitem : Parser Matcher\nitem =\n    Parser.succeed Matcher\n        |= Parser.variable\n            { start = isVarChar\n            , inner = isVarChar\n            , reserved = Set.empty\n            }\n        |= (matchers\n                |> List.map\n                    (\\( keyword, matcher_ ) ->\n                        Parser.succeed matcher_\n                            |. Parser.keyword keyword\n                    )\n                |> Parser.oneOf\n           )\n        |= string '\"'\n\n\nstring : Char -> Parser String\nstring separator =\n    Parser.succeed ()\n        |. Parser.token (String.fromChar separator)\n        |. Parser.loop separator stringHelp\n        |> Parser.getChompedString\n        |> Parser.andThen\n            (\\str ->\n                case Decode.decodeString Decode.string str of\n                    Ok value ->\n                        Parser.succeed value\n\n                    Err _ ->\n                        Parser.problem \"Invalid string\"\n            )\n\n\nstringHelp : Char -> Parser (Parser.Step Char ())\nstringHelp separator =\n    Parser.oneOf\n        [ Parser.succeed (Parser.Done ())\n            |. Parser.token (String.fromChar separator)\n        , Parser.succeed (Parser.Loop separator)\n            |. Parser.chompIf (\\char -> char == '\\\\')\n            |. Parser.chompIf (\\_ -> True)\n        , Parser.succeed (Parser.Loop separator)\n            |. Parser.chompIf (\\char -> char /= '\\\\' && char /= separator)\n        ]\n\n\nisVarChar : Char -> Bool\nisVarChar char =\n    Char.isLower char\n        || Char.isUpper char\n        || (char == '_')\n        || Char.isDigit char\n\n\nwithMatchers : List Matcher -> Filter -> Filter\nwithMatchers matchers_ filter_ =\n    { filter_ | text = Just (stringifyFilter matchers_) }\n\n\nsilencePreviewFilter : List Data.Matcher.Matcher -> Filter\nsilencePreviewFilter apiMatchers =\n    { nullFilter\n        | text =\n            List.map fromApiMatcher apiMatchers\n                |> stringifyFilter\n                |> Just\n        , showSilenced = Just True\n        , showInhibited = Just True\n        , showMuted = Just True\n        , showActive = Just True\n    }\n\n\ntype alias SilenceFormGetParams =\n    { matchers : List Matcher\n    , comment : String\n    }\n\n\nemptySilenceFormGetParams : SilenceFormGetParams\nemptySilenceFormGetParams =\n    { matchers = []\n    , comment = \"\"\n    }\n"
  },
  {
    "path": "ui/app/src/Utils/FormValidation.elm",
    "content": "module Utils.FormValidation exposing\n    ( ValidatedField\n    , ValidationState(..)\n    , initialField\n    , stringNotEmpty\n    , updateValue\n    , validate\n    )\n\n\ntype ValidationState\n    = Initial\n    | Valid\n    | Invalid String\n\n\nfromResult : Result String a -> ValidationState\nfromResult result =\n    case result of\n        Ok _ ->\n            Valid\n\n        Err str ->\n            Invalid str\n\n\ntype alias ValidatedField =\n    { value : String\n    , validationState : ValidationState\n    }\n\n\ninitialField : String -> ValidatedField\ninitialField value =\n    { value = value\n    , validationState = Initial\n    }\n\n\nupdateValue : String -> ValidatedField -> ValidatedField\nupdateValue value field =\n    { field | value = value, validationState = Initial }\n\n\nvalidate : (String -> Result String a) -> ValidatedField -> ValidatedField\nvalidate validator field =\n    { field | validationState = fromResult (validator field.value) }\n\n\nstringNotEmpty : String -> Result String String\nstringNotEmpty string =\n    if String.isEmpty (String.trim string) then\n        Err \"Should not be empty\"\n\n    else\n        Ok string\n"
  },
  {
    "path": "ui/app/src/Utils/Keyboard.elm",
    "content": "module Utils.Keyboard exposing (keys, onKeyDown, onKeyUp)\n\nimport Html exposing (Attribute)\nimport Html.Events exposing (keyCode, on)\nimport Json.Decode as Json\n\n\nkeys :\n    { backspace : Int\n    , enter : Int\n    , up : Int\n    , down : Int\n    }\nkeys =\n    { backspace = 8\n    , enter = 13\n    , up = 38\n    , down = 40\n    }\n\n\nonKeyDown : (Int -> msg) -> Attribute msg\nonKeyDown tagger =\n    on \"keydown\" (Json.map tagger keyCode)\n\n\nonKeyUp : (Int -> msg) -> Attribute msg\nonKeyUp tagger =\n    on \"keyup\" (Json.map tagger keyCode)\n"
  },
  {
    "path": "ui/app/src/Utils/List.elm",
    "content": "module Utils.List exposing (groupBy, lastElem, mstring, nextElem, zip)\n\nimport Data.Matcher exposing (Matcher)\nimport Dict exposing (Dict)\nimport Json.Encode as Encode\n\n\nnextElem : a -> List a -> Maybe a\nnextElem el list =\n    case list of\n        curr :: rest ->\n            if curr == el then\n                List.head rest\n\n            else\n                nextElem el rest\n\n        [] ->\n            Nothing\n\n\nlastElem : List a -> Maybe a\nlastElem =\n    List.foldl (Just >> always) Nothing\n\n\nmstring : Matcher -> String\nmstring m =\n    let\n        isEqual =\n            case m.isEqual of\n                Nothing ->\n                    True\n\n                Just value ->\n                    value\n\n        sep =\n            if not m.isRegex && isEqual then\n                \"=\"\n\n            else if not m.isRegex && not isEqual then\n                \"!=\"\n\n            else if m.isRegex && isEqual then\n                \"=~\"\n\n            else\n                \"!~\"\n    in\n    String.join sep [ m.name, Encode.encode 0 (Encode.string m.value) ]\n\n\n{-| Takes a key-fn and a list.\nCreates a `Dict` which maps the key to a list of matching elements.\nmary = {id=1, name=\"Mary\"}\njack = {id=2, name=\"Jack\"}\njill = {id=1, name=\"Jill\"}\ngroupBy .id [mary, jack, jill] == Dict.fromList [(1, [mary, jill]), (2, [jack])]\n\nCopied from <https://github.com/elm-community/dict-extra/blob/2.0.0/src/Dict/Extra.elm>\n\n-}\ngroupBy : (a -> comparable) -> List a -> Dict comparable (List a)\ngroupBy keyfn list =\n    List.foldr\n        (\\x acc ->\n            Dict.update (keyfn x) (Maybe.map ((::) x) >> Maybe.withDefault [ x ] >> Just) acc\n        )\n        Dict.empty\n        list\n\n\nzip : List a -> List b -> List ( a, b )\nzip a b =\n    List.map2 (\\a1 b1 -> ( a1, b1 )) a b\n"
  },
  {
    "path": "ui/app/src/Utils/Match.elm",
    "content": "module Utils.Match exposing (consecutiveChars, jaroWinkler)\n\nimport Char\nimport Utils.List exposing (zip)\n\n\n{-|\n\n    Adapted from https://blog.art-of-coding.eu/comparing-strings-with-metrics-in-haskell/\n\n-}\njaro : String -> String -> Float\njaro s1 s2 =\n    if s1 == s2 then\n        1.0\n\n    else\n        let\n            l1 =\n                String.length s1\n\n            l2 =\n                String.length s2\n\n            z2 =\n                zip (List.range 1 l2) (String.toList s2)\n                    |> List.map (Tuple.mapSecond Char.toCode)\n\n            searchLength =\n                -- A character must be within searchLength spaces of the\n                -- character we are matching against in order to be considered\n                -- a match.\n                -- (//) is integer division, which removes the need to floor\n                -- the result.\n                (max l1 l2 // 2) - 1\n\n            m =\n                zip (List.range 1 l1) (String.toList s1)\n                    |> List.map (Tuple.mapSecond Char.toCode)\n                    |> List.concatMap (charMatch searchLength z2)\n\n            ml =\n                List.length m\n\n            t =\n                m\n                    |> List.map (transposition z2 >> toFloat >> (*) 0.5)\n                    |> List.sum\n\n            ml1 =\n                toFloat ml / toFloat l1\n\n            ml2 =\n                toFloat ml / toFloat l2\n\n            mtm =\n                (toFloat ml - t) / toFloat ml\n        in\n        if ml == 0 then\n            0\n\n        else\n            (1 / 3) * (ml1 + ml2 + mtm)\n\n\nwinkler : String -> String -> Float -> Float\nwinkler s1 s2 jaro_ =\n    if s1 == \"\" || s2 == \"\" then\n        0.0\n\n    else if s1 == s2 then\n        1.0\n\n    else\n        let\n            l =\n                consecutiveChars s1 s2\n                    |> String.length\n                    |> toFloat\n\n            p =\n                0.25\n        in\n        jaro_ + ((l * p) * (1.0 - jaro_))\n\n\njaroWinkler : String -> String -> Float\njaroWinkler s1 s2 =\n    if s1 == \"\" || s2 == \"\" then\n        0.0\n\n    else if s1 == s2 then\n        1.0\n\n    else\n        jaro s1 s2\n            |> winkler s1 s2\n\n\nconsecutiveChars : String -> String -> String\nconsecutiveChars s1 s2 =\n    if s1 == \"\" || s2 == \"\" then\n        \"\"\n\n    else if s1 == s2 then\n        s1\n\n    else\n        cp (String.toList s1) (String.toList s2) []\n            |> String.fromList\n\n\ncp : List Char -> List Char -> List Char -> List Char\ncp l1 l2 acc =\n    case ( l1, l2 ) of\n        ( x :: xs, y :: ys ) ->\n            if x == y then\n                cp xs ys (acc ++ [ x ])\n\n            else if List.length acc > 0 then\n                -- If we have already found matches, we bail. We only want\n                -- consecutive matches.\n                acc\n\n            else\n                -- Go through every character in l1 until it matches the first\n                -- character in l2, and then start counting from there.\n                cp l1 ys acc\n\n        _ ->\n            acc\n\n\ncharMatch : Int -> List ( Int, Int ) -> ( Int, Int ) -> List ( Int, Int )\ncharMatch matchRange list ( p, q ) =\n    list\n        |> List.drop (p - matchRange - 1)\n        |> List.take (p + matchRange)\n        |> List.filter (Tuple.second >> (==) q)\n\n\ntransposition : List ( Int, Int ) -> ( Int, Int ) -> Int\ntransposition list ( p, q ) =\n    list\n        |> List.filter\n            (\\( x, y ) ->\n                p /= x && q == y\n            )\n        |> List.length\n"
  },
  {
    "path": "ui/app/src/Utils/String.elm",
    "content": "module Utils.String exposing (capitalizeFirst, linkify)\n\nimport Char\nimport String\n\n\ncapitalizeFirst : String -> String\ncapitalizeFirst string =\n    case String.uncons string of\n        Nothing ->\n            string\n\n        Just ( char, rest ) ->\n            String.cons (Char.toUpper char) rest\n\n\nlinkify : String -> List (Result String String)\nlinkify string =\n    List.reverse (linkifyHelp (String.words string) [])\n\n\nlinkifyHelp : List String -> List (Result String String) -> List (Result String String)\nlinkifyHelp words linkified =\n    case words of\n        [] ->\n            linkified\n\n        word :: restWords ->\n            if isUrl word then\n                case linkified of\n                    (Err lastWord) :: restLinkified ->\n                        -- append space to last word\n                        linkifyHelp restWords (Ok word :: Err (lastWord ++ \" \") :: restLinkified)\n\n                    (Ok _) :: _ ->\n                        -- insert space between two links\n                        linkifyHelp restWords (Ok word :: Err \" \" :: linkified)\n\n                    _ ->\n                        linkifyHelp restWords (Ok word :: linkified)\n\n            else\n                case linkified of\n                    (Err lastWord) :: restLinkified ->\n                        -- concatenate with last word\n                        linkifyHelp restWords (Err (lastWord ++ \" \" ++ word) :: restLinkified)\n\n                    (Ok _) :: _ ->\n                        -- insert space after the link\n                        linkifyHelp restWords (Err (\" \" ++ word) :: linkified)\n\n                    _ ->\n                        linkifyHelp restWords (Err word :: linkified)\n\n\nisUrl : String -> Bool\nisUrl =\n    (\\b a -> String.startsWith a b) >> (\\b a -> List.any a b) [ \"http://\", \"https://\" ]\n"
  },
  {
    "path": "ui/app/src/Utils/Types.elm",
    "content": "module Utils.Types exposing (ApiData(..), Label, Labels, Matcher)\n\n\ntype ApiData a\n    = Initial\n    | Loading\n    | Failure String\n    | Success a\n\n\ntype alias Matcher =\n    { isRegex : Bool\n    , isEqual : Maybe Bool\n    , name : String\n    , value : String\n    }\n\n\ntype alias Matchers =\n    List Matcher\n\n\ntype alias Labels =\n    List Label\n\n\ntype alias Label =\n    ( String, String )\n"
  },
  {
    "path": "ui/app/src/Utils/Views.elm",
    "content": "module Utils.Views exposing\n    ( apiData\n    , checkbox\n    , error\n    , labelButton\n    , linkifyText\n    , loading\n    , tab\n    , validatedField\n    , validatedTextareaField\n    )\n\nimport Html exposing (..)\nimport Html.Attributes exposing (..)\nimport Html.Events exposing (onBlur, onCheck, onClick, onInput)\nimport Utils.FormValidation exposing (ValidatedField, ValidationState(..))\nimport Utils.String\nimport Utils.Types as Types\n\n\ntab : tab -> tab -> (tab -> msg) -> List (Html msg) -> Html msg\ntab tab_ currentTab msg content =\n    li [ class \"nav-item\" ]\n        [ if tab_ == currentTab then\n            span [ class \"nav-link active\" ] content\n\n          else\n            button\n                [ style \"background\" \"transparent\"\n                , style \"font\" \"inherit\"\n                , style \"cursor\" \"pointer\"\n                , style \"outline\" \"none\"\n                , class \"nav-link\"\n                , onClick (msg tab_)\n                ]\n                content\n        ]\n\n\nlabelButton : Maybe msg -> String -> Html msg\nlabelButton maybeMsg labelText =\n    case maybeMsg of\n        Nothing ->\n            span\n                [ class \"btn btn-sm btn-light border mr-2 mb-2\"\n                , style \"user-select\" \"text\"\n                , style \"-moz-user-select\" \"text\"\n                , style \"-webkit-user-select\" \"text\"\n                ]\n                [ text labelText ]\n\n        Just msg ->\n            button\n                [ class \"btn btn-sm btn-light border mr-2 mb-2\"\n                , onClick msg\n                ]\n                [ span [ class \"text-muted\" ] [ text labelText ] ]\n\n\nlinkifyText : String -> List (Html msg)\nlinkifyText str =\n    List.map\n        (\\result ->\n            case result of\n                Ok link ->\n                    a [ href link, target \"_blank\" ] [ text link ]\n\n                Err txt ->\n                    text txt\n        )\n        (Utils.String.linkify str)\n\n\ncheckbox : String -> Bool -> (Bool -> msg) -> Html msg\ncheckbox name status msg =\n    label [ class \"f6 dib mb2 mr2 d-flex align-items-center\" ]\n        [ input [ type_ \"checkbox\", checked status, onCheck msg ] []\n        , span [ class \"pl-2\" ] [ text <| \" \" ++ name ]\n        ]\n\n\nvalidatedField : (List (Attribute msg) -> List (Html msg) -> Html msg) -> String -> String -> (String -> msg) -> msg -> ValidatedField -> Html msg\nvalidatedField htmlField labelText classes inputMsg blurMsg field =\n    case field.validationState of\n        Valid ->\n            div [ class <| \"d-flex flex-column form-group has-success \" ++ classes ]\n                [ label [] [ strong [] [ text labelText ] ]\n                , htmlField\n                    [ value field.value\n                    , onInput inputMsg\n                    , onBlur blurMsg\n                    , class \"form-control form-control-success\"\n                    ]\n                    []\n                ]\n\n        Initial ->\n            div [ class <| \"d-flex flex-column form-group \" ++ classes ]\n                [ label [] [ strong [] [ text labelText ] ]\n                , htmlField\n                    [ value field.value\n                    , onInput inputMsg\n                    , onBlur blurMsg\n                    , class \"form-control\"\n                    ]\n                    []\n                ]\n\n        Invalid error_ ->\n            div [ class <| \"d-flex flex-column form-group has-danger \" ++ classes ]\n                [ label [] [ strong [] [ text labelText ] ]\n                , htmlField\n                    [ value field.value\n                    , onInput inputMsg\n                    , onBlur blurMsg\n                    , class \"form-control form-control-danger\"\n                    ]\n                    []\n                , div [ class \"form-control-feedback\" ] [ text error_ ]\n                ]\n\n\nvalidatedTextareaField : String -> String -> (String -> msg) -> msg -> ValidatedField -> Html msg\nvalidatedTextareaField labelText classes inputMsg blurMsg field =\n    let\n        lineCount =\n            String.lines field.value\n                |> List.length\n                |> clamp 3 15\n    in\n    case field.validationState of\n        Valid ->\n            div [ class <| \"d-flex flex-column form-group has-success \" ++ classes ]\n                [ label [] [ strong [] [ text labelText ] ]\n                , textarea\n                    [ value field.value\n                    , onInput inputMsg\n                    , onBlur blurMsg\n                    , class \"form-control form-control-success\"\n                    , rows lineCount\n                    , disableGrammarly\n                    ]\n                    []\n                ]\n\n        Initial ->\n            div [ class <| \"d-flex flex-column form-group \" ++ classes ]\n                [ label [] [ strong [] [ text labelText ] ]\n                , textarea\n                    [ value field.value\n                    , onInput inputMsg\n                    , onBlur blurMsg\n                    , class \"form-control\"\n                    , rows lineCount\n                    , disableGrammarly\n                    ]\n                    []\n                ]\n\n        Invalid error_ ->\n            div [ class <| \"d-flex flex-column form-group has-danger \" ++ classes ]\n                [ label [] [ strong [] [ text labelText ] ]\n                , textarea\n                    [ value field.value\n                    , onInput inputMsg\n                    , onBlur blurMsg\n                    , class \"form-control form-control-danger\"\n                    , rows lineCount\n                    , disableGrammarly\n                    ]\n                    []\n                , div [ class \"form-control-feedback\" ] [ text error_ ]\n                ]\n\n\napiData : (a -> Html msg) -> Types.ApiData a -> Html msg\napiData onSuccess data =\n    case data of\n        Types.Success payload ->\n            onSuccess payload\n\n        Types.Loading ->\n            loading\n\n        Types.Initial ->\n            loading\n\n        Types.Failure msg ->\n            error msg\n\n\nloading : Html msg\nloading =\n    div []\n        [ span [] [ text \"Loading...\" ]\n        ]\n\n\nerror : String -> Html msg\nerror err =\n    div [ class \"alert alert-warning\" ]\n        [ text (Utils.String.capitalizeFirst err) ]\n\n\ndisableGrammarly : Html.Attribute msg\ndisableGrammarly =\n    attribute \"data-gramm_editor\" \"false\"\n"
  },
  {
    "path": "ui/app/src/Views/AlertList/AlertView.elm",
    "content": "module Views.AlertList.AlertView exposing (addLabelMsg, view)\n\nimport Data.GettableAlert exposing (GettableAlert)\nimport Dict\nimport Html exposing (..)\nimport Html.Attributes exposing (class, href, style, title, value)\nimport Html.Events exposing (onClick)\nimport Types exposing (Msg(..))\nimport Url exposing (percentEncode)\nimport Utils.Filter\nimport Views.AlertList.Types exposing (AlertListMsg(..))\nimport Views.FilterBar.Types as FilterBarTypes\nimport Views.Shared.Alert exposing (annotation, annotationsButton, generatorUrlButton, titleView)\nimport Views.SilenceForm.Parsing exposing (newSilenceFromAlertLabels)\n\n\nview : List ( String, String ) -> Maybe String -> GettableAlert -> Html Msg\nview labels maybeActiveId alert =\n    let\n        -- remove the grouping labels, and bring the alertname to front\n        ungroupedLabels =\n            alert.labels\n                |> Dict.toList\n                |> List.filter ((\\b a -> List.member a b) labels >> not)\n                |> List.partition (Tuple.first >> (==) \"alertname\")\n                |> (\\( a, b ) -> a ++ b)\n    in\n    li\n        [ -- speedup rendering in Chrome, because list-group-item className\n          -- creates a new layer in the rendering engine\n          style \"position\" \"static\"\n        , class \"align-items-start list-group-item border-0 p-0 mb-4\"\n        ]\n        [ div\n            [ class \"w-100 mb-2 d-flex align-items-start\" ]\n            [ titleView alert\n            , if Dict.size alert.annotations > 0 then\n                annotationsButton maybeActiveId alert\n                    |> Html.map (\\msg -> MsgForAlertList (SetActive msg))\n\n              else\n                text \"\"\n            , case alert.generatorURL of\n                Just url ->\n                    generatorUrlButton url\n\n                Nothing ->\n                    text \"\"\n            , silenceButton alert\n            , inhibitedIcon alert\n            , mutedIcon alert\n            , linkButton alert\n            ]\n        , if maybeActiveId == Just alert.fingerprint then\n            table [ class \"table w-100 mb-1\" ] (List.map annotation <| Dict.toList alert.annotations)\n\n          else\n            text \"\"\n        , div [] (List.map labelButton ungroupedLabels)\n        ]\n\n\nlabelButton : ( String, String ) -> Html Msg\nlabelButton ( key, val ) =\n    div\n        [ class \"btn-group mr-2 mb-2\" ]\n        [ span\n            [ class \"btn btn-sm border-right-0 text-muted\"\n\n            -- have to reset bootstrap button styles to make the text selectable\n            , style \"user-select\" \"initial\"\n\n            -- have to reset bootstrap button styles to make the text selectable\n            , style \"-moz-user-select\" \"initial\"\n\n            -- have to reset bootstrap button styles to make the text selectable\n            , style \"-webkit-user-select\" \"initial\"\n\n            -- have to reset bootstrap button styles to make the text selectable\n            , style \"border-color\" \"#ccc\"\n            ]\n            [ text (key ++ \"=\\\"\" ++ val ++ \"\\\"\") ]\n        , button\n            [ class \"btn btn-sm bg-light btn-outline-secondary\"\n            , onClick (addLabelMsg ( key, val ))\n            , title \"Filter by this label\"\n            ]\n            [ text \"+\" ]\n        ]\n\n\naddLabelMsg : ( String, String ) -> Msg\naddLabelMsg ( key, value ) =\n    FilterBarTypes.AddFilterMatcher False\n        { key = key\n        , op = Utils.Filter.Eq\n        , value = value\n        }\n        |> MsgForFilterBar\n        |> MsgForAlertList\n\n\nlinkButton : GettableAlert -> Html Msg\nlinkButton alert =\n    let\n        link =\n            alert.labels\n                |> Dict.toList\n                |> List.map (\\( k, v ) -> Utils.Filter.Matcher k Utils.Filter.Eq v)\n                |> Utils.Filter.stringifyFilter\n                |> percentEncode\n                |> (++) \"#/alerts?filter=\"\n    in\n    a\n        [ class \"btn btn-outline-info border-0\"\n        , href link\n        ]\n        [ i [ class \"fa fa-link mr-2\" ] []\n        , text \"Link\"\n        ]\n\n\nsilenceButton : GettableAlert -> Html Msg\nsilenceButton alert =\n    case List.head alert.status.silencedBy of\n        Just sId ->\n            a\n                [ class \"btn btn-outline-danger border-0\"\n                , href (\"#/silences/\" ++ sId)\n                ]\n                [ i [ class \"fa fa-bell-slash mr-2\" ] []\n                , text \"Silenced\"\n                ]\n\n        Nothing ->\n            a\n                [ class \"btn btn-outline-info border-0\"\n                , href (newSilenceFromAlertLabels alert.labels)\n                ]\n                [ i [ class \"fa fa-bell-slash-o mr-2\" ] []\n                , text \"Silence\"\n                ]\n\n\ninhibitedIcon : GettableAlert -> Html Msg\ninhibitedIcon alert =\n    case List.head alert.status.inhibitedBy of\n        Just _ ->\n            span\n                [ class \"btn btn-outline-danger border-0\"\n                ]\n                [ i [ class \"fa fa-eye-slash mr-2\" ] []\n                , text \"Inhibited\"\n                ]\n\n        Nothing ->\n            text \"\"\n\n\nmutedIcon : GettableAlert -> Html Msg\nmutedIcon alert =\n    case List.head alert.status.mutedBy of\n        Just _ ->\n            span\n                [ class \"btn btn-outline-danger border-0\"\n                ]\n                [ i [ class \"fa fa-bell-slash mr-2\" ] []\n                , text \"Muted\"\n                ]\n\n        Nothing ->\n            text \"\"\n"
  },
  {
    "path": "ui/app/src/Views/AlertList/Parsing.elm",
    "content": "module Views.AlertList.Parsing exposing (alertsParser)\n\nimport Url.Parser exposing ((<?>), Parser, map, s)\nimport Url.Parser.Query as Query\nimport Utils.Filter exposing (Filter, MatchOperator(..))\n\n\nboolParam : String -> Query.Parser Bool\nboolParam name =\n    Query.custom name (List.head >> (/=) Nothing)\n\n\nmaybeBoolParam : String -> Query.Parser (Maybe Bool)\nmaybeBoolParam name =\n    Query.custom name\n        (List.head >> Maybe.map (String.toLower >> (/=) \"false\"))\n\n\nalertsParser : Parser (Filter -> a) a\nalertsParser =\n    s \"alerts\"\n        <?> Query.string \"filter\"\n        <?> Query.string \"group\"\n        <?> boolParam \"customGrouping\"\n        <?> Query.string \"receiver\"\n        <?> maybeBoolParam \"silenced\"\n        <?> maybeBoolParam \"inhibited\"\n        <?> maybeBoolParam \"muted\"\n        <?> maybeBoolParam \"active\"\n        |> map Filter\n"
  },
  {
    "path": "ui/app/src/Views/AlertList/Types.elm",
    "content": "module Views.AlertList.Types exposing\n    ( AlertListMsg(..)\n    , Model\n    , Tab(..)\n    , initAlertList\n    )\n\nimport Browser.Navigation exposing (Key)\nimport Data.AlertGroup exposing (AlertGroup)\nimport Data.GettableAlert exposing (GettableAlert)\nimport Set exposing (Set)\nimport Utils.Types exposing (ApiData(..))\nimport Views.FilterBar.Types as FilterBar\nimport Views.GroupBar.Types as GroupBar\nimport Views.ReceiverBar.Types as ReceiverBar\n\n\ntype AlertListMsg\n    = AlertsFetched (ApiData (List GettableAlert))\n    | AlertGroupsFetched (ApiData (List AlertGroup))\n    | FetchAlerts\n    | MsgForReceiverBar ReceiverBar.Msg\n    | MsgForFilterBar FilterBar.Msg\n    | MsgForGroupBar GroupBar.Msg\n    | ToggleSilenced Bool\n    | ToggleInhibited Bool\n    | ToggleMuted Bool\n    | SetActive (Maybe String)\n    | ActiveGroups Int\n    | SetTab Tab\n    | ToggleExpandAll Bool\n\n\ntype Tab\n    = FilterTab\n    | GroupTab\n\n\ntype alias Model =\n    { alerts : ApiData (List GettableAlert)\n    , alertGroups : ApiData (List AlertGroup)\n    , receiverBar : ReceiverBar.Model\n    , groupBar : GroupBar.Model\n    , filterBar : FilterBar.Model\n    , tab : Tab\n    , activeId : Maybe String\n    , activeGroups : Set Int\n    , key : Key\n    , expandAll : Bool\n    }\n\n\ninitAlertList : Key -> Bool -> Model\ninitAlertList key expandAll =\n    { alerts = Initial\n    , alertGroups = Initial\n    , receiverBar = ReceiverBar.initReceiverBar key\n    , groupBar = GroupBar.initGroupBar key\n    , filterBar = FilterBar.initFilterBar []\n    , tab = FilterTab\n    , activeId = Nothing\n    , activeGroups = Set.empty\n    , key = key\n    , expandAll = expandAll\n    }\n"
  },
  {
    "path": "ui/app/src/Views/AlertList/Updates.elm",
    "content": "port module Views.AlertList.Updates exposing (update)\n\nimport Alerts.Api as Api\nimport Browser.Navigation as Navigation\nimport Data.AlertGroup exposing (AlertGroup)\nimport Dict\nimport Set\nimport Task\nimport Types exposing (Msg(..))\nimport Utils.Filter exposing (Filter)\nimport Utils.List\nimport Utils.Types exposing (ApiData(..))\nimport Views.AlertList.Types exposing (AlertListMsg(..), Model, Tab(..))\nimport Views.FilterBar.Updates as FilterBar\nimport Views.GroupBar.Updates as GroupBar\nimport Views.ReceiverBar.Updates as ReceiverBar\n\n\nupdate : AlertListMsg -> Model -> Filter -> String -> String -> ( Model, Cmd Types.Msg )\nupdate msg ({ groupBar, alerts, filterBar, receiverBar, alertGroups } as model) filter apiUrl basePath =\n    let\n        alertsUrl =\n            basePath ++ \"#/alerts\"\n\n        filteredUrl =\n            Utils.Filter.toUrl alertsUrl\n    in\n    case msg of\n        AlertGroupsFetched listOfAlertGroups ->\n            ( { model | alertGroups = listOfAlertGroups }\n            , Cmd.none\n            )\n\n        AlertsFetched listOfAlerts ->\n            let\n                ( groups_, groupBar_ ) =\n                    case listOfAlerts of\n                        Success ungroupedAlerts ->\n                            let\n                                groups =\n                                    ungroupedAlerts\n                                        |> Utils.List.groupBy\n                                            (.labels >> Dict.toList >> List.filter (\\( key, _ ) -> List.member key groupBar.fields))\n                                        |> Dict.toList\n                                        |> List.map\n                                            (\\( labels, alerts_ ) ->\n                                                AlertGroup (Dict.fromList labels) { name = \"unknown\" } alerts_\n                                            )\n\n                                newGroupBar =\n                                    { groupBar\n                                        | list =\n                                            List.concatMap (.labels >> Dict.toList) ungroupedAlerts\n                                                |> List.map Tuple.first\n                                                |> Set.fromList\n                                    }\n                            in\n                            ( Success groups, newGroupBar )\n\n                        Initial ->\n                            ( Initial, groupBar )\n\n                        Loading ->\n                            ( Loading, groupBar )\n\n                        Failure e ->\n                            ( Failure e, groupBar )\n            in\n            ( { model\n                | alerts = listOfAlerts\n                , alertGroups = groups_\n                , groupBar = groupBar_\n              }\n            , Cmd.none\n            )\n\n        FetchAlerts ->\n            let\n                newGroupBar =\n                    GroupBar.setFields filter groupBar\n\n                newFilterBar =\n                    FilterBar.setMatchers filter filterBar\n            in\n            ( { model\n                | alerts =\n                    if filter.customGrouping then\n                        Loading\n\n                    else\n                        alerts\n                , alertGroups =\n                    if filter.customGrouping then\n                        alertGroups\n\n                    else\n                        Loading\n                , filterBar = newFilterBar\n                , groupBar = newGroupBar\n                , activeId = Nothing\n                , activeGroups = Set.empty\n              }\n            , Cmd.batch\n                [ if filter.customGrouping then\n                    Api.fetchAlerts apiUrl filter |> Cmd.map (AlertsFetched >> MsgForAlertList)\n\n                  else\n                    Api.fetchAlertGroups apiUrl filter |> Cmd.map (AlertGroupsFetched >> MsgForAlertList)\n                , ReceiverBar.fetchReceivers apiUrl |> Cmd.map (MsgForReceiverBar >> MsgForAlertList)\n                ]\n            )\n\n        ToggleSilenced showSilenced ->\n            ( model\n            , Navigation.pushUrl model.key (filteredUrl { filter | showSilenced = Just showSilenced })\n            )\n\n        ToggleInhibited showInhibited ->\n            ( model\n            , Navigation.pushUrl model.key (filteredUrl { filter | showInhibited = Just showInhibited })\n            )\n\n        ToggleMuted showMuted ->\n            ( model\n            , Navigation.pushUrl model.key (filteredUrl { filter | showMuted = Just showMuted })\n            )\n\n        SetTab tab ->\n            ( { model | tab = tab }, Cmd.none )\n\n        MsgForFilterBar subMsg ->\n            let\n                ( newFilterBar, shouldFilter, cmd ) =\n                    FilterBar.update subMsg filterBar\n\n                filterBarCmd =\n                    Cmd.map (MsgForFilterBar >> MsgForAlertList) cmd\n\n                newUrl =\n                    filteredUrl (Utils.Filter.withMatchers newFilterBar.matchers filter)\n\n                alertsCmd =\n                    if shouldFilter then\n                        Cmd.batch\n                            [ Navigation.pushUrl model.key newUrl\n                            , filterBarCmd\n                            ]\n\n                    else\n                        filterBarCmd\n            in\n            ( { model | filterBar = newFilterBar, tab = FilterTab }, alertsCmd )\n\n        MsgForGroupBar subMsg ->\n            let\n                ( newGroupBar, cmd ) =\n                    GroupBar.update alertsUrl filter subMsg groupBar\n            in\n            ( { model | groupBar = newGroupBar }, Cmd.map (MsgForGroupBar >> MsgForAlertList) cmd )\n\n        MsgForReceiverBar subMsg ->\n            let\n                ( newReceiverBar, cmd ) =\n                    ReceiverBar.update alertsUrl filter subMsg receiverBar\n            in\n            ( { model | receiverBar = newReceiverBar }, Cmd.map (MsgForReceiverBar >> MsgForAlertList) cmd )\n\n        SetActive maybeId ->\n            ( { model | activeId = maybeId }, Cmd.none )\n\n        ActiveGroups activeGroup ->\n            let\n                activeGroups_ =\n                    if Set.member activeGroup model.activeGroups then\n                        Set.remove activeGroup model.activeGroups\n\n                    else\n                        Set.insert activeGroup model.activeGroups\n            in\n            ( { model | activeGroups = activeGroups_, expandAll = False }, persistGroupExpandAll False )\n\n        ToggleExpandAll expanded ->\n            let\n                allGroupLabels =\n                    case ( alertGroups, expanded ) of\n                        ( Success groups, True ) ->\n                            List.range 0 (List.length groups)\n                                |> Set.fromList\n\n                        _ ->\n                            Set.empty\n            in\n            ( { model\n                | expandAll = expanded\n                , activeGroups = allGroupLabels\n              }\n            , Cmd.batch\n                [ persistGroupExpandAll expanded\n                , Task.succeed expanded |> Task.perform SetGroupExpandAll\n                ]\n            )\n\n\nport persistGroupExpandAll : Bool -> Cmd msg\n"
  },
  {
    "path": "ui/app/src/Views/AlertList/Views.elm",
    "content": "module Views.AlertList.Views exposing (view)\n\nimport Data.AlertGroup exposing (AlertGroup)\nimport Data.GettableAlert exposing (GettableAlert)\nimport Data.Receiver exposing (Receiver)\nimport Dict\nimport Html exposing (..)\nimport Html.Attributes exposing (..)\nimport Html.Events exposing (..)\nimport Set exposing (Set)\nimport Types exposing (Msg(..))\nimport Utils.Filter exposing (Filter)\nimport Utils.Types exposing (ApiData(..), Labels)\nimport Utils.Views\nimport Views.AlertList.AlertView as AlertView\nimport Views.AlertList.Types exposing (AlertListMsg(..), Model, Tab(..))\nimport Views.FilterBar.Views as FilterBar\nimport Views.GroupBar.Views as GroupBar\nimport Views.ReceiverBar.Views as ReceiverBar\n\n\nrenderCheckbox : String -> Maybe Bool -> (Bool -> AlertListMsg) -> Html Msg\nrenderCheckbox textLabel maybeChecked toggleMsg =\n    li [ class \"nav-item\" ]\n        [ div [ class \"mt-1 ml-1 custom-control custom-checkbox\" ]\n            [ input\n                [ type_ \"checkbox\"\n                , id textLabel\n                , class \"custom-control-input\"\n                , checked (Maybe.withDefault False maybeChecked)\n                , onCheck (toggleMsg >> MsgForAlertList)\n                ]\n                []\n            , label [ class \"custom-control-label\", for textLabel ] [ text textLabel ]\n            ]\n        ]\n\n\ngroupTabName : Bool -> Html msg\ngroupTabName customGrouping =\n    if customGrouping then\n        text \"Group (custom)\"\n\n    else\n        text \"Group\"\n\n\nview : Model -> Filter -> Html Msg\nview { alertGroups, groupBar, filterBar, receiverBar, tab, activeId, activeGroups, expandAll } filter =\n    div []\n        [ div\n            [ class \"card mb-3\" ]\n            [ div [ class \"card-header\" ]\n                [ ul [ class \"nav nav-tabs card-header-tabs\" ]\n                    [ Utils.Views.tab FilterTab tab (SetTab >> MsgForAlertList) [ text \"Filter\" ]\n                    , Utils.Views.tab GroupTab tab (SetTab >> MsgForAlertList) [ groupTabName filter.customGrouping ]\n                    , receiverBar\n                        |> ReceiverBar.view filter.receiver\n                        |> Html.map (MsgForReceiverBar >> MsgForAlertList)\n                    , renderCheckbox \"Silenced\" filter.showSilenced ToggleSilenced\n                    , renderCheckbox \"Inhibited\" filter.showInhibited ToggleInhibited\n                    , renderCheckbox \"Muted\" filter.showMuted ToggleMuted\n                    ]\n                ]\n            , div [ class \"card-body\" ]\n                [ case tab of\n                    FilterTab ->\n                        Html.map (MsgForFilterBar >> MsgForAlertList) (FilterBar.view { showSilenceButton = True } filterBar)\n\n                    GroupTab ->\n                        Html.map (MsgForGroupBar >> MsgForAlertList) (GroupBar.view groupBar filter.customGrouping)\n                ]\n            ]\n        , div []\n            [ button\n                [ class \"btn btn-outline-secondary border-0 mr-1 mb-3\"\n                , onClick (MsgForAlertList (ToggleExpandAll (not expandAll)))\n                ]\n                (if expandAll then\n                    [ i [ class \"fa fa-minus mr-3\" ] [], text \"Collapse all groups\" ]\n\n                 else\n                    [ i [ class \"fa fa-plus mr-3\" ] [], text \"Expand all groups\" ]\n                )\n            ]\n        , Utils.Views.apiData (defaultAlertGroups activeId activeGroups expandAll) alertGroups\n        ]\n\n\ndefaultAlertGroups : Maybe String -> Set Int -> Bool -> List AlertGroup -> Html Msg\ndefaultAlertGroups activeId activeGroups expandAll groups =\n    case groups of\n        [] ->\n            Utils.Views.error \"No alert groups found\"\n\n        [ { labels, receiver, alerts } ] ->\n            let\n                labels_ =\n                    Dict.toList labels\n            in\n            alertGroup activeId (Set.singleton 0) receiver labels_ alerts 0 expandAll\n\n        _ ->\n            div [ class \"pl-5\" ]\n                (List.indexedMap\n                    (\\index group ->\n                        alertGroup activeId activeGroups group.receiver (Dict.toList group.labels) group.alerts index expandAll\n                    )\n                    groups\n                )\n\n\nalertGroup : Maybe String -> Set Int -> Receiver -> Labels -> List GettableAlert -> Int -> Bool -> Html Msg\nalertGroup activeId activeGroups receiver labels alerts groupId expandAll =\n    let\n        groupActive =\n            expandAll || Set.member groupId activeGroups\n\n        labels_ =\n            case labels of\n                [] ->\n                    [ span [ class \"btn btn-secondary mr-1 mb-1\" ] [ text \"Not grouped\" ] ]\n\n                _ ->\n                    List.map\n                        (\\( key, value ) ->\n                            div [ class \"btn-group mr-1 mb-1\" ]\n                                [ span\n                                    [ class \"btn text-muted\"\n                                    , style \"user-select\" \"initial\"\n                                    , style \"-moz-user-select\" \"initial\"\n                                    , style \"-webkit-user-select\" \"initial\"\n                                    , style \"border-color\" \"#5bc0de\"\n                                    ]\n                                    [ text (key ++ \"=\\\"\" ++ value ++ \"\\\"\") ]\n                                , button\n                                    [ class \"btn btn-outline-info\"\n                                    , onClick (AlertView.addLabelMsg ( key, value ))\n                                    , title \"Filter by this label\"\n                                    ]\n                                    [ text \"+\" ]\n                                ]\n                        )\n                        labels\n\n        expandButton =\n            expandAlertGroup groupActive groupId receiver\n                |> Html.map (\\msg -> MsgForAlertList (ActiveGroups msg))\n\n        alertCount =\n            List.length alerts\n\n        alertText =\n            if alertCount == 1 then\n                String.fromInt alertCount ++ \" alert\"\n\n            else\n                String.fromInt alertCount ++ \" alerts\"\n\n        alertEl =\n            [ span [ class \"ml-1 mb-0\", style \"white-space\" \"nowrap\" ] [ text alertText ] ]\n    in\n    div []\n        [ div [ class \"mb-3\" ] (expandButton :: labels_ ++ alertEl)\n        , if groupActive then\n            ul [ class \"list-group mb-0\" ] (List.map (AlertView.view labels activeId) alerts)\n\n          else\n            text \"\"\n        ]\n\n\nexpandAlertGroup : Bool -> Int -> Receiver -> Html Int\nexpandAlertGroup expanded groupId receiver =\n    let\n        icon =\n            if expanded then\n                \"fa-minus\"\n\n            else\n                \"fa-plus\"\n    in\n    button\n        [ onClick groupId\n        , class \"btn btn-outline-info border-0 mr-1 mb-1\"\n        , style \"margin-left\" \"-3rem\"\n        ]\n        [ i\n            [ class (\"fa \" ++ icon)\n            , class \"mr-2\"\n            ]\n            []\n        , text receiver.name\n        ]\n"
  },
  {
    "path": "ui/app/src/Views/FilterBar/Types.elm",
    "content": "module Views.FilterBar.Types exposing (Model, Msg(..), initFilterBar)\n\nimport Utils.Filter\n\n\ntype alias Model =\n    { matchers : List Utils.Filter.Matcher\n    , backspacePressed : Bool\n    , matcherText : String\n    }\n\n\ntype Msg\n    = AddFilterMatcher Bool Utils.Filter.Matcher\n    | DeleteFilterMatcher Bool Utils.Filter.Matcher\n    | PressingBackspace Bool\n    | UpdateMatcherText String\n    | Noop\n\n\n{-| A note about the `backspacePressed` attribute:\n\nHolding down the backspace removes (one by one) each last character in the input,\nand the whole time sends multiple keyDown events. This is a guard so that if a user\nholds down backspace to remove the text in the input, they won't accidentally hold\nbackspace too long and then delete the preceding matcher as well. So, once a user holds\nbackspace to clear an input, they have to then lift up the key and press it again to\nproceed to deleting the next matcher.\n\n-}\ninitFilterBar : List Utils.Filter.Matcher -> Model\ninitFilterBar matchers =\n    { matchers = matchers\n    , backspacePressed = False\n    , matcherText = \"\"\n    }\n"
  },
  {
    "path": "ui/app/src/Views/FilterBar/Updates.elm",
    "content": "module Views.FilterBar.Updates exposing (setMatchers, update)\n\nimport Browser.Dom as Dom\nimport Task\nimport Utils.Filter exposing (Filter, parseFilter)\nimport Views.FilterBar.Types exposing (Model, Msg(..))\n\n\n{-| Returns a triple where the Bool component notifies whether the matchers have changed.\n-}\nupdate : Msg -> Model -> ( Model, Bool, Cmd Msg )\nupdate msg model =\n    case msg of\n        AddFilterMatcher emptyMatcherText matcher ->\n            ( { model\n                | matchers =\n                    if List.member matcher model.matchers then\n                        model.matchers\n\n                    else\n                        model.matchers ++ [ matcher ]\n                , matcherText =\n                    if emptyMatcherText then\n                        \"\"\n\n                    else\n                        model.matcherText\n              }\n            , True\n            , Dom.focus \"filter-bar-matcher\"\n                |> Task.attempt (always Noop)\n            )\n\n        DeleteFilterMatcher setMatcherText matcher ->\n            ( { model\n                | matchers = List.filter ((/=) matcher) model.matchers\n                , matcherText =\n                    if setMatcherText then\n                        Utils.Filter.stringifyMatcher matcher\n\n                    else\n                        model.matcherText\n              }\n            , True\n            , Dom.focus \"filter-bar-matcher\"\n                |> Task.attempt (always Noop)\n            )\n\n        UpdateMatcherText value ->\n            ( { model | matcherText = value }, False, Cmd.none )\n\n        PressingBackspace isPressed ->\n            ( { model | backspacePressed = isPressed }, False, Cmd.none )\n\n        Noop ->\n            ( model, False, Cmd.none )\n\n\nsetMatchers : Filter -> Model -> Model\nsetMatchers filter model =\n    { model\n        | matchers =\n            filter.text\n                |> Maybe.andThen parseFilter\n                |> Maybe.withDefault []\n    }\n"
  },
  {
    "path": "ui/app/src/Views/FilterBar/Views.elm",
    "content": "module Views.FilterBar.Views exposing (view)\n\nimport Html exposing (Html, a, button, div, i, input, small, text)\nimport Html.Attributes exposing (class, disabled, href, id, spellcheck, style, value)\nimport Html.Events exposing (onClick, onInput)\nimport Utils.Filter exposing (Matcher, convertFilterMatcher)\nimport Utils.Keyboard exposing (onKeyDown, onKeyUp)\nimport Utils.List\nimport Views.FilterBar.Types exposing (Model, Msg(..))\nimport Views.SilenceForm.Parsing exposing (newSilenceFromMatchers)\n\n\nkeys :\n    { backspace : Int\n    , enter : Int\n    }\nkeys =\n    { backspace = 8\n    , enter = 13\n    }\n\n\nviewMatcher : Matcher -> Html Msg\nviewMatcher matcher =\n    div [ class \"col col-auto\" ]\n        [ div [ class \"btn-group mr-2 mb-2\" ]\n            [ button\n                [ class \"btn btn-outline-info\"\n                , onClick (DeleteFilterMatcher True matcher)\n                ]\n                [ text <| Utils.Filter.stringifyMatcher matcher\n                ]\n            , button\n                [ class \"btn btn-outline-danger\"\n                , onClick (DeleteFilterMatcher False matcher)\n                ]\n                [ text \"×\" ]\n            ]\n        ]\n\n\nviewMatchers : List Matcher -> List (Html Msg)\nviewMatchers matchers =\n    matchers\n        |> List.map viewMatcher\n\n\nview : { showSilenceButton : Bool } -> Model -> Html Msg\nview { showSilenceButton } { matchers, matcherText, backspacePressed } =\n    let\n        maybeMatcher =\n            Utils.Filter.parseMatcher matcherText\n\n        maybeLastMatcher =\n            Utils.List.lastElem matchers\n\n        className =\n            if matcherText == \"\" then\n                \"\"\n\n            else\n                case maybeMatcher of\n                    Just _ ->\n                        \"has-success\"\n\n                    Nothing ->\n                        \"has-danger\"\n\n        keyDown key =\n            if key == keys.enter then\n                maybeMatcher\n                    |> Maybe.map (AddFilterMatcher True)\n                    |> Maybe.withDefault Noop\n\n            else if key == keys.backspace then\n                if matcherText == \"\" then\n                    case ( backspacePressed, maybeLastMatcher ) of\n                        ( False, Just lastMatcher ) ->\n                            DeleteFilterMatcher True lastMatcher\n\n                        _ ->\n                            Noop\n\n                else\n                    PressingBackspace True\n\n            else\n                Noop\n\n        keyUp key =\n            if key == keys.backspace then\n                PressingBackspace False\n\n            else\n                Noop\n\n        onClickAttr =\n            maybeMatcher\n                |> Maybe.map (AddFilterMatcher True)\n                |> Maybe.withDefault Noop\n                |> onClick\n\n        isDisabled =\n            maybeMatcher == Nothing\n\n        dataMatchers =\n            matchers\n                |> List.map convertFilterMatcher\n    in\n    div\n        [ class \"row no-gutters align-items-start\" ]\n        (viewMatchers matchers\n            ++ [ div\n                    [ class (\"col \" ++ className)\n                    , style \"min-width\"\n                        (if showSilenceButton then\n                            \"300px\"\n\n                         else\n                            \"200px\"\n                        )\n                    ]\n                    [ div [ class \"row no-gutters align-content-stretch\" ]\n                        [ div [ class \"col input-group\" ]\n                            [ input\n                                [ id \"filter-bar-matcher\"\n                                , class \"form-control\"\n\n                                -- Setting spellcheck=false on an input element will disable smartquotes in iOS.\n                                , spellcheck False\n                                , value matcherText\n                                , onKeyDown keyDown\n                                , onKeyUp keyUp\n                                , onInput UpdateMatcherText\n                                ]\n                                []\n                            , div\n                                [ class \"input-group-append\" ]\n                                [ button [ class \"btn btn-primary\", disabled isDisabled, onClickAttr ] [ text \"+\" ] ]\n                            ]\n                        , if showSilenceButton then\n                            div [ class \"col col-auto ml-2\" ]\n                                [ div [ class \"input-group\" ]\n                                    [ a\n                                        [ class \"btn btn-outline-info\"\n                                        , href (newSilenceFromMatchers dataMatchers)\n                                        ]\n                                        [ i [ class \"fa fa-bell-slash-o mr-2\" ] []\n                                        , text \"Silence\"\n                                        ]\n                                    ]\n                                ]\n\n                          else\n                            text \"\"\n                        ]\n                    , small [ class \"form-text text-muted\" ]\n                        [ text \"Custom matcher, e.g.\"\n                        , button\n                            [ class \"btn btn-link btn-sm align-baseline\"\n                            , onClick (UpdateMatcherText exampleMatcher)\n                            ]\n                            [ text exampleMatcher ]\n                        ]\n                    ]\n               ]\n        )\n\n\nexampleMatcher : String\nexampleMatcher =\n    \"env=\\\"production\\\"\"\n"
  },
  {
    "path": "ui/app/src/Views/GroupBar/Types.elm",
    "content": "module Views.GroupBar.Types exposing (Model, Msg(..), initGroupBar)\n\nimport Browser.Navigation exposing (Key)\nimport Set exposing (Set)\n\n\ntype alias Model =\n    { list : Set String\n    , fieldText : String\n    , fields : List String\n    , matches : List String\n    , backspacePressed : Bool\n    , focused : Bool\n    , resultsHovered : Bool\n    , maybeSelectedMatch : Maybe String\n    , key : Key\n    }\n\n\ntype Msg\n    = AddField Bool String\n    | DeleteField Bool String\n    | Select (Maybe String)\n    | PressingBackspace Bool\n    | Focus Bool\n    | ResultsHovered Bool\n    | UpdateFieldText String\n    | CustomGrouping Bool\n    | Noop\n\n\ninitGroupBar : Key -> Model\ninitGroupBar key =\n    { list = Set.empty\n    , fieldText = \"\"\n    , fields = []\n    , matches = []\n    , focused = False\n    , resultsHovered = False\n    , backspacePressed = False\n    , maybeSelectedMatch = Nothing\n    , key = key\n    }\n"
  },
  {
    "path": "ui/app/src/Views/GroupBar/Updates.elm",
    "content": "module Views.GroupBar.Updates exposing (setFields, update)\n\nimport Browser.Dom as Dom\nimport Browser.Navigation as Navigation\nimport Set\nimport Task\nimport Utils.Filter exposing (Filter, parseGroup, stringifyGroup)\nimport Utils.Match exposing (jaroWinkler)\nimport Views.GroupBar.Types exposing (Model, Msg(..))\n\n\nupdate : String -> Filter -> Msg -> Model -> ( Model, Cmd Msg )\nupdate url filter msg model =\n    case msg of\n        CustomGrouping customGrouping ->\n            ( model\n            , Cmd.batch\n                [ Navigation.pushUrl model.key (Utils.Filter.toUrl url { filter | customGrouping = customGrouping })\n                , Dom.focus \"group-by-field\" |> Task.attempt (always Noop)\n                ]\n            )\n\n        AddField emptyFieldText text ->\n            immediatelyFilter url\n                filter\n                { model\n                    | fields = model.fields ++ [ text ]\n                    , matches = []\n                    , fieldText =\n                        if emptyFieldText then\n                            \"\"\n\n                        else\n                            model.fieldText\n                }\n\n        DeleteField setFieldText text ->\n            immediatelyFilter url\n                filter\n                { model\n                    | fields = List.filter ((/=) text) model.fields\n                    , matches = []\n                    , fieldText =\n                        if setFieldText then\n                            text\n\n                        else\n                            model.fieldText\n                }\n\n        Select maybeSelectedMatch ->\n            ( { model | maybeSelectedMatch = maybeSelectedMatch }, Cmd.none )\n\n        Focus focused ->\n            ( { model\n                | focused = focused\n                , maybeSelectedMatch = Nothing\n              }\n            , Cmd.none\n            )\n\n        ResultsHovered resultsHovered ->\n            ( { model\n                | resultsHovered = resultsHovered\n              }\n            , Cmd.none\n            )\n\n        PressingBackspace pressed ->\n            ( { model | backspacePressed = pressed }, Cmd.none )\n\n        UpdateFieldText text ->\n            updateAutoComplete\n                { model\n                    | fieldText = text\n                }\n\n        Noop ->\n            ( model, Cmd.none )\n\n\nimmediatelyFilter : String -> Filter -> Model -> ( Model, Cmd Msg )\nimmediatelyFilter url filter model =\n    let\n        newFilter =\n            { filter | group = stringifyGroup model.fields }\n    in\n    ( model\n    , Cmd.batch\n        [ Navigation.pushUrl model.key (Utils.Filter.toUrl url newFilter)\n        , Dom.focus \"group-by-field\" |> Task.attempt (always Noop)\n        ]\n    )\n\n\nsetFields : Filter -> Model -> Model\nsetFields filter model =\n    { model\n        | fields =\n            parseGroup filter.group\n    }\n\n\nupdateAutoComplete : Model -> ( Model, Cmd Msg )\nupdateAutoComplete model =\n    ( { model\n        | matches =\n            if String.isEmpty model.fieldText then\n                []\n\n            else if String.contains \" \" model.fieldText then\n                model.matches\n\n            else\n                -- TODO: How many matches do we want to show?\n                -- NOTE: List.reverse is used because our scale is (0.0, 1.0),\n                -- but we want the higher values to be in the front of the\n                -- list.\n                Set.toList model.list\n                    |> List.filter ((\\a -> List.member a model.fields) >> not)\n                    |> List.sortBy (jaroWinkler model.fieldText)\n                    |> List.reverse\n                    |> List.take 10\n        , maybeSelectedMatch = Nothing\n      }\n    , Cmd.none\n    )\n"
  },
  {
    "path": "ui/app/src/Views/GroupBar/Views.elm",
    "content": "module Views.GroupBar.Views exposing (view)\n\nimport Html exposing (Html, a, button, div, input, small, text)\nimport Html.Attributes exposing (class, disabled, id, style, value)\nimport Html.Events exposing (onBlur, onClick, onFocus, onInput, onMouseEnter, onMouseLeave)\nimport Set\nimport Utils.Keyboard exposing (keys, onKeyDown, onKeyUp)\nimport Utils.List\nimport Utils.Views\nimport Views.GroupBar.Types exposing (Model, Msg(..))\n\n\nview : Model -> Bool -> Html Msg\nview ({ list, fieldText, fields } as model) customGrouping =\n    let\n        isDisabled =\n            not (Set.member fieldText list) || List.member fieldText fields\n\n        className =\n            if String.isEmpty fieldText then\n                \"\"\n\n            else if isDisabled then\n                \"has-danger\"\n\n            else\n                \"has-success\"\n\n        checkbox =\n            div [ class \"mb-3\" ]\n                [ Utils.Views.checkbox \"Enable custom grouping\" customGrouping CustomGrouping ]\n    in\n    if customGrouping then\n        div []\n            [ checkbox\n            , div\n                [ class \"row no-gutters align-items-start\" ]\n                (List.map viewField fields\n                    ++ [ div\n                            [ class (\"col \" ++ className)\n                            , style \"min-width\" \"200px\"\n                            ]\n                            [ textInputField isDisabled model\n                            , exampleField fields\n                            , autoCompleteResults model\n                            ]\n                       ]\n                )\n            ]\n\n    else\n        checkbox\n\n\nexampleField : List String -> Html Msg\nexampleField fields =\n    if List.member \"alertname\" fields then\n        small [ class \"form-text text-muted\" ]\n            [ text \"Label key for grouping alerts\"\n            ]\n\n    else\n        small [ class \"form-text text-muted\" ]\n            [ text \"Label key for grouping alerts, e.g.\"\n            , button\n                [ class \"btn btn-link btn-sm align-baseline\"\n                , onClick (UpdateFieldText \"alertname\")\n                ]\n                [ text \"alertname\" ]\n            ]\n\n\ntextInputField : Bool -> Model -> Html Msg\ntextInputField isDisabled { fieldText, matches, maybeSelectedMatch, fields, backspacePressed } =\n    let\n        onClickMsg =\n            if isDisabled then\n                Noop\n\n            else\n                AddField True fieldText\n\n        nextMatch =\n            maybeSelectedMatch\n                |> Maybe.map ((\\b a -> Utils.List.nextElem a b) <| matches)\n                |> Maybe.withDefault (List.head matches)\n\n        prevMatch =\n            maybeSelectedMatch\n                |> Maybe.map ((\\b a -> Utils.List.nextElem a b) <| List.reverse matches)\n                |> Maybe.withDefault (Utils.List.lastElem matches)\n\n        keyDown key =\n            if key == keys.down then\n                Select nextMatch\n\n            else if key == keys.up then\n                Select prevMatch\n\n            else if key == keys.enter then\n                if not isDisabled then\n                    AddField True fieldText\n\n                else\n                    maybeSelectedMatch\n                        |> Maybe.map (AddField True)\n                        |> Maybe.withDefault Noop\n\n            else if key == keys.backspace then\n                if fieldText == \"\" then\n                    case ( Utils.List.lastElem fields, backspacePressed ) of\n                        ( Just lastField, False ) ->\n                            DeleteField True lastField\n\n                        _ ->\n                            Noop\n\n                else\n                    PressingBackspace True\n\n            else\n                Noop\n\n        keyUp key =\n            if key == keys.backspace then\n                PressingBackspace False\n\n            else\n                Noop\n    in\n    div [ class \"input-group\" ]\n        [ input\n            [ id \"group-by-field\"\n            , class \"form-control\"\n            , value fieldText\n            , onKeyDown keyDown\n            , onKeyUp keyUp\n            , onInput UpdateFieldText\n            , onFocus (Focus True)\n            , onBlur (Focus False)\n            ]\n            []\n        , div\n            [ class \"input-group-append\" ]\n            [ button [ class \"btn btn-primary\", disabled isDisabled, onClick onClickMsg ] [ text \"+\" ] ]\n        ]\n\n\nautoCompleteResults : Model -> Html Msg\nautoCompleteResults { maybeSelectedMatch, focused, resultsHovered, matches } =\n    let\n        autoCompleteClass =\n            if (focused || resultsHovered) && not (List.isEmpty matches) then\n                \"show\"\n\n            else\n                \"\"\n    in\n    div\n        [ class (\"autocomplete-menu \" ++ autoCompleteClass)\n        , onMouseEnter (ResultsHovered True)\n        , onMouseLeave (ResultsHovered False)\n        ]\n        [ matches\n            |> List.map (matchedField maybeSelectedMatch)\n            |> div [ class \"dropdown-menu\" ]\n        ]\n\n\nmatchedField : Maybe String -> String -> Html Msg\nmatchedField maybeSelectedMatch field =\n    let\n        className =\n            if maybeSelectedMatch == Just field then\n                \"active\"\n\n            else\n                \"\"\n    in\n    button\n        [ class (\"dropdown-item \" ++ className)\n        , onClick (AddField True field)\n        ]\n        [ text field ]\n\n\nviewField : String -> Html Msg\nviewField field =\n    div [ class \"col col-auto\" ]\n        [ div [ class \"btn-group mr-2 mb-2\" ]\n            [ button\n                [ class \"btn btn-outline-info\"\n                , onClick (DeleteField True field)\n                ]\n                [ text field\n                ]\n            , button\n                [ class \"btn btn-outline-danger\"\n                , onClick (DeleteField False field)\n                ]\n                [ text \"×\" ]\n            ]\n        ]\n"
  },
  {
    "path": "ui/app/src/Views/NavBar/Types.elm",
    "content": "module Views.NavBar.Types exposing (Tab, alertsTab, noneTab, settingsTab, silencesTab, statusTab, tabs)\n\n\ntype alias Tab =\n    { link : String\n    , name : String\n    }\n\n\nalertsTab : Tab\nalertsTab =\n    { link = \"#/alerts\", name = \"Alerts\" }\n\n\nsilencesTab : Tab\nsilencesTab =\n    { link = \"#/silences\", name = \"Silences\" }\n\n\nstatusTab : Tab\nstatusTab =\n    { link = \"#/status\", name = \"Status\" }\n\n\nsettingsTab : Tab\nsettingsTab =\n    { link = \"#/settings\", name = \"Settings\" }\n\n\nhelpTab : Tab\nhelpTab =\n    { link = \"https://prometheus.io/docs/alerting/alertmanager/\", name = \"Documentation\" }\n\n\nnoneTab : Tab\nnoneTab =\n    { link = \"\", name = \"\" }\n\n\ntabs : List Tab\ntabs =\n    [ alertsTab, silencesTab, statusTab, settingsTab, helpTab ]\n"
  },
  {
    "path": "ui/app/src/Views/NavBar/Views.elm",
    "content": "module Views.NavBar.Views exposing (navBar)\n\nimport Html exposing (Html, a, div, header, li, nav, text, ul)\nimport Html.Attributes exposing (class, href, style, title)\nimport Types exposing (Route(..))\nimport Views.NavBar.Types exposing (Tab, alertsTab, noneTab, settingsTab, silencesTab, statusTab, tabs)\n\n\nnavBar : Route -> Html msg\nnavBar currentRoute =\n    header\n        [ class \"navbar navbar-expand-md navbar-light bg-light mb-5 pt-3 pb-3\"\n        , style \"border-bottom\" \"1px solid rgba(0, 0, 0, .125)\"\n        ]\n        [ nav [ class \"container\" ]\n            [ a [ class \"navbar-brand\", href \"#\" ] [ text \"Alertmanager\" ]\n            , ul [ class \"navbar-nav\" ] (navBarItems currentRoute)\n            , case currentRoute of\n                SilenceFormEditRoute _ ->\n                    text \"\"\n\n                SilenceFormNewRoute _ ->\n                    text \"\"\n\n                _ ->\n                    div [ class \"form-inline ml-auto\" ]\n                        [ a\n                            [ class \"btn btn-outline-info\"\n                            , href \"#/silences/new\"\n                            ]\n                            [ text \"New Silence\" ]\n                        ]\n            ]\n        ]\n\n\nnavBarItems : Route -> List (Html msg)\nnavBarItems currentRoute =\n    List.map (navBarItem currentRoute) tabs\n\n\nnavBarItem : Route -> Tab -> Html msg\nnavBarItem currentRoute tab =\n    li [ class <| \"nav-item\" ++ isActive currentRoute tab ]\n        [ a [ class \"nav-link\", href tab.link, title tab.name ]\n            [ text tab.name ]\n        ]\n\n\nisActive : Route -> Tab -> String\nisActive currentRoute tab =\n    if routeToTab currentRoute == tab then\n        \" active\"\n\n    else\n        \"\"\n\n\nrouteToTab : Route -> Tab\nrouteToTab currentRoute =\n    case currentRoute of\n        AlertsRoute _ ->\n            alertsTab\n\n        NotFoundRoute ->\n            noneTab\n\n        SilenceFormEditRoute _ ->\n            silencesTab\n\n        SilenceFormNewRoute _ ->\n            silencesTab\n\n        SilenceListRoute _ ->\n            silencesTab\n\n        SilenceViewRoute _ ->\n            silencesTab\n\n        StatusRoute ->\n            statusTab\n\n        TopLevelRoute ->\n            noneTab\n\n        SettingsRoute ->\n            settingsTab\n"
  },
  {
    "path": "ui/app/src/Views/NotFound/Views.elm",
    "content": "module Views.NotFound.Views exposing (view)\n\nimport Html exposing (Html, div, h1, text)\nimport Types exposing (Msg)\n\n\nview : Html Msg\nview =\n    div []\n        [ h1 [] [ text \"not found\" ]\n        ]\n"
  },
  {
    "path": "ui/app/src/Views/ReceiverBar/Types.elm",
    "content": "module Views.ReceiverBar.Types exposing (Model, Msg(..), Receiver, apiReceiverToReceiver, initReceiverBar)\n\nimport Browser.Navigation exposing (Key)\nimport Data.Receiver\nimport Regex\nimport Utils.Types exposing (ApiData(..))\n\n\ntype Msg\n    = ReceiversFetched (ApiData (List Data.Receiver.Receiver))\n    | UpdateReceiver String\n    | EditReceivers\n    | FilterByReceiver String\n    | Select (Maybe Receiver)\n    | ResultsHovered Bool\n    | BlurReceiverField\n    | Noop\n\n\ntype alias Model =\n    { receivers : List Receiver\n    , matches : List Receiver\n    , fieldText : String\n    , selectedReceiver : Maybe Receiver\n    , showReceivers : Bool\n    , resultsHovered : Bool\n    , key : Key\n    }\n\n\ntype alias Receiver =\n    { name : String\n    , regex : String\n    }\n\n\nescapeRegExp : String -> String\nescapeRegExp text =\n    let\n        reg =\n            Regex.fromString \"[-[\\\\]{}()*+?.,\\\\\\\\^$|#\\\\s]\" |> Maybe.withDefault Regex.never\n    in\n    Regex.replace reg (.match >> (++) \"\\\\\") text\n\n\napiReceiverToReceiver : Data.Receiver.Receiver -> Receiver\napiReceiverToReceiver r =\n    Receiver r.name (escapeRegExp r.name)\n\n\ninitReceiverBar : Key -> Model\ninitReceiverBar key =\n    { receivers = []\n    , matches = []\n    , fieldText = \"\"\n    , selectedReceiver = Nothing\n    , showReceivers = False\n    , resultsHovered = False\n    , key = key\n    }\n"
  },
  {
    "path": "ui/app/src/Views/ReceiverBar/Updates.elm",
    "content": "module Views.ReceiverBar.Updates exposing (fetchReceivers, update)\n\nimport Alerts.Api as Api\nimport Browser.Dom as Dom\nimport Browser.Navigation as Navigation\nimport Task\nimport Utils.Filter exposing (Filter)\nimport Utils.Match exposing (jaroWinkler)\nimport Utils.Types exposing (ApiData(..))\nimport Views.ReceiverBar.Types exposing (Model, Msg(..), apiReceiverToReceiver)\n\n\nupdate : String -> Filter -> Msg -> Model -> ( Model, Cmd Msg )\nupdate url filter msg model =\n    case msg of\n        ReceiversFetched (Success receivers) ->\n            ( { model | receivers = List.map apiReceiverToReceiver receivers }, Cmd.none )\n\n        ReceiversFetched _ ->\n            ( model, Cmd.none )\n\n        EditReceivers ->\n            ( { model\n                | showReceivers = True\n                , fieldText = \"\"\n                , matches =\n                    model.receivers\n                        |> List.take 10\n                        |> (::) { name = \"All\", regex = \"\" }\n                , selectedReceiver = Nothing\n              }\n            , Dom.focus \"receiver-field\" |> Task.attempt (always Noop)\n            )\n\n        ResultsHovered resultsHovered ->\n            ( { model | resultsHovered = resultsHovered }, Cmd.none )\n\n        UpdateReceiver receiver ->\n            let\n                matches =\n                    model.receivers\n                        |> List.sortBy (.name >> jaroWinkler receiver)\n                        |> List.reverse\n                        |> List.take 10\n                        |> (::) { name = \"All\", regex = \"\" }\n            in\n            ( { model\n                | fieldText = receiver\n                , matches = matches\n              }\n            , Cmd.none\n            )\n\n        BlurReceiverField ->\n            ( { model | showReceivers = False }, Cmd.none )\n\n        Select maybeReceiver ->\n            ( { model | selectedReceiver = maybeReceiver }, Cmd.none )\n\n        FilterByReceiver regex ->\n            ( { model | showReceivers = False, resultsHovered = False }\n            , Navigation.pushUrl model.key\n                (Utils.Filter.toUrl url\n                    { filter\n                        | receiver =\n                            if regex == \"\" then\n                                Nothing\n\n                            else\n                                Just regex\n                    }\n                )\n            )\n\n        Noop ->\n            ( model, Cmd.none )\n\n\nfetchReceivers : String -> Cmd Msg\nfetchReceivers =\n    Api.fetchReceivers >> Cmd.map ReceiversFetched\n"
  },
  {
    "path": "ui/app/src/Views/ReceiverBar/Views.elm",
    "content": "module Views.ReceiverBar.Views exposing (view)\n\nimport Html exposing (Html, div, input, li, text)\nimport Html.Attributes exposing (class, id, style, tabindex, value)\nimport Html.Events exposing (onBlur, onClick, onInput, onMouseEnter, onMouseLeave)\nimport Utils.Keyboard exposing (keys, onKeyDown)\nimport Utils.List\nimport Views.ReceiverBar.Types exposing (Model, Msg(..), Receiver)\n\n\nview : Maybe String -> Model -> Html Msg\nview maybeRegex model =\n    if model.showReceivers || model.resultsHovered then\n        viewDropdown model\n\n    else\n        viewResult maybeRegex model.receivers\n\n\nviewResult : Maybe String -> List Receiver -> Html Msg\nviewResult maybeRegex receivers =\n    let\n        unescapedReceiver =\n            receivers\n                |> List.filter (.regex >> Just >> (==) maybeRegex)\n                |> List.map (.name >> Just)\n                |> List.head\n                |> Maybe.withDefault maybeRegex\n    in\n    li\n        [ class \"nav-item ml-auto\"\n        , tabindex 1\n        , style \"position\" \"relative\"\n        , style \"outline\" \"none\"\n        ]\n        [ div\n            [ onClick EditReceivers\n            , class \"mt-1 mr-4\"\n            , style \"cursor\" \"pointer\"\n            ]\n            [ text (\"Receiver: \" ++ Maybe.withDefault \"All\" unescapedReceiver) ]\n        ]\n\n\nviewDropdown : Model -> Html Msg\nviewDropdown { matches, fieldText, selectedReceiver } =\n    let\n        nextMatch =\n            selectedReceiver\n                |> Maybe.map ((\\b a -> Utils.List.nextElem a b) <| matches)\n                |> Maybe.withDefault (List.head matches)\n\n        prevMatch =\n            selectedReceiver\n                |> Maybe.map ((\\b a -> Utils.List.nextElem a b) <| List.reverse matches)\n                |> Maybe.withDefault (Utils.List.lastElem matches)\n\n        keyDown key =\n            if key == keys.down then\n                Select nextMatch\n\n            else if key == keys.up then\n                Select prevMatch\n\n            else if key == keys.enter then\n                selectedReceiver\n                    |> Maybe.map .regex\n                    |> Maybe.withDefault fieldText\n                    |> FilterByReceiver\n\n            else\n                Noop\n    in\n    li\n        [ class \"nav-item ml-auto mr-4 autocomplete-menu show\"\n        , onMouseEnter (ResultsHovered True)\n        , onMouseLeave (ResultsHovered False)\n        , style \"position\" \"relative\"\n        , style \"outline\" \"none\"\n        ]\n        [ input\n            [ id \"receiver-field\"\n            , value fieldText\n            , onBlur BlurReceiverField\n            , onInput UpdateReceiver\n            , onKeyDown keyDown\n            , class \"mr-4\"\n            , style \"display\" \"block\"\n            , style \"width\" \"100%\"\n            ]\n            []\n        , matches\n            |> List.map (receiverField selectedReceiver)\n            |> div [ class \"dropdown-menu dropdown-menu-right\" ]\n        ]\n\n\nreceiverField : Maybe Receiver -> Receiver -> Html Msg\nreceiverField selected receiver =\n    let\n        attrs =\n            if selected == Just receiver then\n                [ class \"dropdown-item active\" ]\n\n            else\n                [ class \"dropdown-item\"\n                , style \"cursor\" \"pointer\"\n                , onClick (FilterByReceiver receiver.regex)\n                ]\n    in\n    div attrs [ text receiver.name ]\n"
  },
  {
    "path": "ui/app/src/Views/Settings/Parsing.elm",
    "content": "module Views.Settings.Parsing exposing (settingsViewParser)\n\nimport Url.Parser exposing (Parser, s)\n\n\nsettingsViewParser : Parser a a\nsettingsViewParser =\n    s \"settings\"\n"
  },
  {
    "path": "ui/app/src/Views/Settings/Types.elm",
    "content": "module Views.Settings.Types exposing (..)\n\nimport Utils.DateTimePicker.Utils exposing (FirstDayOfWeek)\n\n\ntype alias Model =\n    { firstDayOfWeek : FirstDayOfWeek\n    }\n\n\ntype SettingsMsg\n    = UpdateFirstDayOfWeek String\n"
  },
  {
    "path": "ui/app/src/Views/Settings/Updates.elm",
    "content": "port module Views.Settings.Updates exposing (..)\n\nimport Task\nimport Types exposing (Msg(..))\nimport Utils.DateTimePicker.Utils exposing (FirstDayOfWeek(..))\nimport Views.Settings.Types exposing (..)\nimport Views.SilenceForm.Types\n\n\nupdate : SettingsMsg -> Model -> ( Model, Cmd Msg )\nupdate msg model =\n    case msg of\n        Views.Settings.Types.UpdateFirstDayOfWeek firstDayOfWeekString ->\n            let\n                firstDayOfWeek =\n                    case firstDayOfWeekString of\n                        \"Monday\" ->\n                            Monday\n\n                        \"Sunday\" ->\n                            Sunday\n\n                        \"Saturday\" ->\n                            Saturday\n\n                        _ ->\n                            Monday\n\n                firstDayOfWeekString2 =\n                    case firstDayOfWeek of\n                        Monday ->\n                            \"Monday\"\n\n                        Sunday ->\n                            \"Sunday\"\n\n                        Saturday ->\n                            \"Saturday\"\n            in\n            ( { model | firstDayOfWeek = firstDayOfWeek }\n            , Cmd.batch\n                [ Task.perform identity\n                    (Task.succeed\n                        (MsgForSilenceForm\n                            (Views.SilenceForm.Types.UpdateFirstDayOfWeek\n                                firstDayOfWeek\n                            )\n                        )\n                    )\n                , persistFirstDayOfWeek firstDayOfWeekString2\n                ]\n            )\n\n\nport persistFirstDayOfWeek : String -> Cmd msg\n"
  },
  {
    "path": "ui/app/src/Views/Settings/Views.elm",
    "content": "module Views.Settings.Views exposing (view)\n\nimport Html exposing (..)\nimport Html.Attributes exposing (checked, class, for, id, type_, value)\nimport Html.Events exposing (..)\nimport Utils.DateTimePicker.Utils exposing (FirstDayOfWeek(..))\nimport Views.Settings.Types exposing (Model, SettingsMsg(..))\n\n\nview : Model -> Html SettingsMsg\nview model =\n    div []\n        [ div [ class \"no-gutters\" ]\n            [ label\n                [ for \"fieldset\" ]\n                [ text \"First day of the week:\" ]\n            , fieldset [ id \"fieldset\" ]\n                [ radio \"Monday\" (model.firstDayOfWeek == Monday) UpdateFirstDayOfWeek\n                , radio \"Sunday\" (model.firstDayOfWeek == Sunday) UpdateFirstDayOfWeek\n                , radio \"Saturday\" (model.firstDayOfWeek == Saturday) UpdateFirstDayOfWeek\n                ]\n            , small [ class \"form-text text-muted\" ]\n                [ text \"Note: This setting is saved in local storage of your browser\"\n                ]\n            ]\n        ]\n\n\nradio : String -> Bool -> (String -> msg) -> Html msg\nradio radioValue isChecked msg =\n    div [ class \"mt-1 ml-1 custom-control custom-radio\" ]\n        [ input\n            [ type_ \"radio\"\n            , id radioValue\n            , class \"custom-control-input\"\n            , checked isChecked\n            , value radioValue\n            , onInput msg\n            ]\n            []\n        , label [ class \"custom-control-label\", for radioValue ] [ text radioValue ]\n        ]\n"
  },
  {
    "path": "ui/app/src/Views/Shared/Alert.elm",
    "content": "module Views.Shared.Alert exposing (annotation, annotationsButton, generatorUrlButton, titleView)\n\nimport Data.GettableAlert exposing (GettableAlert)\nimport Html exposing (Html, a, button, i, span, td, text, th, tr)\nimport Html.Attributes exposing (class, href)\nimport Html.Events exposing (onClick)\nimport Utils.Date exposing (dateTimeFormat)\nimport Utils.Views exposing (linkifyText)\nimport Views.Shared.Types exposing (Msg)\n\n\nannotationsButton : Maybe String -> GettableAlert -> Html Msg\nannotationsButton activeAlertId alert =\n    if activeAlertId == Just alert.fingerprint then\n        button\n            [ onClick Nothing\n            , class \"btn btn-outline-info border-0 active\"\n            ]\n            [ i [ class \"fa fa-minus mr-2\" ] [], text \"Info\" ]\n\n    else\n        button\n            [ class \"btn btn-outline-info border-0\"\n            , onClick (Just alert.fingerprint)\n            ]\n            [ i [ class \"fa fa-plus mr-2\" ] [], text \"Info\" ]\n\n\nannotation : ( String, String ) -> Html msg\nannotation ( key, value ) =\n    tr []\n        [ th [ class \"text-nowrap\" ] [ text (key ++ \":\") ]\n        , td [ class \"w-100\" ] (linkifyText value)\n        ]\n\n\ntitleView : GettableAlert -> Html msg\ntitleView alert =\n    span\n        [ class \"align-self-center mr-2\" ]\n        [ text\n            (dateTimeFormat alert.startsAt)\n        ]\n\n\ngeneratorUrlButton : String -> Html msg\ngeneratorUrlButton url =\n    if String.startsWith \"http://\" url || String.startsWith \"https://\" url then\n        a\n            [ class \"btn btn-outline-info border-0\", href url ]\n            [ i [ class \"fa fa-line-chart mr-2\" ] []\n            , text \"Source\"\n            ]\n\n    else\n        text \"\"\n"
  },
  {
    "path": "ui/app/src/Views/Shared/AlertCompact.elm",
    "content": "module Views.Shared.AlertCompact exposing (view)\n\nimport Data.GettableAlert exposing (GettableAlert)\nimport Dict\nimport Html exposing (Html, div, table, text)\nimport Html.Attributes exposing (class, style)\nimport Utils.Views exposing (labelButton)\nimport Views.Shared.Alert exposing (annotation, annotationsButton, generatorUrlButton, titleView)\nimport Views.Shared.Types exposing (Msg)\n\n\nview : Maybe String -> GettableAlert -> Html Msg\nview activeAlertId alert =\n    let\n        -- remove the grouping labels, and bring the alertname to front\n        ungroupedLabels =\n            alert.labels\n                |> Dict.toList\n                |> List.partition (Tuple.first >> (==) \"alertname\")\n                |> (\\( a, b ) -> a ++ b)\n                |> List.map (\\( a, b ) -> String.join \"=\" [ a, b ])\n    in\n    div\n        [ -- speedup rendering in Chrome, because list-group-item className\n          -- creates a new layer in the rendering engine\n          style \"position\" \"static\"\n        , class \"border-0 p-0 mb-4\"\n        ]\n        [ div\n            [ class \"w-100 mb-2 d-flex\" ]\n            [ titleView alert\n            , if Dict.size alert.annotations > 0 then\n                annotationsButton activeAlertId alert\n\n              else\n                text \"\"\n            , case alert.generatorURL of\n                Just url ->\n                    generatorUrlButton url\n\n                Nothing ->\n                    text \"\"\n            ]\n        , if activeAlertId == Just alert.fingerprint then\n            table\n                [ class \"table w-100 mb-1\" ]\n                (List.map annotation <| Dict.toList alert.annotations)\n\n          else\n            text \"\"\n        , div [] (List.map (labelButton Nothing) ungroupedLabels)\n        ]\n"
  },
  {
    "path": "ui/app/src/Views/Shared/AlertListCompact.elm",
    "content": "module Views.Shared.AlertListCompact exposing (view)\n\nimport Data.GettableAlert exposing (GettableAlert)\nimport Html exposing (Html, div)\nimport Html.Attributes exposing (class)\nimport Views.Shared.AlertCompact\nimport Views.Shared.Types exposing (Msg)\n\n\nview : Maybe String -> List GettableAlert -> Html Msg\nview activeAlertId alerts =\n    List.map (Views.Shared.AlertCompact.view activeAlertId) alerts\n        |> div [ class \"pa0 w-100\" ]\n"
  },
  {
    "path": "ui/app/src/Views/Shared/Dialog.elm",
    "content": "module Views.Shared.Dialog exposing (Config, view)\n\nimport Html exposing (Html, button, div, h5, text)\nimport Html.Attributes exposing (class, style)\nimport Html.Events exposing (onClick)\n\n\ntype alias Config msg =\n    { title : String\n    , body : Html msg\n    , footer : Html msg\n    , onClose : msg\n    }\n\n\nview : Maybe (Config msg) -> Html msg\nview maybeConfig =\n    case maybeConfig of\n        Nothing ->\n            div [ style \"clip\" \"rect(0,0,0,0)\", style \"position\" \"fixed\" ]\n                [ div [ class \"modal fade\" ] []\n                , div [ class \"modal-backdrop fade\" ] []\n                ]\n\n        Just { onClose, body, footer, title } ->\n            div []\n                [ div [ class \"modal fade show\", style \"display\" \"block\" ]\n                    [ div [ class \"modal-dialog modal-dialog-centered\" ]\n                        [ div [ class \"modal-content\" ]\n                            [ div [ class \"modal-header\" ]\n                                [ h5 [ class \"modal-title\" ] [ text title ]\n                                , button\n                                    [ class \"close\"\n                                    , onClick onClose\n                                    ]\n                                    [ text \"×\" ]\n                                ]\n                            , div [ class \"modal-body\" ] [ body ]\n                            , div [ class \"modal-footer\" ] [ footer ]\n                            ]\n                        ]\n                    ]\n                , div [ class \"modal-backdrop fade show\" ] []\n                ]\n"
  },
  {
    "path": "ui/app/src/Views/Shared/SilencePreview.elm",
    "content": "module Views.Shared.SilencePreview exposing (view)\n\nimport Data.GettableAlert exposing (GettableAlert)\nimport Html exposing (Html, div, p, strong, text)\nimport Html.Attributes exposing (class)\nimport Utils.Types exposing (ApiData(..))\nimport Utils.Views exposing (loading)\nimport Views.Shared.AlertListCompact\nimport Views.Shared.Types exposing (Msg)\n\n\nview : Maybe String -> ApiData (List GettableAlert) -> Html Msg\nview activeAlertId alertsResponse =\n    case alertsResponse of\n        Success alerts ->\n            if List.isEmpty alerts then\n                div [ class \"w-100\" ]\n                    [ p [] [ strong [] [ text \"No affected alerts\" ] ] ]\n\n            else\n                div [ class \"w-100\" ]\n                    [ p [] [ strong [] [ text (\"Affected alerts: \" ++ String.fromInt (List.length alerts)) ] ]\n                    , Views.Shared.AlertListCompact.view activeAlertId alerts\n                    ]\n\n        Initial ->\n            text \"\"\n\n        Loading ->\n            loading\n\n        Failure e ->\n            div [ class \"alert alert-warning\" ] [ text e ]\n"
  },
  {
    "path": "ui/app/src/Views/Shared/Types.elm",
    "content": "module Views.Shared.Types exposing (Msg)\n\n\ntype alias Msg =\n    Maybe String\n"
  },
  {
    "path": "ui/app/src/Views/SilenceForm/Parsing.elm",
    "content": "module Views.SilenceForm.Parsing exposing (newSilenceFromAlertLabels, newSilenceFromMatchers, newSilenceFromMatchersAndComment, silenceFormEditParser, silenceFormNewParser)\n\nimport Data.Matcher\nimport Dict exposing (Dict)\nimport Url exposing (percentEncode)\nimport Url.Parser exposing ((</>), (<?>), Parser, s, string)\nimport Url.Parser.Query as Query\nimport Utils.Filter exposing (SilenceFormGetParams, parseFilter)\n\n\nnewSilenceFromAlertLabels : Dict String String -> String\nnewSilenceFromAlertLabels labels =\n    labels\n        |> Dict.toList\n        |> List.map (\\( k, v ) -> Utils.Filter.Matcher k Utils.Filter.Eq v)\n        |> encodeMatchers\n\n\nparseGetParams : Maybe String -> Maybe String -> SilenceFormGetParams\nparseGetParams filter comment =\n    { matchers = filter |> Maybe.andThen parseFilter >> Maybe.withDefault []\n    , comment = comment |> Maybe.withDefault \"\"\n    }\n\n\nsilenceFormNewParser : Parser (SilenceFormGetParams -> a) a\nsilenceFormNewParser =\n    s \"silences\"\n        </> s \"new\"\n        <?> Query.map2 parseGetParams (Query.string \"filter\") (Query.string \"comment\")\n\n\nsilenceFormEditParser : Parser (String -> a) a\nsilenceFormEditParser =\n    s \"silences\" </> string </> s \"edit\"\n\n\nnewSilenceFromMatchers : List Data.Matcher.Matcher -> String\nnewSilenceFromMatchers matchers =\n    matchers\n        |> List.map\n            (\\{ name, value, isRegex, isEqual } ->\n                let\n                    isEqualValue =\n                        case isEqual of\n                            Nothing ->\n                                True\n\n                            Just justIsEqual ->\n                                justIsEqual\n\n                    op =\n                        if not isRegex && isEqualValue then\n                            Utils.Filter.Eq\n\n                        else if not isRegex && not isEqualValue then\n                            Utils.Filter.NotEq\n\n                        else if isRegex && isEqualValue then\n                            Utils.Filter.RegexMatch\n\n                        else\n                            Utils.Filter.NotRegexMatch\n                in\n                Utils.Filter.Matcher name op value\n            )\n        |> encodeMatchers\n\n\nnewSilenceFromMatchersAndComment : List Data.Matcher.Matcher -> String -> String\nnewSilenceFromMatchersAndComment matchers comment =\n    newSilenceFromMatchers matchers ++ \"&comment=\" ++ (comment |> percentEncode)\n\n\nencodeMatchers : List Utils.Filter.Matcher -> String\nencodeMatchers matchers =\n    matchers\n        |> Utils.Filter.stringifyFilter\n        |> percentEncode\n        |> (++) \"#/silences/new?filter=\"\n"
  },
  {
    "path": "ui/app/src/Views/SilenceForm/Types.elm",
    "content": "module Views.SilenceForm.Types exposing\n    ( Model\n    , SilenceForm\n    , SilenceFormFieldMsg(..)\n    , SilenceFormMsg(..)\n    , fromDateTimePicker\n    , fromMatchersAndCommentAndTime\n    , fromSilence\n    , initSilenceForm\n    , parseEndsAt\n    , toSilence\n    , validateForm\n    , validateMatchers\n    )\n\nimport Browser.Navigation exposing (Key)\nimport Data.GettableAlert exposing (GettableAlert)\nimport Data.GettableSilence exposing (GettableSilence)\nimport Data.Matcher\nimport Data.PostableSilence exposing (PostableSilence)\nimport DateTime\nimport Silences.Types exposing (nullSilence)\nimport Time exposing (Posix)\nimport Utils.Date exposing (addDuration, durationFormat, parseDuration, timeDifference, timeFromString, timeToString)\nimport Utils.DateTimePicker.Types exposing (DateTimePicker, initDateTimePicker, initFromStartAndEndTime)\nimport Utils.DateTimePicker.Utils exposing (FirstDayOfWeek)\nimport Utils.Filter\nimport Utils.FormValidation\n    exposing\n        ( ValidatedField\n        , ValidationState(..)\n        , initialField\n        , stringNotEmpty\n        , validate\n        )\nimport Utils.Types exposing (ApiData(..))\nimport Views.FilterBar.Types as FilterBar\n\n\ntype alias Model =\n    { form : SilenceForm\n    , filterBar : FilterBar.Model\n    , filterBarValid : ValidationState\n    , silenceId : ApiData String\n    , alerts : ApiData (List GettableAlert)\n    , activeAlertId : Maybe String\n    , key : Key\n    , firstDayOfWeek : FirstDayOfWeek\n    }\n\n\ntype alias SilenceForm =\n    { id : Maybe String\n    , createdBy : ValidatedField\n    , comment : ValidatedField\n    , startsAt : ValidatedField\n    , endsAt : ValidatedField\n    , duration : ValidatedField\n    , dateTimePicker : DateTimePicker\n    , viewDateTimePicker : Bool\n    }\n\n\ntype SilenceFormMsg\n    = UpdateField SilenceFormFieldMsg\n    | CreateSilence\n    | PreviewSilence\n    | AlertGroupsPreview (ApiData (List GettableAlert))\n    | SetActiveAlert (Maybe String)\n    | FetchSilence String\n    | NewSilenceFromMatchersAndComment String Utils.Filter.SilenceFormGetParams\n    | NewSilenceFromMatchersAndCommentAndTime String (List Utils.Filter.Matcher) String Posix\n    | SilenceFetch (ApiData GettableSilence)\n    | SilenceCreate (ApiData String)\n    | UpdateDateTimePicker Utils.DateTimePicker.Types.Msg\n    | MsgForFilterBar FilterBar.Msg\n    | UpdateFirstDayOfWeek FirstDayOfWeek\n\n\ntype SilenceFormFieldMsg\n    = UpdateStartsAt String\n    | UpdateEndsAt String\n    | UpdateDuration String\n    | ValidateTime\n    | UpdateCreatedBy String\n    | ValidateCreatedBy\n    | UpdateComment String\n    | ValidateComment\n    | UpdateTimesFromPicker\n    | OpenDateTimePicker\n    | CloseDateTimePicker\n\n\ninitSilenceForm : Key -> FirstDayOfWeek -> Model\ninitSilenceForm key firstDayOfWeek =\n    { form = empty firstDayOfWeek\n    , filterBar = FilterBar.initFilterBar []\n    , filterBarValid = Utils.FormValidation.Initial\n    , silenceId = Utils.Types.Initial\n    , alerts = Utils.Types.Initial\n    , activeAlertId = Nothing\n    , key = key\n    , firstDayOfWeek = firstDayOfWeek\n    }\n\n\ntoSilence : FilterBar.Model -> SilenceForm -> Maybe PostableSilence\ntoSilence filterBar { id, comment, createdBy, startsAt, endsAt } =\n    Result.map5\n        (\\nonEmptyMatchers nonEmptyComment nonEmptyCreatedBy parsedStartsAt parsedEndsAt ->\n            { nullSilence\n                | id = id\n                , comment = nonEmptyComment\n                , matchers = nonEmptyMatchers\n                , createdBy = nonEmptyCreatedBy\n                , startsAt = parsedStartsAt\n                , endsAt = parsedEndsAt\n            }\n        )\n        (validMatchers filterBar)\n        (stringNotEmpty comment.value)\n        (stringNotEmpty createdBy.value)\n        (timeFromString startsAt.value)\n        (parseEndsAt startsAt.value endsAt.value)\n        |> Result.toMaybe\n\n\nvalidMatchers : FilterBar.Model -> Result String (List Data.Matcher.Matcher)\nvalidMatchers { matchers, matcherText } =\n    if matcherText /= \"\" then\n        Err \"Please complete adding the matcher\"\n\n    else\n        case matchers of\n            [] ->\n                Err \"Matchers are required\"\n\n            nonEmptyMatchers ->\n                Ok (List.map Utils.Filter.toApiMatcher nonEmptyMatchers)\n\n\nfromSilence : GettableSilence -> FirstDayOfWeek -> SilenceForm\nfromSilence { id, createdBy, comment, startsAt, endsAt } firstDayOfWeek =\n    let\n        startsPosix =\n            Utils.Date.timeFromString (DateTime.toString startsAt)\n                |> Result.toMaybe\n\n        endsPosix =\n            Utils.Date.timeFromString (DateTime.toString endsAt)\n                |> Result.toMaybe\n    in\n    { id = Just id\n    , createdBy = initialField createdBy\n    , comment = initialField comment\n    , startsAt = initialField (timeToString startsAt)\n    , endsAt = initialField (timeToString endsAt)\n    , duration = initialField (durationFormat (timeDifference startsAt endsAt) |> Maybe.withDefault \"\")\n    , dateTimePicker = initFromStartAndEndTime startsPosix endsPosix firstDayOfWeek\n    , viewDateTimePicker = False\n    }\n\n\nvalidateForm : SilenceForm -> SilenceForm\nvalidateForm { id, createdBy, comment, startsAt, endsAt, duration, dateTimePicker } =\n    { id = id\n    , createdBy = validate stringNotEmpty createdBy\n    , comment = validate stringNotEmpty comment\n    , startsAt = validate timeFromString startsAt\n    , endsAt = validate (parseEndsAt startsAt.value) endsAt\n    , duration = validate parseDuration duration\n    , dateTimePicker = dateTimePicker\n    , viewDateTimePicker = False\n    }\n\n\nvalidateMatchers : FilterBar.Model -> ValidationState\nvalidateMatchers filter =\n    case validMatchers filter of\n        Err error ->\n            Utils.FormValidation.Invalid error\n\n        Ok _ ->\n            Utils.FormValidation.Valid\n\n\nparseEndsAt : String -> String -> Result String Posix\nparseEndsAt startsAt endsAt =\n    case ( timeFromString startsAt, timeFromString endsAt ) of\n        ( Ok starts, Ok ends ) ->\n            if Time.posixToMillis starts > Time.posixToMillis ends then\n                Err \"Can't be in the past\"\n\n            else\n                Ok ends\n\n        ( _, endsResult ) ->\n            endsResult\n\n\nempty : FirstDayOfWeek -> SilenceForm\nempty firstDayOfWeek =\n    { id = Nothing\n    , createdBy = initialField \"\"\n    , comment = initialField \"\"\n    , startsAt = initialField \"\"\n    , endsAt = initialField \"\"\n    , duration = initialField \"\"\n    , dateTimePicker = initDateTimePicker firstDayOfWeek\n    , viewDateTimePicker = False\n    }\n\n\ndefaultDuration : Float\ndefaultDuration =\n    -- 2 hours\n    2 * 60 * 60 * 1000\n\n\nfromMatchersAndCommentAndTime : String -> String -> Posix -> FirstDayOfWeek -> SilenceForm\nfromMatchersAndCommentAndTime defaultCreator comment now firstDayOfWeek =\n    { id = Nothing\n    , startsAt = initialField (timeToString now)\n    , endsAt = initialField (timeToString (addDuration defaultDuration now))\n    , duration = initialField (durationFormat defaultDuration |> Maybe.withDefault \"\")\n    , createdBy = initialField defaultCreator\n    , comment = initialField comment\n    , dateTimePicker = initFromStartAndEndTime (Just now) (Just (addDuration defaultDuration now)) firstDayOfWeek\n    , viewDateTimePicker = False\n    }\n\n\nfromDateTimePicker : SilenceForm -> DateTimePicker -> SilenceForm\nfromDateTimePicker { id, createdBy, comment, startsAt, endsAt, duration } newPicker =\n    { id = id\n    , createdBy = createdBy\n    , comment = comment\n    , startsAt = startsAt\n    , endsAt = endsAt\n    , duration = duration\n    , dateTimePicker = newPicker\n    , viewDateTimePicker = True\n    }\n"
  },
  {
    "path": "ui/app/src/Views/SilenceForm/Updates.elm",
    "content": "port module Views.SilenceForm.Updates exposing (update)\n\nimport Alerts.Api\nimport Browser.Navigation as Navigation\nimport Silences.Api\nimport Task\nimport Time\nimport Types exposing (Msg(..))\nimport Utils.Date exposing (timeFromString)\nimport Utils.DateTimePicker.Types exposing (initFromStartAndEndTime)\nimport Utils.DateTimePicker.Updates as DateTimePickerUpdates\nimport Utils.Filter exposing (silencePreviewFilter)\nimport Utils.FormValidation exposing (initialField, stringNotEmpty, updateValue, validate)\nimport Utils.Types exposing (ApiData(..))\nimport Views.FilterBar.Types as FilterBar\nimport Views.FilterBar.Updates as FilterBar\nimport Views.SilenceForm.Types\n    exposing\n        ( Model\n        , SilenceForm\n        , SilenceFormFieldMsg(..)\n        , SilenceFormMsg(..)\n        , fromDateTimePicker\n        , fromMatchersAndCommentAndTime\n        , fromSilence\n        , parseEndsAt\n        , toSilence\n        , validateForm\n        , validateMatchers\n        )\n\n\nupdateForm : SilenceFormFieldMsg -> SilenceForm -> SilenceForm\nupdateForm msg form =\n    case msg of\n        UpdateStartsAt time ->\n            let\n                startsAt =\n                    Utils.Date.timeFromString time\n\n                endsAt =\n                    Utils.Date.timeFromString form.endsAt.value\n\n                durationValue =\n                    case Result.map2 Utils.Date.timeDifference startsAt endsAt of\n                        Ok duration ->\n                            case Utils.Date.durationFormat duration of\n                                Just value ->\n                                    value\n\n                                Nothing ->\n                                    form.duration.value\n\n                        Err _ ->\n                            form.duration.value\n            in\n            { form\n                | startsAt = updateValue time form.startsAt\n                , duration = updateValue durationValue form.duration\n            }\n\n        UpdateEndsAt time ->\n            let\n                endsAt =\n                    Utils.Date.timeFromString time\n\n                startsAt =\n                    Utils.Date.timeFromString form.startsAt.value\n\n                durationValue =\n                    case Result.map2 Utils.Date.timeDifference startsAt endsAt of\n                        Ok duration ->\n                            case Utils.Date.durationFormat duration of\n                                Just value ->\n                                    value\n\n                                Nothing ->\n                                    form.duration.value\n\n                        Err _ ->\n                            form.duration.value\n            in\n            { form\n                | endsAt = updateValue time form.endsAt\n                , duration = updateValue durationValue form.duration\n            }\n\n        UpdateDuration time ->\n            let\n                duration =\n                    Utils.Date.parseDuration time\n\n                startsAt =\n                    Utils.Date.timeFromString form.startsAt.value\n\n                endsAtValue =\n                    case Result.map2 Utils.Date.addDuration duration startsAt of\n                        Ok endsAt ->\n                            Utils.Date.timeToString endsAt\n\n                        Err _ ->\n                            form.endsAt.value\n            in\n            { form\n                | endsAt = updateValue endsAtValue form.endsAt\n                , duration = updateValue time form.duration\n            }\n\n        ValidateTime ->\n            { form\n                | startsAt = validate Utils.Date.timeFromString form.startsAt\n                , endsAt = validate (parseEndsAt form.startsAt.value) form.endsAt\n                , duration = validate Utils.Date.parseDuration form.duration\n            }\n\n        UpdateCreatedBy createdBy ->\n            { form | createdBy = updateValue createdBy form.createdBy }\n\n        ValidateCreatedBy ->\n            { form | createdBy = validate stringNotEmpty form.createdBy }\n\n        UpdateComment comment ->\n            { form | comment = updateValue comment form.comment }\n\n        ValidateComment ->\n            { form | comment = validate stringNotEmpty form.comment }\n\n        UpdateTimesFromPicker ->\n            let\n                ( startsAt, endsAt, duration ) =\n                    case ( form.dateTimePicker.startTime, form.dateTimePicker.endTime ) of\n                        ( Just start, Just end ) ->\n                            ( validate timeFromString (initialField (Utils.Date.timeToString start))\n                            , validate (parseEndsAt (Utils.Date.timeToString start)) (initialField (Utils.Date.timeToString end))\n                            , initialField (Utils.Date.durationFormat (Utils.Date.timeDifference start end) |> Maybe.withDefault \"\")\n                                |> validate Utils.Date.parseDuration\n                            )\n\n                        _ ->\n                            ( form.startsAt, form.endsAt, form.duration )\n            in\n            { form\n                | startsAt = startsAt\n                , endsAt = endsAt\n                , duration = duration\n                , viewDateTimePicker = False\n            }\n\n        OpenDateTimePicker ->\n            let\n                startsAtTime =\n                    case timeFromString form.startsAt.value of\n                        Ok time ->\n                            Just time\n\n                        _ ->\n                            form.dateTimePicker.startTime\n\n                endsAtTime =\n                    timeFromString form.endsAt.value |> Result.toMaybe\n            in\n            { form\n                | viewDateTimePicker = True\n                , dateTimePicker = initFromStartAndEndTime startsAtTime endsAtTime form.dateTimePicker.firstDayOfWeek\n            }\n\n        CloseDateTimePicker ->\n            { form\n                | viewDateTimePicker = False\n            }\n\n\nupdate : SilenceFormMsg -> Model -> String -> String -> ( Model, Cmd Msg )\nupdate msg model basePath apiUrl =\n    case msg of\n        CreateSilence ->\n            case toSilence model.filterBar model.form of\n                Just silence ->\n                    ( { model | silenceId = Loading }\n                    , Cmd.batch\n                        [ Silences.Api.create apiUrl silence |> Cmd.map (SilenceCreate >> MsgForSilenceForm)\n                        , persistDefaultCreator silence.createdBy\n                        , Task.succeed silence.createdBy |> Task.perform SetDefaultCreator\n                        ]\n                    )\n\n                Nothing ->\n                    ( { model\n                        | silenceId = Failure \"Could not submit the form, Silence is not yet valid.\"\n                        , form = validateForm model.form\n                        , filterBarValid = validateMatchers model.filterBar\n                      }\n                    , Cmd.none\n                    )\n\n        SilenceCreate silenceId ->\n            let\n                cmd =\n                    case silenceId of\n                        Success id ->\n                            Navigation.pushUrl model.key (basePath ++ \"#/silences/\" ++ id)\n\n                        _ ->\n                            Cmd.none\n            in\n            ( { model | silenceId = silenceId }, cmd )\n\n        NewSilenceFromMatchersAndComment defaultCreator params ->\n            ( model, Task.perform (NewSilenceFromMatchersAndCommentAndTime defaultCreator params.matchers params.comment >> MsgForSilenceForm) Time.now )\n\n        NewSilenceFromMatchersAndCommentAndTime defaultCreator matchers comment time ->\n            ( { form = fromMatchersAndCommentAndTime defaultCreator comment time model.firstDayOfWeek\n              , alerts = Initial\n              , activeAlertId = Nothing\n              , silenceId = Initial\n              , filterBar = FilterBar.initFilterBar matchers\n              , filterBarValid = Utils.FormValidation.Initial\n              , key = model.key\n              , firstDayOfWeek = model.firstDayOfWeek\n              }\n            , Cmd.none\n            )\n\n        FetchSilence silenceId ->\n            ( model, Silences.Api.getSilence apiUrl silenceId (SilenceFetch >> MsgForSilenceForm) )\n\n        SilenceFetch (Success silence) ->\n            ( { form = fromSilence silence model.firstDayOfWeek\n              , filterBar = FilterBar.initFilterBar (List.map Utils.Filter.fromApiMatcher silence.matchers)\n              , filterBarValid = Utils.FormValidation.Initial\n              , silenceId = model.silenceId\n              , alerts = Initial\n              , activeAlertId = Nothing\n              , key = model.key\n              , firstDayOfWeek = model.firstDayOfWeek\n              }\n            , Task.perform identity (Task.succeed (MsgForSilenceForm PreviewSilence))\n            )\n\n        SilenceFetch _ ->\n            ( model, Cmd.none )\n\n        PreviewSilence ->\n            case toSilence model.filterBar model.form of\n                Just silence ->\n                    ( { model | alerts = Loading }\n                    , Alerts.Api.fetchAlerts\n                        apiUrl\n                        (silencePreviewFilter silence.matchers)\n                        |> Cmd.map (AlertGroupsPreview >> MsgForSilenceForm)\n                    )\n\n                Nothing ->\n                    ( { model\n                        | alerts = Failure \"Cannot display affected Alerts, Silence is not yet valid.\"\n                        , form = validateForm model.form\n                        , filterBarValid = validateMatchers model.filterBar\n                      }\n                    , Cmd.none\n                    )\n\n        AlertGroupsPreview alerts ->\n            ( { model | alerts = alerts }\n            , Cmd.none\n            )\n\n        SetActiveAlert maybeAlertId ->\n            ( { model | activeAlertId = maybeAlertId }\n            , Cmd.none\n            )\n\n        UpdateField fieldMsg ->\n            ( { model\n                | form = updateForm fieldMsg model.form\n                , alerts = Initial\n                , silenceId = Initial\n              }\n            , Cmd.none\n            )\n\n        UpdateDateTimePicker subMsg ->\n            let\n                newPicker =\n                    DateTimePickerUpdates.update subMsg model.form.dateTimePicker\n            in\n            ( { model\n                | form = fromDateTimePicker model.form newPicker\n              }\n            , Cmd.none\n            )\n\n        MsgForFilterBar subMsg ->\n            let\n                ( newFilterBar, _, subCmd ) =\n                    FilterBar.update subMsg model.filterBar\n            in\n            ( { model | filterBar = newFilterBar, filterBarValid = Utils.FormValidation.Initial }\n            , Cmd.map (MsgForFilterBar >> MsgForSilenceForm) subCmd\n            )\n\n        UpdateFirstDayOfWeek firstDayOfWeek ->\n            ( { model\n                | firstDayOfWeek = firstDayOfWeek\n              }\n            , Cmd.none\n            )\n\n\nport persistDefaultCreator : String -> Cmd msg\n"
  },
  {
    "path": "ui/app/src/Views/SilenceForm/Views.elm",
    "content": "module Views.SilenceForm.Views exposing (view)\n\nimport Data.GettableAlert exposing (GettableAlert)\nimport Html exposing (Html, button, div, h1, i, input, label, strong, text)\nimport Html.Attributes exposing (class, style)\nimport Html.Events exposing (onClick)\nimport Utils.DateTimePicker.Views exposing (viewDateTimePicker)\nimport Utils.Filter exposing (SilenceFormGetParams)\nimport Utils.FormValidation exposing (ValidatedField, ValidationState(..))\nimport Utils.Types exposing (ApiData)\nimport Utils.Views exposing (loading, validatedField, validatedTextareaField)\nimport Views.FilterBar.Types as FilterBar\nimport Views.FilterBar.Views as FilterBar\nimport Views.Shared.SilencePreview\nimport Views.SilenceForm.Types exposing (Model, SilenceForm, SilenceFormFieldMsg(..), SilenceFormMsg(..))\n\n\nview : Maybe String -> SilenceFormGetParams -> String -> Model -> Html SilenceFormMsg\nview maybeId silenceFormGetParams defaultCreator { form, filterBar, filterBarValid, silenceId, alerts, activeAlertId } =\n    let\n        ( title, resetClick ) =\n            case maybeId of\n                Just silenceId_ ->\n                    ( \"Edit Silence\", FetchSilence silenceId_ )\n\n                Nothing ->\n                    ( \"New Silence\", NewSilenceFromMatchersAndComment defaultCreator silenceFormGetParams )\n    in\n    div []\n        [ h1 [] [ text title ]\n        , timeInput form.startsAt form.endsAt form.duration\n        , matchersInput filterBarValid filterBar\n        , validatedField input\n            \"Creator\"\n            inputSectionPadding\n            (UpdateCreatedBy >> UpdateField)\n            (ValidateCreatedBy |> UpdateField)\n            form.createdBy\n        , validatedTextareaField\n            \"Comment\"\n            inputSectionPadding\n            (UpdateComment >> UpdateField)\n            (ValidateComment |> UpdateField)\n            form.comment\n        , div [ class inputSectionPadding ]\n            [ informationBlock activeAlertId silenceId alerts\n            , silenceActionButtons maybeId resetClick\n            ]\n        , dateTimePickerDialog form\n        ]\n\n\ndateTimePickerDialog : SilenceForm -> Html SilenceFormMsg\ndateTimePickerDialog form =\n    if form.viewDateTimePicker then\n        div []\n            [ div [ class \"modal fade show\", style \"display\" \"block\" ]\n                [ div [ class \"modal-dialog modal-dialog-centered\" ]\n                    [ div [ class \"modal-content\" ]\n                        [ div [ class \"modal-header\" ]\n                            [ button\n                                [ class \"close ml-auto\"\n                                , onClick (CloseDateTimePicker |> UpdateField)\n                                ]\n                                [ text \"x\" ]\n                            ]\n                        , div [ class \"modal-body\" ]\n                            [ viewDateTimePicker form.dateTimePicker |> Html.map UpdateDateTimePicker ]\n                        , div [ class \"modal-footer\" ]\n                            [ button\n                                [ class \"ml-2 btn btn-outline-success mr-auto\"\n                                , onClick (CloseDateTimePicker |> UpdateField)\n                                ]\n                                [ text \"Cancel\" ]\n                            , button\n                                [ class \"ml-2 btn btn-primary\"\n                                , onClick (UpdateTimesFromPicker |> UpdateField)\n                                ]\n                                [ text \"Set Date/Time\" ]\n                            ]\n                        ]\n                    ]\n                ]\n            , div [ class \"modal-backdrop fade show\" ] []\n            ]\n\n    else\n        div [ style \"clip\" \"rect(0,0,0,0)\", style \"position\" \"fixed\" ]\n            [ div [ class \"modal fade\" ] []\n            , div [ class \"modal-backdrop fade\" ] []\n            ]\n\n\ninputSectionPadding : String\ninputSectionPadding =\n    \"mt-5\"\n\n\ntimeInput : ValidatedField -> ValidatedField -> ValidatedField -> Html SilenceFormMsg\ntimeInput startsAt endsAt duration =\n    div [ class <| \"row \" ++ inputSectionPadding ]\n        [ validatedField input\n            \"Start\"\n            \"col-lg-4 col-6\"\n            (UpdateStartsAt >> UpdateField)\n            (ValidateTime |> UpdateField)\n            startsAt\n        , validatedField input\n            \"Duration\"\n            \"col-lg-3 col-6\"\n            (UpdateDuration >> UpdateField)\n            (ValidateTime |> UpdateField)\n            duration\n        , validatedField input\n            \"End\"\n            \"col-lg-4 col-6\"\n            (UpdateEndsAt >> UpdateField)\n            (ValidateTime |> UpdateField)\n            endsAt\n        , div\n            [ class \"form-group col-lg-1 col-6\" ]\n            [ label\n                []\n                [ text \"\\u{00A0}\" ]\n            , button\n                [ class \"form-control btn btn-outline-primary cursor-pointer\"\n                , onClick (OpenDateTimePicker |> UpdateField)\n                ]\n                [ i\n                    [ class \"fa fa-calendar\"\n                    ]\n                    []\n                ]\n            ]\n        ]\n\n\nmatchersInput : Utils.FormValidation.ValidationState -> FilterBar.Model -> Html SilenceFormMsg\nmatchersInput filterBarValid filterBar =\n    let\n        errorClass =\n            case filterBarValid of\n                Invalid _ ->\n                    \" has-danger\"\n\n                _ ->\n                    \"\"\n    in\n    div [ class (inputSectionPadding ++ errorClass) ]\n        [ label [ Html.Attributes.for \"filter-bar-matcher\" ]\n            [ strong [] [ text \"Matchers \" ]\n            , text \"Alerts affected by this silence\"\n            ]\n        , FilterBar.view { showSilenceButton = False } filterBar |> Html.map MsgForFilterBar\n        , case filterBarValid of\n            Invalid error ->\n                div [ class \"form-control-feedback\" ] [ text error ]\n\n            _ ->\n                text \"\"\n        ]\n\n\ninformationBlock : Maybe String -> ApiData String -> ApiData (List GettableAlert) -> Html SilenceFormMsg\ninformationBlock activeAlertId silence alerts =\n    case silence of\n        Utils.Types.Success _ ->\n            text \"\"\n\n        Utils.Types.Initial ->\n            Views.Shared.SilencePreview.view activeAlertId alerts\n                |> Html.map SetActiveAlert\n\n        Utils.Types.Failure error ->\n            Utils.Views.error error\n\n        Utils.Types.Loading ->\n            loading\n\n\nsilenceActionButtons : Maybe String -> SilenceFormMsg -> Html SilenceFormMsg\nsilenceActionButtons maybeId resetClick =\n    div [ class (\"mb-4 \" ++ inputSectionPadding) ]\n        [ previewSilenceBtn\n        , createSilenceBtn maybeId\n        , button\n            [ class \"ml-2 btn btn-danger\", onClick resetClick ]\n            [ text \"Reset\" ]\n        ]\n\n\ncreateSilenceBtn : Maybe String -> Html SilenceFormMsg\ncreateSilenceBtn maybeId =\n    let\n        btnTxt =\n            case maybeId of\n                Just _ ->\n                    \"Update\"\n\n                Nothing ->\n                    \"Create\"\n    in\n    button\n        [ class \"ml-2 btn btn-primary\"\n        , onClick CreateSilence\n        ]\n        [ text btnTxt ]\n\n\npreviewSilenceBtn : Html SilenceFormMsg\npreviewSilenceBtn =\n    button\n        [ class \"btn btn-outline-success\"\n        , onClick PreviewSilence\n        ]\n        [ text \"Preview Alerts\" ]\n"
  },
  {
    "path": "ui/app/src/Views/SilenceList/Parsing.elm",
    "content": "module Views.SilenceList.Parsing exposing (silenceListParser)\n\nimport Url.Parser exposing ((<?>), Parser, map, s)\nimport Url.Parser.Query as Query\nimport Utils.Filter exposing (Filter)\n\n\nsilenceListParser : Parser (Filter -> a) a\nsilenceListParser =\n    map\n        (\\t ->\n            Filter t Nothing False Nothing Nothing Nothing Nothing Nothing\n        )\n        (s \"silences\" <?> Query.string \"filter\")\n"
  },
  {
    "path": "ui/app/src/Views/SilenceList/SilenceView.elm",
    "content": "module Views.SilenceList.SilenceView exposing (editButton, view)\n\nimport Data.GettableSilence exposing (GettableSilence)\nimport Data.Matcher exposing (Matcher)\nimport Data.SilenceStatus exposing (State(..))\nimport Html exposing (Html, a, button, div, li, span, text)\nimport Html.Attributes exposing (class, href, style)\nimport Html.Events exposing (onClick)\nimport Time exposing (Posix)\nimport Types exposing (Msg(..))\nimport Utils.Date\nimport Utils.Filter\nimport Utils.List\nimport Utils.Views\nimport Views.FilterBar.Types as FilterBarTypes\nimport Views.Shared.Dialog as Dialog\nimport Views.SilenceForm.Parsing exposing (newSilenceFromMatchersAndComment)\nimport Views.SilenceList.Types exposing (SilenceListMsg(..))\n\n\nview : Bool -> GettableSilence -> Html Msg\nview showConfirmationDialog silence =\n    li\n        [ -- speedup rendering in Chrome, because list-group-item className\n          -- creates a new layer in the rendering engine\n          style \"position\" \"static\"\n        , class \"align-items-start list-group-item border-0 p-0 mb-4\"\n        ]\n        [ div [ class \"w-100 mb-2 d-flex align-items-start\" ]\n            [ case silence.status.state of\n                Active ->\n                    dateView \"Ends\" silence.endsAt\n\n                Pending ->\n                    dateView \"Starts\" silence.startsAt\n\n                Expired ->\n                    dateView \"Expired\" silence.endsAt\n            , detailsButton silence.id\n            , editButton silence\n            , deleteButton silence\n            ]\n        , div [ class \"\" ] (List.map matcherButton silence.matchers)\n        , Dialog.view\n            (if showConfirmationDialog then\n                Just (confirmSilenceDeleteView silence False)\n\n             else\n                Nothing\n            )\n        ]\n\n\nconfirmSilenceDeleteView : GettableSilence -> Bool -> Dialog.Config Msg\nconfirmSilenceDeleteView silence refresh =\n    { onClose = MsgForSilenceList Views.SilenceList.Types.FetchSilences\n    , title = \"Expire Silence\"\n    , body = text \"Are you sure you want to expire this silence?\"\n    , footer =\n        button\n            [ class \"btn btn-primary\"\n            , onClick (MsgForSilenceList (Views.SilenceList.Types.DestroySilence silence refresh))\n            ]\n            [ text \"Confirm\" ]\n    }\n\n\ndateView : String -> Posix -> Html Msg\ndateView string time =\n    span\n        [ class \"text-muted align-self-center mr-2\"\n        ]\n        [ text (string ++ \" \" ++ Utils.Date.dateTimeFormat time)\n        ]\n\n\nmatcherButton : Matcher -> Html Msg\nmatcherButton matcher =\n    let\n        isEqual =\n            case matcher.isEqual of\n                Nothing ->\n                    True\n\n                Just value ->\n                    value\n\n        op =\n            if not matcher.isRegex && isEqual then\n                Utils.Filter.Eq\n\n            else if not matcher.isRegex && not isEqual then\n                Utils.Filter.NotEq\n\n            else if matcher.isRegex && isEqual then\n                Utils.Filter.RegexMatch\n\n            else\n                Utils.Filter.NotRegexMatch\n\n        msg =\n            FilterBarTypes.AddFilterMatcher False\n                { key = matcher.name\n                , op = op\n                , value = matcher.value\n                }\n                |> MsgForFilterBar\n                |> MsgForSilenceList\n    in\n    Utils.Views.labelButton (Just msg) (Utils.List.mstring matcher)\n\n\neditButton : GettableSilence -> Html Msg\neditButton silence =\n    let\n        editUrl =\n            String.join \"/\" [ \"#/silences\", silence.id, \"edit\" ]\n\n        default =\n            a [ class \"btn btn-outline-info border-0\", href editUrl ]\n                [ text \"Edit\"\n                ]\n    in\n    case silence.status.state of\n        -- If the silence is expired, do not edit it, but instead create a new\n        -- one with the old matchers\n        Expired ->\n            a\n                [ class \"btn btn-outline-info border-0\"\n                , href (newSilenceFromMatchersAndComment silence.matchers silence.comment)\n                ]\n                [ text \"Recreate\"\n                ]\n\n        _ ->\n            default\n\n\ndeleteButton : GettableSilence -> Html Msg\ndeleteButton silence =\n    case silence.status.state of\n        Expired ->\n            text \"\"\n\n        Active ->\n            button\n                [ class \"btn btn-outline-danger border-0\"\n                , onClick (MsgForSilenceList (ConfirmDestroySilence silence))\n                ]\n                [ text \"Expire\"\n                ]\n\n        Pending ->\n            button\n                [ class \"btn btn-outline-danger border-0\"\n                , onClick (MsgForSilenceList (ConfirmDestroySilence silence))\n                ]\n                [ text \"Delete\"\n                ]\n\n\ndetailsButton : String -> Html Msg\ndetailsButton id =\n    a [ class \"btn btn-outline-info border-0\", href (\"#/silences/\" ++ id) ]\n        [ text \"View\"\n        ]\n"
  },
  {
    "path": "ui/app/src/Views/SilenceList/Types.elm",
    "content": "module Views.SilenceList.Types exposing (Model, SilenceListMsg(..), SilenceTab, initSilenceList)\n\nimport Browser.Navigation exposing (Key)\nimport Data.GettableSilence exposing (GettableSilence)\nimport Data.SilenceStatus exposing (State(..))\nimport Utils.Types exposing (ApiData(..))\nimport Views.FilterBar.Types as FilterBar\n\n\ntype SilenceListMsg\n    = ConfirmDestroySilence GettableSilence\n    | DestroySilence GettableSilence Bool\n    | SilencesFetch (ApiData (List GettableSilence))\n    | FetchSilences\n    | MsgForFilterBar FilterBar.Msg\n    | SetTab State\n\n\ntype alias SilenceTab =\n    { silences : List GettableSilence\n    , tab : State\n    , count : Int\n    }\n\n\ntype alias Model =\n    { silences : ApiData (List SilenceTab)\n    , filterBar : FilterBar.Model\n    , tab : State\n    , showConfirmationDialog : Maybe String\n    , key : Key\n    }\n\n\ninitSilenceList : Key -> Model\ninitSilenceList key =\n    { silences = Initial\n    , filterBar = FilterBar.initFilterBar []\n    , tab = Active\n    , showConfirmationDialog = Nothing\n    , key = key\n    }\n"
  },
  {
    "path": "ui/app/src/Views/SilenceList/Updates.elm",
    "content": "module Views.SilenceList.Updates exposing (update)\n\nimport Browser.Navigation as Navigation\nimport Data.GettableSilence exposing (GettableSilence)\nimport Data.SilenceStatus exposing (State(..))\nimport Silences.Api as Api\nimport Utils.Api as ApiData\nimport Utils.Filter exposing (Filter)\nimport Utils.Types exposing (ApiData(..))\nimport Views.FilterBar.Updates as FilterBar\nimport Views.SilenceList.Types exposing (Model, SilenceListMsg(..), SilenceTab)\n\n\nupdate : SilenceListMsg -> Model -> Filter -> String -> String -> ( Model, Cmd SilenceListMsg )\nupdate msg model filter basePath apiUrl =\n    case msg of\n        SilencesFetch fetchedSilences ->\n            ( { model\n                | silences =\n                    ApiData.map\n                        (\\silences -> List.map (groupSilencesByState silences) states)\n                        fetchedSilences\n              }\n            , Cmd.none\n            )\n\n        FetchSilences ->\n            ( { model\n                | filterBar = FilterBar.setMatchers filter model.filterBar\n                , silences = Loading\n                , showConfirmationDialog = Nothing\n              }\n            , Api.getSilences apiUrl filter SilencesFetch\n            )\n\n        ConfirmDestroySilence silence ->\n            ( { model | showConfirmationDialog = Just silence.id }\n            , Cmd.none\n            )\n\n        DestroySilence silence refresh ->\n            -- TODO: \"Deleted id: ID\" growl\n            -- TODO: Check why POST isn't there but is accepted\n            ( { model | silences = Loading, showConfirmationDialog = Nothing }\n            , Cmd.batch\n                [ Api.destroy apiUrl silence (always FetchSilences)\n                , if refresh then\n                    Navigation.pushUrl model.key (basePath ++ \"#/silences\")\n\n                  else\n                    Cmd.none\n                ]\n            )\n\n        MsgForFilterBar subMsg ->\n            let\n                ( newFilterBar, shouldFilter, cmd ) =\n                    FilterBar.update subMsg model.filterBar\n\n                filterBarCmd =\n                    Cmd.map MsgForFilterBar cmd\n\n                newUrl =\n                    Utils.Filter.toUrl (basePath ++ \"#/silences\")\n                        (Utils.Filter.withMatchers newFilterBar.matchers filter)\n\n                silencesCmd =\n                    if shouldFilter then\n                        Cmd.batch\n                            [ Navigation.pushUrl model.key newUrl\n                            , filterBarCmd\n                            ]\n\n                    else\n                        filterBarCmd\n            in\n            ( { model | filterBar = newFilterBar }, silencesCmd )\n\n        SetTab tab ->\n            ( { model | tab = tab }, Cmd.none )\n\n\ngroupSilencesByState : List GettableSilence -> State -> SilenceTab\ngroupSilencesByState silences state =\n    let\n        silencesInTab =\n            filterSilencesByState state silences\n    in\n    { tab = state\n    , silences = silencesInTab\n    , count = List.length silencesInTab\n    }\n\n\nstates : List State\nstates =\n    [ Active, Pending, Expired ]\n\n\nfilterSilencesByState : State -> List GettableSilence -> List GettableSilence\nfilterSilencesByState state =\n    List.filter (filterSilenceByState state)\n\n\nfilterSilenceByState : State -> GettableSilence -> Bool\nfilterSilenceByState state silence =\n    silence.status.state == state\n"
  },
  {
    "path": "ui/app/src/Views/SilenceList/Views.elm",
    "content": "module Views.SilenceList.Views exposing (view)\n\nimport Data.SilenceStatus exposing (State(..))\nimport Html exposing (..)\nimport Html.Attributes exposing (..)\nimport Html.Keyed\nimport Html.Lazy exposing (lazy2, lazy3)\nimport Silences.Types exposing (stateToString)\nimport Types exposing (Msg(..))\nimport Utils.String as StringUtils\nimport Utils.Types exposing (ApiData(..))\nimport Utils.Views exposing (error, loading)\nimport Views.FilterBar.Views as FilterBar\nimport Views.SilenceList.SilenceView\nimport Views.SilenceList.Types exposing (Model, SilenceListMsg(..), SilenceTab)\n\n\nview : Model -> Html Msg\nview { filterBar, tab, silences, showConfirmationDialog } =\n    div []\n        [ div [ class \"mb-4\" ]\n            [ label [ class \"mb-2\", for \"filter-bar-matcher\" ] [ text \"Filter\" ]\n            , Html.map (MsgForFilterBar >> MsgForSilenceList) (FilterBar.view { showSilenceButton = False } filterBar)\n            ]\n        , lazy2 tabsView tab silences\n        , lazy3 silencesView showConfirmationDialog tab silences\n        ]\n\n\ntabsView : State -> ApiData (List SilenceTab) -> Html Msg\ntabsView currentTab tabs =\n    case tabs of\n        Success silencesTabs ->\n            List.map (\\{ tab, count } -> tabView currentTab count tab) silencesTabs\n                |> ul [ class \"nav nav-tabs mb-4\" ]\n\n        _ ->\n            List.map (tabView currentTab 0) states\n                |> ul [ class \"nav nav-tabs mb-4\" ]\n\n\ntabView : State -> Int -> State -> Html Msg\ntabView currentTab count tab =\n    Utils.Views.tab tab currentTab (SetTab >> MsgForSilenceList) <|\n        case count of\n            0 ->\n                [ text (StringUtils.capitalizeFirst (stateToString tab)) ]\n\n            n ->\n                [ text (StringUtils.capitalizeFirst (stateToString tab))\n                , span\n                    [ class \"badge badge-pillow badge-default align-text-top ml-2\" ]\n                    [ text (String.fromInt n) ]\n                ]\n\n\nsilencesView : Maybe String -> State -> ApiData (List SilenceTab) -> Html Msg\nsilencesView showConfirmationDialog tab silencesTab =\n    case silencesTab of\n        Success tabs ->\n            tabs\n                |> List.filter (.tab >> (==) tab)\n                |> List.head\n                |> Maybe.map .silences\n                |> Maybe.withDefault []\n                |> (\\silences ->\n                        if List.isEmpty silences then\n                            Utils.Views.error \"No silences found\"\n\n                        else\n                            Html.Keyed.ul [ class \"list-group\" ]\n                                (List.map\n                                    (\\silence ->\n                                        ( silence.id\n                                        , Views.SilenceList.SilenceView.view\n                                            (showConfirmationDialog == Just silence.id)\n                                            silence\n                                        )\n                                    )\n                                    silences\n                                )\n                   )\n\n        Failure msg ->\n            error msg\n\n        _ ->\n            loading\n\n\nstates : List State\nstates =\n    [ Active, Pending, Expired ]\n"
  },
  {
    "path": "ui/app/src/Views/SilenceView/Parsing.elm",
    "content": "module Views.SilenceView.Parsing exposing (silenceViewParser)\n\nimport Url.Parser exposing ((</>), Parser, s, string)\n\n\nsilenceViewParser : Parser (String -> a) a\nsilenceViewParser =\n    s \"silences\" </> string\n"
  },
  {
    "path": "ui/app/src/Views/SilenceView/Types.elm",
    "content": "module Views.SilenceView.Types exposing (Model, SilenceViewMsg(..), initSilenceView)\n\nimport Browser.Navigation exposing (Key)\nimport Data.GettableAlert exposing (GettableAlert)\nimport Data.GettableSilence exposing (GettableSilence)\nimport Utils.Types exposing (ApiData(..))\n\n\ntype SilenceViewMsg\n    = SilenceFetched (ApiData GettableSilence)\n    | SetActiveAlert (Maybe String)\n    | AlertGroupsPreview (ApiData (List GettableAlert))\n    | InitSilenceView String\n    | ConfirmDestroySilence\n    | Reload String\n\n\ntype alias Model =\n    { silence : ApiData GettableSilence\n    , alerts : ApiData (List GettableAlert)\n    , activeAlertId : Maybe String\n    , showConfirmationDialog : Bool\n    , key : Key\n    }\n\n\ninitSilenceView : Key -> Model\ninitSilenceView key =\n    { silence = Initial\n    , alerts = Initial\n    , activeAlertId = Nothing\n    , showConfirmationDialog = False\n    , key = key\n    }\n"
  },
  {
    "path": "ui/app/src/Views/SilenceView/Updates.elm",
    "content": "module Views.SilenceView.Updates exposing (update)\n\nimport Alerts.Api\nimport Browser.Navigation as Navigation\nimport Silences.Api exposing (getSilence)\nimport Utils.Filter exposing (silencePreviewFilter)\nimport Utils.Types exposing (ApiData(..))\nimport Views.SilenceView.Types exposing (Model, SilenceViewMsg(..))\n\n\nupdate : SilenceViewMsg -> Model -> String -> ( Model, Cmd SilenceViewMsg )\nupdate msg model apiUrl =\n    case msg of\n        AlertGroupsPreview alerts ->\n            ( { model | alerts = alerts }\n            , Cmd.none\n            )\n\n        SetActiveAlert activeAlertId ->\n            ( { model | activeAlertId = activeAlertId }\n            , Cmd.none\n            )\n\n        SilenceFetched (Success silence) ->\n            ( { model\n                | silence = Success silence\n                , alerts = Loading\n              }\n            , Alerts.Api.fetchAlerts\n                apiUrl\n                (silencePreviewFilter silence.matchers)\n                |> Cmd.map AlertGroupsPreview\n            )\n\n        ConfirmDestroySilence ->\n            ( { model | showConfirmationDialog = True }\n            , Cmd.none\n            )\n\n        SilenceFetched silence ->\n            ( { model | silence = silence, alerts = Initial }, Cmd.none )\n\n        InitSilenceView silenceId ->\n            ( { model | showConfirmationDialog = False }, getSilence apiUrl silenceId SilenceFetched )\n\n        Reload silenceId ->\n            ( { model | showConfirmationDialog = False }, Navigation.pushUrl model.key (\"#/silences/\" ++ silenceId) )\n"
  },
  {
    "path": "ui/app/src/Views/SilenceView/Views.elm",
    "content": "module Views.SilenceView.Views exposing (view)\n\nimport Data.GettableAlert exposing (GettableAlert)\nimport Data.GettableSilence exposing (GettableSilence)\nimport Data.SilenceStatus\nimport Html exposing (Html, b, button, div, h1, label, span, text)\nimport Html.Attributes exposing (class)\nimport Html.Events exposing (onClick)\nimport Silences.Types exposing (stateToString)\nimport Types exposing (Msg(..))\nimport Utils.Date exposing (dateTimeFormat)\nimport Utils.List\nimport Utils.Types exposing (ApiData(..))\nimport Utils.Views exposing (error, loading)\nimport Views.Shared.Dialog as Dialog\nimport Views.Shared.SilencePreview\nimport Views.SilenceList.SilenceView exposing (editButton)\nimport Views.SilenceList.Types exposing (SilenceListMsg(..))\nimport Views.SilenceView.Types as SilenceViewTypes exposing (Model)\n\n\nview : Model -> Html Msg\nview { silence, alerts, activeAlertId, showConfirmationDialog } =\n    case silence of\n        Success sil ->\n            if showConfirmationDialog then\n                viewSilence activeAlertId alerts sil True\n\n            else\n                viewSilence activeAlertId alerts sil False\n\n        Initial ->\n            loading\n\n        Loading ->\n            loading\n\n        Failure msg ->\n            error msg\n\n\nviewSilence : Maybe String -> ApiData (List GettableAlert) -> GettableSilence -> Bool -> Html Msg\nviewSilence activeAlertId alerts silence showPromptDialog =\n    let\n        affectedAlerts =\n            Views.Shared.SilencePreview.view activeAlertId alerts\n                |> Html.map (\\msg -> MsgForSilenceView (SilenceViewTypes.SetActiveAlert msg))\n    in\n    div []\n        [ h1 []\n            [ text \"Silence\"\n            , span\n                [ class \"ml-3\" ]\n                [ editButton silence\n                , expireButton silence\n                ]\n            ]\n        , formGroup \"ID\" <| text silence.id\n        , formGroup \"Starts at\" <| text <| dateTimeFormat silence.startsAt\n        , formGroup \"Ends at\" <| text <| dateTimeFormat silence.endsAt\n        , formGroup \"Updated at\" <| text <| dateTimeFormat silence.updatedAt\n        , formGroup \"Created by\" <| text <| silence.createdBy\n        , formGroup \"Comment\" <| text silence.comment\n        , formGroup \"State\" <| text <| stateToString silence.status.state\n        , formGroup \"Matchers\" <|\n            div [] <|\n                List.map (Utils.List.mstring >> Utils.Views.labelButton Nothing) silence.matchers\n        , affectedAlerts\n        , Dialog.view\n            (if showPromptDialog then\n                Just (confirmSilenceDeleteView silence True)\n\n             else\n                Nothing\n            )\n        ]\n\n\nconfirmSilenceDeleteView : GettableSilence -> Bool -> Dialog.Config Msg\nconfirmSilenceDeleteView silence refresh =\n    { onClose = MsgForSilenceView (SilenceViewTypes.Reload <| silence.id)\n    , title = \"Expire Silence\"\n    , body = text \"Are you sure you want to expire this silence?\"\n    , footer =\n        button\n            [ class \"btn btn-primary\"\n            , onClick (MsgForSilenceList (DestroySilence silence refresh))\n            ]\n            [ text \"Confirm\" ]\n    }\n\n\nformGroup : String -> Html Msg -> Html Msg\nformGroup key content =\n    div [ class \"form-group row\" ]\n        [ label [ class \"col-2 col-form-label\" ] [ b [] [ text key ] ]\n        , div [ class \"col-10 d-flex align-items-center\" ]\n            [ content\n            ]\n        ]\n\n\nexpireButton : GettableSilence -> Html Msg\nexpireButton silence =\n    case silence.status.state of\n        Data.SilenceStatus.Expired ->\n            text \"\"\n\n        Data.SilenceStatus.Active ->\n            button\n                [ class \"btn btn-outline-danger border-0\"\n                , onClick (MsgForSilenceView SilenceViewTypes.ConfirmDestroySilence)\n                ]\n                [ text \"Expire\"\n                ]\n\n        Data.SilenceStatus.Pending ->\n            button\n                [ class \"btn btn-outline-danger border-0\"\n                , onClick (MsgForSilenceView SilenceViewTypes.ConfirmDestroySilence)\n                ]\n                [ text \"Delete\"\n                ]\n"
  },
  {
    "path": "ui/app/src/Views/Status/Parsing.elm",
    "content": "module Views.Status.Parsing exposing (statusParser)\n\nimport Url.Parser exposing (Parser, s)\n\n\nstatusParser : Parser a a\nstatusParser =\n    s \"status\"\n"
  },
  {
    "path": "ui/app/src/Views/Status/Types.elm",
    "content": "module Views.Status.Types exposing (StatusModel, StatusMsg(..), initStatusModel)\n\nimport Data.AlertmanagerStatus exposing (AlertmanagerStatus)\nimport Utils.Types exposing (ApiData(..))\n\n\ntype StatusMsg\n    = NewStatus (ApiData AlertmanagerStatus)\n      -- String carries the api url.\n    | InitStatusView String\n\n\ntype alias StatusModel =\n    { statusInfo : ApiData AlertmanagerStatus\n    }\n\n\ninitStatusModel : StatusModel\ninitStatusModel =\n    { statusInfo = Initial }\n"
  },
  {
    "path": "ui/app/src/Views/Status/Updates.elm",
    "content": "module Views.Status.Updates exposing (update)\n\nimport Status.Api exposing (getStatus)\nimport Types exposing (Model, Msg(..))\nimport Views.Status.Types exposing (StatusMsg(..))\n\n\nupdate : StatusMsg -> Model -> ( Model, Cmd Msg )\nupdate msg model =\n    case msg of\n        NewStatus apiResponse ->\n            ( { model | status = { statusInfo = apiResponse } }, Cmd.none )\n\n        InitStatusView apiUrl ->\n            ( model, getStatus apiUrl (NewStatus >> MsgForStatus) )\n"
  },
  {
    "path": "ui/app/src/Views/Status/Views.elm",
    "content": "module Views.Status.Views exposing (view)\n\nimport Data.AlertmanagerStatus exposing (AlertmanagerStatus)\nimport Data.ClusterStatus exposing (ClusterStatus, Status(..))\nimport Data.PeerStatus exposing (PeerStatus)\nimport Data.VersionInfo exposing (VersionInfo)\nimport Html exposing (..)\nimport Html.Attributes exposing (class, classList, style)\nimport Status.Api exposing (clusterStatusToString)\nimport Status.Types exposing (VersionInfo)\nimport Types\nimport Utils.Date exposing (timeToString)\nimport Utils.Types exposing (ApiData(..))\nimport Utils.Views\nimport Views.Status.Types exposing (StatusModel)\n\n\nview : StatusModel -> Html Types.Msg\nview { statusInfo } =\n    Utils.Views.apiData viewStatusInfo statusInfo\n\n\nviewStatusInfo : AlertmanagerStatus -> Html Types.Msg\nviewStatusInfo status =\n    div []\n        [ h1 [] [ text \"Status\" ]\n        , div [ class \"form-group row\" ]\n            [ b [ class \"col-sm-2\" ] [ text \"Uptime:\" ]\n            , div [ class \"col-sm-10\" ] [ text <| timeToString status.uptime ]\n            ]\n        , viewClusterStatus status.cluster\n        , viewVersionInformation status.versionInfo\n        , viewConfig status.config.original\n        ]\n\n\nviewConfig : String -> Html Types.Msg\nviewConfig config =\n    div []\n        [ h2 [] [ text \"Config\" ]\n        , pre [ class \"p-4\", style \"background\" \"#f7f7f9\", style \"font-family\" \"monospace\" ]\n            [ code []\n                [ text config\n                ]\n            ]\n        ]\n\n\nviewClusterStatus : ClusterStatus -> Html Types.Msg\nviewClusterStatus { name, status, peers } =\n    span []\n        [ h2 [] [ text \"Cluster Status\" ]\n        , case name of\n            Just n ->\n                div [ class \"form-group row\" ]\n                    [ b [ class \"col-sm-2\" ] [ text \"Name:\" ]\n                    , div [ class \"col-sm-10\" ] [ text n ]\n                    ]\n\n            Nothing ->\n                text \"\"\n        , div [ class \"form-group row\" ]\n            [ b [ class \"col-sm-2\" ] [ text \"Status:\" ]\n            , div [ class \"col-sm-10\" ]\n                [ span\n                    [ classList\n                        [ ( \"badge\", True )\n                        , case status of\n                            Ready ->\n                                ( \"badge-success\", True )\n\n                            Settling ->\n                                ( \"badge-warning\", True )\n\n                            Disabled ->\n                                ( \"badge-danger\", True )\n                        ]\n                    ]\n                    [ text <| clusterStatusToString status ]\n                ]\n            ]\n        , case peers of\n            Just p ->\n                div [ class \"form-group row\" ]\n                    [ b [ class \"col-sm-2\" ] [ text \"Peers:\" ]\n                    , ul [ class \"col-sm-10\" ] <|\n                        List.map viewClusterPeer p\n                    ]\n\n            Nothing ->\n                text \"\"\n        ]\n\n\nviewClusterPeer : PeerStatus -> Html Types.Msg\nviewClusterPeer peer =\n    li []\n        [ div [ class \"\" ]\n            [ b [ class \"\" ] [ text \"Name: \" ]\n            , text peer.name\n            ]\n        , div [ class \"\" ]\n            [ b [ class \"\" ] [ text \"Address: \" ]\n            , text peer.address\n            ]\n        ]\n\n\nviewVersionInformation : VersionInfo -> Html Types.Msg\nviewVersionInformation versionInfo =\n    span []\n        [ h2 [] [ text \"Version Information\" ]\n        , div [ class \"form-group row\" ]\n            [ b [ class \"col-sm-2\" ] [ text \"Branch:\" ], div [ class \"col-sm-10\" ] [ text versionInfo.branch ] ]\n        , div [ class \"form-group row\" ]\n            [ b [ class \"col-sm-2\" ] [ text \"BuildDate:\" ], div [ class \"col-sm-10\" ] [ text versionInfo.buildDate ] ]\n        , div [ class \"form-group row\" ]\n            [ b [ class \"col-sm-2\" ] [ text \"BuildUser:\" ], div [ class \"col-sm-10\" ] [ text versionInfo.buildUser ] ]\n        , div [ class \"form-group row\" ]\n            [ b [ class \"col-sm-2\" ] [ text \"GoVersion:\" ], div [ class \"col-sm-10\" ] [ text versionInfo.goVersion ] ]\n        , div [ class \"form-group row\" ]\n            [ b [ class \"col-sm-2\" ] [ text \"Revision:\" ], div [ class \"col-sm-10\" ] [ text versionInfo.revision ] ]\n        , div [ class \"form-group row\" ]\n            [ b [ class \"col-sm-2\" ] [ text \"Version:\" ], div [ class \"col-sm-10\" ] [ text versionInfo.version ] ]\n        ]\n"
  },
  {
    "path": "ui/app/src/Views.elm",
    "content": "module Views exposing (view)\n\nimport Html exposing (Html, div, node, text)\nimport Html.Attributes exposing (class, href, rel, style)\nimport Html.Events exposing (on)\nimport Json.Decode exposing (succeed)\nimport Types exposing (Model, Msg(..), Route(..))\nimport Utils.Filter exposing (emptySilenceFormGetParams)\nimport Utils.Types exposing (ApiData(..))\nimport Utils.Views\nimport Views.AlertList.Views as AlertList\nimport Views.NavBar.Views exposing (navBar)\nimport Views.NotFound.Views as NotFound\nimport Views.Settings.Views as SettingsView\nimport Views.SilenceForm.Views as SilenceForm\nimport Views.SilenceList.Views as SilenceList\nimport Views.SilenceView.Views as SilenceView\nimport Views.Status.Views as Status\n\n\nview : Model -> Html Msg\nview model =\n    div []\n        [ renderCSS model.libUrl\n        , case ( model.bootstrapCSS, model.fontAwesomeCSS, model.elmDatepickerCSS ) of\n            ( Success _, Success _, Success _ ) ->\n                div []\n                    [ navBar model.route\n                    , div [ class \"container pb-4\" ] [ currentView model ]\n                    ]\n\n            ( Failure err, _, _ ) ->\n                failureView model err\n\n            ( _, Failure err, _ ) ->\n                failureView model err\n\n            ( _, _, Failure err ) ->\n                failureView model err\n\n            _ ->\n                text \"\"\n        ]\n\n\nfailureView : Model -> String -> Html Msg\nfailureView model err =\n    div []\n        [ div [ style \"padding\" \"40px\", style \"color\" \"red\" ] [ text err ]\n        , navBar model.route\n        , div [ class \"container pb-4\" ] [ currentView model ]\n        ]\n\n\nrenderCSS : String -> Html Msg\nrenderCSS assetsUrl =\n    div []\n        [ cssNode (assetsUrl ++ \"lib/bootstrap-4.6.2-dist/css/bootstrap.min.css\") BootstrapCSSLoaded\n        , cssNode (assetsUrl ++ \"lib/font-awesome-4.7.0/css/font-awesome.min.css\") FontAwesomeCSSLoaded\n        , cssNode (assetsUrl ++ \"lib/elm-datepicker/css/elm-datepicker.css\") ElmDatepickerCSSLoaded\n        ]\n\n\ncssNode : String -> (ApiData String -> Msg) -> Html Msg\ncssNode url msg =\n    node \"link\"\n        [ href url\n        , rel \"stylesheet\"\n        , on \"load\" (succeed (msg (Success url)))\n        , on \"error\" (succeed (msg (Failure (\"Failed to load CSS from: \" ++ url))))\n        ]\n        []\n\n\ncurrentView : Model -> Html Msg\ncurrentView model =\n    case model.route of\n        SettingsRoute ->\n            SettingsView.view model.settings |> Html.map MsgForSettings\n\n        StatusRoute ->\n            Status.view model.status\n\n        SilenceViewRoute _ ->\n            SilenceView.view model.silenceView\n\n        AlertsRoute filter ->\n            AlertList.view model.alertList filter\n\n        SilenceListRoute _ ->\n            SilenceList.view model.silenceList\n\n        SilenceFormNewRoute getParams ->\n            SilenceForm.view Nothing getParams model.defaultCreator model.silenceForm |> Html.map MsgForSilenceForm\n\n        SilenceFormEditRoute silenceId ->\n            SilenceForm.view (Just silenceId) emptySilenceFormGetParams \"\" model.silenceForm |> Html.map MsgForSilenceForm\n\n        TopLevelRoute ->\n            Utils.Views.loading\n\n        NotFoundRoute ->\n            NotFound.view\n"
  },
  {
    "path": "ui/app/tests/Filter.elm",
    "content": "module Filter exposing (parseMatcher, stringifyFilter, toUrl)\n\nimport Expect\nimport Fuzz exposing (string, tuple)\nimport Helpers exposing (isNotEmptyTrimmedAlphabetWord)\nimport Test exposing (..)\nimport Utils.Filter exposing (MatchOperator(..), Matcher)\n\n\nparseMatcher : Test\nparseMatcher =\n    describe \"parseMatcher\"\n        [ test \"should parse empty matcher string\" <|\n            \\() ->\n                Expect.equal Nothing (Utils.Filter.parseMatcher \"\")\n        , test \"should parse empty matcher value\" <|\n            \\() ->\n                Expect.equal (Just (Matcher \"alertname\" Eq \"\")) (Utils.Filter.parseMatcher \"alertname=\\\"\\\"\")\n        , test \"should unescape quoted matcher value\" <|\n            \\() ->\n                Expect.equal\n                    (Just (Matcher \"alertname\" Eq \"foo\\\"bar\"))\n                    (Utils.Filter.parseMatcher \"alertname=\\\"foo\\\\\\\"bar\\\"\")\n        , test \"should unescape backslash matcher value\" <|\n            \\() ->\n                Expect.equal\n                    (Just (Matcher \"alertname\" Eq \"foo\\\\bar\"))\n                    (Utils.Filter.parseMatcher \"alertname=\\\"foo\\\\\\\\bar\\\"\")\n        , fuzz (tuple ( string, string )) \"should parse random matcher string\" <|\n            \\( key, value ) ->\n                if List.map isNotEmptyTrimmedAlphabetWord [ key, value ] /= [ True, True ] then\n                    Expect.equal\n                        Nothing\n                        (Utils.Filter.parseMatcher <| String.concat [ key, \"=\", value ])\n\n                else\n                    Expect.equal\n                        (Just (Matcher key Eq value))\n                        (Utils.Filter.parseMatcher <| String.concat [ key, \"=\", \"\\\"\", value, \"\\\"\" ])\n        ]\n\n\ntoUrl : Test\ntoUrl =\n    describe \"toUrl\"\n        [ test \"should not render keys with Nothing value except the silenced, inhibited, muted and active parameters, which default to false, false, false, true, respectively.\" <|\n            \\() ->\n                Expect.equal \"/alerts?silenced=false&inhibited=false&muted=false&active=true\"\n                    (Utils.Filter.toUrl \"/alerts\" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Nothing, showInhibited = Nothing, showMuted = Nothing, showActive = Nothing })\n        , test \"should not render filter key with empty value\" <|\n            \\() ->\n                Expect.equal \"/alerts?silenced=false&inhibited=false&muted=false&active=true\"\n                    (Utils.Filter.toUrl \"/alerts\" { receiver = Nothing, group = Nothing, customGrouping = False, text = Just \"\", showSilenced = Nothing, showInhibited = Nothing, showMuted = Nothing, showActive = Nothing })\n        , test \"should render filter key with values\" <|\n            \\() ->\n                Expect.equal \"/alerts?silenced=false&inhibited=false&muted=false&active=true&filter=%7Bfoo%3D%22bar%22%2C%20baz%3D~%22quux.*%22%7D\"\n                    (Utils.Filter.toUrl \"/alerts\" { receiver = Nothing, group = Nothing, customGrouping = False, text = Just \"{foo=\\\"bar\\\", baz=~\\\"quux.*\\\"}\", showSilenced = Nothing, showInhibited = Nothing, showMuted = Nothing, showActive = Nothing })\n        , test \"should render silenced key with bool\" <|\n            \\() ->\n                Expect.equal \"/alerts?silenced=true&inhibited=false&muted=false&active=true\"\n                    (Utils.Filter.toUrl \"/alerts\" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Just True, showInhibited = Nothing, showMuted = Nothing, showActive = Nothing })\n        , test \"should render inhibited key with bool\" <|\n            \\() ->\n                Expect.equal \"/alerts?silenced=false&inhibited=true&muted=false&active=true\"\n                    (Utils.Filter.toUrl \"/alerts\" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Nothing, showInhibited = Just True, showMuted = Nothing, showActive = Nothing })\n        , test \"should render muted key with bool\" <|\n            \\() ->\n                Expect.equal \"/alerts?silenced=false&inhibited=false&muted=true&active=true\"\n                    (Utils.Filter.toUrl \"/alerts\" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Nothing, showInhibited = Nothing, showMuted = Just True, showActive = Nothing })\n        , test \"should render active key with bool\" <|\n            \\() ->\n                Expect.equal \"/alerts?silenced=false&inhibited=false&muted=false&active=false\"\n                    (Utils.Filter.toUrl \"/alerts\" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Nothing, showInhibited = Nothing, showMuted = Nothing, showActive = Just False })\n        , test \"should add customGrouping key\" <|\n            \\() ->\n                Expect.equal \"/alerts?silenced=false&inhibited=false&muted=false&active=true&customGrouping=true\"\n                    (Utils.Filter.toUrl \"/alerts\" { receiver = Nothing, group = Nothing, customGrouping = True, text = Nothing, showSilenced = Nothing, showInhibited = Nothing, showMuted = Nothing, showActive = Nothing })\n        ]\n\n\nstringifyFilter : Test\nstringifyFilter =\n    describe \"stringifyFilter\"\n        [ test \"empty\" <|\n            \\() ->\n                Expect.equal \"\"\n                    (Utils.Filter.stringifyFilter [])\n        , test \"non-empty\" <|\n            \\() ->\n                Expect.equal \"{foo=\\\"bar\\\", baz=~\\\"quux.*\\\"}\"\n                    (Utils.Filter.stringifyFilter\n                        [ { key = \"foo\", op = Eq, value = \"bar\" }\n                        , { key = \"baz\", op = RegexMatch, value = \"quux.*\" }\n                        ]\n                    )\n        , test \"escapes matcher values\" <|\n            \\() ->\n                Expect.equal \"{foo=\\\"bar\\\\\\\"baz\\\\\\\\qux\\\"}\"\n                    (Utils.Filter.stringifyFilter\n                        [ { key = \"foo\", op = Eq, value = \"bar\\\"baz\\\\qux\" } ]\n                    )\n        ]\n"
  },
  {
    "path": "ui/app/tests/Helpers.elm",
    "content": "module Helpers exposing (isNotEmptyTrimmedAlphabetWord)\n\nimport String\n\n\nisNotEmptyTrimmedAlphabetWord : String -> Bool\nisNotEmptyTrimmedAlphabetWord string =\n    let\n        stringLength =\n            String.length string\n    in\n    stringLength\n        /= 0\n        && String.length (String.filter isLetter string)\n        == stringLength\n\n\nisLetter : Char -> Bool\nisLetter char =\n    String.contains (String.fromChar char) lowerCaseAlphabet\n        || String.contains (String.fromChar char) upperCaseAlphabet\n\n\nlowerCaseAlphabet : String\nlowerCaseAlphabet =\n    \"abcdefghijklmnopqrstuvwxyz\"\n\n\nupperCaseAlphabet : String\nupperCaseAlphabet =\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n"
  },
  {
    "path": "ui/app/tests/Match.elm",
    "content": "module Match exposing (testConsecutiveChars, testJaroWinkler)\n\nimport Expect\nimport Test exposing (..)\nimport Utils.Match exposing (consecutiveChars, jaroWinkler)\n\n\ntestJaroWinkler : Test\ntestJaroWinkler =\n    describe \"jaroWinkler\"\n        [ test \"should find the right values 1\" <|\n            \\() ->\n                Expect.greaterThan (jaroWinkler \"zi\" \"zone\")\n                    (jaroWinkler \"zo\" \"zone\")\n        , test \"should find the right values 2\" <|\n            \\() ->\n                Expect.greaterThan (jaroWinkler \"hook\" \"alertname\")\n                    (jaroWinkler \"de\" \"dev\")\n        , test \"should find the right values 3\" <|\n            \\() ->\n                Expect.equal 0.0\n                    (jaroWinkler \"l\" \"zone\")\n        , test \"should find the right values 4\" <|\n            \\() ->\n                Expect.equal 1.0\n                    (jaroWinkler \"zone\" \"zone\")\n        , test \"should find the right values 5\" <|\n            \\() ->\n                Expect.greaterThan 0.688\n                    (jaroWinkler \"atleio3tefdoisahdf\" \"attributefdoiashfoihfeowfh9w8f9afaw9fahw\")\n        ]\n\n\ntestConsecutiveChars : Test\ntestConsecutiveChars =\n    describe \"consecutiveChars\"\n        [ test \"should find the consecutiveChars 1\" <|\n            \\() ->\n                Expect.equal \"zo\"\n                    (consecutiveChars \"zo\" \"bozo\")\n        , test \"should find the consecutiveChars 2\" <|\n            \\() ->\n                Expect.equal \"zo\"\n                    (consecutiveChars \"zol\" \"zone\")\n        , test \"should find the consecutiveChars 3\" <|\n            \\() ->\n                Expect.equal \"oon\"\n                    (consecutiveChars \"oon\" \"baboone\")\n        , test \"should find the consecutiveChars 4\" <|\n            \\() ->\n                Expect.equal \"dom\"\n                    (consecutiveChars \"dom\" \"random\")\n        ]\n"
  },
  {
    "path": "ui/app/tests/StringUtils.elm",
    "content": "module StringUtils exposing (testLinkify)\n\nimport Expect\nimport Test exposing (..)\nimport Utils.String exposing (linkify)\n\n\ntestLinkify : Test\ntestLinkify =\n    describe \"linkify\"\n        [ test \"should linkify a url in the middle\" <|\n            \\() ->\n                Expect.equal (linkify \"word1 http://url word2\")\n                    [ Err \"word1 \", Ok \"http://url\", Err \" word2\" ]\n        , test \"should linkify a url in the beginning\" <|\n            \\() ->\n                Expect.equal (linkify \"http://url word1 word2\")\n                    [ Ok \"http://url\", Err \" word1 word2\" ]\n        , test \"should linkify a url in the end\" <|\n            \\() ->\n                Expect.equal (linkify \"word1 word2 http://url\")\n                    [ Err \"word1 word2 \", Ok \"http://url\" ]\n        ]\n"
  },
  {
    "path": "ui/mantine-ui/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "ui/mantine-ui/.nvmrc",
    "content": "v24.3.0\n"
  },
  {
    "path": "ui/mantine-ui/.prettierrc.mjs",
    "content": "/** @type {import(\"@ianvs/prettier-plugin-sort-imports\").PrettierConfig} */\nconst config = {\n  printWidth: 100,\n  singleQuote: true,\n  trailingComma: \"es5\",\n  plugins: [\"@ianvs/prettier-plugin-sort-imports\"],\n  importOrder: [\n    \".*styles.css$\",\n    \"\",\n    \"dayjs\",\n    \"^react$\",\n    \"^next$\",\n    \"^next/.*$\",\n    \"<BUILTIN_MODULES>\",\n    \"<THIRD_PARTY_MODULES>\",\n    \"^@mantine/(.*)$\",\n    \"^@mantinex/(.*)$\",\n    \"^@mantine-tests/(.*)$\",\n    \"^@docs/(.*)$\",\n    \"^@/.*$\",\n    \"^../(?!.*.css$).*$\",\n    \"^./(?!.*.css$).*$\",\n    \"\\\\.css$\",\n  ],\n  overrides: [\n    {\n      files: \"*.mdx\",\n      options: {\n        printWidth: 70,\n      },\n    },\n  ],\n};\n\nexport default config;\n"
  },
  {
    "path": "ui/mantine-ui/.stylelintignore",
    "content": "dist\n"
  },
  {
    "path": "ui/mantine-ui/.stylelintrc.json",
    "content": "{\n  \"extends\": [\n    \"stylelint-config-standard-scss\"\n  ],\n  \"rules\": {\n    \"custom-property-pattern\": null,\n    \"selector-class-pattern\": null,\n    \"scss/no-duplicate-mixins\": null,\n    \"declaration-empty-line-before\": null,\n    \"declaration-block-no-redundant-longhand-properties\": null,\n    \"alpha-value-notation\": null,\n    \"custom-property-empty-line-before\": null,\n    \"property-no-vendor-prefix\": null,\n    \"color-function-notation\": null,\n    \"length-zero-no-unit\": null,\n    \"selector-not-notation\": null,\n    \"no-descending-specificity\": null,\n    \"comment-empty-line-before\": null,\n    \"scss/at-mixin-pattern\": null,\n    \"scss/at-rule-no-unknown\": null,\n    \"value-keyword-case\": null,\n    \"media-feature-range-notation\": null,\n    \"selector-pseudo-class-no-unknown\": [\n      true,\n      {\n        \"ignorePseudoClasses\": [\n          \"global\"\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "ui/mantine-ui/eslint.config.js",
    "content": "import mantine from \"eslint-config-mantine\";\nimport tseslint from \"typescript-eslint\";\n\nexport default tseslint.config(\n  ...mantine,\n  { ignores: [\"**/*.{mjs,cjs,js,d.ts,d.mts}\", \"./.storybook/main.ts\"] },\n  {\n    files: [\"**/*.story.tsx\"],\n    rules: { \"no-console\": \"off\" },\n  }\n);\n"
  },
  {
    "path": "ui/mantine-ui/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/src/favicon.svg\" />\n    <meta\n      name=\"viewport\"\n      content=\"minimum-scale=1, initial-scale=1, width=device-width, user-scalable=no\"\n    />\n    <title>Alertmanager</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "ui/mantine-ui/package.json",
    "content": "{\n  \"name\": \"alertmanager\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite --host\",\n    \"build\": \"tsc && vite build\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"lint\": \"npm run eslint && npm run stylelint\",\n    \"eslint\": \"eslint . --cache\",\n    \"stylelint\": \"stylelint '**/*.css' --cache\",\n    \"prettier\": \"prettier --check \\\"**/*.{ts,tsx}\\\"\",\n    \"prettier:write\": \"prettier --write \\\"**/*.{ts,tsx}\\\"\",\n    \"vitest\": \"vitest run\",\n    \"vitest:watch\": \"vitest\",\n    \"test\": \"npm run typecheck && npm run prettier && npm run lint && npm run vitest && npm run build\"\n  },\n  \"dependencies\": {\n    \"@mantine/code-highlight\": \"^8.3.16\",\n    \"@mantine/core\": \"^8.3.13\",\n    \"@mantine/hooks\": \"^8.3.13\",\n    \"@tanstack/react-query\": \"^5.90.21\",\n    \"highlight.js\": \"^11.11.1\",\n    \"react\": \"^19.1.0\",\n    \"react-dom\": \"^19.2.4\",\n    \"react-router-dom\": \"^7.13.1\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.29.0\",\n    \"@ianvs/prettier-plugin-sort-imports\": \"^4.7.1\",\n    \"@testing-library/dom\": \"^10.4.0\",\n    \"@testing-library/jest-dom\": \"^6.9.1\",\n    \"@testing-library/react\": \"^16.3.2\",\n    \"@testing-library/user-event\": \"^14.6.1\",\n    \"@types/node\": \"^25.3.5\",\n    \"@types/react\": \"^19.2.14\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"@vitejs/plugin-react\": \"^5.1.4\",\n    \"eslint\": \"^9.39.2\",\n    \"eslint-config-mantine\": \"^4.0.3\",\n    \"eslint-plugin-jsx-a11y\": \"^6.10.2\",\n    \"eslint-plugin-react\": \"^7.37.5\",\n    \"jsdom\": \"^28.1.0\",\n    \"postcss\": \"^8.5.8\",\n    \"postcss-preset-mantine\": \"1.18.0\",\n    \"postcss-simple-vars\": \"^7.0.1\",\n    \"prettier\": \"^3.8.1\",\n    \"prop-types\": \"^15.8.1\",\n    \"stylelint\": \"^17.4.0\",\n    \"stylelint-config-standard-scss\": \"^17.0.0\",\n    \"typescript\": \"^5.9.3\",\n    \"typescript-eslint\": \"^8.56.1\",\n    \"vite\": \"^7.3.1\",\n    \"vite-tsconfig-paths\": \"^6.1.1\",\n    \"vitest\": \"^4.0.18\"\n  }\n}\n"
  },
  {
    "path": "ui/mantine-ui/postcss.config.cjs",
    "content": "module.exports = {\n  plugins: {\n    'postcss-preset-mantine': {},\n    'postcss-simple-vars': {\n      variables: {\n        'mantine-breakpoint-xs': '36em',\n        'mantine-breakpoint-sm': '48em',\n        'mantine-breakpoint-md': '62em',\n        'mantine-breakpoint-lg': '75em',\n        'mantine-breakpoint-xl': '88em',\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "ui/mantine-ui/src/App.tsx",
    "content": "import '@mantine/core/styles.css';\nimport '@mantine/code-highlight/styles.css';\n\nimport { Suspense } from 'react';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport hljs from 'highlight.js/lib/core';\nimport { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';\nimport { CodeHighlightAdapterProvider, createHighlightJsAdapter } from '@mantine/code-highlight';\nimport { AppShell, Box, MantineProvider, Skeleton } from '@mantine/core';\nimport ErrorBoundary from './components/ErrorBoundary';\nimport { Header } from './components/Header';\nimport { AlertsPage } from './pages/Alerts.page';\nimport { ConfigPage } from './pages/Config.page';\nimport { SilencesPage } from './pages/Silences.page';\nimport { StatusPage } from './pages/Status.page';\nimport { theme } from './theme';\n\nimport './highlightjs.css';\n\nimport yamlLang from 'highlight.js/lib/languages/yaml';\n\nhljs.registerLanguage('yaml', yamlLang);\nconst highlightJsAdapter = createHighlightJsAdapter(hljs);\n\nconst queryClient = new QueryClient();\n\nexport default function App() {\n  return (\n    <BrowserRouter>\n      <MantineProvider theme={theme}>\n        <CodeHighlightAdapterProvider adapter={highlightJsAdapter}>\n          <QueryClientProvider client={queryClient}>\n            <AppShell padding=\"md\" header={{ height: 60 }}>\n              <Header />\n              <AppShell.Main>\n                <ErrorBoundary key={location.pathname}>\n                  <Suspense\n                    fallback={\n                      <Box mt=\"lg\">\n                        {Array.from(Array(10), (_, i) => (\n                          <Skeleton key={i} height={40} mb={15} width={1000} mx=\"auto\" />\n                        ))}\n                      </Box>\n                    }\n                  >\n                    {/* Main content will be rendered here by the Router */}\n                    <Routes>\n                      {/* Redirect the root path to the alerts page */}\n                      {/* TODO(@sysadmind): This should take the fact that previous UI used /#/routeName */}\n                      <Route path=\"/\" element={<Navigate to=\"/alerts\" replace />} />\n                      <Route path=\"/alerts\" element={<AlertsPage />} />\n                      <Route path=\"/silences\" element={<SilencesPage />} />\n                      <Route path=\"/status\" element={<StatusPage />} />\n                      <Route path=\"/config\" element={<ConfigPage />} />\n                    </Routes>\n                  </Suspense>\n                </ErrorBoundary>\n              </AppShell.Main>\n            </AppShell>\n          </QueryClientProvider>\n        </CodeHighlightAdapterProvider>\n      </MantineProvider>\n    </BrowserRouter>\n  );\n}\n"
  },
  {
    "path": "ui/mantine-ui/src/components/ErrorBoundary.tsx",
    "content": "// import { IconAlertTriangle } from \"@tabler/icons-react\";\nimport { Component, ErrorInfo, ReactNode } from 'react';\nimport { useLocation } from 'react-router-dom';\nimport { Alert } from '@mantine/core';\n\ninterface Props {\n  children?: ReactNode;\n  title?: string;\n}\n\ninterface State {\n  error: Error | null;\n}\n\nclass ErrorBoundary extends Component<Props, State> {\n  public state: State = {\n    error: null,\n  };\n\n  public static getDerivedStateFromError(error: Error): State {\n    // Update state so the next render will show the fallback UI.\n    return { error };\n  }\n\n  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n    console.error('Uncaught error:', error, errorInfo);\n  }\n\n  public render() {\n    if (this.state.error !== null) {\n      return (\n        <Alert\n          color=\"red\"\n          title={this.props.title || 'Error querying page data'}\n          // icon={<IconAlertTriangle />}\n          maw={500}\n          mx=\"auto\"\n          mt=\"lg\"\n        >\n          <strong>Error:</strong> {this.state.error.message}\n        </Alert>\n      );\n    }\n\n    return this.props.children;\n  }\n}\n\nconst ResettingErrorBoundary = (props: Props) => {\n  const location = useLocation();\n  return (\n    <ErrorBoundary key={location.pathname} title={props.title}>\n      {props.children}\n    </ErrorBoundary>\n  );\n};\n\nexport default ResettingErrorBoundary;\n"
  },
  {
    "path": "ui/mantine-ui/src/components/Header.module.css",
    "content": ".navMain {\n  flex: 1;\n}\n\n.navLink {\n  display: block;\n  line-height: 1;\n  padding: rem(8px) rem(12px);\n  border-radius: var(--mantine-radius-sm);\n  text-decoration: none;\n  color: var(--mantine-color-gray-0);\n  font-size: var(--mantine-font-size-sm);\n  font-weight: 500;\n  background-color: transparent;\n\n  @mixin hover {\n    background-color: var(--mantine-color-gray-6);\n    color: var(--mantine-color-gray-0);\n  }\n\n  [data-mantine-color-scheme] &[aria-current='page'] {\n    background-color: var(--mantine-color-blue-filled);\n    color: var(--mantine-color-white);\n  }\n}\n"
  },
  {
    "path": "ui/mantine-ui/src/components/Header.tsx",
    "content": "import { Link, NavLink, Route, Routes } from 'react-router-dom';\nimport { AppShell, Button, Group, Menu, Text } from '@mantine/core';\nimport { AlertsPage } from '@/pages/Alerts.page';\nimport { SilencesPage } from '@/pages/Silences.page';\nimport classes from './Header.module.css';\n\nconst navLinkXPadding = 'md';\n\nexport const Header = () => {\n  const mainNavPages = [\n    {\n      title: 'Alerts',\n      path: '/alerts',\n      // icon: <IconBellFilled style={navIconStyle} />,\n      element: <AlertsPage />,\n    },\n    {\n      title: 'Silences',\n      path: '/silences',\n      // icon: <IconSearch style={navIconStyle} />,\n      element: <SilencesPage />,\n    },\n  ];\n\n  const navLinks = (\n    <>\n      {mainNavPages.map((page) => (\n        <Button\n          key={page.path}\n          component={NavLink}\n          to={page.path}\n          className={classes.navLink}\n          // leftSection={page.icon}\n          px={navLinkXPadding}\n        >\n          {page.title}\n        </Button>\n      ))}\n      <Menu>\n        <Routes>\n          <Route\n            path=\"/status\"\n            element={\n              <Menu.Target>\n                <Button\n                  component={NavLink}\n                  to=\"/status\"\n                  className={classes.navLink}\n                  px={navLinkXPadding}\n                >\n                  Status {'>'} Runtime & Build Information\n                </Button>\n              </Menu.Target>\n            }\n          />\n          <Route\n            path=\"/config\"\n            element={\n              <Menu.Target>\n                <Button\n                  component={NavLink}\n                  to=\"/config\"\n                  className={classes.navLink}\n                  px={navLinkXPadding}\n                >\n                  Status {'>'} Configuration\n                </Button>\n              </Menu.Target>\n            }\n          />\n          {/* Default menu item when no status pages are selected */}\n          <Route\n            path=\"*\"\n            element={\n              <Menu.Target>\n                <Button\n                  className={classes.navLink}\n                  // leftSection={<IconServer style={navIconStyle} />}\n                  // rightSection={<IconChevronDown style={navIconStyle} />}\n                  px={navLinkXPadding}\n                >\n                  Status\n                </Button>\n              </Menu.Target>\n            }\n          />\n        </Routes>\n        <Menu.Dropdown>\n          <Menu.Item key=\"runtime\" component={NavLink} to=\"/status\">\n            Runtime & Build Information\n          </Menu.Item>\n          <Menu.Item key=\"config\" component={NavLink} to=\"/config\">\n            Configuration\n          </Menu.Item>\n        </Menu.Dropdown>\n      </Menu>\n    </>\n  );\n\n  return (\n    <AppShell.Header className={classes.header}>\n      <Group h=\"100%\" px=\"md\" wrap=\"nowrap\">\n        <Group className={classes.navMain} justify=\"space-between\" wrap=\"nowrap\">\n          <Group gap={40} wrap=\"nowrap\">\n            <Link to=\"/\" style={{ textDecoration: 'none', color: 'white' }}>\n              <Group gap={10} wrap=\"nowrap\">\n                {/* <img src={PrometheusLogo} height={30} /> */}\n                <Text hiddenFrom=\"sm\" fz={20}>\n                  Alertmanager\n                </Text>\n                <Text visibleFrom=\"md\" fz={20}>\n                  Alertmanager\n                </Text>\n              </Group>\n            </Link>\n            <Group gap={12} visibleFrom=\"sm\" wrap=\"nowrap\">\n              {navLinks}\n            </Group>\n          </Group>\n        </Group>\n      </Group>\n    </AppShell.Header>\n  );\n};\n"
  },
  {
    "path": "ui/mantine-ui/src/components/InfoPageCard.tsx",
    "content": "// import { IconProps } from \"@tabler/icons-react\";\nimport { FC, ReactNode } from 'react';\nimport { Card, em, Group } from '@mantine/core';\n\nconst infoPageCardTitleIconStyle = { width: em(17.5), height: em(17.5) };\n\nconst InfoPageCard: FC<{\n  children: ReactNode;\n  title?: string;\n  icon?: React.ComponentType<any>;\n}> = ({ children, title, icon: Icon }) => {\n  return (\n    <Card shadow=\"xs\" withBorder p=\"md\">\n      {title && (\n        <Group wrap=\"nowrap\" align=\"center\" ml=\"xs\" mb=\"sm\" gap=\"xs\" fz=\"xl\" fw={600}>\n          {Icon && <Icon style={infoPageCardTitleIconStyle} />}\n          {title}\n        </Group>\n      )}\n      {children}\n    </Card>\n  );\n};\n\nexport default InfoPageCard;\n"
  },
  {
    "path": "ui/mantine-ui/src/components/InfoPageStack.tsx",
    "content": "import { FC, ReactNode } from 'react';\nimport { Stack } from '@mantine/core';\n\nconst InfoPageStack: FC<{ children: ReactNode }> = ({ children }) => {\n  return (\n    <Stack gap=\"lg\" maw={1000} mx=\"auto\" mt=\"xs\">\n      {children}\n    </Stack>\n  );\n};\n\nexport default InfoPageStack;\n"
  },
  {
    "path": "ui/mantine-ui/src/data/api.ts",
    "content": "import { QueryKey, useQuery, useSuspenseQuery } from '@tanstack/react-query';\n\n// TODO(@sysadmind): Infer this from the current location.\n// We don't have a good strategy for storing global settings yet.\nconst pathPrefix = '';\nexport const API_PATH = 'api/v2';\n\ntype APIError = {\n  status: 'error';\n  error?: string;\n  errorType?: string;\n};\n\ntype APISuccess<T> = {\n  status: 'success';\n  data: T;\n};\n\nexport type APIResponse<T> = APISuccess<T> | APIError;\n\nconst isAPIEnvelope = <T>(value: unknown): value is APIResponse<T> => {\n  return (\n    typeof value === 'object' &&\n    value !== null &&\n    'status' in value &&\n    ((value as { status?: unknown }).status === 'success' ||\n      (value as { status?: unknown }).status === 'error')\n  );\n};\n\nconst createQueryFn =\n  <T>({\n    pathPrefix,\n    path,\n    params,\n    recordResponseTime,\n  }: {\n    pathPrefix: string;\n    path: string;\n    params?: Record<string, string | string[]>;\n    recordResponseTime?: (time: number) => void;\n  }) =>\n  async ({ signal }: { signal: AbortSignal }) => {\n    const queryParams = new URLSearchParams();\n    if (params) {\n      Object.entries(params).forEach(([key, value]) => {\n        if (Array.isArray(value)) {\n          value.forEach((v) => queryParams.append(key, v));\n        } else {\n          queryParams.set(key, value);\n        }\n      });\n    }\n    const queryString = params ? `?${queryParams.toString()}` : '';\n\n    try {\n      const startTime = Date.now();\n\n      const res = await fetch(`${pathPrefix}/${API_PATH}${path}${queryString}`, {\n        cache: 'no-store',\n        credentials: 'same-origin',\n        signal,\n      });\n\n      if (!res.ok && !res.headers.get('content-type')?.startsWith('application/json')) {\n        // For example, Alertmanager may send a 503 Service Unavailable response\n        // with a \"text/plain\" content type when it's starting up. But the API\n        // may also respond with a JSON error message and the same error code.\n        throw new Error(res.statusText);\n      }\n\n      const parsed = await res.json();\n\n      if (recordResponseTime) {\n        recordResponseTime(Date.now() - startTime);\n      }\n\n      if (isAPIEnvelope<T>(parsed)) {\n        if (parsed.status === 'error') {\n          throw new Error(\n            parsed.error !== undefined ? parsed.error : 'missing \"error\" field in response JSON'\n          );\n        }\n\n        return parsed.data;\n      }\n\n      return parsed as T;\n    } catch (error) {\n      if (!(error instanceof Error)) {\n        throw new Error('Unknown error');\n      }\n\n      switch (error.name) {\n        case 'TypeError':\n          throw new Error('Network error or unable to reach the server');\n        case 'SyntaxError':\n          throw new Error('Invalid JSON response');\n        default:\n          throw error;\n      }\n    }\n  };\n\ntype QueryOptions = {\n  key?: QueryKey;\n  path: string;\n  params?: Record<string, string | string[]>;\n  enabled?: boolean;\n  refetchInterval?: false | number;\n  recordResponseTime?: (time: number) => void;\n};\n\nexport const useAPIQuery = <T>({\n  key,\n  path,\n  params,\n  enabled,\n  refetchInterval,\n  recordResponseTime,\n}: QueryOptions) => {\n  return useQuery<T>({\n    queryKey: key ?? [API_PATH, path, params],\n    retry: false,\n    refetchOnWindowFocus: false,\n    refetchInterval,\n    gcTime: 0,\n    enabled,\n    queryFn: createQueryFn<T>({ pathPrefix, path, params, recordResponseTime }),\n  });\n};\n\nexport const useSuspenseAPIQuery = <T>({ key, path, params }: QueryOptions) => {\n  return useSuspenseQuery<T>({\n    queryKey: key !== undefined ? key : [path, params],\n    retry: false,\n    refetchOnWindowFocus: false,\n    gcTime: 0,\n    queryFn: createQueryFn({ pathPrefix, path, params }),\n  });\n};\n"
  },
  {
    "path": "ui/mantine-ui/src/data/groups.ts",
    "content": "import { useSuspenseAPIQuery } from '@/data/api';\n\ntype Group = {\n  alerts: Alert[];\n  labels: Record<string, string>;\n  receiver: Receiver;\n};\n\ntype Receiver = {\n  name: string;\n};\n\ntype AlertStatus = {\n  inhibitedBy: string[];\n  silencedBy: string[];\n  mutedBy: string[];\n  state: 'active';\n};\n\ntype Alert = {\n  annotations: Record<string, string>;\n  endsAt: string;\n  fingerprint: string;\n  receivers: Receiver[];\n  startsAt: string;\n  status: AlertStatus;\n  updatedAt: string;\n  labels: Record<string, string>;\n};\n\nexport const useGroups = () => {\n  return useSuspenseAPIQuery<Array<Group>>({\n    path: '/alerts/groups',\n  });\n};\n"
  },
  {
    "path": "ui/mantine-ui/src/data/silences.ts",
    "content": "import { useSuspenseAPIQuery } from '@/data/api';\n\ntype Silence = {\n  id: string;\n  status: {\n    state: 'active' | 'expired' | 'pending';\n  };\n  startsAt: string;\n  updatedAt: string;\n  endsAt: string;\n  createdBy: string;\n  comment: string;\n  matchers: Array<{\n    name: string;\n    value: string;\n    isRegex: boolean;\n    isEqual: boolean;\n  }>;\n};\n\nexport const useSilences = () => {\n  return useSuspenseAPIQuery<Array<Silence>>({\n    path: '/silences',\n  });\n};\n\nexport const useSilence = (id: string) => {\n  return useSuspenseAPIQuery<Silence>({\n    path: `/silence/${id}`,\n  });\n};\n"
  },
  {
    "path": "ui/mantine-ui/src/data/status.ts",
    "content": "import { useSuspenseAPIQuery } from '@/data/api';\n\ntype Status = {\n  cluster: {\n    name: string;\n    peers: Array<{\n      name: string;\n      address: string;\n    }>;\n    status: 'ready' | 'not_ready';\n  };\n  config: {\n    original: string;\n  };\n  uptime: string;\n  versionInfo: {\n    branch: string;\n    buildDate: string;\n    buildUser: string;\n    goVersion: string;\n    version: string;\n    revision: string;\n  };\n};\n\nexport const useStatus = () => {\n  return useSuspenseAPIQuery<Status>({\n    path: '/status',\n  });\n};\n"
  },
  {
    "path": "ui/mantine-ui/src/highlightjs.css",
    "content": "/* Adapted from Mantine 7, where highlighting was still included automatically as part of <CodeHighlight>,\n   see https://github.com/mantinedev/mantine/blob/v7/packages/%40mantine/code-highlight/src/CodeHighlight.theme.module.css */\n.hljs {\n  color: var(--code-text-color);\n  background: var(--code-background);\n\n  @mixin where-light {\n    --code-text-color: var(--mantine-color-gray-7);\n    --code-background: var(--mantine-color-gray-0);\n    --code-comment-color: var(--mantine-color-gray-6);\n    --code-keyword-color: var(--mantine-color-violet-8);\n    --code-tag-color: var(--mantine-color-red-9);\n    --code-literal-color: var(--mantine-color-blue-6);\n    --code-string-color: var(--mantine-color-blue-9);\n    --code-variable-color: var(--mantine-color-lime-9);\n    --code-class-color: var(--mantine-color-orange-9);\n  }\n\n  @mixin where-dark {\n    --code-text-color: var(--mantine-color-dark-1);\n    --code-background: var(--mantine-color-dark-8);\n    --code-comment-color: var(--mantine-color-dark-3);\n    --code-keyword-color: var(--mantine-color-violet-3);\n    --code-tag-color: var(--mantine-color-yellow-4);\n    --code-literal-color: var(--mantine-color-blue-4);\n    --code-string-color: var(--mantine-color-green-6);\n    --code-variable-color: var(--mantine-color-blue-2);\n    --code-class-color: var(--mantine-color-orange-5);\n  }\n\n  .hljs-comment,\n  .hljs-quote {\n    font-style: italic;\n    color: var(--code-comment-color);\n  }\n\n  .hljs-doctag,\n  .hljs-formula,\n  .hljs-keyword {\n    color: var(--code-keyword-color);\n  }\n\n  .hljs-deletion,\n  .hljs-name,\n  .hljs-section,\n  .hljs-selector-tag,\n  .hljs-subst {\n    color: var(--code-tag-color);\n  }\n\n  .hljs-literal {\n    color: var(--code-literal-color);\n  }\n\n  .hljs-addition,\n  .hljs-attribute,\n  .hljs-meta .hljs-string,\n  .hljs-regexp,\n  .hljs-string {\n    color: var(--code-string-color);\n  }\n\n  .hljs-attr,\n  .hljs-number,\n  .hljs-selector-attr,\n  .hljs-selector-class,\n  .hljs-selector-pseudo,\n  .hljs-template-variable,\n  .hljs-type,\n  .hljs-variable {\n    color: var(--code-variable-color);\n  }\n\n  .hljs-bullet,\n  .hljs-link,\n  .hljs-meta,\n  .hljs-selector-id,\n  .hljs-symbol,\n  .hljs-title,\n  .hljs-built_in,\n  .hljs-class .hljs-title,\n  .hljs-title.class_ {\n    color: var(--code-class-color);\n  }\n\n  .hljs-emphasis {\n    font-style: italic;\n  }\n\n  .hljs-strong {\n    font-weight: 700;\n  }\n\n  .hljs-link {\n    text-decoration: underline;\n  }\n}\n"
  },
  {
    "path": "ui/mantine-ui/src/main.tsx",
    "content": "import ReactDOM from 'react-dom/client';\nimport App from './App';\n\nReactDOM.createRoot(document.getElementById('root')!).render(<App />);\n"
  },
  {
    "path": "ui/mantine-ui/src/pages/Alerts.page.test.tsx",
    "content": "import { render } from '@test-utils';\nimport { AlertsPage } from './Alerts.page';\n\ndescribe('AlertsPage', () => {\n  it('renders without crashing', () => {\n    render(<AlertsPage />);\n  });\n});\n"
  },
  {
    "path": "ui/mantine-ui/src/pages/Alerts.page.tsx",
    "content": "import { Text } from '@mantine/core';\n\nexport function AlertsPage() {\n  return (\n    <Text ta=\"center\" size=\"lg\" maw={580} mx=\"auto\" mt=\"xl\">\n      Alerts List\n    </Text>\n  );\n}\n"
  },
  {
    "path": "ui/mantine-ui/src/pages/Config.page.tsx",
    "content": "import { CodeHighlight } from '@mantine/code-highlight';\nimport { useStatus } from '@/data/status';\n\nexport function ConfigPage() {\n  const { data } = useStatus();\n  return (\n    <CodeHighlight\n      language=\"yaml\"\n      code={data.config.original}\n      miw=\"50vw\"\n      w=\"fit-content\"\n      maw=\"calc(100vw - 75px)\"\n      mx=\"auto\"\n      mt=\"xs\"\n    />\n  );\n}\n"
  },
  {
    "path": "ui/mantine-ui/src/pages/Silences.page.tsx",
    "content": "import { Text } from '@mantine/core';\n\nexport function SilencesPage() {\n  return (\n    <Text ta=\"center\" size=\"lg\" maw={580} mx=\"auto\" mt=\"xl\">\n      Silences\n    </Text>\n  );\n}\n"
  },
  {
    "path": "ui/mantine-ui/src/pages/Status.page.tsx",
    "content": "import { Table } from '@mantine/core';\nimport InfoPageCard from '@/components/InfoPageCard';\nimport InfoPageStack from '@/components/InfoPageStack';\nimport { useStatus } from '@/data/status';\n\nexport function StatusPage() {\n  const { data } = useStatus();\n  return (\n    <InfoPageStack>\n      <InfoPageCard title=\"Build information\">\n        <Table layout=\"fixed\">\n          <Table.Tbody>\n            <Table.Tr>\n              <Table.Th>Version</Table.Th>\n              <Table.Td>{data.versionInfo.version}</Table.Td>\n            </Table.Tr>\n            <Table.Tr>\n              <Table.Th>Revision</Table.Th>\n              <Table.Td>{data.versionInfo.revision}</Table.Td>\n            </Table.Tr>\n            <Table.Tr>\n              <Table.Th>Branch</Table.Th>\n              <Table.Td>{data.versionInfo.branch}</Table.Td>\n            </Table.Tr>\n            <Table.Tr>\n              <Table.Th>Build User</Table.Th>\n              <Table.Td>{data.versionInfo.buildUser}</Table.Td>\n            </Table.Tr>\n            <Table.Tr>\n              <Table.Th>Build Date</Table.Th>\n              <Table.Td>{data.versionInfo.buildDate}</Table.Td>\n            </Table.Tr>\n            <Table.Tr>\n              <Table.Th>Go Version</Table.Th>\n              <Table.Td>{data.versionInfo.goVersion}</Table.Td>\n            </Table.Tr>\n          </Table.Tbody>\n        </Table>\n      </InfoPageCard>\n      <InfoPageCard title=\"Runtime information\">\n        <Table layout=\"fixed\">\n          <Table.Tbody>\n            <Table.Tr>\n              <Table.Th>Uptime</Table.Th>\n              <Table.Td>{data.uptime}</Table.Td>\n            </Table.Tr>\n            <Table.Tr>\n              <Table.Th>Cluster Name</Table.Th>\n              <Table.Td>{data.cluster.name}</Table.Td>\n            </Table.Tr>\n            <Table.Tr>\n              <Table.Th>Cluster Status</Table.Th>\n              <Table.Td>{data.cluster.status}</Table.Td>\n            </Table.Tr>\n            <Table.Tr>\n              <Table.Th>Number of Peers</Table.Th>\n              <Table.Td>{data.cluster.peers.length}</Table.Td>\n            </Table.Tr>\n          </Table.Tbody>\n        </Table>\n      </InfoPageCard>\n      <InfoPageCard title=\"Cluster Peers\">\n        <Table layout=\"fixed\">\n          <Table.Tbody>\n            <Table.Tr>\n              <Table.Th>Peer Name</Table.Th>\n              <Table.Th>Address</Table.Th>\n            </Table.Tr>\n            {data.cluster.peers.map((peer, index) => (\n              <Table.Tr key={index}>\n                <Table.Td>{peer.name}</Table.Td>\n                <Table.Td>{peer.address}</Table.Td>\n              </Table.Tr>\n            ))}\n          </Table.Tbody>\n        </Table>\n      </InfoPageCard>\n    </InfoPageStack>\n  );\n}\n"
  },
  {
    "path": "ui/mantine-ui/src/theme.ts",
    "content": "import { createTheme } from '@mantine/core';\n\nexport const theme = createTheme({\n  /** Put your mantine theme override here */\n});\n"
  },
  {
    "path": "ui/mantine-ui/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "ui/mantine-ui/test-utils/index.ts",
    "content": "import userEvent from '@testing-library/user-event';\n\nexport * from '@testing-library/react';\nexport { render } from './render';\nexport { userEvent };\n"
  },
  {
    "path": "ui/mantine-ui/test-utils/render.tsx",
    "content": "import { render as testingLibraryRender } from '@testing-library/react';\nimport { MantineProvider } from '@mantine/core';\nimport { theme } from '../src/theme';\n\nexport function render(ui: React.ReactNode) {\n  return testingLibraryRender(ui, {\n    wrapper: ({ children }: { children: React.ReactNode }) => (\n      <MantineProvider theme={theme} env=\"test\">\n        {children}\n      </MantineProvider>\n    ),\n  });\n}\n"
  },
  {
    "path": "ui/mantine-ui/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"types\": [\"node\", \"@testing-library/jest-dom\", \"vitest/globals\"],\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"],\n      \"@test-utils\": [\"./test-utils\"]\n    }\n  },\n  \"include\": [\"src\", \"test-utils\"]\n}\n"
  },
  {
    "path": "ui/mantine-ui/vite.config.mjs",
    "content": "import react from '@vitejs/plugin-react';\nimport { defineConfig } from 'vite';\nimport tsconfigPaths from 'vite-tsconfig-paths';\n\nexport default defineConfig({\n  plugins: [react(), tsconfigPaths()],\n  test: {\n    globals: true,\n    environment: 'jsdom',\n    setupFiles: './vitest.setup.mjs',\n  },\n  server: {\n    proxy: {\n      '/api': {\n        target: 'http://127.0.0.1:9093',\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "ui/mantine-ui/vitest.setup.mjs",
    "content": "import '@testing-library/jest-dom/vitest';\n\nimport { vi } from 'vitest';\n\nconst { getComputedStyle } = window;\nwindow.getComputedStyle = (elt) => getComputedStyle(elt);\nwindow.HTMLElement.prototype.scrollIntoView = () => {};\n\nObject.defineProperty(window, 'matchMedia', {\n  writable: true,\n  value: vi.fn().mockImplementation((query) => ({\n    matches: false,\n    media: query,\n    onchange: null,\n    addListener: vi.fn(),\n    removeListener: vi.fn(),\n    addEventListener: vi.fn(),\n    removeEventListener: vi.fn(),\n    dispatchEvent: vi.fn(),\n  })),\n});\n\nclass ResizeObserver {\n  observe() {}\n  unobserve() {}\n  disconnect() {}\n}\n\nwindow.ResizeObserver = ResizeObserver;\n"
  },
  {
    "path": "ui/web.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage ui\n\nimport (\n\t\"embed\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"net/http\"\n\t_ \"net/http/pprof\" // Comment this line to disable pprof endpoint.\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n\t\"github.com/prometheus/common/route\"\n)\n\n//go:embed app/script.js app/index.html app/favicon.ico app/lib\nvar asset embed.FS\n\n// Register registers handlers to serve files for the web interface.\nfunc Register(r *route.Router, reloadCh chan<- chan error, logger *slog.Logger) {\n\tr.Get(\"/metrics\", promhttp.Handler().ServeHTTP)\n\n\tappFS, err := fs.Sub(asset, \"app\")\n\tif err != nil {\n\t\tpanic(err) // During build step, we did not embed a directory named `app`.\n\t}\n\tfs := http.FileServerFS(appFS)\n\tr.Get(\"/\", func(w http.ResponseWriter, req *http.Request) {\n\t\tdisableCaching(w)\n\t\tfs.ServeHTTP(w, req)\n\t})\n\n\tr.Get(\"/script.js\", func(w http.ResponseWriter, req *http.Request) {\n\t\tdisableCaching(w)\n\t\tfs.ServeHTTP(w, req)\n\t})\n\n\tr.Get(\"/favicon.ico\", func(w http.ResponseWriter, req *http.Request) {\n\t\tdisableCaching(w)\n\t\tfs.ServeHTTP(w, req)\n\t})\n\n\tr.Get(\"/lib/*path\", func(w http.ResponseWriter, req *http.Request) {\n\t\tdisableCaching(w)\n\t\tfs.ServeHTTP(w, req)\n\t})\n\n\tr.Post(\"/-/reload\", func(w http.ResponseWriter, req *http.Request) {\n\t\terrc := make(chan error)\n\t\tdefer close(errc)\n\n\t\treloadCh <- errc\n\t\tif err := <-errc; err != nil {\n\t\t\thttp.Error(w, fmt.Sprintf(\"failed to reload config: %s\", err), http.StatusInternalServerError)\n\t\t}\n\t})\n\n\tr.Get(\"/-/healthy\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t\tfmt.Fprintf(w, \"OK\")\n\t})\n\tr.Head(\"/-/healthy\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t})\n\tr.Get(\"/-/ready\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t\tfmt.Fprintf(w, \"OK\")\n\t})\n\tr.Head(\"/-/ready\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t})\n\n\tdebugHandlerFunc := func(w http.ResponseWriter, req *http.Request) {\n\t\tsubpath := route.Param(req.Context(), \"subpath\")\n\t\treq.URL.Path = path.Join(\"/debug\", subpath)\n\t\t// path.Join removes trailing slashes, but some pprof handlers expect them.\n\t\tif strings.HasSuffix(subpath, \"/\") && !strings.HasSuffix(req.URL.Path, \"/\") {\n\t\t\treq.URL.Path += \"/\"\n\t\t}\n\t\thttp.DefaultServeMux.ServeHTTP(w, req)\n\t}\n\tr.Get(\"/debug/*subpath\", debugHandlerFunc)\n\tr.Post(\"/debug/*subpath\", debugHandlerFunc)\n}\n\nfunc disableCaching(w http.ResponseWriter) {\n\tw.Header().Set(\"Cache-Control\", \"no-cache, no-store, must-revalidate\")\n\tw.Header().Set(\"Pragma\", \"no-cache\")\n\tw.Header().Set(\"Expires\", \"0\") // Prevent proxies from caching.\n}\n"
  },
  {
    "path": "ui/web_test.go",
    "content": "// Copyright The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage ui\n\nimport (\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/prometheus/common/route\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDebugHandlersWithRoutePrefix(t *testing.T) {\n\tlogger := slog.New(slog.NewTextHandler(os.Stderr, nil))\n\treloadCh := make(chan chan error)\n\n\t// Test with route prefix\n\troutePrefix := \"/prometheus/alertmanager\"\n\trouter := route.New().WithPrefix(routePrefix)\n\tRegister(router, reloadCh, logger)\n\n\t// Test GET request to pprof index (note: pprof index returns text/html)\n\treq := httptest.NewRequest(\"GET\", routePrefix+\"/debug/pprof/\", nil)\n\tw := httptest.NewRecorder()\n\trouter.ServeHTTP(w, req)\n\n\trequire.Equal(t, http.StatusOK, w.Code)\n\trequire.Contains(t, w.Body.String(), \"/debug/pprof/\", \"pprof page did not load with expected content when using a route prefix\")\n\n\t// Test GET request to pprof heap endpoint\n\treq = httptest.NewRequest(\"GET\", routePrefix+\"/debug/pprof/heap\", nil)\n\tw = httptest.NewRecorder()\n\trouter.ServeHTTP(w, req)\n\n\trequire.Equal(t, http.StatusOK, w.Code)\n\n\t// Test without route prefix (should also work)\n\trouter2 := route.New()\n\tRegister(router2, reloadCh, logger)\n\n\treq = httptest.NewRequest(\"GET\", \"/debug/pprof/\", nil)\n\tw = httptest.NewRecorder()\n\trouter2.ServeHTTP(w, req)\n\n\trequire.Equal(t, http.StatusOK, w.Code)\n\trequire.Contains(t, w.Body.String(), \"/debug/pprof/\", \"pprof page did not load with expected content\")\n}\n\nfunc TestWebRoutes(t *testing.T) {\n\trouter := route.New()\n\tlogger := slog.New(slog.NewTextHandler(os.Stderr, nil))\n\tRegister(router, make(chan chan error), logger)\n\n\ttests := []struct {\n\t\tname         string\n\t\tpath         string\n\t\texpectedCode int\n\t}{\n\t\t{\n\t\t\tname: \"root\",\n\t\t\tpath: \"/\",\n\t\t},\n\t\t{\n\t\t\tname: \"script.js\",\n\t\t\tpath: \"/script.js\",\n\t\t},\n\t\t{\n\t\t\tname: \"favicon.ico\",\n\t\t\tpath: \"/favicon.ico\",\n\t\t},\n\t\t{\n\t\t\tname: \"Lib wildcard path\",\n\t\t\t// Replace with any path under `lib`, in case you want to remove elm-datepicker.\n\t\t\tpath: \"/lib/elm-datepicker/css/elm-datepicker.css\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(http.MethodGet, tt.path, nil)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\trouter.ServeHTTP(w, req)\n\n\t\t\tres := w.Result()\n\t\t\tdefer res.Body.Close()\n\n\t\t\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\t\t})\n\t}\n}\n"
  }
]