pax_global_header00006660000000000000000000000064131451236030014510gustar00rootroot0000000000000052 comment=62761fa03540d8b362851f16d7261c6d9e845d91 prometheus-alertmanager-0.6.2+ds/000077500000000000000000000000001314512360300167525ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/.dockerignore000066400000000000000000000000511314512360300214220ustar00rootroot00000000000000.build/ .tarballs/ !.build/linux-amd64/ prometheus-alertmanager-0.6.2+ds/.gitignore000066400000000000000000000002121314512360300207350ustar00rootroot00000000000000/data/ /alertmanager /amtool *.yml *.yaml /.build /.release /.tarballs !/doc/examples/simple.yml !/circle.yml !/.travis.yml !/.promu.yml prometheus-alertmanager-0.6.2+ds/.promu.yml000066400000000000000000000024161314512360300207200ustar00rootroot00000000000000repository: path: github.com/prometheus/alertmanager build: binaries: - name: alertmanager path: ./cmd/alertmanager - name: amtool path: ./cmd/amtool flags: -a -tags netgo ldflags: | -X {{repoPath}}/vendor/github.com/prometheus/common/version.Version={{.Version}} -X {{repoPath}}/vendor/github.com/prometheus/common/version.Revision={{.Revision}} -X {{repoPath}}/vendor/github.com/prometheus/common/version.Branch={{.Branch}} -X {{repoPath}}/vendor/github.com/prometheus/common/version.BuildUser={{user}}@{{host}} -X {{repoPath}}/vendor/github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} tarball: files: - doc/examples/simple.yml - LICENSE - NOTICE crossbuild: platforms: - linux/amd64 - linux/386 - darwin/amd64 - darwin/386 - windows/amd64 - windows/386 - freebsd/amd64 - freebsd/386 - openbsd/amd64 - openbsd/386 - netbsd/amd64 - netbsd/386 - dragonfly/amd64 - linux/arm - linux/arm64 - freebsd/arm - netbsd/arm - linux/ppc64 - linux/ppc64le - linux/mips64 - linux/mips64le prometheus-alertmanager-0.6.2+ds/.travis.yml000066400000000000000000000000661314512360300210650ustar00rootroot00000000000000sudo: false language: go go: - 1.7.3 script: - make prometheus-alertmanager-0.6.2+ds/CHANGELOG.md000066400000000000000000000171331314512360300205700ustar00rootroot00000000000000## v0.6.2 / 2017-05-09 * [BUGFIX] Correctly link to silences from alert again * [BUGFIX] Correctly hide silenced/show active alerts in UI again * [BUGFIX] Fix regression of alerts not being displayed until first processing * [BUGFIX] Fix internal usage of wrong lock for silence markers * [BUGFIX] Adapt amtool's API parsing to recent API changes * [BUGFIX] Correctly marshal regexes in config JSON response * [ENHANCEMENT] Anchor silence regex matchers to be consistent with Prometheus * [ENHANCEMENT] Error if root route is using `continue` keyword ## v0.6.1 / 2017-04-28 * [BUGFIX] Fix incorrectly serialized hash for notification providers. * [ENHANCEMENT] Add config hash metric. * [ENHANCEMENT] Add processing status field to alerts. ## v0.6.0 / 2017-04-25 * [BUGFIX] Add `groupKey` to `alerts/groups` endpoint https://github.com/prometheus/alertmanager/pull/576 * [BUGFIX] Only notify on firing alerts https://github.com/prometheus/alertmanager/pull/595 * [BUGFIX] Correctly marshal regex's in config for routing tree https://github.com/prometheus/alertmanager/pull/602 * [BUGFIX] Prevent panic when failing to load config https://github.com/prometheus/alertmanager/pull/607 * [BUGFIX] Prevent panic when alertmanager is started with an empty `-mesh.peer` https://github.com/prometheus/alertmanager/pull/726 * [CHANGE] Add `DELETE` as accepted CORS method https://github.com/prometheus/alertmanager/pull/641 * [CHANGE] Rename VictorOps config variables https://github.com/prometheus/alertmanager/pull/667 * [CHANGE] Switch to using `gogoproto` for protobuf https://github.com/prometheus/alertmanager/pull/715 * [CHANGE] No longer generate releases for openbsd/arm https://github.com/prometheus/alertmanager/pull/732 * [ENHANCEMENT] Add `reReplaceAll` template function https://github.com/prometheus/alertmanager/pull/639 * [ENHANCEMENT] Expose mesh peers on status page https://github.com/prometheus/alertmanager/pull/644 * [ENHANCEMENT] Allow label-based filtering alerts/silences through API https://github.com/prometheus/alertmanager/pull/633 * [ENHANCEMENT] Include notifier type in logs and errors https://github.com/prometheus/alertmanager/pull/702 * [ENHANCEMENT] Add commandline tool for interacting with alertmanager https://github.com/prometheus/alertmanager/pull/636 ## v0.5.1 / 2016-11-24 * [BUGFIX] Fix crash caused by race condition in silencing * [ENHANCEMENT] Improve logging of API errors * [ENHANCEMENT] Add metrics for the notification log ## v0.5.0 / 2016-11-01 This release requires a storage wipe. It contains fundamental internal changes that came with implementing the high availability mode. * [FEATURE] Alertmanager clustering for high availability * [FEATURE] Garbage collection of old silences and notification logs * [CHANGE] New storage format * [CHANGE] Stricter silence semantics for consistent historical view ## v0.4.2 / 2016-09-02 * [BUGFIX] Fix broken regex checkbox in silence form * [BUGFIX] Simplify inconsistent silence update behavior ## v0.4.1 / 2016-08-31 * [BUGFIX] Wait for silence query to finish instead of showing error * [BUGFIX] Fix sorting of silences * [BUGFIX] Provide visual feedback after creating a silence * [BUGFIX] Fix styling of silences * [ENHANCEMENT] Provide cleaner API silence interface ## v0.4.0 / 2016-08-23 * [FEATURE] Silences are now paginated in the web ui * [CHANGE] Failure to start on unparsed flags ## 0.3.0 / 2016-07-07 * [CHANGE] Alerts are purely in memory and no longer persistent across restarts * [FEATURE] Add SMTP LOGIN authentication mechanism ## 0.2.1 / 2016-06-23 * [ENHANCEMENT] Allow inheritance of route receiver * [ENHANCEMENT] Add silence cache to silence provider * [BUGFIX] Fix HipChat room number in integration URL ## 0.2.0 / 2016-06-17 This release uses a new storage backend based on BoltDB. You have to backup and wipe your former storage path to run it. * [CHANGE] Use BoltDB as data store. * [CHANGE] Move SMTP authentification to configuration file * [FEATURE] add /-/reload HTTP endpoint * [FEATURE] Filter silenced alerts in web UI * [ENHANCEMENT] reduce inhibition computation complexity * [ENHANCEMENT] Add support for teams and tags in OpsGenie integration * [BUGFIX] Handle OpsGenie responses correctly * [BUGFIX] Fix Pushover queue length issue * [BUGFIX] STARTTLS before querying auth mechanism in email integration ## 0.1.1 / 2016-03-15 * [BUGFIX] Fix global database lock issue * [ENHANCEMENT] Improve SQLite alerts index * [ENHANCEMENT] Enable debug endpoint ## 0.1.0 / 2016-02-23 This version is a full rewrite of the Alertmanager with a very different feature set. Thus, there is no meaningful changelog. Changes with respect to 0.1.0-beta2: * [CHANGE] Expose same data structure to templates and webhook * [ENHANCEMENT] Show generator URL in default templates and web UI * [ENHANCEMENT] Support for Slack icon_emoji field * [ENHANCEMENT] Expose incident key to templates and webhook data * [ENHANCEMENT] Allow markdown in Slack 'text' field * [BUGFIX] Fixed database locking issue ## 0.1.0-beta2 / 2016-02-03 * [BUGFIX] Properly set timeout for incoming alerts with fixed start time * [ENHANCEMENT] Send source field in OpsGenie integration * [ENHANCEMENT] Improved routing configuration validation * [FEATURE] Basic instrumentation added ## 0.1.0-beta1 / 2016-01-08 * [BUGFIX] Send full alert group state on each update. Fixes erroneous resolved notifications. * [FEATURE] HipChat integration * [CHANGE] Slack integration no longer sends resolved notifications by default ## 0.1.0-beta0 / 2015-12-23 This version is a full rewrite of the Alertmanager with a very different feature set. Thus, there is no meaningful changelog. ## 0.0.4 / 2015-09-09 * [BUGFIX] Fix version info string in startup message. * [BUGFIX] Fix Pushover notifications by setting the right priority level, as well as required retry and expiry intervals. * [FEATURE] Make it possible to link to individual alerts in the UI. * [FEATURE] Rearrange alert columns in UI and allow expanding more alert details. * [FEATURE] Add Amazon SNS notifications. * [FEATURE] Add OpsGenie Webhook notifications. * [FEATURE] Add `-web.external-url` flag to control the externally visible Alertmanager URL. * [FEATURE] Add runbook and alertmanager URLs to PagerDuty and email notifications. * [FEATURE] Add a GET API to /api/alerts which pulls JSON formatted AlertAggregates. * [ENHANCEMENT] Sort alerts consistently in web UI. * [ENHANCEMENT] Suggest to use email address as silence creator. * [ENHANCEMENT] Make Slack timeout configurable. * [ENHANCEMENT] Add channel name to error logging about Slack notifications. * [ENHANCEMENT] Refactoring and tests for Flowdock notifications. * [ENHANCEMENT] New Dockerfile using alpine-golang-make-onbuild base image. * [CLEANUP] Add Docker instructions and other cleanups in README.md. * [CLEANUP] Update Makefile.COMMON from prometheus/utils. ## 0.0.3 / 2015-06-10 * [BUGFIX] Fix email template body writer being called with parameters in wrong order. ## 0.0.2 / 2015-06-09 * [BUGFIX] Fixed silences.json permissions in Docker image. * [CHANGE] Changed case of API JSON properties to initial lower letter. * [CHANGE] Migrated logging to use http://github.com/prometheus/log. * [FEATURE] Flowdock notification support. * [FEATURE] Slack notification support. * [FEATURE] Generic webhook notification support. * [FEATURE] Support for "@"-mentions in HipChat notifications. * [FEATURE] Path prefix option to support reverse proxies. * [ENHANCEMENT] Improved web redirection and 404 behavior. * [CLEANUP] Updated compiled web assets from source. * [CLEANUP] Updated fsnotify package to its new source location. * [CLEANUP] Updates to README.md and AUTHORS.md. * [CLEANUP] Various smaller cleanups and improvements. prometheus-alertmanager-0.6.2+ds/CONTRIBUTING.md000066400000000000000000000015461314512360300212110ustar00rootroot00000000000000# Contributing Prometheus uses GitHub to manage reviews of pull requests. * If you have a trivial fix or improvement, go ahead and create a pull request, addressing (with `@...`) the maintainer of this repository (see [MAINTAINERS.md](MAINTAINERS.md)) in the description of the pull request. * If you plan to do something more involved, first discuss your ideas on our [mailing list](https://groups.google.com/forum/?fromgroups#!forum/prometheus-developers). This will avoid unnecessary work and surely give you and us a good deal of inspiration. * Relevant coding style guidelines are the [Go Code Review Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments) and the _Formatting and style_ section of Peter Bourgon's [Go: Best Practices for Production Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style). prometheus-alertmanager-0.6.2+ds/Dockerfile000066400000000000000000000006611314512360300207470ustar00rootroot00000000000000FROM prom/busybox:latest MAINTAINER The Prometheus Authors COPY alertmanager /bin/alertmanager COPY doc/examples/simple.yml /etc/alertmanager/config.yml EXPOSE 9093 VOLUME [ "/alertmanager" ] WORKDIR /alertmanager ENTRYPOINT [ "/bin/alertmanager" ] CMD [ "-config.file=/etc/alertmanager/config.yml", \ "-storage.path=/alertmanager" ] prometheus-alertmanager-0.6.2+ds/LICENSE000066400000000000000000000261351314512360300177660ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. prometheus-alertmanager-0.6.2+ds/MAINTAINERS.md000066400000000000000000000000451314512360300210450ustar00rootroot00000000000000* Stuart Nelson prometheus-alertmanager-0.6.2+ds/Makefile000066400000000000000000000040261314512360300204140ustar00rootroot00000000000000# Copyright 2015 The Prometheus Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. GO := GO15VENDOREXPERIMENT=1 go PROMU := $(GOPATH)/bin/promu pkgs = $(shell $(GO) list ./... | grep -v /vendor/) PREFIX ?= $(shell pwd) BIN_DIR ?= $(shell pwd) DOCKER_IMAGE_NAME ?= alertmanager DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) ifdef DEBUG bindata_flags = -debug endif all: format build test test: @echo ">> running tests" @$(GO) test -short $(pkgs) style: @echo ">> checking code style" @! gofmt -d $(shell find . -path ./vendor -prune -o -name '*.go' -print) | grep '^' format: @echo ">> formatting code" @$(GO) fmt $(pkgs) vet: @echo ">> vetting code" @$(GO) vet $(pkgs) build: promu @echo ">> building binaries" @$(PROMU) build --prefix $(PREFIX) tarball: promu @echo ">> building release tarball" @$(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) docker: @echo ">> building docker image" @docker build -t "$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" . assets: @echo ">> writing assets" -@$(GO) get -u github.com/jteeuwen/go-bindata/... @go-bindata $(bindata_flags) -pkg ui -o ui/bindata.go ui/... @go-bindata $(bindata_flags) -pkg deftmpl -o template/internal/deftmpl/bindata.go template/default.tmpl promu: @GOOS=$(shell uname -s | tr A-Z a-z) \ GOARCH=$(subst x86_64,amd64,$(patsubst i%86,386,$(shell uname -m))) \ $(GO) get -u github.com/prometheus/promu proto: scripts/genproto.sh .PHONY: all style format build test vet assets tarball docker promu proto prometheus-alertmanager-0.6.2+ds/NOTICE000066400000000000000000000007111314512360300176550ustar00rootroot00000000000000Prometheus Alertmanager Copyright 2013-2015 The Prometheus Authors This product includes software developed at SoundCloud Ltd. (http://soundcloud.com/). The following components are included in this product: Bootstrap http://getbootstrap.com Copyright 2011-2014 Twitter, Inc. Licensed under the MIT License bootstrap-datetimepicker.js http://www.eyecon.ro/bootstrap-datepicker Copyright 2012 Stefan Petre Licensed under the Apache License, Version 2.0 prometheus-alertmanager-0.6.2+ds/Procfile000066400000000000000000000013051314512360300204370ustar00rootroot00000000000000a1: ./alertmanager -log.level=debug -storage.path=$TMPDIR/a1 -web.listen-address=:9093 -mesh.peer-id=00:00:00:00:00:01 -mesh.nickname=a -mesh.listen-address=:8001 -config.file=examples/ha/alertmanager.yaml a2: ./alertmanager -log.level=debug -storage.path=$TMPDIR/a2 -web.listen-address=:9094 -mesh.peer-id=00:00:00:00:00:02 -mesh.nickname=b -mesh.listen-address=:8002 -mesh.peer=127.0.0.1:8001 -config.file=examples/ha/alertmanager.yaml a3: ./alertmanager -log.level=debug -storage.path=$TMPDIR/a3 -web.listen-address=:9095 -mesh.peer-id=00:00:00:00:00:03 -mesh.nickname=c -mesh.listen-address=:8003 -mesh.peer=127.0.0.1:8001 -config.file=examples/ha/alertmanager.yaml wh: go run ./examples/webhook/echo.go prometheus-alertmanager-0.6.2+ds/README.md000066400000000000000000000155671314512360300202470ustar00rootroot00000000000000# Alertmanager [![Build Status](https://travis-ci.org/prometheus/alertmanager.svg)][travis] [![CircleCI](https://circleci.com/gh/prometheus/alertmanager/tree/master.svg?style=shield)][circleci] [![Docker Repository on Quay](https://quay.io/repository/prometheus/alertmanager/status)][quay] [![Docker Pulls](https://img.shields.io/docker/pulls/prom/alertmanager.svg?maxAge=604800)][hub] The 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 such as email, PagerDuty, or OpsGenie. It also takes care of silencing and inhibition of alerts. * [Documentation](http://prometheus.io/docs/alerting/alertmanager/) ## Install There are various ways of installing Alertmanager. ### Precompiled binaries Precompiled binaries for released versions are available in the [*download* section](https://prometheus.io/download/) on [prometheus.io](https://prometheus.io). Using the latest production release binary is the recommended way of installing Alertmanager. ### Docker images Docker images are available on [Quay.io](https://quay.io/repository/prometheus/alertmanager). ### Compiling the binary You can either `go get` it: ``` $ GO15VENDOREXPERIMENT=1 go get github.com/prometheus/alertmanager/cmd/... # cd $GOPATH/src/github.com/prometheus/alertmanager $ alertmanager -config.file= ``` Or checkout the source code and build manually: ``` $ mkdir -p $GOPATH/src/github.com/prometheus $ cd $GOPATH/src/github.com/prometheus $ git clone https://github.com/prometheus/alertmanager.git $ cd alertmanager $ make build $ ./alertmanager -config.file= ``` ## Example This 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/). ```yaml global: # The smarthost and SMTP sender used for mail notifications. smtp_smarthost: 'localhost:25' smtp_from: 'alertmanager@example.org' # The root route on which each incoming alert enters. route: # The root route must not have any matchers as it is the entry point for # all alerts. It needs to have a receiver configured so alerts that do not # match any of the sub-routes are sent to someone. receiver: 'team-X-mails' # The labels by which incoming alerts are grouped together. For example, # multiple alerts coming in for cluster=A and alertname=LatencyHigh would # be batched into a single group. group_by: ['alertname', 'cluster'] # When a new group of alerts is created by an incoming alert, wait at # least 'group_wait' to send the initial notification. # This way ensures that you get multiple alerts for the same group that start # firing shortly after another are batched together on the first # notification. group_wait: 30s # When the first notification was sent, wait 'group_interval' to send a batch # of new alerts that started firing for that group. group_interval: 5m # If an alert has successfully been sent, wait 'repeat_interval' to # resend them. repeat_interval: 3h # All the above attributes are inherited by all child routes and can # overwritten on each. # The child route trees. routes: # This routes performs a regular expression match on alert labels to # catch alerts that are related to a list of services. - match_re: service: ^(foo1|foo2|baz)$ receiver: team-X-mails # The service has a sub-route for critical alerts, any alerts # that do not match, i.e. severity != critical, fall-back to the # parent node and are sent to 'team-X-mails' routes: - match: severity: critical receiver: team-X-pager - match: service: files receiver: team-Y-mails routes: - match: severity: critical receiver: team-Y-pager # This route handles all alerts coming from a database service. If there's # no team to handle it, it defaults to the DB team. - match: service: database receiver: team-DB-pager # Also group alerts by affected database. group_by: [alertname, cluster, database] routes: - match: owner: team-X receiver: team-X-pager - match: owner: team-Y receiver: team-Y-pager # Inhibition rules allow to mute a set of alerts given that another alert is # firing. # We use this to mute any warning-level notifications if the same alert is # already critical. inhibit_rules: - source_match: severity: 'critical' target_match: severity: 'warning' # Apply inhibition if the alertname is the same. equal: ['alertname'] receivers: - name: 'team-X-mails' email_configs: - to: 'team-X+alerts@example.org' - name: 'team-X-pager' email_configs: - to: 'team-X+alerts-critical@example.org' pagerduty_configs: - service_key: - name: 'team-Y-mails' email_configs: - to: 'team-Y+alerts@example.org' - name: 'team-Y-pager' pagerduty_configs: - service_key: - name: 'team-DB-pager' pagerduty_configs: - service_key: ``` ## High Availability > Warning: High Availablility is under active development To create a highly available cluster of the Alertmanager the instances need to be configured to communicate with each other. This is configured using the `-mesh.*` flags. - `-mesh.peer-id` string: mesh peer ID (default "<hardware-mac-address>") - `-mesh.listen-address` string: mesh listen address (default "0.0.0.0:6783") - `-mesh.nickname` string: mesh peer nickname (default "<machine-hostname>") - `-mesh.peer` value: initial peers (repeat flag for each additional peer) The `mesh.peer-id` flag is used as a unique ID among the peers. It defaults to the MAC address, therefore the default value should typically be a good option. The same applies to the default of the `mesh.nickname` flag, as it defaults to the hostname. The chosen port in the `mesh.listen-address` flag is the port that needs to be specified in the `mesh.peer` flag of the other peers. To start a cluster of three peers on your local machine use `goreman` and the Procfile within this repository. goreman start To point your prometheus instance to multiple Alertmanagers use the `-alertmanager.url` parameter. It allows passing in a comma separated list. Start your prometheus like this, for example: ./prometheus -config.file=prometheus.yml -alertmanager.url http://localhost:9095,http://localhost:9094,http://localhost:9093 > Note: make sure to have a valid `prometheus.yml` in your current directory ## Architecture ![](https://raw.githubusercontent.com/prometheus/alertmanager/4e6695682acd2580773a904e4aa2e3b927ee27b7/doc/arch.jpg) [travis]: https://travis-ci.org/prometheus/alertmanager [hub]: https://hub.docker.com/r/prom/alertmanager/ [circleci]: https://circleci.com/gh/prometheus/alertmanager [quay]: https://quay.io/repository/prometheus/alertmanager prometheus-alertmanager-0.6.2+ds/VERSION000066400000000000000000000000061314512360300200160ustar00rootroot000000000000000.6.2 prometheus-alertmanager-0.6.2+ds/api/000077500000000000000000000000001314512360300175235ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/api/api.go000066400000000000000000000342311314512360300206260ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package api import ( "encoding/json" "fmt" "net/http" "sync" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/log" "github.com/prometheus/common/model" "github.com/prometheus/common/route" "github.com/prometheus/common/version" "github.com/prometheus/prometheus/pkg/labels" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/dispatch" "github.com/prometheus/alertmanager/pkg/parse" "github.com/prometheus/alertmanager/provider" "github.com/prometheus/alertmanager/silence" "github.com/prometheus/alertmanager/silence/silencepb" "github.com/prometheus/alertmanager/types" "github.com/weaveworks/mesh" ) var ( numReceivedAlerts = prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "alertmanager", Name: "alerts_received_total", Help: "The total number of received alerts.", }, []string{"status"}) numInvalidAlerts = prometheus.NewCounter(prometheus.CounterOpts{ Namespace: "alertmanager", Name: "alerts_invalid_total", Help: "The total number of received alerts that were invalid.", }) ) func init() { prometheus.Register(numReceivedAlerts) prometheus.Register(numInvalidAlerts) } var corsHeaders = map[string]string{ "Access-Control-Allow-Headers": "Accept, Authorization, Content-Type, Origin", "Access-Control-Allow-Methods": "GET, DELETE, OPTIONS", "Access-Control-Allow-Origin": "*", "Access-Control-Expose-Headers": "Date", } // Enables cross-site script calls. func setCORS(w http.ResponseWriter) { for h, v := range corsHeaders { w.Header().Set(h, v) } } // API provides registration of handlers for API routes. type API struct { alerts provider.Alerts silences *silence.Silences config string configJSON config.Config resolveTimeout time.Duration uptime time.Time mrouter *mesh.Router groups groupsFn mtx sync.RWMutex } type groupsFn func([]*labels.Matcher) dispatch.AlertOverview // New returns a new API. func New(alerts provider.Alerts, silences *silence.Silences, gf groupsFn, router *mesh.Router) *API { return &API{ alerts: alerts, silences: silences, groups: gf, uptime: time.Now(), mrouter: router, } } // Register registers the API handlers under their correct routes // in the given router. func (api *API) Register(r *route.Router) { ihf := func(name string, f http.HandlerFunc) http.HandlerFunc { return prometheus.InstrumentHandlerFunc(name, func(w http.ResponseWriter, r *http.Request) { setCORS(w) f(w, r) }) } r.Options("/*path", ihf("options", func(w http.ResponseWriter, r *http.Request) {})) // Register legacy forwarder for alert pushing. r.Post("/alerts", ihf("legacy_add_alerts", api.legacyAddAlerts)) // Register actual API. r = r.WithPrefix("/v1") r.Get("/status", ihf("status", api.status)) r.Get("/alerts/groups", ihf("alert_groups", api.alertGroups)) r.Get("/alerts", ihf("list_alerts", api.listAlerts)) r.Post("/alerts", ihf("add_alerts", api.addAlerts)) r.Get("/silences", ihf("list_silences", api.listSilences)) r.Post("/silences", ihf("add_silence", api.addSilence)) r.Get("/silence/:sid", ihf("get_silence", api.getSilence)) r.Del("/silence/:sid", ihf("del_silence", api.delSilence)) } // Update sets the configuration string to a new value. func (api *API) Update(cfg string, resolveTimeout time.Duration) error { api.mtx.Lock() defer api.mtx.Unlock() api.config = cfg api.resolveTimeout = resolveTimeout configJSON, err := config.Load(cfg) if err != nil { log.Errorf("error: %v", err) return err } api.configJSON = *configJSON return nil } type errorType string const ( errorNone errorType = "" errorInternal = "server_error" errorBadData = "bad_data" ) type apiError struct { typ errorType err error } func (e *apiError) Error() string { return fmt.Sprintf("%s: %s", e.typ, e.err) } func (api *API) status(w http.ResponseWriter, req *http.Request) { api.mtx.RLock() var status = struct { Config string `json:"config"` ConfigJSON config.Config `json:"configJSON"` VersionInfo map[string]string `json:"versionInfo"` Uptime time.Time `json:"uptime"` MeshStatus meshStatus `json:"meshStatus"` }{ Config: api.config, ConfigJSON: api.configJSON, VersionInfo: map[string]string{ "version": version.Version, "revision": version.Revision, "branch": version.Branch, "buildUser": version.BuildUser, "buildDate": version.BuildDate, "goVersion": version.GoVersion, }, Uptime: api.uptime, MeshStatus: getMeshStatus(api), } api.mtx.RUnlock() respond(w, status) } type meshStatus struct { Name string `json:"name"` NickName string `json:"nickName"` Peers []peerStatus `json:"peers"` } type peerStatus struct { Name string `json:"name"` // e.g. "00:00:00:00:00:01" NickName string `json:"nickName"` // e.g. "a" UID uint64 `json:"uid"` // e.g. "14015114173033265000" } func getMeshStatus(api *API) meshStatus { status := mesh.NewStatus(api.mrouter) strippedStatus := meshStatus{ Name: status.Name, NickName: status.NickName, Peers: make([]peerStatus, len(status.Peers)), } for i := 0; i < len(status.Peers); i++ { strippedStatus.Peers[i] = peerStatus{ Name: status.Peers[i].Name, NickName: status.Peers[i].NickName, UID: uint64(status.Peers[i].UID), } } return strippedStatus } func (api *API) alertGroups(w http.ResponseWriter, req *http.Request) { var err error matchers := []*labels.Matcher{} if filter := req.FormValue("filter"); filter != "" { matchers, err = parse.Matchers(filter) if err != nil { respondError(w, apiError{ typ: errorBadData, err: err, }, nil) return } } groups := api.groups(matchers) respond(w, groups) } func (api *API) listAlerts(w http.ResponseWriter, r *http.Request) { alerts := api.alerts.GetPending() defer alerts.Close() var ( err error res []*types.Alert ) // TODO(fabxc): enforce a sensible timeout. for a := range alerts.Next() { if err = alerts.Err(); err != nil { break } res = append(res, a) } if err != nil { respondError(w, apiError{ typ: errorInternal, err: err, }, nil) return } respond(w, types.Alerts(res...)) } func (api *API) legacyAddAlerts(w http.ResponseWriter, r *http.Request) { var legacyAlerts = []struct { Summary model.LabelValue `json:"summary"` Description model.LabelValue `json:"description"` Runbook model.LabelValue `json:"runbook"` Labels model.LabelSet `json:"labels"` Payload model.LabelSet `json:"payload"` }{} if err := receive(r, &legacyAlerts); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } var alerts []*types.Alert for _, la := range legacyAlerts { a := &types.Alert{ Alert: model.Alert{ Labels: la.Labels, Annotations: la.Payload, }, } if a.Annotations == nil { a.Annotations = model.LabelSet{} } a.Annotations["summary"] = la.Summary a.Annotations["description"] = la.Description a.Annotations["runbook"] = la.Runbook alerts = append(alerts, a) } api.insertAlerts(w, r, alerts...) } func (api *API) addAlerts(w http.ResponseWriter, r *http.Request) { var alerts []*types.Alert if err := receive(r, &alerts); err != nil { respondError(w, apiError{ typ: errorBadData, err: err, }, nil) return } api.insertAlerts(w, r, alerts...) } func (api *API) insertAlerts(w http.ResponseWriter, r *http.Request, alerts ...*types.Alert) { now := time.Now() for _, alert := range alerts { alert.UpdatedAt = now // Ensure StartsAt is set. if alert.StartsAt.IsZero() { alert.StartsAt = now } // If no end time is defined, set a timeout after which an alert // is marked resolved if it is not updated. if alert.EndsAt.IsZero() { alert.Timeout = true alert.EndsAt = now.Add(api.resolveTimeout) numReceivedAlerts.WithLabelValues("firing").Inc() } else { numReceivedAlerts.WithLabelValues("resolved").Inc() } } // Make a best effort to insert all alerts that are valid. var ( validAlerts = make([]*types.Alert, 0, len(alerts)) validationErrs = &types.MultiError{} ) for _, a := range alerts { if err := a.Validate(); err != nil { validationErrs.Add(err) numInvalidAlerts.Inc() continue } validAlerts = append(validAlerts, a) } if err := api.alerts.Put(validAlerts...); err != nil { respondError(w, apiError{ typ: errorInternal, err: err, }, nil) return } if validationErrs.Len() > 0 { respondError(w, apiError{ typ: errorBadData, err: validationErrs, }, nil) return } respond(w, nil) } func (api *API) addSilence(w http.ResponseWriter, r *http.Request) { var sil types.Silence if err := receive(r, &sil); err != nil { respondError(w, apiError{ typ: errorBadData, err: err, }, nil) return } psil, err := silenceToProto(&sil) if err != nil { respondError(w, apiError{ typ: errorBadData, err: err, }, nil) return } // Drop start time for new silences so we default to now. if sil.ID == "" && sil.StartsAt.Before(time.Now()) { psil.StartsAt = time.Time{} } sid, err := api.silences.Create(psil) if err != nil { respondError(w, apiError{ typ: errorInternal, err: err, }, nil) return } respond(w, struct { SilenceID string `json:"silenceId"` }{ SilenceID: sid, }) } func (api *API) getSilence(w http.ResponseWriter, r *http.Request) { sid := route.Param(r.Context(), "sid") sils, err := api.silences.Query(silence.QIDs(sid)) if err != nil || len(sils) == 0 { http.Error(w, fmt.Sprint("Error getting silence: ", err), http.StatusNotFound) return } sil, err := silenceFromProto(sils[0]) if err != nil { respondError(w, apiError{ typ: errorInternal, err: err, }, nil) return } respond(w, sil) } func (api *API) delSilence(w http.ResponseWriter, r *http.Request) { sid := route.Param(r.Context(), "sid") if err := api.silences.Expire(sid); err != nil { respondError(w, apiError{ typ: errorBadData, err: err, }, nil) return } respond(w, nil) } func (api *API) listSilences(w http.ResponseWriter, r *http.Request) { psils, err := api.silences.Query() if err != nil { respondError(w, apiError{ typ: errorInternal, err: err, }, nil) return } matchers := []*labels.Matcher{} if filter := r.FormValue("filter"); filter != "" { matchers, err = parse.Matchers(filter) if err != nil { respondError(w, apiError{ typ: errorBadData, err: err, }, nil) return } } sils := []*types.Silence{} for _, ps := range psils { s, err := silenceFromProto(ps) if err != nil { respondError(w, apiError{ typ: errorInternal, err: err, }, nil) return } if !matchesFilterLabels(s, matchers) { continue } sils = append(sils, s) } respond(w, sils) } func matchesFilterLabels(s *types.Silence, matchers []*labels.Matcher) bool { sms := map[string]string{} for _, m := range s.Matchers { sms[m.Name] = m.Value } for _, m := range matchers { if v, prs := sms[m.Name]; !prs || !m.Matches(v) { return false } } return true } func silenceToProto(s *types.Silence) (*silencepb.Silence, error) { sil := &silencepb.Silence{ Id: s.ID, StartsAt: s.StartsAt, EndsAt: s.EndsAt, UpdatedAt: s.UpdatedAt, } for _, m := range s.Matchers { matcher := &silencepb.Matcher{ Name: m.Name, Pattern: m.Value, Type: silencepb.Matcher_EQUAL, } if m.IsRegex { matcher.Type = silencepb.Matcher_REGEXP } sil.Matchers = append(sil.Matchers, matcher) } sil.Comments = append(sil.Comments, &silencepb.Comment{ Timestamp: s.UpdatedAt, Author: s.CreatedBy, Comment: s.Comment, }) return sil, nil } func silenceFromProto(s *silencepb.Silence) (*types.Silence, error) { sil := &types.Silence{ ID: s.Id, StartsAt: s.StartsAt, EndsAt: s.EndsAt, UpdatedAt: s.UpdatedAt, } for _, m := range s.Matchers { matcher := &types.Matcher{ Name: m.Name, Value: m.Pattern, } switch m.Type { case silencepb.Matcher_EQUAL: case silencepb.Matcher_REGEXP: matcher.IsRegex = true default: return nil, fmt.Errorf("unknown matcher type") } sil.Matchers = append(sil.Matchers, matcher) } if len(s.Comments) > 0 { sil.CreatedBy = s.Comments[0].Author sil.Comment = s.Comments[0].Comment } return sil, nil } type status string const ( statusSuccess status = "success" statusError = "error" ) type response struct { Status status `json:"status"` Data interface{} `json:"data,omitempty"` ErrorType errorType `json:"errorType,omitempty"` Error string `json:"error,omitempty"` } func respond(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) b, err := json.Marshal(&response{ Status: statusSuccess, Data: data, }) if err != nil { log.Errorf("errorr: %v", err) return } w.Write(b) } func respondError(w http.ResponseWriter, apiErr apiError, data interface{}) { w.Header().Set("Content-Type", "application/json") switch apiErr.typ { case errorBadData: w.WriteHeader(http.StatusBadRequest) case errorInternal: w.WriteHeader(http.StatusInternalServerError) default: panic(fmt.Sprintf("unknown error type %q", apiErr)) } b, err := json.Marshal(&response{ Status: statusError, ErrorType: apiErr.typ, Error: apiErr.err.Error(), Data: data, }) if err != nil { return } log.Errorf("api error: %v", apiErr.Error()) w.Write(b) } func receive(r *http.Request, v interface{}) error { dec := json.NewDecoder(r.Body) defer r.Body.Close() err := dec.Decode(v) if err != nil { log.Debugf("Decoding request failed: %v", err) } return err } prometheus-alertmanager-0.6.2+ds/artifacts/000077500000000000000000000000001314512360300207325ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/artifacts/README.md000066400000000000000000000007051314512360300222130ustar00rootroot00000000000000# Generating amtool artifacts Amtool comes with the option to create a number of eaze-of-use artifacts that can be created. go run generate_amtool_artifacts.go ## Bash completion The bash completion file can be added to `/etc/bash_completion.d/`. ## Man pages Man pages can be added to the man directory of your choice cp artifacts/*.1 /usr/local/share/man/man1/ sudo mandb Then you should be able to view the man pages as expected. prometheus-alertmanager-0.6.2+ds/artifacts/generate_amtool_artifacts.go000066400000000000000000000004371314512360300264720ustar00rootroot00000000000000package main import ( "github.com/spf13/cobra/doc" "github.com/prometheus/alertmanager/cli" ) func main() { cli.RootCmd.GenBashCompletionFile("amtool_completion.sh") header := &doc.GenManHeader{ Title: "amtool", Section: "1", } doc.GenManTree(cli.RootCmd, header, ".") } prometheus-alertmanager-0.6.2+ds/circle.yml000066400000000000000000000045101314512360300207360ustar00rootroot00000000000000machine: environment: DOCKER_IMAGE_NAME: prom/alertmanager QUAY_IMAGE_NAME: quay.io/prometheus/alertmanager DOCKER_TEST_IMAGE_NAME: quay.io/prometheus/golang-builder:1.8-base REPO_PATH: github.com/prometheus/alertmanager pre: - sudo curl -L -o /usr/bin/docker 'https://s3-external-1.amazonaws.com/circle-downloads/docker-1.9.1-circleci' - sudo chmod 0755 /usr/bin/docker - sudo curl -L 'https://github.com/aktau/github-release/releases/download/v0.6.2/linux-amd64-github-release.tar.bz2' | tar xvjf - --strip-components 3 -C $HOME/bin services: - docker dependencies: pre: - make promu - docker info override: - promu crossbuild - ln -s .build/linux-amd64/alertmanager alertmanager - | if [ -n "$CIRCLE_TAG" ]; then make docker DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME DOCKER_IMAGE_TAG=$CIRCLE_TAG make docker DOCKER_IMAGE_NAME=$QUAY_IMAGE_NAME DOCKER_IMAGE_TAG=$CIRCLE_TAG else make docker DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME make docker DOCKER_IMAGE_NAME=$QUAY_IMAGE_NAME fi post: - mkdir $CIRCLE_ARTIFACTS/binaries/ && cp -a .build/* $CIRCLE_ARTIFACTS/binaries/ - docker images test: override: - docker run --rm -t -v "$(pwd):/app" "${DOCKER_TEST_IMAGE_NAME}" -i "${REPO_PATH}" -T deployment: hub_branch: branch: master owner: prometheus commands: - docker login -e $DOCKER_EMAIL -u $DOCKER_LOGIN -p $DOCKER_PASSWORD - docker login -e $QUAY_EMAIL -u $QUAY_LOGIN -p $QUAY_PASSWORD quay.io - docker push $DOCKER_IMAGE_NAME - docker push $QUAY_IMAGE_NAME hub_tag: tag: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ owner: prometheus commands: - promu crossbuild tarballs - promu checksum .tarballs - promu release .tarballs - mkdir $CIRCLE_ARTIFACTS/releases/ && cp -a .tarballs/* $CIRCLE_ARTIFACTS/releases/ - docker login -e $DOCKER_EMAIL -u $DOCKER_LOGIN -p $DOCKER_PASSWORD - docker login -e $QUAY_EMAIL -u $QUAY_LOGIN -p $QUAY_PASSWORD quay.io - | if [[ "$CIRCLE_TAG" =~ ^v[0-9]+(\.[0-9]+){2}$ ]]; then docker tag "$DOCKER_IMAGE_NAME:$CIRCLE_TAG" "$DOCKER_IMAGE_NAME:latest" docker tag "$QUAY_IMAGE_NAME:$CIRCLE_TAG" "$QUAY_IMAGE_NAME:latest" fi - docker push $DOCKER_IMAGE_NAME - docker push $QUAY_IMAGE_NAME prometheus-alertmanager-0.6.2+ds/cli/000077500000000000000000000000001314512360300175215ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/cli/alert.go000066400000000000000000000117411314512360300211630ustar00rootroot00000000000000package cli import ( "encoding/json" "errors" "fmt" "net/http" "net/url" "path" "strings" "time" "github.com/prometheus/alertmanager/cli/format" "github.com/prometheus/alertmanager/dispatch" "github.com/prometheus/alertmanager/pkg/parse" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" "github.com/spf13/cobra" "github.com/spf13/viper" ) type alertmanagerAlertResponse struct { Status string `json:"status"` Data []*alertGroup `json:"data,omitempty"` ErrorType string `json:"errorType,omitempty"` Error string `json:"error,omitempty"` } type alertGroup struct { Labels model.LabelSet `json:"labels"` GroupKey string `json:"groupKey"` Blocks []*alertBlock `json:"blocks"` } type alertBlock struct { RouteOpts interface{} `json:"routeOpts"` Alerts []*dispatch.APIAlert `json:"alerts"` } // alertCmd represents the alert command var alertCmd = &cobra.Command{ Use: "alert", Short: "View and search through current alerts", Long: `View and search through current alerts. Amtool has a simplified prometheus query syntax, but contains robust support for bash variable expansions. The non-option section of arguments constructs a list of "Matcher Groups" that will be used to filter your query. The following examples will attempt to show this behaviour in action: amtool alert query alertname=foo node=bar This query will match all alerts with the alertname=foo and node=bar label value pairs set. amtool alert query foo node=bar If alertname is ommited and the first argument does not contain a '=' or a '=~' then it will be assumed to be the value of the alertname pair. amtool alert query 'alertname=~foo.*' As well as direct equality, regex matching is also supported. The '=~' syntax (similar to prometheus) is used to represent a regex match. Regex matching can be used in combination with a direct match. `, Run: CommandWrapper(queryAlerts), } var alertQueryCmd = &cobra.Command{ Use: "query", Short: "View and search through current alerts", Long: alertCmd.Long, RunE: queryAlerts, } func init() { RootCmd.AddCommand(alertCmd) alertCmd.AddCommand(alertQueryCmd) alertQueryCmd.Flags().Bool("expired", false, "Show expired alerts as well as active") alertQueryCmd.Flags().BoolP("silenced", "s", false, "Show silenced alerts") viper.BindPFlag("expired", alertQueryCmd.Flags().Lookup("expired")) viper.BindPFlag("silenced", alertQueryCmd.Flags().Lookup("silenced")) } func fetchAlerts(filter string) ([]*dispatch.APIAlert, error) { alertResponse := alertmanagerAlertResponse{} u, err := GetAlertmanagerURL() if err != nil { return []*dispatch.APIAlert{}, err } u.Path = path.Join(u.Path, "/api/v1/alerts/groups") u.RawQuery = "filter=" + url.QueryEscape(filter) res, err := http.Get(u.String()) if err != nil { return []*dispatch.APIAlert{}, err } defer res.Body.Close() err = json.NewDecoder(res.Body).Decode(&alertResponse) if err != nil { return []*dispatch.APIAlert{}, fmt.Errorf("Unable to decode json response: %s", err) } if alertResponse.Status != "success" { return []*dispatch.APIAlert{}, fmt.Errorf("[%s] %s", alertResponse.ErrorType, alertResponse.Error) } return flattenAlertOverview(alertResponse.Data), nil } func flattenAlertOverview(overview []*alertGroup) []*dispatch.APIAlert { alerts := []*dispatch.APIAlert{} for _, group := range overview { for _, block := range group.Blocks { alerts = append(alerts, block.Alerts...) } } return alerts } func queryAlerts(cmd *cobra.Command, args []string) error { expired := viper.GetBool("expired") showSilenced := viper.GetBool("silenced") var filterString = "" if len(args) == 1 { // If we only have one argument then it's possible that the user wants me to assume alertname= // Attempt to use the parser to pare the argument // If the parser fails then we likely don't have a (=|=~|!=|!~) so lets prepend `alertname=` to the front _, err := parse.Matcher(args[0]) if err != nil { filterString = fmt.Sprintf("{alertname=%s}", args[0]) } else { filterString = fmt.Sprintf("{%s}", strings.Join(args, ",")) } } else if len(args) > 1 { filterString = fmt.Sprintf("{%s}", strings.Join(args, ",")) } fetchedAlerts, err := fetchAlerts(filterString) if err != nil { return err } displayAlerts := []*dispatch.APIAlert{} for _, alert := range fetchedAlerts { // If we are only returning current alerts and this one has already expired skip it if !expired { if !alert.EndsAt.IsZero() && alert.EndsAt.Before(time.Now()) { continue } } if !showSilenced { // If any silence mutes this alert don't show it if alert.Status.State == types.AlertStateSuppressed && len(alert.Status.SilencedBy) > 0 { continue } } displayAlerts = append(displayAlerts, alert) } formatter, found := format.Formatters[viper.GetString("output")] if !found { return errors.New("Unknown output formatter") } return formatter.FormatAlerts(displayAlerts) } prometheus-alertmanager-0.6.2+ds/cli/config.go000066400000000000000000000046741314512360300213300ustar00rootroot00000000000000package cli import ( "encoding/json" "errors" "fmt" "net/http" "path" "time" "github.com/prometheus/alertmanager/cli/format" "github.com/prometheus/alertmanager/config" "github.com/spf13/cobra" "github.com/spf13/viper" ) // Config is the response type of alertmanager config endpoint // Duped in cli/format needs to be moved to common/model type Config struct { Config string `json:"config"` ConfigJSON config.Config `json:configJSON` MeshStatus map[string]interface{} `json:"meshStatus"` VersionInfo map[string]string `json:"versionInfo"` Uptime time.Time `json:"uptime"` } type MeshStatus struct { Name string `json:"name"` NickName string `json:"nickName"` Peers []PeerStatus `json:"peerStatus"` } type PeerStatus struct { Name string `json:"name"` NickName string `json:"nickName"` UID uint64 `uid` } type alertmanagerStatusResponse struct { Status string `json:"status"` Data Config `json:"data,omitempty"` ErrorType string `json:"errorType,omitempty"` Error string `json:"error,omitempty"` } // alertCmd represents the alert command var configCmd = &cobra.Command{ Use: "config", Short: "View the running config", Long: `View current config The amount of output is controlled by the output selection flag: - Simple: Print just the running config - Extended: Print the running config as well as uptime and all version info - Json: Print entire config object as json`, RunE: queryConfig, } func init() { RootCmd.AddCommand(configCmd) } func fetchConfig() (Config, error) { configResponse := alertmanagerStatusResponse{} u, err := GetAlertmanagerURL() if err != nil { return Config{}, err } u.Path = path.Join(u.Path, "/api/v1/status") res, err := http.Get(u.String()) if err != nil { return Config{}, err } defer res.Body.Close() err = json.NewDecoder(res.Body).Decode(&configResponse) if err != nil { return configResponse.Data, err } if configResponse.Status != "success" { return Config{}, fmt.Errorf("[%s] %s", configResponse.ErrorType, configResponse.Error) } return configResponse.Data, nil } func queryConfig(cmd *cobra.Command, args []string) error { config, err := fetchConfig() if err != nil { return err } formatter, found := format.Formatters[viper.GetString("output")] if !found { return errors.New("Unknown output formatter") } c := format.Config(config) return formatter.FormatConfig(c) } prometheus-alertmanager-0.6.2+ds/cli/format/000077500000000000000000000000001314512360300210115ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/cli/format/format.go000066400000000000000000000026251314512360300226350ustar00rootroot00000000000000package format import ( "io" "time" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/dispatch" "github.com/prometheus/alertmanager/types" "github.com/spf13/viper" ) const DefaultDateFormat = "2006-01-02 15:04:05 MST" // Config representation // Need to get this moved to the prometheus/common/model repo having is duplicated here is smelly type Config struct { Config string `json:"config"` ConfigJSON config.Config `json:configJSON` MeshStatus map[string]interface{} `json:"meshStatus"` VersionInfo map[string]string `json:"versionInfo"` Uptime time.Time `json:"uptime"` } type MeshStatus struct { Name string `json:"name"` NickName string `json:"nickName"` Peers []PeerStatus `json:"peerStatus"` } type PeerStatus struct { Name string `json:"name"` NickName string `json:"nickName"` UID uint64 `uid` } // Formatter needs to be implemented for each new output formatter type Formatter interface { SetOutput(io.Writer) FormatSilences([]types.Silence) error FormatAlerts([]*dispatch.APIAlert) error FormatConfig(Config) error } // Formatters is a map of cli argument name to formatter inferface object var Formatters map[string]Formatter = map[string]Formatter{} func FormatDate(input time.Time) string { dateformat := viper.GetString("date.format") return input.Format(dateformat) } prometheus-alertmanager-0.6.2+ds/cli/format/format_extended.go000066400000000000000000000060771314512360300245220ustar00rootroot00000000000000package format import ( "fmt" "io" "os" "sort" "strings" "text/tabwriter" "github.com/prometheus/alertmanager/dispatch" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" ) type ExtendedFormatter struct { writer io.Writer } func init() { Formatters["extended"] = &ExtendedFormatter{writer: os.Stdout} } func (formatter *ExtendedFormatter) SetOutput(writer io.Writer) { formatter.writer = writer } func (formatter *ExtendedFormatter) FormatSilences(silences []types.Silence) error { w := tabwriter.NewWriter(formatter.writer, 0, 0, 2, ' ', 0) sort.Sort(ByEndAt(silences)) fmt.Fprintln(w, "ID\tMatchers\tStarts At\tEnds At\tUpdated At\tCreated By\tComment\t") for _, silence := range silences { fmt.Fprintf( w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t\n", silence.ID, extendedFormatMatchers(silence.Matchers), FormatDate(silence.StartsAt), FormatDate(silence.EndsAt), FormatDate(silence.UpdatedAt), silence.CreatedBy, silence.Comment, ) } w.Flush() return nil } func (formatter *ExtendedFormatter) FormatAlerts(alerts []*dispatch.APIAlert) error { w := tabwriter.NewWriter(formatter.writer, 0, 0, 2, ' ', 0) sort.Sort(ByStartsAt(alerts)) fmt.Fprintln(w, "Labels\tAnnotations\tStarts At\tEnds At\tGenerator URL\t") for _, alert := range alerts { fmt.Fprintf( w, "%s\t%s\t%s\t%s\t%s\t\n", extendedFormatLabels(alert.Labels), extendedFormatAnnotations(alert.Annotations), FormatDate(alert.StartsAt), FormatDate(alert.EndsAt), alert.GeneratorURL, ) } w.Flush() return nil } func (formatter *ExtendedFormatter) FormatConfig(config Config) error { fmt.Fprintln(formatter.writer, config.Config) fmt.Fprintln(formatter.writer, "buildUser", config.VersionInfo["buildUser"]) fmt.Fprintln(formatter.writer, "goVersion", config.VersionInfo["goVersion"]) fmt.Fprintln(formatter.writer, "revision", config.VersionInfo["revision"]) fmt.Fprintln(formatter.writer, "version", config.VersionInfo["version"]) fmt.Fprintln(formatter.writer, "branch", config.VersionInfo["branch"]) fmt.Fprintln(formatter.writer, "buildDate", config.VersionInfo["buildDate"]) fmt.Fprintln(formatter.writer, "uptime", config.Uptime) return nil } func extendedFormatLabels(labels model.LabelSet) string { output := []string{} for name, value := range labels { output = append(output, fmt.Sprintf("%s=\"%s\"", name, value)) } sort.Strings(output) return strings.Join(output, " ") } func extendedFormatAnnotations(labels model.LabelSet) string { output := []string{} for name, value := range labels { output = append(output, fmt.Sprintf("%s=\"%s\"", name, value)) } sort.Strings(output) return strings.Join(output, " ") } func extendedFormatMatchers(matchers types.Matchers) string { output := []string{} for _, matcher := range matchers { output = append(output, extendedFormatMatcher(*matcher)) } return strings.Join(output, " ") } func extendedFormatMatcher(matcher types.Matcher) string { if matcher.IsRegex { return fmt.Sprintf("%s~=%s", matcher.Name, matcher.Value) } return fmt.Sprintf("%s=%s", matcher.Name, matcher.Value) } prometheus-alertmanager-0.6.2+ds/cli/format/format_json.go000066400000000000000000000014531314512360300236640ustar00rootroot00000000000000package format import ( "encoding/json" "io" "os" "github.com/prometheus/alertmanager/dispatch" "github.com/prometheus/alertmanager/types" ) type JsonFormatter struct { writer io.Writer } func init() { Formatters["json"] = &JsonFormatter{writer: os.Stdout} } func (formatter *JsonFormatter) SetOutput(writer io.Writer) { formatter.writer = writer } func (formatter *JsonFormatter) FormatSilences(silences []types.Silence) error { enc := json.NewEncoder(formatter.writer) return enc.Encode(silences) } func (formatter *JsonFormatter) FormatAlerts(alerts []*dispatch.APIAlert) error { enc := json.NewEncoder(formatter.writer) return enc.Encode(alerts) } func (formatter *JsonFormatter) FormatConfig(config Config) error { enc := json.NewEncoder(formatter.writer) return enc.Encode(config) } prometheus-alertmanager-0.6.2+ds/cli/format/format_simple.go000066400000000000000000000035111314512360300242010ustar00rootroot00000000000000package format import ( "fmt" "io" "os" "sort" "strings" "text/tabwriter" "github.com/prometheus/alertmanager/dispatch" "github.com/prometheus/alertmanager/types" ) type SimpleFormatter struct { writer io.Writer } func init() { Formatters["simple"] = &SimpleFormatter{writer: os.Stdout} } func (formatter *SimpleFormatter) SetOutput(writer io.Writer) { formatter.writer = writer } func (formatter *SimpleFormatter) FormatSilences(silences []types.Silence) error { w := tabwriter.NewWriter(formatter.writer, 0, 0, 2, ' ', 0) sort.Sort(ByEndAt(silences)) fmt.Fprintln(w, "ID\tMatchers\tEnds At\tCreated By\tComment\t") for _, silence := range silences { fmt.Fprintf( w, "%s\t%s\t%s\t%s\t%s\t\n", silence.ID, simpleFormatMatchers(silence.Matchers), FormatDate(silence.EndsAt), silence.CreatedBy, silence.Comment, ) } w.Flush() return nil } func (formatter *SimpleFormatter) FormatAlerts(alerts []*dispatch.APIAlert) error { w := tabwriter.NewWriter(formatter.writer, 0, 0, 2, ' ', 0) sort.Sort(ByStartsAt(alerts)) fmt.Fprintln(w, "Alertname\tStarts At\tSummary\t") for _, alert := range alerts { fmt.Fprintf( w, "%s\t%s\t%s\t\n", alert.Labels["alertname"], FormatDate(alert.StartsAt), alert.Annotations["summary"], ) } w.Flush() return nil } func (formatter *SimpleFormatter) FormatConfig(config Config) error { fmt.Fprintln(formatter.writer, config.Config) return nil } func simpleFormatMatchers(matchers types.Matchers) string { output := []string{} for _, matcher := range matchers { output = append(output, simpleFormatMatcher(*matcher)) } return strings.Join(output, " ") } func simpleFormatMatcher(matcher types.Matcher) string { if matcher.IsRegex { return fmt.Sprintf("%s=~%s", matcher.Name, matcher.Value) } return fmt.Sprintf("%s=%s", matcher.Name, matcher.Value) } prometheus-alertmanager-0.6.2+ds/cli/format/sort.go000066400000000000000000000011311314512360300223230ustar00rootroot00000000000000package format import ( "github.com/prometheus/alertmanager/dispatch" "github.com/prometheus/alertmanager/types" ) type ByEndAt []types.Silence func (s ByEndAt) Len() int { return len(s) } func (s ByEndAt) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s ByEndAt) Less(i, j int) bool { return s[i].EndsAt.Before(s[j].EndsAt) } type ByStartsAt []*dispatch.APIAlert func (s ByStartsAt) Len() int { return len(s) } func (s ByStartsAt) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s ByStartsAt) Less(i, j int) bool { return s[i].StartsAt.Before(s[j].StartsAt) } prometheus-alertmanager-0.6.2+ds/cli/root.go000066400000000000000000000047711314512360300210440ustar00rootroot00000000000000package cli import ( "fmt" "os" "github.com/prometheus/alertmanager/cli/format" "github.com/spf13/cobra" "github.com/spf13/viper" ) // RootCmd represents the base command when called without any subcommands var RootCmd = &cobra.Command{ Use: "amtool", Short: "Alertmanager CLI", Long: `View and modify the current Alertmanager state. [Config File] The alertmanger tool will read a config file from the --config cli argument, AMTOOL_CONFIG environment variable, $HOME/.amtool.yml or /etc/amtool.yml the options are as follows alertmanager.url Set a default alertmanager url for each request author Set a default author value for new silences. If this argument is not specified then the username will be used comment_required Require a comment on silence creation output Set a default output type. Options are (simple, extended, json) `, } // Execute adds all child commands to the root command sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { if err := RootCmd.Execute(); err != nil { os.Exit(1) } } func init() { cobra.OnInitialize(initConfig) RootCmd.PersistentFlags().String("config", "", "config file (default is $HOME/.amtool.yml)") viper.BindPFlag("config", RootCmd.PersistentFlags().Lookup("config")) RootCmd.PersistentFlags().String("alertmanager.url", "", "Alertmanager to talk to") viper.BindPFlag("alertmanager.url", RootCmd.PersistentFlags().Lookup("alertmanager.url")) RootCmd.PersistentFlags().StringP("output", "o", "simple", "Output formatter (simple, extended, json)") viper.BindPFlag("output", RootCmd.PersistentFlags().Lookup("output")) RootCmd.PersistentFlags().BoolP("verbose", "v", false, "Verbose running information") viper.BindPFlag("verbose", RootCmd.PersistentFlags().Lookup("verbose")) viper.SetDefault("date.format", format.DefaultDateFormat) } // initConfig reads in config file and ENV variables if set. func initConfig() { viper.SetConfigName(".amtool") // name of config file (without extension) viper.AddConfigPath("/etc") viper.AddConfigPath("$HOME") viper.SetEnvPrefix("AMTOOL") viper.AutomaticEnv() // read in environment variables that match // If a config file is found, read it in. cfgFile := viper.GetString("config") if cfgFile != "" { // enable ability to specify config file via flag viper.SetConfigFile(cfgFile) } err := viper.ReadInConfig() if err == nil { if viper.GetBool("verbose") { fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) } } } prometheus-alertmanager-0.6.2+ds/cli/silence.go000066400000000000000000000016641314512360300215010ustar00rootroot00000000000000package cli import ( "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/prometheus/alertmanager/types" ) //var labels []string type alertmanagerSilenceResponse struct { Status string `json:"status"` Data []types.Silence `json:"data,omitempty"` ErrorType string `json:"errorType,omitempty"` Error string `json:"error,omitempty"` } // silenceCmd represents the silence command var silenceCmd = &cobra.Command{ Use: "silence", Short: "Manage silences", Long: `Add, expire or view silences. For more information and additional flags see query help`, Run: CommandWrapper(query), } func init() { RootCmd.AddCommand(silenceCmd) silenceCmd.PersistentFlags().BoolP("quiet", "q", false, "Only show silence ids") viper.BindPFlag("quiet", silenceCmd.PersistentFlags().Lookup("quiet")) silenceCmd.AddCommand(addCmd) silenceCmd.AddCommand(expireCmd) silenceCmd.AddCommand(queryCmd) } prometheus-alertmanager-0.6.2+ds/cli/silence_add.go000066400000000000000000000075361314512360300223150ustar00rootroot00000000000000package cli import ( "bytes" "encoding/json" "errors" "fmt" "net/http" "os/user" "path" "time" "github.com/prometheus/alertmanager/types" "github.com/spf13/cobra" flag "github.com/spf13/pflag" "github.com/spf13/viper" ) type addResponse struct { Status string `json:"status"` Data struct { SilenceID string `json:"silenceId"` } `json:"data,omitempty"` ErrorType string `json:"errorType,omitempty"` Error string `json:"error,omitempty"` } var addFlags *flag.FlagSet var addCmd = &cobra.Command{ Use: "add", Short: "Add silence", Long: `Add a new alertmanager silence Amtool uses a simplified prometheus syntax to represent silences. The non-option section of arguments constructs a list of "Matcher Groups" that will be used to create a number of silences. The following examples will attempt to show this behaviour in action: amtool silence add alertname=foo node=bar This statement will add a silence that matches alerts with the alertname=foo and node=bar label value pairs set. amtool silence add foo node=bar If alertname is ommited and the first argument does not contain a '=' or a '=~' then it will be assumed to be the value of the alertname pair. amtool silence add 'alertname=~foo.*' As well as direct equality, regex matching is also supported. The '=~' syntax (similar to prometheus) is used to represent a regex match. Regex matching can be used in combination with a direct match. `, Run: CommandWrapper(add), } func init() { user, _ := user.Current() addCmd.Flags().StringP("author", "a", user.Username, "Username for CreatedBy field") addCmd.Flags().StringP("expires", "e", "1h", "Duration of silence (100h)") addCmd.Flags().String("expire-on", "", "Expire at a certain time (Overwrites expires) RFC3339 format 2006-01-02T15:04:05Z07:00") addCmd.Flags().StringP("comment", "c", "", "A comment to help describe the silence") viper.BindPFlag("author", addCmd.Flags().Lookup("author")) viper.BindPFlag("expires", addCmd.Flags().Lookup("expires")) viper.BindPFlag("comment", addCmd.Flags().Lookup("comment")) viper.SetDefault("comment_required", false) addFlags = addCmd.Flags() } func add(cmd *cobra.Command, args []string) error { var err error matchers, err := parseMatchers(args) if err != nil { return err } if len(matchers) < 1 { return fmt.Errorf("No matchers specified") } expire_on, err := addFlags.GetString("expire-on") if err != nil { return err } expires := viper.GetString("expires") var endsAt time.Time if expire_on != "" { endsAt, err = time.Parse(time.RFC3339, expire_on) if err != nil { return err } } else { duration, err := time.ParseDuration(expires) if err != nil { return err } endsAt = time.Now().UTC().Add(duration) } author := viper.GetString("author") comment := viper.GetString("comment") comment_required := viper.GetBool("comment_required") if comment_required && comment == "" { return errors.New("Comment required by config") } typeMatchers, err := TypeMatchers(matchers) if err != nil { return err } silence := types.Silence{ Matchers: typeMatchers, StartsAt: time.Now().UTC(), EndsAt: endsAt, CreatedBy: author, Comment: comment, } u, err := GetAlertmanagerURL() if err != nil { return err } u.Path = path.Join(u.Path, "/api/v1/silences") buf := bytes.NewBuffer([]byte{}) err = json.NewEncoder(buf).Encode(silence) if err != nil { return err } res, err := http.Post(u.String(), "application/json", buf) if err != nil { return err } defer res.Body.Close() response := addResponse{} err = json.NewDecoder(res.Body).Decode(&response) if err != nil { return errors.New(fmt.Sprintf("Unable to parse silence json response from %s", u.String())) } if response.Status == "error" { fmt.Printf("[%s] %s\n", response.ErrorType, response.Error) } else { fmt.Println(response.Data.SilenceID) } return nil } prometheus-alertmanager-0.6.2+ds/cli/silence_expire.go000066400000000000000000000017061314512360300230520ustar00rootroot00000000000000package cli import ( "encoding/json" "errors" "net/http" "path" "github.com/spf13/cobra" ) var expireCmd = &cobra.Command{ Use: "expire", Short: "expire silence", Long: `expire an alertmanager silence`, Run: CommandWrapper(expire), } func expire(cmd *cobra.Command, args []string) error { u, err := GetAlertmanagerURL() if err != nil { return err } basePath := path.Join(u.Path, "/api/v1/silence") if len(args) < 1 { return errors.New("No silence IDs specified") } for _, arg := range args { u.Path = path.Join(basePath, arg) req, err := http.NewRequest("DELETE", u.String(), nil) res, err := http.DefaultClient.Do(req) if err != nil { return err } defer res.Body.Close() decoder := json.NewDecoder(res.Body) response := alertmanagerSilenceResponse{} err = decoder.Decode(&response) if err != nil { return err } if response.Status == "error" { return errors.New(response.Error) } } return nil } prometheus-alertmanager-0.6.2+ds/cli/silence_query.go000066400000000000000000000071221314512360300227210ustar00rootroot00000000000000package cli import ( "encoding/json" "errors" "fmt" "net/http" "net/url" "path" "strings" "time" "github.com/prometheus/alertmanager/cli/format" "github.com/prometheus/alertmanager/pkg/parse" "github.com/prometheus/alertmanager/types" "github.com/spf13/cobra" flag "github.com/spf13/pflag" "github.com/spf13/viper" ) var queryFlags *flag.FlagSet var queryCmd = &cobra.Command{ Use: "query", Short: "Query silences", Long: `Query Alertmanager silences. Amtool has a simplified prometheus query syntax, but contains robust support for bash variable expansions. The non-option section of arguments constructs a list of "Matcher Groups" that will be used to filter your query. The following examples will attempt to show this behaviour in action: amtool silence query alertname=foo node=bar This query will match all silences with the alertname=foo and node=bar label value pairs set. amtool silence query foo node=bar If alertname is ommited and the first argument does not contain a '=' or a '=~' then it will be assumed to be the value of the alertname pair. amtool silence query 'alertname=~foo.*' As well as direct equality, regex matching is also supported. The '=~' syntax (similar to prometheus) is used to represent a regex match. Regex matching can be used in combination with a direct match. `, Run: CommandWrapper(query), } func init() { queryCmd.Flags().Bool("expired", false, "Show expired silences as well as active") queryFlags = queryCmd.Flags() } func fetchSilences(filter string) ([]types.Silence, error) { silenceResponse := alertmanagerSilenceResponse{} u, err := GetAlertmanagerURL() if err != nil { return []types.Silence{}, err } u.Path = path.Join(u.Path, "/api/v1/silences") u.RawQuery = "filter=" + url.QueryEscape(filter) res, err := http.Get(u.String()) if err != nil { return []types.Silence{}, err } defer res.Body.Close() err = json.NewDecoder(res.Body).Decode(&silenceResponse) if err != nil { return []types.Silence{}, err } if silenceResponse.Status != "success" { return []types.Silence{}, fmt.Errorf("[%s] %s", silenceResponse.ErrorType, silenceResponse.Error) } return silenceResponse.Data, nil } func query(cmd *cobra.Command, args []string) error { expired, err := queryFlags.GetBool("expired") if err != nil { return err } quiet := viper.GetBool("quiet") var filterString = "" if len(args) == 1 { // If we only have one argument then it's possible that the user wants me to assume alertname= // Attempt to use the parser to pare the argument // If the parser fails then we likely don't have a (=|=~|!=|!~) so lets prepend `alertname=` to the front _, err := parse.Matcher(args[0]) if err != nil { filterString = fmt.Sprintf("{alertname=%s}", args[0]) } else { filterString = fmt.Sprintf("{%s}", strings.Join(args, ",")) } } else if len(args) > 1 { filterString = fmt.Sprintf("{%s}", strings.Join(args, ",")) } fetchedSilences, err := fetchSilences(filterString) if err != nil { return err } displaySilences := []types.Silence{} for _, silence := range fetchedSilences { // If we are only returning current silences and this one has already expired skip it if !expired && silence.EndsAt.Before(time.Now()) { continue } displaySilences = append(displaySilences, silence) } if quiet { for _, silence := range displaySilences { fmt.Println(silence.ID) } } else { formatter, found := format.Formatters[viper.GetString("output")] if !found { return errors.New("Unknown output formatter") } formatter.FormatSilences(displaySilences) } return nil } prometheus-alertmanager-0.6.2+ds/cli/utils.go000066400000000000000000000046521314512360300212170ustar00rootroot00000000000000package cli import ( "errors" "fmt" "net/url" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/prometheus/alertmanager/pkg/parse" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" "github.com/prometheus/prometheus/pkg/labels" ) type ByAlphabetical []labels.Matcher func (s ByAlphabetical) Len() int { return len(s) } func (s ByAlphabetical) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s ByAlphabetical) Less(i, j int) bool { if s[i].Name != s[j].Name { return s[i].Name < s[j].Name } else if s[i].Type != s[j].Type { return s[i].Type < s[j].Type } else if s[i].Value != s[j].Value { return s[i].Value < s[j].Value } else { return false } } func GetAlertmanagerURL() (*url.URL, error) { u, err := url.ParseRequestURI(viper.GetString("alertmanager.url")) if err != nil { return nil, errors.New("Invalid alertmanager url") } return u, nil } // Parse a list of labels (cli arguments) func parseMatchers(inputLabels []string) ([]labels.Matcher, error) { matchers := make([]labels.Matcher, 0) for _, v := range inputLabels { name, value, matchType, err := parse.Input(v) if err != nil { return []labels.Matcher{}, err } matchers = append(matchers, labels.Matcher{ Type: matchType, Name: name, Value: value, }) } return matchers, nil } // Only valid for when you are going to add a silence func TypeMatchers(matchers []labels.Matcher) (types.Matchers, error) { typeMatchers := types.Matchers{} for _, matcher := range matchers { typeMatcher, err := TypeMatcher(matcher) if err != nil { return types.Matchers{}, err } typeMatchers = append(typeMatchers, &typeMatcher) } return typeMatchers, nil } // Only valid for when you are going to add a silence // Doesn't allow negative operators func TypeMatcher(matcher labels.Matcher) (types.Matcher, error) { typeMatcher := types.NewMatcher(model.LabelName(matcher.Name), matcher.Value) switch matcher.Type { case labels.MatchEqual: typeMatcher.IsRegex = false case labels.MatchRegexp: typeMatcher.IsRegex = true default: return types.Matcher{}, fmt.Errorf("invalid match type for creation operation: %s", matcher.Type) } return *typeMatcher, nil } func CommandWrapper(command func(*cobra.Command, []string) error) func(*cobra.Command, []string) { return func(cmd *cobra.Command, args []string) { err := command(cmd, args) if err != nil { fmt.Printf("Error: %s\n", err) } } } prometheus-alertmanager-0.6.2+ds/cmd/000077500000000000000000000000001314512360300175155ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/cmd/alertmanager/000077500000000000000000000000001314512360300221575ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/cmd/alertmanager/main.go000066400000000000000000000270561314512360300234440ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "crypto/md5" "encoding/binary" "flag" "fmt" "io/ioutil" stdlog "log" "net" "net/http" "net/url" "os" "os/signal" "path" "path/filepath" "sort" "strconv" "strings" "sync" "syscall" "time" "github.com/prometheus/alertmanager/api" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/dispatch" "github.com/prometheus/alertmanager/inhibit" "github.com/prometheus/alertmanager/nflog" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/provider/mem" "github.com/prometheus/alertmanager/silence" "github.com/prometheus/alertmanager/template" "github.com/prometheus/alertmanager/types" "github.com/prometheus/alertmanager/ui" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/log" "github.com/prometheus/common/route" "github.com/prometheus/common/version" "github.com/prometheus/prometheus/pkg/labels" "github.com/weaveworks/mesh" ) var ( configHash = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "alertmanager_config_hash", Help: "Hash of the currently loaded alertmanager configuration.", }) configSuccess = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "alertmanager_config_last_reload_successful", Help: "Whether the last configuration reload attempt was successful.", }) configSuccessTime = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "alertmanager_config_last_reload_success_timestamp_seconds", Help: "Timestamp of the last successful configuration reload.", }) ) func init() { prometheus.MustRegister(configSuccess) prometheus.MustRegister(configSuccessTime) prometheus.MustRegister(configHash) prometheus.MustRegister(version.NewCollector("alertmanager")) } func main() { peers := &stringset{} var ( showVersion = flag.Bool("version", false, "Print version information.") configFile = flag.String("config.file", "alertmanager.yml", "Alertmanager configuration file name.") dataDir = flag.String("storage.path", "data/", "Base path for data storage.") retention = flag.Duration("data.retention", 5*24*time.Hour, "How long to keep data for.") externalURL = flag.String("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.") listenAddress = flag.String("web.listen-address", ":9093", "Address to listen on for the web interface and API.") meshListen = flag.String("mesh.listen-address", net.JoinHostPort("0.0.0.0", strconv.Itoa(mesh.Port)), "mesh listen address") hwaddr = flag.String("mesh.peer-id", "", "mesh peer ID (default: MAC address)") nickname = flag.String("mesh.nickname", mustHostname(), "mesh peer nickname") password = flag.String("mesh.password", "", "password to join the peer network (empty password disables encryption)") ) flag.Var(peers, "mesh.peer", "initial peers (may be repeated)") flag.Parse() if *hwaddr == "" { *hwaddr = mustHardwareAddr() } if len(flag.Args()) > 0 { log.Fatalln("Received unexpected and unparsed arguments: ", strings.Join(flag.Args(), ", ")) } if *showVersion { fmt.Fprintln(os.Stdout, version.Print("alertmanager")) os.Exit(0) } log.Infoln("Starting alertmanager", version.Info()) log.Infoln("Build context", version.BuildContext()) err := os.MkdirAll(*dataDir, 0777) if err != nil { log.Fatal(err) } logger := log.NewLogger(os.Stderr) mrouter := initMesh(*meshListen, *hwaddr, *nickname, *password) stopc := make(chan struct{}) var wg sync.WaitGroup wg.Add(1) notificationLog, err := nflog.New( nflog.WithMesh(func(g mesh.Gossiper) mesh.Gossip { return mrouter.NewGossip("nflog", g) }), nflog.WithRetention(*retention), nflog.WithSnapshot(filepath.Join(*dataDir, "nflog")), nflog.WithMaintenance(15*time.Minute, stopc, wg.Done), nflog.WithMetrics(prometheus.DefaultRegisterer), nflog.WithLogger(logger.With("component", "nflog")), ) if err != nil { log.Fatal(err) } marker := types.NewMarker() silences, err := silence.New(silence.Options{ SnapshotFile: filepath.Join(*dataDir, "silences"), Retention: *retention, Logger: logger.With("component", "silences"), Metrics: prometheus.DefaultRegisterer, Gossip: func(g mesh.Gossiper) mesh.Gossip { return mrouter.NewGossip("silences", g) }, }) if err != nil { log.Fatal(err) } // Start providers before router potentially sends updates. wg.Add(1) go func() { silences.Maintenance(15*time.Minute, filepath.Join(*dataDir, "silences"), stopc) wg.Done() }() mrouter.Start() defer func() { close(stopc) // Stop receiving updates from router before shutting down. mrouter.Stop() wg.Wait() }() mrouter.ConnectionMaker.InitiateConnections(peers.slice(), true) alerts, err := mem.NewAlerts(marker, 30*time.Minute, *dataDir) if err != nil { log.Fatal(err) } defer alerts.Close() var ( inhibitor *inhibit.Inhibitor tmpl *template.Template pipeline notify.Stage disp *dispatch.Dispatcher ) defer disp.Stop() apiv := api.New(alerts, silences, func(matchers []*labels.Matcher) dispatch.AlertOverview { return disp.Groups(matchers) }, mrouter) amURL, err := extURL(*listenAddress, *externalURL) if err != nil { log.Fatal(err) } waitFunc := meshWait(mrouter, 5*time.Second) timeoutFunc := func(d time.Duration) time.Duration { if d < notify.MinTimeout { d = notify.MinTimeout } return d + waitFunc() } var hash float64 reload := func() (err error) { log.With("file", *configFile).Infof("Loading configuration file") defer func() { if err != nil { log.With("file", *configFile).Errorf("Loading configuration file failed: %s", err) configSuccess.Set(0) } else { configSuccess.Set(1) configSuccessTime.Set(float64(time.Now().Unix())) configHash.Set(hash) } }() conf, err := config.LoadFile(*configFile) if err != nil { return err } hash = md5HashAsMetricValue([]byte(conf.String())) err = apiv.Update(conf.String(), time.Duration(conf.Global.ResolveTimeout)) if err != nil { return err } tmpl, err = template.FromGlobs(conf.Templates...) if err != nil { return err } tmpl.ExternalURL = amURL inhibitor.Stop() disp.Stop() inhibitor = inhibit.NewInhibitor(alerts, conf.InhibitRules, marker) pipeline = notify.BuildPipeline( conf.Receivers, tmpl, waitFunc, inhibitor, silences, notificationLog, marker, ) disp = dispatch.NewDispatcher(alerts, dispatch.NewRoute(conf.Route, nil), pipeline, marker, timeoutFunc) go disp.Run() go inhibitor.Run() return nil } if err := reload(); err != nil { os.Exit(1) } router := route.New() webReload := make(chan struct{}) ui.Register(router.WithPrefix(amURL.Path), webReload) apiv.Register(router.WithPrefix(path.Join(amURL.Path, "/api"))) log.Infoln("Listening on", *listenAddress) go listen(*listenAddress, router) var ( hup = make(chan os.Signal) hupReady = make(chan bool) term = make(chan os.Signal) ) signal.Notify(hup, syscall.SIGHUP) signal.Notify(term, os.Interrupt, syscall.SIGTERM) go func() { <-hupReady for { select { case <-hup: case <-webReload: } reload() } }() // Wait for reload or termination signals. close(hupReady) // Unblock SIGHUP handler. <-term log.Infoln("Received SIGTERM, exiting gracefully...") } type peerDescSlice []mesh.PeerDescription func (s peerDescSlice) Len() int { return len(s) } func (s peerDescSlice) Less(i, j int) bool { return s[i].UID < s[j].UID } func (s peerDescSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // meshWait returns a function that inspects the current peer state and returns // a duration of one base timeout for each peer with a higher ID than ourselves. func meshWait(r *mesh.Router, timeout time.Duration) func() time.Duration { return func() time.Duration { var peers peerDescSlice for _, desc := range r.Peers.Descriptions() { peers = append(peers, desc) } sort.Sort(peers) k := 0 for _, desc := range peers { if desc.Self { break } k++ } // TODO(fabxc): add metric exposing the "position" from AM's own view. return time.Duration(k) * timeout } } func initMesh(addr, hwaddr, nickname, pw string) *mesh.Router { host, portStr, err := net.SplitHostPort(addr) if err != nil { log.Fatalf("mesh address: %s: %v", addr, err) } port, err := strconv.Atoi(portStr) if err != nil { log.Fatalf("mesh address: %s: %v", addr, err) } name, err := mesh.PeerNameFromString(hwaddr) if err != nil { log.Fatalf("invalid hardware address %q: %v", hwaddr, err) } password := []byte(pw) if len(password) == 0 { // Emtpy password is used to disable secure communication. Using a nil // password disables encryption in mesh. password = nil } return mesh.NewRouter(mesh.Config{ Host: host, Port: port, ProtocolMinVersion: mesh.ProtocolMinVersion, Password: password, ConnLimit: 64, PeerDiscovery: true, TrustedSubnets: []*net.IPNet{}, }, name, nickname, mesh.NullOverlay{}, stdlog.New(ioutil.Discard, "", 0)) } func extURL(listen, external string) (*url.URL, error) { if external == "" { hostname, err := os.Hostname() if err != nil { return nil, err } _, port, err := net.SplitHostPort(listen) if err != nil { return nil, err } external = fmt.Sprintf("http://%s:%s/", hostname, port) } u, err := url.Parse(external) if err != nil { return nil, err } ppref := strings.TrimRight(u.Path, "/") if ppref != "" && !strings.HasPrefix(ppref, "/") { ppref = "/" + ppref } u.Path = ppref return u, nil } func listen(listen string, router *route.Router) { if err := http.ListenAndServe(listen, router); err != nil { log.Fatal(err) } } type stringset map[string]struct{} func (ss stringset) Set(value string) error { for _, v := range strings.Split(value, ",") { if v = strings.TrimSpace(v); v != "" { ss[v] = struct{}{} } } return nil } func (ss stringset) String() string { return strings.Join(ss.slice(), ",") } func (ss stringset) slice() []string { slice := make([]string, 0, len(ss)) for k := range ss { slice = append(slice, k) } sort.Strings(slice) return slice } func mustHardwareAddr() string { // TODO(fabxc): consider a safe-guard against colliding MAC addresses. ifaces, err := net.Interfaces() if err != nil { panic(err) } for _, iface := range ifaces { if s := iface.HardwareAddr.String(); s != "" { return s } } panic("no valid network interfaces") } func mustHostname() string { hostname, err := os.Hostname() if err != nil { panic(err) } return hostname } func md5HashAsMetricValue(data []byte) float64 { sum := md5.Sum(data) // We only want 48 bits as a float64 only has a 53 bit mantissa. smallSum := sum[0:6] var bytes = make([]byte, 8) copy(bytes, smallSum) return float64(binary.LittleEndian.Uint64(bytes)) } prometheus-alertmanager-0.6.2+ds/cmd/amtool/000077500000000000000000000000001314512360300210105ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/cmd/amtool/main.go000066400000000000000000000001361314512360300222630ustar00rootroot00000000000000package main import "github.com/prometheus/alertmanager/cli" func main() { cli.Execute() } prometheus-alertmanager-0.6.2+ds/config/000077500000000000000000000000001314512360300202175ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/config/config.go000066400000000000000000000374731314512360300220310ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package config import ( "errors" "fmt" "io/ioutil" "path/filepath" "regexp" "strings" "time" "encoding/json" "github.com/prometheus/common/model" "gopkg.in/yaml.v2" ) var patAuthLine = regexp.MustCompile(`((?:api_key|service_key|api_url|token|user_key|password|secret):\s+)(".+"|'.+'|[^\s]+)`) // Secret is a string that must not be revealed on marshaling. type Secret string // MarshalYAML implements the yaml.Marshaler interface. func (s Secret) MarshalYAML() (interface{}, error) { return "", nil } // Load parses the YAML input s into a Config. func Load(s string) (*Config, error) { cfg := &Config{} err := yaml.Unmarshal([]byte(s), cfg) if err != nil { return nil, err } // Check if we have a root route. We cannot check for it in the // UnmarshalYAML method because it won't be called if the input is empty // (e.g. the config file is empty or only contains whitespace). if cfg.Route == nil { return nil, errors.New("no route provided in config") } // Check if continue in root route. if cfg.Route.Continue { return nil, errors.New("cannot have continue in root route") } cfg.original = s return cfg, nil } // LoadFile parses the given YAML file into a Config. func LoadFile(filename string) (*Config, error) { content, err := ioutil.ReadFile(filename) if err != nil { return nil, err } cfg, err := Load(string(content)) if err != nil { return nil, err } resolveFilepaths(filepath.Dir(filename), cfg) return cfg, nil } // resolveFilepaths joins all relative paths in a configuration // with a given base directory. func resolveFilepaths(baseDir string, cfg *Config) { join := func(fp string) string { if len(fp) > 0 && !filepath.IsAbs(fp) { fp = filepath.Join(baseDir, fp) } return fp } for i, tf := range cfg.Templates { cfg.Templates[i] = join(tf) } } // Config is the top-level configuration for Alertmanager's config files. type Config struct { Global *GlobalConfig `yaml:"global,omitempty" json:"global,omitempty"` Route *Route `yaml:"route,omitempty" json:"route,omitempty"` InhibitRules []*InhibitRule `yaml:"inhibit_rules,omitempty" json:"inhibit_rules,omitempty"` Receivers []*Receiver `yaml:"receivers,omitempty" json:"receivers,omitempty"` Templates []string `yaml:"templates" json:"templates"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` // original is the input from which the config was parsed. original string } func checkOverflow(m map[string]interface{}, ctx string) error { if len(m) > 0 { var keys []string for k := range m { keys = append(keys, k) } return fmt.Errorf("unknown fields in %s: %s", ctx, strings.Join(keys, ", ")) } return nil } func (c Config) String() string { var s string if c.original != "" { s = c.original } else { b, err := yaml.Marshal(c) if err != nil { return fmt.Sprintf("", err) } s = string(b) } return patAuthLine.ReplaceAllString(s, "${1}") } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { // We want to set c to the defaults and then overwrite it with the input. // To make unmarshal fill the plain data struct rather than calling UnmarshalYAML // again, we have to hide it using a type indirection. type plain Config if err := unmarshal((*plain)(c)); err != nil { return err } // If a global block was open but empty the default global config is overwritten. // We have to restore it here. if c.Global == nil { c.Global = &GlobalConfig{} *c.Global = DefaultGlobalConfig } names := map[string]struct{}{} for _, rcv := range c.Receivers { if _, ok := names[rcv.Name]; ok { return fmt.Errorf("notification config name %q is not unique", rcv.Name) } for _, ec := range rcv.EmailConfigs { if ec.Smarthost == "" { if c.Global.SMTPSmarthost == "" { return fmt.Errorf("no global SMTP smarthost set") } ec.Smarthost = c.Global.SMTPSmarthost } if ec.From == "" { if c.Global.SMTPFrom == "" { return fmt.Errorf("no global SMTP from set") } ec.From = c.Global.SMTPFrom } if ec.AuthUsername == "" { ec.AuthUsername = c.Global.SMTPAuthUsername } if ec.AuthPassword == "" { ec.AuthPassword = c.Global.SMTPAuthPassword } if ec.AuthSecret == "" { ec.AuthSecret = c.Global.SMTPAuthSecret } if ec.AuthIdentity == "" { ec.AuthIdentity = c.Global.SMTPAuthIdentity } if ec.RequireTLS == nil { ec.RequireTLS = new(bool) *ec.RequireTLS = c.Global.SMTPRequireTLS } } for _, sc := range rcv.SlackConfigs { if sc.APIURL == "" { if c.Global.SlackAPIURL == "" { return fmt.Errorf("no global Slack API URL set") } sc.APIURL = c.Global.SlackAPIURL } } for _, hc := range rcv.HipchatConfigs { if hc.APIURL == "" { if c.Global.HipchatURL == "" { return fmt.Errorf("no global Hipchat API URL set") } hc.APIURL = c.Global.HipchatURL } if !strings.HasSuffix(hc.APIURL, "/") { hc.APIURL += "/" } if hc.AuthToken == "" { if c.Global.HipchatAuthToken == "" { return fmt.Errorf("no global Hipchat Auth Token set") } hc.AuthToken = c.Global.HipchatAuthToken } } for _, pdc := range rcv.PagerdutyConfigs { if pdc.URL == "" { if c.Global.PagerdutyURL == "" { return fmt.Errorf("no global PagerDuty URL set") } pdc.URL = c.Global.PagerdutyURL } } for _, ogc := range rcv.OpsGenieConfigs { if ogc.APIHost == "" { if c.Global.OpsGenieAPIHost == "" { return fmt.Errorf("no global OpsGenie URL set") } ogc.APIHost = c.Global.OpsGenieAPIHost } if !strings.HasSuffix(ogc.APIHost, "/") { ogc.APIHost += "/" } } for _, voc := range rcv.VictorOpsConfigs { if voc.APIURL == "" { if c.Global.VictorOpsAPIURL == "" { return fmt.Errorf("no global VictorOps URL set") } voc.APIURL = c.Global.VictorOpsAPIURL } if !strings.HasSuffix(voc.APIURL, "/") { voc.APIURL += "/" } } names[rcv.Name] = struct{}{} } // The root route must not have any matchers as it is the fallback node // for all alerts. if c.Route == nil { return fmt.Errorf("No routes provided") } if len(c.Route.Receiver) == 0 { return fmt.Errorf("Root route must specify a default receiver") } if len(c.Route.Match) > 0 || len(c.Route.MatchRE) > 0 { return fmt.Errorf("Root route must not have any matchers") } // Validate that all receivers used in the routing tree are defined. if err := checkReceiver(c.Route, names); err != nil { return err } return checkOverflow(c.XXX, "config") } // checkReceiver returns an error if a node in the routing tree // references a receiver not in the given map. func checkReceiver(r *Route, receivers map[string]struct{}) error { if r.Receiver == "" { return nil } if _, ok := receivers[r.Receiver]; !ok { return fmt.Errorf("Undefined receiver %q used in route", r.Receiver) } for _, sr := range r.Routes { if err := checkReceiver(sr, receivers); err != nil { return err } } return nil } // DefaultGlobalConfig provides global default values. var DefaultGlobalConfig = GlobalConfig{ ResolveTimeout: model.Duration(5 * time.Minute), SMTPRequireTLS: true, PagerdutyURL: "https://events.pagerduty.com/generic/2010-04-15/create_event.json", HipchatURL: "https://api.hipchat.com/", OpsGenieAPIHost: "https://api.opsgenie.com/", VictorOpsAPIURL: "https://alert.victorops.com/integrations/generic/20131114/alert/", } // GlobalConfig defines configuration parameters that are valid globally // unless overwritten. type GlobalConfig struct { // ResolveTimeout is the time after which an alert is declared resolved // if it has not been updated. ResolveTimeout model.Duration `yaml:"resolve_timeout" json:"resolve_timeout"` SMTPFrom string `yaml:"smtp_from" json:"smtp_from"` SMTPSmarthost string `yaml:"smtp_smarthost" json:"smtp_smarthost"` SMTPAuthUsername string `yaml:"smtp_auth_username" json:"smtp_auth_username"` SMTPAuthPassword Secret `yaml:"smtp_auth_password" json:"smtp_auth_password"` SMTPAuthSecret Secret `yaml:"smtp_auth_secret" json:"smtp_auth_secret"` SMTPAuthIdentity string `yaml:"smtp_auth_identity" json:"smtp_auth_identity"` SMTPRequireTLS bool `yaml:"smtp_require_tls" json:"smtp_require_tls"` SlackAPIURL Secret `yaml:"slack_api_url" json:"slack_api_url"` PagerdutyURL string `yaml:"pagerduty_url" json:"pagerduty_url"` HipchatURL string `yaml:"hipchat_url" json:"hipchat_url"` HipchatAuthToken Secret `yaml:"hipchat_auth_token" json:"hipchat_auth_token"` OpsGenieAPIHost string `yaml:"opsgenie_api_host" json:"opsgenie_api_host"` VictorOpsAPIURL string `yaml:"victorops_api_url" json:"victorops_api_url"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *GlobalConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultGlobalConfig type plain GlobalConfig if err := unmarshal((*plain)(c)); err != nil { return err } return checkOverflow(c.XXX, "global") } // A Route is a node that contains definitions of how to handle alerts. type Route struct { Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty"` GroupBy []model.LabelName `yaml:"group_by,omitempty" json:"group_by,omitempty"` Match map[string]string `yaml:"match,omitempty" json:"match,omitempty"` MatchRE map[string]Regexp `yaml:"match_re,omitempty" json:"match_re,omitempty"` Continue bool `yaml:"continue,omitempty" json:"continue,omitempty"` Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"` GroupWait *model.Duration `yaml:"group_wait,omitempty" json:"group_wait,omitempty"` GroupInterval *model.Duration `yaml:"group_interval,omitempty" json:"group_interval,omitempty"` RepeatInterval *model.Duration `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (r *Route) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain Route if err := unmarshal((*plain)(r)); err != nil { return err } for k := range r.Match { if !model.LabelNameRE.MatchString(k) { return fmt.Errorf("invalid label name %q", k) } } for k := range r.MatchRE { if !model.LabelNameRE.MatchString(k) { return fmt.Errorf("invalid label name %q", k) } } groupBy := map[model.LabelName]struct{}{} for _, ln := range r.GroupBy { if _, ok := groupBy[ln]; ok { return fmt.Errorf("duplicated label %q in group_by", ln) } groupBy[ln] = struct{}{} } return checkOverflow(r.XXX, "route") } // InhibitRule defines an inhibition rule that mutes alerts that match the // target labels if an alert matching the source labels exists. // Both alerts have to have a set of labels being equal. type InhibitRule struct { // SourceMatch defines a set of labels that have to equal the given // value for source alerts. SourceMatch map[string]string `yaml:"source_match" json:"source_match"` // SourceMatchRE defines pairs like SourceMatch but does regular expression // matching. SourceMatchRE map[string]Regexp `yaml:"source_match_re" json:"source_match_re"` // TargetMatch defines a set of labels that have to equal the given // value for target alerts. TargetMatch map[string]string `yaml:"target_match" json:"target_match"` // TargetMatchRE defines pairs like TargetMatch but does regular expression // matching. TargetMatchRE map[string]Regexp `yaml:"target_match_re" json:"target_match_re"` // A set of labels that must be equal between the source and target alert // for them to be a match. Equal model.LabelNames `yaml:"equal" json:"equal"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (r *InhibitRule) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain InhibitRule if err := unmarshal((*plain)(r)); err != nil { return err } for k := range r.SourceMatch { if !model.LabelNameRE.MatchString(k) { return fmt.Errorf("invalid label name %q", k) } } for k := range r.SourceMatchRE { if !model.LabelNameRE.MatchString(k) { return fmt.Errorf("invalid label name %q", k) } } for k := range r.TargetMatch { if !model.LabelNameRE.MatchString(k) { return fmt.Errorf("invalid label name %q", k) } } for k := range r.TargetMatchRE { if !model.LabelNameRE.MatchString(k) { return fmt.Errorf("invalid label name %q", k) } } return checkOverflow(r.XXX, "inhibit rule") } // Receiver configuration provides configuration on how to contact a receiver. type Receiver struct { // A unique identifier for this receiver. Name string `yaml:"name" json:"name"` EmailConfigs []*EmailConfig `yaml:"email_configs,omitempty" json:"email_configs,omitempty"` PagerdutyConfigs []*PagerdutyConfig `yaml:"pagerduty_configs,omitempty" json:"pagerduty_configs,omitempty"` HipchatConfigs []*HipchatConfig `yaml:"hipchat_configs,omitempty" json:"hipchat_configs,omitempty"` SlackConfigs []*SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"` WebhookConfigs []*WebhookConfig `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"` OpsGenieConfigs []*OpsGenieConfig `yaml:"opsgenie_configs,omitempty" json:"opsgenie_configs,omitempty"` PushoverConfigs []*PushoverConfig `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"` VictorOpsConfigs []*VictorOpsConfig `yaml:"victorops_configs,omitempty" json:"victorops_configs,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *Receiver) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain Receiver if err := unmarshal((*plain)(c)); err != nil { return err } if c.Name == "" { return fmt.Errorf("missing name in receiver") } return checkOverflow(c.XXX, "receiver config") } // Regexp encapsulates a regexp.Regexp and makes it YAML marshalable. type Regexp struct { *regexp.Regexp } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (re *Regexp) UnmarshalYAML(unmarshal func(interface{}) error) error { var s string if err := unmarshal(&s); err != nil { return err } regex, err := regexp.Compile("^(?:" + s + ")$") if err != nil { return err } re.Regexp = regex return nil } // MarshalYAML implements the yaml.Marshaler interface. func (re *Regexp) MarshalYAML() (interface{}, error) { if re != nil { return re.String(), nil } return nil, nil } // UnmarshalJSON implements the json.Marshaler interface func (re *Regexp) UnmarshalJSON(data []byte) error { var s string if err := json.Unmarshal(data, &s); err != nil { return err } regex, err := regexp.Compile(s) if err != nil { return err } re.Regexp = regex return nil } // MarshalJSON implements the json.Marshaler interface. func (re Regexp) MarshalJSON() ([]byte, error) { if re.Regexp != nil { return json.Marshal(re.String()) } return nil, nil } prometheus-alertmanager-0.6.2+ds/config/config_test.go000066400000000000000000000026221314512360300230540ustar00rootroot00000000000000// Copyright 2016 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package config import ( "testing" "gopkg.in/yaml.v2" ) func TestDefaultReceiverExists(t *testing.T) { in := ` route: group_wait: 30s ` conf := &Config{} err := yaml.Unmarshal([]byte(in), conf) expected := "Root route must specify a default receiver" if err == nil { t.Fatalf("no error returned, expected:\n%v", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error()) } } func TestContinueErrorInRouteRoot(t *testing.T) { in := ` route: receiver: team-X-mails continue: true receivers: - name: 'team-X-mails' ` _, err := Load(in) expected := "cannot have continue in root route" if err == nil { t.Fatalf("no error returned, expeceted:\n%q", expected) } if err.Error() != expected { t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error()) } } prometheus-alertmanager-0.6.2+ds/config/notifiers.go000066400000000000000000000332371314512360300225600ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package config import ( "fmt" "strings" "time" ) var ( // DefaultWebhookConfig defines default values for Webhook configurations. DefaultWebhookConfig = WebhookConfig{ NotifierConfig: NotifierConfig{ VSendResolved: true, }, } // DefaultEmailConfig defines default values for Email configurations. DefaultEmailConfig = EmailConfig{ NotifierConfig: NotifierConfig{ VSendResolved: false, }, HTML: `{{ template "email.default.html" . }}`, } // DefaultEmailSubject defines the default Subject header of an Email. DefaultEmailSubject = `{{ template "email.default.subject" . }}` // DefaultPagerdutyConfig defines default values for PagerDuty configurations. DefaultPagerdutyConfig = PagerdutyConfig{ NotifierConfig: NotifierConfig{ VSendResolved: true, }, Description: `{{ template "pagerduty.default.description" .}}`, Client: `{{ template "pagerduty.default.client" . }}`, ClientURL: `{{ template "pagerduty.default.clientURL" . }}`, Details: map[string]string{ "firing": `{{ template "pagerduty.default.instances" .Alerts.Firing }}`, "resolved": `{{ template "pagerduty.default.instances" .Alerts.Resolved }}`, "num_firing": `{{ .Alerts.Firing | len }}`, "num_resolved": `{{ .Alerts.Resolved | len }}`, }, } // DefaultSlackConfig defines default values for Slack configurations. DefaultSlackConfig = SlackConfig{ NotifierConfig: NotifierConfig{ VSendResolved: false, }, Color: `{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}`, Username: `{{ template "slack.default.username" . }}`, Title: `{{ template "slack.default.title" . }}`, TitleLink: `{{ template "slack.default.titlelink" . }}`, IconEmoji: `{{ template "slack.default.iconemoji" . }}`, IconURL: `{{ template "slack.default.iconurl" . }}`, Pretext: `{{ template "slack.default.pretext" . }}`, Text: `{{ template "slack.default.text" . }}`, Fallback: `{{ template "slack.default.fallback" . }}`, } // DefaultHipchatConfig defines default values for Hipchat configurations. DefaultHipchatConfig = HipchatConfig{ NotifierConfig: NotifierConfig{ VSendResolved: false, }, Color: `{{ if eq .Status "firing" }}red{{ else }}green{{ end }}`, From: `{{ template "hipchat.default.from" . }}`, Notify: false, Message: `{{ template "hipchat.default.message" . }}`, MessageFormat: `text`, } // DefaultOpsGenieConfig defines default values for OpsGenie configurations. DefaultOpsGenieConfig = OpsGenieConfig{ NotifierConfig: NotifierConfig{ VSendResolved: true, }, Message: `{{ template "opsgenie.default.message" . }}`, Description: `{{ template "opsgenie.default.description" . }}`, Source: `{{ template "opsgenie.default.source" . }}`, // TODO: Add a details field with all the alerts. } // DefaultVictorOpsConfig defines default values for VictorOps configurations. DefaultVictorOpsConfig = VictorOpsConfig{ NotifierConfig: NotifierConfig{ VSendResolved: true, }, MessageType: `CRITICAL`, StateMessage: `{{ template "victorops.default.state_message" . }}`, MonitoringTool: `{{ template "victorops.default.monitoring_tool" . }}`, } // DefaultPushoverConfig defines default values for Pushover configurations. DefaultPushoverConfig = PushoverConfig{ NotifierConfig: NotifierConfig{ VSendResolved: true, }, Title: `{{ template "pushover.default.title" . }}`, Message: `{{ template "pushover.default.message" . }}`, URL: `{{ template "pushover.default.url" . }}`, Priority: `{{ if eq .Status "firing" }}2{{ else }}0{{ end }}`, // emergency (firing) or normal Retry: duration(1 * time.Minute), Expire: duration(1 * time.Hour), } ) // NotifierConfig contains base options common across all notifier configurations. type NotifierConfig struct { VSendResolved bool `yaml:"send_resolved" json:"send_resolved"` } func (nc *NotifierConfig) SendResolved() bool { return nc.VSendResolved } // EmailConfig configures notifications via mail. type EmailConfig struct { NotifierConfig `yaml:",inline" json:",inline"` // Email address to notify. To string `yaml:"to" json:"to"` From string `yaml:"from" json:"from"` Smarthost string `yaml:"smarthost,omitempty" json:"smarthost,omitempty"` AuthUsername string `yaml:"auth_username" json:"auth_username"` AuthPassword Secret `yaml:"auth_password" json:"auth_password"` AuthSecret Secret `yaml:"auth_secret" json:"auth_secret"` AuthIdentity string `yaml:"auth_identity" json:"auth_identity"` Headers map[string]string `yaml:"headers" json:"headers"` HTML string `yaml:"html" json:"html"` RequireTLS *bool `yaml:"require_tls,omitempty" json:"require_tls,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *EmailConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultEmailConfig type plain EmailConfig if err := unmarshal((*plain)(c)); err != nil { return err } if c.To == "" { return fmt.Errorf("missing to address in email config") } // Header names are case-insensitive, check for collisions. normalizedHeaders := map[string]string{} for h, v := range c.Headers { normalized := strings.Title(h) if _, ok := normalizedHeaders[normalized]; ok { return fmt.Errorf("duplicate header %q in email config", normalized) } normalizedHeaders[normalized] = v } c.Headers = normalizedHeaders return checkOverflow(c.XXX, "email config") } // PagerdutyConfig configures notifications via PagerDuty. type PagerdutyConfig struct { NotifierConfig `yaml:",inline" json:",inline"` ServiceKey Secret `yaml:"service_key" json:"service_key"` URL string `yaml:"url" json:"url"` Client string `yaml:"client" json:"client"` ClientURL string `yaml:"client_url" json:"client_url"` Description string `yaml:"description" json:"description"` Details map[string]string `yaml:"details" json:"details"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *PagerdutyConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultPagerdutyConfig type plain PagerdutyConfig if err := unmarshal((*plain)(c)); err != nil { return err } if c.ServiceKey == "" { return fmt.Errorf("missing service key in PagerDuty config") } return checkOverflow(c.XXX, "pagerduty config") } // SlackConfig configures notifications via Slack. type SlackConfig struct { NotifierConfig `yaml:",inline" json:",inline"` APIURL Secret `yaml:"api_url" json:"api_url"` // Slack channel override, (like #other-channel or @username). Channel string `yaml:"channel" json:"channel"` Username string `yaml:"username" json:"username"` Color string `yaml:"color" json:"color"` Title string `yaml:"title" json:"title"` TitleLink string `yaml:"title_link" json:"title_link"` Pretext string `yaml:"pretext" json:"pretext"` Text string `yaml:"text" json:"text"` Fallback string `yaml:"fallback" json:"fallback"` IconEmoji string `yaml:"icon_emoji" json:"icon_emoji"` IconURL string `yaml:"icon_url" json:"icon_url"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *SlackConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultSlackConfig type plain SlackConfig if err := unmarshal((*plain)(c)); err != nil { return err } return checkOverflow(c.XXX, "slack config") } // HipchatConfig configures notifications via Hipchat. type HipchatConfig struct { NotifierConfig `yaml:",inline" json:",inline"` APIURL string `yaml:"api_url" json:"api_url"` AuthToken Secret `yaml:"auth_token" json:"auth_token"` RoomID string `yaml:"room_id" json:"room_id"` From string `yaml:"from" json:"from"` Notify bool `yaml:"notify" json:"notify"` Message string `yaml:"message" json:"message"` MessageFormat string `yaml:"message_format" json:"message_format"` Color string `yaml:"color" json:"color"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *HipchatConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultHipchatConfig type plain HipchatConfig if err := unmarshal((*plain)(c)); err != nil { return err } if c.RoomID == "" { return fmt.Errorf("missing room id in Hipchat config") } return checkOverflow(c.XXX, "hipchat config") } // WebhookConfig configures notifications via a generic webhook. type WebhookConfig struct { NotifierConfig `yaml:",inline" json:",inline"` // URL to send POST request to. URL string `yaml:"url" json:"url"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *WebhookConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultWebhookConfig type plain WebhookConfig if err := unmarshal((*plain)(c)); err != nil { return err } if c.URL == "" { return fmt.Errorf("missing URL in webhook config") } return checkOverflow(c.XXX, "webhook config") } // OpsGenieConfig configures notifications via OpsGenie. type OpsGenieConfig struct { NotifierConfig `yaml:",inline" json:",inline"` APIKey Secret `yaml:"api_key" json:"api_key"` APIHost string `yaml:"api_host" json:"api_host"` Message string `yaml:"message" json:"message"` Description string `yaml:"description" json:"description"` Source string `yaml:"source" json:"source"` Details map[string]string `yaml:"details" json:"details"` Teams string `yaml:"teams" json:"teams"` Tags string `yaml:"tags" json:"tags"` Note string `yaml:"note" json:"note"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *OpsGenieConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultOpsGenieConfig type plain OpsGenieConfig if err := unmarshal((*plain)(c)); err != nil { return err } if c.APIKey == "" { return fmt.Errorf("missing API key in OpsGenie config") } return checkOverflow(c.XXX, "opsgenie config") } // VictorOpsConfig configures notifications via VictorOps. type VictorOpsConfig struct { NotifierConfig `yaml:",inline" json:",inline"` APIKey Secret `yaml:"api_key" json:"api_key"` APIURL string `yaml:"api_url" json:"api_url"` RoutingKey string `yaml:"routing_key" json:"routing_key"` MessageType string `yaml:"message_type" json:"message_type"` StateMessage string `yaml:"state_message" json:"state_message"` MonitoringTool string `yaml:"monitoring_tool" json:"monitoring_tool"` XXX map[string]interface{} `yaml:",inline" json:"-"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *VictorOpsConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultVictorOpsConfig type plain VictorOpsConfig if err := unmarshal((*plain)(c)); err != nil { return err } if c.APIKey == "" { return fmt.Errorf("missing API key in VictorOps config") } if c.RoutingKey == "" { return fmt.Errorf("missing Routing key in VictorOps config") } return checkOverflow(c.XXX, "victorops config") } type duration time.Duration func (d *duration) UnmarshalText(text []byte) error { parsed, err := time.ParseDuration(string(text)) if err == nil { *d = duration(parsed) } return err } func (d duration) MarshalText() ([]byte, error) { return []byte(time.Duration(d).String()), nil } type PushoverConfig struct { NotifierConfig `yaml:",inline" json:",inline"` UserKey Secret `yaml:"user_key" json:"user_key"` Token Secret `yaml:"token" json:"token"` Title string `yaml:"title" json:"title"` Message string `yaml:"message" json:"message"` URL string `yaml:"url" json:"url"` Priority string `yaml:"priority" json:"priority"` Retry duration `yaml:"retry" json:"retry"` Expire duration `yaml:"expire" json:"expire"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *PushoverConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultPushoverConfig type plain PushoverConfig if err := unmarshal((*plain)(c)); err != nil { return err } if c.UserKey == "" { return fmt.Errorf("missing user key in Pushover config") } if c.Token == "" { return fmt.Errorf("missing token in Pushover config") } return checkOverflow(c.XXX, "pushover config") } prometheus-alertmanager-0.6.2+ds/dispatch/000077500000000000000000000000001314512360300205515ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/dispatch/dispatch.go000066400000000000000000000237341314512360300227100ustar00rootroot00000000000000package dispatch import ( "fmt" "sort" "sync" "time" "github.com/prometheus/common/log" "github.com/prometheus/common/model" "github.com/prometheus/prometheus/pkg/labels" "golang.org/x/net/context" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/provider" "github.com/prometheus/alertmanager/types" ) // Dispatcher sorts incoming alerts into aggregation groups and // assigns the correct notifiers to each. type Dispatcher struct { route *Route alerts provider.Alerts stage notify.Stage marker types.Marker timeout func(time.Duration) time.Duration aggrGroups map[*Route]map[model.Fingerprint]*aggrGroup mtx sync.RWMutex done chan struct{} ctx context.Context cancel func() log log.Logger } // NewDispatcher returns a new Dispatcher. func NewDispatcher( ap provider.Alerts, r *Route, s notify.Stage, mk types.Marker, to func(time.Duration) time.Duration, ) *Dispatcher { disp := &Dispatcher{ alerts: ap, stage: s, route: r, marker: mk, timeout: to, log: log.With("component", "dispatcher"), } return disp } // Run starts dispatching alerts incoming via the updates channel. func (d *Dispatcher) Run() { d.done = make(chan struct{}) d.mtx.Lock() d.aggrGroups = map[*Route]map[model.Fingerprint]*aggrGroup{} d.mtx.Unlock() d.ctx, d.cancel = context.WithCancel(context.Background()) d.run(d.alerts.Subscribe()) close(d.done) } // AlertBlock contains a list of alerts associated with a set of // routing options. type AlertBlock struct { RouteOpts *RouteOpts `json:"routeOpts"` Alerts []*APIAlert `json:"alerts"` } // APIAlert is the API representation of an alert, which is a regular alert // annotated with silencing and inhibition info. type APIAlert struct { *model.Alert Status types.AlertStatus `json:"status"` } // AlertGroup is a list of alert blocks grouped by the same label set. type AlertGroup struct { Labels model.LabelSet `json:"labels"` GroupKey string `json:"groupKey"` Blocks []*AlertBlock `json:"blocks"` } // AlertOverview is a representation of all active alerts in the system. type AlertOverview []*AlertGroup func (ao AlertOverview) Swap(i, j int) { ao[i], ao[j] = ao[j], ao[i] } func (ao AlertOverview) Less(i, j int) bool { return ao[i].Labels.Before(ao[j].Labels) } func (ao AlertOverview) Len() int { return len(ao) } func matchesFilterLabels(a *APIAlert, matchers []*labels.Matcher) bool { for _, m := range matchers { if v, prs := a.Labels[model.LabelName(m.Name)]; !prs || !m.Matches(string(v)) { return false } } return true } // Groups populates an AlertOverview from the dispatcher's internal state. func (d *Dispatcher) Groups(matchers []*labels.Matcher) AlertOverview { overview := AlertOverview{} d.mtx.RLock() defer d.mtx.RUnlock() seen := map[model.Fingerprint]*AlertGroup{} for route, ags := range d.aggrGroups { for _, ag := range ags { alertGroup, ok := seen[ag.fingerprint()] if !ok { alertGroup = &AlertGroup{Labels: ag.labels} alertGroup.GroupKey = ag.GroupKey() seen[ag.fingerprint()] = alertGroup } now := time.Now() var apiAlerts []*APIAlert for _, a := range types.Alerts(ag.alertSlice()...) { if !a.EndsAt.IsZero() && a.EndsAt.Before(now) { continue } status := d.marker.Status(a.Fingerprint()) aa := &APIAlert{ Alert: a, Status: status, } if !matchesFilterLabels(aa, matchers) { continue } apiAlerts = append(apiAlerts, aa) } if len(apiAlerts) == 0 { continue } alertGroup.Blocks = append(alertGroup.Blocks, &AlertBlock{ RouteOpts: &route.RouteOpts, Alerts: apiAlerts, }) overview = append(overview, alertGroup) } } sort.Sort(overview) return overview } func (d *Dispatcher) run(it provider.AlertIterator) { cleanup := time.NewTicker(30 * time.Second) defer cleanup.Stop() defer it.Close() for { select { case alert, ok := <-it.Next(): if !ok { // Iterator exhausted for some reason. if err := it.Err(); err != nil { log.Errorf("Error on alert update: %s", err) } return } d.log.With("alert", alert).Debug("Received alert") // Log errors but keep trying. if err := it.Err(); err != nil { log.Errorf("Error on alert update: %s", err) continue } for _, r := range d.route.Match(alert.Labels) { d.processAlert(alert, r) } case <-cleanup.C: d.mtx.Lock() for _, groups := range d.aggrGroups { for _, ag := range groups { if ag.empty() { ag.stop() delete(groups, ag.fingerprint()) } } } d.mtx.Unlock() case <-d.ctx.Done(): return } } } // Stop the dispatcher. func (d *Dispatcher) Stop() { if d == nil || d.cancel == nil { return } d.cancel() d.cancel = nil <-d.done } // notifyFunc is a function that performs notifcation for the alert // with the given fingerprint. It aborts on context cancelation. // Returns false iff notifying failed. type notifyFunc func(context.Context, ...*types.Alert) bool // processAlert determines in which aggregation group the alert falls // and insert it. func (d *Dispatcher) processAlert(alert *types.Alert, route *Route) { group := model.LabelSet{} for ln, lv := range alert.Labels { if _, ok := route.RouteOpts.GroupBy[ln]; ok { group[ln] = lv } } fp := group.Fingerprint() d.mtx.Lock() groups, ok := d.aggrGroups[route] if !ok { groups = map[model.Fingerprint]*aggrGroup{} d.aggrGroups[route] = groups } d.mtx.Unlock() // If the group does not exist, create it. ag, ok := groups[fp] if !ok { ag = newAggrGroup(d.ctx, group, route, d.timeout) groups[fp] = ag go ag.run(func(ctx context.Context, alerts ...*types.Alert) bool { _, _, err := d.stage.Exec(ctx, alerts...) if err != nil { log.Errorf("Notify for %d alerts failed: %s", len(alerts), err) } return err == nil }) } ag.insert(alert) } // aggrGroup aggregates alert fingerprints into groups to which a // common set of routing options applies. // It emits notifications in the specified intervals. type aggrGroup struct { labels model.LabelSet opts *RouteOpts log log.Logger routeKey string ctx context.Context cancel func() done chan struct{} next *time.Timer timeout func(time.Duration) time.Duration mtx sync.RWMutex alerts map[model.Fingerprint]*types.Alert hasSent bool } // newAggrGroup returns a new aggregation group. func newAggrGroup(ctx context.Context, labels model.LabelSet, r *Route, to func(time.Duration) time.Duration) *aggrGroup { if to == nil { to = func(d time.Duration) time.Duration { return d } } ag := &aggrGroup{ labels: labels, routeKey: r.Key(), opts: &r.RouteOpts, timeout: to, alerts: map[model.Fingerprint]*types.Alert{}, } ag.ctx, ag.cancel = context.WithCancel(ctx) ag.log = log.With("aggrGroup", ag) // Set an initial one-time wait before flushing // the first batch of notifications. ag.next = time.NewTimer(ag.opts.GroupWait) return ag } func (ag *aggrGroup) fingerprint() model.Fingerprint { return ag.labels.Fingerprint() } func (ag *aggrGroup) GroupKey() string { return fmt.Sprintf("%s:%s", ag.routeKey, ag.labels) } func (ag *aggrGroup) String() string { return ag.GroupKey() } func (ag *aggrGroup) alertSlice() []*types.Alert { ag.mtx.RLock() defer ag.mtx.RUnlock() var alerts []*types.Alert for _, a := range ag.alerts { alerts = append(alerts, a) } return alerts } func (ag *aggrGroup) run(nf notifyFunc) { ag.done = make(chan struct{}) defer close(ag.done) defer ag.next.Stop() for { select { case now := <-ag.next.C: // Give the notifcations time until the next flush to // finish before terminating them. ctx, cancel := context.WithTimeout(ag.ctx, ag.timeout(ag.opts.GroupInterval)) // The now time we retrieve from the ticker is the only reliable // point of time reference for the subsequent notification pipeline. // Calculating the current time directly is prone to flaky behavior, // which usually only becomes apparent in tests. ctx = notify.WithNow(ctx, now) // Populate context with information needed along the pipeline. ctx = notify.WithGroupKey(ctx, ag.GroupKey()) ctx = notify.WithGroupLabels(ctx, ag.labels) ctx = notify.WithReceiverName(ctx, ag.opts.Receiver) ctx = notify.WithRepeatInterval(ctx, ag.opts.RepeatInterval) // Wait the configured interval before calling flush again. ag.mtx.Lock() ag.next.Reset(ag.opts.GroupInterval) ag.mtx.Unlock() ag.flush(func(alerts ...*types.Alert) bool { return nf(ctx, alerts...) }) cancel() case <-ag.ctx.Done(): return } } } func (ag *aggrGroup) stop() { // Calling cancel will terminate all in-process notifications // and the run() loop. ag.cancel() <-ag.done } // insert inserts the alert into the aggregation group. If the aggregation group // is empty afterwards, it returns true. func (ag *aggrGroup) insert(alert *types.Alert) { ag.mtx.Lock() defer ag.mtx.Unlock() ag.alerts[alert.Fingerprint()] = alert // Immediately trigger a flush if the wait duration for this // alert is already over. if !ag.hasSent && alert.StartsAt.Add(ag.opts.GroupWait).Before(time.Now()) { ag.next.Reset(0) } } func (ag *aggrGroup) empty() bool { ag.mtx.RLock() defer ag.mtx.RUnlock() return len(ag.alerts) == 0 } // flush sends notifications for all new alerts. func (ag *aggrGroup) flush(notify func(...*types.Alert) bool) { if ag.empty() { return } ag.mtx.Lock() var ( alerts = make(map[model.Fingerprint]*types.Alert, len(ag.alerts)) alertsSlice = make([]*types.Alert, 0, len(ag.alerts)) ) for fp, alert := range ag.alerts { alerts[fp] = alert alertsSlice = append(alertsSlice, alert) } ag.mtx.Unlock() ag.log.Debugln("flushing", alertsSlice) if notify(alertsSlice...) { ag.mtx.Lock() for fp, a := range alerts { // Only delete if the fingerprint has not been inserted // again since we notified about it. if a.Resolved() && ag.alerts[fp] == a { delete(ag.alerts, fp) } } ag.hasSent = true ag.mtx.Unlock() } } prometheus-alertmanager-0.6.2+ds/dispatch/dispatch_test.go000066400000000000000000000150531314512360300237420ustar00rootroot00000000000000package dispatch import ( "reflect" "sort" "testing" "time" "github.com/prometheus/common/model" "github.com/prometheus/prometheus/pkg/labels" "golang.org/x/net/context" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/types" ) func newAPIAlert(labels model.LabelSet) APIAlert { return APIAlert{ Alert: &model.Alert{ Labels: labels, StartsAt: time.Now().Add(1 * time.Minute), EndsAt: time.Now().Add(1 * time.Hour), }, } } func TestFilterLabels(t *testing.T) { var ( a1 = newAPIAlert(model.LabelSet{ "a": "v1", "b": "v2", "c": "v3", }) a2 = newAPIAlert(model.LabelSet{ "a": "v1", "b": "v2", "c": "v4", }) a3 = newAPIAlert(model.LabelSet{ "a": "v1", "b": "v2", "c": "v5", }) a4 = newAPIAlert(model.LabelSet{ "foo": "bar", "baz": "qux", }) alertsSlices = []struct { in, want []APIAlert }{ { in: []APIAlert{a1, a2, a3}, want: []APIAlert{a1, a2, a3}, }, { in: []APIAlert{a1, a4}, want: []APIAlert{a1}, }, { in: []APIAlert{a4}, want: []APIAlert{}, }, } ) matcher, err := labels.NewMatcher(labels.MatchRegexp, "c", "v.*") if err != nil { t.Fatalf("error making matcher: %v", err) } matcher2, err := labels.NewMatcher(labels.MatchEqual, "a", "v1") if err != nil { t.Fatalf("error making matcher: %v", err) } matchers := []*labels.Matcher{matcher, matcher2} for _, alerts := range alertsSlices { got := []APIAlert{} for _, a := range alerts.in { if matchesFilterLabels(&a, matchers) { got = append(got, a) } } if !reflect.DeepEqual(got, alerts.want) { t.Fatalf("error: returned alerts do not match:\ngot %v\nwant %v", got, alerts.want) } } } func TestAggrGroup(t *testing.T) { lset := model.LabelSet{ "a": "v1", "b": "v2", } opts := &RouteOpts{ Receiver: "n1", GroupBy: map[model.LabelName]struct{}{}, GroupWait: 1 * time.Second, GroupInterval: 300 * time.Millisecond, RepeatInterval: 1 * time.Hour, } route := &Route{ RouteOpts: *opts, } var ( a1 = &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{ "a": "v1", "b": "v2", "c": "v3", }, StartsAt: time.Now().Add(time.Minute), EndsAt: time.Now().Add(time.Hour), }, UpdatedAt: time.Now(), } a2 = &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{ "a": "v1", "b": "v2", "c": "v4", }, StartsAt: time.Now().Add(-time.Hour), EndsAt: time.Now().Add(2 * time.Hour), }, UpdatedAt: time.Now(), } a3 = &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{ "a": "v1", "b": "v2", "c": "v5", }, StartsAt: time.Now().Add(time.Minute), EndsAt: time.Now().Add(5 * time.Minute), }, UpdatedAt: time.Now(), } ) var ( last = time.Now() current = time.Now() alertsCh = make(chan types.AlertSlice) ) ntfy := func(ctx context.Context, alerts ...*types.Alert) bool { // Validate that the context is properly populated. if _, ok := notify.Now(ctx); !ok { t.Errorf("now missing") } if _, ok := notify.GroupKey(ctx); !ok { t.Errorf("group key missing") } if lbls, ok := notify.GroupLabels(ctx); !ok || !reflect.DeepEqual(lbls, lset) { t.Errorf("wrong group labels: %q", lbls) } if rcv, ok := notify.ReceiverName(ctx); !ok || rcv != opts.Receiver { t.Errorf("wrong receiver: %q", rcv) } if ri, ok := notify.RepeatInterval(ctx); !ok || ri != opts.RepeatInterval { t.Errorf("wrong repeat interval: %q", ri) } last = current current = time.Now() alertsCh <- types.AlertSlice(alerts) return true } // Test regular situation where we wait for group_wait to send out alerts. ag := newAggrGroup(context.Background(), lset, route, nil) go ag.run(ntfy) ag.insert(a1) select { case <-time.After(2 * opts.GroupWait): t.Fatalf("expected initial batch after group_wait") case batch := <-alertsCh: if s := time.Since(last); s < opts.GroupWait { t.Fatalf("received batch to early after %v", s) } exp := types.AlertSlice{a1} sort.Sort(batch) if !reflect.DeepEqual(batch, exp) { t.Fatalf("expected alerts %v but got %v", exp, batch) } } for i := 0; i < 3; i++ { // New alert should come in after group interval. ag.insert(a3) select { case <-time.After(2 * opts.GroupInterval): t.Fatalf("expected new batch after group interval but received none") case batch := <-alertsCh: if s := time.Since(last); s < opts.GroupInterval { t.Fatalf("received batch to early after %v", s) } exp := types.AlertSlice{a1, a3} sort.Sort(batch) if !reflect.DeepEqual(batch, exp) { t.Fatalf("expected alerts %v but got %v", exp, batch) } } } ag.stop() // Add an alert that started more than group_interval in the past. We expect // immediate flushing. // Finally, set all alerts to be resolved. After successful notify the aggregation group // should empty itself. ag = newAggrGroup(context.Background(), lset, route, nil) go ag.run(ntfy) ag.insert(a1) ag.insert(a2) // a2 lies way in the past so the initial group_wait should be skipped. select { case <-time.After(opts.GroupWait / 2): t.Fatalf("expected immediate alert but received none") case batch := <-alertsCh: exp := types.AlertSlice{a1, a2} sort.Sort(batch) if !reflect.DeepEqual(batch, exp) { t.Fatalf("expected alerts %v but got %v", exp, batch) } } for i := 0; i < 3; i++ { // New alert should come in after group interval. ag.insert(a3) select { case <-time.After(2 * opts.GroupInterval): t.Fatalf("expected new batch after group interval but received none") case batch := <-alertsCh: if s := time.Since(last); s < opts.GroupInterval { t.Fatalf("received batch to early after %v", s) } exp := types.AlertSlice{a1, a2, a3} sort.Sort(batch) if !reflect.DeepEqual(batch, exp) { t.Fatalf("expected alerts %v but got %v", exp, batch) } } } // Resolve all alerts, they should be removed after the next batch was sent. a1.EndsAt = time.Now() a2.EndsAt = time.Now() a3.EndsAt = time.Now() select { case <-time.After(2 * opts.GroupInterval): t.Fatalf("expected new batch after group interval but received none") case batch := <-alertsCh: if s := time.Since(last); s < opts.GroupInterval { t.Fatalf("received batch to early after %v", s) } exp := types.AlertSlice{a1, a2, a3} sort.Sort(batch) if !reflect.DeepEqual(batch, exp) { t.Fatalf("expected alerts %v but got %v", exp, batch) } if !ag.empty() { t.Fatalf("Expected aggregation group to be empty after resolving alerts") } } ag.stop() } prometheus-alertmanager-0.6.2+ds/dispatch/route.go000066400000000000000000000117201314512360300222370ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package dispatch import ( "encoding/json" "fmt" "time" "github.com/prometheus/common/model" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/types" ) // DefaultRouteOpts are the defaulting routing options which apply // to the root route of a routing tree. var DefaultRouteOpts = RouteOpts{ GroupWait: 30 * time.Second, GroupInterval: 5 * time.Minute, RepeatInterval: 4 * time.Hour, GroupBy: map[model.LabelName]struct{}{ model.AlertNameLabel: struct{}{}, }, } // A Route is a node that contains definitions of how to handle alerts. type Route struct { parent *Route // The configuration parameters for matches of this route. RouteOpts RouteOpts // Equality or regex matchers an alert has to fulfill to match // this route. Matchers types.Matchers // If true, an alert matches further routes on the same level. Continue bool // Children routes of this route. Routes []*Route } // NewRoute returns a new route. func NewRoute(cr *config.Route, parent *Route) *Route { // Create default and overwrite with configured settings. opts := DefaultRouteOpts if parent != nil { opts = parent.RouteOpts } if cr.Receiver != "" { opts.Receiver = cr.Receiver } if cr.GroupBy != nil { opts.GroupBy = map[model.LabelName]struct{}{} for _, ln := range cr.GroupBy { opts.GroupBy[ln] = struct{}{} } } if cr.GroupWait != nil { opts.GroupWait = time.Duration(*cr.GroupWait) } if cr.GroupInterval != nil { opts.GroupInterval = time.Duration(*cr.GroupInterval) } if cr.RepeatInterval != nil { opts.RepeatInterval = time.Duration(*cr.RepeatInterval) } // Build matchers. var matchers types.Matchers for ln, lv := range cr.Match { matchers = append(matchers, types.NewMatcher(model.LabelName(ln), lv)) } for ln, lv := range cr.MatchRE { matchers = append(matchers, types.NewRegexMatcher(model.LabelName(ln), lv.Regexp)) } route := &Route{ parent: parent, RouteOpts: opts, Matchers: matchers, Continue: cr.Continue, } route.Routes = NewRoutes(cr.Routes, route) return route } // NewRoutes returns a slice of routes. func NewRoutes(croutes []*config.Route, parent *Route) []*Route { res := []*Route{} for _, cr := range croutes { res = append(res, NewRoute(cr, parent)) } return res } // Match does a depth-first left-to-right search through the route tree // and returns the matching routing nodes. func (r *Route) Match(lset model.LabelSet) []*Route { if !r.Matchers.Match(lset) { return nil } var all []*Route for _, cr := range r.Routes { matches := cr.Match(lset) all = append(all, matches...) if matches != nil && !cr.Continue { break } } // If no child nodes were matches, the current node itself is a match. if len(all) == 0 { all = append(all, r) } return all } // Key returns a key for the route. It does not uniquely identify a the route in general. func (r *Route) Key() string { b := make([]byte, 0, 1024) if r.parent != nil { b = append(b, r.parent.Key()...) b = append(b, '/') } return string(append(b, r.Matchers.String()...)) } // RouteOpts holds various routing options necessary for processing alerts // that match a given route. type RouteOpts struct { // The identifier of the associated notification configuration Receiver string // What labels to group alerts by for notifications. GroupBy map[model.LabelName]struct{} // How long to wait to group matching alerts before sending // a notificaiton GroupWait time.Duration GroupInterval time.Duration RepeatInterval time.Duration } func (ro *RouteOpts) String() string { var labels []model.LabelName for ln := range ro.GroupBy { labels = append(labels, ln) } return fmt.Sprintf("", ro.Receiver, labels, ro.GroupWait, ro.GroupInterval) } // MarshalJSON returns a JSON representation of the routing options. func (ro *RouteOpts) MarshalJSON() ([]byte, error) { v := struct { Receiver string `json:"receiver"` GroupBy model.LabelNames `json:"groupBy"` GroupWait time.Duration `json:"groupWait"` GroupInterval time.Duration `json:"groupInterval"` RepeatInterval time.Duration `json:"repeatInterval"` }{ Receiver: ro.Receiver, GroupWait: ro.GroupWait, GroupInterval: ro.GroupInterval, RepeatInterval: ro.RepeatInterval, } for ln := range ro.GroupBy { v.GroupBy = append(v.GroupBy, ln) } return json.Marshal(&v) } prometheus-alertmanager-0.6.2+ds/dispatch/route_test.go000066400000000000000000000115721314512360300233030ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package dispatch import ( "reflect" "testing" "time" "github.com/prometheus/common/model" "gopkg.in/yaml.v2" "github.com/prometheus/alertmanager/config" ) func TestRouteMatch(t *testing.T) { in := ` receiver: 'notify-def' routes: - match: owner: 'team-A' receiver: 'notify-A' routes: - match: env: 'testing' receiver: 'notify-testing' group_by: [] - match: env: "production" receiver: 'notify-productionA' group_wait: 1m continue: true - match_re: env: "produ.*" receiver: 'notify-productionB' group_wait: 30s group_interval: 5m repeat_interval: 1h group_by: ['job'] - match_re: owner: 'team-(B|C)' group_by: ['foo', 'bar'] group_wait: 2m receiver: 'notify-BC' - match: group_by: 'role' group_by: ['role'] routes: - match: env: 'testing' receiver: 'notify-testing' routes: - match: wait: 'long' group_wait: 2m ` var ctree config.Route if err := yaml.Unmarshal([]byte(in), &ctree); err != nil { t.Fatal(err) } var ( def = DefaultRouteOpts tree = NewRoute(&ctree, nil) ) lset := func(labels ...string) map[model.LabelName]struct{} { s := map[model.LabelName]struct{}{} for _, ls := range labels { s[model.LabelName(ls)] = struct{}{} } return s } tests := []struct { input model.LabelSet result []*RouteOpts }{ { input: model.LabelSet{ "owner": "team-A", }, result: []*RouteOpts{ { Receiver: "notify-A", GroupBy: def.GroupBy, GroupWait: def.GroupWait, GroupInterval: def.GroupInterval, RepeatInterval: def.RepeatInterval, }, }, }, { input: model.LabelSet{ "owner": "team-A", "env": "unset", }, result: []*RouteOpts{ { Receiver: "notify-A", GroupBy: def.GroupBy, GroupWait: def.GroupWait, GroupInterval: def.GroupInterval, RepeatInterval: def.RepeatInterval, }, }, }, { input: model.LabelSet{ "owner": "team-C", }, result: []*RouteOpts{ { Receiver: "notify-BC", GroupBy: lset("foo", "bar"), GroupWait: 2 * time.Minute, GroupInterval: def.GroupInterval, RepeatInterval: def.RepeatInterval, }, }, }, { input: model.LabelSet{ "owner": "team-A", "env": "testing", }, result: []*RouteOpts{ { Receiver: "notify-testing", GroupBy: lset(), GroupWait: def.GroupWait, GroupInterval: def.GroupInterval, RepeatInterval: def.RepeatInterval, }, }, }, { input: model.LabelSet{ "owner": "team-A", "env": "production", }, result: []*RouteOpts{ { Receiver: "notify-productionA", GroupBy: def.GroupBy, GroupWait: 1 * time.Minute, GroupInterval: def.GroupInterval, RepeatInterval: def.RepeatInterval, }, { Receiver: "notify-productionB", GroupBy: lset("job"), GroupWait: 30 * time.Second, GroupInterval: 5 * time.Minute, RepeatInterval: 1 * time.Hour, }, }, }, { input: model.LabelSet{ "group_by": "role", }, result: []*RouteOpts{ { Receiver: "notify-def", GroupBy: lset("role"), GroupWait: def.GroupWait, GroupInterval: def.GroupInterval, RepeatInterval: def.RepeatInterval, }, }, }, { input: model.LabelSet{ "env": "testing", "group_by": "role", }, result: []*RouteOpts{ { Receiver: "notify-testing", GroupBy: lset("role"), GroupWait: def.GroupWait, GroupInterval: def.GroupInterval, RepeatInterval: def.RepeatInterval, }, }, }, { input: model.LabelSet{ "env": "testing", "group_by": "role", "wait": "long", }, result: []*RouteOpts{ { Receiver: "notify-testing", GroupBy: lset("role"), GroupWait: 2 * time.Minute, GroupInterval: def.GroupInterval, RepeatInterval: def.RepeatInterval, }, }, }, } for _, test := range tests { var matches []*RouteOpts for _, r := range tree.Match(test.input) { matches = append(matches, &r.RouteOpts) } if !reflect.DeepEqual(matches, test.result) { t.Errorf("\nexpected:\n%v\ngot:\n%v", test.result, matches) } } } prometheus-alertmanager-0.6.2+ds/doc/000077500000000000000000000000001314512360300175175ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/doc/arch.jpg000066400000000000000000012770701314512360300211540ustar00rootroot00000000000000JFIFC   %# , #&')*)-0-(0%()(C   ((((((((((((((((((((((((((((((((((((((((((((((((((( , P" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((x~FKpJ»Lc8U22qEtG;cw0ZG6ژ3`=Q@pWzOX5m/,&Y G_%~)=SZ%0tpQgM}k@cg\=yC pjcw}ͭɂHVO00`p:@EPEPHK1@' E6+YרJ*}o鶗R,Q,H! 5f ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (9?'O?6( qmti"168?gυp|Eo\jfxlmDт7\8;Hnh\k?ZCIs=͎%d9]TݘWIHȿ3ry(Q@zh|57bZIC ~@~/_A%A|-:/kNc%OG-|χz- LZ# :]gĺ3oq㺶?W~ZbjH>kxpcuokvE8iӣ{5Q@Q@|p M8{ QG5|rNȭx cY%i=֥4Qs׻WLݸR2LWPEPEPEPEPEPEPEPEPEPEPEPEPEPEPVsgň-<=+oh~[-.[R,c F=Xմ>5Da_ ~ߦ^n^]2pު(^^E#<^te©RAEP@ -QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEs'ÛKWfۂT猀;mMwM։wwQiY_`I8I 3OI]NҮ=hퟌ1~xXd &(Jaog 9'bs|N¯՘A1Rmd'!NAgI_*p /@2xǏLjQqa=^O),15 !urp3qBӾ%1k Lnϩݑ6;k(%" @&=oxw6>$g|=49dH$_r>ֻax]3B 59 wqӮGpX]6G'U@ߍ!W}^+)&-$=v'|T@<3Z; ::)\dG#ҼgĚ3%֡jW,Y)T~OSZx7X>k'݃p@K~ŒW)Ġ}Vo#e]:Xu ;ȭj(OxE~&+R摰HD^AVkH|=iɤu*T3z<;Ͱ#Miq2 }(((___W1]67XU 94濤h>YiгmWbR}b*Յi֟sմ),2Fpk;ž*]>-F䓷o0{"Q <s o-}Z$4g9gހ?Jc៊~u,䳒- 0J窒2pEtQEEusw4P@-$T{ ha{o ng?mk+kyX\nGF =Ah?{XJ\Lɷm͜mgI}2O7Hik Z4Rz?\H::ȊC+ ;Z~ORvTKiݾ0׾s]Tumi%V dOA^Oem 1D fq+ȼA7+jSlvjJ:8b}%dae9z@umi%V dOA^/ocK4zim؀G=ŠAnJTz}4I$gr:SJ%"FYk۽A,m|SMvjlX OqMկMhټ$d8>[JPq޾.wWF k )PʧX~V)ikW U"( :jp1 dm"KQOx*(tn((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((ɢ6:Ƨ^ӲjںA1$}n0H#Q@'_G4K6D;ggx.d׵%|Ig߀:ťIrqQd}K@o xq:zz]mJѢ>}+gWOmn<0;eJ1%y=TW(K9En%Y嘩#@aohNeM- '7ξtnJ= ǰ~:ÿu# olpw };_[VH ox'↓Pbf̋s,PW$_Sk}+eD21=9IּO{&FEoTaBRsۺ{ eOjW#k&)e( E''/5kHVՌ1Pp9{|jqinZ6*"@O:ݯφ:ƹ4orsӚ_ |WoMCUuOЦ&Ha8<+o⶝/Mɢ؆1f<®Gok< <%&-j>UbJf o\\P~'Kcu3~DۑH I<+ӗk_S}m[XNNI Zg4/[CɖF7?+~+ f[𾕮kн;lQa>(_cilY`OPs#$WzG۩NwK}r]OoBD<X,ryd0 XIOjd:uQadǼc*\ej\{i-m;KWEe`0NG݆;h?ho ΋Yl4$ ?{ BhZv r_J9>|/SXmB;uչ9 ۋ_[\YK@KE+&xUɨ|n:@$z!E^Kʿ@t}RZӠnἲCG4,XW?oo|52O[ VeUP#>?sao5lg<)jl$t}+-N4흤^c~ &V/-UPzOQ+hem6%k1F00¾& ^4-4Z=6kh202Չlq+,h=F(xT1 G3P CD4Qqav_˅f<`޼W2ū;nvFs1zc8|kzu!T$DP(F  t;^|:>%=VG[q~ʾ-Iiմxس{^=ՏZO/|B4ai&Df3XvelRHCp2ǬxLwkWZZs*u (nXaI H;~5i '׌n" H+ŀ%>#"ڇ ]O Ҁ>x{{^4lLg+<>f G9KkRT-mk$a[^y]GXU㉱ls-݁};W~N_,sY#DZ#_~ t9-k̐em۲O^O[յyH|Ax$'@O@q׎7'ֵ7ĺu%ť6ҹca mj83zghVT20p{Cھ)MK2ϤNd~GuLtc@[|^GzN!E` ־5]gxi`"LLT95)_~%`LJ"-̫,a7wv#0V~ ='E*ĖIy|3 Ѯm`vM2rH hzwt;Z[>6Kq@ A o>2R.&R_*Av:0dwM3:\vco}c/ކt GP| ۦxLd;HklK%';s ױß~*mC[i&$Mi)vd`᳞)=Bo>d[o"*'c=>((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((_>ǎOiix䘤ș);UEA[i6VGoin(v(SEQEQExŏ:7?[kRObO8hfr;+V?gS]>^n F8w W=+Y)w{$fiȃ+*$gRj?w;zpƧ쮠UxX^2SPw3֓j~K99ieq![#`WZ)IEaf?]Q _֨Ta$w[ӵﵚ1l:w~$gmmwT2cҼLJE>)[HT4l>+~$k-ui{h62]~tyCwkjvoQ݋/4' p9Z%Bf 3`7qA/ٯT,5ֲeei$C߃@-X۶bY?}xt+k?kC{K}FN; HŠƃxcAѴ[uM'$OrMp> h?{CXo,d^P>&ZDY,f Em%c|/nkxSLCl27b{WCE|S':1LZ sA#q'9+>#Dqkj(ߌ/ԯu N$lwϸA~x^-+}[syHO8;tPwJ;>}G:rA'|P|C-Hot -N:$gҿCiF.]}dPcVyc]߮?J,cRй==| UIKx!OR=ZGn \(0Ax5y%M#vBr7_P߳Ɲ IYxʪ)dR~5Pu;a#N]EeKt,9rWd _t:\A.md-293_C@7-GŜ9goox-uk}: i $ c<"ƋQGSh#t^"CuExV|K y$=yq_OeZuVp HB(@h((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((+/Ǟ ~ â֡acM,VJ+v+rpF6Cn)0 olmFnNoJ| ;zO|A[j?<~NTm6*z CRԠOFr@bO+`n ?hJ1y<2 ŠzJTay<B>Gs$B}2 $Ӄ)E}KE|doa+؝8ʥs&KxN["k(?eO"=[ÌO8?j|t/<KU~>Ě)]7T?fψO mM&(wl*+oej˜03_}@/GУx>0i|O7aVk?0q_P|S=n^ &/⛓JdVU9:S1@AA|LEc?0{+|#jEK|COuB+9|1LC#?ҳ}فKM|wIHXI_E?OXOyYoʃyfgC?ݲr|y'?B7oq﨓)?k[J_7@6W/߇\g/#}#aKE,c'1Ҁ>s?h Oki?e/gg}wT ǯq /&?Z+,6%W4^m#N T^K1*3~)?*k[T[!jx3k> }~YΝ}s5|Y!h|9<-G6k? v4B3铁h((>%'S"ݷ̐"$h-Ծ=9ӵ)u@9 v:FUH?`|uTR8/U``yO>45д4{]WRAtN|?/ K}#z}.lc$]Ѻȯ`]cܟ̦'?{=HS@8q5R&ܗ6Rq֓[*E>Ep'RY2$Z[=<8?%51J1PWY8Q}&7h?NkR?}/$> B}3ƪ{o&> tLuR #t5lQ?ܖ6xKWxP]EE<tgCkB-jٿ%?"=Ҋo?ĸ5K3=,8An@] (4kUImgR?i _M$YMh^p01YXeX((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((?k#{$Z)/]4OJ7^ѯ sg A #ֵy[Dž. ilhzQEQEQEQEQEQEQEQEQEQEQEQEQEhmŸ>ikHl7+t H3K@h|O \:Irc]S]y?$3ÿ\LQEQEW~7kPen8anz?A^^k,ҿ5<,u[ә?u覾ЯaxB[5ؙ}]@Q@QEUNϟgm.z[?xZJxoEOQ%Mֺ (|~3({S!/c)z%rm{oMTrpmgodM{_ϗs;*x+.0}*e z> uw&hퟱ/O6$Ek(dQ?i_k*PjM4۰$?je)Пnm0xz>K+(W~!8[v?²"IZCpd?}E~/~1ڮȬf)'`mmXwq_P^)ţǖ|*!2j{_t֯4ۻTTPր>GjH?jr蒏Gٚa;gsTo(ڿF2٫b?x:&>} s7,Z}o|!6wxKHxƝZٹo B?Ƚg\V|?jkg߇ZlDkW %ĈW=WW V7zc ^bmRnF:'?A3-T8Y'8G#BoO.s*|1FRJb (+ś݀-l*|]KwUROι*+wֱ*J==dv~~5S+=ZN. xF۹KP?k-CxkQl|";}+E|gT2i.4vXa}$+?hZZh`xF(Fc~Y ڷl&eX\phMYӨ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((n+DMcWy,1G:ھe43@𶥓u5;1@p&%F:5gf_bqZ캋ȇ(((((((((((((?kEw*V u!vgAWuIM&ӄk$ր>m/Xֵ2yݘעWWMw?m!Yg *((+XŮGm\|&z[CP R͐#}Q_,~!|>l}O@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@:Y~*έog![IR7U m?Lln5<)(%ʂH_R'C]m~m|!:g ])#(H8vF5K@Q@Q@ԓ7k<_ܡ_-&5kwڮ&] +OO?. - 'ׂ?J༉/_d4uFAΊ((( =UxV%|'o3K(:(&u_\ƭ:|&")AoOd  x:Qa엪cY-G=(-gO諦|AhUFRI{mjφ[i&?5YuW%8=h(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((#7wp 2:?O]ONJlhO?P__ n ,QR?'j[d#zc8ȹE;ՏMQEQE]\|J#;E-Q I\C xtUQYc,P(aoGYlƾ?cm(^ى1֛&̮Oߙ((((((((((((((({⦗'*v/hF6"D0gNK>.c%~ξ%jQJ\k/ rV˟j?vQEQE|unY,~6|1ڕU>bMJE,Dg?rg\8^onQo>thx"qWa$ޥ>`^xz!QEQEQEQEQEQEQEQEQEQEQEQEQEQE~/' }2+[ JRHQR5Āyz|o5 ۓG 9*>g,S5 ۂȪp{8w5| ›&եp=%M{QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEm=uojsneې~E|#%jdEck5#Nƾ<Ÿ-#D Ҿ(((((((((((((V5wfQzAO[eϗye=c6S΀>;RK]irƊO HۧZj;gӾ3xNiOy(([+i>Ѵm#5hM"`>}qa#3!ǻ}_G!u(iV[0Jpدu!22(((((((((((((((ۆ-߅%߷sdS: Jqako?Z۝F7r/GO|ۻh袊(kD:mW zǖr/u'k/ilF䷹A,[@J/ZZkKܕ7\~k_W}+ffuYme0gЀTQEQEQEQEQEQEQEQEQEQEQEQEQEQEn>hމ_Fׇzj!7,Z -YY1} _,+uu?((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((f5;c<$7s7_px[TMo:F_ZEro_3+ƹ+@SCJWhqm=v V=b((((  (${#yQPUs}h:A9n8  rԓEPEPEPEPEPM:~t(v?k7$gI?~ƕ^Py7r+v ( ( (>t'~+iY`mcCȃ`=0?:޴!]NiVYUwL~kȿm}<#q#%@z?c?F/]K}袊((((((((((((((xVs((<#J/mkOF)1>\}_~՚ToƍZv\E71qQo΀>UR,b#nEPEPEPEPEPEPEPEPEPEPEPEPEP^QRoC_Zܩ?2C  ʳ!'⿱ <[HG{D+?؞g=n[IeeQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQExaI|gK*uw#F?yoOSR(J9X >־{[kiѢ7WV \gKɯ$&efb}$G&:zƗ7k3DbSR=&Dk<}s9~Ț]sk):Fh-?QMxLڸw;F~UTxt۴vV_Sǫ=sDm[wIdNb0+5,=Ҿ |:$>p;^c5iJ4H!bP?|-/ǟr7&*׉nP5Lމim[§5KMxH4Eⴋz8_ E~}? 6nB>H#Ek_TP("/*zV#We~ F_IQ f 'F0l$} _-]Y`ڑ4Wd~Db{eL״e*(85փjS֭}~k8*(Y1Z͹ֵPq{ǣ?>&Fvo7_ = Fm'&(/7=U?ޚ,V?9y}F֕ݡKYXjPޓpN>Pߵ_qhRUUچKWLj_Ռ?-¾?>M=-+j3ß 7;-WCIt1ͲIA;C9澟9-/ӿ&YIG%zk٣Ǘ;K9p 888z}/a$sf# MCec|a]9'I'4EPEPEP G M;Z"$d:*_2>#]73"ū3,ZCW$# k_|Qvm/eyV 2)wj*bI=3GKqX_n&[kލ8h~KmJ[@+:vP煵m-0#4E~t 1^t2*q|U,/DH=A@WlƓ+xn[B£_`eg<[ХPԊw Hg(PF~*gJDrDo_!|'/5b/uHC~ ,Gct#.$$|bΣP{GRĨQtPM% &ղ\I!?Em~>q$z,KV Ԟ;M۴r,jF/4"vc2GW~֚?oS[?ʿn7&Ta>y0`TlAM|>2?+z[}MG?Wȁ[ױ ? | ~D ?޶C+O>VOh,rIW_EqoOCC-!T1c? !Z,h3Vh,2dY6az׸P n27|=w5|;~PW,״Q@u+"W?OUQWkDA_DQ@8K'tA2?uC]YOig =; rb.'Q\)_uSG$FY_z0>lش] s3((kk cu# ">M|.e.I,&6pz oP0 I'+ȼa+B7ĻOuskhy##pPqWIuZwdզD!!`K 'e iZxגIy^6*ꗑug/4t斋qx*k~KX<)~ MC#_ƾE,$@_UR? pAbٿ- $}og=n]]_q?M}h~&ER'=?־O箈?z~˞ %jV~mGɏ@ߴO@25ُ~|e'LqMOMl(xk5dh_ꌭƤYwcWo⩯|LjI(T eg8^G7==@/=S1S[C5~.uj3,lE$Z~|K㧺{ڟ6FD=1vO" ]Z}lkγ X'23ѿ /r_&_VkK$|޼> uma z ǏjL~'0qm+R<h?'0T8QoƉ`%UŰ$=cwuK>Cq9?}\(deaRɤ]L/_\x&^_vE I_P~z~U񋨚Ho-,y;}}@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@6WڠJu:tm`+"ށ$pA=3 |W &.Y\h\*A(r_TFGLuO>hKp0J:=G";*( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (>}VP8 '־㯈?j WN)NHB'((((((((9EPEPEPEPEPEP/Mh1ʒ6+9;W؛@JioyWVxWӑi *U?]oWտ eXZ+ۈ|nPXta~@EPEPEPEPEPEP$9FPQZ(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((k/ .>g1 HVs;k6Cga96Gv8,zu +*Ue/8[X 4cH;e@{ KG"KIG "_3~R:ٝN=TGϧ?LEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPa>+X02i>lW:|uamp+,J?m>~{BF|5vMf5(((((((((((((|w;o?UoOl2c c¨rJi/RK*Xx;|XS#U!,8Z@:`7^A.9<-xzps));5QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQER:+ea"_kwk-g$gpK9n>vm!*wNfB|1;Wc*.}j;;*%OfSهPGJZy|CZΰ>Bbv#>V~QX&z,uҒF TElPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP?4xdo0{'>x@i/Jۊ>;̜=8}P:o~ VT$hIꦀ=((((((((((((()dO:K}Y1 ǽb8?+鯎.O55!1X͗!-S^CL?W񖠮w5#[OM}1$QqT@T AN((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((+~<|67䷁ Mrdm9,;(٫<6gHԤX%fHA?/p:H ;ߵ߷iZ.j* cb^ʀA$ڵ%|OmgN;[Pu><B(((((((((((((((((((?m=_[8J?e /À e?ְ?l5~tbՕn+KJK_Nn.LOd((((((((((((-<+}K\w}+ފ= ~4S^nI2# S[8Te?F58|3]'DmXZnU@,G9?|+i:"k|B\DNPvG((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((Oǟ5ks J(-`'mwjTp%ofFNw W^%s=i'X1~Z1]EݸFӿd=!aiIL 61Vd5I<b vMM_?{kJ}0c|"[& ?0R|$:RoZ׀/ tv,Dzёkb;ἀnn{)5̷,|޼Qt7#^&ԡmQ~5|</XE&#lG'xŘrO^ g }?*+"O]]49ģPҶޕv@g'##WQ| :g(Og/ma +Ŧ#PtW+< |X^C(+vZÏv ceN瞔U]*O ˣwp8TQE=x"m{+CPUX C($q\}aՈ?ɎW1ʶ~0~V>l!LT[ʪWP>dyOEkZ+߆u?#_jKC+YyGd ?@T5:^ͭk{<#i?s[IJk}MH)?:~J~iOkM^g{]*EXg/ɍEԟ^PMx-1ڨ?ęi igφ(S0uOc@St]y2rObίYGoۤoWZ-@ߟs+6f XñF>YSpߵwGt=v6#X~+ҿhZ>sf]Za?:eXY#!c㽐}?JOٳ*B [9LM,|UM#r^'5/mFμh~_ov+x"I G%?'ŚA<(s|{m$pςzp ( ( ( ?|_yCߥosp`H Nwr2J??][M$V2]g,ѳfPC Ԁt.0.-ClV25֌-W+PNj-s&%_xk?={<?O?ލ+?cfqy?d}7X?a?J"\& h mx-իI6Bwm9 o'4eGÿ7c\? M_I-xy?cixo U\}6}E|g_O154fV"߭w ^9o(n1u@EPEP\g/ojwZE,}sqgX2凋1Zr܇9V\7u񼟴ٲ [V[O[-vjzyq=#e7읠ĚEU⍛Q]9|<;N5srJLb</!jG5e[Z?mM|h+ӯ8<Cq$cVIgiwk/emj iu/º _h\jVFJfI_c/xխ&,Vp]=CN$;ĚieҴV֐4$~ mC3Hw!U~I=׀? WDoST.daI/,$gOM}kk{kvna!0?O_K)uy~a j~2R|golkPWo>;mxKhDSߌ} j%[h {(J(4i4OȲDUCw+"įif$1;\ i7\"?w l~"xBS.xfNGn8V| %R{TO(pG5Ze̓\Ʋ"V<'3xcė6˩c;[3܈c,6+:W.?k;-?iګkUu_jz׊'[K.1wl/3o5z?K7Yd3@~֚qyWX~eEBu~^?ZaR֞!]by/`cQ@6_{WOVmj Y~zA?6UT;Cx y 'qd׈U_F6?PUoJ|: fkWLPgX@|B,k?d)qo܏he73ڴq.єKOֳDŽ&M_7I$|۱j5F_;^^$v4a|mnTwUj|q6$mN?_'m='Q]J }mE|'V錙dbG_@nT\(4QyjO|O J( ( ( n_ξݗ.E)KM7PTNPE.)v:r[|h`~ Z( /NK|ByN26RSxGh7OO R`5} iA/ㅃ3\í=.4PW~lmōuYȊԴyl.6X-c$%m&Bߜk!ntvbNsd<%g^N@d, ~Jjx kfɈ3&i< HQܠ5^EYh|T?ݶY܉'^; ܹ6M= AB>(@k|mHGZޗomj6ģjC['@E|ߵn;|6Ω7p06,sc@T_)N _a؇R?j>F ~¶JvOVޫz?@[Q_#i<)fc/]C?@VQ_3H3{iL*ֳ)QuԈuҀ>{K>߰Vnek Ϲ.?8BwV}j(a\u|0kB ( ( ( 񔖚N;n`Z2@, s}3#h#E|k{Ե,nMŕUltj3O[ 4*c.3+]{t((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((i1M}`FڄgԐ/5ƶ</Lemf8N〭cDt6GksfN21繠h )'<8nɺd;(7@Q@Q@Q@Q@Q@Q@Q@Q@Q@.:ύ`V 5h\߻aKF#a+4 ` uQEQEQEQEQE\ O RG@ΪãipH^67=Y PO㊿EC&".*( x6 [>x: D׼UlH%YЅ}iuk4Bc挐 le^6}cK6s[G8Шn9_`,֟FH7F2>WTP?譀|C]\gHO僟ֺ7lygi^29L>:<ѫke_H`uh?d6ӼYq_+ (?(iSD~ E-#8NQ!%\E}E|@Gm/eq;?$_^QnH:M$*h䣦88$ltN_eCA qD#BגWῇs9Mr5@B2DcR>}X=1t?5 j<^SjBM7|e]C Qeo2j^!S.!Uie ַ3B(]1U2?筴?2ƿA*~~:teEZJ?EIgx̩5+yx+Tzd֊VIr1 b>rn98EPEPEx7+@ta\۪^X،)\mN?}?}ME|oMxdJ/گlI8'Ʉh+ġIjgM]wr9ծ`7o&?hFp r^ہ ]OZ|phk^FkEd[Az\ ]i[rdl~-Q@Q@Q@Q@ZmBLvt1PkemQuohuZ((((((((((((((((((((((((((3xJK[W헑4 0#}ڳ0j>ڕ^KX]c@GQ_:WY ( (fK{ygh]MIE|oڧVRxY`'VIf ۧJ,j[gERyY-qU}E|cZޫ'm"qz}]U]f_ ["}EyVޭ*ƚdMV<~?4=j1&izkA&5h((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((*X/絼9CHԌA>3S=Y=\iFt$`MF9ܽ{wkvպ,0[ az#؊3>o"p Ju>c <->+\Z1!Xa0t`:zTU-T=OMgfE=TƮEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP_ |lm1A{!c ٦dĩծdN@hEQEQEQEQEQEQEQEQEQEQEW NQz-A8>dh#h%5WV|GѧxG'Ȳa]!u$g"6z/6&%{oG)l9S?7m{Vm2OGkk A¢Uf ( ( ( ( ( ( ( ( ( (<#wxWqȯsLWRzjʞ`ƱvY0~i55~kf_YIIEL~/#[ח煿Oo`}3)Nгω5<:('kX5 xToG3ɚI'goT?5WIWϴg|JBo"pNޏ'+3i-|b#&>Q~ |joVe)o:H 2(I=WqEQEQE˝3zOn%[ ,2;⾏x?@b[t[n1_o~wo@%ȱ_Dqdg8c85G0%dk_m[XYAicV (bP8 >lk xbVOob^Qfׯ>:xr#UrZ{7ε,i_:3mXi}um$rc*+h.P1J~Y\.gD2Y9qU3ͣs*#\+u_^ e[ hM'ސZ@^| k/=.Bۿ/kM3TXDeDepzr_4{-B"UMzQEQEh>KjpXć搎Tr ߯j)fSF9<:WD ̀?* $-:Bxe֩q^X!|(GWiv5?jϏ%ϰ`rvΤٟEMkH|9 8@k>_ @sgJ1|Rd~k@=o-? Vͯ\#~ ѧ"oOOiaaQNJĚ7E}giw}RW _.?dñesO V,(mTQ ph:+[g;XX|VYD؅?fUWhQjq$*DUH&/QEQEQEq}>jƑmީX%IH ;/1]ZWmxnOFPҀj)GȤqYc }>8Fw=}E|"4/H>6ʜ٘xe-?\}E|D%,dUxkz/4=.u#Ki1Xᇁ ,"Ҁ9!U@gkMdk=pkk;Czml[qIhEPEP\w&ԇ. ڄrGۂd` q@xX{خf7S 0Tcwko7HkI$*ߞ(e7W8/b5~Ͽ9]qpg'X<}qw&К3 ̑6*߳oÙ a}Ke<P<9i떧&g /Ot|{5cehs\y4F#xvk~|:o56[淧8w;AzQEP@ 2j(F_K|%I4Yc;䓑ZxSötK8Erzjo<)I#uF WOX\irlA_ҽ~Oux#~ke v73.!{~>}E|7q/V!>Я iWUqؠ#j?+&5FV>e$: Ϥe 04;V/hWnǝo(@=XaX}5~)56;w`85CUuiڗ4[K(ˑ3Zh6 eXZY,!F=NzEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP^3H|)~}WL#ӢcEc(򾄟S^E|Y.|O>{.#ijN3AB_i_$ Vg񯇭6걡$'h6;&oxSnXM'u(OBԏR FQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEW!dL.b98?]|_bךdTtRI(((((((((((~"x/x#Yeglm'DLco KkUY-瓒~ϹO-fԳOpnDAT^±/#{hJctKQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEռ7VAsK UC[oû4 O:irO\L7ČQ~?b^4YXGtlU=epǭ[N >>]{J*1>B~FH#؊} XnR7"DAAEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP_~زtdĸk>N-m0ķP 318 wlZ.!u`6<}##R t9<(TU(((((((((*ڝ7i5wL29*׬<1gW ǩzHw$W5s$ռ߶$hHM }>o#R!g#.=y|tkn+G{I-LꅄLX1|wmC+)ހEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPx_k'ֹb%9gX@>?ge#/Pl#`<0uC)/19[ {xfi5Mդ+(w(312^FSKLȇrAuwN5ϳ.ap,Py.r2=_º{xK{N mnO jUP'=A'J?N==ܴZ֙F7Ր?ޠ(((((((((((((/Ѫ3g>~ѝy y'f[Ƅy t\{οҽW'6[SopYT!X€=M_1Ԓyɥ[V1]Ux셮ͬ|&,-/jm(((((((((mB #0ݘneyǚ"O${coxQխm;ٻ[He Q}ԟή>346VM;z4}q?f (z3*1}@C~V&ך@YeΙʡ}@XQQ$qT@TtSfx+`ר|!OM3^,Z(ϚgoؑzƁ_:熵[[n,RXFGb7`v>D$& eaЃӫf|&Zg, =Ias*(((((((((((((ݱXc@n=}15чoNG8@ N̑.k/FīK1ITeƞ7m:%S"2#]S b?3d >zޙifd""i#+tc*H#u+m3O;{KxYe($V}ڷ՜d w3cR&ER0G1߀ ⧉~ H,7FaU6U7R< }^M6<>*Im%Fh?zWEPEPEPEPEPEPEPEg0={TY"ݡld6+#CӮ%&UHNUT`ھC<2o|Ex9qXL> h$I &o[<;ok% .Z67| +V(|G,7 anIh/Ŀ\u?sE@wQ_7K&|=Mc㮨|>2Qm,#?}E|&Sc:`>ЯˁK]yǣ꫏Z|!2~m+~mO5ƾ)vw%˸>_j17\3K8>P/}hud4u+WtȾ*F6i2Y5gʤur\CjG$Vӭ} kJ:=wH=VϢ!#M?Kh_ٻ1@pl{iYn=ˮi1KXk־%WCN[ũWeG1C6[wxQ4|p?f??5*~=a"@__ Co.ױ?gp'hE=_'/x{E5eĚ)]7@fxF@ڶ2;a00?fges⢏+VfJ0{-}AƖ_?x@v_w|*?Տ>碾>K5'?Ro|u[&&]~|-º>x+) 8ȯ|sſ u֒+R;fXFà`<>o|v#[K Ao/,Osn)Yt }(m]WL&t-aɑ6Z>joŭxڰ  RpG}E|@TsjWH^#34~Y5? B"&9?jo1Tgm_=8X+n jhOf#=]/ÝJ=x $'Q[w"Go s_F ?Bh>ϩ˟\##WUcZ( ( ( KK`uh.s$|>m~~WB23GAjp=4w<3Itϝ(Ԧbzv@^aD|tq#S.L7$g*N8kBK7t JX죴*ȪpͿYk<\  G9ȿUON`I-d]L%XaIJ/])霑c(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((*=Hc&p *rx(? ~?>;j_X\n60VRv)>u{5QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE~~wqěDm j=?kT/uXu T@q.O@ExAAę]!E>:I6-1xg_do w۬ko\ufcA;GN8AO?=먠r2X_BZ+akMt])~b>?hQ@cӬ91(aQbEQEQEQEQEQEQEQEQEQEQEQEQEQE5aXzswG7:m@_1W~Uppq^+x '>H?Ί%AR xKI;djëkbl~TP]~H㲿vGxn~OKGV/ 1_C@(dC5%\Xqmh3D>F?qڴPr~!S.l~|pbWbP䢀>!2O6>Tt U-a#*8%5|t]VgnMT#ۅZCZ Ogٖ  TvL12",hNM$`m韵U-?@k]IFv5Bn~ :IK{XPڿ9u^ Ǟ[6ya|E;T Tt$~aW7>Mh!ܼ_Qg?F_ը_z9.PZExc<O ־!5oِ7fNF-B?oe\cɹ-q{o|ۈ-"\fJy?Eֹ'g1Qь{׻XOϧCy@\@z(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@U ${uUZ++پԱ^߷.2m 9?Glp $=AIg]Kcϭ{_Xn(Od|il8Eko VѬPD`*K@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@~֩Nmn _h|?'|on^WnH>S_W? #&2I=IEvTQEQEQEQEQEQEQEQEQEQEQEW%3?ÿ9$a `v)) |uB:'y 'GBEQEQ\m@W UrK=dʸ_hܛPQ\?,xėPi$opɺc8h ^W޷$Ar7 ՉP7B,,!!Av-N&ԥuE p+迅~8=j˹ݹ}?+㞮ެ$Fp" A9qj/ĽFDVN}Y Csd DƯ}!'oNx[D]2>}}gu&# g+jD>/h;յ-FͼW<rzįS45w,6L_Ҁ?Io]eM[NKg8 }Z^$G u۟XG;K>䌆ʟcPy?#ρfHqme2r 3>7<-G<֗ nqe:r5mc|X{F>s~=y^^֡;4-.frrpx]*|^ŸXR;'b=<»סTm,ׯh}i_.z%+\mr 4k!#84jza5s *^Ipw$3gc> QC;Q|R,Z8+ \N#=qԚ$ +r&G5ڇjwO}@x'Ҡ41OH'{pq&jy Z&ܑۅZ|o76<0!>SFV*˞e#=뢬h^t./&%$8P;hPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP?݁2q>`ok?f+Ǽ!ᶕS1П(9ՖT!Hm>H=Ѡu((((((((((>,Uɚ8v?fA'Cl4hܒw| CWe ݟ3m#"d^W?A^UM+ƺܒ-.2c?+WH+iO<gƀ>좊#m-;.Q;i|?O0~9>9bנ7r%@nzljuIp^U?^xu%V;PV"pH&R18]Ǟ߄z7SvH'b B2:{mMWWռ;5վ=ұc'FG^'j4㷶ew9\y$p: ë?M2 g#͓P~=IQ݂N?7'< eLbZRaiaؖ@}A"=FtmnHԭ/E LrqҴo 5FPWJXKy6rO\WW$c&(((((((((((((( \7XuJ۾:x` p:Dy OB- ]\A~ 5J(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((7~ -+} 2[1%O2:V?|)kgs;\#!F}eJ{X|dI $ 2S Ϸ;OMi$U"XǙk{s sd_'f]=}; ,Z7z8=x f߉? k֗IfϚ1a؎A`}/\vqul`?)$quh{KxcUEv[2Mu4v2xK_c-G~ rxƻy댆k>/x1%|-8VhyW>\ W=FlbCῈ|uGĶa%hX' C@/ {Kȶ7*zp@#_?>xިm`֮طa׃NGRTYV>)G5-l]*UUOߎx*_sx7DŽmbK*%۳1AZςyn<#<řΟXI'o&ekiDCAA61>;|NZizDGHՉ##{gO >:ovФqdX PU+IJ0@e/W^r:b#܇P>fKJ+}?,$-Ą^4\t+9NdHmpC@ Ek=v찼"9@7oxo}.p@!kω_xن?/MxG{^,8Og14QEQEQEQEQEQEQEQEQEQEk;6V6ȍ}Y >zg\xд <5Is< ĄRamCv-i%㺎cq!-9FicEo6[5Һp =|A4q,ͣHnrc:gk'ܖjdBYN? 㷉L!JK2ƪN;}?@Q:H(0>ӫeeǀ+MS6z6+}'" Rۛg{׫h²ڥl2ucM~|vHT ?zKj[v֋ Dl 1`{Y|kզcF1"Xc's2(b@Xy#Ҕw+v~;|j`Oǎ:Kס}ia|?a@/-ycI5EW_2?[ƾ?m]-1[W]"T~"5c$[ηY(? "֖MĚR  B?(~uQEQEQEQEQEQEQEQEQEQEQEQEQEQE_aadٛ*?|)]Dff%n?4S~ BWW\A~ 5J((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((j6Nuj6v4TdExu; x+`$V }y@Y[<oC"phZF'ŏ~(tQ^u|kyn?Z7[G?ێچ>qBb3Fk*Rbm@&?(}GE|/mtOLT/ZjEwK5oSx">4'TY#pUCW94]gT&dkV™J k}F"3T$x͕Pz-J?~k'wf?Z~vCUYl,NpG7s`t+?y>CpXbA<>CzP`08Wv$ #]-U"|r!X4$:2M0z챕O"iZ yf98r@+PW'nǢ6tϖS~V?%s*d̲"V8g>'H7F3.x??獯>}OZ38SO/RI%!\<θY^}!s.kRP+śhؖIU}OukyT4Gu2\h}2w]% #Е| ͧ;g^/VEִ I&CFh5q5m>A(H e"N 爴G$!<i=|3Piq '/3Ǐ|dw?/t=Tr" i,ތ(F>FDGaBX e_<86OBd<lX.I$@1@mٯAli1?Wd~Q?gߊqΧp?J^Qz ZvV &[n">Ѣou(ŀMM?)8ֿY'[q\\N}IBU?{EO/i֌|?}E|]~WAV wI芪?jO O@m_[>!R>*A˒Dd֌_|  3@YQ_-Z~1?l|=b8krheh+jo0I5e?BJ]q g"HF(ը ?,:в m S}[Ec|>ٮMK&?)΀.QTƩ֧2Xxe]K4%B()qϩ(ϿhOd\f_Z_C0etxewr%ra de'7^m?᷅lhn ҭc6"Poۂ ^j'x2ooκؖu>נ7&o1 ;R^QnE$\r24h?Ui1xRDҮD1E3pHmazQEQE6u*C`s[P;+N .l-& !V*0a!#ATP)PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP2`b"4ϮO+ci351+>O g]֑Erw pX7VψR~I:Z_yX2s {WQ@!}2|?k XjkGm ]s]Oi' uE]웡l/9o?ĂF bVPO+_Ƒ\bTЦ$iC}E|iw3A8ˮiSa]̛4Sg$ZZ$`GДm3ĖF>(ӄm{q~ͿS]障) w<W۔P~񏅼!=X$;YZ^w)#3aR<_-MoZĈ5C=R b$饽᫁,tqo4EPEPEPEPEPEPEPEPX4> kZ;[d-68EV=wӵ|?wm̺Q?qp?zs@Ɵ xgDosse*;csqieτskzZ=*@pȘL>Au#ۜ_5_[j$];Ùݵ܁ #':ܱ8-mX5ڨ0`&(K R2ƎPK(8>((Ef_WܖͼFPD{sO*T::REPEPEPEPEPEPEPEPEPEPEPEPEPEP_ t]1M?a4߆-Z饉2c94TQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE6DFq_2|35|^$ЬL4A\`[};EW|?>vH]qOc q~}KE|<n?k9[STU5&G3ƨ ?N'So.9dyTu5Ŀ&' -OT4e;<[ lб@x8]PKGf,PQ^}Ər <N~m.38 Zxc3e@mȷĿ/_wa?5W⿀b\B?ٺVF;j+?4Q_2,@i1@EpGяt•0|>Unis(fv:Nu:?+D~4:f &¶}e! |cv+_ yUQb`a w®O5)mm GQIEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEG< 䯉uhfͱA!x<먪H4k4 ~AAW(߁|=>;?it1h]xe =ȥ<=_l'>{> ?Iauh~>7o'L[//{2-?Oݵe/ kz> tKX[׉x}E|߲|D5VMxRH+Z(A%UzOTd;˿_.>d_/KU֊?j7D*mK _J@6'hc6yQUe w\cɈ솾>{ h2j z5@~ (‡-z}1UTof0mЊg?FoUz 1~nAAo?(.7?s+)C=|2qz(?ើиs(zeB}W< m?r=1lP=1l} gz? ]6qʟK׭@3'镂ߡ= ?g. ՂϮFOBI憽ڊe?X UNO?Ä/:ے3WP&i8;|S|lڡ٪#2u顿>Q"aSH?1Oկ(n׌!c֞G5R_3W~(clהPOqψC~/}E|왫3(}G$\ħ8+ (/dy?sO;Hdy{x?X}]E| 7q_dy1#g>R,?e.4GH#-d߳//]eВVS@0}yqVZz;OFtR _xQ@hpmsEij[jR#u1ڿPjΙcs{ ͝PE+ =f֥ME;),4I"f0#b~Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@S"H9Gh ھMTk,͟W4QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEU kZ+3wj6z} nX'Y_<_ao _뺙vD+A$}:+~4|oh-Y5 Ђ/$z2r@$\O*0ԼGP1y9|_5|Tׯ|KO ώڏLJ~Zln/eP>e_n p2|t n8AaSgfľ<`ZR/U/{s:_,=w֖:m{{I1"Z`ɐ9kzEP@ h ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( t^%4ۙ߮/S2u | o#%tyHIhZ9MT?%}@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@W~Ӿ0kyE^Psb d#_'\xKԴ]Ƶ`$rTil@#8erkݨ(((_Wx('e  '`܂+AsYYw)Y9u,Dg85:妥\uK{n#RK>rKg=s1Thz H1R>{'&,үf3XTd`l'5zKtBKC'Wj? iga;>4u%fx)qXӂ;4e]]+TnKV3JÜv|._͍[ۆ4=p8PwPU5mJGӧ.wK4PzPx]Ӽ7jZ!y$oarkNɥ<ఏʮ?y;g@H'cw.Cz/!Ǚ/Y[zQ^ԐFظヂO=_ tbIisK[<'*d;f. ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( =H"9'uֻ_Bo'{ФciW"c'BzgOb>7ƁI[Mv܉ɷX  ʾ0GsCpK,1)m {pqf?@h7Di~)HhK֤!#qo;cIx{^QEC5ռ%|_k)f5ds:IgޛƎA:,y "?';]ཱI6HrHnOz_ꚏ}6-pW9 ~h| y\kC4nMS7.B?4v pxxź79uo]:z*^[ AzWO7w5%ޣi mNԝv=kO OIF`U}ѐ8yӾ o { _,$3?a R{8ھFw=%ݸc ~T^8z${k$O(8Q9:v;G}at2򬤌X 4X}DZ$3-nq"0eГ}ߚ湦hZl)yF~{h [-"=[:cdUfYXdf08`tзzKڌsq\QfPƟ7UtԹ#q+aD?q '}aw|<=,l|ɇI&`7c`(_rFX]x˓շD`A=khoKuk+ogՏeQݏ@)|]]/Zγ {p762I':M|W=o_'(4 6"VV+s]['QiXûL.!n ]/ _x@4X{KT+EE|Z'((((((((((((((((((((([EƉx{qpM.8>Pn822kinBCiW\4Py6Uf{d ~5_?xƗ⟳M;F~`90F__Ꚗkh#]ҬRMfA9}3mGoEI',Yd#:@H5GWGou 7Lp,* >ii5YW'$z@e^Ӽ1]B9@rzN.a WFe־ߋMǶVo*Xy/D ґpy=3_xMѢhl#."@0d8^-izlhsu<4>#PG/p'Q޻((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((+lkBȤƥQق%5U|0#?;)]l +cKq[YI?F-{QEQEQEQEQEQEQEQT5MgLҡyu=B4fe@?3@68:4&'G!~^}vq[/>W͏YhA je}Lъz78ơ ~ֺ~we{zjA'4mSX-ΗLiߴ/kҊ; Ť*?:+3c^x(QTrqH't>다kbƲܸU9Oڼ])3k.G<6pFqhǼq6⏉ma}tXZF9ڋ. >/MacfD Hȯ I<[RkC[Zk;q־AiuMb-lmtA$CxWB5˥]ԞrOw5O/x㟌t?[Om<0J>toQ8sO>=4?:6y|9-$pN}`{aoᖙëmnϫN^^o ? dZ7W0[\gH(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((vO[ z)+?o1QAEgF cWWʑ<tMW| G׮PEPEPEPEPE2yD%gv &['/ oHoYKʾBznӜqlk^4+fֵ;+W{03}|K> _}AZt,=;LÎY zT< F̒jRNu'"=şǂgxt֧V*Mlۨד5(]7Nwpj|1)h$&w~Cgʵ@zKb>𿄭Bl)S39|iӉ|Uq?)MT2Vg4'/'?W4Pʺo앉s1c`+}?e lǔ38D9Kݑ ÀyoOcBkLʫWy?e~oT4J|C ~O|Y#sЂ(>)jJ(&=Dq۲T+LtV'/o!u [ʲ)]Ϡ1WR8י>#7fcloOL$x hJ+_k[mj7ӿV!_Z R|;<?}$>͉k\!mȔ2pw2@ EPEPEPEPEPEGO7(5[ \dxp:V_#K&#*Hz+'^"<+]j[Xۮf}OǞ0|ۍ_\XOwFX~(|Yǩi[ʶj֤0~y8뎼 o㟎t&tY)fX83K3-?UK5ҜKjZ0 cC&FyhYu χ_{=AuYT+-Xyqӌ(澆Ҵ";=*10th:Nj&e:% iQEQEQEQEQEQEQEQEQEQETt JQIY"?Cr6diR3 ? WQ@xUlm$.0Ha1kŗφtX}q#U {O./Z@ڻA ^2pw=W 'k 5E}]zQ_ |)?2:RjLRmh. zc#!=7žִYwHyFEPE" De#;s=q@(_7n}_ZQua\?d@z_Aiq?ΰŕH2w=A#=şľIܟ,ރWW,%UWh'& 3SՏg-¶Np,+Iݏ|s2p0*Oxz=/E'OpyY+(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((H\o‰[6OxJ4{}O@F|] ?+<1!-uQEQEW_?&֭e$&%Fc1lH8=ߖ3\ƾ!.Z%fdGp?i|t㯈wzΉbm-43(W+ sAGJ.50juOvwZ>˗.^;q`wZ |:| ҙtM8}H8L|s}E6hX4nBCN zΧiiw:,%30iuIٱtS߂txn C4d܎ IG74ͩpŻs4Wd?齏iE.hJ+m s-UvݶI)~jؠhůvi3ܑ55i|p[.>Oy>Ul~XX0m(k#՜g4Hx؜'OLPtW $ЏGs #T.=Fvz 4|jdM2jϏweړPh- =sMm S/P}Ime#q7V<`ߕ}50 4q\;:}*9D?+_-~:/m2Lgi]Ɖ&i:bųPjYaΓuff`׽gz}RI˪Nf175V6 5 |U]L^xgF'}ƣ?.?*OSЕz?eo Y$umGRsG"eھ8 +W94]+'2LIڊ(X|[O_̟<\H#ϱjCX_ڬ grx[~5rXZIq?x8f|'Ꮕmn1\C[zEyWⱤAVWy||mVl'5)̗ >]`Ϡ+|E lKmN@ 3~\%q*P+o"#pUB=s21(((((((((((((((;hL+~Ww6gX#y?C,% EE+6R3+\_ZWğ3U[yb6,GC)@{㷅 Nob,lCgy 0A5~xkC-(昀I;2N4f5?K,#Hy{)61 }%iZF k+8b] EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQElJ}Dg?)n7Fj;Uo_JuǶ{ՈFu۞ޏDŽtn=mr~~6,4*`>m@/Yh Kix䯰 hZl [ă "~#肀> u1kil`ZK/`e=xX(aT{ S!U6jևs/5;Eޏ]`sd+hP~"΋F1Z/|;;OVP@x^a<.&~HU&>~]c/93cG6?օhM][e)򦢻NEe[Ex٬Y?eO1&=WKg!PoaɎb?v?fl'tۓo?e ]u袹.= fK~$7i@?a_իaPMtwAw7?[#Jpώs/>яPf;vʧՅeq`S̿c8H}EQ7m:͆"mnҀ>o7?f֡cū?Q-jYwӓd`sG)}E|^j BzZG'|^7j6|N=z@pQ_O㥤d\[jσhJ1b?h/z;X2(5ƚ내::t7_;Z(ޙ,|"9@`_+i_,V[}(uoXx|jx@H^cQxqkڸFЫoK|;}c=hhn@:XD_J[?o/BwtW7m[x@GɫZWnug::4z {((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((m'ᶌ݆0-x?i-kYc]Vd5dٖ_R/X.|oWׅ 79Q[& NhC6<+((;L.8[||5$a֑IZ\)Ǩh((((}oiW64UFI'+sWou oPv>k21 ؃M}'b@Ovs$S 1>` v^x<:|)[/P$fٝ1^boNhb}mHvƧ$x(5u|ܪj-,IziЅ}k@2m7Zgoi,ӡhX 5j,%ʹ?R+٪孾6xd$Hc~QEQER1ڤf2= ~lwrfu{; O@;*_?|#goh:{;nyolN+YKefkBɌLO9kN ( ( ( ( 8"0K{x=cP߱ƙN Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@|nGW1jUSdg ~U}|kڼ{RRQUE''ETaJ[kP=?*Br%SD>!Ġ&((((((((((((((x"pCČRQ@ҴF%S+^$_x_CpK{k8~xPCx?B/}7wW?X4PI;|4bv2{-~oY/Qjv+x5{mρHŽn0Yn#m1,VLoUSW4P_^7_YD kYhع8(> jbXcּ?dh wO|Ƹ~^qܢ>)_&č}%6Y~yIWWP_>>u[[YUDRn7L_T]]ep=%_ᗁ']>%K@9mXx|OUn["m?jf??)3W]|ytzjyGX<2oOokL|=`Oۮ>dtV~  GTofWD677HV~ '"*Eź7D[5WvۿIG|HSP%#l)" @[vht?5iX7 |iq.q&ya|p _t679k%(:+∼%Bi :OVt9+2H?<'M~;#oր>Ϣ'_$ soU@U!Ɨ(>0kIXhU\C\\7pEk3@q3@TWťKiua{~%di@|Q_ x5qU`2fxOүxmY}E|&|;eۿƻqK~+F}_ƒ^ŶtW~*x,)fkfYA#0lgDPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP^GV[uU,bNp<柴cOFOq?I?af_}_>~iŎWv=a+:((ea؁$A Ϯ-}_[pdOx(((++ |/]XZ$<u$5#Q2 ,ԓ_ ?|k\hZ5 K1.cAӿ<`;B>:l{y -̅LI:~)xH!keC K vc_l7뷓ܹOqE?(8=٫ӵ'EC߻ P_#˪V_㶚VC}c4$u5~dxķ~fisc/8 )#f?jo\iz5gchʀ> AkSxc㞍e[3t,!k޾|aZ~}&Mڥj,v4HdVxkiee{k =̢%bczW~՟ӾZi k  w(Zi^jgwend)䐆pb=~{R$$:n+)`zk.~ h/{#Hc󡍛P^_ֵ&x(o8QfE bsD?~LOi?f_|[hbY V#"(F xڧ_`6%>еZG$: 1ϋ<%#fQlJ۱`:I=:f?txJ,ɸi<rN̰'fFQ[Z)r -Q@Q@Q@~εI$V`sL5_ "48do]=TeQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@55v;mE6O&GXt ʤ)y< uZo}C,s+@QEߵ&_WN:Ey^^CW ֏#%p?!nI܏fWA5| {d/?k((((((($#hwڍAZmc\CC(%d]Söރ3Mc9`У="7(((((((((((c5s +Ypԓ}[N֬GnkiVD?(Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@B--Ao[:(((@@֖AZ:j VA'׊gGK7M Ӭ&/D+j"B[߸ʹ$@[QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEW~IkŊ{Y?ҽߌЉm*Joؖf"=HEFvr5O3_ZPEPM'#SN>)yt?Ҿ⯈ctpaLW۴QEQER;*!g`I'Rhtl/ 6d"GP>?GIK-׆!r#1]'g$SGMNO|cQ=qHc{D X08kW{^$I~5@q@2\}+@&?e7}>l|8?Og%06[O\*OAyxt\t)]\dzု<>BЬZ0pN]ޝv_:^D-ٶ20cA_vQ<]SHQ[@UB({Wǿ朐ڊcGj⿆n|AO8@5W_g_`MW*(<UQǡ8?>}&[K= *!#ONyaUS2 FS"K>0}/^QCF)Q@ћ^%4tկgH*n8Ϲ~,MZhd[TY؜Vf$M|ُzUZX5 42,7dCaN9A\o:m}1v7jȟMρ@fk" XEVaN=REQEQE2A#V4˯7w.)H?}YV|ʴ&/Z0RVOL_>ᢊ(((GuE}I-kN}Z@*H_~ք4Q^7o6fO ?0+&ko;|Bd*pZ;9OЄV s!Ͽfj)u qb=?jݏZmW<H?}E| [x)վ5诞y6>?TWG~xѼAY|Gh*+Vx;oC~5g?g;"0 jm*}jOjA^$x?}E|jg#?;G5g诟[^#DڴVx3"0 j)m_?X5վ'u=)[-h=vC(z+9j ƁlĿɍ,A֕oPtW VG6B8~վk?r>}_ڳy 'j?$ ODRJ+mi9>nKi5Տͬ'3E4W /n_9HOl?罼͐ +_xIfK c<_OK@ZIUܞ]iv:݇\ҧ@Vn/\^ڕ.` U{oKj6p|?xo]jW.ټHT0Al^'M/MB>tǶ o᠗Ś:ʤx#zWO~E^A{e.vM V_n͖Kt/֚žS Lջ׹xΣe~/}"I-4 s;rwkܵ I4=B9aY>#^ ^ѵIf}i[)}; K,[ot˨n&P Wz~b]΋ q'J!| (θ^C(o2A8ZV~xk]vLwU7RT~Dr_Urzl&kZ3dwd,?FR?BQEQEQEQEQEQE|;bXEixO{ZOi G~z>C In@%n1xm`*-|?5yo|J (L":.ϡ.3e|SqO2[h((((((((((((((((( .LK $h 2I'l,*88ր;z( ( *{]_]{hi[TO+>Zwq0?η+?IxSAխe?!lxĉ1}$7_!O 2?GW4QEQEw쥱>;j K_U|9$)[;$w_(((((擧^Z^XZOyhK[,* <FWQ@04Q@4L Ҵ;#rl62}_((+ x/irmRr&zA:pj( ZoK]#CKM>vd㜒II$I5EQEQEQE|bOakoz~⫼ Em_9+?hQ4^%L쫏Y'&,4^K- c }C^;Wt}]@25k6=+kx;i =kORR 'x~ZF)g sW^4cZ0Xʾ~8|~nQ|cYl|+A* ߞANc-lSޟO&HfY-߯m_1E((p@SsHSKW ]Vܡ-e_<'|?y30m%M?vi7a_\Q@)$>3xv琚o$}|׶tE>&dp"<ҴP϶_ljzϳ:(V_WPPr:v?{M)96Q\75/3_OuP~C.<|3Eu4P<|3y>Q 9^Ey73?? ^h'ye_?5gdqJZ<gO zE9:o&xf ?/O׭@L߳2OwfF7]z<|3=Iq+.e +5‡0U3&h??ƾ>_2-x|xƆg6223kuEw[F~p[x=~~xuMz]0-r߭j |O"0_ր}_ 1y oe;~Z ( ( ( ( ( (>L&^oX[_sᶏ?4dneľ n^?aQd}}YEP~c6ӂ3>^^ojϣxIh."@Ճ&3\_?~Ì|^81Z2zo [Wiu㹖bv 9{t<7?MMwg Z_KG`2Æ6Cu~'J׬at=OUa؎E|q]~%zėZ-Eq&1nA\7 uqodr0pFk|qIWZޗiejVJ%X.98#&=ʊf&Eǃ"&ML1@b;|IL<~c@(nr1@EPEPEPEPEPw4{_h E90oξseܾYkB>U1GO0!+W&YghR+^0Sp66dt>U"0ÍXi[*\?~,€ (<$|:o۩SM#$R%`1'En+Wߴ@ͷȴk}_7S^HbeUu9 Pm؜I89 ((xKqk׉ganyXI$0yC~EȈM'h;GҀ=4ohv+6;KAF;XUGֶhjZl I4dP.oK"ԋ?Zh:⇁xEfր4 YCllLloC Z( ( ( ( ( (1(((((((((((((((((i>6 }Os+eh| r"A+n;6#zvп*/Ra'?<PEQE|Pᯊ̞m*8nc+/ |RC''p\Z "2b;< 'HYpzżQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@WPɧ"D?j|[cU+"ӣCG+}ŷ9BP!?Ҿʠ((fe'Km¾ݯfxH$<Ͼ R ( ((4|Iko.ʴݷ{w$'?A޾8"Ip1ieEgCrXMǺ֫y%Ȇh-`Yj(_m| qgbmj[.ǐPp2M|i)b5]F_Y!e3hxE#Epn>e1énq`|r{}r@#_o~6x@6Io) {7~4!E60V5uQEQEQEQEQET7g;!!lA+cD牴og]i$\UԜ9<~X+#ozm.te>nPEPEPEP~w&`V{k ,[A 8򢹿ωkW.omRYtG${WOxHB|IZi`Lk33d~tsH.}7YpLr8b*?WЯؐ% ֕yg?^}{oJyF[tc=?i-VT]-%q?~WTe7ٸݹ噙Ϧ~?6^ Ě]NF틩;Xʽѷj((kž @|EAm).dtE^GeD|7홡.Ci*$㓷xU,;II0eu# :(z( ( ( ( ( ( !Lm)M>vj+ǟT0n[P߱?o>>?勞kJ: +*((((((މυ30U1[%2:*#Vƺf0~hcU8d ئH#k@&KiY%}E=jÚ +`;〿*|;.W\k\*Dsrgxwukū[}2X܋{XXҿ8%=~Wm|'=/B0,AYىcMmU-[IӵuV\HIJ(`rGyoo[ڤ7ך >X`T2kl?-uvi!u8"l*9 :? ~ ˫i,zN1 TC1bA:}wiV$QxCDeAeIY'H[}Y±/ _GkXGmWDS\g״PY# zpdB rI*G-|5[^ 81;c -~:Ƴz~}'KBA*|yb9\f|^<1^QJ2zzw{![Ӽ7m'Wִ+ IL[q3A! <W+Ye.ϓ*p3pqHCᏋT$y.!iԳFz?<>I[jTer3q澚ԵOPaE,$n9__4o>?mλck!˖<^NB0:PMҼ#:e꺞Yf芙$s|=]x:[LMs03Oƀ?ChC&{F$LhKȥ[IK[~Nj -#8 phm?tG26T5&5.yAAkoڻzRUVOKk6ITUd>x _Sᯇt&}FksG8O,<|&Nmu ο4kD5Xdg8}ע|{@#7p'1t& qϗ4o>!x%n$c =ɲXXáG+Z$[_ i6]od6`͑!AV|9n4oVKwuv[30@hRKL@P)YH#5])?LK mY'ܐ_ 5Hkz[Sb Bg : ۟z³ i7Y-"c:}{/>q}b`Y6 ;$VoW({om5]dD`q\g b|z@߈?߫u;K"@ Ӑk_vh? 5O J-B1G<w ]go;wW@Ol=U=yee!1f҆e0*Iuu`|3+l%.uɉ$U'g߭`_\me$Q!wci7>*UO2ydyS"y6bPƿAo{4]'O@$< 3xԧYR՟%# X: .5h^:Ibյ ƢW-|}ed،wha\^${i-qdؚSgVᴘ#7sS9bIZv+t^;4Ǟ85 "bl fڤ{t7Sa&)7I9;xюp<]-bX,&sp0y#hohEiW.#7IX\6 ^_~xt[{屸xJUҴ˽BA/<K1 TWi/krAVm KRBa%31.F>UG59_Z}qyb+gS[=8( *Zv5,pm M4Q^[ߎ$еKw Hm *' zR,dpHAhQ\>0—X[ᡂ7~m:Ey߇~4?ӼGn.R9 O`]@'kՃ(e EQEx/ĿOCqh\嬆+C(mX1PQ_1{m|5%Ko<O) u89} '{v6{ymlpGZ'HkF}?6b.>69akH#U}n?z]%exz\~+_2~^#YD6-Ch# CB؅>wmnXx+5+M?R.ncW,b]Ԩ////%in$i'Im*Z&[ۇ Q.KP8 A=ľ+ *\Iu ;K{ XbO'kSZ%RFڷ1Ue@FW,hߵ (-;W޶[qm"KCЃ_)υ>?뚸n~ů\yJ<8 ~w3%IFd̶_/<%uެ٩D  ǯ4it/6%ؒ}k[N4hƥKݬP ~}kxr1}+vA泙ZHUE}1 ][ϧg֢ 4eġw3ݵ8q#Pe:k}jIf*5fQQBQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEoˁmZj0'58#oү~/;8((⟁~ӺhV#W 17_;؛ ( 6OjvEqqznHه+zeoׄ5yI3Fw(;O6ы3$"L0$_ |ke4m%- E~kWei?t0z RļuŽx<|g< ϧi3yIѣH>1B?Nkd\^1Ē[ʤ@{©xhfV,r%H%+ Otm&%0voRNI=ɠ +7|z hvL2P~\\Grv٠2Iq[rY|4tW͐~:gަc$Ici(pkѼ7!4˻mJd,yeo`{( (eƒj7֑lVU3zOZ0xεsb(wFBp_np=k߄?VfޱLU|e2dz(e t"G=a \Sn 2>F24KQoml,8upqЎhWGe )G(A0#85ï6"K3g pȩ~"7珴-W_Dgc>!~>-a:g:͝yC*Ia@p߷<'b_`QKzK[i:-<c'$ƽ[_>Ŭxnx.1IBߜS@ N_z2pYHr X~5i^7jBeXd3_~̟lK-dҵc<-wmb=ۃڻo/:nY.njf?Ll[(4{IRky؎+/W7iVS=f$&7_ 1\ CY11#z?7xኬZwz?OXaTpsP|񵿎隂J,cx W?SCTMKڅŔuÞH6O`[)1Pu F|Mҍ:3˒y}ѿkr-Zޝu&g6KW}-+ĺ\Eپ]'r;[k'ۯ jڼv\?wG/#p}:W-Sx&QЬ/̍MbYT'ry\㫭6 /쫘'KtrFGUh4SG谫%Mn}O@G"_>'$l5;(+#8'>팜 |mIrnJ#!p9ڣ*09'$e?_[\Xs sjl$ Nў2N%|8MͼiڄK B!W9c ~x_%ƫ:}@@眜Eږ0#*$7ZekiM`pGhP`ھt GM[VkBI>X]1Zg.|'{}/ƶzZi.RE'xؓIin;޿c7_Y=ԶG&݃8#i:m]ǫKO5wFVۑ֮\4V-kU2]1'8',&M F)N69͞5סF.^CB#}šж|K^\Ȇ AtUo ЮulK$3Fs^+տe?ω/𗈤co\) rO )s@⅟}ZM6حEibNd)ܑWW2\O-ćs+f>M{g)Xƽxܭ<|Ff6}+ x_پ3CZʱ$Q `hQGTE,p }} xWĺi/B ;)+)V}loFuϮC<#=YI?fX{e]UQEQEQEQEQEQEC#!끏_ݵtG]z&nzKo"~jE|Q_* |Wܵ/wo`b."'=?r?UQEQEQEQEQEQEyWb5 O2@$_ [Լ;[z-w0d&r~xHM/ܾ󏼤Z3WFYikȲjbtd)aq$t4ڏ‹kMI"pdC^8>4xK$ p1189n2XӚ X\4Zh_`̄^oG4[2T+̩d8n@? %{Ÿ^"}p|'3E'e%kkbbeb8ʻQ>̖o:ǏexaO|MVf[KMÑ~x$1xѭnV4r c 8砠wӼ]XEΕ ^Y$ 268pNkſ -]?U]C7s# E{<uo |Gulm{f0Ӑ9z[;[{}RUM%v$ [FkgCIXj((.Y6PH4ko4 p  ~zxG< {2Ү7m!z'u]PJu L~_ _+CPCKj3&rdkmbm4 /wᯊḅR{iHdY`1#^ڟR TBr|+L:/rJ$Xr5CrmȹϷM ?|⿊~+rhBgw˝Nn+rB㨯u8xN]ՏbQoYcܦЁs'>Oh:]tc}N'֥~{/|WTԅw֬A$hg ;_>0k:kK` 2ȋ[UǦ+O4/*kа"Z, `b?x8ϯK>4hiڈ;,Nr210|AeA4}snGx' M-tZ1^YZI,RAu_ G ~5މ^q|OО3|'1q[\ p;dy΀>lپ"Α ˏlDZ_ip|xR&e;^ @_|3 A5DyM]܀YQO5~.>mmp8 A?e o6t":Z xw3Jy_dxǾF=[Q }>q}Xe"KjۮHv! G~z|ݫ-:MO 0DE(Q@}~UjzpaymN M#ݟ+ᯆ^SZ~͆|~FjH^Pk d޾Sտddkt31t"Tc'+@Y\7nm4r8+>xRm2a_DpG;>h?2rBa1d8<}VoπwnKtsin8ߎCg8A~)-\5cđܤG~-϶:`0sڻ_~̞8׭eWtb}F ܑ_N6`5΋m&ҳye b3>'xROCtFW[df`&,JaIҾw^li;&Vazdհkqoxm|MCx#ϕ/ݖ,N2 s^~ɚTUKTꬔ柴OƋoh}I42' ^N{c~Ͼ/w:]6 7rIoF ^ٗš usȆtTA~kB(UT xC◀;_Y[!f2J`a<㨯8i.'iyffbrI'&SCV gEHýxn#$ pq4?>/)ktr:a_w*2)T3Z/|E, F!M@=Ev6p1\o*4٧J&ٻlAR {PK_+wm-5KI#ky|ʭ^@89>)߄k: yI[.>1aȰ־'|Ӥ餂)&$+Ip}OZ|W}YO"kʂpҀ= j'Z xvVU?dn@ `[ hq>({}&_`ns9uQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE|W[~3I;9m%oԯ.%6߶fT.?ބ_gEPEP¿ V#^5U|#弿ڢ.9vWQEU]Vs@,.HTPop9GF?~sT܅UgA"eJk[A-q#b){QCcOP_idBV@\!~b>iڗ6gkI%y'2:9= }VzoEsgp& 'HH`ruNյ-.'myp8Nzc:uߋqd=į P2G}AxM>F5M.Gycn<đٕrAǸO]'NLYvE rql`Ӿ@8/n5ľ;St$%Cs@8`x_>ouk6ƻuf,L˖"ɸ0N~_7$ڎR6:p.hZ47C$(*pJ?8ヌ m.i4O / բi0aD~?nYѦ G8aЊ񥎡.m>'Vr#mQ}3rGn=hOķO0d91;t{xửBO|$nZ5#DȊ\j%SD`,D۷/lt+}3NaG82I?A_Uψ_keiG)$%DrH8ksImƝ+@şŶwwBbfjm6:mTcQvO{9\{# k={2+!?$_!G`,r,*vc*zyGxZVWO\@fEn٠{Bnc?M|-x OImS`9`L'/buEܩ!S@8?lO˥dUQC5뫐a0;19<|}]O7ĺ_ 5ĚrM3)'C_ŏě+OU嶴Gj /$t@}?4& bO&1 d ށqJ^*|5_,4~蹝Q *xCCfw"`(8sV<M|%m{_/`U}ukZeCkb-gˑ3^CS~ЉT Y#n :s}/h0[]mNg1@q9>{OKORH0;NjI`i3a_@<ӯ[]3K9g2Mj F7.>ܾ5ͤ'"Jdgw#۾ ƅ(={GSUT8t|#~w":F@FxĂF˹r! g >T=^n9]_J}SQ5:1  ^O}hZNl-]2O 랊zѠҚ߅|3cZ1,H%h\F7 >|+y⏊٣sk\s(*,l Ҿfg &b1>:p@#T6BDӭ4PI[DOS+B??5|@[tuC$l G#G2d  w ΣѯGi=Z{uyW GR'ttP?/Kw^(ozy| ўA=smCo#țWլLjvD<,n+&6C~^5d5OT~[U$`[7|Gѭj`K8`uWVs< ziZƝqa[Eug:T2?2kzL.aXZHBܠ_p:_e@?~#|;1{ŵVPI<|8qCR1m/W.0#5#}.Ŝ6I(Y=;GQ墒Kfi{ 3@^)GOK:1'* ?j <Zaшƀ>ak[ /wSsqz*6:Ļ5V? ]nO@\_KSܘƹ~)MQA_qk'(%~!d\=c4o]fDvA+97|X.# O_ꦥO QJO(zkOi/=͆A h?ƬCNx?'0诈y>)Sxz}f9;4v_گ6+ä ZN9wGMjͧYm oO-%O!W3;Vycm֔Wq~?1OxUcgEne}%\ſ e\QU'-o&_?אXxk/*w΅">:Ot'S}AUı6}ۂ8%]s_o_gT{3)K<"Roafƀ=hion2Z~f5eA|8F#\h"ki^i/nG-;ʯ[|`p_i0O@ǧ?Ohf-a᫈k/be?jܢttZ!2jv2=Sh ][ܞ&85'_΀E7LzN((((((((((((+8?gzo'Ծ7ka04V*z:HҬ/䢭*PQEQEQEQEgOxNLS݋.qـ%G<+蟇|;(Z=N2a"w<ᇡm,u}2{K`MG  ~y|TJZ ep a\qwvڇ B,-.}ê*~3!]bM<c+e@?R(k//Z̨#@r ~$Poh:vd6EE 3W(((;!H.bhd^92 i?iux[E4SmS$\7ՇEwGI}c51 ̄)DZ ][\E< RX:0I[+{Ѭ##}͝M ռ Tu$0?B ~xb7Js$6FP@TQEQEQEQEQEQEQEQEQE {t8 B%s\Q@QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE|]"?XSk&nZ\q,ʾ̠((4M?pGwVe[Fcq7㏼((/V~euKFWTe9Wv>𥎅) +(V$c&*(=E4WK"F{g^E|CO4>N7S4nֈnwKšo1ykH2Õu'8 ~eS/ΜY.>RqU/mL:mx+鮒ʀ#wo;; Y ;Ar݆$sǶp?LxK zvypt%rIqԃtP玻Y#ssE0ߡW|{-gʶV$`p`$q_`Q@B@ik K548V 2đj4QE xqKJࣆ0]GczЎqǀ5k#.5x-;Qp1OsWM,Yn\D/E|sXk2iEbdO(1+}㯈zM/LwWdgz{- ]QoI]b?q^iv6gC \2hzG~pHsuk 'jKs{o.﯈?chϋ7&~1k(((((((((((((((((((((((((((()?TuU[fx݅:h Mc<*JXK}W[a4[2谷BkS_~Ŗ~oĭRp 1orZD?}@)$񂭹c>'E#eS?0]3c_UQ@8'mTy!ݻH~DӿeM\\c˒x~*׽@W|u.?ҧyGN+]K~Ot k9V:s I൯g+Kx5Ҭ@ EAN(( WK/<+>c-4+Ox_H֙>hOOp*(|K:G|]a[ŧC)7gױJa,H?¯3Ɖe[To_HӘ!Ee'44mɣiޢKU.5NwੜrYI?څx<>tC-uՃrO x']֡dYFΪJP~ɜOI*~9>#^=7a$šx;-B9X3D2#8 )s_:?fχOlo2|>'՗]5PO1?DNFv /ʑV`WWTPχSomc?5L?{kZj([K;xXHBXDp9_6¶~0ekɣ8vZ2_Rg_Zn@_|/BMmwqk&#?P&mWnu5lC/V/bgyFog@}\ƫ(&<]8卿3!o[܄gfWQ ~^Ys!Y?~\;3x^ a~@鿵!^o|%'(η-j9Q>EDq0WUy<7bؒq]ʿV/x衶ZۜnQ}w~oH|8s&yC]Ɵ,"Gd~\P7AY߲U\3-VO@ŏE](u(B3 G5/$L36{M#jWŶ{ط@A~8h_$[)v[$u7O_&?7!N'jP_/ëlk2\v=ָSp1Ҽ;+s$p)/%9k?jOٗ:xz9Fh?kkFG^-~^:-KEGnze*Ic(E? eSmgkj1km #4 ~Ӿ+M{OP\*\2c g<sL0 ʼn_qWu:G2OQEQEųOںG_t?寄hF5 Fh-x%X~U=S!$@B3@QEQEQEQEQEQEQEQEQEQEQEQEQEQEVAtAgph |dloU?Zp>Hs4XFү?awx3cZ™yQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEWU]]zUzO\P߱tΟ|JXz,Xf֯cFn{K_nEPEPEPEPEPESStgmin%X@I'h ET#oGE`ps{ޙ<:uHդ-s\ M񝎟iqݪȺhdY@Q@Q@Q@axGr3[O@.+vQ'OZ Iu FFeBG·+WהQ@Q@Q@Q@|Gݧ IӔu7%#!TҾ +ϗxfr)u9f4^if6Ikh((((((((((((((((''|'KpCìj%Dr-}kҼ7X-%?|qEZBYҦ'ƀ>٢((((o4+c[[]>7#s9$5_8~ٵ~!ViG$Km)(3Om; KIKҢvbp}?B{+:ޗmiW1Xܠ)9 _'7w'[magUMymO\te- _/-5Oe3!J+ǃ؎q :oi7imYt1̬Kc6֗_f/[1~s_? Yg+="=fte۟@Q@Q@Q@Wh=W5@𵞛8@.漎G>iڡYp#rOlrߏ~k $r$9G+rI']QEQEQEQEQEQEQEQEQEQEW~ѿEHonRȻ_)b{.xX_'}O T"K6A%AA{Q@Q@Q@Q@y|q4~/÷y $,"Cm+ǽzŤh9UK,p96ky}xα'y%|lPSG[m& $'l m˹g(CFq18+AN{Ƅ;\88e@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@{,BZb#ڬߴݛq}k((гh%e$r(cЕv?_[x5/v"v\i ƦyZ&m{yјF?Ww)x|31h 19i((((((((((((((*MU|x?i#h1f@x?:ռZkZ+/:{1E$?ƾѠ((((((((((((((((((((((((((((:2bs;g OT?Ҿ㯄dmlh((KMt[YZ}o?X@gڀ>mƀ:( ( ( Z}ZA)YxA @?jQu[=F59D6Vq4> ڪ2xho~u-t-5fUɑ 5_+xg{zke 0q H-`8xBu/ _-媹VFFX:4QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE|p+i@ |?WͬZx9=ٷRmF?mLr<;~kSVd5Q@>#j>3oDyB#nwQ3c&Tu&= jEt3nl!]\O@?5:JiV%+[=YMy?ҼaV7rQAHZ( ( )ȑFJꑠ,@I:Q2ͦ'I%*hsH־-8Un!V&fTQ ,+YI0du# 8">.ߴD`}C-t]M b4 Q@Q@Q@Q@R;*)g!UFI' Z+%QEQEs^׊m#8y׌ba_( JN?`-~kyEDd<6&F8Hn0\+;pH㪩?~p1[m?Q-:މn y3c 7>7zpLl?J;VӤG32?}im VֱPD#TQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@|eh,u ُ_maoErcmf$z8QEQEs<x7&:<_ #$B֙-Ĺ^@$ I8 ~oLcA{@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@᧻mKk'07(jHHGV+S(((((((((((((((((((((((((((c$uR=hvWt_$9WTQEQEG]YCRͶ\&pN> Im-#2\HD}~_:h,躣n=ϐY{)xpimkZڴQ4߲`vXJu{~;|ciIq7v2>Bu5[2Ʋoe?"s#k:(K٫%3Ci^`PO~ښ>D(1Ǩ,~-gx=3ú+[Ygv'UBMъ}~fx\]DVn>ѿk7VEi6R`2?p+_S_*w{_Qn}zb8cPP8(z(+_BAsͼJ=Q*"+ ;*F]ʑy(iRh"}F~E~[|C8o r{j@4GZu $/i:x @7^T.^ׂ{} EUuM_L45S V(((((((((((((?kYd6> mnulDH*rba?嶩<Yঈ'M+01SPE>u(uX9tZ8- ΎD< |,'ɇ_[Q@izy6~䱴#_W>^9ZڌgS'y=9'j((m-kMԼG=>!9 x-=^]Rq%*V$ A߀(l OY̓F\d@̨O/-]x>&ugBU=m͎'=FRloI.cheUц#Ѓ_xt|gm?X%-@q=Tݨ~7|wd^=4 j'LӴQ<΍)=A>kZo-& >j~3E-[ɁadsY?g[ǩ{ސG" cFr #챩]ژ=lv+ wAjO񥾟Qo4!Y6QGO=8<`J ikqa[JRNsՀ\}z4\>#}m#k;FE{}Ӧ[mN0sȌpQ&I73vi1ګ8lc ǀz0X3dfX ]I_ѼB!}p2}+ӣ/4ּ):_nܜJ F8=x9'Ă*R#J͌r/s#5u_-4xRcQ80X|@VP6Rpx^ ;?ݯ36heD>(fOf^}EPEP3*)g!T Nwڵ߄-*/%,#`W,¿ /Tq!9UՋ}H4k9TN6Ѣ F'qr2q٧jmREW?)Ǩl5,օufB@Rs@owоɧۖYrm|_[A5VI48RN9$1_wԞ wI$-Դ!E1IlN3ҾTO~:hn,n!6: *YNGqSGu jvvsdty&LI;N1+$|Ht_Z2bPX k?i?7o gA5Dc ^Iv4 ":p>hݖYs#x~)쭾mI,p=|KQ_?hi^4q nŒuwc\Xيyl^eX=׊oH!V#DʼndtP^+/&i|bepMCA+^!G{ :crMøh_q^o]ja5hA$8{{JX6F'}RĊo 2=ӷRowZ7-m,x{b.0W+G9< WWa`1eg, ?U%~+oڟ=Dn,Rwi}c&G t' s0Ij pd |.s${?#|CDd>jEsWW_]ZԐҼ*[ڀ?J* Bucsn4bh2r1} Xӵ* KFq+` ;_-~~(a 4iOgB1wgtޡBgӥp)g>x))1|r29>QX{ ~+I6v.E$qG_|4;Pٯi$x~W,_(|4?jF7) <.Llv #^5;xPҧswjTz0ˏ|q@Eqwj36~+W;UP%^'WF 4( ( ( ( ( ( ( ( ( ( ">㿁:Ş{5堾S"],N>K?~ mʜ&0PYG}'Iɼpf9PwK/=Iq>'>gP\&9h\a@UQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEz {<.۝=q+{84ُ< QEQETX?kr5@Vn+ [,i!.|Ĭƀ>颊(((cm7D.=azꇈ-Aԭef2=AB?|=%@|jӝ ĊOcLk #SCPD\Ü~+;0%cB@3@AC|^t%O^GTGݳ=O~|*wZ]nYaӮewծ2z|gV=j>2GV$a-,@{4[hvZF1Qܞ䜒{h4ٓ6+_M-c=/>\^HLb;yoz7_\ʊW]c!O |C4DJk<۾m#;` ذM$R(tt9  Wߵ8ǡdH곋OٮX wA_[@{/D1IqM5n9E\9qP|nWc7C'jd2zx<`(ړjbOjAtg^@QӱzWB)xei:ͺIHv\ x ~A:(Ŀfφ*yom5 ".r:㞂L|,k">6>"2P5TPW~%?\|3o - lmFem -;0[-(ȅՉ]ޛzq_^UoH2};Y`Nհr8=،>h𝯇|\X qr<E\>03ſ 4o w{<7-fmW r]]~0x i&0"VLYZi+x*)Qœ3D4_ WgP񕷛ggL"eBAS@X>PW#^)΅H v=sƏZiVԒ2m< c8;[{;724m!) aGgi11N}&>yޝuh#ҵKݯ˿& t>;gi ;r{mfȾ/#k6vfx2$цH fվ\ZoK*'ۿ ׉^.Ҽ?kL:PY*OV8ҾÏM4ە61BfR[I݃mF@ =2A'}xAR<;~46$V!@ '~Woj7[[ IS$#3}EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPgMkw[܌7uq$j`mx |Ji(Y}M M9-|TEPEPEPEPEPX}B.w[$>#آ/jՍA9!tB>ǿ%lZ/%;VqCȼ4$qu9l  nd4g>-/ W˜孯3elzDͅzNO֡o YF:/ ~hxbomBWn$~tK|=ZPc'f#+>5|;soXEmךj?/kRzH?EZo;[i .!x=M#žwdkMӃkea`G5?ٯ%lz}('MoƛD Au|b7?i|uFҽ ̧ TWڟ\46Ն x-}E|of> k Ѓn>eMI W0SQIwo<ˈS?p+ቾllm}d9(-\y B7o  U4%3qw|$W?}kQ &U|/ѡl5ifj>)H-Y}ʀ=Rq֩yq\럵oIE5KG3AcTZg졠FA.|qIkړbI1]@>(;+m䕡o:Ҟ7𮎒Gdkew&ɮ XXb?ƁG(~>/oLzz\$ U$ߑ< ZOD6H&|]v8((((((((((((((((((((((((((((3PE:8'MG|A"G,7 pߠ((R~Q{Z$5$_:ݖIH( ( ( 6 t՚O@2Z&~>?xg޿ZdD@!zd>xr)Ԥ2ξ?g]L))Yw6~B?_m=<;iSo4F)o $gnx屜 g*e9EP2RR0A_  _*Uؖe,`PĴkF0q }^biI}^4?z|Og/izyb`H +~y*G55W|Zߤ8>^X\4B`<`eIW"I:S :(+Z|#xS9k~[_MtvFqǏT0nSWx6>(((3&Hj2u+hYsXkS\|Ο`t_Ҁ>a#)ã;*4BV~bq< sx/D񦵬|wU#I}:tڗ^MkwMo:4rF*F# |x#NrY[d;;;zbIVJH,mmAcU+?x"5Y1/2냌&(⟉%3 }[N/ 3#ku ]b:MΙ?o\nUCdzsDpYp'l.0`ge}Ey/IcB5M6F1=R{k֨((((((((((ەA%=N!2:_K7BCZ_ۘ ׿B"E-uQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQ״]sDҵ/i{ʠu*p{g|Fmwih2hi8FH=Z((((((((((((((7Bx*ʹ/ \E''ea„?*AOg r&Yz;3߲kxl4䜒zhF((((((((((((((((((((((((((((((((((((((((((((((((((m0yW%n:eٿ:σ ^ >Uߨ5_n~X]589k 7x&?~Ȋ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (>)[~W &WO&?ia}A17_kPEPEP_~V?١[Z $SIR:|_}߶8qy7Ӧ÷h \>o%͜3L ,?2kR|3mmoR,w"44\096nϵ|Y/>..#5iV96s6I\޿@qm/^ On F־"ѤixKmRDn8?!_\u_WfΧpIJBݽ^Ѽw -m.Nn|XYK 0GڻI|=go bEk8g q:/K:dv';+;VMw8!lg\ qڽ[ĭ+ῇmJpV6gϢO W~b#+Fz(2cr>!&Hl`#;7@=U׊|+ RvYnH%ub@=h>0? .wMe+&a,|0%k~ mM-r_mcM}7@O+%DeZ= 11?f Kƞ't_$%3xHh5I6p떊Mи?SCx "ᚿo%> =#?\¨&뿌!yQEQEQEQEQEQEQEQEQEQEGm󂾀D1+_?~ܠ} } I_ g:( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (*k]å^ID^L_Y7լ,W42NďݦH a@h htkM+FK[ XqFOROry'hQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEyY~Xlf3$| 䵽u>,Bٙd׈~Rb1z;F@XQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE SRQ#qkBjmjHKE.((l jxv~6ُ90B8}5-W,+#)r"?2 i%.V3,ѻ^<.峗IewV$r(W;*-!kHX0+d` 9Rxk?~*a tH{ٚv?&>Ϫ2VUh%sqր>Ϣ!SmpбC?S"@6{VSV,\[>4c}ǚފ?~&u ]F \j6sptH=v4:ws#I(Xω&I+z}G^gfdMn}zU;S9 t=ѱҀ .>^ݳPYxGfE90 ~W?g+=-d[neP-F.C; c9l xY20C$-+6$\ iu渋/BxME|z?־Q@끊Zÿ1ai.t~@ o98=^]='$~((#Q0T@YS@3}{S`lg>- dO@z0GeX!?+mONf6 G cBӇ8tv@R_)X2iPx+t[Om/nC$JA.EQEW?Xk/9ԏ\f>y55~~_|G-9vo"6}4k42DS5aq+D*cNA\nB~7:zφRLW\#:ξ6~'Ch궈I6Pzu휋-k,N!Aj|-QǢxR-_xmY]0\ ִn?ivsݎ񝮒Ȭڗ3ko,,pĥ*M|# ZiM.6Ry@3H=|/pY سcg 먢 ( ( ( ( ( ( ( ( ( (>M:f/|gom[CbQ+Oot%`lRΠ*LAD:G(f((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((( z͔Z{erHn!xOB"6fvfoq:vfGM:Vß}"$O@vEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE6gz*E09 !̿Wd7pKAh8+*((((((a2rcL( #bn@g8(((((_ =uz꩓D$S(xRpA6ֆFMrjv2"GBOu'4߁,vK CٗbrAt'ZIt=BA-)Cg+ccQA')F<6dP8{%0+(-Kş>jVo7OR~8a}v!ֈk:(+UׯmYlaL1\mHem{u-Ω$s\3¨'_2F]}dU{}:va%V#܁@Zei]+kco@HDPs~{,m"rB[Ap+0m/48+}Cv,Ė$[M7E?QuU_-PEPEPEPEPEPEPEPEPEPEPEPaLC#1AD +_ff 6۞qQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEPN8'b$V\_\A˱p8 )#p讹 EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP_x`~18- }_||4v"CIqar6/ f#"((((((((((((((((((((((((((((((((((((((((((((((++wgOx& IewZp};."%_FlaxC;UBe|<[{YVCʾ((((((((((((({ƾ Ա6Жdt>C|kcM4k>b$T@0*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE_}GɁb>D_jWxI59r5MQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEe1޷*7m8 ?Ҿᯇl]у}_@nY%AVtU*QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEߴc%`N}5?55_k!ѥ~po߿T0jB;=H.^#T(((((((((((((+K/xJ^`#B@g,q=O]M|k90\b|=*|g=oFc|M@ax[pe[H:@b<׿PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEɈXd'RM|E(OF.9B ]k ,i%x]OM$xWTQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEW` O+U|%i f`qugm#8؟ƀ>LTؗlH@Q@Q@Q@Q@TW70Ze(b^F hZ+ǼoCx2mn.O;tҲ"y2A{_v=<;vy݇c 9׎@>¨./-7]W~:鿲֞ N n^-Z:gX6e4s- =n[}+F^K ')$jM_m=?OFl鿲TmOű'ķFg"ɨd2FNƖ;dV̍m<~2WӺgi$qxSJhKg5:uQ¨?A@GC|Jgcf-\͘Ԛd6>`:>Y& (ۏziHEO] \" 3,1E hemj?ݚ*?>BWl|eu?ԧ#x#D0:7!Ɲ_j*|:I-uXҧgF;u`+=(K-n2Ign{<[i|SPdX[H@dV[yc&W"$(((((((((((((((((((((((((((($wtvKcֽn8/vҘ/AIzwA?c`z q-{QEQEQEQEQEQEQEQEQEQEQEQEQE?O6K/Uʿ^eWR) (OJ}/&T"A?t唟Wٕ[? ,anF>Q$\9m5'+s W;qqҽK K"]o^u *x z L_^!lb`^{1^φ p3z`% /{s]E REQEQEQEQEQEQEQEQEQEdR.HW1GuP32u S$< W+׉@?y$zیdqdG28ȯJ~O]λdUʷ^Ryq=oX7ѕoz>vdH?_H&ex̋H<h((((((((((((((((((((((((((+jQ^$>UԾ x K#qqL$@hοbޯ : hkZ4/iq\qV\/־((((((((((((((?loR:~f?ȩ12[ԝ+NZ֕wN$ N0p˟+k heR)NDJ2«sj7fO jV}플myB %$zb2kzdOj P6r"W ᇄPAن %3z((((((((((((((((+?jY>xk/h^s+Oe ت᮫+]Qʟc澂" Z1^N_ҽ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( mw#*La xS$~;_5E~v|8i|<,a]jz_W>1I|5Ē-f |F6#*OAe:m~/ui )k&@>s H @$(((((((((((((((((((((((((((*fYܷDG#(rVqc Cvd( 3s_[EPEPEPEPEPEPEPEPEPEPEPEPEPEP1:G5.F|ȥC@d<|{ W@'cE*A8_MF|qBB7m'$FdYJ6p^ig#qXPm׀1k4ٷݭ1]Xʣ[*+|9A<-g!'$ܼlhg`9M:>|)e'sdjkQc/>x 43wX.E_I{Zih>&X^'ݛE|~g~]y|<3]S?5%o Z9zɨF<_ q̶y xJwd_~վ,ra48G;ՖB}1\~OX?T`M|Q@j>OVÖmj0͌v?g`ԭM6K=cgݏ |ʞ2kɿfE,Vț0TFy9-Ӡ(((((((((((+l? m3j>$oھZ%}u@:OM6?\'<;s2WPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP_*џ b~U|q<"0KJڭ<)>n#@8j ZQ^^_ Aj3A\?r(΅/jEݖ=%@uHȯ;^~.:~=2h-&BĒIA1z#ѭkf[ͣLK ¤Y@}{ο!Ygk[]rە#qºqqm-v gЎ3~/ 8_F}?ZVj+uo^2؏\+DZtZ]l]2Q5+=G4hM/܄"HgbH` AEQEQEQEQEQEQEQEQEQER;*)g`:ps:h5_%ons@=^%C?o5kQ s^YZm C<!~Ԓ}Loo q>wMo;u|{F#X׮. Az !shDQݐC]LL8v >sȅ|xv^>뻰1h2fb}_sWTx}@ay/اrZxs[ lo- GsY8Uy: 7S\k6.AV?e}O_ hy5+űU0)N3韭x?4_W'aȌA2Nef'#Ph߀Tөiؾj xCn2bo e< x'i[/lI2v:X?Z*OКLzivzF9h/K#8>)JJ 4YƣSp wP·沛Sao 91wVWw7V)d?0k=cN$I[I?k'ݕzwyKHMasiwq&>٢(wMbQOdq"-Ens"1;gr zmPTWxG|D$WnELAyd?'=^iuoyiُslC^ֳlix]x{SmnBT K(?Z'ڶjE& [O p1 {?>q/Huٮa02s"~,f_P@?PgW=~mo? C1N?pҿI( A> {ޅl 9r+>vᛡ;ZqVhAW$>)x~Xϋ6s}<kj(坿wJjﵳKq+֓x";Y4fhk AՒ2-#gRǂ5my)=j#m90ş]<hlxē/UO7mi?x7\lnlW'E_G%Kx]Y4SDh/|QK(Ƿj+4-lR"CknWi")C_'1"xk-w?=_w|]_M ⾀66`)_m~(e<0+kOB%?HO8޹+yэ{ӬH% U[Kp]H ?j7uOÍRPG&ePbE9S*()><|KOؾkK袩ZWa2Dgsi7HS l6?FDI |SVȢ0P;c=?g_v'm<iTC?K]!K8V B[wf#q\G2?jjUgNttmҫyI\Oøi0'" >ǰ˷c_pq QB;(~Ѽi;Oس7u'5sú&IѭqFry'ԞMh&8#i'#z~&\'^i'6.NA> _=KWC_;$Vr31[ץ|4xOI_Z Kő>m 8==8־&IyeaGE,$?7 SQڻΓ=hufU' utQEQEQEQEQEQEQEQEQE$G&j~Z%5@:hW3-zy7OH}J(((((((((((((((((((((kM6^cc73mQ>Ю^4߄[zՎ ,sJ<+_.M`h '(oڅS1!W܌8 Ξ'e&74˓]dfTsg ? x^rF^SnѢ*:Jaq;q_si7}-OR#n ,m% ĊCHX@->9MsN 1Durֺ(~ʺ]R^{+o||ȏy'w~+g_ůW^_mNasq0p1W4@ ApeW/r;r C>!|OAh72Fz{0<~xOƖ[ŤL˼]o1 8翽|}H :K-[+R![ր>#VfF Q91Tp<(((((((((((((((((((((((((((((((((((((((((((((((((?,Ku]6f- q.N$R4gP{Z{(y(Ǯ2NT WZxLk,P*<(_?g>?݉WU| ,qCa_}EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPϿᖓ _7Vx? 'ڄz?\yK?c,cgh!j"Pl2w.h(((((((((((((((((((((OM흞Y8Kh'Qb@$;ZQEQEexZOi]~++~g9b{ 4Eq>o^jKdI G$g((>^][Z9E$@;>RԀu4~5꫌36=޷aM W&nD {yt":5WG|O~hҀ#.ߚeIҡllXaL=O@袊(((((((((((((+&%,& 1;vcw/BؼEc@UCSZ𮑩ڳ<vLPy^=(]W;]*wӷPǢHkh((( 'y|r:G4g)"Sȯ?mmLJxPmB#o3dlSIftQQ[?+bCkkdSKQ XZ*(((((((((((((((((((*iX{Aii i" ,xf٘OG4k(ӼX [9.ѭWYUT_ >)i +L&.,ޠ͓]$z/| _L,KWL{u_𷎾;!<1B̐jp$h U|(iQX=:*LɯD~3A-w`XG"=Y n^7}JO |B🋊'7q&ɀaEQEQEQEQEQEQEQEQEQEQEV_ 3֑ZGwar^7=:9+˾3_>W6GqI!ckhl|aZLAH'^qu㯑atGaY?rV;H>=ox/xsw}y!©9uМ<ǾȊg|S!$rpkψ|csW]>93mٱAn\}xy/ğgѼjXn(nvrO@{#v'3!{H _?$A'LY- Bs<++Tn5Ia8٧v <GRkx:m 3|dUʁ7Ҽ @7z49N$y[(dg%y;too95k8P+ ~>=[:6:4AMw#,{]Of_ ?<څB' l$䁀s׷jO/f jݼrF㞣#P +~:|dӾigbb< e"A/|p5І1q#+`7`I '_ >O~6mH36(pM}h~pc%}I!'@X e 3ZAKj8n4ZKj?ᐓkH$0E>((((((((((((((W[kY#핞SduVcTP~ǟZ\i~3첣9(x.e;GǗl" aGҀ<{W@J붗V=\@+$NOЎ+˼]"'"9Ow7_=&Ĥs$Ra sǠW7?>3.D6'k\:crx(~mm<, ntWw’ɺfF({C(_?Lec,HCt3?b-e=|I,RSN#k)Fx ?S?e\ǭ\ZpKG+{UkVI9f?,CSςGLCsv} 4ًk`݉X (((((((((+ſk+j⼀^^d pA?icG(CIM뛃zq?{QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE|ogCq4ٟ껇+{Mfu s#4d|YiS"?Ҿl-5;)l+X.%dxãB_#?o1ȑWt-<Y[hn&\!G?g;tS& cp2p߅}E|+|N׿d)kv)!eaWWЮGmO 'S>U,u{),[;{YĈ+ޮ}\)jD0nAoȏ@g 5:W Ji2]͐xG1p_޲QEQEVa3Lu(mڻTXgYr|Gqfo3}_wzLw=Ϋ#C#噘<*(~X=jl ,-oR8f$+JGmŞ Y/e,?y:ݖǹʀ:_~ukKTM)9>o׽wI&[ox~A )L3$0+s}(ǃedO{+--#̐c|]|N֚~O!k P!w!T gv.W&%ЃU~ĖP/4Ƭ.h޼!>N_ZZE2 2~M5h&#ϔǯs%Yti?-Xg I펕xgEĞ 4 vYĪ&rpr8@ kNF Im?HUlIt1;ŽOc}V 93_sk5ԑV5v%Or}fFYcN 2=E!8݅f6NvrP;4φTmn|Dw8$90|i'kEft1\1S)?.neSN1V.YA}c.7:V8>X/ 5օ:y @I3sxc춶&=7}Ey?l ija<OoE}4ƓIٺc&:@Gqyk;]46ݕ8؀}mFGqֵU.fNqp4+m: ;"qCTP0iZu[iZ[Hv(ZӼš`<+?{]M>C,yz /IP9yy sg=G?d-[G6ͻv{5X{~{eQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ 46ꮡ r }pSCgG]e\| }q$@n\;"zdÏ %z:hl/,u!%F(}@bAǽl=3ss5хd9ؠgOP:W# _ |LJ~$74 q0!O\`𧁓[ ( ( ( ( ( ( ( ( ( ( ( [IfaD( hdq?a@_;?%zyG찛~tyOL?zQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEG^͡1m"M^N)|K6>-\is}_~ɺflkqۿdh((}iIu\8:ՊB:T ?Z/x.Vz -!s}Nܚꨠ>16I'(a䆈"[zdzxqs+ͻɳMn-x0ڸJSK4Hƣ,p$m.`g9~VH2|+5sX?|)W4P.&0:զ?;ٳjWVq{=QuKmDoإq3x:|5ᾷUg᭢!|i?'5ߏ>8xƺxzP4mJTstdqfOǯx'R6!{b1K=I̧"B Y@Y[#n:9?Ѳ{'i=)G]>-ۚg|<#VQLUz>io?!Vh ( ( ڳHo^ϧdG_cWxcS~ ZRMOφd?6>S+d[?8ɷeQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEW߷ɧx[IYUkjIiqwcV}iu^YF݈FtS9<1E}5_3~؟ҭ<_Yڄ2 {$d4$10R1F1FkR4[Z2Dt[8&FyAy1ʌq]GȺ|=$ɒ-Vw Mچ&K ־}^+j ٴU ;\PYx Jog'~{^#^#o0"gM}LYO|< zĒ+1Ao¾ F{Vs-"&}} ? =gvؒ7FGe.G87j_kڄv-lnJ ҭgfGn$Fi>{ g4+w}quo$U'ϥv~ Q .u9} 12v?'k[F76$qU#c{x07tI)#Ulgf8#}~ݸ}RG/i0OQ] I gq&vnxps׸_G~6:mu4ۗX"If!ʙ"R<$zxӽy_=[yMϨVT1Ԍs_/~7F,j .y^~ ;x[tDO$,<5oeK渶m i,앝Q>a۷>@}iwѤRU17A?);OÏ $oEe(&*pHĶ8gW[@Q@ krDd_2>%|ټچٓk,>5QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEV/k Zadv4Vɜp8?//+\o2ZdnQ!x>nLeTg׎G&>ڢO~?> ior[62Q{]]Q^5K|Rw;]ֵ_1a`m6}9ZiyP[Co@U^5ca3 9 6I?u&:Ρuz(+> xT:=Cqd9qpo.$/5 Jcgtf]:m/!%N:l:s"KdDT.8zP|l؍O&Gq1 zoXs}w;j-'y}kR~_&"9/s;]ߗ,Bwo_jpxCŤ٥n)Opz/_W;Eηݎpl3?/#$V$Qq N(8Z%E WN^}QwEy/E%//P8{wĻa{6ł Fv&b ݞ'=Y6?r?}{EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP_:D/O#?ZmwxB'?-wDBUc9?ֽ:\krћ'+ը(((((((((((((((((((ʇMO\j/+Ė|A徦> `;ioX֮m#QPL~|J?'pC|SuOҀ ( (O*A vcɠ/4x e ϳd#Ӕ8InC(?~cxRX~ɽ20v?M9JiNHF@|Y;žֵ6G';H{_csqۉK0YMǜ$d'r?_=YTJاC:ֺlֻÏB4QE|j4 tWHdE8R능#x304;u?WO[hIfRϥݥWF0Y]Iq=Df@`袊+3g%&z~$X1̸.b\x~W=>+F qʩi7`򯤼moxZKW\)$gRtg\5`ۼ˛%p8??(᫫}gt,z=—Nթb6F<|?'tlŤ QTO([}F \/ᾍ&0d,3vtQEQEQE? 4iz+G"+^^]LRj5G>c@7K.|ß$-}_,~×a}[L((((((((((((((((((((((((((((((((((((((((((((((((m WQD*5NKw +[s1TDTX~hNgC_WsJmo,$ lzna]QEQE88__F"kR[`FN@ֿF~?->aR㾷s;g>;nI5_)NQf.2aV빜.2I=i%M-"&LIfQ%z^~f+a e8"W'=+ݼ-@|7ek%lǁր<;ƽ-GЯfQq< RVS9Qqt>U;㮧 ]|[}j % Xn3Q_w :9ӼGd1)$-99k<;5H#T:y01;`r2+ڗA - }mR˝.W>&Ad&98==Tſ[_{(%-.2>@A 9Y!u6 4 uMh} D09+I9tS'mHઓS A\#cG[6i-1/h^Hϒ_*y5|C~-}4?}@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@xo'짾 ZA,i;@8>j?3|K򵬗:>` )3~ |eҾ# Il$ho|k fR0BGc܇>+_ 3$+&m5[&eV=Ôlv%_*Z|=ETuE~*/Ge%%b]]{2Xg k-/UJ¶_Vx[%ȏcɷ9>)iGΣ_{7U5 Bs},c(2"}K\: FIVʒ+ {7ik}3H'vM{ y6 <-Dg|q#~tMy*~k ZXWF%jPq 9cks%_] y\Vp^8<`g)M;qF(j-JLQ/,8TE=+_ߌ#`GQT'o.7}ӑ~9>)?dӴ%n_?v9Trz_>EG) "DkF2 'sg;Ƙ.D$fkBWz/\n(((( ǐǁEg&r} | KU8:4 +TM]FCGM|_J#zZi3/EJf((((((((((((((((((((((((((((((((R~id Y>߸[@n귖XkOmqٮFckſdK;ݬgX]Οn٫h((((((((((((((((((((گOm7N:e P)ݹWּ/jgʽ3:W/xwZw6of|crSv> x^r0ۛB3<Pz( ~(O-O[I.uV󮚸WIi3\Ƨ86Q@u̷-r[TW~ƷכZR~h&X ]#u]ſS\>#(?nj? x{J'k=jտ؞٣;&pF@DQX^&|./9ϔg=͵CZ_fq%dgzuRҮF`H]JZ_τ>?i}63AU1O0)+ :S#A$SDەԌ | #៎֗$I,wr]?~W|aÏZgBP O&=5+\s}F+&ڊyH|@K!WQEQEQEQEWw|'? ?M]c-KxgVtEtĖybeYWn>.?LgWQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQETk7%Լ[ќ\jw2{~He2]CF{:s\k d9L?]y[UEQEQEQEQEQEQEQEQEQEQEQEQEQEQESXb/sHM[o^4#+A}~C|ډ)Џ_k?%xӄ#*_cPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPQ[wo$QG4 Xzzقf ($@mo~P9ĞT7SK{p_f3UWLON3nI2=s@9Ȉڎ. >?#;ƾ_O jzۗ8^N:kg;ßUC E;Uwgk( q@UE&/eu4Vr.\ې&6À7\J?jv: 3 x#T>@>zWyiZ~fZZyO±oS2jj-ޫ-fYdn ɬ9Ѽٵmnp01n=z{M|i/|;g&y.m=&tOߴl^EFdl2$q>85/ZG]f]:6:&+G!6D48Y߻d!8EPEPEPEP9f.n?jr?d\Wٷmf<J4mF%Ε19$( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( O`7qn/`~Jk?mo%W # }7q4;\Z ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (X Tx~7o_WKs}WQC'L kѿbhXhPO;a? Kho4ۻkH&tqA@hF sd*p?ﰟ}qq 8oy'ƀ>9SLek-囅W@#`GU<@:(ſu]+-\#۝k(*nK5jBOGgXA6\FDcD)lS:v^=aÿ隑q{h뎊9`@Iώ^?o9}O'l ibwmQ @?!(RFpЮכ| y"ɩJnr3פPEPEPEPEPEPE|Y_wKjn6X>w>€;+ɾ|bzjPM 7Q17deNF;kh(( Ԃ/-cŗ Zkiy?1OZv<-?v uդ ־QOj ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 򟎟]im_߉435\pI#Ww@X` Oy'?(x.ww}5^;HC@xOz~/ >1}{[%Āz8b^wJi[ȯ!`FC^) /9@1 :U BpGl<#^,iw ~ϋQo7K OO$'#cAw nbѤ"@ϔ{pbd?慡:yzyK<ƀ:(!T '~sYcTG]-k&ʸ62_b~0-زM#(][s9*ׂίM u#ʓJh߇o4K8vܒߋk(+3 oxvpm'p9=jG2܀| %ؚ_|XWQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE_ˉM|7S{G$;ƾnWğ?.!Q@Q@Q@{q$퓎K_>$֯|EƩ'{{3O)$$3W鶵eL p;RݼwS[\x]U`pG(4|MjfMŌ/w.Qrw)y[7|ZsWI@EfNzHF&p 8v<#SQcxsOŘm[Y^9{ۃkh'4Qm 8"2"oﴥעEr%/ j ck7d= N%{z6{iCn8~EPEr?_'|}çG| 3y¿3AuXx|={,kӀ.?@_>e xj@ W'.dVa )FO8x#> L > j Ic{;nzpҿLCZ^[Xs&:E,r~'sͬ2kP8@ܐ+/POe;cE>C'9wxQu&>uܥ=ۄI1z CYѢ]Jim2g'?gZ&kFw178NۇPppznB6ڏQuf(u8UڕV!Y`I$.k5tg;t(҆"KcqI$gv[ў6=>k/[ɵX6.8ypts5o/'=?G&9',@7;o=;Q$&cf,[}{ PEPEPEPEPEPEP_|:) Qqk2K ~5|(toږgP"5k' ]:,qb&t<8-k#HK{/Su-wL&UF:WW"m+i">8zlG9Go[7}qIEPEPEPEP(9RFJ)Q@Av ^ڰ8ZGcQIr?3']?Tn J㨠0<;k>Du GKX|XEYu`8.0O{?? ࿆z&toR#5L]:ݷ]QEQEWZc2D?Cog,^ tN1yw;uKsg ޾alvi,cbOҾ((((((((((((((((((((((((((((((((((((((((((((((((O"LuqW߲/(9>UҶ_m 1|M7&Ex? j(((72oUUBYW#,FC_~O-lKT <>D}}hTm[ෆ&B05爤hBפׂs^?˸Qo'YH^@nŸ ou5>Yc}#U e _'~^)}xeFlx?*ᮝpnlQlA" ڀ;oo^afI\t[ihgB |8Rn~_ [z%xb@GVew=% |-w00a~tc6r҄MԄ >|mڸ~zk7,>~m?H sҾwM <=nCM4IyL>==ljƣ;P(5/_ EŚoA c1Ov!Z}zDR$f0TzW?Xhy<oLPC {$)9Cpd㑁@gI['+_UsM}VW+W"> < cZ"-hw:w4쥹F/KF`ERx5KHiux?Kcap.e;;S$x">, +R{K)qtazk[0Ţu>aR$,A<#3u k޳OY\xrho>fhÔO-*z?e?)񞳬j6֐,K$h$G-21| Im.%n.aߴ6t\Ey'~7xm [wD rNG;+돍 l~(iBK+gYr2у˾87\ؾ {v8y:W^ekąs"5Ƹ'#d~+ku c6 ǖ b:=q =ԣ.t :Nk@YU@!IO o|OJmB>nfO'@o( Cdg`7uP{5G+g -=DJz6|Q᦮p~QEQEQEQEQEQEQEQEQEQEQEQEQEQEexG__WўS ڙMW8~o:ςYd~̽)Wq>tV kw߅^:8 W?J;}3 c|E*Hxqp7|w$~ˈ%_[g\Ӟ^Q2\]JnNa$d<xԊ7}\V[_۾ xR`rF?ɏn? Z(((((((((((((((((((((((((((((((+j?\?ݒtֽr8v҈eυV PSX->a~k:?3sFH"9~0žk-.Ŗ+mجL9aEytz<)ޗ%y Q*Նe9 A:+_; J+^-)@Yc9'#oNkgNj3DС%ĒɌņ}޾(O᧎WdUfj :ȃX7~XM&6n8 ]zUe# Es <9zziNGION>QZ\yk͜Oo*X2|uUyqg{p[\` 13q#$r>Ϣ~3ǿ4z&QPHT 3d⟵?s\!>u>11+o٧1}" م|) ,LVD`è#hFN:&|N~&^Ym/X%ݛ>9C`eN;J?m~hV@XeǿZF tf+in]?`Ơ)=ZRoդbU_6gK7Yn'|P zQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE2~ ?U|< {ktn"]7ve1۹NQEQE|AVKkG$gl#'AWĿ׊5-4FK;9%_g@c᭏_ yM o.Xc{ ;(נQEBh g!?ѕG1_ xb |w+PMDcE<ch5㿲^?Ji^@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@R+kǡ-Q@Q@`xhEub~.݌nss5߄<t*PnCB0,A`+jZ4 +@!  lPO [&!V9 $%DxHLg|I|9շZ2@ku6ed!/Oqg KşWQgXtl6;c՛'=A_X>ȗB_ib'xOS煼7xUo%(d!GRI <5Y)dw+;1̊['{F0æ>8\3O5q_  Y,tQ,Nn9b3pNMuڅc=[Y`I$h>N;EޒfE=SV{Ѻ٧_C&囬``tz%xO]m%?h>ay+דO~6SRP#07_ݎn>]Ý8Y&YgŽԒ{:(((((((((+m8Olx@ag"#.>a<(X9#&eX$h?G^@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@yGNuL?zycgp U9AQ܋#ٯӴK|;((((((((((((((((((((ͱx0jzdl7PK`=Lkjf_YAy2W=Ȣ/7 ϘJ؂ ~.OZfF9Q \~ +ڳf7/uu w-+E*8eS5߳O|,Ҵw | 4 r2:rGjƉq|[4./dcrJܑW_ |Qm6h14!$r't F+i_M-u=$"ǻI8 ~/0C/ t2{[63s}) oԠ9;>g'{cP33k 귺z s404F7mQA?m Niç7B#>xow|i--܌ARF>eq@$ү䱖kr/GB.=P*xc⎿gs\.F7#lWȞ ռ~"Qic<#\(Avl{_ڷuNJȼQ,5HYvF께2A>=š7SY.`W#c<1q_~gO]"x^rlH-߭i|%=Eڞlf$ I=y[[sD#!+^H]+bSeob3'{iڔ; L5[(*:n 5 FSfaWDG [9sԾ">e3jMeayђ=p|/NcmEMON5sm*6ŭM@*Iim m)5euT$n,'~c>j?hZsgz9Ӯ\WZ۠!sR! 9ھ23DҼ?{msycSG.61'< ?xoKҧm G#$ `wl-ї~,F#*"1ԚZghV'Ub.B* ?P:u#{48vg(~5=_5֬"Y(qˎ۾P<wVaQ/ 0ډ<}gp9zp _Ο}wwco+\*{vzg'F}RjMt[NҴ[[H䕿3 ^;~!=c];[e[Td@X _R_ 4{,}n<5†GU: zjbV!# 6ь)y_}|.,Yt]ИW\)H͹HOM@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@~oxWg}jI,5[J&ѕ6AV5C_~3x^ (XLr>2XjZO}6r+pz9'5mCi:ʑ[KpD,I/g+c\[]>CI#zHZ|YWx5so%/j"{HmY9@]O}{qww!FW $M}0(Oi-nDd,Y8 0NO?w ƫ\F33[3|~:+O jڔ1 0P?yqJT0ItG``g`,xΡxH|?wu{adDvV`@e8\D!\(ҭepV''jk|:mu-N_GBgܰlP h{}oVp@!a2rX?0Z4 {UtԵ>K_4GN:; ;Ԛ?Ե -ydQAF9稏vBz玆Ҫ60pFq@¨ᇄb="aT`ƺET*v((((((((((((((((((((((((((((((((((((((((((((((((;F TK4GBk9о8F-a17f:(##QEQEQEQEQEQEQEQEQEQEQEQEQEQEWʿ?W e-SRcqukj1С,W sǸB}?3(埳._4{Mb[Ide*Vay?zQEQEQEQEQEQEQEQEQEPN'ڒ-/׺|~ikY1l)8f~|V/K%f9QOF$ PT5_^nln1K2c9ЎE_govm_^dK s$`;HǏíQ}eRKfF,Nz5Þ=#QgiȠ,lrNNy7 |`>*Wih3K*Ʊ I7!=</xJe\%s&<$s{[#N;5GtQC欖{M#>-x{I .1.Ǚ$@91]p!L63MPksxڔ êjF!ٞR 䞹^uo şBs6ƙD:$gg2Nk x_EotllV1˶yfOA63[nGq} 䎥Qssq^EQEQEQEQEQEQEQEQEQEQEQEW~?UȷF>W/c? m/+4DReaҀ<5 ˛R?P1eW٫ku}MAO+qY 33PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP\]`h? If vp ڣ$ j\Š&:f?xgM;5!OIkK `}3?Z((((((((((((((((((((gK9/㊚k>5]ecTUnUOH?y=St=+SG,mM0$s\0@g NO~xEGebӯ"c2C3ߺ޷c= WҦYYcaFpGb:Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@+OGޟ+SӝΠK+rϳgO4NuZ[M* A28;*Sj{5=`}O,c<`#>W v_ydx #y.Gdwv(((((((((((((((((((((((((((((((((((((((((((((((+CpSRȄ~ }^imnT&îJQeý [Isn1"9iMu|,/4t֊#ݝ; ( ( ( ( ( ( ( ( ( ( ( ( ( ( `(+.u=ӎ>ѼW-:͎J6ܧ ZKgPmP.&` AJŇ2+ j9<|fq`wkz# GIsߪLH'Z?~yֽ+?|9aFk냖R:*ӓIz=64HXUDQUzN(((((((((((((O,--B0wnxt~> ;N|d'Q^/χ#iPxw۱k{w 4$vA8׿d[@4f8^w̼P7#X[nFv_&l Gla \{ve/k^a¿L~_5Ui&>>C*2?+Oիo{P''?ޒP jvҼIc~՞ ' #j,Ӳ\,5g_K4ǟڳãxbP?jwѼA~iu//V1\ |qp'$.yվsD׏!R?q_oW6g>vzU}}֩ʚ?PW[l|w]_V# [EQ)ejX| obkV'{,ڔ/[ |.p^ |w!z©!g(|@o߸m9^ ?@²uO5'VIn?(s@5o? ߸~8ѼAg'_̶r VC?6pO_ڳdG ?V_lb𭓯$v&ž{%߯q7d'?0j~_[h>j3lƧPoq^l@c>On?U eb3XS~_\%Keo;MOQ"h?kjH(S?޾/?j"q(Y6=$ qW^]ҮQ47?T)09.d0&_O6[Y7?/6{ưC5y *FO0ƶCݮG**2# v(+@Z|xK ` o)Y/[Փ}sE|dؿ׏4o7?S@]Q_\mͧ!>R>:T:G]xY-"8 #$۰h-?k+" ܪpL]-KiP8!?C@Exu; n n5Sz+?jAE|5s!]K_Ibn K7߈l֍_]ȱx@F5uuTU+][M`{E2 ](((((((((.>x$}"Y`?k+j2WW?z}*z.l%8٫z#wQ\T?󯰨(((((((((((((((((((((i?|Az$qiŚ o"#=7d)G&|!WRS ?nF22~^qz^OƓ۲i"Bx"%QrpG ~m.-GAdI gu֭|qJwvum<16aΕ0 s ϑ~:+ϼ3>" >%f`m x]FSuQ@Q@Q@Q@Q@Q@Q@Q@R;*)g`9$@ Eq/iK$ +,&N}^KړA/j7|DcSQ`gր>+k/3K^U;lm>[N9l_9M㟌1 e6:ee&mKU$}p9,=yiThKj0KͻbNXCy3<߅ ; ۭl1|OrI$kA<3i:Vv"~dRO$:(((((((((((((((((((((((((((((((((((((((((((((((((#xH$5ד #2L碨$ '@kuO3C{!ͼ t&cI𞃩[kG@d Щ*co{LԼ?K yu"QoPA;O@XEQEQEQEQEQEQEQEQEQEQEQEQEQEUu-25-`@Y@@'F2%֫26Xۖ;S/~ԚΦg!t.f[>G^6-`^VB -ƹ?:\Zɶ.8Ǔn a g7n >%X#<_{uefz\ꄗ x"(8A\iZLdI f ;n 1 op"B"ρ%[/ xrT `d3=΀ ( ( ( ( ( gt hj K;20$z(O3Χ6%QX}!)R3ٹ:3>>1}HFnv]m~z#'uK>!2id{Ksݎ Cg#~u@Q@W_[ jw#E7W("rUE bO#>xGEOјHxteaʞH M|{O_E$޳no!A f@`6v<-CFRkLjﶺopꡁq@?e}nPZiHcxnsJg959$,Y#۶@2kS JԷaW3mvDe˽cFE = F랫~7eo~.b3\b\b8G^q_YxcWh4+;4O,O$֜hƩ"0(((((((((((((((((((((((((((((((((FP imʲs+uB*]x7d h31̚|,qszZq{8YzF{65PF9MJg{)A=(?e?c*ږR:Yn?@+ ( OQ-$NPF* f/QE!#Qj(<%CRz7>w?ʴN<5XWהP?>?xnNKtǚj'~UsFNcߨh:dH:Ɵ:+(Kڻň2B2]ZfR5o {?}R讻]CB3T46 \ira?k ?CDO庭Vx0B ڢM|//|8oY|+?ݳEB8[o!}Z͏Mi?aΟ@|6mCc8d~{Q!PMxZ9mHO8±IEYߋ?ùX*NxWAx~f .3rμ9c\]?JĻ|2kk97:([{k_῕O_*kFf*60?+J]3q!b(G|[' kw?)DheRSoS|}|}}E|eSeL}ɠp??hKneҬ3"@PUM[N۝?S r ʺƾby7hVăD/b۴<4}G4R6Z./o<5d ӴOiKscVW1f%IA #|w&ۏ Esk26V)p7^9$k>ƭ9Y#fjlur[^ٱ^;c-\97J(OH%k|hwy VŢo0WЫZn$`ұu/+7gqqc_+B=Oeti_n\l-Cx51IJ/ |{+&6Ц]A( +]>;[ hmR(P"(p*z((((((((((((((((((((((((((((((((((((((((((((((((((( zΗcivkݍFU^_ɮS^x%Uw%ls½,pz*~(KyzlFHˁn 8Ua"G~{ IQ,58ZDm)q]n. X7U{6??B wyb}BMwV^C]hZA:[,DL">eek/~Z9J}?}E|>,Fu=J@N<:pj֓x#㾮iV^vϫ4}z^o$ig cs=y5k?~LEԄgm2M*kK/كǷ okS#nrOʄ5i?W̭-uM\7ό$i65W)SGݝBI=LW܀-i}ei}k}ȣkMq]z,&}SmQ}?|o~0|Jk}'Pn9mbhFcۭjiܛuldMB)&ؑWƑ HQEQ)em H>sL'# cߐE{?xK¯9 Ҡ !nbu m *HmaѸ `G "Qkygw9iKiڤcl?F#U>0KnQ+vq؜~; 8=ciVޕ>ݞs{W5ᾯs7R-i%m!s$9aҀ>+q>mi#7址ne u8$W~{,ռr3 H*_|Uii\N",oS=w 7÷7gඹdޭ22 ~!x:g[(t'*,Ds,|{"-7X-;!FA3+Rqvn⼌DICuV<~"=Cĺp?v8=3@&QL#ִb-mDr0BOCsx.V?Ö|66X5l e v 88h/u.L%d93,S2~< "[xsG$٘O: P76J7Z&.uO꩟\yG(((((((((((((((((((((((((((((((((((((((((((((((({[{EJ`@ 7>ca|2t|2׷ U2`n鶖ZmkEo  `h((((((((((((((((((((((g.sV/K`|h "PŧIHRd>PPya9e@1~ښ|VD\ID `Z5+/Um/QH*><At_#_Ei:ź]%rdy##h&S$ȭ.,t<a׍À? ־ky"MΛ}! qç#~ |JӾ$d^ZJj_Z [GQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@#*aFih ͧ3kKr1.dj~ 7kD'FԮk8[߄? #(×|g$,rB\yWQ@3w6|; ljK_캞jGk_@@57B%\c/읠C1_IQ@9'dCef;/buEE{uK:5'DC}?Wl4&O6yU (_Qǃ3+cItt+:U{H8kj(((((((~)|1>"f Z c%ı/'6_|g3א];[V>] G_(zg:~ͧP^L0΁}(~ l>"Et`"FZKS^6;is ׮W?jI8$F.,W=sՕO9#|BˏYfdyx> M:WS (((eXeAwi($3{V)~O<i $Gy:'toGןٷ7^a$AAV22Gc_u^$6麄0%Y݌ڀ??1W4ws5$}FvW<1kZ!Lc̐v*hk}K`vidOxg ##?Ts݅, |z/b:/,ڶ,$SB8 >x⮱|ys&+-<ϻTIc9|ş#kQ4n#$=xpzWԱFFĊ @ Faii6pYYB10F\((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((?Gxtv{]׹1!:l_4PS ?texQ}taٮGFsh;oC.D!w%drA>Ƽ/vU|JVE0I ĉtBx`X }kJ(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((( Ap<[}|4P\0SG?>&>[gĿ utMnK]:u+%&,8>}3"t Ĺq.F]W7kFLum5t\[QF Fvwt?.G!X3qm};0}h"_4ωZ9`kpg`bPsҨ(((vTRBNSW}2PbC$U@^!au;XT~L8Tq{W uᏇi$+Y54$qnF o|>*7ˏEa*KvnCI^A) |-|97wO $1bvD zSҽf ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (QEOƿ*+tGwwu/DR:drO\wE|]mq[J/j}sv8*G_v XgQ h((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((+'ҼSZWvSVGJzEkQ@|R{1tb%iڤksx:;_{?Κ.zBf=.XyVvs qڞgXe[Cui2:=A1:'?s>d$(0r3ZM#Ǘ)j ܜ~Ƕ:Wa >az9}诜g<6ѯcY2~x:VϔG@RW+Ǻt95vU"Xy ?8;׌EXx}~Pw$? ]  Sח(WsIT,ƻ#6mFMd?$g= tceFee_:c@$;}rkO~<1[x^+=a Zb݇bE@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@{Am3}^kL[=RăeWՆ@'P?a֞,qԔQEQE翴1 6_ֽ [+pLcH;@ ̰7k 3dxόi;}8ݴQEQEW~п/>|Uf*- duWm=2KUh _~Uԏ綀:LNomiXȫ,VvȌ8IךzfJx_ޘӮ`c5i<((((((((MkZ[{{F~O_R~+uC lXYҼZ.hGd[\^&҄@Gj~|?Դi&C>zΑHMZ( _[i}ioK,0UE$xbR#Lܪ'|яހ<ƛquսRPbep}'B@|$|;J,jzc˵8+ ޾((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((]]C+ FAPş/kOl(ہ--gqwbp@{ F,u) hn&R2G_B+#C_4 wx{I#p3ϋRj鸨gT>BǫGxE}[E|{%\Eo#9U%RM8'"Sʾdn!!28Q_:Ć"QHা&m"$|&FpENh̼;#RomKPҮ"dRx¾EKAEx]:wMF667|kb!XH c·=|5>X@ך%iz>\~dw(@ W~_zbSX\je0L3MO239k((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((+Ű  $p{ |Njc-c5ﲸ\g1+/؉z!A4Q@\ks4PƊ]F GRI^k͔eQKۈ"y^(<{w]sT͒M鶰('mYӨ]2g5lb6M>%2$]v$u=h++/ 9A$RԌ״ZXCc8%󯙾~ҟaҴ-_CyvM=`*rǐAF7QOϦDїl[?/s}9LPGcq?r)0$4!غLBsھ?{g;& Y{2GퟶFb?l'Z]WHkIRH?fSm X!^\0wmdBEF{ mq|kogs>lI +vRw烓xg4*[{򤸺)p.HӷJ?j_s]h0tbrׁ_ ?fKbOYEkc0IōD#;YjxºF-j܈ zמ~s:/yKp#џ1ݹHR? A$z+9MLJUێ"lַ^o٠o*3,Z؃tր$N"pT2}>Oj XA; ʯe&}cXuma#QFrF0ڀ=}-VZ"䌈ySw _5\5YXa*~QS^miuh$8ZX`>82zДW̾>-vm0HAu+G1 󒽺ץ|y3ږ ݝshlrWLQETwy6˴-u8*Jz}SGebhOdrZKqp1!ݎ$? ^#9InlɎiw p6v9k?Z֩EY, 2(#"v8 I>= hT}!V;"+ּSkxBWpd sc 7eOg Aϰ$ei еtm>UoUv P'n*4|341.j-${D)~?ciNTw-*Xܕր:_ Q=+(7+.x +8>xoF@)|Fq8rt }+?bNAkAY'W V#xǾg5?JOc O1ԨelV΀>%2}դDbVЎ1߭z%xg|3(nlM"m]W_Mpd$BcL;~\O>F5DsA'8ʓqҀ:H%д,hTc*Tm#IlS˴-Lj"QV(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((+Vhd##}|QZaSȫ # 2R~,ݑt nsizM;^94`c.FC؂ڷkȿjqopʱͨI䌕fˀ\@|4ß|= Ik}JgE~(X~5ZC)ʑ_ |,2Sm-½PNT=ϩHB Z( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (a1_~miw᎒/&?}_~=mVCq {/&>Ӣ(NKs,rI..e9LVȌpHQOž=bFSa }xBҼE=_ٱɊt 3= |<~ I砏sMր?;u kֳC0snѫx\Ҭ<)IZDx 'WxZW+#^[)*x*{2;+^w>'΂tȖSyuv>u os&_'SM6J@ bp6`ڶB;oG5{v }Qz0@Ӳͤe XzGN5 G@e$]M0Ȋ3Kp=}JHAccia d'%a '/VwmB :vOowf(Ð|6ќgf3 #teXEQ1:n8`D5( 'hqw?v"Q0,,e6_yOsҾ0o5{=:{ I' u緾;5RF5 Q{y%}c4G¿٢}Kw<U nH |8kuX3Ik+-RʮmVk(Ŵᦗ}+HOQf6 Q^Eَohڦi% qlc` #_cW/Ώko[iϘA 1h+KPa5H1gJ1 y_UAbV[RHAv=׏((Qxºq#Eۙd{kbl|_W.Ud]p+Ew3Uku95^Q x%ml@q'}ɯI6.C}c'ފQ>W1x[y>ϻ?bXqc;|wT[k7/a; ׸.ft\0>bZ|Af A]IBW' ~Z[ikk HQ2V]J6 zx4χ?joAkVPϫc W|]ďǭ]Xbl(ܸ³kKk8$[ ~Vf6:䷵,'lmFtzr>. D|Tk&[C1m$x+;|Lo( i-486A~XvB~Q6*]$-G?w gGQSnu(I Dt>^N8Fhk:|Wi$nDFTUe?F v[%ZtMc ֛vN%@ \Þ?ɸ;[:Wxk_мik1xym,.KcfQ*7;zzu u {]֟64B+k#V;FL`Q@W!wukE]YƭN˪`9†,zp:_|3RY__G7%@@@\$\ڴQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEWUsx&/ /@#\spkoufm6htmL2 v+шG}X-{N-IϗeKWaFVX=AF+<7c >#Xv2t3F٦m=V ٧NIޤ|a{~0o<4ՌˈnXE!Ax9A[E|YsZv^$]0pdg wt9& &Ň,}g#o@Q_3hs|kb\R$WxcG܋{rpWʼn8L9 4tR) #-QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEGɟF/,l |K _yl[dQ@Q@Q@Q@Q@Q@Q@Q@~z'?ŝqQ4 Q8dqXk*Z9cũK$j 9Y3(8' zgh/m<' +Clp*d 7Vc'PEPEPEPEPEPEPEPEPHꮌ# JZ(¿x[\}cA`_wD8]v1UEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQ^AuxY_` gd#=ޓyJOþVĚVh>L<v#'5_~_$v,1n1W+_XT1NPqŒ"`rj϶^m1${ԭ0I8vs+(o-`緕Jr(e`{jj(_?^Gak.fs ;'+ĭg_3jX q_{lsJފe-8КA1s&Ocq:_z$RKwbCօn23qW?5}6NCit# =k_o@Wץ}0 ЃN~|pԼ:iPa4b$:eIqsM&iIw&L3< @TQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE,x:55 xcY-v);{^)YQ Χ(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((GuA`ԜU)}6?h,]4z.$x&VH2? Չnq*rN?ђK@EyCп M]'b=}E|~֖jOgn%t#JՃ:ܐ:ӭ?usWħ-۴r~_e$T??>Įǁo@}OSt4TU+?L#G@/r6ig@(nn~8Leq1w u|s6H0:v'ݨ+?=0?)&ٵ̘OjS\B>+gq,ZZO-dFiK,[?}6ŒjqKΠ5;gw|>qB&M_%Y~.3 "MvN?jӶG0wԥQ,? cVx3;A<4bYĵaODzZqE|zn&2úH5xt8u_ xl}|aBOR{YV–"9%i[*r=>][idnIb~ګ6o a9OI. ?;1һ/࿅|Q5i{ˉ<.eG0ܮ~_ 닇{2Nq]]u%y#Is{r53~Q ?-M[^c>:kMO?/,c׷| okH-jid?94;,f–gxM'5B^>)–sU~ ш߶V?|yXx 6񹉆y }GMm-" >\@R3(ch6`}ajڇR*\PÚחsl]3徛?Xw?ρ&tw v;ǣ&5mi/2_Pu'D|3I}~XO䆹*x'15U.e C5Ihe$` 1}MnAg3(U?+o?d?b\نL)?R9Xl7\Ҁ=O ?8hԣGx|p&K%6>a?^q,F@SXxF!Y5}:&WH `9s˃_\~%[]%u OYo!4lh YZ{zH^`fP*?v Go-?6hj+u>2 mMS>3vGt1->c@hQ_iߵ_7>s>i,M\aV~֚ Y>Vk@fƫ]zs~LG[.>?N>ױ@jOB;0 EzCbG>%#auƢ-Uhz9`\\rP#G4}o֤_~n1B?jhsI5ctր @Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@|ymiAv쁂+#  c_Tzz d!GPՏ<.?Eئ}>S16yW*@ N.>=ƕnv0һ*?B}&d[?&IDe!nXPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP_.|P.j}/T>eCs6e;p1jz+[^乏8c37#ڽRKZGҫ΃yFZj>*yAAO"w~}P?%5S~ο $\&4G/_ٿ\2񏩽h?k=Uo XN ?`SLiLWEeVp;h_#nmc}o~Qka*y$@2X*ξm3IaҴ+TaR@ ^_ƽn V°y(?PE?O" 6}HV?T񄓨!?yY}urM< h! &ܤQsW!$?zP[~I 1bյ>@ٖzKx㸯f<KYT;O6RVڌE 뭒I]GQ~⺜. m.flW E1!>hEEQEQEQEQEQEQEQEQEQE^[@ҵI6W>L<6;;S׫~Ȟ.oZm.cMcNb<Ċ3O>7;&D2, `qHH8=_x'O[jRqvќu$s@__~& \ܦk\nVK~ $IJ1Ŷ24%_TĢňCʾ><e/GO*9d*oTIrGeC}E|N?Æ[Lj?:m\xar? W۔Pگȋ:x+E_Ī괟;+kc_p@'OEU!'i"n/ڧ |YJ*cq֕Q "Uqb32\dǁc2osO D9Ƴg<2~ eqcQ@_LO`xȫI<I<=^=k(xic"Ēz"A?Σ>zgo{ĺ;? u-皹.e 1Ҩ->RA 0g%_|[ śgLjK` αC%.3Us_Dj"8ßO)I?^}8jڳ6l}$sHߘ*>24ShAZp2Sx^OZfiW1!7EfĂzur?JcmW\r Wŷ?, p #T;f"3V7QuXPO2Aƀ>Ǣ8ѿhq%w#|+U?h+i RA}Ìh+EGrEΏG;>XA}E|J%$O}ԟkn|Lc0(j+[{/3ѵ̈́{7~LsUo|+Bʲ?cGCY@(B@$U.K DgLkdGVjN6k{j\KSBRowԗ n%~8!obk*oÛ& $E&WM/j~,&~t$ٙt~>y޺ƅ O\g4?< >`+6uO$hG2;$?uq~? `Iڶ0H {I8C6 y_}g_Fy!qF+ž ~Ϟ/|G-kJ|5[wc3}1K4+֥ 6|XL9 W(bA9+Vٳ,=${%}23Gr1:jK?6 d[[5e)xnB?!Qh>4|;F,Ӊd~ oؑk4-Vtm[lKk=ff?ʶ6[^ }boPphO#fRhO?21 7\}L]kZf<*ԤugPOP~sC3H}ŅX߲. .LDW(?U" \}G̜o=͍\2?//35@r_ X 6 N? |J"݌يPY?N_SR9*Juo@:GI.8WRʹ]CL5:47,,u{?X- kҰŷCkg9a;?ګ_׼ڕjğ8}'7++KJ1U%}ƀ<]5?H5iZɂ/<$E?{|/n AqT~кq(ΤZv~xTSc.G///ש<2u>R1k=?z [nTÊOڷF$_vhA~W˯ٗza䯸ܤ~tWM up#,[#(] y?!he8v`}kSsk> C1\&EмP@#1)M{Oܮc~4H"*\̰x*& 1s~:"\ƫ&llȰO"エ:omdi\e05|mIZ7դm-7؝ѷ97R|6O?Nf$O~+X.uH&@~_п p[)2coj_b[$mis ЏjX՞.lhcĒ~u4<+d)׆`~eh+j#JنE]|q:r[Z@Ex_×l5cլfC|3q 3@Ep\[M&حM;);#>tT^..`}R@(((_xNbP* #cL 1S[UYOCkiv,ܺ{c77?>([KۋYlaRRvDAHMu:K#Λ%mCX7dge/kQjdA&9H -7< /+kZ- ҾȢ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((+]as3Ǖ&_B˟%ԩeEoK%ܬ vf xuQ yѼ힬]^\ <`&ձO ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (34"Fa/ϯ 5]oGhkgw$Hn9wk_~Mg_b#[y?ҵ}->b%ڑi|"Ae?ֵQEQEQEQE|E|6/Zqo O*~ſ~^f*wÙ Ş{!M{%w啞=C0q֠oOu4'iWtP !ޑOKFUKB+_YQ@K+xԼ;&:q0'f~aWq\_s@GCƔdu'adS>8bH? _xQ@êm]in>/|`/5]R%;:0?޴@ Yҟ [>嗬٨fj_7Zh[I .t sIlVܮ$~3? VP>k=]:55鿵%5xds>ۙk\7l|~>YW_Wɛ1Zj;?h24IzT}q)?|_'5]u7neiQ8=1_OvmjbW7E8#`$~5򏆼4,|5st3\ދpmy>߱_*FY,@)?>̻8?|u%꪿*Ij"Oj="v/4/(:>'x#>P6]?žc4HOAy-,xE Uakmcl? f Bw ݼz*J'-2'щ3f!VSwV$;p/\AAs@wQ_"0?)OSFNx1ծJ.BH4w"FՈ52هͬ'3E^?h_L2|Dl.uo<[66 wtW'H^p>)Ѧ-d+Yq\@s@EQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEW3~k/O N$7b ?ţ|<𽜿-X갨?ʺ*'8 /䢭PEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP_Jd'Jz]Os[2Z[F`X4^cKA5[5BhC%QEQEQEQEQEp$OkqLJm'bRxB}xrh>9IoG?#SM zg8{%7>p%-ɾ3$( 0{  V:h{mB(b:JH8n:r|mƺ+!F<~3nK*{r?swD 16~18,pvăWԴP$\~y]cItYW5Fox?BFPYḏz-Gqc֍e\cQ?X@uꗟ]/4 0L[?Rg?#~ ڕjD*Ix=]!~ҷenv_ƫg/?ý}Q qVngW__ǃn9u2; ?c&i[gaT纀;E|5?یEjij@>(9 f"3ߨU+&A? s~S?>Ѽu]ja.Bqcκ,gk;O=gB{,gEY7r}" (J+ot+a8k]JbAZ *P;&O>䢾 <=xxH'ܵVoA(qeqh+=񽛨Ԡ5t&&'u61jΊN4gYU+^?ڷn'j oSԪ K7?k_NE?32h(2◁.Xf1¯ڔ]Tݼ "#@h((((((((((((((((((((((((((((((((f f ( ( ( ( ( ( ( ( տ|BiϦ^FE}_~Ԗ~Ҵ?,aP qEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP_U+◉hGa.0 FhlOD6lӨ((((((O wSƒk#eaAO>]5qTqE6IKL#u9sqc~|]_n]Q^^i!Mc'8G 1ݕNig[HbXI/9x ?Cֈngdl7]{9~7~ kvw jjDC'o8[ ׼&ti5SwO#ڀ>]C! 29REPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPL@IR@5N#Op^HC;Xg3*GWQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQESe9Wl3N3.b`]=gӼ+i8WQ:>~i. Ww=IA.hĴF0u/ jw 0O[wIk\y[?'Û+/,Tܳ5aXwÛqyjOCoq4vmyq#^hYfvہѢ_@åíZ(T`*~0x:HկHRvj]-Өnz>Cпk Z8 L>& JO<),.lYr?08(+ʴΦ̿ۦ.#wsx!ɡz}}EnQYw~",ݒWӠuଗ(~lK1[=cNQ1 kQMDwD"@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ D**F 6pCrrNIzԔPEPEPEPEPEPEPEPEP_hdt[ٯ+mE /jM}EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP_ o7-!a;uW.?mi[9Bv Fq_ Yv̭ )*nF=(m@UtQ@Q@Q@Q@Q@Q@Q@Q@Q@ (牢4'du |mQJ9l~O_L@ӭ6_xm"#kyŗ$r>Sۧ}0S76vVy{FӼVo c'85򶳡xs4:w ֝yD66 *ga烃ܴW||I1[ r&N:dwOEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP? 'w":zz!fs^J}uKAޠ((((((((((((((((((((((((((]JSih_U{ZS.}R0dk83\~=6{v~Ow*]z?e-{hZl%@=ʟē\?e_Y46.&'V{}bG־ˢ?2%bvI|=#Rj kqyz.ޖE{E~fsLg#n]GM=Ф.UAA_4P|L QV8ΝP)߷w*VU=0vQxXmT|13~*p?* u /C_H^xײ~&t,ۚ'¯Hoh`٢!@5]GwHcP:m?jI"Γ NWN4nnd@>I_ҸCΟ: zH? @|l{ۤxԷ>C/v+/H,|UNv8Y5(f6^0FRN]<"CW;~ʞ0b,5} $G@f,s.G_U9 K%xJ~tx U+?|S<[Y 48P@?B(OگVGoHwX~h'D΁N%? :<=:QW wLa]խpmfhG)EKEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP_"o'U|oܭFhҀ>Ȣ((((((((((((((((((((O"bB8(7CBԃ-"9JG2(=vo(iǪڹ ߶v8bAuN L`2pÅǦ<5=+PⴻwTʎ~Q_|[]JS֏}U3Pkh:xSwog,IkxyYۈ~EPEWl`!TCb ( ( ( ( ( ( ( iz=m՜RHPt5nyX&3* dѺq7`g&n"|~Xl4d*r_ î|+.4OZ %53nuSЂ9״hw݅D?#A4袊(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((sዖ {Y&֢?!=}@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@W|LO\ -k l6XcF-=<(huڿēqM[se/K=2>ۢ[_Qy\Kiݺٍ@QY@l2IKQgNukiKZ Stȏ\Z"D箾x>gtUXP~n_B )?j'_ᓍ=pb?5uËv]&ĜEy( 5{06ͩ7JO>@-?jK:^`P$mI:_S_EUVP}YzH"䢀;}?q}s䦽g|FZ۷rBxH?+dC]VOTG|fhߏU{N59|;ܸ/v6k湿dŶ/k/S|Q5&"Ҁ>ņa9, I_ _~͟4՘ӮNUm.ȓܪ?ZŹ~4h2l9G \PҀ>G}Z|dEYa+~f};,QhEÎ IZ>Ƣww_>no * Ì~=qTQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEW*lTӞ=r}_Xl)Puyx>TQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQ_'|ƺ|4l,B1$1Ub~lq>X__ CtЏ~$j+}n\:9 0u?$`i>/v^P5HN+.d<\rOŀœ#|)'$Oh/gᶻ֝fܢU!#?:WuzƉ>D%**(y:?'e 3@^;P,4/cJ[Hk$ܨےNIRg`m޹ Z6:Z3 dcoS'u ~ϟt7YJR.ڄP} /@5k?-J?'Z\s_!][ +ԼV@#4x[Hgm/Ú= ƤHSP~p,γ/Z錄E=;n@G .dBi3ھT+h)Pn1;qIh(((((((((1|$Ѿ%i?uVrl#LqC־M[|%HCP\J9~G_d# (|mxkZ+-I`r<$QzpA]=|+ x|}_I>3˨іL3/@x3̹w@PהWŗWxz5fcҨOO,_Zs@p_ ~ҟ\-ޟv5=&%?1习z+⦰,ZTpsQIF|l@W5ߊ~&iQ/5Ƙ{Q_;眷7qI.dgxC;? iN =ŲM)vo<V5u* Vf sm R<'lǎ u^Tg856্#&IloS!,m uǷ%Y>* H-Rv7Q{0<:P3Ľ>KyKZ@Y8#%zd #A>_Ff?{ݙ`N|n@d܁X: i%*۾8|g#r=((((((((((((((((((((((((((((((+h.Wm1ʾ_~S.+(ͺܢv}__O~ACַ2Xٿ )h^ѮOiHwܞ滊lhƱ ;N((((((((((((((((7_+u$?"'o킷'`ĤF>آ(((((((((((((((((ƞ*ҼzvUfT.Ř'5nչ7$w <2K1^-lWԴ? Go_R? ~i:W _E]Ca8h |=/u-]srZ۬Jg%Pr\+( ( ( ( ( (>GZՕhA}]}JJj"_s%\GucKeRr@{IEPEPEPEPEPEPEPEPEPEP]WNմۋ J;;1 1_iA|.գn/_EK18 J7}7w VK(tP!$)~%Hu8mp'ͽc瞣_z+/~-ƌΚٷ$Jd^wG5^<m-T:9y4w_ ]OWsC`ھc'[|޺XkHQnE n9zuѠ /Heu#|=۞e+uxXg`gs^֮|;?W$\Y̲ TB2h_:ĭR~{:RE$h(YǞx*+;"v8 I>Bb?>n~|IeW1B\9vZg9HW؀~Ή&D|RCcV]fnq6"׺Q@Stˡy't|.$C c1]ɏ&s5<5nEUK(( M*Qxo焯 [B0"U֭/VS}mqCZth^д/X4T & '=@pQ\Oj~-}kzY_]]Le 0Z((((((((( ɯCs$X1O ?!GLn+k>ĺl { {W<'GҼW2?Z~ H4K'Xh"D?_oEPEP~О O|3mX؃ir"7ԃڼ,<־)|5#f wʟpWL<K>#5E;%O@vEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEdm.Ǚ>2q2ݴW?"=mQ_(O? Ba򭴹2݃4?h((((((((((((((((+ߏ-4ө?=Մ؄칯/Ai B~@nEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEƯitLϨY"0?oξq/PinKe|,Ѱ9lr01װxO>-^*oZ<0#S ns׎ W/}2hVrs$}I+Z[_wR^XyLV-NF# }([|֤ѵ{+4ԐKluo5><~xnCMW{z0^zd\>.GL,2[ږLSEdbU=5fOi ~,31T7fP+S4mJx-.I0eu# :( ( ( ( ( (f=k+ZfKwtp?xZFS?n.ߒ[M?4IaX.&9*( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( L1qXc3_AW]'>5>$]jVIe hbO٣Mu_+OT Mf6ZzRٛ<ťsw֯[eDzS{ Ƴ?goVZ7 W?u'U ?2ugF'm@w˽ϮTcъ(#;F^X*eŶ16Ґ޿ο@(?|M@?elu_mqɥ^{L $DV_FT.iI8l?k&iSyI~-|Pް^&b;{? :_=_撺}ARVR6@ _>$<miC9զpq䁑IdEx20YNAh|NgZ,19;T`dn ( ( >#^.iqucjTc砮DY#dC#H '=\fryy5}ř dsu_`ZV?~Y1f) :i?-q@aV1D cv?kks_aQ? V]RLvgwq}f8/ݜv0cκ=;lVM֭ (G)_d 2}?wu'y'h57Ahr|Uhr $.?dx˓SQ7U?]/Z=>4|;tPB[hw kL'u5%XŧY]c0OqZʓ/ĸsx^R?ٺ?A zZĝ=Y0|H+j+]⥣fI ;mt~B?D证BhŤjjy~(gO2[?6ǣh,iЪKY]9qڧƑHk4Q?*N.\}6zw̟ kX~ֺ{ L)Т|b? 9K8'~QEW_on~ AڄiG%H=+~4V> 1{ WijR\CP&aI\K|#^t?i 2\WJá%pkOQC~_XC8#1/Bi GY(lG}EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP~ZV?n?.`?kM+:RiAbUũH^Я,2 sz/byBm{n&~:zQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@|Ad_zd-?}_ۖ;t'kfbFǫ(?>IalD=QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEWUO/f8ϠU$*\ř _ <`Ha/@ɚtzkIS_v߱|{+߷4"?}@Q@Q@Q@Q@Q@# GLR@7vYmsx]U`pG+i2,2+ᯊ6ӗpHڗ@8;{O_zҀ ( ( ( ( ( ( ( ( ( ( ( lȪ+ PiPe_qݒ͈ɚI( uVg|wS <@ Ar0|3 1P#u$ӣY#X~f8SzO_G|qMsƝ+~C?Ҿl'UaM?pd\B>ˢ((((?hk'Ol,g[쪠*O|]_[O 6G"J;nMx547/d}]K g5 ܿ RcŬA {(+xyM[dSBq$1%ODg 53!RMFX͒!'ǹL}uPc>? Z5:;O5uP6mh UKc'HS׊#_ 񼎰gy yW :[ч@?FhOxNluMIp}VQEQEQEC="x":5?Ԛgd^[z}빢<ᬃn!rcN/ gZV^E|/B5^L\%+i^,^2|B׾+(cre1r˯?7so\3OeHqtYֿEhW«εِD-؁]P]Ox=Q_p:+:fs0N@r<}AE|~ZqxgN~w~LO~E,c,mpGPYT~2ãe$[{rٗLcھs?xj4s"yG% :`w^(?R.-<7p &g`7p:dg޾Ծ1nudqt3=߅?Z ( ( ( (Q݂Nm%1Z}2+/GMkkI-m2rz~F-U5/j3X^/V# a@TWaW]~^g?L}+* >ĸ}Ex4_[NkZCĈ+ȉe!#7?h}Gs Ku bѰrIPuW] E P?)[ghH3m }$w5mţKm?Az/ᛠ-ʵ#[gs'Ɵ]G\%w"%'GDʄ d !czם^|Jm[mۤP 2V-sI/{CMd,۔eO& ;E[QKc8'N; s- Iަb@ 3`(@ĭsDU MȮѦA*8uK? ^3HRF7@e|9G~~ڔ:ƛH-d;;)€@8|r+*((((((((((((((((((((((((((((( #MvYLv?/_V|:ѓtEHI[h$dğe5{bh*}_~"EEUQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEo91]*ʾS<}E߆|9IHAZ57ZGV3§8]QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEfMҮW]q> A?bHUq ʍ Ij\ i}y@Q@Q@Q@Q@Q@Q@|])~Һ"=Zwv<*`2T;ߵתLs{yj?|/{9k.g#訢(((((((((((+mG!l51l+ڏ?9(N~ \ѭ^^3#Ŋwpf(((((( :q'>)bm^%#lr,?5Oʾ1?,kl3@ă{iqn>|_n{jVx綒&S2G_~ϳ㏅sA@QEQEQEQEx?g=kIZ3d|c3}:=kbsP~EH-! YF~k~.9~:\Ꭿsƕ5h>@2!@{Ͼ3q]6DY#xe ?,./n[W.}I7};q<ҰqD.>ۙG_D~՟|G໏Y[(וN ˒޻A,>wOٷnSz qӂ uGv_x{TE Ybd2A |x|CxbYLW d'(ߏB+㟆v2nqyk (*cI7Om`Ԭ הv|Q±m%Kpx+oLmJuhp=HRk ^ ##`qr:_On,-Bd\ЂQEQEQEQEQEW ͙>x_GF]r?]Xtӄ_Vq@|w4~ `_}_ ~Ȅ<9E}@Q@Q@Q@Q@ciy;cF > ^Ɵ4MSyR2kHL3}F4n3@ꎭ+Oّ|r??BvݷAPK4|CAd>x?M f[8[s5 >&Fo G7vI_P-eEmpƣ ^{TkxH_y@ߦ\dԩTԍOZdfI|W kk7 ~,#(e! ??65 'bM@o-ӽ4k/>akdp~5?\ .ZfP9O |o\۾@2$\zmw&_3yΤM~2^ľrB`r0}ڀ;;kLɪݰ$6'*+'Gs\ao*x_WM%i% gy]F&Ƴg>%i";|*AtwZWLzq5eOX?j?e?ET~9YڨМU#UxCx?ʞ?"3G2+O?ڋw?筪2ٵ̹ZIc1?$s> _M=cLr'OiZ];mBo*#zA4 }:_ !|c6uOGHKtk7l$~_?PcK_y2@di擩麥{ZngOvRZV,{{{)? ~3Z..?瞧R4W7:OƝ_/G[BGS|hє4~,3wk#4>B|JO%^8}?:괟گŰMMF"Y!|Co/HxuG)Wkd.ه?Zz+Ҿ1|=ut!?kEkJG4AOCkr$Q@c_=r9$t{خ#}OA6H;~;q@G]ON]3T*/9#'0ҽt'<޳'?_^?x{=ODZ ( ( ( ( ( (>A"Hqk,O':? _E| HNpэ~MxlZ G%dc o=Fh}$PQEQEQEQEQEQEQEQEQEQEQEQEWϟ=#虫:?ma!2~b o O"79W_,]pQPgr(((((( F%ίe9C_+~?>̢(((((((((((((((((((((((((((((((*bOA_7|Ct+Qj65V qJ1 xH4c+ŝ7vra[0is:\=F93!]mq =( -1@0{)_s?_tWW;ѧdC^?bEh((((($]{+vcw,1\ O_U?:ޝŝ H@@q gx 67)OЄ9?%c2wYhkC h?pp+g?M1?y.5 qs\io:oE4g*a2PB@e@iYG8o_S?ٞ1~rO~&޺۶<[\I@YroyWֿ?|c^d\QY*wF30:S54 o./v w6#ŏ+ྱj~5)])CI:5|-a,g&;߆%T*`H{"_LKŚNߍS|ѓS@5/ 0nlQx.X*u#-}G~~(|9CDF:6wܲ$z%vH W~Հ: -^] f lzN33GѭLG_;~WSmFXgH*FG?4QE~wк|zgƏ[ȋ!\SH|}j^$k&I7xnᕹM7zmmא$$.(7ß4w[NidgG@ϜC^?ڲotKeSqnfq1k|B>"j: LKt$MppH#@W ⯄wX䶆dx :h(\am:f5+}5M),$8 X( 'nadԏ\Vh Ahp:Ɵ 8bY}_U_G9hH1[?x܅9kb ( ( ( (ADz]ޣ{m2#ܡZ8|\[leyg#\ q,sƽ/ Ƶmk\ypbMAVA<W:mocami>WRKM9;wtO0oi(Vt@R _ue`c-#RdqEqK 2: |k\xþtSAInkT,g`E/Pt8$soi~numv;; u?OM|+CŚƿvwi2Ȥ$ $@Lҟ?/߄-Eǰ_)c}jKg+HͺF<zjhm ~\…S9ȹƼ7!$hTC0i$g^{ g_j#a]72qȥz|+O|k𫃌<Nր?C((((+w5O]6]фM]EcxH:!I e5qT}(_ΰß\F?_dK@Q@Q@W>X&0|,P^HC {.W<6!aG@ _~cxwJ:~1 $?u_^-'uF s lrն__û{%k+BIxJ cxh"ExG#c8ݴk%k$vמ`a)VmRC=0;)x u{nu]6V,11F^ @Esi:~mvqO?5xtjAv;9* TňA9Α0Gtonns<x_ϊ_žv)qJ2sPZ=ZUѴ؛ZbMWN2ӣ+[^߇w5Dx?,CӃ5j߳oësva7ҸNP|?KvEK0?lnƾ>*m}Qg0<=?Uey\Z_?ˆ">5.YX07OWMOs6:-%Ӝ#yךDA|W*LJw/ydn?OW 8|?H4{PtQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQA II:΋cAyp6] ~||u/'5fmp2'8xDi0ͣ+H&ߧמ@^)_-~%Ԯ=84yvcg!F11@^ jbm@'I>vy {O_6ͯh!= xLO#d{%~ \7P¦ Cz ftҺk"²8ʦh&Z0(((((??i}]6$lW﫨¿@_ۂ8AkEm4,rHy<CCGmWv ?kjVk:qX&rZ(2tk?+03\$SySIV6;Iz->"GLzp>s'ӊb ! (4!ĺ͜.Cn9dj9^1|3_XGmu^]1;`39:WUTt 0)h?|QeJ-ª evm>8xw8~~`WpIlm}~`חנapUѺ{@tS '99I:b2)[j5x, 钭|CB FcS_:\j0%ŝm8ʺ p ~_q%!dq#A9a}I4?xkSO״ ɌPz2+|[W Avh ^? xWNmx"-W`v!3n:~մ-cNUu,tqא}k?ŝԍs]HXe~珻 ~#ލc]WzfeoklF`8QԜq_YvzҤ32~g9#!TAڽ9fa1`7!_b^ŷ,4J#7 $Srп~,{ _5-[4ͷ18nzo3v5,%pj12w98E}oυN ^o~_# ί xi-FNӎ.z֟[ Y -A^!_K~-w<+Yv#kwopsH&<T:f-D-! s#mh4)|E-29~0~K~u!3ͧOqcq axf8O'?&|~~e\>&[|D]%ZV}>U=b E#Ɛ+Lr"k_كSz_EkKV TQ؎Y漫෍dUc[T f:SO!F' tHg 9*O9^]/j:LqnV('j8IV,_F sqʽ%>j+ѴkTtpĤ{d>:?oCk$懨GauHw |gzy<_e{%Ur[eʄt c'8>%Z$\ TS'Z/A_U#c|w.۟ ?5ehڳ \CV(剏ҷ`<@ޥ/8z>VslѮ#[cXN7{2'\o_|R!ޫs7#\(O1zf. BeKvDwQּ7+m*\(Gz8@m{fR;MHG=Շ|dr >}i ɫm-8q23wY5axMP-P<\.#qz꟰rg>YV(`r8 z[*h^ݫyf_,pYUyapH8<$q V^կ0Q cq >h["^HTJ93@,_,ej}b=kO tP!aeO8!Aul oicEQȯuSkl k~5^;m+A(8ldp4'Bz?-F\*"_}ď@98> y&,ltMՃ\G&T5|A/vYh̓Ky6SU*cI Á99_?5 }d%q"Ed~։O#մl3[1zGχgJ iB,788{b/ŧnm V;?{'f<9sKkRK),1`R0ێ{ cr:c5îX kOߵ|<<1,f߷ql|WҴ_Z<ֳI$MU$\b?u^A7;\$crg~z€>UxXS 55|][Ԯ :f%,`n  WiM;Si U"m]Lِ0C90=}Ey?#>YEi}iqi9Vܹ$ 8L?lcƺD%aU8"RNq'Rk/B\#G59 ݘ$пC;Xn< 3|'q$iwCY~_Y($PLJ.x_&FH>!c,'NE,s x]FS_5څ ~hSl`i'״h|[,"vZ E~Q_ypxVe77ɭX>>|LaW~Z]jAN OUbi됻 zPƙ(( ( ( (?=i+o|mL`F٫/_ǪxKD!$ucHCF:G̳|&~P((((((W[ow37WB" |U6߇$Ud5gx~(yqwFc C«ٛ^5Y\}q/ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( O'A׵׊׉6^^H99/O]y*k¥s9*(((((((((((((((+#)awlֽct~ͨXf+1Xi3b((amk4*$3@\oz=.{O#}s2l"FFGzC]}QEQExzCukrO  h2su1f8h-=vƼ"w."FcAh( ( ( ( ( ( ( ( ( ( ( ( ]l'+XůOFb!)>)F?9_NzrWPEPEPEPEPEPEPFAd-?r ?}_~ɶ?ٟw[2zƹ(( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (;oÿ-v2Y<:t < 3O v>%״ wzt}V$9$?>M?'Hă:~05xO<=>xv AaXCPʿ '$z L#1E}YhG0I=>e#(3]uZj֦Sf@ܫl)yIGz3Sgr5[Vmo&hN3ѹSZz|BkZuhU HBk$:I >((((tCyX@ iITgk7Uk?6 忾P['/]v--ݲ-Yl~e:+ٔE$zgھ(((7>1 -V S0cݏ¿HЯtov/ڭ?vUe 6AjڞyۢV[io  t{+\y-dxc2 RFdwsq <0- ?#@EyBп  7V{5ŸJ G<߯ze|ju/]x:5X )t@/?0в=oP8|a~@5ꭎFFq\,V^?Eo_.6@5ii]Rl;okE]rzZ( ( ߧ/{QF2G# ~> >O}&t˨R& P4ۍCFBdѤ@7(k=oH2};YpNհr8=،gp\5mZ4$p=:u(((mvÓl}\?O?{pn~ xil1W܁~>bר~ɥ=(7Aqp{ Q@Q@Q@>.cmiZ Y-2Szݎq_$~,mD>Gb9ZVc`ٔPׇ͗<#Wƻs 5ck"#XF(ſzĚvdIt>8#Lk WM"?-m+x^_ {,+ MY\`pE~x|GO1.;# MH,ӌ.+Sxf+Io>q+s'=8 O-oNH Ɂ>`/$u_7hg6:#Tt-'-'º,Ve#rǻ1{ɠ <6.SV@ #Sp+ںfʙ&C5r?J^KO9W_oWW6#?W:U9u6R#G~TN S`u-L#L~`ֵ7Ï1x;Äd?M#|6;c> B?Z(ON !i Ad<溪(O鏱|2ޯ}麍d b;,eD.O Z|/o.ZB3#02X}s@EoUЬ>ӥm oV@Of'*W[Csy37Z㳤` e9$'j_G'I$xVRNO6mV}.0( Y G@Z28& * d? E¯c%k1\v(?>e> L'I^KOP)TQEQEW߶OnFKb'dkĿk w{9>nn㇇B_F$8gqso%~EP7 ]W↿gwsylT42;2á'Y]/ SB݂U1+kx9%ݲt쓐I pBO5?ijq/6_ߏր:__巏otBr1EԄ 6 0qҾ<7~ۛo5 #*W<3Opgy<9ůx.n=$ȑgr@@N ( ( ( (>%mZ֓OD}O\/@+?z$_jQVf'-cN!@Q@Q@Q@Q@Q@Q@|o#Ɛc-6{(+/bG F"IP:]qP]QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEߵBOzs۰?{ xi/WR_sn@W ĺ#k C6z((((((,M&Br c.&E$o/g gS躆w5gwUd8( ( ( ( ( ( ( ( ( *d|0c|~xO#? e`k^3^HXk+{h+j(((?m#cǩ_]C 0.}a_|z_-KO+k;\&lrIǫϮO}o]2_I.-%NDžn*_{61^xR ˌyPJWm 2;+ќ> jZo_fM%>d&b r?A$88/e iFB9gfbN: @QEx#'1"  +b)xq0A+m`W'"j?amSw @Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@x$J_"z[f/=j~ [ }8^^% v,v?{mQEQEQEgW4;[Ree O+FNs=kᇏW⻆H+ؐF y틯G-M+ڵqa#"F>ƴd ]Q;/$E?@EPEP_~LG7`<}_~`Nxw Q@Q@Q@Q@Q@Q@Q@5#:Í}:ԴQEQEQHYA !t%\޿_Ρ6%(Ofp(z*%S(J)"9º9 0$v-Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@#ƌ0TQp{~2< +omˍUK rM{¨A ^??Go0dÐV J|!@&Wm cs_y@G&wR3~e2j.\j Qm">;|LN{X Jx4oYqL{xd]C/(">>$d-?ޒheS_vKe]= 3O^ j/orYߺ8<"& BsZW|[}Q/o& &#r2+j( Jg { 5ؑvzW(ҼghwA֭mJI۝rzwڊ(((+<*~d^-c!VڼF=z,׎h̿1Qwǣ^43oO(C-h?kI=&GC??A?'(WLGàt͟Pwǀ.4#._n@wǤ>>O?@C?Ku}4Qznʿ$Zc=`cehh>oUz34,A'(RۖLDԀ&wm|[0'?y3u~r[*(OP |2fppvouh?eO}3J vHkC>Ԥj>{ĭsz_z?h'u?Q@c:Լ9A&o,s}+[A֬qT?#E SsidUCZdm$(OM7Ҁ=Jm<\ e0BWc|Evr_Xw:h]?uw$z`|bm_p`=Ekx9?ȍOoStyjpM+-]{pQڞWCRoĽZk$ ;ߍ_tߺ/CpvZEb:߱#[[rjVwMYǻ~6P[R=; ftm>Mm eG ~N:?mDMOѴ4$a$Z Fc̪ȒQ>sƩGś- :²Jţf$98s舋*"Ut(((M͕4mUP3=ڽ>x Þ;ĺr]aŜgk@8( j&Ԥ.˃ZŝosK2>S㟇qlKtNg}2o*^:;L>#YVwAml VuGƺφﴹ+J pHj/w V' q^=I|]z%Q@Q@Q@Q@Q@Q@|CeM[1K?fcO}oF=kWBx=.fJ(((((((((((((((((Xw~KiI" $al(SUW!|#+((((2| [sWbb)s¢O2kj_rңiZ!0&$#q~Lv}p9 ^"&h=5ԬJŸd'8UG/t{OVF%1OR{^c1"¿ 5 &C1=?&z ( ( ("EP#U$`(Z+?4/iOj]G!B;`k(((((?^2# u|a} E}{@Q@Q@Q@">={B*)6CDfu|/u|7EC,veϖNIԓ$Vƾ4 [x@7FW}CQ8ψ֦ZCϘG^N,|3n]+[PВ7vuAkym4YPEPB3yˮX_Pf/n6sh{7k$ϪC #%5~׉nR}袊(((((((((((+?mo%W׿׀~K4 E' ?%×,'&?5?9o?O^@Q@Q@Q@#mOM :==$+;_ l&^B[@5_ RPO쫭fFsjU6o :\w[Z2 g,qPI\Ǐu Lķmo1glҼOiRZMi}v"9X-Wi+}]N{S~=F?C k jIx MI#qXpkdփ׊uFH,K4[[pi#` rOzWҴWǿGxj(>o}_tgCԂ|?((((((QHzdb?/aH5oc|?B$z|kN/w!ļ^͞FG{~2xfP٦rʡr- mpo][Kl=-$[D(OM>#3k(e< E]O#?k8}>U_ܚ5q@~nGZEZ@ɟGgPx)E\F>b!닟x:\ONmc^yxzbx@"]iCi:ޝx+#[ z eIg5ZZB&?DhS>5, Yb=F*ƿG+eAg 1?RIh>'M$\ 'mS, *ֽH]JgǟhZ+Oq;NZ/\UUx(ӿj?]N#,&FQHopHMuk*5WonZF_i+;qԾ p7cϵ{O,סVzC% }iPEx?iOgwi627P H  9%yÚ=YN̎Pp1ЃAzA :?/.d P3mm pqѓ8k?hoMdž| u42G!KRN2 q\|z?^㻈5m<:ĉ;)=X t/pk$8`'mZ=(:'7­Γv0GubR!=B؞=9 {Jec}XYrI&((((((((((((((((((((((((((((Οetnm-SI?fᗁ!IKГW_Eyoe(c*~?g?']""}S~Pvuv[Z@8B(TQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ (Uot:77epЂ9P'i6^Z 92S>.?|>xtsu4Pso'OB V_ FF`U?|([{bG#_b >yY): mxH'*#~>P=M!Bđ@ʊY*6RaOgSw|*~Bψ+$F%<.~]xG R-ẉsW_jXM#rBfG"α.2Di+b?c.mCtkx47Hݗr23쨿ᢾnOD:BIUG*?dShϊwѱ^O'uf_) bu2~ɺ?6Ƣ_;?-$}#J>ƽ+},n?t7I]T^p? q&ĺge،3A F{2 uN߇=0%DP dqqAvg? 2c]{}E; v 8O΀ (9߈~)W5OBeo)N 0E\0<;Px_KrK}m F6Dm?z{1h//aدpC#~  Ӽ{[w;E"pGE~Xjlm)x-AfI}q@Q@Q@Q@Q@Q@q@? a\]qg/@هZ?q\eOk?m#!opۍ3[XbVfF]FI v3 e d;K a; tFBrd92 ַOC< }5.Wg!$9%'={9ѵ#H&(((((((oٷKw봟%& Sn,i+huyWޔQEQEQEQEQEQEQEQEQEQEQEQEU{KX&FՊ(|Dbc;r+gZt3l`5?vbZOIXfCq%m*e>>ajJO@?wDc֗k#SGt2hg8ӃD!_u@4ϿU.VE'e2}r Pߵ//ڊ?#HsV?Fk?yis$:tM埳?_kO<[eNUpp:K VzgV̧2E<T*vl,(,`X5 `(((((((((((((((((((((((((((((((((((((((((+]gt c\t:I(WI6:168|Q!mh9溝>RNE A A 8",QEQEQEQEQEQEQEQEQEQEEq$3WP^I7 ( lҦ(((((((kH1"B3Y76rGGbhF~8SrRwq=1[^E|w)E}Yסcxι%1+&ӹ݄U]֣'0?AzWOO|EiʳI# eB}cEQEQEQEQEQEQEG:,"yNW5~}jmi\AyQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@pmEDO_okkm8Md &cR10_7#jҢ} Q7c^_;[ꅿ8c " ( ( ( ( * lWVn}/|^hl&FWpnaNxN9'Yi?&W;+@yk/jj8-mp9x>%x.-l-:y\K9šH<-!ip?\8 Z( ke{Ox<&p@3f$?rVwL%d E~jx|Om#wys1ȬJR [ڦ?ڵ)P-c@NI9]_CSC*U'%C?/@]αi_ ~xN #ge,i%vV9_@(oᏂ<-OT-Wrں;5@bKGF}$ 8ZR ( ( ( ( ( ~>Hc9Y21 D?Zv%%e\5?A<客L}_LPEPEPEPH"2HVz_??~"]4;{("D~YpL'z5kˆʺѮ?aA=:--nH钠_./> ϟuA92HD)3v:Tqً( U6+dqgR ωy:t9VfgBDU=2{/4 )h>meDŽ<9fv#giϪ\.!G ݻ^9Bkdq]6@9Ch袊(((((((((((+ƿkt/[Pa.`ZZں9݇Q@ǝxIӗu쐬Р\hd=fϽ3Tdr][IcpU1*:ҟ6, `I}V*9$٧_&Q9y lW,x2ֽ I/O ]&^4bex-9q@U>tѰ<4dHskZ9?m 0~yeGA.R D~"S'y8Hk.RW:~q4Q@Q@Q@Q@Q@Q@T746W$l|}Cׇ& q{,ܲ9ok>f2|p^IC+ ( ( ( * ko//<7 c}QҀ$((((((((((((((Ͼ?_?k8TLa,Z&`}k?Q_` }iqn1 s~.kYG8kٍrߜ~v((((((((((((((((((((((((((((((((((((((((ڕaq}\EminIf(I=*|kxk?rHڧسg@sּQ-ޕfm+CߴO+sp#%NAx|mFk6vyHPK(}q']?[7^/K'6vǔV2Ԍ={^8P7cc7d~ !^ewx࿋meK2*\_(~Ğ$qubG_(ԡ];X9}_@Q@Q@|aGM8 &f`s;\z̞]+mqI-eX'ߐ0~m5U 5)'RMXfO;z}s}!"jM`M-t\gvWU}:MlbXm-Xa"GU(((((+qwŸHy> \ naJ|Vq-^(((htFׇ52_Gvé_K,l ^9@g߀'1ij\ y#„; z%wq'L"l쬢Gb4\^k>HΛ;D5{ ?Ѽ-|ř@lOUr,`YI GsڼJxRuFmGOMtq:(u x> |(ޝ\i]fy!}mdi?gzUƜn4ppG_Z5mrN-2qʀ;rL֍6Gft*H>QlWׁ\|,g,ڤSm5Wǿ k A}_aWǟ3 h:An>â((((((ڙM'Vj7iaq#^ᏭK_o @ Ae<6&5EQEQEexz7#g[Z=̡63?hlQo\:~U_"AfoV<XKSWMFn>qN}޾S_|W/B;gbYN3(W񟎥I5-PKvvF>Ww] /wn]cp3b{nz-K;('$"(Qu"KG"F2G|8|rF{ړ!kK c+zX/K?eաrH'ҿCcbѫ2$A@(((mP^h4t7öXX\lq3,;IQq+c)ɩC- ܩod~J[zbYm_]Cqgkww}[ ^9zeOSEPEPEPEPEPEPEPEP_b|1^gɷWBM|'0DqPHC;$W?x?M_!~[G٨( ( ( ( ( RX$W~">ZsԻ]>w"&F`>1&;~X.|[4LUǕ##=k־<|;ҊoK_7šiO`@}ldM \|1VvG5Gi@bV)1Z⨟ڳyH ־BLAOeSxyva[.3ad'(ikIYkOKVBLUc's!# ~VLe]5\ dnK5׿^cdfYe$LeҿTP|J<"EU_ 'ޮVӮH̭-izy.xXGRnliWD 7 W=&% ;^$TTzEKa/6` ?{O@y_ X~џtKTJMiڛC^8iyȴ>-аԖN[ GN1\2dr1ny(((((((((((((((((((((((((k}H|h akol }_ Հ>FhG0|΀>A| VKaqm]r?$|)s) "PӬn/o6YdmI'uQxm4mMKGGUbfFW$$du=τ]~4H6yȤdxG 7:13fKg3;6s0=6#Ɠ }Zn##(2[ >~pK-RY9uY y QGG((((((((((((((((((((((((((((cIϯGᩤ9KdcbrkS7>CM6(&ic@xEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEI򛻭J1'膾FwˆF.5X7# zʾ͠(((*iֵ M>0S5dbh+V:74wOHOJSh = vo_ ٵ5ww%ljE+m8YP۷ /|zׯxl//-.ٴ+A6p3W gWDLwķweU"^{4~ŰJu)[ɏI;L`pOkZ>|:~GOUbS}{((o+^'!k֭gs6}xA~-F$giPBaW~?eЗw~]4w gJ[ZMky Om29UW_>OŖچDMe996(r0 @y^7Ǒ|A ~[lZ^1U%d׮:^@Q@Q@Q@Q@Q@y3SҢКڈ+0b?QGL}#4󌿵|k 鹣_'Z^},w ? ]:kBTLu$j~7m*IpUنBgQ% VH 1Z[+pN:'޽wè|@L2ˆ$9KCеm~&yMmi68w_rX~οm.s\m`r2ps22=km-`a8!QH((g>yu \"`AA'8.(RwoTQAV!|ev\+}c|*p^((((((((((+?Zj:mcnm;QI8Bk\|}}]ɹ`dܙY$ԍخ]ȐxEDfzy(Qxѱ9rvk|\gJ/~(x3Ŧ45)ndi[ʘ YDmKywAFQeǔ{dFOY鳱Z$>,Sȫ(O,pB"G _K5kEүmomm*ȇH tQE|mD(]L)>)Mx/3K_N~:|w/g7v !gK>v w$x€?C+/;Qu]*3YO=tSM<1gw8U'icǞ/Zd&3Gm$dw o/97|HcwrIaQ$,U3Ì ~I>oJ0)hA<(((+jo'>Mm6nm}Y Q*7`sP@`CA@u/쯋iH0$0}oŶqNd$?UNORXkcoIYfy,cnO8TWė/GgABfH@tmO ؐ,H(6>$'ͳpHSWg?;lEu4*y-a-^y[DW CԵf۾`0?@X :^ep1islG3 #A_Dյ yu,{ɯ~Ͼ ӼY {vaĒ.ku98ۃUO𾐪vܺU:|eRx+ac؆eY3 7$*cǩ"gK^ <᳅/$BKcme%ߒSetPq+k]> U"@Ӂ@7Z܉$ԯ y3i#Vk?>5B@S ymp!$G^(((2|&[W?\Ql_Ͽ|w9?\'O?“vB;i}G@pQEQEQEQETIC<* tW>IZ#mjGxOYR/x}Sn,%$܁߆'ڀ>64hu#l4gwCZLm=E5k*Ru!X z՟j_tƼ?.h᷃/{M Ld31Z"lc4;+XRg9b@^+/WP{ZvR/q 8MhY,?k?]#5?&6KUTI$W`zy>Wk/*OW??je?8(Z&o82e/7ᴟWS\^'zψ$'iu'h- _j>S$QP1΀;B2?1 7Vm><4b#y3B}iHON?r=/?e_cv1R~?7h+dT8xɓխO_M'c}ǀ\QBEe yHH-n|y);f{MkIa7ހ85?0JGKx梀>؋X&8Qs賩qdFWR=~}|h[&T1\C&}ΓZb^ßu$SGmq*OS`@Q@Q@Q@yϏ~>hmR65B}#w5;߉u+wkO,/q >xJt[{ݳ5Ԁ;sM}ߵwd{()~kױ+JQ޷M oV%|vj~"L N}Pړr쿲մxEI?v:W_B.ן`~ɯ1hO\֩%ܯWY ?ʀ> ;P}^)'dYmN=fr#_꿳?5vL1m“}s_$ ks'?Bd~W:x G-|=kIvh~$|`:i  }Lh_g_ dմU^8 zg *˥޲{L3}vl~՞/IѴ+r~ɟxGs9<ǫ)R5~`hڻi(RZGx!\:qCxKz/$l o!~vMVf'V0>S 2}t|VPO+\y6|yw8qTw}3u gYG]sO}a\iq$!#Pk([+A巉GL'_}x'ö𦗡ٝY@ ܜƿ=| X*^?#v׈*/OsoS?AP=_čZNek "XPRJ^@>h(((((((((((((((((((((((((((:`yO_p|gk A\"c@YZ(((((((((((((((((((hM{Mapz|N2dr |py{gFʲu}+I5蚔g)we Zآ((&j?;zh9 =8~fPxݹ~==]j"kҬTrB^K9<`{ SmmY|_yqw4a*@zg t5?fFǨKUls!8UM";hj(Q~+B?7txᖩwcay]"{gA3Sc:ס>=cǛ->+W|'֣{-:}63#+&s$+_\[xBfcgF(ُ p3>>,~5ԴIޜs%`m\[p_~|7i1nɴ E{{@Q@Q@p?|+m߆ݥ 5Ŭ^ل8aQң++}B}_~Z\4먳d`bۙ_wEPEPEPEPEP^UQ 7TU_!Jb+%gQ'UJ? +(+>/ >"j?{>yb7(đBq:d\^E|g'uEi_8!?fiCskjl9 sȌ({EyǟKoZg"Gˋ[2 |@G?Ij=>+VĖօGrךگc j5Oζۭ2Nw,s$e_^9*@@-q~4vw5/'PIRp)>}+tooͯjo˭m% n>eU _5|h.S6yvql&GUIf$א(J7Y^6 V QKU41iVVJ1v*|=c`6O_]/JF$HFB_3|f  gn=2k5i(> ۭ\GexDx*~n8 {f=$ƻ}X+[⮹cH`X'*Gxc߆9ԘO kdYK G wJ_65kO0 f&F(GĩP2Zp SK=J'Z>Ծ4|;ӡig^"o575ks}|+4?[_Up۴_[VD@.#ӂS?j[3ưN~?o).-g/P…VH{~RgS[7xeT`0^[ @yc~Z?/=sQXsJSԒ+Stj޾5N%o<2?ڻBBdд6ND{> V뤟^G|+hGRJ-S?>$󜿵WcҼ:1#kB Dt2Io^`N<p쵋3+?+d]Cz(Z?mh߆~sc)8.ym쐦V62+~UMGԉF*G릟;@g^d^*U$L՛2Z 14R-&abC_?~ɺ xc%'5O3BFTL}hxzI$? ]3/ar10ms'g_xٮnOӭ2\,1s:V6#F Mᩦ0XLXDV,~zW/h9Otȑ.&fl Q;݃w0_NYZDVڷVVJYGo^3OS[xOW[:GFn>yQךiUx1wfyRn|3O͒:tɠ ؓo z>?O)1k:Ѽquۖ?f֐NhAK}Ӡ((((3/xWS6^NkyHǒT$H-7^֣0%OG\pksx[ڕ_hbM RN$$E\Z jWHOI(b_I&XC Ka f!~cj7]io-!#X]6fTu<7qmp&Xg瑌W -g8¾~ı*G2yѷ|lvP%RGVuoɚC]N&hF?w϶Iq\dywq4shC!v2y? ¨7Q8:Kiz&hPȷHl~Y~?DQe/5f@jOg=tnp|v?@?'whez5m[|wl1]I@1Er-!-_Sm'r=0PPỌ K:&$|vi?r1?m7C|5Z?EOg3~Pߵ##K=vl mH^s96>^ssLk*_{`ZltWJ y׋?h/7G!*H#[\$+}A"j?]nxݎ3JoG¤~]boW]oxVRoʗꙶ \@ڡH=@]kǏ6ٶUF1Sp 1r>~i5+XW_ C̑B$0TDf'M}![SOI\DXFvyߎE|oNJ|'|-4_H4'aTupe t@~y?uoxUnHW%r:8AG5 o2q?rgvq3ږ79@Vƺ϶wQP/ M;P[Q%hp{ڞ%#kA}nrQ:u`q?kQEy =p+?c-fpo vqkϿmg񥥓Oc#+c}h~׊aa/.}ϵBX8Ryedozh67ּ٢Ka-̭#uaגnel ԌH3@GsX3Pr:WψZ3XIlٜlzbAOH5g-EK flvA=G翉4=Gzޕ[Im{m!ԌsN2B+=};X. 9!qԃ޾GykDkyVqϨ6񵎻llq+-Џp+wOlml'MWR2#~!~ξ'l5-8+ p3=" qqg۟4^ K5!;d,yۏrrGshGY]* ▀ ( ( (<v[u$z5`ʿ/C f ͇_AΩ/aͲs550l251{du@W^P\Xx]rr է3OĺS' a E~|DEH>?|M3[?>&m~'#(e*#oRYWt-VW/T42E= :s`1ivȵ] sj{4G)JOzOˑ_h+_G5RG*]yO%U$I9ɥj?4),Hś=Ŝ)y9(o>/%/#:ap| xᏊE𹳙](&PqOFFz~&\|Mk핻E467Bqr q2|56ߍ'z?8$zz4ό~PFo܂:3j((((((7+N|߬~vsGUQ^M ,dTWx -3oq(V ^,k$NrEPEPEPEPEPEPM8\HцiP>R NAY-*?kә<3 g9~6gbA:xD^8Y5 l<"?.pF3ȯ5-S-obY~0 ( W <K(8dMX~lxV4/u4[{}{3k7Y#WC`>ƀEPEPEP~>7м9cHzZ*f,Ct\5?w{(msiݫ+ڰi#<^!{ZjN>}&i@\Q׭|֌n"]k @Iiwp_B^֚מ#TǛ e@އm\z}"`z1\q=/s//'EwtPΟ fB#Ѯ ?ގb5 G{}O_1A4L33^{woag5[If"(,:to[>P'YB7N |~ɚ{M EժˑV\W'ʞ%>iڄc=A_d@jě7!4@3 H?`JțH=@*(~_?ҮEbyY V.V{8#ᐅEu+=bOσuC1jE.om,t%.T5@~ veh'~#:-j쳬ŶT 붠((kZ6Q`<E>Wkky-O I^@$q(#/Pz$p{MuJe<Am3Nocon-i2p 8봿io{hږQB#;P^SahO0b?JnI^>{[!>+*:m/{vn/%fvv+,x[4iey2m9vUkBP̰ʮ?0jQEQEQEQEQEQEQEQEQEQEQEQEQEQEC|l1WI[/{Y;_t^96?|1#oN(((((((((((((((((((?m[Fᾑt)9z'+տ79a?v> eA+J$(ܩZ((OCwm w#t{D+6 RH(?W/t@Ѵ0f[bOs9;|,VP"cVA$D{`+䏌 Y=Q#.\l1]ijSD[M܀?ti3qE}E|U}}ǚ橣KQi"# HA<ƚ :Ŝ*UсVS?Ϩ7hF`Y I=gf1_2GQ^5[h? PէXb1 !'9ۍqbS752q~(? LJ&ٔ-DK`y\-bIۈxU(A@E~wqi[f*sٗ^CFhn<wyo.)#1s_~n{Y.m׾$:K+yl֊epsLMV$01< J(((((+j_!^%LU1Sl?f*?btδΪ/񯡫؜ŶW՜Z((+N[izբ ۝YpCWoEb3Z<3a񔳳 -7;ckj_tF^#(wGkrW# oّuk'+뇞[{wسm`FG<3]/LV꭬j6ȱyPcv2KgLkCğ%^ͯꋿoaDG9lchzqyfGtLԙXcVAyEfxc['gLrwђ0@# Ӡk٣%GL+;!8kڭ|qFG?}ע:-g3@h((((((((((JCگ"HXnUݶ\2LJBw/ x[Tm[sl>I䖒>$䑂}hD-#[ݑBw,גZxrAѢ.X 3J .p3J\oH~*_G[xZc:|Ĵ [Rz\?06`ם-6@_>;@)߰8+>)ak/_O닻E㦾aSe( ( ( ( (q2[$ҜGcɯ Ҽ*Vc[(ȠE z@//6}6Ͷ;0]P3}_/("am1ǁUuQEQEaXOt-U"iصOW2IkZlIHU_`p>^k$KIrc̍QȧЌ\})PbAȊ0}ST((RǀM|~^O;CƲ\~"wkơ緳)nT2g}(_/|Uao7e`3%m7[ֱFAeK>ޜzaORi?\I`p[XkFWIh\J+!\#,We#Mdx}BZ$~fhwnF;_I[L?ݶ$yI)1jƳ3Hd%s ioM?87(1$N0Rk|E־ 2o}ȝΙRq\dkoT.:`'tG|eK%$|Ѯ ,.l!qW)iSC^"1k c-)ZE> j>mƝ,Ws`^(俈kV4`ܹa8A?goUu^j60]1Q1ǘո'AΕ}6 ZVhmb1Uʀ>G7\cG|%}qYۍZO;?6Z> .gF$fZ{1+dz%ѽsȌ=~ѶZ^;$vZ'}ןZ(og- Q[8C+ ">tAFe=:M[)eN< rX]ZkvxdFrJ|%w*f9-'_4QMDz2JYWhkW¾,G{$W*dT&AkwXԳ *-gLִ_پ먉 _\`/ƹ?]&YC:sx}G{W3W9/Ң$}:) 4ƎsOOZ@Q:H*0>ӫu[^g6X deqNd('STDm4-[MEᴼiê v?}{W# _ ?+~!P4nݪ$m=k|J4&7Wrd*O.|Goхׯ|ɾ;+qwH#,@8;*^{=wkLd0Q]#J/3_@N 1$RȪGN0vuUoSOia\rF$8z:ש$z+!'<>(ʿjw%, -qs2WUsdt@e՛9Xy _4'7k)N @W洱2r$JR:;=ƽ@tkMH\Gb ԯb-5IO]HbnWДQEQEQEQEQEV~zYBIW=zТ>3? huKaT|qgV%~˧Y#P8sG?/,kujf(Az 64{tIoYy2\ؓב/<)\Hynqu]mPEPEP_.ܗrՇ,S8d$oξn<ˠWRŽ}|VBhe+Ƶݢs,OaAsC$C]A5i-R7$G4 %!Q$nJ 5p~ C-|ڇl|NZX܄/{w_6dʰw:Z(SJyi^-w6BF|M5J-upWcr=Jk(? jJ<9Z'p8Z39hr}~H߰kε?2ɟ%~(q/!d~3Ŷ6,?z~''܁uon? +?)?iS>8Yc0W+?e^pZw@4q@f;dk׮mmV rD 3SPWZu5'T&ə7Ȫ0Hi hѿm~"Gc6&k i]K6Q'۟D6^k%ujk+IvWI8=k×_h/uo+Hd;~*Ꭻ#)y6: =?5gÝGhe]ۿ~tH+(((((81LU_ ouҨ?} _<~N ((+tkaf[\0{<*DŽWhveur w|(vyN y^[q]qs#<9_߉#d4an <~l𯇴 6l˵rIX䜒hΏ-Y6V0[G+2!j+ͥB,dv7>)xGT5yC$S$vH g |M STrItY>ѩJYHE'Y2h[?qkc(컳½:]U";h9;BG%W6+}#QWߵ|MnK}hwZG9E?KEPEPEPEPEPEPEPEPEPEPEPEP\O6c)5W'qC*#?'?.ha|Uu5o𧏊Oeo=EWtQEQEQEQEUfKC.X) ƀ-W-7~^^KILѹǝ9Sԏd4ړ֭'۬{`v*{r?3A?o s~QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE_f}O fMsgO-9>7y4w&tf@Th6/^",REVCV8$Tx P(((((g&qI%ʰ8+4GO_||=41^ڀ$_6~՟ 5C:EΣoy GvqeYW QrH*d6ktS 4}7]6 ܁vN8I((((((lūSB$|oi ꫟2[fN~??\ӿg}7ƻPi:|-td28L30U98x?dYاWo½OG5ckx%\}⽞<7B a)-5ϖ# Ԛ#DZK<>qԾI oZ!Xc;~Aj-CetIc^yeq]%6TYcxe#_KxNe gM:D['H?7|EE B5Zb{-ZBX[BY\צ#_hCtF ̤/n|3x[hֺj$kK3pGSTgl@:-e|l&J$ M~<ǩyz$-{׉t}_ږyHbig2~6#_9}PyW_zdm6V/1P+UM/,g]k3+Q_Shŭl2F V@GkW>ZkgIۆ_UtkibO@zֻZ5cHHUY qIP k&ĺ]εC'dOM|9ow~G2>V[7=k,^d2@~?ku䌻vӻn?{Sm*خAr~uo?ecu SXG`^?Z,ՌpS,R5E~sY{fg!,ⶹGZ]Bpş飸A}6_P|"!5K;.o<G5l:^VAUّ99eM=FD;k-S6Gzp 8~;K+hl/$A )Wo3ovpH뻚SZѾ+|VSJe4.sKAw9q5Ϣoa iʺe=lê>WA38y-S$`^_<~~gjԱ&Qz?ꦾ"t(A R%gUbu|l]PUQD~eG%k5W. VBYO~}B?VA,ڑ,n\A+b]HimVI6G@c|<+أ:_7 ![*G^ }s࿅ik8]TS-a&L`85}m4ҹ¢(1>kAgC -u>Fe5?eߋC&S~ K+`cz *xs5:=Q ͧυ0%06<7~(FOԤkY9uXǦ߳&ku<--5R"0ewOBXt^WߴOuyYp<TgP_ $ ۽,kRu:AYNG= W*!$8H nU;|1?-XGQH̪2'JX-Qw̝W? Vm޿j|%E_̚"V/ 2v_>O^Ke[hy p'p h _LJ> 6/xZ8hFtS 1#@L+ᦧc%6EsJ)hH]dUk?l}5~tXLv >o)~p{A 3@ ђ==s__j~jɣ/P7 [iB(a$;YI2W5^\[܈oBlrp ;qGzh hdE!(QEQEQEQEQEQEQEǏmt߱w#|΃_``9q۵{~k&R9CGG!c E|j4({Dz?4j"^bCA,NK-jI$]}m/x:÷^O[ 1BSۅ$ᛦ3|_O'՞8[5~l9]10Ǯ+⏅LNhAbhH5-f>yqV4k%um8\aO@Q@Q@Q@Q@#c+(Op~+s:Ѷ"q3~66:O~I"r ?ylm#4Dq>ȗMOp+d;x2\'𵵝B?V[8%c!%Xfc;g!oVŮx.SÍ#M=IJ-d!9',>f}cV֐ (bP:A@(FWP4QEQEQEQEQEW~p|R OѰ@K㍸ACK&?~w7C`y,{hAu71kKI N1ۢF/O |6sC3x7G,0Kq,pg oY4}CCE$d䑌g(=JkjWZ+z &+ſx;Sm;^bPA)&tاi |s!ĽRVNAba,yNձw).`g֦]̠m^=WTf]; ĝbPN;[a]}xEP,l;I[{!̋́IF(Z(>m Z{HkXPG |GJ͗"_yKB* >((((((((((((o*I>CSY^,^m6h4">g@f{m|ݪx6eia]g((((|c?=;NAV.B+`zծ΋O`H^#P=9o Jw6a7RK>UI Ok*|]M#΄4 ` c?Pdcg,;,}_8žO?&XmcXw噇?ܯC#}5] -񝄇&GM#5|/ox Η.s.Tڀ>((((((M ?(T >|;٥UL>t c,cw-IbI"exVSAExg^-m_!Yv0z\FGh~ Iv6`=4v((((((<h70.s4Cr㊯&D俴DYP@h&T1o/Ro- H>1#Ӈ.>)wI g{I t)aLC*+O t!?K 7P?[[ǪV=v5c?NiAZ#?/D4^+bzasJgÙ*xt(E>+xvxDOEZ|/*:+;LtU隭opt4QP]^]kե( >A($#v/ \nS_í7?h]}jœjnn5WEG|7qjQ5MXOH[ 4W ȶksvnᡕ2v[5EPEPEPEPEPEPE`x'4__^}ʰg,*I' qV9((Fnep/zZN-'[}bAZc[VR'F4~`ǯ([f8[Iq@HX()3(`J(m(( ߼*x#_Hׇ~zaDn g?F'SϦ$FQ}3_(Rs8%cf ( ( ( ޺O Z`IsǏ&x_W> RO/o#s7uܹQߒGJm R䶒&q;[G; +:={;Ҿ|*]}5ao#.u[xW8 {b/¿ k_٭n1 @8l~Nz#^G^`:gS:zZI&:3H>;H)E||.\}CIY :KNeBYTwK>#i⻼`qfˎ߻gci m# me~ pAm\3`BsھRY_" c%8>"kK[U{h|I䓓^ªkǞ&Ə{xWO%cd>7/m&=#ķM[BVS|zv|6͞[/Kh/(2##^5||g5X\.Ix+#)補8=>@|A >(E)aΙIp3=M|Mkg0ӵ.$k+^wv5ᑧfJI޾lv|C61w-@-%FOS J|JY rdm7U`+㧊Z4R-E"f7r PK`??Kf ^434BK AgsS\0]7O2f8$ 8 f3hl&=dH4ῲ-OSx=-lF qَy<}!EQEnPG@4hи?˟בZ-ƕ* H~8_`5O kz{.wc<}wˏ־^_1U >P%Q@Q@FϽB*3RQ@|n^j8F&[Yedgֵ5_ţ((W h¿bm<} X/L0?g~2xz8apaB8K9O~PX1]#BXt`A"? xzw4$$c$/xGB#`iq*y+z/gxT60N %V*Ig-9 ) n@3iBAGk2+9UoAWؾ -ĺzjzOٸ ~C)zE4FmPF|1t߲OI&d>s$m.i_8U0d+׌0} ,扭EYGu`y־' D.mTbvHXz1zPPƛ{a 0,e W~̾'cN-IEH[ Q_\xAմ;x7u|WN@,wIY-̀>d#iRq:| ubƙ|yGLKo# 8滟د}߇.ۗ[pyYAu㡯|Qi(o4mnnl.d ؂WŸ>x5坵n|$\rɥYHAw`RM||~±.nU 9}w&JwssmdQueqrh/hi:OAr )Ǿ'W_ ~? E_i~!UME܊TQF:_uEP\W?fOkK$iٚR_kǍuѧ 'km]xXסԢrEqu*<r1_(0ǣ?Ҿzn:ʨA-դrQe8~rZ$g[+Kbi2K$޸PI5x|9 &??_:ViI,P H@>xRoewkqɴWE?/&c>mA~V-WTP>۟3EϧڛE >McYӣH๹%HPF1N쓜kپ*E>hi}sQhX7;kGWhKއIީJ}Nb~PiG_ izZٽ[S0y1*qTgw =1:o,R@HݟGyWƫ|;{d ڀc=+w+_ ܋mkQd{E _O. o N5ro-_a_S~^'O#Гi4SSWS⯆jY|7{t#uT&s1T0>Ԣ(+|]IlRK+:{n` s8ִK[VQwRN $l'śE#P}Zi^`U,hJs$s4MD'V )fc׆i/|@4-C_^1eS<{謌Xdr((O %`"!FI> 3jgh`vnj<*y c}_,/m%ޗéxG;y9IaCrdGPE| PiksCuP?a^#C^:@ m*;:695߶xDv]ܓH(Ꟈzπ<7NAMM7j[t54&U`WEPEP]V-IaxzW`/dmq_9kⶳ?T*.mh `̊oxs S}j( ( ( ( (!bW V6#esq>|.?}|Y\1?|+;'hS<h((((((((((((((((((+0wܜ6!_BO ׺}3"b~7@eQEQEQEQEWw/4Z4`X'V4WІhjG_hyv21#fR9 pGC^I%6ҼUe#[IlEQݍq4nkUkז Rʇz}kor6|]lR[i@::x3ɰʶvp\2 ۹Z3u6tFv*=bz=*[o]6w]\L.q_W>'*$)#<20A_5{ioҬ._959ϮHΝY(TcDZ[GvQO;vW(D2 :cw~w6<|8?K"Žиs"WJ>Hn6 s{:(~4Ϟ#_PӤPew&* 6<};TPį+yAkqZMa,QS"v 88J345;;?&G<1:}r6>y;O=wYBj\WP|3^# vG2}cEP o8z*ʾ?xmWV,݇FO@EQEQEQEQEQEQEQEQEQEQEQET7us zSR:FSЌP߱geX\eI͋p>?#_iW_ kКݐ9 庑lٴQEQEs<][W-Ds(p @z4sxn][+(S=뢠PŨX\-aL@Ym1$g\hD5fvMy9&H㿺Uci$>@sذ ڶVxˋcѥI_Y˞Ejj6袊(((((+kx~`12MkХQ[9M.Xˍ΁}= s"?}d|goa^cZj1WcF]W?emY7]i!VXp2,ָdl3y^&Y{ԟZڳi{#z@;~F1nڻ­?B`O&:@N,[kV?" 1_tG@4Aj|">拯B?rcI>/?r4*K̘`o@k=!>}fHVEo9[x:4=oD+e[3CV "o8|8K€<]da܎ Lk_nL݊kX?gPȎ<470$z3[|$)n><%v4&n7G?U$?E} < <N쵷Ʃo"*(@~sIjw![ľ FI7*ss!_(UP:1NM? t:k)?06@O_kM~|Ilc"&F'7O 7}l?~Q@Xn_l5Б3ڋ[یށ\#5d_ED$n"yZ2 |Ltr.>TPǏ'x&SzS?@?<oSj#'5P|qAO xԲ~~2)Mc')`\U;~:CUi?f_Fnh~ >^_Z?/itG{+*(UGDe߆u74e`s ?EmĨ#)j16# ʿF>|I,i+hڒpnSЊ?gډo3GW,J?e#];P$|A[ɞ`G!pz551< x>do*5KőoCfibl0k(O~͞5_jZEVw)3k<.9= ҾXIfuHRpI&\} _lRXa,WyS: jе/_&kkm#ȭ?AA#[^Z9Jݲ8=~ZմSA-Q@Q@Q@Q@Q@m֑m"GE"ٳo2.{n+]?g{{ gig"OgЪ(o|NV`<4l ;`}/"}xndqK{nW]|tnk%fV4^%$~ E~uIJ˿FGj~ ׮xP Esw+c8<=;Wq8i/&⻹v$# ^H]A#$hṾ2|CHŚI F ߃ ;ſx\mP`Qum l0zO ϶I皿e>5$xr<~#~(ڣ:3FQ[{!0{$ 9@Q@Q@Q@Wx<+⫽Q:6ln4|%'tWE\?1FϮkN?Ἂ jq٬aȉ"U 2 ymyV5 VnGJ`V.5PPQK\]_\c~o\.mAJ=R6M@}z6ҴrahX&-XӘ ռ9u= 9 _3Þ$[l[ReX`< s:?]nVKrY}^E:QEV|!FXԒgQ=~|7 xfW@д @r?޺PU*F=h|i4Xj55{YgYH2Lm8'0+i]:kZLo|uiܚ6~~6~ծ{JF]v@gblEt~=-䙇2lrS>ץPEPEPI",H 2"/ ,^l䕷8&EQEQAp3_񽧈/,4?IKyJ{ZIF;} @dQ^ ?{Kx|-vUk~kc!N%XM€;:*+k.[Mџ⍃TQEQEQEWǛmu/ YƮSʸ`}Ð?)k_zl|5mo/i#%]"UX=_>"umJqkiRmϙp)( g!-{ڹ%;+Hpu>~ͺ^㵵,-Ss[׮q1`{>? hOayfMmP-4TDPtEQEW'p E 7:orѬ3Iym)9ğ4?j.}_|Ik? VQN$Fq?J0A@Q@Q@5]6WTQpkÙh–!Xi0#E11}Fú]̰FcԞ$PWP%ͬJG*`{0kG^o k +Rtlr)NkξXtnFi ,&<4_xkMִef@dr9kWϟNKȘ}`ݚ9~!1_Adž.Y^aq 茬Y2=??3>;|g$s\>zײxwt ZKݏP ;o?cg%|g0H?6oY{Kh#t88wqW" }zώ {> ӭ>YxM]OV>l!<0x=I&|:0&g)u'f꿵'y-Z')V=c AWɾ?oj:́*NUDZ=F:~?[u 7Zj1EfQ+ K`P )x_N4 [TlnV84ExB;$b k $hc<][}Buke]'j)c@_ hz揤X^6`Q9'#$ƾ}yм1F3[K22~"~~ZwV6..eqV'8G'h]=oojo ~H@qkwcDyY,/|B8XQ_E֭^[I1qI#=B((j#*3+\=9S HBׇ~ckRvˎIE#E_w|?ݺ??p_,Uqǘ@1>(((((; 9_&7k.w??}ͮN81[J_([鳟yh((((((((((((((((((+.m//uiJM-ᐰ<?*X-kF>]Y[/ݖ5~#55s 5վxn#\iHTWI@Q@Q@UmNK/,6 Myo,XIun+aG_($|3m& k{|SJx?1_97zt-bcv4HgR9_8}+G_Vx<s_Zyo2Gy zxSu r5P]Ix}kh e$h?3cw~GҴ"VYpq p8Z6ww76P=4pLlFpñ TQEFxn5:Q9޲'m\ϏxEm;6i:)FVH[+Gӡ@6sz;YVhєgھ??:e9& ԇY!#]\g"7@ގFU|/9xWԖJd#;U ,qP}cw|Z%mg,KI>?ka-+ctD7,0dq (((((qr1KE|-9\ #UؓIwixrM|ڂk)(((k(!C$T ~ mۏW HY5mf ǸG4W vOvZb<> 渓^p>/[6z+n v!=>U qz~]h_;sFEӣ2&m"2ހ9Skj (>TLVğ!9C4&E|)W,~diئ=<4ݡmE^(((((((((((((⏄khX5H[ 9W7bcf ( ڷA[Gvmuscqʄ.v:>=h ZׇyZMʗ6 tݴd}kf#qeGdz ?_|jֳ8X0Iue?Rk#GAs ivcɳ6FPŲ9G\kKjW.yI88 2.KKY[x"y#-{W<;-α|aБSգ.qА<%>>&jUk`<>~Ͼ ŤRjQkږI=nʏ'޽4HR5TE jux:~ӾSԬ+ Z,ωdC {!sުS-1w#{QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE_?g ?^(\uvKDmN2]O\x>E|ŦzrF~*M6J|^Cy^)SuWTPcG~4_Ody6wa}[E|dq`j dDVl9obH#Vj71)/WPQV#Ua*uowʞͬ寸%ǘy|[1\Vh2}~5XLc79.7ϐi{ip9ˎ/ A3\xa˟j?_QC?vu (|_A?L->7iU܁_PڏZ$}vaZPF"g?}*O 5xoƾ>,g,/5= /eY}+짦^,'eaR٭C6q}1ER4?B Ӵ{8,`G$${Wh ( ( ( ( ( _0=S> rtІ]m&<*kELx#pZR6YȗVmYY}kE|RKq&`ƟI:YS%{F/︎$?}E|G<ѧx56LŸEo}MX(ߵ:?}AuekvnOKa3wa rK2+{ yUW'|A<@J?hφӠ276 3?5f|ir]cbc7~Uk?׃6iu]eS} _j{E@U_%=5Zke>ɨZM9I| .̈́ҹe? <2#΀=?'˭7QRBcG 85|$_ < @+~%L~j*߇tyk~hSg`?%0y2-II}@hw RR>2JjFzv&'c+B:>\:ZOR S[&?L@t:m%ơufi(ɯ-׿hZJ"g*qe*WΖKuE82WG|M~ZfsX#v~wߵ]Io쥇w~}q} c;CxҼ3[-P?dhռG^UbLM~x*Kny\>XXWXf9SهQ@~> }+# }ZHJu' C#Cy>-ȂXn @OAb}q +?\4zzujN*%s:٦<v~|;NzQ+.q {ğR=3KR/71s_wMsOI2: 7xFRhlY>^@ ⅵyt}O@w$ 2ˑ2:ȊC+ ;~!jk:M^TGr d6=Y6}'Jkb{J(]$ǩ j( (`T?x*q;m}gEPEPEPEPEP?_#^"9m_"~XwM}K?\-> |_!OSE@tEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEWؚLw uRſ Y++h5O(=.A#D# C$y{vZ*̩".?z* ( ( ơxO +$F[Lo`Sߏ/Ox]Y״ Bpu+e:lW6)O:HffSu sT׵x^ 1ZdvIzV)\nl =Ѣ!"8B(P:; ڟA"Ѥd 3"= ~z=O i;q E 1 a૱G}+d/~2uo4j.ĉ=y}h_"ԮuV7K!sx<}h 6(XFi%Q$Oھv~򷦽j ((^-7?]Dӭ.qH*)<#'GOQZaVvr8铒q5>ŧcSjhge>CblPS}$qƅy IlWÚ+Νr^>߼.?Jox> kS. )uƀ~o \xPQhAQK`c+w Ma3$ g޾y55}XFHRV 猞_-GNӼAcq{02s 袊C}ul_^`E|?\H+Kr{Bˢi1=lOQ@((((((((((((<~W9?Կ}_y`L.SvtJd+QEQ^m#?~ޛ ^R3YD$XUss5BEҮLոiz#"O5.[eW-"@<؜qPEP_8|Ga'\Lmd2Ha<'Ojox/RM4vbS#UOaL~!kSԾ糒5~*s/|< x@ur/˂#\3ZD$Xj0/[9ɊMSEwWy%(ļ_TWʚ?n ?#"L}h( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (+) sif۶A! c2r85œž +~K.{8G<䎃o'Q3[`z^~GNMw^7y 8nv\>־-?Mr,eշ: '3e 9©ӱzjt7vSa+pkU5_ |l|7~m4PsAn5mQ 6``e #^'of_xwMuB.ČU9:WصxgABoE5+Tiy6MO[UuE~2ߨi_dYFG)%"&Cί"s{H]F(?GZ| {| 9f}phl<]Cou.pwH.!s=QU JMd>.bA.KOlp'h@\_E:|Pm&CrE+5+kkQC̠dPTWte&Rwn}^HMNoiPq~i'WQeѡ%s4gk}Iaӌ0zU/ڳ6_ Mpx?q@`Q_&[]<þ@kbW~EvUs@MQ_"k |Ro M{Tse+q@:uQqoؙ/ǎu#NVY1bl?=Q@Q@Q@Q@Q@ Ku*GWw߰|d𤻶XO=wǫR99n4]t$QҀ?F(((((((((((((((((((*XlPFFA`P߱MO<ں+qC(Rs¾5'O~؀YXfաWPq"϶e'(/M=*G G!]O *x#|O^C&s!$R i(1袳5h^nXiz""43%<;ڈ&M@#kIf" +V?E\W~`7AlJc_ _:NF>0*N}՗_r^&%wQWdPq}QeV_0[[͋K |s@FQ_=EV~/fk[JQqthO_j(((((KfMksq~-{/>ʫ;_q#oEaЌEVt{/hwNey C*k du (´NZmcQOrU?k96=MW@>ZUpSH&06;ܳk[C(8KLpTK⧏t߇Wq,)kjk\Krpp+=N0|LĥMST(o?.rBuvNsaS_~2qX@#k/ _^J(((((((((((((2۝3Ydl.W9Ak,RIt'V^!A+iJ!6GNEIEP3Rpdm/[M&H`` ;+hİmJ{掻cw_v[z6|2| px o^+]z_=fx vJpj:xSOte$n#{ (>d|1؂ifI PG@o5ٵu6t,jV!]8WVZj}fmw6g 3EVPd[hf+_nxSJCƓ[HbU'$P9?|G*mⵕ ]Zufm 15~v6ͥS .r{cW t)ZO? .iQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@sgr- )Mݶᔑ0ހc~?.zzt*wrvTIfHnabf,sἌ!TtӂthݑԫR0A5'cw+6fI dPd Vlt}" B(u8IOR{ZtQ@ 0fF=3O(kcCtu[Nû2 Xgk/|34nJK `nE+|QMk09IW;OV<xŠj> ּ=s.7LY/RX`P|5CG.ڀ~n8#8b>墱'm'Ś,y՜ 0QEQEQEQEQE^?cnX_+ڞ>/Id$]:x|&{X}:2+#V)k?f#-S1rD%;6QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEP@a<)XU4sg5 | ǟxsa-uPs7#SXW{.XPM5|;YqOrI Dfe_$ͩky$ESO釟tPhh-4>cn}?J[h- H-!Pac`8h(((((:~'97wKmIxxeT~DW|9a%9#D7|3Q'PZe_iq2w$r`#5aVX$"و=5GWPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP/v|oi~x,ѻ/ Beh)"R;3_)~- ߬go<&;YF%]+rUv=FPh(?io5u/kiQ*a~-^xui$'v~E|.>j[EdUa?mhvi6 G `uחZBg,xſ/4Q4-&Y)6=Ϛ r [8hI@ڿ.'}ffUA3=^Rb:OHu?xJ_Ӡsbӂ25}ymw}qdi""π*WL%sg xO NDu~Ok1C AY>׊(riZn% f;Fz@|}[/KP5"V H\g W?Jڛ ,:~a ?lMh\G X"PX 9zNKy_ZZ9!fdCe$Px;_.(vD[H68("EDQ0^{aܝN1#d荜6 D((((((lh4WyYCe3Ⱦ?{7߁^Sƙl"CzN5moGY1jl%+YrS#u:<~խ k|[G\O$%5)q }N0ѵu (ngfeZƱZĐD0`YkO{l|<(, |c`&ߌR}?}wi)9]" @5Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@vE_8dY0z_Qi0)u^%r 6> ?E+k:[diJ[@ʹLAڃ _<[]GX|ܑځdԯ ۅxK⿊-.MǸj.x@w\l K޹RT9_^Lz_yW ko)uK:vT*Ə )-\\N-W fDzyPz8F'+OҾ?e_y2dzk>h:[u4- *a fW9'$V0sÞ{Zx;\YdeId tOL@?E,.m,In#YUцU ,ZfOl.$H"2 ( ( /4$ڞi> bʛa<щ?|}Am?6CxoJ5H[$dʣt?~ |zt!50#-nl{ϡ5&}*];^A;OgWRS?2gF2}g? E6%~sELp)@<x\{oi^5害N$Ah|Q' 3ȭ((()C)Vޖp(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((mR(% {__W߷};zn"ܮ? w?$oD|˄|ۿjزflr# 0(((((((((((((((((((R- SX$ 3\WU/]5Z?>/fp?LUOYPA<3OƱc;.~]BvT$t74QEQE|]bWzkmBJv`g:O*~,-"􃜜?_v⾸|)sPt]>bmFAH涨+K{ 8-,` H@U^?[|dV;vh8oo fl/jĄX'3DWf=/z5u{VD$;85E;G=n- 8fr2˃$W|=xGún٥׉nK0Meϗi@Q@|oZ~&x[KvVNfF9#= Lbfѻ=5j/tKmi^١[!Lб! pUz$Z ?ēZkQ;22=~I}EQEQEQEQEQEQEa +@LJgP8x݈ϡ(]8~?VwbdgS?!ؾ>۹ԝ{2#2hߨ(cڮsCal yzg׵|Z܁|3ۜ|hWsi/5W9g?n?5}_5XռM{kyd>`?x)g5Og$OiĶ:س0\xO.Lvω5)Ps)}?~ ^0ץ" 쿹]8f<q<~~IuFWU.-!I}u?O|3|^ ]+yY6<6 I݂8#z]"**"E Ohbas+> 'L |Ƕl`J޵XQ᫡3pjoCPG丠ފ(((((((((((((nXq3q0z۾pt܏Zm]?^ޏ?%u%w_-mpiwOCY' .ga.oOGOjꨠ^æ %1>#ُʺ(Ѽǥxz+;E; l]Vb ۢ+䏊^wDйVeQ_[Ɵý\[J3c .(((((((((((((((((((((((((|g;5]Pmr qV?e PoI&6C ggʼ+^/ bKխbnX5_>|Z$`o*pzqvTQEQES';dxXdRVSE|sD ~ucسq5uoմʥd0#2O.:}mN( ,2˸)  j:} #WL]1Lb"nV${_ᖧFo6JB=t{zICx.s(h((+Ǎ=;$bFWޒS95W_f[Vϧ/};LJ~$Y^3!:$:w :U |Xڶb7#Rs;((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((mY$}\%!6M>;㿵+'MM۬W>`_'WmzɏMѨ+Cl]x;?k(((((((((((((((((((8@n>4埱-R?塚?-ߊ-"qD`?|8S0:Q@ #ѝA'^M㷆|s;Yp[(h2py+֫ƶ? 4j+J6z&,CO#k~_%iv1&+{<_r}2jWx6,/q#ǰ9?J QN?__A׵nXǴ_Oƺ_(-oI&ƶmJ # bG<EYML?djCh|9XBX澕 П5[XXYj;4mH+$_uh"$@JѸ<x#J!|jOi&VUw>evrsq<7a<j%|p4bDYɸA +|ோvlqkF5e dqNs@Tfx"fV"7ncy׋6Hu/.7=n%*OhbzGz_èlq=ogZQϟ A}rGW@:=W:wn"3/c"S((nXq7&x~"gcDze1J?O$r|.º?e?f^;e÷O,pC=?Oր>((((((i˗6߽ wξٯ,\.R\WPEV>zEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQER:,EVSنE-~ itlL:~lb ʜ-9M|3MQַukvkZYab܀ÐH9ƿPk7EEY*zg ?&O΀:F o7-#FT䣻{ /?ZL4V6}k{8/m2ȠeIټC} eɁ̉l I8'qV|{xgRc**E~^=:ZNymGo`A`$ 89nj{ QEQEVgu3%ίݭlfRr@($Hq7Ɵzsk>-]й(_⏂|}W{b2`)GpH_|4f:d{[SY3O##GjҵM?W[*نV[iVE?+OڮVU e %s@xX^[6SG=k,Rr2Sט~:oI8P#"WEPEP_<~_ 5O}Ğ58~=}! 99P~˚~(EOou/Q&!<0p~Z(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((׊cdGk+/z*= ׊,5?|?]y䄹 qLs־((((((((((((((((((((TPk/ن8|9A1 Cya9UDϯo?~9]Q X #@kQEW)/~O׳*!WEyV=";G}1Ʒ/w~q&W\8>blQ1q"j0Fœx4W p) V)^2l.Ms}9 OCi#mk(H!T`n5;?"VlGGwl36ӎ^LItqR8 NZuX6Ic8uM_M4ۄA$RrnxkfoJDS|CkhihwN`{y0p{z( Ux>:qPy;2pj RX9]aAo fSxb&Ru#qP5a#MWU0y@O|'׈ 7sM9n//Al,Am On@> &"rKEpKEQEQEQEQEQEaS{:+ȿal쟴p_߼^kf((((((ԅ~"7ǸAaa~+ӈ$A迵|ozkyN?媎?:؞DZLT ;ft w9>_{xNZUl„m٠fMO:wKi?"C kʝwb2ԟ㌟zDK=j,Yb< e 3N=zұFDġ#@T ANg?9c`ot@z/ xLPOtbs``d_ءs1@m97FktBy7skN(((_Eu?XY$$`%8<گ=u&{ U_Ȋ-WF>>hOFYQN )9 :ZsxVPXm &M Vbpxz#^\xD:W,զ"a4n <Q|{svڍ&rD1 ;,#}ܘ/U7' ^a=jԑ‰#ug \:o (K-JCX[\3eW5j?gωK&nNDeGOR7ď*6u@de5}mO\DFsd o|!?55t=8הi^f>]vHSdieEKxLQpʽ+ȘwSI.ec23OQuʊ((((((((((㭫lq]>IO-| 5=eQC_=oN3۰pZ__&נ,Rηc爫ҩ(( ( ( ( ( +^񗆼>ֽXq{V'.rku7Ν RQ Ubfp~~ xF}{He$a*r8<X >h? ܺ\WwJK0v(23+g >1A'*OFqԃMuQEQEQEQEQEQEQEQEQEQEQEQEQEش 귷2,p+'5d wT2*:|z5 b&_f/;KG {d9z((((((=?h(J6m> 'xGc &$ltR8^C_>6Ƈ 1neIlW蔈#$aAǿEaIik!1][Y3LyA<yg;mEC$"3*A Fy1~{P5 [ MH(9V^iZyu%0f c# d^Y?|My#vEϖK;?x A#b=85\/m *㺰wEykakG{ 8 -5ϧ## f* ( ( ( ( mtY!J:0e#¤fH5IzMTrֲ.?J=6-|z8G?.9M,*mb#*+$9k10ݟV5zUU ?~_cŸ:IixG9(m<^:q*Հ诃ǂ>7|Svÿ`W)#J9aF"bɟ^!>颾ggBж48ӱ7gl8?J>T|4H:UnJBgXffL`M_M]z< <7OML?}E|i}AUi++X3$dȣ<KCu.>ߢ&7|Sm/6_Lis(?}=.~ַV|E}kE|oZd(ŏP:ѵ~Tec>~St#"JKIf(z*? RP^#5 &kN?F5,:fԊ+xHS_sfƬ?j]R{ F6=u/:c7@Exѿ [>2JOC]}lg(!ln$[lwwvtYKQ^FߴW@25ɏO#uWc?J+hzVXROH9e5ُYH?Wӿ∼'`?Z͗XBbZ]O!?<XgƜ ;c>7ŭMw5(.?3oG_íП ^Pn Sǀ\dxE[μe b?%D(x41Z'I/}1#Q =OxdfA!gYO[Jj+O4?~Bt(+h6h~m:,rZ]:8d ;8I试G%EGm~enеnH>CO>&ίfsg¶-h_sm-=Bb=^` gJ̊># G|92g@U*a*)p$Hzs@(((((WD=uDI?zwqvWZ%<?(9'(uC|u>'% ;LY5r0ɂALEc_xEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP^eJijM HbnʎzmP5P}弖.qe*|jqi{w=6mc5|ESi%[Ich/"|`?Zf6hlT8ꤎ.팢!qz&Tֳ⮗Ο3)%/uGWϭR|7Nt+o{5}J[ʑim`p}_3h2̳4rU~!m>,Ա)T959mV>ee G+L?Qٛ {.⏉9d8ZR?x345?%Μ<4|R āK6 w8 Ӛ7G=/͏hT sy-t3@u^c]xJI^K8kE$Ub@1xa{WP_6۶hE2E| ͛-B=G@߱Cgឰ髹+:# P͏P 4jM}@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@}K/~#Dj=l؅FAvqF8MIw᭟-N>$\@}?ZhEΙ^$Anz] ( ( ( ( ( ( ( ( ( ( ( ( ( ?}𗃼Uuyw{X{$@~7Fw닣2l]#ŧÀťW?qN ?AԊOU P:QEQEQEQEQEQEQEQEQEQEQEbhL" s ]n|=O_2&k[P=c a۫Ca-Y x~BKZSlQ@SñǠ(ٳJsS4>,XNL69R? C!j$OtpOʻ(59OMO .eJ"5N7bG3oßk{--6|:=,o14|<Dt,׵Q@3 v~O=Y_7o*vx?gJӐ{9kj?c igތ5Q@d <?B?YFVJbm65ojߠE""*(((((((((((((((kƏ[3Te4.]hQ@H\O}6c{n|%.f?蘒!tjn#[鿫~n8!, Xa^E|/">з+V-Z/xtwKPTPɺ.-,팶Џ*96N? J< Z]᫠k6xUr(ͳ[} 0cdS05)y Ӧne UǤm~uP*)Zzݭ+*0'?2-~;x'G【k{_r@#@ڏ?+3x ږi9?in3]bӽ*\/^݇:#>y'ݪ\;imY1$ q\lQ[YHɵKĦz= ďB×2HDKGvq{pyp}+s> 4Λ2!fbIttQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEVN]v+4J}Oo9Kek8gIy[?!G?߇e?@5?LZOZG "#Op6OEr<p6 ? lz6ۈ4:0#cP>_W1T/nB$}Q)(QP>տm Z( ^LAf%5_?䙚 vb`IWxVͤKj=-^|{Y X|)Ȝ1@>鿵Լ990K$9?+~ xjmZ}Ώ!4x_Qqr#y$cV? ;~q|~(b!/7Q?0Hs>#*IW(Ocj߉Tx{IxuUA-mh-C6{n*Tt#+Shf^*ڠY'3?L89Tb;[8%oV'$SZQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@џ~,&b&`~kkCUb^3/,$o˟^Y9> xj€>((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((Ǟ+[sV|Al+ /׀J8c jT ⟲lHn03y$j((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((++6iw4_98]PM}MItoje''|E4\x㵮u'`3s_#uQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@ zcM ԰*rH뙔ZԙtEsxPJO9T?<bOuO5u4W&~$Tc c ?aEr-72|cQF_?-ҋ.(1~<|6?Y~_jg '~WoVx=](8 #_uSmZ@Oƀ>U?$W-[;<_C?*>Po}/b u"GSGӠoq﨟5P7im1hz+kkm=_kk-<ΊEky7x62{R#iS8UѠ诖S L~Iʷir;x'*Wky<d*7?`?`}eE|}X뮎,<7@|i?jΙPڴW~վ1P~Ȧ\}s!MYבTh:#8 6I=}E|[K*#B4?DWNl2q1S|מ\cs2/ U+E~s+2BS^"}Z6=̒)b?N~j/#_:V ˤ̽E)]K|*)Z g34 6@Bx SD[s_q2$n_U~3*_ O2 _~ ftl?+e(WĀtSy8IgΫ?t^XGb*XV$5~Q_xOuy>]\L T|sO]~To<)Ƹ=W2m|R?^U| ݣckl*~˞r FŞ}\&3+~a|x.4&]?alwM֕p3o=Rtojw0Khg/tkz ej0 I^[@yr#~7vjR9jP!`:??%??CK`ON_qiGA()?ρO0tA _q*7u<ϕsԇWhOʀ?B(%ayM+\d=Q'_|@(jOث.rn|Zwkzj/@~*{h"p$C%x^n}n=#||/7s@'AgNV&j ^"d۟uIlm./b-L<>>Ocey.R_"! á[s=~W ~)i5xQJ9>VWŸ%,|b<0q@4W.|7n—2dS:KgNwn?[,b?A诅-R8|kXhvS1S?}E|$,)y'td8-"ϟq~8Du@u'>6Ù=?Uk-u H\ J*Ws undkZ\syRjVK/t f/Y]:O&]]RhT4Stw֮2hn88?ʀ&@@ ( ( ( ( (*jڅj:;H^y?Š '/l/~>|rԃe9]6G6@݈^c|BlF:qnQ@q둵{ٟ1xCᥕ?{j31\R??JTQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@wFĪ6b?u9ItU̱cctB?|I_G $]:+\_m<\r ڢSSxN}AqטxH=᧹S p?YPxeM: 'LFE]o]"_VN7ZXcPuץ#Xc@(H#1qo*uaƿ/|i_=un^]7c +2cugoc1#Uk+w1,IZe:I=trb5V'o9lJԉBokؼ=BO37x~G9 Nakgsү@w|>š &R@Gdve_vo[jIF>sWДP-|<~rWkYRw3]ONQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@1AE0Tf6=-U/t-&vv7@h@ï]7>%+}ge]ytO=1s<]V_%\*.~.;ᾤ`}Y`?zE;39Ɓ"/8ao%\nFr>oS8uoq5ZoG„'\.7+(d̤>U&^2Zi_k({/RTͯ%I,idxmGG9+(㙿e^IGGii}ܖ 0}E|7'>*A0x&MCt+ (0x)*foQtܻ "u BqU>'гr?>&*rNj;4 ŏYH{"wcٜ]|cs$_Q}Yo_1 y!p/, D5 |~ :#ZџaMk9% }HAP6发09XDlh߉63\jc?r{"k*kV)S@ y_-m韵Su9{{O־ռ!`cUtjsZzh^ӣr kcx_Ơc_ﴉwDʀǎ~uzZZT55 ȣ>EY"Y?pː3V/xvOWXY#W_~|3<dm}mqb 70+ԂFy!@NkMUgR91'Nh zp+f4XQUPg?ht6fbGˎtw((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((( ǚ3: *}c5l$Wz&߀xS}dYGG(ĎTGlpM}QW xTvh]S3\[Ysͦl@A_4}̪*i:t@=[5@1N;&ۀ?L䁘` Wx{ty,f}6Up}#P *:eO [Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@2Hdhg((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((prometheus-alertmanager-0.6.2+ds/doc/examples/000077500000000000000000000000001314512360300213355ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/doc/examples/simple.yml000066400000000000000000000067131314512360300233600ustar00rootroot00000000000000global: # The smarthost and SMTP sender used for mail notifications. smtp_smarthost: 'localhost:25' smtp_from: 'alertmanager@example.org' smtp_auth_username: 'alertmanager' smtp_auth_password: 'password' # The auth token for Hipchat. hipchat_auth_token: '1234556789' # Alternative host for Hipchat. hipchat_url: 'https://hipchat.foobar.org/' # The directory from which notification templates are read. templates: - '/etc/alertmanager/template/*.tmpl' # The root route on which each incoming alert enters. route: # The labels by which incoming alerts are grouped together. For example, # multiple alerts coming in for cluster=A and alertname=LatencyHigh would # be batched into a single group. group_by: ['alertname', 'cluster', 'service'] # When a new group of alerts is created by an incoming alert, wait at # least 'group_wait' to send the initial notification. # This way ensures that you get multiple alerts for the same group that start # firing shortly after another are batched together on the first # notification. group_wait: 30s # When the first notification was sent, wait 'group_interval' to send a batch # of new alerts that started firing for that group. group_interval: 5m # If an alert has successfully been sent, wait 'repeat_interval' to # resend them. repeat_interval: 3h # A default receiver receiver: team-X-mails # All the above attributes are inherited by all child routes and can # overwritten on each. # The child route trees. routes: # This routes performs a regular expression match on alert labels to # catch alerts that are related to a list of services. - match_re: service: ^(foo1|foo2|baz)$ receiver: team-X-mails # The service has a sub-route for critical alerts, any alerts # that do not match, i.e. severity != critical, fall-back to the # parent node and are sent to 'team-X-mails' routes: - match: severity: critical receiver: team-X-pager - match: service: files receiver: team-Y-mails routes: - match: severity: critical receiver: team-Y-pager # This route handles all alerts coming from a database service. If there's # no team to handle it, it defaults to the DB team. - match: service: database receiver: team-DB-pager # Also group alerts by affected database. group_by: [alertname, cluster, database] routes: - match: owner: team-X receiver: team-X-pager - match: owner: team-Y receiver: team-Y-pager # Inhibition rules allow to mute a set of alerts given that another alert is # firing. # We use this to mute any warning-level notifications if the same alert is # already critical. inhibit_rules: - source_match: severity: 'critical' target_match: severity: 'warning' # Apply inhibition if the alertname is the same. equal: ['alertname', 'cluster', 'service'] receivers: - name: 'team-X-mails' email_configs: - to: 'team-X+alerts@example.org' - name: 'team-X-pager' email_configs: - to: 'team-X+alerts-critical@example.org' pagerduty_configs: - service_key: - name: 'team-Y-mails' email_configs: - to: 'team-Y+alerts@example.org' - name: 'team-Y-pager' pagerduty_configs: - service_key: - name: 'team-DB-pager' pagerduty_configs: - service_key: - name: 'team-X-hipchat' hipchat_configs: - auth_token: room_id: 85 message_format: html notify: true prometheus-alertmanager-0.6.2+ds/examples/000077500000000000000000000000001314512360300205705ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/examples/ha/000077500000000000000000000000001314512360300211605ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/examples/ha/alertmanager.yaml000066400000000000000000000003511314512360300245050ustar00rootroot00000000000000global: resolve_timeout: 5m route: group_by: ['alertname'] group_wait: 10s group_interval: 10s repeat_interval: 1h receiver: 'webhook' receivers: - name: 'webhook' webhook_configs: - url: 'http://127.0.0.1:5001/' prometheus-alertmanager-0.6.2+ds/examples/ha/send_alerts.sh000077500000000000000000000014531314512360300240250ustar00rootroot00000000000000alerts1='[ { "labels": { "alertname": "DiskRunningFull", "dev": "sda1", "instance": "example1" } }, { "labels": { "alertname": "DiskRunningFull", "dev": "sda2", "instance": "example1" } }, { "labels": { "alertname": "DiskRunningFull", "dev": "sda1", "instance": "example2" } }, { "labels": { "alertname": "DiskRunningFull", "dev": "sdb2", "instance": "example2" } }, { "labels": { "alertname": "DiskRunningFull", "dev": "sda1", "instance": "example3" } } ]' curl -XPOST -d"$alerts1" http://localhost:9093/api/v1/alerts curl -XPOST -d"$alerts1" http://localhost:9094/api/v1/alerts curl -XPOST -d"$alerts1" http://localhost:9095/api/v1/alerts prometheus-alertmanager-0.6.2+ds/examples/webhook/000077500000000000000000000000001314512360300222265ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/examples/webhook/echo.go000066400000000000000000000006451314512360300235000ustar00rootroot00000000000000package main import ( "bytes" "encoding/json" "fmt" "io/ioutil" "net/http" ) func main() { http.ListenAndServe(":5001", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, err := ioutil.ReadAll(r.Body) if err != nil { panic(err) } defer r.Body.Close() var buf bytes.Buffer if err := json.Indent(&buf, b, " >", " "); err != nil { panic(err) } fmt.Println(buf.String()) })) } prometheus-alertmanager-0.6.2+ds/inhibit/000077500000000000000000000000001314512360300204005ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/inhibit/inhibit.go000066400000000000000000000127451314512360300223660ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package inhibit import ( "fmt" "sync" "time" "github.com/prometheus/common/log" "github.com/prometheus/common/model" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/provider" "github.com/prometheus/alertmanager/types" ) // An Inhibitor determines whether a given label set is muted // based on the currently active alerts and a set of inhibition rules. type Inhibitor struct { alerts provider.Alerts rules []*InhibitRule marker types.Marker mtx sync.RWMutex stopc chan struct{} } // NewInhibitor returns a new Inhibitor. func NewInhibitor(ap provider.Alerts, rs []*config.InhibitRule, mk types.Marker) *Inhibitor { ih := &Inhibitor{ alerts: ap, marker: mk, } for _, cr := range rs { r := NewInhibitRule(cr) ih.rules = append(ih.rules, r) } return ih } func (ih *Inhibitor) runGC() { for { select { case <-time.After(15 * time.Minute): for _, r := range ih.rules { r.gc() } case <-ih.stopc: return } } } // Run the Inihibitor's background processing. func (ih *Inhibitor) Run() { ih.mtx.Lock() ih.stopc = make(chan struct{}) ih.mtx.Unlock() go ih.runGC() it := ih.alerts.Subscribe() defer it.Close() for { select { case <-ih.stopc: return case a := <-it.Next(): if err := it.Err(); err != nil { log.Errorf("Error iterating alerts: %s", err) continue } if a.Resolved() { // As alerts can also time out without an update, we never // handle new resolved alerts but invalidate the cache on read. continue } // Populate the inhibition rules' cache. for _, r := range ih.rules { if r.SourceMatchers.Match(a.Labels) { r.set(a) } } } } } // Stop the Inhibitor's background processing. func (ih *Inhibitor) Stop() { if ih == nil { return } ih.mtx.Lock() defer ih.mtx.Unlock() if ih.stopc != nil { close(ih.stopc) ih.stopc = nil } } // Mutes returns true iff the given label set is muted. func (ih *Inhibitor) Mutes(lset model.LabelSet) bool { fp := lset.Fingerprint() for _, r := range ih.rules { if inhibitedByFP, eq := r.hasEqual(lset); r.TargetMatchers.Match(lset) && eq { ih.marker.SetInhibited(fp, fmt.Sprintf("%d", inhibitedByFP)) return true } } ih.marker.SetInhibited(fp) return false } // An InhibitRule specifies that a class of (source) alerts should inhibit // notifications for another class of (target) alerts if all specified matching // labels are equal between the two alerts. This may be used to inhibit alerts // from sending notifications if their meaning is logically a subset of a // higher-level alert. type InhibitRule struct { // The set of Filters which define the group of source alerts (which inhibit // the target alerts). SourceMatchers types.Matchers // The set of Filters which define the group of target alerts (which are // inhibited by the source alerts). TargetMatchers types.Matchers // A set of label names whose label values need to be identical in source and // target alerts in order for the inhibition to take effect. Equal map[model.LabelName]struct{} mtx sync.RWMutex // Cache of alerts matching source labels. scache map[model.Fingerprint]*types.Alert } // NewInhibitRule returns a new InihibtRule based on a configuration definition. func NewInhibitRule(cr *config.InhibitRule) *InhibitRule { var ( sourcem types.Matchers targetm types.Matchers ) for ln, lv := range cr.SourceMatch { sourcem = append(sourcem, types.NewMatcher(model.LabelName(ln), lv)) } for ln, lv := range cr.SourceMatchRE { sourcem = append(sourcem, types.NewRegexMatcher(model.LabelName(ln), lv.Regexp)) } for ln, lv := range cr.TargetMatch { targetm = append(targetm, types.NewMatcher(model.LabelName(ln), lv)) } for ln, lv := range cr.TargetMatchRE { targetm = append(targetm, types.NewRegexMatcher(model.LabelName(ln), lv.Regexp)) } equal := map[model.LabelName]struct{}{} for _, ln := range cr.Equal { equal[ln] = struct{}{} } return &InhibitRule{ SourceMatchers: sourcem, TargetMatchers: targetm, Equal: equal, scache: map[model.Fingerprint]*types.Alert{}, } } // set the alert in the source cache. func (r *InhibitRule) set(a *types.Alert) { r.mtx.Lock() defer r.mtx.Unlock() r.scache[a.Fingerprint()] = a } // hasEqual checks whether the source cache contains alerts matching // the equal labels for the given label set. func (r *InhibitRule) hasEqual(lset model.LabelSet) (model.Fingerprint, bool) { r.mtx.RLock() defer r.mtx.RUnlock() Outer: for fp, a := range r.scache { // The cache might be stale and contain resolved alerts. if a.Resolved() { continue } for n := range r.Equal { if a.Labels[n] != lset[n] { continue Outer } } return fp, true } return model.Fingerprint(0), false } // gc clears out resolved alerts from the source cache. func (r *InhibitRule) gc() { r.mtx.Lock() defer r.mtx.Unlock() for fp, a := range r.scache { if a.Resolved() { delete(r.scache, fp) } } } prometheus-alertmanager-0.6.2+ds/inhibit/inhibit_test.go000066400000000000000000000105141314512360300234150ustar00rootroot00000000000000// Copyright 2016 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package inhibit import ( "reflect" "testing" "time" "github.com/kylelemons/godebug/pretty" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" ) func TestInhibitRuleHasEqual(t *testing.T) { now := time.Now() cases := []struct { initial map[model.Fingerprint]*types.Alert equal model.LabelNames input model.LabelSet result bool }{ { // No source alerts at all. initial: map[model.Fingerprint]*types.Alert{}, input: model.LabelSet{"a": "b"}, result: false, }, { // No equal labels, any source alerts satisfies the requirement. initial: map[model.Fingerprint]*types.Alert{1: &types.Alert{}}, input: model.LabelSet{"a": "b"}, result: true, }, { // Matching but already resolved. initial: map[model.Fingerprint]*types.Alert{ 1: &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"a": "b", "b": "f"}, StartsAt: now.Add(-time.Minute), EndsAt: now.Add(-time.Second), }, }, 2: &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"a": "b", "b": "c"}, StartsAt: now.Add(-time.Minute), EndsAt: now.Add(-time.Second), }, }, }, equal: model.LabelNames{"a", "b"}, input: model.LabelSet{"a": "b", "b": "c"}, result: false, }, { // Matching but already resolved. initial: map[model.Fingerprint]*types.Alert{ 1: &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"a": "b", "c": "d"}, StartsAt: now.Add(-time.Minute), EndsAt: now.Add(-time.Second), }, }, 2: &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"a": "b", "c": "f"}, StartsAt: now.Add(-time.Minute), EndsAt: now.Add(-time.Second), }, }, }, equal: model.LabelNames{"a"}, input: model.LabelSet{"a": "b"}, result: false, }, { // Equal label does not match. initial: map[model.Fingerprint]*types.Alert{ 1: &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"a": "c", "c": "d"}, StartsAt: now.Add(-time.Minute), EndsAt: now.Add(-time.Second), }, }, 2: &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"a": "c", "c": "f"}, StartsAt: now.Add(-time.Minute), EndsAt: now.Add(-time.Second), }, }, }, equal: model.LabelNames{"a"}, input: model.LabelSet{"a": "b"}, result: false, }, } for _, c := range cases { r := &InhibitRule{ Equal: map[model.LabelName]struct{}{}, scache: map[model.Fingerprint]*types.Alert{}, } for _, ln := range c.equal { r.Equal[ln] = struct{}{} } for k, v := range c.initial { r.scache[k] = v } if _, have := r.hasEqual(c.input); have != c.result { t.Errorf("Unexpected result %t, expected %t", have, c.result) } if !reflect.DeepEqual(r.scache, c.initial) { t.Errorf("Cache state unexpectedly changed") t.Errorf(pretty.Compare(r.scache, c.initial)) } } } func TestInhibitRuleGC(t *testing.T) { // TODO(fabxc): add now() injection function to Resolved() to remove // dependency on machine time in this test. now := time.Now() newAlert := func(start, end time.Duration) *types.Alert { return &types.Alert{ Alert: model.Alert{ Labels: model.LabelSet{"a": "b"}, StartsAt: now.Add(start * time.Minute), EndsAt: now.Add(end * time.Minute), }, } } before := map[model.Fingerprint]*types.Alert{ 0: newAlert(-10, -5), 1: newAlert(10, 20), 2: newAlert(-10, 10), 3: newAlert(-10, -1), } after := map[model.Fingerprint]*types.Alert{ 1: newAlert(10, 20), 2: newAlert(-10, 10), } r := &InhibitRule{scache: before} r.gc() if !reflect.DeepEqual(r.scache, after) { t.Errorf("Unexpected cache state after GC") t.Errorf(pretty.Compare(r.scache, after)) } } prometheus-alertmanager-0.6.2+ds/nflog/000077500000000000000000000000001314512360300200575ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/nflog/nflog.go000066400000000000000000000362331314512360300215220ustar00rootroot00000000000000// Copyright 2016 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package nflog implements a garbage-collected and snapshottable append-only log of // active/resolved notifications. Each log entry stores the active/resolved state, // the notified receiver, and a hash digest of the notification's identifying contents. // The log can be queried along different paramters. package nflog import ( "bytes" "errors" "fmt" "io" "math/rand" "os" "sync" "time" "github.com/matttproud/golang_protobuf_extensions/pbutil" pb "github.com/prometheus/alertmanager/nflog/nflogpb" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/log" "github.com/weaveworks/mesh" ) // ErrNotFound is returned for empty query results. var ErrNotFound = errors.New("not found") // Log stores and serves information about notifications // about byte-slice addressed alert objects to different receivers. type Log interface { // The Log* methods store a notification log entry for // a fully qualified receiver and a given IDs identifying the // alert object. Log(r *pb.Receiver, key string, firing, resolved []uint64) error // Query the log along the given Paramteres. // // TODO(fabxc): // - extend the interface by a `QueryOne` method? // - return an iterator rather than a materialized list? Query(p ...QueryParam) ([]*pb.Entry, error) // Snapshot the current log state and return the number // of bytes written. Snapshot(w io.Writer) (int, error) // GC removes expired entries from the log. It returns // the total number of deleted entries. GC() (int, error) } // query currently allows filtering by and/or receiver group key. // It is configured via QueryParameter functions. // // TODO(fabxc): Future versions could allow querying a certain receiver // group or a given time interval. type query struct { recv *pb.Receiver groupKey string } // QueryParam is a function that modifies a query to incorporate // a set of parameters. Returns an error for invalid or conflicting // parameters. type QueryParam func(*query) error // QReceiver adds a receiver parameter to a query. func QReceiver(r *pb.Receiver) QueryParam { return func(q *query) error { q.recv = r return nil } } // QGroupKey adds a group key as querying argument. func QGroupKey(gk string) QueryParam { return func(q *query) error { q.groupKey = gk return nil } } type nlog struct { logger log.Logger metrics *metrics now func() time.Time retention time.Duration runInterval time.Duration snapf string stopc chan struct{} done func() gossip mesh.Gossip // gossip channel for sharing log state. // For now we only store the most recently added log entry. // The key is a serialized concatenation of group key and receiver. // Currently our memory state is equivalent to the mesh.GossipData // representation. This may change in the future as we support history // and indexing. mtx sync.RWMutex st gossipData } type metrics struct { gcDuration prometheus.Summary snapshotDuration prometheus.Summary queriesTotal prometheus.Counter queryErrorsTotal prometheus.Counter queryDuration prometheus.Histogram } func newMetrics(r prometheus.Registerer) *metrics { m := &metrics{} m.gcDuration = prometheus.NewSummary(prometheus.SummaryOpts{ Name: "alertmanager_nflog_gc_duration_seconds", Help: "Duration of the last notification log garbage collection cycle.", }) m.snapshotDuration = prometheus.NewSummary(prometheus.SummaryOpts{ Name: "alertmanager_nflog_snapshot_duration_seconds", Help: "Duration of the last notification log snapshot.", }) m.queriesTotal = prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_nflog_queries_total", Help: "Number of notification log queries were received.", }) m.queryErrorsTotal = prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_nflog_query_errors_total", Help: "Number notification log received queries that failed.", }) m.queryDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ Name: "alertmanager_nflog_query_duration_seconds", Help: "Duration of notification log query evaluation.", }) if r != nil { r.MustRegister( m.gcDuration, m.snapshotDuration, m.queriesTotal, m.queryErrorsTotal, m.queryDuration, ) } return m } // Option configures a new Log implementation. type Option func(*nlog) error // WithMesh registers the log with a mesh network with which // the log state will be shared. func WithMesh(create func(g mesh.Gossiper) mesh.Gossip) Option { return func(l *nlog) error { l.gossip = create(l) return nil } } // WithRetention sets the retention time for log st. func WithRetention(d time.Duration) Option { return func(l *nlog) error { l.retention = d return nil } } // WithNow overwrites the function used to retrieve a timestamp // for the current point in time. // This is generally useful for injection during tests. func WithNow(f func() time.Time) Option { return func(l *nlog) error { l.now = f return nil } } // WithLogger configures a logger for the notification log. func WithLogger(logger log.Logger) Option { return func(l *nlog) error { l.logger = logger return nil } } // WithMetrics registers metrics for the notification log. func WithMetrics(r prometheus.Registerer) Option { return func(l *nlog) error { l.metrics = newMetrics(r) return nil } } // WithMaintenance configures the Log to run garbage collection // and snapshotting, if configured, at the given interval. // // The maintenance terminates on receiving from the provided channel. // The done function is called after the final snapshot was completed. func WithMaintenance(d time.Duration, stopc chan struct{}, done func()) Option { return func(l *nlog) error { if d == 0 { return fmt.Errorf("maintenance interval must not be 0") } l.runInterval = d l.stopc = stopc l.done = done return nil } } // WithSnapshot configures the log to be initialized from a given snapshot file. // If maintenance is configured, a snapshot will be saved periodically and on // shutdown as well. func WithSnapshot(sf string) Option { return func(l *nlog) error { l.snapf = sf return nil } } func utcNow() time.Time { return time.Now().UTC() } // New creates a new notification log based on the provided options. // The snapshot is loaded into the Log if it is set. func New(opts ...Option) (Log, error) { l := &nlog{ logger: log.NewNopLogger(), now: utcNow, st: map[string]*pb.MeshEntry{}, } for _, o := range opts { if err := o(l); err != nil { return nil, err } } if l.metrics == nil { l.metrics = newMetrics(nil) } if l.snapf != "" { if f, err := os.Open(l.snapf); !os.IsNotExist(err) { if err != nil { return l, err } defer f.Close() if err := l.loadSnapshot(f); err != nil { return l, err } } } go l.run() return l, nil } // run periodic background maintenance. func (l *nlog) run() { if l.runInterval == 0 || l.stopc == nil { return } t := time.NewTicker(l.runInterval) defer t.Stop() if l.done != nil { defer l.done() } f := func() error { start := l.now() l.logger.Info("running maintenance") defer l.logger.With("duration", l.now().Sub(start)).Info("maintenance done") if _, err := l.GC(); err != nil { return err } if l.snapf == "" { return nil } f, err := openReplace(l.snapf) if err != nil { return err } // TODO(fabxc): potentially expose snapshot size in log message. if _, err := l.Snapshot(f); err != nil { return err } return f.Close() } Loop: for { select { case <-l.stopc: break Loop case <-t.C: if err := f(); err != nil { l.logger.With("err", err).Error("running maintenance failed") } } } // No need to run final maintenance if we don't want to snapshot. if l.snapf == "" { return } if err := f(); err != nil { l.logger.With("err", err).Error("creating shutdown snapshot failed") } } func receiverKey(r *pb.Receiver) string { return fmt.Sprintf("%s/%s/%d", r.GroupName, r.Integration, r.Idx) } // stateKey returns a string key for a log entry consisting of the group key // and receiver. func stateKey(k string, r *pb.Receiver) string { return fmt.Sprintf("%s:%s", k, receiverKey(r)) } func (l *nlog) Log(r *pb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64) error { // Write all st with the same timestamp. now := l.now() key := stateKey(gkey, r) l.mtx.Lock() defer l.mtx.Unlock() if prevle, ok := l.st[key]; ok { // Entry already exists, only overwrite if timestamp is newer. // This may happen with raciness or clock-drift across AM nodes. if prevle.Entry.Timestamp.After(now) { return nil } } e := &pb.MeshEntry{ Entry: &pb.Entry{ Receiver: r, GroupKey: []byte(gkey), Timestamp: now, FiringAlerts: firingAlerts, ResolvedAlerts: resolvedAlerts, }, ExpiresAt: now.Add(l.retention), } l.gossip.GossipBroadcast(gossipData{ key: e, }) l.st[key] = e return nil } // GC implements the Log interface. func (l *nlog) GC() (int, error) { start := time.Now() defer func() { l.metrics.gcDuration.Observe(time.Since(start).Seconds()) }() now := l.now() var n int l.mtx.Lock() defer l.mtx.Unlock() for k, le := range l.st { if le.ExpiresAt.IsZero() { return n, errors.New("unexpected zero expiration timestamp") } if !le.ExpiresAt.After(now) { delete(l.st, k) n++ } } return n, nil } // Query implements the Log interface. func (l *nlog) Query(params ...QueryParam) ([]*pb.Entry, error) { start := time.Now() l.metrics.queriesTotal.Inc() entries, err := func() ([]*pb.Entry, error) { q := &query{} for _, p := range params { if err := p(q); err != nil { return nil, err } } // TODO(fabxc): For now our only query mode is the most recent entry for a // receiver/group_key combination. if q.recv == nil || q.groupKey == "" { // TODO(fabxc): allow more complex queries in the future. // How to enable pagination? return nil, errors.New("no query parameters specified") } l.mtx.RLock() defer l.mtx.RUnlock() if le, ok := l.st[stateKey(q.groupKey, q.recv)]; ok { return []*pb.Entry{le.Entry}, nil } return nil, ErrNotFound }() if err != nil { l.metrics.queryErrorsTotal.Inc() } l.metrics.queryDuration.Observe(time.Since(start).Seconds()) return entries, err } // loadSnapshot loads a snapshot generated by Snapshot() into the state. func (l *nlog) loadSnapshot(r io.Reader) error { l.mtx.Lock() defer l.mtx.Unlock() st := gossipData{} for { var e pb.MeshEntry if _, err := pbutil.ReadDelimited(r, &e); err != nil { if err == io.EOF { break } return err } st[stateKey(string(e.Entry.GroupKey), e.Entry.Receiver)] = &e } l.st = st return nil } // Snapshot implements the Log interface. func (l *nlog) Snapshot(w io.Writer) (int, error) { start := time.Now() defer func() { l.metrics.snapshotDuration.Observe(time.Since(start).Seconds()) }() l.mtx.RLock() defer l.mtx.RUnlock() var n int for _, e := range l.st { m, err := pbutil.WriteDelimited(w, e) if err != nil { return n + m, err } n += m } return n, nil } // Gossip implements the mesh.Gossiper interface. func (l *nlog) Gossip() mesh.GossipData { l.mtx.RLock() defer l.mtx.RUnlock() gd := make(gossipData, len(l.st)) for k, v := range l.st { gd[k] = v } return gd } // OnGossip implements the mesh.Gossiper interface. func (l *nlog) OnGossip(msg []byte) (mesh.GossipData, error) { gd, err := decodeGossipData(msg) if err != nil { return nil, err } l.mtx.Lock() defer l.mtx.Unlock() if delta := l.st.mergeDelta(gd); len(delta) > 0 { return delta, nil } return nil, nil } // OnGossipBroadcast implements the mesh.Gossiper interface. func (l *nlog) OnGossipBroadcast(src mesh.PeerName, msg []byte) (mesh.GossipData, error) { gd, err := decodeGossipData(msg) if err != nil { return nil, err } l.mtx.Lock() defer l.mtx.Unlock() return l.st.mergeDelta(gd), nil } // OnGossipUnicast implements the mesh.Gossiper interface. func (l *nlog) OnGossipUnicast(src mesh.PeerName, msg []byte) error { panic("not implemented") } // gossipData is a representation of the current log state that // implements the mesh.GossipData interface. type gossipData map[string]*pb.MeshEntry func decodeGossipData(msg []byte) (gossipData, error) { gd := gossipData{} rd := bytes.NewReader(msg) for { var e pb.MeshEntry if _, err := pbutil.ReadDelimited(rd, &e); err != nil { if err == io.EOF { break } return gd, err } gd[stateKey(string(e.Entry.GroupKey), e.Entry.Receiver)] = &e } return gd, nil } // Encode implements the mesh.GossipData interface. func (gd gossipData) Encode() [][]byte { // Split into sub-messages of ~1MB. const maxSize = 1024 * 1024 var ( buf bytes.Buffer res [][]byte n int ) for _, e := range gd { m, err := pbutil.WriteDelimited(&buf, e) n += m if err != nil { // TODO(fabxc): log error and skip entry. Or can this really not happen with a bytes.Buffer? panic(err) } if n > maxSize { res = append(res, buf.Bytes()) buf = bytes.Buffer{} } } if buf.Len() > 0 { res = append(res, buf.Bytes()) } return res } func (gd gossipData) clone() gossipData { res := make(gossipData, len(gd)) for k, e := range gd { res[k] = e } return res } // Merge the notification set with gossip data and return a new notification // state. // TODO(fabxc): can we just return the receiver. Does it have to remain // unmodified. Needs to be clarified upstream. func (gd gossipData) Merge(other mesh.GossipData) mesh.GossipData { for k, e := range other.(gossipData) { prev, ok := gd[k] if !ok { gd[k] = e continue } if prev.Entry.Timestamp.Before(e.Entry.Timestamp) { gd[k] = e } } return gd } // mergeDelta behaves like Merge but returns a gossipData only containing // things that have changed. func (gd gossipData) mergeDelta(od gossipData) gossipData { delta := gossipData{} for k, e := range od { prev, ok := gd[k] if !ok { gd[k] = e delta[k] = e continue } if prev.Entry.Timestamp.Before(e.Entry.Timestamp) { gd[k] = e delta[k] = e } } return delta } // replaceFile wraps a file that is moved to another filename on closing. type replaceFile struct { *os.File filename string } func (f *replaceFile) Close() error { if err := f.File.Sync(); err != nil { return err } if err := f.File.Close(); err != nil { return err } return os.Rename(f.File.Name(), f.filename) } // openReplace opens a new temporary file that is moved to filename on closing. func openReplace(filename string) (*replaceFile, error) { tmpFilename := fmt.Sprintf("%s.%x", filename, uint64(rand.Int63())) f, err := os.Create(tmpFilename) if err != nil { return nil, err } rf := &replaceFile{ File: f, filename: filename, } return rf, nil } prometheus-alertmanager-0.6.2+ds/nflog/nflog_test.go000066400000000000000000000167111314512360300225600ustar00rootroot00000000000000// Copyright 2016 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package nflog import ( "io/ioutil" "os" "path/filepath" "testing" "time" pb "github.com/prometheus/alertmanager/nflog/nflogpb" "github.com/stretchr/testify/require" ) func TestNlogGC(t *testing.T) { now := utcNow() // We only care about key names and expiration timestamps. newEntry := func(ts time.Time) *pb.MeshEntry { return &pb.MeshEntry{ ExpiresAt: ts, } } l := &nlog{ st: gossipData{ "a1": newEntry(now), "a2": newEntry(now.Add(time.Second)), "a3": newEntry(now.Add(-time.Second)), }, now: func() time.Time { return now }, metrics: newMetrics(nil), } n, err := l.GC() require.NoError(t, err, "unexpected error in garbage collection") require.Equal(t, 2, n, "unexpected number of removed entries") expected := gossipData{ "a2": newEntry(now.Add(time.Second)), } require.Equal(t, l.st, expected, "unepexcted state after garbage collection") } func TestNlogSnapshot(t *testing.T) { // Check whether storing and loading the snapshot is symmetric. now := utcNow() cases := []struct { entries []*pb.MeshEntry }{ { entries: []*pb.MeshEntry{ { Entry: &pb.Entry{ GroupKey: []byte("d8e8fca2dc0f896fd7cb4cb0031ba249"), Receiver: &pb.Receiver{GroupName: "abc", Integration: "test1", Idx: 1}, GroupHash: []byte("126a8a51b9d1bbd07fddc65819a542c3"), Resolved: false, Timestamp: now, }, ExpiresAt: now, }, { Entry: &pb.Entry{ GroupKey: []byte("d8e8fca2dc0f8abce7cb4cb0031ba249"), Receiver: &pb.Receiver{GroupName: "def", Integration: "test2", Idx: 29}, GroupHash: []byte("122c2331b9d1bbd07fddc65819a542c3"), Resolved: true, Timestamp: now, }, ExpiresAt: now, }, { Entry: &pb.Entry{ GroupKey: []byte("aaaaaca2dc0f896fd7cb4cb0031ba249"), Receiver: &pb.Receiver{GroupName: "ghi", Integration: "test3", Idx: 0}, GroupHash: []byte("126a8a51b9d1bbd07fddc6e3e3e542c3"), Resolved: false, Timestamp: now, }, ExpiresAt: now, }, }, }, } for _, c := range cases { f, err := ioutil.TempFile("", "snapshot") require.NoError(t, err, "creating temp file failed") l1 := &nlog{ st: gossipData{}, metrics: newMetrics(nil), } // Setup internal state manually. for _, e := range c.entries { l1.st[stateKey(string(e.Entry.GroupKey), e.Entry.Receiver)] = e } _, err = l1.Snapshot(f) require.NoError(t, err, "creating snapshot failed") require.NoError(t, f.Close(), "closing snapshot file failed") f, err = os.Open(f.Name()) require.NoError(t, err, "opening snapshot file failed") // Check again against new nlog instance. l2 := &nlog{} err = l2.loadSnapshot(f) require.NoError(t, err, "error loading snapshot") require.Equal(t, l1.st, l2.st, "state after loading snapshot did not match snapshotted state") require.NoError(t, f.Close(), "closing snapshot file failed") } } func TestReplaceFile(t *testing.T) { dir, err := ioutil.TempDir("", "replace_file") require.NoError(t, err, "creating temp dir failed") origFilename := filepath.Join(dir, "testfile") of, err := os.Create(origFilename) require.NoError(t, err, "creating file failed") nf, err := openReplace(origFilename) require.NoError(t, err, "opening replacement file failed") _, err = nf.Write([]byte("test")) require.NoError(t, err, "writing replace file failed") require.NotEqual(t, nf.Name(), of.Name(), "replacement file must have different name while editing") require.NoError(t, nf.Close(), "closing replacement file failed") require.NoError(t, of.Close(), "closing original file failed") ofr, err := os.Open(origFilename) require.NoError(t, err, "opening original file failed") defer ofr.Close() res, err := ioutil.ReadAll(ofr) require.NoError(t, err, "reading original file failed") require.Equal(t, "test", string(res), "unexpected file contents") } func TestGossipDataMerge(t *testing.T) { now := utcNow() // We only care about key names and timestamps for the // merging logic. newEntry := func(ts time.Time) *pb.MeshEntry { return &pb.MeshEntry{ Entry: &pb.Entry{Timestamp: ts}, } } cases := []struct { a, b gossipData final, delta gossipData }{ { a: gossipData{ "a1": newEntry(now), "a2": newEntry(now), "a3": newEntry(now), }, b: gossipData{ "b1": newEntry(now), // new key, should be added "a2": newEntry(now.Add(-time.Minute)), // older timestamp, should be dropped "a3": newEntry(now.Add(time.Minute)), // newer timestamp, should overwrite }, final: gossipData{ "a1": newEntry(now), "a2": newEntry(now), "a3": newEntry(now.Add(time.Minute)), "b1": newEntry(now), }, delta: gossipData{ "b1": newEntry(now), "a3": newEntry(now.Add(time.Minute)), }, }, } for _, c := range cases { ca, cb := c.a.clone(), c.b.clone() res := ca.Merge(cb) require.Equal(t, c.final, res, "Merge result should match expectation") require.Equal(t, c.final, ca, "Merge should apply changes to original state") require.Equal(t, c.b, cb, "Merged state should remain unmodified") ca, cb = c.a.clone(), c.b.clone() delta := ca.mergeDelta(cb) require.Equal(t, c.delta, delta, "Merge delta should match expectation") require.Equal(t, c.final, ca, "Merge should apply changes to original state") require.Equal(t, c.b, cb, "Merged state should remain unmodified") } } func TestGossipDataCoding(t *testing.T) { // Check whether encoding and decoding the data is symmetric. now := utcNow() cases := []struct { entries []*pb.MeshEntry }{ { entries: []*pb.MeshEntry{ { Entry: &pb.Entry{ GroupKey: []byte("d8e8fca2dc0f896fd7cb4cb0031ba249"), Receiver: &pb.Receiver{GroupName: "abc", Integration: "test1", Idx: 1}, GroupHash: []byte("126a8a51b9d1bbd07fddc65819a542c3"), Resolved: false, Timestamp: now, }, ExpiresAt: now, }, { Entry: &pb.Entry{ GroupKey: []byte("d8e8fca2dc0f8abce7cb4cb0031ba249"), Receiver: &pb.Receiver{GroupName: "def", Integration: "test2", Idx: 29}, GroupHash: []byte("122c2331b9d1bbd07fddc65819a542c3"), Resolved: true, Timestamp: now, }, ExpiresAt: now, }, { Entry: &pb.Entry{ GroupKey: []byte("aaaaaca2dc0f896fd7cb4cb0031ba249"), Receiver: &pb.Receiver{GroupName: "ghi", Integration: "test3", Idx: 0}, GroupHash: []byte("126a8a51b9d1bbd07fddc6e3e3e542c3"), Resolved: false, Timestamp: now, }, ExpiresAt: now, }, }, }, } for _, c := range cases { // Create gossip data from input. in := gossipData{} for _, e := range c.entries { in[stateKey(string(e.Entry.GroupKey), e.Entry.Receiver)] = e } msg := in.Encode() require.Equal(t, 1, len(msg), "expected single message for input") out, err := decodeGossipData(msg[0]) require.NoError(t, err, "decoding message failed") require.Equal(t, in, out, "decoded data doesn't match encoded data") } } prometheus-alertmanager-0.6.2+ds/nflog/nflogpb/000077500000000000000000000000001314512360300215065ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/nflog/nflogpb/nflog.pb.go000066400000000000000000000636671314512360300235640ustar00rootroot00000000000000// Code generated by protoc-gen-gogo. // source: nflog.proto // DO NOT EDIT! /* Package nflogpb is a generated protocol buffer package. It is generated from these files: nflog.proto It has these top-level messages: Receiver Entry MeshEntry */ package nflogpb import proto "github.com/gogo/protobuf/proto" import fmt "fmt" import math "math" import time "time" import github_com_gogo_protobuf_types "github.com/gogo/protobuf/types" import io "io" // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf var _ = time.Kitchen // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package type Receiver struct { // Configured name of the receiver group. GroupName string `protobuf:"bytes,1,opt,name=group_name,json=groupName,proto3" json:"group_name,omitempty"` // Name of the integration of the receiver. Integration string `protobuf:"bytes,2,opt,name=integration,proto3" json:"integration,omitempty"` // Index of the receiver with respect to the integration. // Every integration in a group may have 0..N configurations. Idx uint32 `protobuf:"varint,3,opt,name=idx,proto3" json:"idx,omitempty"` } func (m *Receiver) Reset() { *m = Receiver{} } func (m *Receiver) String() string { return proto.CompactTextString(m) } func (*Receiver) ProtoMessage() {} func (*Receiver) Descriptor() ([]byte, []int) { return fileDescriptorNflog, []int{0} } // Entry holds information about a successful notification // sent to a receiver. type Entry struct { // The key identifying the dispatching group. GroupKey []byte `protobuf:"bytes,1,opt,name=group_key,json=groupKey,proto3" json:"group_key,omitempty"` // The receiver that was notified. Receiver *Receiver `protobuf:"bytes,2,opt,name=receiver" json:"receiver,omitempty"` // Hash over the state of the group at notification time. // Deprecated in favor of FiringAlerts field, but kept for compatibility. GroupHash []byte `protobuf:"bytes,3,opt,name=group_hash,json=groupHash,proto3" json:"group_hash,omitempty"` // Whether the notification was about a resolved alert. // Deprecated in favor of ResolvedAlerts field, but kept for compatibility. Resolved bool `protobuf:"varint,4,opt,name=resolved,proto3" json:"resolved,omitempty"` // Timestamp of the succeeding notification. Timestamp time.Time `protobuf:"bytes,5,opt,name=timestamp,stdtime" json:"timestamp"` // FiringAlerts list of hashes of firing alerts at the last notification time. FiringAlerts []uint64 `protobuf:"varint,6,rep,packed,name=firing_alerts,json=firingAlerts" json:"firing_alerts,omitempty"` // ResolvedAlerts list of hashes of resolved alerts at the last notification time. ResolvedAlerts []uint64 `protobuf:"varint,7,rep,packed,name=resolved_alerts,json=resolvedAlerts" json:"resolved_alerts,omitempty"` } func (m *Entry) Reset() { *m = Entry{} } func (m *Entry) String() string { return proto.CompactTextString(m) } func (*Entry) ProtoMessage() {} func (*Entry) Descriptor() ([]byte, []int) { return fileDescriptorNflog, []int{1} } // MeshEntry is a wrapper message to communicate a notify log // entry through a mesh network. type MeshEntry struct { // The original raw notify log entry. Entry *Entry `protobuf:"bytes,1,opt,name=entry" json:"entry,omitempty"` // A timestamp indicating when the mesh peer should evict // the log entry from its state. ExpiresAt time.Time `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt,stdtime" json:"expires_at"` } func (m *MeshEntry) Reset() { *m = MeshEntry{} } func (m *MeshEntry) String() string { return proto.CompactTextString(m) } func (*MeshEntry) ProtoMessage() {} func (*MeshEntry) Descriptor() ([]byte, []int) { return fileDescriptorNflog, []int{2} } func init() { proto.RegisterType((*Receiver)(nil), "nflogpb.Receiver") proto.RegisterType((*Entry)(nil), "nflogpb.Entry") proto.RegisterType((*MeshEntry)(nil), "nflogpb.MeshEntry") } func (m *Receiver) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Receiver) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if len(m.GroupName) > 0 { dAtA[i] = 0xa i++ i = encodeVarintNflog(dAtA, i, uint64(len(m.GroupName))) i += copy(dAtA[i:], m.GroupName) } if len(m.Integration) > 0 { dAtA[i] = 0x12 i++ i = encodeVarintNflog(dAtA, i, uint64(len(m.Integration))) i += copy(dAtA[i:], m.Integration) } if m.Idx != 0 { dAtA[i] = 0x18 i++ i = encodeVarintNflog(dAtA, i, uint64(m.Idx)) } return i, nil } func (m *Entry) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Entry) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if len(m.GroupKey) > 0 { dAtA[i] = 0xa i++ i = encodeVarintNflog(dAtA, i, uint64(len(m.GroupKey))) i += copy(dAtA[i:], m.GroupKey) } if m.Receiver != nil { dAtA[i] = 0x12 i++ i = encodeVarintNflog(dAtA, i, uint64(m.Receiver.Size())) n1, err := m.Receiver.MarshalTo(dAtA[i:]) if err != nil { return 0, err } i += n1 } if len(m.GroupHash) > 0 { dAtA[i] = 0x1a i++ i = encodeVarintNflog(dAtA, i, uint64(len(m.GroupHash))) i += copy(dAtA[i:], m.GroupHash) } if m.Resolved { dAtA[i] = 0x20 i++ if m.Resolved { dAtA[i] = 1 } else { dAtA[i] = 0 } i++ } dAtA[i] = 0x2a i++ i = encodeVarintNflog(dAtA, i, uint64(github_com_gogo_protobuf_types.SizeOfStdTime(m.Timestamp))) n2, err := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.Timestamp, dAtA[i:]) if err != nil { return 0, err } i += n2 if len(m.FiringAlerts) > 0 { dAtA4 := make([]byte, len(m.FiringAlerts)*10) var j3 int for _, num := range m.FiringAlerts { for num >= 1<<7 { dAtA4[j3] = uint8(uint64(num)&0x7f | 0x80) num >>= 7 j3++ } dAtA4[j3] = uint8(num) j3++ } dAtA[i] = 0x32 i++ i = encodeVarintNflog(dAtA, i, uint64(j3)) i += copy(dAtA[i:], dAtA4[:j3]) } if len(m.ResolvedAlerts) > 0 { dAtA6 := make([]byte, len(m.ResolvedAlerts)*10) var j5 int for _, num := range m.ResolvedAlerts { for num >= 1<<7 { dAtA6[j5] = uint8(uint64(num)&0x7f | 0x80) num >>= 7 j5++ } dAtA6[j5] = uint8(num) j5++ } dAtA[i] = 0x3a i++ i = encodeVarintNflog(dAtA, i, uint64(j5)) i += copy(dAtA[i:], dAtA6[:j5]) } return i, nil } func (m *MeshEntry) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *MeshEntry) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if m.Entry != nil { dAtA[i] = 0xa i++ i = encodeVarintNflog(dAtA, i, uint64(m.Entry.Size())) n7, err := m.Entry.MarshalTo(dAtA[i:]) if err != nil { return 0, err } i += n7 } dAtA[i] = 0x12 i++ i = encodeVarintNflog(dAtA, i, uint64(github_com_gogo_protobuf_types.SizeOfStdTime(m.ExpiresAt))) n8, err := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.ExpiresAt, dAtA[i:]) if err != nil { return 0, err } i += n8 return i, nil } func encodeFixed64Nflog(dAtA []byte, offset int, v uint64) int { dAtA[offset] = uint8(v) dAtA[offset+1] = uint8(v >> 8) dAtA[offset+2] = uint8(v >> 16) dAtA[offset+3] = uint8(v >> 24) dAtA[offset+4] = uint8(v >> 32) dAtA[offset+5] = uint8(v >> 40) dAtA[offset+6] = uint8(v >> 48) dAtA[offset+7] = uint8(v >> 56) return offset + 8 } func encodeFixed32Nflog(dAtA []byte, offset int, v uint32) int { dAtA[offset] = uint8(v) dAtA[offset+1] = uint8(v >> 8) dAtA[offset+2] = uint8(v >> 16) dAtA[offset+3] = uint8(v >> 24) return offset + 4 } func encodeVarintNflog(dAtA []byte, offset int, v uint64) int { for v >= 1<<7 { dAtA[offset] = uint8(v&0x7f | 0x80) v >>= 7 offset++ } dAtA[offset] = uint8(v) return offset + 1 } func (m *Receiver) Size() (n int) { var l int _ = l l = len(m.GroupName) if l > 0 { n += 1 + l + sovNflog(uint64(l)) } l = len(m.Integration) if l > 0 { n += 1 + l + sovNflog(uint64(l)) } if m.Idx != 0 { n += 1 + sovNflog(uint64(m.Idx)) } return n } func (m *Entry) Size() (n int) { var l int _ = l l = len(m.GroupKey) if l > 0 { n += 1 + l + sovNflog(uint64(l)) } if m.Receiver != nil { l = m.Receiver.Size() n += 1 + l + sovNflog(uint64(l)) } l = len(m.GroupHash) if l > 0 { n += 1 + l + sovNflog(uint64(l)) } if m.Resolved { n += 2 } l = github_com_gogo_protobuf_types.SizeOfStdTime(m.Timestamp) n += 1 + l + sovNflog(uint64(l)) if len(m.FiringAlerts) > 0 { l = 0 for _, e := range m.FiringAlerts { l += sovNflog(uint64(e)) } n += 1 + sovNflog(uint64(l)) + l } if len(m.ResolvedAlerts) > 0 { l = 0 for _, e := range m.ResolvedAlerts { l += sovNflog(uint64(e)) } n += 1 + sovNflog(uint64(l)) + l } return n } func (m *MeshEntry) Size() (n int) { var l int _ = l if m.Entry != nil { l = m.Entry.Size() n += 1 + l + sovNflog(uint64(l)) } l = github_com_gogo_protobuf_types.SizeOfStdTime(m.ExpiresAt) n += 1 + l + sovNflog(uint64(l)) return n } func sovNflog(x uint64) (n int) { for { n++ x >>= 7 if x == 0 { break } } return n } func sozNflog(x uint64) (n int) { return sovNflog(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } func (m *Receiver) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Receiver: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Receiver: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field GroupName", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + intStringLen if postIndex > l { return io.ErrUnexpectedEOF } m.GroupName = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Integration", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + intStringLen if postIndex > l { return io.ErrUnexpectedEOF } m.Integration = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 3: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field Idx", wireType) } m.Idx = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ m.Idx |= (uint32(b) & 0x7F) << shift if b < 0x80 { break } } default: iNdEx = preIndex skippy, err := skipNflog(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthNflog } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *Entry) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Entry: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Entry: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field GroupKey", wireType) } var byteLen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ byteLen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if byteLen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + byteLen if postIndex > l { return io.ErrUnexpectedEOF } m.GroupKey = append(m.GroupKey[:0], dAtA[iNdEx:postIndex]...) if m.GroupKey == nil { m.GroupKey = []byte{} } iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Receiver", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if m.Receiver == nil { m.Receiver = &Receiver{} } if err := m.Receiver.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field GroupHash", wireType) } var byteLen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ byteLen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if byteLen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + byteLen if postIndex > l { return io.ErrUnexpectedEOF } m.GroupHash = append(m.GroupHash[:0], dAtA[iNdEx:postIndex]...) if m.GroupHash == nil { m.GroupHash = []byte{} } iNdEx = postIndex case 4: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field Resolved", wireType) } var v int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= (int(b) & 0x7F) << shift if b < 0x80 { break } } m.Resolved = bool(v != 0) case 5: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.Timestamp, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 6: if wireType == 0 { var v uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } m.FiringAlerts = append(m.FiringAlerts, v) } else if wireType == 2 { var packedLen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ packedLen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if packedLen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + packedLen if postIndex > l { return io.ErrUnexpectedEOF } for iNdEx < postIndex { var v uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } m.FiringAlerts = append(m.FiringAlerts, v) } } else { return fmt.Errorf("proto: wrong wireType = %d for field FiringAlerts", wireType) } case 7: if wireType == 0 { var v uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } m.ResolvedAlerts = append(m.ResolvedAlerts, v) } else if wireType == 2 { var packedLen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ packedLen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if packedLen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + packedLen if postIndex > l { return io.ErrUnexpectedEOF } for iNdEx < postIndex { var v uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ v |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } m.ResolvedAlerts = append(m.ResolvedAlerts, v) } } else { return fmt.Errorf("proto: wrong wireType = %d for field ResolvedAlerts", wireType) } default: iNdEx = preIndex skippy, err := skipNflog(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthNflog } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *MeshEntry) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: MeshEntry: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: MeshEntry: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Entry", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if m.Entry == nil { m.Entry = &Entry{} } if err := m.Entry.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field ExpiresAt", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowNflog } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthNflog } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.ExpiresAt, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipNflog(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthNflog } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func skipNflog(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 for iNdEx < l { var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowNflog } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } wireType := int(wire & 0x7) switch wireType { case 0: for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowNflog } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } iNdEx++ if dAtA[iNdEx-1] < 0x80 { break } } return iNdEx, nil case 1: iNdEx += 8 return iNdEx, nil case 2: var length int for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowNflog } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ length |= (int(b) & 0x7F) << shift if b < 0x80 { break } } iNdEx += length if length < 0 { return 0, ErrInvalidLengthNflog } return iNdEx, nil case 3: for { var innerWire uint64 var start int = iNdEx for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowNflog } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ innerWire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } innerWireType := int(innerWire & 0x7) if innerWireType == 4 { break } next, err := skipNflog(dAtA[start:]) if err != nil { return 0, err } iNdEx = start + next } return iNdEx, nil case 4: return iNdEx, nil case 5: iNdEx += 4 return iNdEx, nil default: return 0, fmt.Errorf("proto: illegal wireType %d", wireType) } } panic("unreachable") } var ( ErrInvalidLengthNflog = fmt.Errorf("proto: negative length found during unmarshaling") ErrIntOverflowNflog = fmt.Errorf("proto: integer overflow") ) func init() { proto.RegisterFile("nflog.proto", fileDescriptorNflog) } var fileDescriptorNflog = []byte{ // 385 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x90, 0xcf, 0x6e, 0xd3, 0x40, 0x10, 0xc6, 0xbb, 0x4d, 0xd3, 0xda, 0xe3, 0xb4, 0x94, 0x15, 0x07, 0xcb, 0x08, 0xc7, 0x0a, 0x48, 0xf8, 0x82, 0x23, 0x95, 0x27, 0x68, 0x10, 0x12, 0x12, 0x82, 0xc3, 0x8a, 0x2b, 0xb2, 0x36, 0x74, 0xb2, 0x5e, 0x61, 0x7b, 0xad, 0xf5, 0x36, 0x6a, 0xde, 0x82, 0x47, 0xe0, 0x71, 0x72, 0xe4, 0x09, 0xf8, 0x93, 0x27, 0x41, 0xde, 0xb5, 0x1d, 0x8e, 0xdc, 0x66, 0x7f, 0xf3, 0xcd, 0xcc, 0xb7, 0x1f, 0x04, 0xf5, 0xa6, 0x54, 0x22, 0x6b, 0xb4, 0x32, 0x8a, 0x5e, 0xd8, 0x47, 0xb3, 0x8e, 0xe6, 0x42, 0x29, 0x51, 0xe2, 0xd2, 0xe2, 0xf5, 0xfd, 0x66, 0x69, 0x64, 0x85, 0xad, 0xe1, 0x55, 0xe3, 0x94, 0xd1, 0x13, 0xa1, 0x84, 0xb2, 0xe5, 0xb2, 0xab, 0x1c, 0x5d, 0x7c, 0x06, 0x8f, 0xe1, 0x17, 0x94, 0x5b, 0xd4, 0xf4, 0x19, 0x80, 0xd0, 0xea, 0xbe, 0xc9, 0x6b, 0x5e, 0x61, 0x48, 0x12, 0x92, 0xfa, 0xcc, 0xb7, 0xe4, 0x23, 0xaf, 0x90, 0x26, 0x10, 0xc8, 0xda, 0xa0, 0xd0, 0xdc, 0x48, 0x55, 0x87, 0xa7, 0xb6, 0xff, 0x2f, 0xa2, 0xd7, 0x30, 0x91, 0x77, 0x0f, 0xe1, 0x24, 0x21, 0xe9, 0x25, 0xeb, 0xca, 0xc5, 0xf7, 0x53, 0x98, 0xbe, 0xad, 0x8d, 0xde, 0xd1, 0xa7, 0xe0, 0x56, 0xe5, 0x5f, 0x71, 0x67, 0x77, 0xcf, 0x98, 0x67, 0xc1, 0x7b, 0xdc, 0xd1, 0x57, 0xe0, 0xe9, 0xde, 0x85, 0xdd, 0x1b, 0xdc, 0x3c, 0xce, 0xfa, 0x8f, 0x65, 0x83, 0x3d, 0x36, 0x4a, 0x8e, 0x46, 0x0b, 0xde, 0x16, 0xf6, 0xdc, 0xac, 0x37, 0xfa, 0x8e, 0xb7, 0x05, 0x8d, 0xba, 0x6d, 0xad, 0x2a, 0xb7, 0x78, 0x17, 0x9e, 0x25, 0x24, 0xf5, 0xd8, 0xf8, 0xa6, 0x2b, 0xf0, 0xc7, 0x60, 0xc2, 0xa9, 0x3d, 0x15, 0x65, 0x2e, 0xba, 0x6c, 0x88, 0x2e, 0xfb, 0x34, 0x28, 0x56, 0xde, 0xfe, 0xe7, 0xfc, 0xe4, 0xdb, 0xaf, 0x39, 0x61, 0xc7, 0x31, 0xfa, 0x1c, 0x2e, 0x37, 0x52, 0xcb, 0x5a, 0xe4, 0xbc, 0x44, 0x6d, 0xda, 0xf0, 0x3c, 0x99, 0xa4, 0x67, 0x6c, 0xe6, 0xe0, 0xad, 0x65, 0xf4, 0x25, 0x3c, 0x1a, 0x8e, 0x0e, 0xb2, 0x0b, 0x2b, 0xbb, 0x1a, 0xb0, 0x13, 0x2e, 0xb6, 0xe0, 0x7f, 0xc0, 0xb6, 0x70, 0x29, 0xbd, 0x80, 0x29, 0x76, 0x85, 0x4d, 0x28, 0xb8, 0xb9, 0x1a, 0x53, 0xb0, 0x6d, 0xe6, 0x9a, 0xf4, 0x0d, 0x00, 0x3e, 0x34, 0x52, 0x63, 0x9b, 0x73, 0xd3, 0x07, 0xf6, 0x9f, 0xbf, 0xe8, 0xe7, 0x6e, 0xcd, 0xea, 0x7a, 0xff, 0x27, 0x3e, 0xd9, 0x1f, 0x62, 0xf2, 0xe3, 0x10, 0x93, 0xdf, 0x87, 0x98, 0xac, 0xcf, 0xed, 0xe8, 0xeb, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x49, 0xcd, 0xa7, 0x1e, 0x61, 0x02, 0x00, 0x00, } prometheus-alertmanager-0.6.2+ds/nflog/nflogpb/nflog.proto000066400000000000000000000035571314512360300237120ustar00rootroot00000000000000syntax = "proto3"; package nflogpb; import "google/protobuf/timestamp.proto"; import "gogoproto/gogo.proto"; option (gogoproto.marshaler_all) = true; option (gogoproto.sizer_all) = true; option (gogoproto.unmarshaler_all) = true; option (gogoproto.goproto_getters_all) = false; message Receiver { // Configured name of the receiver group. string group_name = 1; // Name of the integration of the receiver. string integration = 2; // Index of the receiver with respect to the integration. // Every integration in a group may have 0..N configurations. uint32 idx = 3; } // Entry holds information about a successful notification // sent to a receiver. message Entry { // The key identifying the dispatching group. bytes group_key = 1; // The receiver that was notified. Receiver receiver = 2; // Hash over the state of the group at notification time. // Deprecated in favor of FiringAlerts field, but kept for compatibility. bytes group_hash = 3; // Whether the notification was about a resolved alert. // Deprecated in favor of ResolvedAlerts field, but kept for compatibility. bool resolved = 4; // Timestamp of the succeeding notification. google.protobuf.Timestamp timestamp = 5 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; // FiringAlerts list of hashes of firing alerts at the last notification time. repeated uint64 firing_alerts = 6; // ResolvedAlerts list of hashes of resolved alerts at the last notification time. repeated uint64 resolved_alerts = 7; } // MeshEntry is a wrapper message to communicate a notify log // entry through a mesh network. message MeshEntry { // The original raw notify log entry. Entry entry = 1; // A timestamp indicating when the mesh peer should evict // the log entry from its state. google.protobuf.Timestamp expires_at = 2 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; } prometheus-alertmanager-0.6.2+ds/nflog/nflogpb/set.go000066400000000000000000000026471314512360300226410ustar00rootroot00000000000000// Copyright 2017 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package nflogpb // IsFiringSubset returns whether the given subset is a subset of the alerts // that were firing at the time of the last notification. func (m *Entry) IsFiringSubset(subset map[uint64]struct{}) bool { set := map[uint64]struct{}{} for i := range m.FiringAlerts { set[m.FiringAlerts[i]] = struct{}{} } return isSubset(set, subset) } // IsFiringSubset returns whether the given subset is a subset of the alerts // that were resolved at the time of the last notification. func (m *Entry) IsResolvedSubset(subset map[uint64]struct{}) bool { set := map[uint64]struct{}{} for i := range m.ResolvedAlerts { set[m.ResolvedAlerts[i]] = struct{}{} } return isSubset(set, subset) } func isSubset(set, subset map[uint64]struct{}) bool { for k, _ := range subset { _, exists := set[k] if !exists { return false } } return true } prometheus-alertmanager-0.6.2+ds/notify/000077500000000000000000000000001314512360300202625ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/notify/impl.go000066400000000000000000000653221314512360300215620ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package notify import ( "bytes" "crypto/sha256" "crypto/tls" "encoding/json" "errors" "fmt" "io" "io/ioutil" "mime" "net" "net/http" "net/mail" "net/smtp" "net/url" "strings" "time" "github.com/prometheus/common/log" "github.com/prometheus/common/model" "golang.org/x/net/context" "golang.org/x/net/context/ctxhttp" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/template" "github.com/prometheus/alertmanager/types" ) type notifierConfig interface { SendResolved() bool } // A Notifier notifies about alerts under constraints of the given context. // It returns an error if unsuccessful and a flag whether the error is // recoverable. This information is useful for a retry logic. type Notifier interface { Notify(context.Context, ...*types.Alert) (bool, error) } // An Integration wraps a notifier and its config to be uniquely identified by // name and index from its origin in the configuration. type Integration struct { notifier Notifier conf notifierConfig name string idx int } // Notify implements the Notifier interface. func (i *Integration) Notify(ctx context.Context, alerts ...*types.Alert) (bool, error) { var res []*types.Alert // Resolved alerts have to be filtered only at this point, because they need // to end up unfiltered in the SetNotifiesStage. if i.conf.SendResolved() { res = alerts } else { for _, a := range alerts { if a.Status() != model.AlertResolved { res = append(res, a) } } } if len(res) == 0 { return false, nil } return i.notifier.Notify(ctx, res...) } // BuildReceiverIntegrations builds a list of integration notifiers off of a // receivers config. func BuildReceiverIntegrations(nc *config.Receiver, tmpl *template.Template) []Integration { var ( integrations []Integration add = func(name string, i int, n Notifier, nc notifierConfig) { integrations = append(integrations, Integration{ notifier: n, conf: nc, name: name, idx: i, }) } ) for i, c := range nc.WebhookConfigs { n := NewWebhook(c, tmpl) add("webhook", i, n, c) } for i, c := range nc.EmailConfigs { n := NewEmail(c, tmpl) add("email", i, n, c) } for i, c := range nc.PagerdutyConfigs { n := NewPagerDuty(c, tmpl) add("pagerduty", i, n, c) } for i, c := range nc.OpsGenieConfigs { n := NewOpsGenie(c, tmpl) add("opsgenie", i, n, c) } for i, c := range nc.SlackConfigs { n := NewSlack(c, tmpl) add("slack", i, n, c) } for i, c := range nc.HipchatConfigs { n := NewHipchat(c, tmpl) add("hipchat", i, n, c) } for i, c := range nc.VictorOpsConfigs { n := NewVictorOps(c, tmpl) add("victorops", i, n, c) } for i, c := range nc.PushoverConfigs { n := NewPushover(c, tmpl) add("pushover", i, n, c) } return integrations } const contentTypeJSON = "application/json" // Webhook implements a Notifier for generic webhooks. type Webhook struct { // The URL to which notifications are sent. URL string tmpl *template.Template } // NewWebhook returns a new Webhook. func NewWebhook(conf *config.WebhookConfig, t *template.Template) *Webhook { return &Webhook{URL: conf.URL, tmpl: t} } // WebhookMessage defines the JSON object send to webhook endpoints. type WebhookMessage struct { *template.Data // The protocol version. Version string `json:"version"` GroupKey string `json:"groupKey"` } // Notify implements the Notifier interface. func (w *Webhook) Notify(ctx context.Context, alerts ...*types.Alert) (bool, error) { data := w.tmpl.Data(receiverName(ctx), groupLabels(ctx), alerts...) groupKey, ok := GroupKey(ctx) if !ok { log.Errorf("group key missing") } msg := &WebhookMessage{ Version: "4", Data: data, GroupKey: groupKey, } var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(msg); err != nil { return false, err } resp, err := ctxhttp.Post(ctx, http.DefaultClient, w.URL, contentTypeJSON, &buf) if err != nil { return true, err } resp.Body.Close() return w.retry(resp.StatusCode) } func (w *Webhook) retry(statusCode int) (bool, error) { // Webhooks are assumed to respond with 2xx response codes on a successful // request and 5xx response codes are assumed to be recoverable. if statusCode/100 != 2 { return (statusCode/100 == 5), fmt.Errorf("unexpected status code %v from %s", statusCode, w.URL) } return false, nil } // Email implements a Notifier for email notifications. type Email struct { conf *config.EmailConfig tmpl *template.Template } // NewEmail returns a new Email notifier. func NewEmail(c *config.EmailConfig, t *template.Template) *Email { if _, ok := c.Headers["Subject"]; !ok { c.Headers["Subject"] = config.DefaultEmailSubject } if _, ok := c.Headers["To"]; !ok { c.Headers["To"] = c.To } if _, ok := c.Headers["From"]; !ok { c.Headers["From"] = c.From } return &Email{conf: c, tmpl: t} } // auth resolves a string of authentication mechanisms. func (n *Email) auth(mechs string) (smtp.Auth, error) { username := n.conf.AuthUsername for _, mech := range strings.Split(mechs, " ") { switch mech { case "CRAM-MD5": secret := string(n.conf.AuthSecret) if secret == "" { continue } return smtp.CRAMMD5Auth(username, secret), nil case "PLAIN": password := string(n.conf.AuthPassword) if password == "" { continue } identity := n.conf.AuthIdentity // We need to know the hostname for both auth and TLS. host, _, err := net.SplitHostPort(n.conf.Smarthost) if err != nil { return nil, fmt.Errorf("invalid address: %s", err) } return smtp.PlainAuth(identity, username, password, host), nil case "LOGIN": password := string(n.conf.AuthPassword) if password == "" { continue } return LoginAuth(username, password), nil } } return nil, nil } // Notify implements the Notifier interface. func (n *Email) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { // Connect to the SMTP smarthost. c, err := smtp.Dial(n.conf.Smarthost) if err != nil { return true, err } defer c.Quit() // We need to know the hostname for both auth and TLS. host, _, err := net.SplitHostPort(n.conf.Smarthost) if err != nil { return false, fmt.Errorf("invalid address: %s", err) } // Global Config guarantees RequireTLS is not nil if *n.conf.RequireTLS { if ok, _ := c.Extension("STARTTLS"); !ok { return true, fmt.Errorf("require_tls: true (default), but %q does not advertise the STARTTLS extension", n.conf.Smarthost) } tlsConf := &tls.Config{ServerName: host} if err := c.StartTLS(tlsConf); err != nil { return true, fmt.Errorf("starttls failed: %s", err) } } if ok, mech := c.Extension("AUTH"); ok { auth, err := n.auth(mech) if err != nil { return true, err } if auth != nil { if err := c.Auth(auth); err != nil { return true, fmt.Errorf("%T failed: %s", auth, err) } } } var ( data = n.tmpl.Data(receiverName(ctx), groupLabels(ctx), as...) tmpl = tmplText(n.tmpl, data, &err) from = tmpl(n.conf.From) to = tmpl(n.conf.To) ) if err != nil { return false, err } addrs, err := mail.ParseAddressList(from) if err != nil { return false, fmt.Errorf("parsing from addresses: %s", err) } if len(addrs) != 1 { return false, fmt.Errorf("must be exactly one from address") } if err := c.Mail(addrs[0].Address); err != nil { return true, fmt.Errorf("sending mail from: %s", err) } addrs, err = mail.ParseAddressList(to) if err != nil { return false, fmt.Errorf("parsing to addresses: %s", err) } for _, addr := range addrs { if err := c.Rcpt(addr.Address); err != nil { return true, fmt.Errorf("sending rcpt to: %s", err) } } // Send the email body. wc, err := c.Data() if err != nil { return true, err } defer wc.Close() for header, t := range n.conf.Headers { value, err := n.tmpl.ExecuteTextString(t, data) if err != nil { return false, fmt.Errorf("executing %q header template: %s", header, err) } fmt.Fprintf(wc, "%s: %s\r\n", header, mime.QEncoding.Encode("utf-8", value)) } fmt.Fprintf(wc, "Content-Type: text/html; charset=UTF-8\r\n") fmt.Fprintf(wc, "Date: %s\r\n", time.Now().Format(time.RFC1123Z)) // TODO: Add some useful headers here, such as URL of the alertmanager // and active/resolved. fmt.Fprintf(wc, "\r\n") // TODO(fabxc): do a multipart write that considers the plain template. body, err := n.tmpl.ExecuteHTMLString(n.conf.HTML, data) if err != nil { return false, fmt.Errorf("executing email html template: %s", err) } _, err = io.WriteString(wc, body) if err != nil { return true, err } return false, nil } // PagerDuty implements a Notifier for PagerDuty notifications. type PagerDuty struct { conf *config.PagerdutyConfig tmpl *template.Template } // NewPagerDuty returns a new PagerDuty notifier. func NewPagerDuty(c *config.PagerdutyConfig, t *template.Template) *PagerDuty { return &PagerDuty{conf: c, tmpl: t} } const ( pagerDutyEventTrigger = "trigger" pagerDutyEventResolve = "resolve" ) type pagerDutyMessage struct { ServiceKey string `json:"service_key"` IncidentKey string `json:"incident_key"` EventType string `json:"event_type"` Description string `json:"description"` Client string `json:"client,omitempty"` ClientURL string `json:"client_url,omitempty"` Details map[string]string `json:"details,omitempty"` } // Notify implements the Notifier interface. // // http://developer.pagerduty.com/documentation/integration/events/trigger func (n *PagerDuty) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { key, ok := GroupKey(ctx) if !ok { return false, fmt.Errorf("group key missing") } var err error var ( alerts = types.Alerts(as...) data = n.tmpl.Data(receiverName(ctx), groupLabels(ctx), as...) tmpl = tmplText(n.tmpl, data, &err) eventType = pagerDutyEventTrigger ) if alerts.Status() == model.AlertResolved { eventType = pagerDutyEventResolve } log.With("incident", key).With("eventType", eventType).Debugln("notifying PagerDuty") details := make(map[string]string, len(n.conf.Details)) for k, v := range n.conf.Details { details[k] = tmpl(v) } msg := &pagerDutyMessage{ ServiceKey: tmpl(string(n.conf.ServiceKey)), EventType: eventType, IncidentKey: hashKey(key), Description: tmpl(n.conf.Description), Details: details, } if eventType == pagerDutyEventTrigger { msg.Client = tmpl(n.conf.Client) msg.ClientURL = tmpl(n.conf.ClientURL) } if err != nil { return false, err } var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(msg); err != nil { return false, err } resp, err := ctxhttp.Post(ctx, http.DefaultClient, n.conf.URL, contentTypeJSON, &buf) if err != nil { return true, err } resp.Body.Close() return n.retry(resp.StatusCode) } func (n *PagerDuty) retry(statusCode int) (bool, error) { // Retrying can solve the issue on 403 (rate limiting) and 5xx response codes. // 2xx response codes indicate a successful request. // https://v2.developer.pagerduty.com/docs/trigger-events if statusCode/100 != 2 { return (statusCode == 403 || statusCode/100 == 5), fmt.Errorf("unexpected status code %v", statusCode) } return false, nil } // Slack implements a Notifier for Slack notifications. type Slack struct { conf *config.SlackConfig tmpl *template.Template } // NewSlack returns a new Slack notification handler. func NewSlack(conf *config.SlackConfig, tmpl *template.Template) *Slack { return &Slack{ conf: conf, tmpl: tmpl, } } // slackReq is the request for sending a slack notification. type slackReq struct { Channel string `json:"channel,omitempty"` Username string `json:"username,omitempty"` IconEmoji string `json:"icon_emoji,omitempty"` IconURL string `json:"icon_url,omitempty"` Attachments []slackAttachment `json:"attachments"` } // slackAttachment is used to display a richly-formatted message block. type slackAttachment struct { Title string `json:"title,omitempty"` TitleLink string `json:"title_link,omitempty"` Pretext string `json:"pretext,omitempty"` Text string `json:"text"` Fallback string `json:"fallback"` Color string `json:"color,omitempty"` MrkdwnIn []string `json:"mrkdwn_in,omitempty"` } // slackAttachmentField is displayed in a table inside the message attachment. type slackAttachmentField struct { Title string `json:"title"` Value string `json:"value"` Short bool `json:"short,omitempty"` } // Notify implements the Notifier interface. func (n *Slack) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { var err error var ( data = n.tmpl.Data(receiverName(ctx), groupLabels(ctx), as...) tmplText = tmplText(n.tmpl, data, &err) ) attachment := &slackAttachment{ Title: tmplText(n.conf.Title), TitleLink: tmplText(n.conf.TitleLink), Pretext: tmplText(n.conf.Pretext), Text: tmplText(n.conf.Text), Fallback: tmplText(n.conf.Fallback), Color: tmplText(n.conf.Color), MrkdwnIn: []string{"fallback", "pretext", "text"}, } req := &slackReq{ Channel: tmplText(n.conf.Channel), Username: tmplText(n.conf.Username), IconEmoji: tmplText(n.conf.IconEmoji), IconURL: tmplText(n.conf.IconURL), Attachments: []slackAttachment{*attachment}, } if err != nil { return false, err } var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(req); err != nil { return false, err } resp, err := ctxhttp.Post(ctx, http.DefaultClient, string(n.conf.APIURL), contentTypeJSON, &buf) if err != nil { return true, err } resp.Body.Close() return n.retry(resp.StatusCode) } func (n *Slack) retry(statusCode int) (bool, error) { // Only 5xx response codes are recoverable and 2xx codes are successful. // https://api.slack.com/incoming-webhooks#handling_errors // https://api.slack.com/changelog/2016-05-17-changes-to-errors-for-incoming-webhooks if statusCode/100 != 2 { return (statusCode/100 == 5), fmt.Errorf("unexpected status code %v", statusCode) } return false, nil } // Hipchat implements a Notifier for Hipchat notifications. type Hipchat struct { conf *config.HipchatConfig tmpl *template.Template } // NewHipchat returns a new Hipchat notification handler. func NewHipchat(conf *config.HipchatConfig, tmpl *template.Template) *Hipchat { return &Hipchat{ conf: conf, tmpl: tmpl, } } type hipchatReq struct { From string `json:"from"` Notify bool `json:"notify"` Message string `json:"message"` MessageFormat string `json:"message_format"` Color string `json:"color"` } // Notify implements the Notifier interface. func (n *Hipchat) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { var err error var msg string var ( data = n.tmpl.Data(receiverName(ctx), groupLabels(ctx), as...) tmplText = tmplText(n.tmpl, data, &err) tmplHTML = tmplHTML(n.tmpl, data, &err) url = fmt.Sprintf("%sv2/room/%s/notification?auth_token=%s", n.conf.APIURL, n.conf.RoomID, n.conf.AuthToken) ) if n.conf.MessageFormat == "html" { msg = tmplHTML(n.conf.Message) } else { msg = tmplText(n.conf.Message) } req := &hipchatReq{ From: tmplText(n.conf.From), Notify: n.conf.Notify, Message: msg, MessageFormat: n.conf.MessageFormat, Color: tmplText(n.conf.Color), } if err != nil { return false, err } var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(req); err != nil { return false, err } resp, err := ctxhttp.Post(ctx, http.DefaultClient, url, contentTypeJSON, &buf) if err != nil { return true, err } defer resp.Body.Close() return n.retry(resp.StatusCode) } func (n *Hipchat) retry(statusCode int) (bool, error) { // Response codes 429 (rate limiting) and 5xx can potentially recover. 2xx // responce codes indicate successful requests. // https://developer.atlassian.com/hipchat/guide/hipchat-rest-api/api-response-codes if statusCode/100 != 2 { return (statusCode == 429 || statusCode/100 == 5), fmt.Errorf("unexpected status code %v", statusCode) } return false, nil } // OpsGenie implements a Notifier for OpsGenie notifications. type OpsGenie struct { conf *config.OpsGenieConfig tmpl *template.Template } // NewOpsGenie returns a new OpsGenie notifier. func NewOpsGenie(c *config.OpsGenieConfig, t *template.Template) *OpsGenie { return &OpsGenie{conf: c, tmpl: t} } type opsGenieMessage struct { APIKey string `json:"apiKey"` Alias string `json:"alias"` } type opsGenieCreateMessage struct { *opsGenieMessage `json:",inline"` Message string `json:"message"` Description string `json:"description,omitempty"` Details map[string]string `json:"details"` Source string `json:"source"` Teams string `json:"teams,omitempty"` Tags string `json:"tags,omitempty"` Note string `json:"note,omitempty"` } type opsGenieCloseMessage struct { *opsGenieMessage `json:",inline"` } type opsGenieErrorResponse struct { Code int `json:"code"` Error string `json:"error"` } // Notify implements the Notifier interface. func (n *OpsGenie) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { key, ok := GroupKey(ctx) if !ok { return false, fmt.Errorf("group key missing") } data := n.tmpl.Data(receiverName(ctx), groupLabels(ctx), as...) log.With("incident", key).Debugln("notifying OpsGenie") var err error tmpl := tmplText(n.tmpl, data, &err) details := make(map[string]string, len(n.conf.Details)) for k, v := range n.conf.Details { details[k] = tmpl(v) } var ( msg interface{} apiURL string apiMsg = opsGenieMessage{ APIKey: string(n.conf.APIKey), Alias: hashKey(key), } alerts = types.Alerts(as...) ) switch alerts.Status() { case model.AlertResolved: apiURL = n.conf.APIHost + "v1/json/alert/close" msg = &opsGenieCloseMessage{&apiMsg} default: apiURL = n.conf.APIHost + "v1/json/alert" msg = &opsGenieCreateMessage{ opsGenieMessage: &apiMsg, Message: tmpl(n.conf.Message), Description: tmpl(n.conf.Description), Details: details, Source: tmpl(n.conf.Source), Teams: tmpl(n.conf.Teams), Tags: tmpl(n.conf.Tags), Note: tmpl(n.conf.Note), } } if err != nil { return false, fmt.Errorf("templating error: %s", err) } var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(msg); err != nil { return false, err } resp, err := ctxhttp.Post(ctx, http.DefaultClient, apiURL, contentTypeJSON, &buf) if err != nil { return true, err } defer resp.Body.Close() // Missing documentation therefore assuming only 5xx response codes are // recoverable. if resp.StatusCode/100 == 5 { return true, fmt.Errorf("unexpected status code %v", resp.StatusCode) } else if resp.StatusCode == 400 && alerts.Status() == model.AlertResolved { body, _ := ioutil.ReadAll(resp.Body) var responseMessage opsGenieErrorResponse if err := json.Unmarshal(body, &responseMessage); err != nil { return false, fmt.Errorf("could not parse error response %q", body) } const alreadyClosedError = 5 if responseMessage.Code == alreadyClosedError { return false, nil } return false, fmt.Errorf("error when closing alert: code %d, error %q", responseMessage.Code, responseMessage.Error) } else if resp.StatusCode/100 == 4 { return false, fmt.Errorf("unexpected status code %v", resp.StatusCode) } else if resp.StatusCode/100 != 2 { body, _ := ioutil.ReadAll(resp.Body) log.With("incident", key).Debugf("unexpected OpsGenie response from %s (POSTed %s), %s: %s", apiURL, msg, resp.Status, body) return false, fmt.Errorf("unexpected status code %v", resp.StatusCode) } return false, nil } // VictorOps implements a Notifier for VictorOps notifications. type VictorOps struct { conf *config.VictorOpsConfig tmpl *template.Template } // NewVictorOps returns a new VictorOps notifier. func NewVictorOps(c *config.VictorOpsConfig, t *template.Template) *VictorOps { return &VictorOps{ conf: c, tmpl: t, } } const ( victorOpsEventTrigger = "CRITICAL" victorOpsEventResolve = "RECOVERY" ) type victorOpsMessage struct { MessageType string `json:"message_type"` EntityID string `json:"entity_id"` StateMessage string `json:"state_message"` MonitoringTool string `json:"monitoring_tool"` } type victorOpsErrorResponse struct { Result string `json:"result"` Message string `json:"message"` } // Notify implements the Notifier interface. func (n *VictorOps) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { victorOpsAllowedEvents := map[string]bool{ "INFO": true, "WARNING": true, "CRITICAL": true, } key, ok := GroupKey(ctx) if !ok { return false, fmt.Errorf("group key missing") } var err error var ( alerts = types.Alerts(as...) data = n.tmpl.Data(receiverName(ctx), groupLabels(ctx), as...) tmpl = tmplText(n.tmpl, data, &err) apiURL = fmt.Sprintf("%s%s/%s", n.conf.APIURL, n.conf.APIKey, n.conf.RoutingKey) messageType = n.conf.MessageType ) if alerts.Status() == model.AlertFiring && !victorOpsAllowedEvents[messageType] { messageType = victorOpsEventTrigger } if alerts.Status() == model.AlertResolved { messageType = victorOpsEventResolve } msg := &victorOpsMessage{ MessageType: messageType, EntityID: hashKey(key), StateMessage: tmpl(n.conf.StateMessage), MonitoringTool: tmpl(n.conf.MonitoringTool), } if err != nil { return false, fmt.Errorf("templating error: %s", err) } var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(msg); err != nil { return false, err } resp, err := ctxhttp.Post(ctx, http.DefaultClient, apiURL, contentTypeJSON, &buf) if err != nil { return true, err } defer resp.Body.Close() // Missing documentation therefore assuming only 5xx response codes are // recoverable. if resp.StatusCode/100 == 5 { return true, fmt.Errorf("unexpected status code %v", resp.StatusCode) } if resp.StatusCode/100 != 2 { body, _ := ioutil.ReadAll(resp.Body) var responseMessage victorOpsErrorResponse if err := json.Unmarshal(body, &responseMessage); err != nil { return false, fmt.Errorf("could not parse error response %q", body) } log.With("incident", key).Debugf("unexpected VictorOps response from %s (POSTed %s), %s: %s", apiURL, msg, resp.Status, body) return false, fmt.Errorf("error when posting alert: result %q, message %q", responseMessage.Result, responseMessage.Message) } return false, nil } // Pushover implements a Notifier for Pushover notifications. type Pushover struct { conf *config.PushoverConfig tmpl *template.Template } // NewPushover returns a new Pushover notifier. func NewPushover(c *config.PushoverConfig, t *template.Template) *Pushover { return &Pushover{conf: c, tmpl: t} } // Notify implements the Notifier interface. func (n *Pushover) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { key, ok := GroupKey(ctx) if !ok { return false, fmt.Errorf("group key missing") } data := n.tmpl.Data(receiverName(ctx), groupLabels(ctx), as...) log.With("incident", key).Debugln("notifying Pushover") var err error tmpl := tmplText(n.tmpl, data, &err) parameters := url.Values{} parameters.Add("token", tmpl(string(n.conf.Token))) parameters.Add("user", tmpl(string(n.conf.UserKey))) title := tmpl(n.conf.Title) message := tmpl(n.conf.Message) parameters.Add("title", title) if len(title) > 512 { title = title[:512] log.With("incident", key).Debugf("Truncated title to %q due to Pushover message limit", title) } if len(title)+len(message) > 512 { message = message[:512-len(title)] log.With("incident", key).Debugf("Truncated message to %q due to Pushover message limit", message) } message = strings.TrimSpace(message) if message == "" { // Pushover rejects empty messages. message = "(no details)" } parameters.Add("message", message) parameters.Add("url", tmpl(n.conf.URL)) parameters.Add("priority", tmpl(n.conf.Priority)) parameters.Add("retry", fmt.Sprintf("%d", int64(time.Duration(n.conf.Retry).Seconds()))) parameters.Add("expire", fmt.Sprintf("%d", int64(time.Duration(n.conf.Expire).Seconds()))) if err != nil { return false, err } apiURL := "https://api.pushover.net/1/messages.json" u, err := url.Parse(apiURL) if err != nil { return false, err } u.RawQuery = parameters.Encode() log.With("incident", key).Debugf("Pushover URL = %q", u.String()) resp, err := ctxhttp.Post(ctx, http.DefaultClient, u.String(), "text/plain", nil) if err != nil { return true, err } defer resp.Body.Close() // Only documented behaviour is that 2xx response codes are successful and // 4xx are unsuccessful, therefore assuming only 5xx are recoverable. // https://pushover.net/api#response if resp.StatusCode/100 == 5 { return true, fmt.Errorf("unexpected status code %v", resp.StatusCode) } if resp.StatusCode/100 != 2 { body, err := ioutil.ReadAll(resp.Body) if err != nil { return false, err } return false, fmt.Errorf("unexpected status code %v (body: %s)", resp.StatusCode, string(body)) } return false, nil } func tmplText(tmpl *template.Template, data *template.Data, err *error) func(string) string { return func(name string) (s string) { if *err != nil { return } s, *err = tmpl.ExecuteTextString(name, data) return s } } func tmplHTML(tmpl *template.Template, data *template.Data, err *error) func(string) string { return func(name string) (s string) { if *err != nil { return } s, *err = tmpl.ExecuteHTMLString(name, data) return s } } type loginAuth struct { username, password string } func LoginAuth(username, password string) smtp.Auth { return &loginAuth{username, password} } func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { return "LOGIN", []byte{}, nil } // Used for AUTH LOGIN. (Maybe password should be encrypted) func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { if more { switch strings.ToLower(string(fromServer)) { case "username:": return []byte(a.username), nil case "password:": return []byte(a.password), nil default: return nil, errors.New("unexpected server challenge") } } return nil, nil } // hashKey returns the sha256 for a group key as integrations may have // maximum length requirements on deduplication keys. func hashKey(s string) string { h := sha256.New() h.Write([]byte(s)) return fmt.Sprintf("%x", h.Sum(nil)) } prometheus-alertmanager-0.6.2+ds/notify/notify.go000066400000000000000000000420041314512360300221210ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package notify import ( "fmt" "sort" "sync" "time" "github.com/cenkalti/backoff" "github.com/cespare/xxhash" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/log" "github.com/prometheus/common/model" "golang.org/x/net/context" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/inhibit" "github.com/prometheus/alertmanager/nflog" "github.com/prometheus/alertmanager/nflog/nflogpb" "github.com/prometheus/alertmanager/silence" "github.com/prometheus/alertmanager/template" "github.com/prometheus/alertmanager/types" ) var ( numNotifications = prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "alertmanager", Name: "notifications_total", Help: "The total number of attempted notifications.", }, []string{"integration"}) numFailedNotifications = prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "alertmanager", Name: "notifications_failed_total", Help: "The total number of failed notifications.", }, []string{"integration"}) ) func init() { prometheus.Register(numNotifications) prometheus.Register(numFailedNotifications) } // MinTimeout is the minimum timeout that is set for the context of a call // to a notification pipeline. const MinTimeout = 10 * time.Second // notifyKey defines a custom type with which a context is populated to // avoid accidental collisions. type notifyKey int const ( keyReceiverName notifyKey = iota keyRepeatInterval keyGroupLabels keyGroupKey keyFiringAlerts keyResolvedAlerts keyNow ) // WithReceiverName populates a context with a receiver name. func WithReceiverName(ctx context.Context, rcv string) context.Context { return context.WithValue(ctx, keyReceiverName, rcv) } // WithGroupKey populates a context with a group key. func WithGroupKey(ctx context.Context, s string) context.Context { return context.WithValue(ctx, keyGroupKey, s) } // WithFiringAlerts populates a context with a slice of firing alerts. func WithFiringAlerts(ctx context.Context, alerts []uint64) context.Context { return context.WithValue(ctx, keyFiringAlerts, alerts) } // WithResolvedAlerts populates a context with a slice of resolved alerts. func WithResolvedAlerts(ctx context.Context, alerts []uint64) context.Context { return context.WithValue(ctx, keyResolvedAlerts, alerts) } // WithGroupLabels populates a context with grouping labels. func WithGroupLabels(ctx context.Context, lset model.LabelSet) context.Context { return context.WithValue(ctx, keyGroupLabels, lset) } // WithNow populates a context with a now timestamp. func WithNow(ctx context.Context, t time.Time) context.Context { return context.WithValue(ctx, keyNow, t) } // WithRepeatInterval populates a context with a repeat interval. func WithRepeatInterval(ctx context.Context, t time.Duration) context.Context { return context.WithValue(ctx, keyRepeatInterval, t) } // RepeatInterval extracts a repeat interval from the context. Iff none exists, the // second argument is false. func RepeatInterval(ctx context.Context) (time.Duration, bool) { v, ok := ctx.Value(keyRepeatInterval).(time.Duration) return v, ok } // ReceiverName extracts a receiver name from the context. Iff none exists, the // second argument is false. func ReceiverName(ctx context.Context) (string, bool) { v, ok := ctx.Value(keyReceiverName).(string) return v, ok } func receiverName(ctx context.Context) string { recv, ok := ReceiverName(ctx) if !ok { log.Error("missing receiver") } return recv } // GroupKey extracts a group key from the context. Iff none exists, the // second argument is false. func GroupKey(ctx context.Context) (string, bool) { v, ok := ctx.Value(keyGroupKey).(string) return v, ok } func groupLabels(ctx context.Context) model.LabelSet { groupLabels, ok := GroupLabels(ctx) if !ok { log.Error("missing group labels") } return groupLabels } // GroupLabels extracts grouping label set from the context. Iff none exists, the // second argument is false. func GroupLabels(ctx context.Context) (model.LabelSet, bool) { v, ok := ctx.Value(keyGroupLabels).(model.LabelSet) return v, ok } // Now extracts a now timestamp from the context. Iff none exists, the // second argument is false. func Now(ctx context.Context) (time.Time, bool) { v, ok := ctx.Value(keyNow).(time.Time) return v, ok } // FiringAlerts extracts a slice of firing alerts from the context. // Iff none exists, the second argument is false. func FiringAlerts(ctx context.Context) ([]uint64, bool) { v, ok := ctx.Value(keyFiringAlerts).([]uint64) return v, ok } // ResolvedAlerts extracts a slice of firing alerts from the context. // Iff none exists, the second argument is false. func ResolvedAlerts(ctx context.Context) ([]uint64, bool) { v, ok := ctx.Value(keyResolvedAlerts).([]uint64) return v, ok } // A Stage processes alerts under the constraints of the given context. type Stage interface { Exec(ctx context.Context, alerts ...*types.Alert) (context.Context, []*types.Alert, error) } // StageFunc wraps a function to represent a Stage. type StageFunc func(ctx context.Context, alerts ...*types.Alert) (context.Context, []*types.Alert, error) // Exec implements Stage interface. func (f StageFunc) Exec(ctx context.Context, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { return f(ctx, alerts...) } // BuildPipeline builds a map of receivers to Stages. func BuildPipeline( confs []*config.Receiver, tmpl *template.Template, wait func() time.Duration, inhibitor *inhibit.Inhibitor, silences *silence.Silences, notificationLog nflog.Log, marker types.Marker, ) RoutingStage { rs := RoutingStage{} is := NewInhibitStage(inhibitor, marker) ss := NewSilenceStage(silences, marker) for _, rc := range confs { rs[rc.Name] = MultiStage{is, ss, createStage(rc, tmpl, wait, notificationLog)} } return rs } // createStage creates a pipeline of stages for a receiver. func createStage(rc *config.Receiver, tmpl *template.Template, wait func() time.Duration, notificationLog nflog.Log) Stage { var fs FanoutStage for _, i := range BuildReceiverIntegrations(rc, tmpl) { recv := &nflogpb.Receiver{ GroupName: rc.Name, Integration: i.name, Idx: uint32(i.idx), } var s MultiStage s = append(s, NewWaitStage(wait)) s = append(s, NewDedupStage(notificationLog, recv, i.conf.SendResolved())) s = append(s, NewRetryStage(i)) s = append(s, NewSetNotifiesStage(notificationLog, recv)) fs = append(fs, s) } return fs } // RoutingStage executes the inner stages based on the receiver specified in // the context. type RoutingStage map[string]Stage // Exec implements the Stage interface. func (rs RoutingStage) Exec(ctx context.Context, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { receiver, ok := ReceiverName(ctx) if !ok { return ctx, nil, fmt.Errorf("receiver missing") } s, ok := rs[receiver] if !ok { return ctx, nil, fmt.Errorf("stage for receiver missing") } return s.Exec(ctx, alerts...) } // A MultiStage executes a series of stages sequencially. type MultiStage []Stage // Exec implements the Stage interface. func (ms MultiStage) Exec(ctx context.Context, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { var err error for _, s := range ms { if len(alerts) == 0 { return ctx, nil, nil } ctx, alerts, err = s.Exec(ctx, alerts...) if err != nil { return ctx, nil, err } } return ctx, alerts, nil } // FanoutStage executes its stages concurrently type FanoutStage []Stage // Exec attempts to execute all stages concurrently and discards the results. // It returns its input alerts and a types.MultiError if one or more stages fail. func (fs FanoutStage) Exec(ctx context.Context, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { var ( wg sync.WaitGroup me types.MultiError ) wg.Add(len(fs)) for _, s := range fs { go func(s Stage) { if _, _, err := s.Exec(ctx, alerts...); err != nil { me.Add(err) log.Errorf("Error on notify: %s", err) } wg.Done() }(s) } wg.Wait() if me.Len() > 0 { return ctx, alerts, &me } return ctx, alerts, nil } // InhibitStage filters alerts through an inhibition muter. type InhibitStage struct { muter types.Muter marker types.Marker } // NewInhibitStage return a new InhibitStage. func NewInhibitStage(m types.Muter, mk types.Marker) *InhibitStage { return &InhibitStage{ muter: m, marker: mk, } } // Exec implements the Stage interface. func (n *InhibitStage) Exec(ctx context.Context, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { var filtered []*types.Alert for _, a := range alerts { _, ok := n.marker.Inhibited(a.Fingerprint()) // TODO(fabxc): increment total alerts counter. // Do not send the alert if the silencer mutes it. if !n.muter.Mutes(a.Labels) { // TODO(fabxc): increment muted alerts counter. filtered = append(filtered, a) // Store whether a previously inhibited alert is firing again. a.WasInhibited = ok } } return ctx, filtered, nil } // SilenceStage filters alerts through a silence muter. type SilenceStage struct { silences *silence.Silences marker types.Marker } // NewSilenceStage returns a new SilenceStage. func NewSilenceStage(s *silence.Silences, mk types.Marker) *SilenceStage { return &SilenceStage{ silences: s, marker: mk, } } // Exec implements the Stage interface. func (n *SilenceStage) Exec(ctx context.Context, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { var filtered []*types.Alert for _, a := range alerts { _, ok := n.marker.Silenced(a.Fingerprint()) // TODO(fabxc): increment total alerts counter. // Do not send the alert if the silencer mutes it. sils, err := n.silences.Query( silence.QState(silence.StateActive), silence.QMatches(a.Labels), ) if err != nil { log.Errorf("Querying silences failed: %s", err) } if len(sils) == 0 { // TODO(fabxc): increment muted alerts counter. filtered = append(filtered, a) n.marker.SetSilenced(a.Labels.Fingerprint()) // Store whether a previously silenced alert is firing again. a.WasSilenced = ok } else { ids := make([]string, len(sils)) for i, s := range sils { ids[i] = s.Id } n.marker.SetSilenced(a.Labels.Fingerprint(), ids...) } } return ctx, filtered, nil } // WaitStage waits for a certain amount of time before continuing or until the // context is done. type WaitStage struct { wait func() time.Duration } // NewWaitStage returns a new WaitStage. func NewWaitStage(wait func() time.Duration) *WaitStage { return &WaitStage{ wait: wait, } } // Exec implements the Stage interface. func (ws *WaitStage) Exec(ctx context.Context, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { select { case <-time.After(ws.wait()): case <-ctx.Done(): return ctx, nil, ctx.Err() } return ctx, alerts, nil } // DedupStage filters alerts. // Filtering happens based on a notification log. type DedupStage struct { nflog nflog.Log recv *nflogpb.Receiver sendResolved bool now func() time.Time hash func(*types.Alert) uint64 } // NewDedupStage wraps a DedupStage that runs against the given notification log. func NewDedupStage(l nflog.Log, recv *nflogpb.Receiver, sendResolved bool) *DedupStage { return &DedupStage{ nflog: l, recv: recv, now: utcNow, sendResolved: sendResolved, hash: hashAlert, } } func utcNow() time.Time { return time.Now().UTC() } var hashBuffers = sync.Pool{} func getHashBuffer() []byte { b := hashBuffers.Get() if b == nil { return make([]byte, 0, 1024) } return b.([]byte) } func putHashBuffer(b []byte) { b = b[:0] hashBuffers.Put(b) } func hashAlert(a *types.Alert) uint64 { const sep = '\xff' b := getHashBuffer() defer putHashBuffer(b) names := make(model.LabelNames, 0, len(a.Labels)) for ln, _ := range a.Labels { names = append(names, ln) } sort.Sort(names) for _, ln := range names { b = append(b, string(ln)...) b = append(b, sep) b = append(b, string(a.Labels[ln])...) b = append(b, sep) } hash := xxhash.Sum64(b) return hash } func allAlertsResolved(alerts []*types.Alert) bool { for _, a := range alerts { if !a.Resolved() { return false } } return true } func (n *DedupStage) needsUpdate(entry *nflogpb.Entry, firing, resolved map[uint64]struct{}, repeat time.Duration) (bool, error) { // If we haven't notified about the alert group before, notify right away // unless we only have resolved alerts. if entry == nil { return ((len(firing) > 0) || (n.sendResolved && len(resolved) > 0)), nil } if !entry.IsFiringSubset(firing) { return true, nil } if n.sendResolved && !entry.IsResolvedSubset(resolved) { return true, nil } // Nothing changed, only notify if the repeat interval has passed. return entry.Timestamp.Before(n.now().Add(-repeat)), nil } // Exec implements the Stage interface. func (n *DedupStage) Exec(ctx context.Context, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { gkey, ok := GroupKey(ctx) if !ok { return ctx, nil, fmt.Errorf("group key missing") } repeatInterval, ok := RepeatInterval(ctx) if !ok { return ctx, nil, fmt.Errorf("repeat interval missing") } firingSet := map[uint64]struct{}{} resolvedSet := map[uint64]struct{}{} firing := []uint64{} resolved := []uint64{} var hash uint64 for _, a := range alerts { hash = n.hash(a) if a.Resolved() { resolved = append(resolved, hash) resolvedSet[hash] = struct{}{} } else { firing = append(firing, hash) firingSet[hash] = struct{}{} } } ctx = WithFiringAlerts(ctx, firing) ctx = WithResolvedAlerts(ctx, resolved) entries, err := n.nflog.Query(nflog.QGroupKey(gkey), nflog.QReceiver(n.recv)) if err != nil && err != nflog.ErrNotFound { return ctx, nil, err } var entry *nflogpb.Entry switch len(entries) { case 0: case 1: entry = entries[0] case 2: return ctx, nil, fmt.Errorf("Unexpected entry result size %d", len(entries)) } if ok, err := n.needsUpdate(entry, firingSet, resolvedSet, repeatInterval); err != nil { return ctx, nil, err } else if ok { return ctx, alerts, nil } return ctx, nil, nil } // RetryStage notifies via passed integration with exponential backoff until it // succeeds. It aborts if the context is canceled or timed out. type RetryStage struct { integration Integration } // NewRetryStage returns a new instance of a RetryStage. func NewRetryStage(i Integration) *RetryStage { return &RetryStage{ integration: i, } } // Exec implements the Stage interface. func (r RetryStage) Exec(ctx context.Context, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { var ( i = 0 b = backoff.NewExponentialBackOff() tick = backoff.NewTicker(b) iErr error ) defer tick.Stop() for { i++ // Always check the context first to not notify again. select { case <-ctx.Done(): if iErr != nil { return ctx, nil, iErr } return ctx, nil, ctx.Err() default: } select { case <-tick.C: if retry, err := r.integration.Notify(ctx, alerts...); err != nil { numFailedNotifications.WithLabelValues(r.integration.name).Inc() log.Debugf("Notify attempt %d for %q failed: %s", i, r.integration.name, err) if !retry { return ctx, alerts, fmt.Errorf("Cancelling notify retry for %q due to unrecoverable error: %s", r.integration.name, err) } // Save this error to be able to return the last seen error by an // integration upon context timeout. iErr = err } else { numNotifications.WithLabelValues(r.integration.name).Inc() return ctx, alerts, nil } case <-ctx.Done(): if iErr != nil { return ctx, nil, iErr } return ctx, nil, ctx.Err() } } } // SetNotifiesStage sets the notification information about passed alerts. The // passed alerts should have already been sent to the receivers. type SetNotifiesStage struct { nflog nflog.Log recv *nflogpb.Receiver } // NewSetNotifiesStage returns a new instance of a SetNotifiesStage. func NewSetNotifiesStage(l nflog.Log, recv *nflogpb.Receiver) *SetNotifiesStage { return &SetNotifiesStage{ nflog: l, recv: recv, } } // Exec implements the Stage interface. func (n SetNotifiesStage) Exec(ctx context.Context, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { gkey, ok := GroupKey(ctx) if !ok { return ctx, nil, fmt.Errorf("group key missing") } firing, ok := FiringAlerts(ctx) if !ok { return ctx, nil, fmt.Errorf("firing alerts missing") } resolved, ok := ResolvedAlerts(ctx) if !ok { return ctx, nil, fmt.Errorf("resolved alerts missing") } return ctx, alerts, n.nflog.Log(n.recv, gkey, firing, resolved) } prometheus-alertmanager-0.6.2+ds/notify/notify_test.go000066400000000000000000000317451314512360300231720ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package notify import ( "errors" "fmt" "io" "reflect" "testing" "time" "github.com/golang/protobuf/ptypes" "github.com/golang/protobuf/ptypes/timestamp" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" "golang.org/x/net/context" "github.com/prometheus/alertmanager/nflog" "github.com/prometheus/alertmanager/nflog/nflogpb" "github.com/prometheus/alertmanager/silence" "github.com/prometheus/alertmanager/silence/silencepb" "github.com/prometheus/alertmanager/types" ) type notifierConfigFunc func() bool func (f notifierConfigFunc) SendResolved() bool { return f() } type notifierFunc func(ctx context.Context, alerts ...*types.Alert) (bool, error) func (f notifierFunc) Notify(ctx context.Context, alerts ...*types.Alert) (bool, error) { return f(ctx, alerts...) } type failStage struct{} func (s failStage) Exec(ctx context.Context, as ...*types.Alert) (context.Context, []*types.Alert, error) { return ctx, nil, fmt.Errorf("some error") } type testNflog struct { qres []*nflogpb.Entry qerr error logFunc func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64) error } func (l *testNflog) Query(p ...nflog.QueryParam) ([]*nflogpb.Entry, error) { return l.qres, l.qerr } func (l *testNflog) Log(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64) error { return l.logFunc(r, gkey, firingAlerts, resolvedAlerts) } func (l *testNflog) GC() (int, error) { return 0, nil } func (l *testNflog) Snapshot(w io.Writer) (int, error) { return 0, nil } func mustTimestampProto(ts time.Time) *timestamp.Timestamp { tspb, err := ptypes.TimestampProto(ts) if err != nil { panic(err) } return tspb } func alertHashSet(hashes ...uint64) map[uint64]struct{} { res := map[uint64]struct{}{} for _, h := range hashes { res[h] = struct{}{} } return res } func TestDedupStageNeedsUpdate(t *testing.T) { now := utcNow() cases := []struct { entry *nflogpb.Entry firingAlerts map[uint64]struct{} repeat time.Duration res bool resErr bool }{ { entry: nil, firingAlerts: alertHashSet(2, 3, 4), res: true, }, { entry: &nflogpb.Entry{FiringAlerts: []uint64{1, 2, 3}}, firingAlerts: alertHashSet(2, 3, 4), res: true, }, { entry: &nflogpb.Entry{ FiringAlerts: []uint64{1, 2, 3}, Timestamp: time.Time{}, // zero timestamp should always update }, firingAlerts: alertHashSet(1, 2, 3), res: true, }, { entry: &nflogpb.Entry{ FiringAlerts: []uint64{1, 2, 3}, Timestamp: now.Add(-9 * time.Minute), }, repeat: 10 * time.Minute, firingAlerts: alertHashSet(1, 2, 3), res: false, }, { entry: &nflogpb.Entry{ FiringAlerts: []uint64{1, 2, 3}, Timestamp: now.Add(-11 * time.Minute), }, repeat: 10 * time.Minute, firingAlerts: alertHashSet(1, 2, 3), res: true, }, } for i, c := range cases { t.Log("case", i) s := &DedupStage{ now: func() time.Time { return now }, } ok, err := s.needsUpdate(c.entry, c.firingAlerts, nil, c.repeat) if c.resErr { require.Error(t, err) } else { require.NoError(t, err) } require.Equal(t, c.res, ok) } } func TestDedupStage(t *testing.T) { i := 0 now := utcNow() s := &DedupStage{ hash: func(a *types.Alert) uint64 { res := uint64(i) i++ return res }, now: func() time.Time { return now }, } ctx := context.Background() _, _, err := s.Exec(ctx) require.EqualError(t, err, "group key missing") ctx = WithGroupKey(ctx, "1") _, _, err = s.Exec(ctx) require.EqualError(t, err, "repeat interval missing") ctx = WithRepeatInterval(ctx, time.Hour) alerts := []*types.Alert{{}, {}, {}} // Must catch notification log query errors. s.nflog = &testNflog{ qerr: errors.New("bad things"), } ctx, res, err := s.Exec(ctx, alerts...) require.EqualError(t, err, "bad things") // ... but skip ErrNotFound. s.nflog = &testNflog{ qerr: nflog.ErrNotFound, } ctx, res, err = s.Exec(ctx, alerts...) require.NoError(t, err, "unexpected error on not found log entry") require.Equal(t, alerts, res, "input alerts differ from result alerts") s.nflog = &testNflog{ qerr: nil, qres: []*nflogpb.Entry{ {FiringAlerts: []uint64{0, 1, 2}}, {FiringAlerts: []uint64{1, 2, 3}}, }, } ctx, res, err = s.Exec(ctx, alerts...) require.Contains(t, err.Error(), "result size") // Must return no error and no alerts no need to update. i = 0 s.nflog = &testNflog{ qerr: nflog.ErrNotFound, qres: []*nflogpb.Entry{ { FiringAlerts: []uint64{0, 1, 2}, Timestamp: now, }, }, } ctx, res, err = s.Exec(ctx, alerts...) require.NoError(t, err) require.Nil(t, res, "unexpected alerts returned") // Must return no error and all input alerts on changes. i = 0 s.nflog = &testNflog{ qerr: nil, qres: []*nflogpb.Entry{ { FiringAlerts: []uint64{1, 2, 3, 4}, Timestamp: now, }, }, } ctx, res, err = s.Exec(ctx, alerts...) require.NoError(t, err) require.Equal(t, alerts, res, "unexpected alerts returned") } func TestMultiStage(t *testing.T) { var ( alerts1 = []*types.Alert{{}} alerts2 = []*types.Alert{{}, {}} alerts3 = []*types.Alert{{}, {}, {}} ) stage := MultiStage{ StageFunc(func(ctx context.Context, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { if !reflect.DeepEqual(alerts, alerts1) { t.Fatal("Input not equal to input of MultiStage") } ctx = context.WithValue(ctx, "key", "value") return ctx, alerts2, nil }), StageFunc(func(ctx context.Context, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { if !reflect.DeepEqual(alerts, alerts2) { t.Fatal("Input not equal to output of previous stage") } v, ok := ctx.Value("key").(string) if !ok || v != "value" { t.Fatalf("Expected value %q for key %q but got %q", "value", "key", v) } return ctx, alerts3, nil }), } _, alerts, err := stage.Exec(context.Background(), alerts1...) if err != nil { t.Fatalf("Exec failed: %s", err) } if !reflect.DeepEqual(alerts, alerts3) { t.Fatal("Output of MultiStage is not equal to the output of the last stage") } } func TestMultiStageFailure(t *testing.T) { var ( ctx = context.Background() s1 = failStage{} stage = MultiStage{s1} ) _, _, err := stage.Exec(ctx, nil) if err.Error() != "some error" { t.Fatal("Errors were not propagated correctly by MultiStage") } } func TestRoutingStage(t *testing.T) { var ( alerts1 = []*types.Alert{{}} alerts2 = []*types.Alert{{}, {}} ) stage := RoutingStage{ "name": StageFunc(func(ctx context.Context, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { if !reflect.DeepEqual(alerts, alerts1) { t.Fatal("Input not equal to input of RoutingStage") } return ctx, alerts2, nil }), "not": failStage{}, } ctx := WithReceiverName(context.Background(), "name") _, alerts, err := stage.Exec(ctx, alerts1...) if err != nil { t.Fatalf("Exec failed: %s", err) } if !reflect.DeepEqual(alerts, alerts2) { t.Fatal("Output of RoutingStage is not equal to the output of the inner stage") } } func TestIntegrationNoResolved(t *testing.T) { res := []*types.Alert{} r := notifierFunc(func(ctx context.Context, alerts ...*types.Alert) (bool, error) { res = append(res, alerts...) return false, nil }) i := Integration{ notifier: r, conf: notifierConfigFunc(func() bool { return false }), } alerts := []*types.Alert{ &types.Alert{ Alert: model.Alert{ EndsAt: time.Now().Add(-time.Hour), }, }, &types.Alert{ Alert: model.Alert{ EndsAt: time.Now().Add(time.Hour), }, }, } i.Notify(nil, alerts...) require.Equal(t, len(res), 1) } func TestIntegrationSendResolved(t *testing.T) { res := []*types.Alert{} r := notifierFunc(func(ctx context.Context, alerts ...*types.Alert) (bool, error) { res = append(res, alerts...) return false, nil }) i := Integration{ notifier: r, conf: notifierConfigFunc(func() bool { return true }), } alerts := []*types.Alert{ &types.Alert{ Alert: model.Alert{ EndsAt: time.Now().Add(-time.Hour), }, }, } i.Notify(nil, alerts...) require.Equal(t, len(res), 1) require.Equal(t, res, alerts) } func TestSetNotifiesStage(t *testing.T) { tnflog := &testNflog{} s := &SetNotifiesStage{ recv: &nflogpb.Receiver{GroupName: "test"}, nflog: tnflog, } alerts := []*types.Alert{{}, {}, {}} ctx := context.Background() resctx, res, err := s.Exec(ctx, alerts...) require.EqualError(t, err, "group key missing") require.Nil(t, res) require.NotNil(t, resctx) ctx = WithGroupKey(ctx, "1") resctx, res, err = s.Exec(ctx, alerts...) require.EqualError(t, err, "firing alerts missing") require.Nil(t, res) require.NotNil(t, resctx) ctx = WithFiringAlerts(ctx, []uint64{0, 1, 2}) resctx, res, err = s.Exec(ctx, alerts...) require.EqualError(t, err, "resolved alerts missing") require.Nil(t, res) require.NotNil(t, resctx) ctx = WithResolvedAlerts(ctx, []uint64{}) tnflog.logFunc = func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64) error { require.Equal(t, s.recv, r) require.Equal(t, "1", gkey) require.Equal(t, []uint64{0, 1, 2}, firingAlerts) require.Equal(t, []uint64{}, resolvedAlerts) return nil } resctx, res, err = s.Exec(ctx, alerts...) require.Nil(t, err) require.Equal(t, alerts, res) require.NotNil(t, resctx) ctx = WithFiringAlerts(ctx, []uint64{}) ctx = WithResolvedAlerts(ctx, []uint64{0, 1, 2}) tnflog.logFunc = func(r *nflogpb.Receiver, gkey string, firingAlerts, resolvedAlerts []uint64) error { require.Equal(t, s.recv, r) require.Equal(t, "1", gkey) require.Equal(t, []uint64{}, firingAlerts) require.Equal(t, []uint64{0, 1, 2}, resolvedAlerts) return nil } resctx, res, err = s.Exec(ctx, alerts...) require.Nil(t, err) require.Equal(t, alerts, res) require.NotNil(t, resctx) } func TestSilenceStage(t *testing.T) { silences, err := silence.New(silence.Options{}) if err != nil { t.Fatal(err) } if _, err := silences.Create(&silencepb.Silence{ EndsAt: utcNow().Add(time.Hour), Matchers: []*silencepb.Matcher{{Name: "mute", Pattern: "me"}}, }); err != nil { t.Fatal(err) } marker := types.NewMarker() silencer := NewSilenceStage(silences, marker) in := []model.LabelSet{ {}, {"test": "set"}, {"mute": "me"}, {"foo": "bar", "test": "set"}, {"foo": "bar", "mute": "me"}, {}, {"not": "muted"}, } out := []model.LabelSet{ {}, {"test": "set"}, {"foo": "bar", "test": "set"}, {}, {"not": "muted"}, } var inAlerts []*types.Alert for _, lset := range in { inAlerts = append(inAlerts, &types.Alert{ Alert: model.Alert{Labels: lset}, }) } // Set the second alert as previously silenced. It is expected to have // the WasSilenced flag set to true afterwards. marker.SetSilenced(inAlerts[1].Fingerprint(), "123") _, alerts, err := silencer.Exec(nil, inAlerts...) if err != nil { t.Fatalf("Exec failed: %s", err) } var got []model.LabelSet for i, a := range alerts { got = append(got, a.Labels) if a.WasSilenced != (i == 1) { t.Errorf("Expected WasSilenced to be %v for %d, was %v", i == 1, i, a.WasSilenced) } } if !reflect.DeepEqual(got, out) { t.Fatalf("Muting failed, expected: %v\ngot %v", out, got) } } func TestInhibitStage(t *testing.T) { // Mute all label sets that have a "mute" key. muter := types.MuteFunc(func(lset model.LabelSet) bool { _, ok := lset["mute"] return ok }) marker := types.NewMarker() inhibitor := NewInhibitStage(muter, marker) in := []model.LabelSet{ {}, {"test": "set"}, {"mute": "me"}, {"foo": "bar", "test": "set"}, {"foo": "bar", "mute": "me"}, {}, {"not": "muted"}, } out := []model.LabelSet{ {}, {"test": "set"}, {"foo": "bar", "test": "set"}, {}, {"not": "muted"}, } var inAlerts []*types.Alert for _, lset := range in { inAlerts = append(inAlerts, &types.Alert{ Alert: model.Alert{Labels: lset}, }) } // Set the second alert as previously inhibited. It is expected to have // the WasInhibited flag set to true afterwards. marker.SetInhibited(inAlerts[1].Fingerprint(), "123") _, alerts, err := inhibitor.Exec(nil, inAlerts...) if err != nil { t.Fatalf("Exec failed: %s", err) } var got []model.LabelSet for i, a := range alerts { got = append(got, a.Labels) if a.WasInhibited != (i == 1) { t.Errorf("Expected WasInhibited to be %v for %d, was %v", i == 1, i, a.WasInhibited) } } if !reflect.DeepEqual(got, out) { t.Fatalf("Muting failed, expected: %v\ngot %v", out, got) } } prometheus-alertmanager-0.6.2+ds/pkg/000077500000000000000000000000001314512360300175335ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/pkg/parse/000077500000000000000000000000001314512360300206455ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/pkg/parse/parse.go000066400000000000000000000026451314512360300223150ustar00rootroot00000000000000package parse import ( "fmt" "regexp" "strings" "github.com/prometheus/prometheus/pkg/labels" ) var ( re = regexp.MustCompile(`(?:\s?)(\w+)(=|=~|!=|!~)(?:\"([^"=~!]+)\"|([^"=~!]+))`) typeMap = map[string]labels.MatchType{ "=": labels.MatchEqual, "!=": labels.MatchNotEqual, "=~": labels.MatchRegexp, "!~": labels.MatchNotRegexp, } ) func Matchers(s string) ([]*labels.Matcher, error) { matchers := []*labels.Matcher{} if strings.HasPrefix(s, "{") { s = s[1:] } if strings.HasSuffix(s, "}") { s = s[:len(s)-1] } for _, toParse := range strings.Split(s, ",") { m, err := Matcher(toParse) if err != nil { return nil, err } matchers = append(matchers, m) } return matchers, nil } func Matcher(s string) (*labels.Matcher, error) { name, value, matchType, err := Input(s) if err != nil { return nil, err } m, err := labels.NewMatcher(matchType, name, value) if err != nil { return nil, err } return m, nil } func Input(s string) (name, value string, matchType labels.MatchType, err error) { ms := re.FindStringSubmatch(s) if len(ms) < 4 { return "", "", labels.MatchEqual, fmt.Errorf("bad matcher format") } var prs bool name = ms[1] matchType, prs = typeMap[ms[2]] if ms[3] != "" { value = ms[3] } else { value = ms[4] } if name == "" || value == "" || !prs { return "", "", labels.MatchEqual, fmt.Errorf("failed to parse") } return name, value, matchType, nil } prometheus-alertmanager-0.6.2+ds/pkg/parse/parse_test.go000066400000000000000000000062321314512360300233500ustar00rootroot00000000000000package parse import ( "reflect" "testing" "github.com/prometheus/prometheus/pkg/labels" ) func TestMatchers(t *testing.T) { testCases := []struct { input string want []*labels.Matcher }{ { input: `{foo="bar"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchEqual, "foo", "bar") return append(ms, m) }(), }, { input: `{foo=~"bar.*"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchRegexp, "foo", "bar.*") return append(ms, m) }(), }, { input: `{foo!="bar"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchNotEqual, "foo", "bar") return append(ms, m) }(), }, { input: `{foo!~"bar.*"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchNotRegexp, "foo", "bar.*") return append(ms, m) }(), }, { input: `{foo="bar", baz!="quux"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchEqual, "foo", "bar") m2, _ := labels.NewMatcher(labels.MatchNotEqual, "baz", "quux") return append(ms, m, m2) }(), }, { input: `{foo="bar", baz!~"quux.*"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchEqual, "foo", "bar") m2, _ := labels.NewMatcher(labels.MatchNotRegexp, "baz", "quux.*") return append(ms, m, m2) }(), }, { input: `{foo="bar",baz!~".*quux", derp="wat"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchEqual, "foo", "bar") m2, _ := labels.NewMatcher(labels.MatchNotRegexp, "baz", ".*quux") m3, _ := labels.NewMatcher(labels.MatchEqual, "derp", "wat") return append(ms, m, m2, m3) }(), }, { input: `{foo="bar", baz!="quux", derp="wat"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchEqual, "foo", "bar") m2, _ := labels.NewMatcher(labels.MatchNotEqual, "baz", "quux") m3, _ := labels.NewMatcher(labels.MatchEqual, "derp", "wat") return append(ms, m, m2, m3) }(), }, { input: `{foo="bar", baz!~".*quux.*", derp="wat"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchEqual, "foo", "bar") m2, _ := labels.NewMatcher(labels.MatchNotRegexp, "baz", ".*quux.*") m3, _ := labels.NewMatcher(labels.MatchEqual, "derp", "wat") return append(ms, m, m2, m3) }(), }, { input: `{foo="bar", instance=~"some-api.*"}`, want: func() []*labels.Matcher { ms := []*labels.Matcher{} m, _ := labels.NewMatcher(labels.MatchEqual, "foo", "bar") m2, _ := labels.NewMatcher(labels.MatchRegexp, "instance", "some-api.*") return append(ms, m, m2) }(), }, } for i, tc := range testCases { got, err := Matchers(tc.input) if err != nil { t.Fatalf("error (i=%d): %v", i, err) } if !reflect.DeepEqual(got, tc.want) { t.Fatalf("error not equal (i=%d):\ngot %v\nwant %v", i, got, tc.want) } } } prometheus-alertmanager-0.6.2+ds/provider/000077500000000000000000000000001314512360300206045ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/provider/mem/000077500000000000000000000000001314512360300213625ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/provider/mem/mem.go000066400000000000000000000101351314512360300224670ustar00rootroot00000000000000// Copyright 2016 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/lic:wenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package mem import ( "sync" "time" "github.com/prometheus/alertmanager/provider" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" ) // Alerts gives access to a set of alerts. All methods are goroutine-safe. type Alerts struct { mtx sync.RWMutex alerts map[model.Fingerprint]*types.Alert marker types.Marker intervalGC time.Duration stopGC chan struct{} listeners map[int]chan *types.Alert next int } // NewAlerts returns a new alert provider. func NewAlerts(m types.Marker, intervalGC time.Duration, path string) (*Alerts, error) { a := &Alerts{ alerts: map[model.Fingerprint]*types.Alert{}, marker: m, intervalGC: intervalGC, stopGC: make(chan struct{}), listeners: map[int]chan *types.Alert{}, next: 0, } go a.runGC() return a, nil } func (a *Alerts) runGC() { for { select { case <-a.stopGC: return case <-time.After(a.intervalGC): } a.mtx.Lock() for fp, alert := range a.alerts { // As we don't persist alerts, we no longer consider them after // they are resolved. Alerts waiting for resolved notifications are // held in memory in aggregation groups redundantly. if alert.EndsAt.Before(time.Now()) { delete(a.alerts, fp) a.marker.Delete(fp) } } a.mtx.Unlock() } } // Close the alert provider. func (a *Alerts) Close() error { close(a.stopGC) return nil } // Subscribe returns an iterator over active alerts that have not been // resolved and successfully notified about. // They are not guaranteed to be in chronological order. func (a *Alerts) Subscribe() provider.AlertIterator { var ( ch = make(chan *types.Alert, 200) done = make(chan struct{}) ) alerts, err := a.getPending() a.mtx.Lock() i := a.next a.next++ a.listeners[i] = ch a.mtx.Unlock() go func() { defer func() { a.mtx.Lock() delete(a.listeners, i) close(ch) a.mtx.Unlock() }() for _, a := range alerts { select { case ch <- a: case <-done: return } } <-done }() return provider.NewAlertIterator(ch, done, err) } // GetPending returns an iterator over all alerts that have // pending notifications. func (a *Alerts) GetPending() provider.AlertIterator { var ( ch = make(chan *types.Alert, 200) done = make(chan struct{}) ) alerts, err := a.getPending() go func() { defer close(ch) for _, a := range alerts { select { case ch <- a: case <-done: return } } }() return provider.NewAlertIterator(ch, done, err) } func (a *Alerts) getPending() ([]*types.Alert, error) { a.mtx.RLock() defer a.mtx.RUnlock() res := make([]*types.Alert, 0, len(a.alerts)) for _, alert := range a.alerts { res = append(res, alert) } return res, nil } // Get returns the alert for a given fingerprint. func (a *Alerts) Get(fp model.Fingerprint) (*types.Alert, error) { a.mtx.RLock() defer a.mtx.RUnlock() alert, ok := a.alerts[fp] if !ok { return nil, provider.ErrNotFound } return alert, nil } // Put adds the given alert to the set. func (a *Alerts) Put(alerts ...*types.Alert) error { a.mtx.Lock() defer a.mtx.Unlock() for _, alert := range alerts { fp := alert.Fingerprint() if old, ok := a.alerts[fp]; ok { // Merge alerts if there is an overlap in activity range. if (alert.EndsAt.After(old.StartsAt) && alert.EndsAt.Before(old.EndsAt)) || (alert.StartsAt.After(old.StartsAt) && alert.StartsAt.Before(old.EndsAt)) { alert = old.Merge(alert) } } a.alerts[fp] = alert for _, ch := range a.listeners { ch <- alert } } return nil } prometheus-alertmanager-0.6.2+ds/provider/mem/mem_test.go000066400000000000000000000112031314512360300235230ustar00rootroot00000000000000// Copyright 2016 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/lic:wenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package mem import ( "io/ioutil" "os" "reflect" "testing" "time" "github.com/kylelemons/godebug/pretty" "github.com/prometheus/alertmanager/provider" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" ) func init() { pretty.CompareConfig.IncludeUnexported = true } func TestAlertsPut(t *testing.T) { dir, err := ioutil.TempDir("", "alerts_test") if err != nil { t.Fatal(err) } marker := types.NewMarker() alerts, err := NewAlerts(marker, 30*time.Minute, dir) if err != nil { t.Fatal(err) } var ( t0 = time.Now() t1 = t0.Add(10 * time.Minute) ) insert := []*types.Alert{ { Alert: model.Alert{ Labels: model.LabelSet{"bar": "foo"}, Annotations: model.LabelSet{"foo": "bar"}, StartsAt: t0, EndsAt: t1, GeneratorURL: "http://example.com/prometheus", }, UpdatedAt: t0, Timeout: false, }, { Alert: model.Alert{ Labels: model.LabelSet{"bar": "foo2"}, Annotations: model.LabelSet{"foo": "bar2"}, StartsAt: t0, EndsAt: t1, GeneratorURL: "http://example.com/prometheus", }, UpdatedAt: t0, Timeout: false, }, { Alert: model.Alert{ Labels: model.LabelSet{"bar": "foo3"}, Annotations: model.LabelSet{"foo": "bar3"}, StartsAt: t0, EndsAt: t1, GeneratorURL: "http://example.com/prometheus", }, UpdatedAt: t0, Timeout: false, }, } if err := alerts.Put(insert...); err != nil { t.Fatalf("Insert failed: %s", err) } for i, a := range insert { res, err := alerts.Get(a.Fingerprint()) if err != nil { t.Fatalf("retrieval error: %s", err) } if !alertsEqual(res, a) { t.Errorf("Unexpected alert: %d", i) t.Fatalf(pretty.Compare(res, a)) } } } func TestAlertsGC(t *testing.T) { dir, err := ioutil.TempDir("", "alerts_test") if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) marker := types.NewMarker() alerts, err := NewAlerts(marker, 200*time.Millisecond, dir) if err != nil { t.Fatal(err) } var ( t0 = time.Now() t1 = t0.Add(100 * time.Millisecond) ) insert := []*types.Alert{ { Alert: model.Alert{ Labels: model.LabelSet{"bar": "foo"}, Annotations: model.LabelSet{"foo": "bar"}, StartsAt: t0, EndsAt: t1, GeneratorURL: "http://example.com/prometheus", }, UpdatedAt: t0, Timeout: false, }, { Alert: model.Alert{ Labels: model.LabelSet{"bar": "foo2"}, Annotations: model.LabelSet{"foo": "bar2"}, StartsAt: t0, EndsAt: t1, GeneratorURL: "http://example.com/prometheus", }, UpdatedAt: t0, Timeout: false, }, { Alert: model.Alert{ Labels: model.LabelSet{"bar": "foo3"}, Annotations: model.LabelSet{"foo": "bar3"}, StartsAt: t0, EndsAt: t1, GeneratorURL: "http://example.com/prometheus", }, UpdatedAt: t0, Timeout: false, }, } if err := alerts.Put(insert...); err != nil { t.Fatalf("Insert failed: %s", err) } for _, a := range insert { marker.SetActive(a.Fingerprint()) if !marker.Active(a.Fingerprint()) { t.Errorf("error setting status: %v", a) } } time.Sleep(300 * time.Millisecond) for i, a := range insert { _, err := alerts.Get(a.Fingerprint()) if err != provider.ErrNotFound { t.Errorf("alert %d didn't get GC'd", i) } s := marker.Status(a.Fingerprint()) if s.State != types.AlertStateUnprocessed { t.Errorf("marker %d didn't get GC'd: %v", i, s) } } } func alertsEqual(a1, a2 *types.Alert) bool { if !reflect.DeepEqual(a1.Labels, a2.Labels) { return false } if !reflect.DeepEqual(a1.Annotations, a2.Annotations) { return false } if a1.GeneratorURL != a2.GeneratorURL { return false } if !a1.StartsAt.Equal(a2.StartsAt) { return false } if !a1.EndsAt.Equal(a2.EndsAt) { return false } if !a1.UpdatedAt.Equal(a2.UpdatedAt) { return false } return a1.Timeout == a2.Timeout } func alertListEqual(a1, a2 []*types.Alert) bool { if len(a1) != len(a2) { return false } for i, a := range a1 { if !alertsEqual(a, a2[i]) { return false } } return true } prometheus-alertmanager-0.6.2+ds/provider/provider.go000066400000000000000000000054721314512360300227750ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package provider import ( "fmt" "github.com/prometheus/common/model" "github.com/prometheus/alertmanager/types" ) var ( // ErrNotFound is returned if a provider cannot find a requested item. ErrNotFound = fmt.Errorf("item not found") ) // Iterator provides the functions common to all iterators. To be useful, a // specific iterator interface (e.g. AlertIterator) has to be implemented that // provides a Next method. type Iterator interface { // Err returns the current error. It is not safe to call it concurrently // with other iterator methods or while reading from a channel returned // by the iterator. Err() error // Close must be called to release resources once the iterator is not // used anymore. Close() } // AlertIterator is an Iterator for Alerts. type AlertIterator interface { Iterator // Next returns a channel that will be closed once the iterator is // exhausted. It is not necessary to exhaust the iterator but Close must // be called in any case to release resources used by the iterator (even // if the iterator is exhausted). Next() <-chan *types.Alert } // NewAlertIterator returns a new AlertIterator based on the generic alertIterator type func NewAlertIterator(ch <-chan *types.Alert, done chan struct{}, err error) AlertIterator { return &alertIterator{ ch: ch, done: done, err: err, } } // alertIterator implements AlertIterator. So far, this one fits all providers. type alertIterator struct { ch <-chan *types.Alert done chan struct{} err error } func (ai alertIterator) Next() <-chan *types.Alert { return ai.ch } func (ai alertIterator) Err() error { return ai.err } func (ai alertIterator) Close() { close(ai.done) } // Alerts gives access to a set of alerts. All methods are goroutine-safe. type Alerts interface { // Subscribe returns an iterator over active alerts that have not been // resolved and successfully notified about. // They are not guaranteed to be in chronological order. Subscribe() AlertIterator // GetPending returns an iterator over all alerts that have // pending notifications. GetPending() AlertIterator // Get returns the alert for a given fingerprint. Get(model.Fingerprint) (*types.Alert, error) // Put adds the given alert to the set. Put(...*types.Alert) error } prometheus-alertmanager-0.6.2+ds/scripts/000077500000000000000000000000001314512360300204415ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/scripts/genproto.sh000077500000000000000000000021101314512360300226270ustar00rootroot00000000000000#!/usr/bin/env bash # # Generate all protobuf bindings. # Run from repository root. # # Initial script taken from etcd under the Apache 2.0 license # File: https://github.com/coreos/etcd/blob/78a5eb79b510eb497deddd1a76f5153bc4b202d2/scripts/genproto.sh set -e set -u if ! [[ "$0" =~ "scripts/genproto.sh" ]]; then echo "must be run from repository root" exit 255 fi if ! [[ $(protoc --version) =~ "3.2.0" ]]; then echo "could not find protoc 3.2.0, is it installed + in PATH?" exit 255 fi GOGOPROTO_ROOT="${GOPATH}/src/github.com/gogo/protobuf" GOGOPROTO_PATH="${GOGOPROTO_ROOT}:${GOGOPROTO_ROOT}/protobuf" GRPC_GATEWAY_ROOT="${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway" DIRS="nflog/nflogpb silence/silencepb" for dir in ${DIRS}; do pushd ${dir} protoc --gogofast_out=plugins=grpc:. -I=. \ -I="${GOGOPROTO_PATH}" \ -I="${GRPC_GATEWAY_ROOT}/third_party/googleapis" \ *.proto sed -i.bak -E 's/import _ \"gogoproto\"//g' *.pb.go sed -i.bak -E 's/import _ \"google\/protobuf\"//g' *.pb.go rm -f *.bak goimports -w *.pb.go popd done prometheus-alertmanager-0.6.2+ds/silence/000077500000000000000000000000001314512360300203745ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/silence/silence.go000066400000000000000000000467041314512360300223600ustar00rootroot00000000000000// Copyright 2016 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package silence provides a storage for silences, which can share its // state over a mesh network and snapshot it. package silence import ( "bytes" "errors" "fmt" "io" "math/rand" "os" "regexp" "sync" "time" "github.com/matttproud/golang_protobuf_extensions/pbutil" pb "github.com/prometheus/alertmanager/silence/silencepb" "github.com/prometheus/alertmanager/types" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/log" "github.com/prometheus/common/model" "github.com/satori/go.uuid" "github.com/weaveworks/mesh" ) // ErrNotFound is returned if a silence was not found. var ErrNotFound = fmt.Errorf("not found") func utcNow() time.Time { return time.Now().UTC() } type matcherCache map[*pb.Silence]types.Matchers // Get retrieves the matchers for a given silence. If it is a missed cache // access, it compiles and adds the matchers of the requested silence to the // cache. func (c matcherCache) Get(s *pb.Silence) (types.Matchers, error) { if m, ok := c[s]; ok { return m, nil } return c.add(s) } // add compiles a silences' matchers and adds them to the cache. // It returns the compiled matchers. func (c matcherCache) add(s *pb.Silence) (types.Matchers, error) { var ( ms types.Matchers mt *types.Matcher ) for _, m := range s.Matchers { mt = &types.Matcher{ Name: m.Name, Value: m.Pattern, } switch m.Type { case pb.Matcher_EQUAL: mt.IsRegex = false case pb.Matcher_REGEXP: mt.IsRegex = true } err := mt.Init() if err != nil { return nil, err } ms = append(ms, mt) } c[s] = ms return ms, nil } // Silences holds a silence state that can be modified, queried, and snapshot. type Silences struct { logger log.Logger metrics *metrics now func() time.Time retention time.Duration gossip mesh.Gossip // gossip channel for sharing silences // We store silences in a map of IDs for now. Currently, the memory // state is equivalent to the mesh.GossipData representation. // In the future we'll want support for efficient queries by time // range and affected labels. // Mutex also guards the matcherCache, which always need write lock access. mtx sync.Mutex st gossipData mc matcherCache } type metrics struct { gcDuration prometheus.Summary snapshotDuration prometheus.Summary queriesTotal prometheus.Counter queryErrorsTotal prometheus.Counter queryDuration prometheus.Histogram } func newMetrics(r prometheus.Registerer) *metrics { m := &metrics{} m.gcDuration = prometheus.NewSummary(prometheus.SummaryOpts{ Name: "alertmanager_silences_gc_duration_seconds", Help: "Duration of the last silence garbage collection cycle.", }) m.snapshotDuration = prometheus.NewSummary(prometheus.SummaryOpts{ Name: "alertmanager_silences_snapshot_duration_seconds", Help: "Duration of the last silence snapshot.", }) m.queriesTotal = prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_silences_queries_total", Help: "How many silence queries were received.", }) m.queryErrorsTotal = prometheus.NewCounter(prometheus.CounterOpts{ Name: "alertmanager_silences_query_errors_total", Help: "How many silence received queries did not succeed.", }) m.queryDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ Name: "alertmanager_silences_query_duration_seconds", Help: "Duration of silence query evaluation.", }) if r != nil { r.MustRegister( m.gcDuration, m.snapshotDuration, m.queriesTotal, m.queryErrorsTotal, m.queryDuration, ) } return m } // Options exposes configuration options for creating a new Silences object. // Its zero value is a safe default. type Options struct { // A snapshot file or reader from which the initial state is loaded. // None or only one of them must be set. SnapshotFile string SnapshotReader io.Reader // Retention time for newly created Silences. Silences may be // garbage collected after the given duration after they ended. Retention time.Duration // A function creating a mesh.Gossip on being called with a mesh.Gossiper. Gossip func(g mesh.Gossiper) mesh.Gossip // A logger used by background processing. Logger log.Logger Metrics prometheus.Registerer } func (o *Options) validate() error { if o.SnapshotFile != "" && o.SnapshotReader != nil { return fmt.Errorf("only one of SnapshotFile and SnapshotReader must be set") } return nil } // New returns a new Silences object with the given configuration. func New(o Options) (*Silences, error) { if err := o.validate(); err != nil { return nil, err } if o.SnapshotFile != "" { if r, err := os.Open(o.SnapshotFile); err != nil { if !os.IsNotExist(err) { return nil, err } } else { o.SnapshotReader = r } } s := &Silences{ mc: matcherCache{}, logger: log.NewNopLogger(), metrics: newMetrics(o.Metrics), retention: o.Retention, now: utcNow, gossip: nopGossip{}, st: gossipData{}, } if o.Logger != nil { s.logger = o.Logger } if o.Gossip != nil { s.gossip = o.Gossip(gossiper{s}) } if o.SnapshotReader != nil { if err := s.loadSnapshot(o.SnapshotReader); err != nil { return s, err } } return s, nil } type nopGossip struct{} func (nopGossip) GossipBroadcast(d mesh.GossipData) {} func (nopGossip) GossipUnicast(mesh.PeerName, []byte) error { return nil } // Maintenance garbage collects the silence state at the given interval. If the snapshot // file is set, a snapshot is written to it afterwards. // Terminates on receiving from stopc. func (s *Silences) Maintenance(interval time.Duration, snapf string, stopc <-chan struct{}) { t := time.NewTicker(interval) defer t.Stop() f := func() error { start := s.now() s.logger.Info("running maintenance") defer s.logger.With("duration", s.now().Sub(start)).Info("maintenance done") if _, err := s.GC(); err != nil { return err } if snapf == "" { return nil } f, err := openReplace(snapf) if err != nil { return err } // TODO(fabxc): potentially expose snapshot size in log message. if _, err := s.Snapshot(f); err != nil { return err } return f.Close() } Loop: for { select { case <-stopc: break Loop case <-t.C: if err := f(); err != nil { s.logger.With("err", err).Error("running maintenance failed") } } } // No need for final maintenance if we don't want to snapshot. if snapf == "" { return } if err := f(); err != nil { s.logger.With("err", err).Info("msg", "creating shutdown snapshot failed") } } // GC runs a garbage collection that removes silences that have ended longer // than the configured retention time ago. func (s *Silences) GC() (int, error) { start := time.Now() defer func() { s.metrics.gcDuration.Observe(time.Since(start).Seconds()) }() now := s.now() var n int s.mtx.Lock() defer s.mtx.Unlock() for id, sil := range s.st { if sil.ExpiresAt.IsZero() { return n, errors.New("unexpected zero expiration timestamp") } if !sil.ExpiresAt.After(now) { delete(s.st, id) delete(s.mc, sil.Silence) n++ } } return n, nil } func validateMatcher(m *pb.Matcher) error { if !model.LabelName(m.Name).IsValid() { return fmt.Errorf("invalid label name %q", m.Name) } switch m.Type { case pb.Matcher_EQUAL: if !model.LabelValue(m.Pattern).IsValid() { return fmt.Errorf("invalid label value %q", m.Pattern) } case pb.Matcher_REGEXP: if _, err := regexp.Compile(m.Pattern); err != nil { return fmt.Errorf("invalid regular expression %q: %s", m.Pattern, err) } default: return fmt.Errorf("unknown matcher type %q", m.Type) } return nil } func validateSilence(s *pb.Silence) error { if s.Id == "" { return errors.New("ID missing") } if len(s.Matchers) == 0 { return errors.New("at least one matcher required") } for i, m := range s.Matchers { if err := validateMatcher(m); err != nil { return fmt.Errorf("invalid label matcher %d: %s", i, err) } } if s.StartsAt.IsZero() { return errors.New("invalid zero start timestamp") } if s.EndsAt.IsZero() { return errors.New("invalid zero end timestamp") } if s.EndsAt.Before(s.StartsAt) { return errors.New("end time must not be before start time") } if s.UpdatedAt.IsZero() { return errors.New("invalid zero update timestamp") } return nil } // cloneSilence returns a shallow copy of a silence. func cloneSilence(sil *pb.Silence) *pb.Silence { s := *sil return &s } func (s *Silences) getSilence(id string) (*pb.Silence, bool) { msil, ok := s.st[id] if !ok { return nil, false } return msil.Silence, true } func (s *Silences) setSilence(sil *pb.Silence) error { msil := &pb.MeshSilence{ Silence: sil, ExpiresAt: sil.EndsAt.Add(s.retention), } st := gossipData{sil.Id: msil} s.st.Merge(st) s.gossip.GossipBroadcast(st) return nil } // Create adds a new silence and returns its ID. func (s *Silences) Create(sil *pb.Silence) (id string, err error) { if sil.Id != "" { return "", fmt.Errorf("unexpected ID in new silence") } sil.Id = uuid.NewV4().String() now := s.now() if sil.StartsAt.IsZero() { sil.StartsAt = now } else if sil.StartsAt.Before(now) { return "", fmt.Errorf("new silence must not start in the past") } sil.UpdatedAt = now if err := validateSilence(sil); err != nil { return "", fmt.Errorf("invalid silence: %s", err) } s.mtx.Lock() defer s.mtx.Unlock() if err := s.setSilence(sil); err != nil { return "", err } return sil.Id, nil } // Expire the silence with the given ID immediately. func (s *Silences) Expire(id string) error { s.mtx.Lock() defer s.mtx.Unlock() sil, ok := s.getSilence(id) if !ok { return ErrNotFound } now := s.now() sil, err := silenceSetTimeRange(sil, now, sil.StartsAt, now) if err != nil { return err } return s.setSilence(sil) } // SetTimeRange adjust the time range of a silence if allowed. If start or end // are zero times, the current value remains unmodified. func (s *Silences) SetTimeRange(id string, start, end time.Time) error { now := s.now() s.mtx.Lock() defer s.mtx.Unlock() sil, ok := s.getSilence(id) if !ok { return ErrNotFound } if start.IsZero() { start = sil.StartsAt } if end.IsZero() { end = sil.EndsAt } sil, err := silenceSetTimeRange(sil, now, start, end) if err != nil { return err } return s.setSilence(sil) } func silenceSetTimeRange(sil *pb.Silence, now, start, end time.Time) (*pb.Silence, error) { if end.Before(start) { return nil, fmt.Errorf("end time must not be before start time") } // Validate modification based on current silence state. switch st := getState(sil, now); st { case StateActive: if !start.Equal(sil.StartsAt) { return nil, fmt.Errorf("start time of active silence cannot be modified") } if end.Before(now) { return nil, fmt.Errorf("end time cannot be set into the past") } case StatePending: if start.Before(now) { return nil, fmt.Errorf("start time cannot be set into the past") } case StateExpired: return nil, fmt.Errorf("expired silence must not be modified") default: return nil, fmt.Errorf("unknown silence state %v", st) } sil = cloneSilence(sil) sil.StartsAt = start sil.EndsAt = end sil.UpdatedAt = now return sil, nil } // AddComment adds a new comment to the silence with the given ID. func (s *Silences) AddComment(id string, author, comment string) error { panic("not implemented") } // QueryParam expresses parameters along which silences are queried. type QueryParam func(*query) error type query struct { ids []string filters []silenceFilter } // silenceFilter is a function that returns true if a silence // should be dropped from a result set for a given time. type silenceFilter func(*pb.Silence, *Silences, time.Time) (bool, error) var errNotSupported = errors.New("query parameter not supported") // QIDs configures a query to select the given silence IDs. func QIDs(ids ...string) QueryParam { return func(q *query) error { q.ids = append(q.ids, ids...) return nil } } // QTimeRange configures a query to search for silences that are active // in the given time range. // TODO(fabxc): not supported yet. func QTimeRange(start, end time.Time) QueryParam { return func(q *query) error { return errNotSupported } } // QMatches returns silences that match the given label set. func QMatches(set model.LabelSet) QueryParam { return func(q *query) error { f := func(sil *pb.Silence, s *Silences, _ time.Time) (bool, error) { m, err := s.mc.Get(sil) if err != nil { return true, err } return m.Match(set), nil } q.filters = append(q.filters, f) return nil } } // SilenceState describes the state of a silence based on its time range. type SilenceState string // The only possible states of a silence w.r.t a timestamp. const ( StateActive SilenceState = "active" StatePending = "pending" StateExpired = "expired" ) // getState returns a silence's SilenceState at the given timestamp. func getState(sil *pb.Silence, ts time.Time) SilenceState { if ts.Before(sil.StartsAt) { return StatePending } if ts.After(sil.EndsAt) { return StateExpired } return StateActive } // QState filters queried silences by the given states. func QState(states ...SilenceState) QueryParam { return func(q *query) error { f := func(sil *pb.Silence, _ *Silences, now time.Time) (bool, error) { s := getState(sil, now) for _, ps := range states { if s == ps { return true, nil } } return false, nil } q.filters = append(q.filters, f) return nil } } // Query for silences based on the given query parameters. func (s *Silences) Query(params ...QueryParam) ([]*pb.Silence, error) { start := time.Now() s.metrics.queriesTotal.Inc() sils, err := func() ([]*pb.Silence, error) { q := &query{} for _, p := range params { if err := p(q); err != nil { return nil, err } } return s.query(q, s.now()) }() if err != nil { s.metrics.queryErrorsTotal.Inc() } s.metrics.queryDuration.Observe(time.Since(start).Seconds()) return sils, err } func (s *Silences) query(q *query, now time.Time) ([]*pb.Silence, error) { // If we have an ID constraint, all silences are our base set. // This and the use of post-filter functions is the // the trivial solution for now. var res []*pb.Silence s.mtx.Lock() defer s.mtx.Unlock() if q.ids != nil { for _, id := range q.ids { if s, ok := s.st[string(id)]; ok { res = append(res, s.Silence) } } } else { for _, sil := range s.st { res = append(res, sil.Silence) } } var resf []*pb.Silence for _, sil := range res { remove := false for _, f := range q.filters { ok, err := f(sil, s, now) if err != nil { return nil, err } if !ok { remove = true break } } if !remove { resf = append(resf, sil) } } return resf, nil } // loadSnapshot loads a snapshot generated by Snapshot() into the state. // Any previous state is wiped. func (s *Silences) loadSnapshot(r io.Reader) error { st := gossipData{} s.mtx.Lock() defer s.mtx.Unlock() for { var sil pb.MeshSilence if _, err := pbutil.ReadDelimited(r, &sil); err != nil { if err == io.EOF { break } return err } st[sil.Silence.Id] = &sil _, err := s.mc.Get(sil.Silence) if err != nil { return err } } s.st = st return nil } // Snapshot writes the full internal state into the writer and returns the number of bytes // written. func (s *Silences) Snapshot(w io.Writer) (int, error) { start := time.Now() defer func() { s.metrics.snapshotDuration.Observe(time.Since(start).Seconds()) }() s.mtx.Lock() defer s.mtx.Unlock() var n int for _, s := range s.st { m, err := pbutil.WriteDelimited(w, s) if err != nil { return n + m, err } n += m } return n, nil } type gossiper struct { *Silences } // Gossip implements the mesh.Gossiper interface. func (g gossiper) Gossip() mesh.GossipData { g.mtx.Lock() defer g.mtx.Unlock() return g.st.clone() } // OnGossip implements the mesh.Gossiper interface. func (g gossiper) OnGossip(msg []byte) (mesh.GossipData, error) { gd, err := decodeGossipData(msg) if err != nil { return nil, err } g.mtx.Lock() defer g.mtx.Unlock() if delta := g.st.mergeDelta(gd); len(delta) > 0 { return delta, nil } return nil, nil } // OnGossipBroadcast implements the mesh.Gossiper interface. func (g gossiper) OnGossipBroadcast(src mesh.PeerName, msg []byte) (mesh.GossipData, error) { gd, err := decodeGossipData(msg) if err != nil { return nil, err } g.mtx.Lock() defer g.mtx.Unlock() return g.st.mergeDelta(gd), nil } // OnGossipUnicast implements the mesh.Gossiper interface. // It always panics. func (g gossiper) OnGossipUnicast(src mesh.PeerName, msg []byte) error { panic("not implemented") } type gossipData map[string]*pb.MeshSilence func decodeGossipData(msg []byte) (gossipData, error) { gd := gossipData{} rd := bytes.NewReader(msg) for { var s pb.MeshSilence if _, err := pbutil.ReadDelimited(rd, &s); err != nil { if err == io.EOF { break } return gd, err } gd[s.Silence.Id] = &s } return gd, nil } // Encode implements the mesh.GossipData interface. func (gd gossipData) Encode() [][]byte { // Split into sub-messages of ~1MB. const maxSize = 1024 * 1024 var ( buf bytes.Buffer res [][]byte n int ) for _, s := range gd { m, err := pbutil.WriteDelimited(&buf, s) n += m if err != nil { // TODO(fabxc): log error and skip entry. Or can this really not happen with a bytes.Buffer? panic(err) } if n > maxSize { res = append(res, buf.Bytes()) buf = bytes.Buffer{} } } if buf.Len() > 0 { res = append(res, buf.Bytes()) } return res } func (gd gossipData) clone() gossipData { res := make(gossipData, len(gd)) for id, s := range gd { res[id] = s } return res } // Merge the silence set with gossip data and return a new silence state. func (gd gossipData) Merge(other mesh.GossipData) mesh.GossipData { for id, s := range other.(gossipData) { prev, ok := gd[id] if !ok { gd[id] = s continue } if prev.Silence.UpdatedAt.Before(s.Silence.UpdatedAt) { gd[id] = s } } return gd } // mergeDelta behaves like Merge but returns a gossipData only // containing things that have changed. func (gd gossipData) mergeDelta(od gossipData) gossipData { delta := gossipData{} for id, s := range od { prev, ok := gd[id] if !ok { gd[id] = s delta[id] = s continue } if prev.Silence.UpdatedAt.Before(s.Silence.UpdatedAt) { gd[id] = s delta[id] = s } } return delta } // replaceFile wraps a file that is moved to another filename on closing. type replaceFile struct { *os.File filename string } func (f *replaceFile) Close() error { if err := f.File.Sync(); err != nil { return err } if err := f.File.Close(); err != nil { return err } return os.Rename(f.File.Name(), f.filename) } // openReplace opens a new temporary file that is moved to filename on closing. func openReplace(filename string) (*replaceFile, error) { tmpFilename := fmt.Sprintf("%s.%x", filename, uint64(rand.Int63())) f, err := os.Create(tmpFilename) if err != nil { return nil, err } rf := &replaceFile{ File: f, filename: filename, } return rf, nil } prometheus-alertmanager-0.6.2+ds/silence/silence_test.go000066400000000000000000000514451314512360300234150ustar00rootroot00000000000000// Copyright 2016 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package silence import ( "bytes" "io/ioutil" "os" "sort" "strings" "testing" "time" pb "github.com/prometheus/alertmanager/silence/silencepb" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" "github.com/weaveworks/mesh" ) func TestOptionsValidate(t *testing.T) { cases := []struct { options *Options err string }{ { options: &Options{ SnapshotReader: &bytes.Buffer{}, }, }, { options: &Options{ SnapshotFile: "test.bkp", }, }, { options: &Options{ SnapshotFile: "test bkp", SnapshotReader: &bytes.Buffer{}, }, err: "only one of SnapshotFile and SnapshotReader must be set", }, } for _, c := range cases { err := c.options.validate() if err == nil { if c.err != "" { t.Errorf("expected error containing %q but got none", c.err) } continue } if err != nil && c.err == "" { t.Errorf("unexpected error %q", err) continue } if !strings.Contains(err.Error(), c.err) { t.Errorf("expected error to contain %q but got %q", c.err, err) } } } func TestSilencesGC(t *testing.T) { s, err := New(Options{}) require.NoError(t, err) now := utcNow() s.now = func() time.Time { return now } newSilence := func(exp time.Time) *pb.MeshSilence { return &pb.MeshSilence{ExpiresAt: exp} } s.st = gossipData{ "1": newSilence(now), "2": newSilence(now.Add(-time.Second)), "3": newSilence(now.Add(time.Second)), } want := gossipData{ "3": newSilence(now.Add(time.Second)), } n, err := s.GC() require.NoError(t, err) require.Equal(t, 2, n) require.Equal(t, want, s.st) } func TestSilencesSnapshot(t *testing.T) { // Check whether storing and loading the snapshot is symmetric. now := utcNow() cases := []struct { entries []*pb.MeshSilence }{ { entries: []*pb.MeshSilence{ { Silence: &pb.Silence{ Id: "3be80475-e219-4ee7-b6fc-4b65114e362f", Matchers: []*pb.Matcher{ {Name: "label1", Pattern: "val1", Type: pb.Matcher_EQUAL}, {Name: "label2", Pattern: "val.+", Type: pb.Matcher_REGEXP}, }, StartsAt: now, EndsAt: now, UpdatedAt: now, }, ExpiresAt: now, }, { Silence: &pb.Silence{ Id: "4b1e760d-182c-4980-b873-c1a6827c9817", Matchers: []*pb.Matcher{ {Name: "label1", Pattern: "val1", Type: pb.Matcher_EQUAL}, }, StartsAt: now.Add(time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now, }, ExpiresAt: now.Add(24 * time.Hour), }, }, }, } for _, c := range cases { f, err := ioutil.TempFile("", "snapshot") require.NoError(t, err, "creating temp file failed") s1 := &Silences{st: gossipData{}, metrics: newMetrics(nil)} // Setup internal state manually. for _, e := range c.entries { s1.st[e.Silence.Id] = e } _, err = s1.Snapshot(f) require.NoError(t, err, "creating snapshot failed") require.NoError(t, f.Close(), "closing snapshot file failed") f, err = os.Open(f.Name()) require.NoError(t, err, "opening snapshot file failed") // Check again against new nlog instance. s2 := &Silences{mc: matcherCache{}} err = s2.loadSnapshot(f) require.NoError(t, err, "error loading snapshot") require.Equal(t, s1.st, s2.st, "state after loading snapshot did not match snapshotted state") require.NoError(t, f.Close(), "closing snapshot file failed") } } type mockGossip struct { broadcast func(mesh.GossipData) } func (g *mockGossip) GossipBroadcast(d mesh.GossipData) { g.broadcast(d) } func (g *mockGossip) GossipUnicast(mesh.PeerName, []byte) error { panic("not implemented") } func TestSilencesSetSilence(t *testing.T) { s, err := New(Options{ Retention: time.Minute, }) require.NoError(t, err) now := utcNow() nowpb := now sil := &pb.Silence{ Id: "some_id", EndsAt: nowpb, } want := gossipData{ "some_id": &pb.MeshSilence{ Silence: sil, ExpiresAt: now.Add(time.Minute), }, } var called bool s.gossip = &mockGossip{ broadcast: func(d mesh.GossipData) { data, ok := d.(gossipData) require.True(t, ok, "gossip data of unknown type") require.Equal(t, want, data, "unexpected gossip broadcast data") called = true }, } require.NoError(t, s.setSilence(sil)) require.True(t, called, "GossipBroadcast was not called") require.Equal(t, want, s.st, "Unexpected silence state") } func TestSilenceCreate(t *testing.T) { s, err := New(Options{ Retention: time.Hour, }) require.NoError(t, err) now := utcNow() s.now = func() time.Time { return now } // Insert silence with fixed start time. sil1 := &pb.Silence{ Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: now.Add(2 * time.Minute), EndsAt: now.Add(5 * time.Minute), } id1, err := s.Create(sil1) require.NoError(t, err) require.NotEqual(t, id1, "") want := gossipData{ id1: &pb.MeshSilence{ Silence: &pb.Silence{ Id: id1, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: now.Add(2 * time.Minute), EndsAt: now.Add(5 * time.Minute), UpdatedAt: now, }, ExpiresAt: now.Add(5*time.Minute + s.retention), }, } require.Equal(t, want, s.st, "unexpected state after silence creation") // Insert silence with unset start time. Must be set to now. sil2 := &pb.Silence{ Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, EndsAt: now.Add(1 * time.Minute), } id2, err := s.Create(sil2) require.NoError(t, err) require.NotEqual(t, id2, "") want = gossipData{ id1: &pb.MeshSilence{ Silence: &pb.Silence{ Id: id1, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: now.Add(2 * time.Minute), EndsAt: now.Add(5 * time.Minute), UpdatedAt: now, }, ExpiresAt: now.Add(5*time.Minute + s.retention), }, id2: &pb.MeshSilence{ Silence: &pb.Silence{ Id: id2, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: now, EndsAt: now.Add(1 * time.Minute), UpdatedAt: now, }, ExpiresAt: now.Add(1*time.Minute + s.retention), }, } require.Equal(t, want, s.st, "unexpected state after silence creation") } func TestSilencesCreateFail(t *testing.T) { s, err := New(Options{}) require.NoError(t, err) now := utcNow() s.now = func() time.Time { return now } cases := []struct { s *pb.Silence err string }{ { s: &pb.Silence{Id: "some_id"}, err: "unexpected ID in new silence", }, { s: &pb.Silence{StartsAt: now.Add(-time.Minute)}, err: "new silence must not start in the past", }, { s: &pb.Silence{}, // Silence without matcher. err: "invalid silence", }, } for _, c := range cases { _, err := s.Create(c.s) if err == nil { if c.err != "" { t.Errorf("expected error containing %q but got none", c.err) } continue } if err != nil && c.err == "" { t.Errorf("unexpected error %q", err) continue } if !strings.Contains(err.Error(), c.err) { t.Errorf("expected error to contain %q but got %q", c.err, err) } } } func TestQState(t *testing.T) { now := utcNow() cases := []struct { sil *pb.Silence states []SilenceState keep bool }{ { sil: &pb.Silence{ StartsAt: now.Add(time.Minute), EndsAt: now.Add(time.Hour), }, states: []SilenceState{StateActive, StateExpired}, keep: false, }, { sil: &pb.Silence{ StartsAt: now.Add(time.Minute), EndsAt: now.Add(time.Hour), }, states: []SilenceState{StatePending}, keep: true, }, { sil: &pb.Silence{ StartsAt: now.Add(time.Minute), EndsAt: now.Add(time.Hour), }, states: []SilenceState{StateExpired, StatePending}, keep: true, }, } for i, c := range cases { q := &query{} QState(c.states...)(q) f := q.filters[0] keep, err := f(c.sil, nil, now) require.NoError(t, err) require.Equal(t, c.keep, keep, "unexpected filter result for case %d", i) } } func TestQMatches(t *testing.T) { qp := QMatches(model.LabelSet{ "job": "test", "instance": "web-1", "path": "/user/profile", "method": "GET", }) q := &query{} qp(q) f := q.filters[0] cases := []struct { sil *pb.Silence drop bool }{ { sil: &pb.Silence{ Matchers: []*pb.Matcher{ {Name: "job", Pattern: "test", Type: pb.Matcher_EQUAL}, }, }, drop: true, }, { sil: &pb.Silence{ Matchers: []*pb.Matcher{ {Name: "job", Pattern: "test", Type: pb.Matcher_EQUAL}, {Name: "method", Pattern: "POST", Type: pb.Matcher_EQUAL}, }, }, drop: false, }, { sil: &pb.Silence{ Matchers: []*pb.Matcher{ {Name: "path", Pattern: "/user/.+", Type: pb.Matcher_REGEXP}, }, }, drop: true, }, { sil: &pb.Silence{ Matchers: []*pb.Matcher{ {Name: "path", Pattern: "/user/.+", Type: pb.Matcher_REGEXP}, {Name: "path", Pattern: "/nothing/.+", Type: pb.Matcher_REGEXP}, }, }, drop: false, }, } for _, c := range cases { drop, err := f(c.sil, &Silences{mc: matcherCache{}}, time.Time{}) require.NoError(t, err) require.Equal(t, c.drop, drop, "unexpected filter result") } } func TestSilencesQuery(t *testing.T) { s, err := New(Options{}) require.NoError(t, err) s.st = gossipData{ "1": &pb.MeshSilence{Silence: &pb.Silence{Id: "1"}}, "2": &pb.MeshSilence{Silence: &pb.Silence{Id: "2"}}, "3": &pb.MeshSilence{Silence: &pb.Silence{Id: "3"}}, "4": &pb.MeshSilence{Silence: &pb.Silence{Id: "4"}}, "5": &pb.MeshSilence{Silence: &pb.Silence{Id: "5"}}, } cases := []struct { q *query exp []*pb.Silence }{ { // Default query of retrieving all silences. q: &query{}, exp: []*pb.Silence{ {Id: "1"}, {Id: "2"}, {Id: "3"}, {Id: "4"}, {Id: "5"}, }, }, { // Retrieve by IDs. q: &query{ ids: []string{"2", "5"}, }, exp: []*pb.Silence{ {Id: "2"}, {Id: "5"}, }, }, { // Retrieve all and filter q: &query{ filters: []silenceFilter{ func(sil *pb.Silence, _ *Silences, _ time.Time) (bool, error) { return sil.Id == "1" || sil.Id == "2", nil }, }, }, exp: []*pb.Silence{ {Id: "1"}, {Id: "2"}, }, }, { // Retrieve by IDs and filter q: &query{ ids: []string{"2", "5"}, filters: []silenceFilter{ func(sil *pb.Silence, _ *Silences, _ time.Time) (bool, error) { return sil.Id == "1" || sil.Id == "2", nil }, }, }, exp: []*pb.Silence{ {Id: "2"}, }, }, } for _, c := range cases { // Run default query of retrieving all silences. res, err := s.query(c.q, time.Time{}) require.NoError(t, err, "unexpected error on querying") // Currently there are no sorting guarantees in the querying API. sort.Sort(silencesByID(c.exp)) sort.Sort(silencesByID(res)) require.Equal(t, c.exp, res, "unexpected silences in result") } } type silencesByID []*pb.Silence func (s silencesByID) Len() int { return len(s) } func (s silencesByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s silencesByID) Less(i, j int) bool { return s[i].Id < s[j].Id } func TestSilenceSetTimeRange(t *testing.T) { now := utcNow() cases := []struct { sil *pb.Silence start, end time.Time err string }{ // Bad arguments. { sil: &pb.Silence{}, start: now, end: now.Add(-time.Minute), err: "end time must not be before start time", }, // Expired silence. { sil: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(-time.Second), }, start: now, end: now, err: "expired silence must not be modified", }, // Pending silences. { sil: &pb.Silence{ StartsAt: now.Add(time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, start: now.Add(-time.Minute), end: now.Add(time.Hour), err: "start time cannot be set into the past", }, { sil: &pb.Silence{ StartsAt: now.Add(time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, start: now.Add(time.Minute), end: now.Add(time.Minute), }, { sil: &pb.Silence{ StartsAt: now.Add(time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, start: now, // set to exactly start now. end: now.Add(2 * time.Hour), }, // Active silences. { sil: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, start: now.Add(-time.Minute), end: now.Add(2 * time.Hour), err: "start time of active silence cannot be modified", }, { sil: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, start: now.Add(-time.Hour), end: now.Add(-time.Second), err: "end time cannot be set into the past", }, { sil: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, start: now.Add(-time.Hour), end: now, }, { sil: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, start: now.Add(-time.Hour), end: now.Add(3 * time.Hour), }, } for _, c := range cases { origSilence := cloneSilence(c.sil) sil, err := silenceSetTimeRange(c.sil, now, c.start, c.end) if err == nil { if c.err != "" { t.Errorf("expected error containing %q but got none", c.err) } // The original silence must not have been modified. require.Equal(t, origSilence, c.sil, "original silence illegally modified") require.Equal(t, sil.StartsAt, c.start) require.Equal(t, sil.EndsAt, c.end) require.Equal(t, sil.UpdatedAt, now) continue } if err != nil && c.err == "" { t.Errorf("unexpected error %q", err) continue } if !strings.Contains(err.Error(), c.err) { t.Errorf("expected error to contain %q but got %q", c.err, err) } } } func TestValidateMatcher(t *testing.T) { cases := []struct { m *pb.Matcher err string }{ { m: &pb.Matcher{ Name: "a", Pattern: "b", Type: pb.Matcher_EQUAL, }, err: "", }, { m: &pb.Matcher{ Name: "00", Pattern: "a", Type: pb.Matcher_EQUAL, }, err: "invalid label name", }, { m: &pb.Matcher{ Name: "a", Pattern: "((", Type: pb.Matcher_REGEXP, }, err: "invalid regular expression", }, { m: &pb.Matcher{ Name: "a", Pattern: "\xff", Type: pb.Matcher_EQUAL, }, err: "invalid label value", }, { m: &pb.Matcher{ Name: "a", Pattern: "b", Type: 333, }, err: "unknown matcher type", }, } for _, c := range cases { err := validateMatcher(c.m) if err == nil { if c.err != "" { t.Errorf("expected error containing %q but got none", c.err) } continue } if err != nil && c.err == "" { t.Errorf("unexpected error %q", err) continue } if !strings.Contains(err.Error(), c.err) { t.Errorf("expected error to contain %q but got %q", c.err, err) } } } func TestValidateSilence(t *testing.T) { var ( now = utcNow() zeroTimestamp = time.Time{} validTimestamp = now ) cases := []struct { s *pb.Silence err string }{ { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: validTimestamp, EndsAt: validTimestamp, UpdatedAt: validTimestamp, }, err: "", }, { s: &pb.Silence{ Id: "", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: validTimestamp, EndsAt: validTimestamp, UpdatedAt: validTimestamp, }, err: "ID missing", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{}, StartsAt: validTimestamp, EndsAt: validTimestamp, UpdatedAt: validTimestamp, }, err: "at least one matcher required", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, &pb.Matcher{Name: "00", Pattern: "b"}, }, StartsAt: validTimestamp, EndsAt: validTimestamp, UpdatedAt: validTimestamp, }, err: "invalid label matcher", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: now, EndsAt: now.Add(-time.Second), UpdatedAt: validTimestamp, }, err: "end time must not be before start time", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: zeroTimestamp, EndsAt: validTimestamp, UpdatedAt: validTimestamp, }, err: "invalid zero start timestamp", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: validTimestamp, EndsAt: zeroTimestamp, UpdatedAt: validTimestamp, }, err: "invalid zero end timestamp", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: validTimestamp, EndsAt: validTimestamp, UpdatedAt: zeroTimestamp, }, err: "invalid zero update timestamp", }, } for _, c := range cases { err := validateSilence(c.s) if err == nil { if c.err != "" { t.Errorf("expected error containing %q but got none", c.err) } continue } if err != nil && c.err == "" { t.Errorf("unexpected error %q", err) continue } if !strings.Contains(err.Error(), c.err) { t.Errorf("expected error to contain %q but got %q", c.err, err) } } } func TestGossipDataMerge(t *testing.T) { now := utcNow() // We only care about key names and timestamps for the // merging logic. newSilence := func(ts time.Time) *pb.MeshSilence { return &pb.MeshSilence{ Silence: &pb.Silence{UpdatedAt: ts}, } } cases := []struct { a, b gossipData final, delta gossipData }{ { a: gossipData{ "a1": newSilence(now), "a2": newSilence(now), "a3": newSilence(now), }, b: gossipData{ "b1": newSilence(now), // new key, should be added "a2": newSilence(now.Add(-time.Minute)), // older timestamp, should be dropped "a3": newSilence(now.Add(time.Minute)), // newer timestamp, should overwrite }, final: gossipData{ "a1": newSilence(now), "a2": newSilence(now), "a3": newSilence(now.Add(time.Minute)), "b1": newSilence(now), }, delta: gossipData{ "b1": newSilence(now), "a3": newSilence(now.Add(time.Minute)), }, }, } for _, c := range cases { ca, cb := c.a.clone(), c.b.clone() res := ca.Merge(cb) require.Equal(t, c.final, res, "Merge result should match expectation") require.Equal(t, c.final, ca, "Merge should apply changes to original state") require.Equal(t, c.b, cb, "Merged state should remain unmodified") ca, cb = c.a.clone(), c.b.clone() delta := ca.mergeDelta(cb) require.Equal(t, c.delta, delta, "Merge delta should match expectation") require.Equal(t, c.final, ca, "Merge should apply changes to original state") require.Equal(t, c.b, cb, "Merged state should remain unmodified") } } func TestGossipDataCoding(t *testing.T) { // Check whether encoding and decoding the data is symmetric. now := utcNow() cases := []struct { entries []*pb.MeshSilence }{ { entries: []*pb.MeshSilence{ { Silence: &pb.Silence{ Id: "3be80475-e219-4ee7-b6fc-4b65114e362f", Matchers: []*pb.Matcher{ {Name: "label1", Pattern: "val1", Type: pb.Matcher_EQUAL}, {Name: "label2", Pattern: "val.+", Type: pb.Matcher_REGEXP}, }, StartsAt: now, EndsAt: now, UpdatedAt: now, }, ExpiresAt: now, }, { Silence: &pb.Silence{ Id: "4b1e760d-182c-4980-b873-c1a6827c9817", Matchers: []*pb.Matcher{ {Name: "label1", Pattern: "val1", Type: pb.Matcher_EQUAL}, }, StartsAt: now.Add(time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now, }, ExpiresAt: now.Add(24 * time.Hour), }, }, }, } for _, c := range cases { // Create gossip data from input. in := gossipData{} for _, e := range c.entries { in[e.Silence.Id] = e } msg := in.Encode() require.Equal(t, 1, len(msg), "expected single message for input") out, err := decodeGossipData(msg[0]) require.NoError(t, err, "decoding message failed") require.Equal(t, in, out, "decoded data doesn't match encoded data") } } prometheus-alertmanager-0.6.2+ds/silence/silencepb/000077500000000000000000000000001314512360300223405ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/silence/silencepb/silence.pb.go000066400000000000000000000721761314512360300247260ustar00rootroot00000000000000// Code generated by protoc-gen-gogo. // source: silence.proto // DO NOT EDIT! /* Package silencepb is a generated protocol buffer package. It is generated from these files: silence.proto It has these top-level messages: Matcher Comment Silence MeshSilence */ package silencepb import proto "github.com/gogo/protobuf/proto" import fmt "fmt" import math "math" import time "time" import github_com_gogo_protobuf_types "github.com/gogo/protobuf/types" import io "io" // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf var _ = time.Kitchen // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package // Type specifies how the given name and pattern are matched // against a label set. type Matcher_Type int32 const ( Matcher_EQUAL Matcher_Type = 0 Matcher_REGEXP Matcher_Type = 1 ) var Matcher_Type_name = map[int32]string{ 0: "EQUAL", 1: "REGEXP", } var Matcher_Type_value = map[string]int32{ "EQUAL": 0, "REGEXP": 1, } func (x Matcher_Type) String() string { return proto.EnumName(Matcher_Type_name, int32(x)) } func (Matcher_Type) EnumDescriptor() ([]byte, []int) { return fileDescriptorSilence, []int{0, 0} } // Matcher specifies a rule, which can match or set of labels or not. type Matcher struct { Type Matcher_Type `protobuf:"varint,1,opt,name=type,proto3,enum=silencepb.Matcher_Type" json:"type,omitempty"` // The label name in a label set to against which the matcher // checks the pattern. Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // The pattern being checked according to the matcher's type. Pattern string `protobuf:"bytes,3,opt,name=pattern,proto3" json:"pattern,omitempty"` } func (m *Matcher) Reset() { *m = Matcher{} } func (m *Matcher) String() string { return proto.CompactTextString(m) } func (*Matcher) ProtoMessage() {} func (*Matcher) Descriptor() ([]byte, []int) { return fileDescriptorSilence, []int{0} } // A comment can be attached to a silence. type Comment struct { Author string `protobuf:"bytes,1,opt,name=author,proto3" json:"author,omitempty"` Comment string `protobuf:"bytes,2,opt,name=comment,proto3" json:"comment,omitempty"` Timestamp time.Time `protobuf:"bytes,3,opt,name=timestamp,stdtime" json:"timestamp"` } func (m *Comment) Reset() { *m = Comment{} } func (m *Comment) String() string { return proto.CompactTextString(m) } func (*Comment) ProtoMessage() {} func (*Comment) Descriptor() ([]byte, []int) { return fileDescriptorSilence, []int{1} } // Silence specifies an object that ignores alerts based // on a set of matchers during a given time frame. type Silence struct { // A globally unique identifier. Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // A set of matchers all of which have to be true for a silence // to affect a given label set. Matchers []*Matcher `protobuf:"bytes,2,rep,name=matchers" json:"matchers,omitempty"` // The time range during which the silence is active. StartsAt time.Time `protobuf:"bytes,3,opt,name=starts_at,json=startsAt,stdtime" json:"starts_at"` EndsAt time.Time `protobuf:"bytes,4,opt,name=ends_at,json=endsAt,stdtime" json:"ends_at"` // The last motification made to the silence. UpdatedAt time.Time `protobuf:"bytes,5,opt,name=updated_at,json=updatedAt,stdtime" json:"updated_at"` // A set of comments made on the silence. Comments []*Comment `protobuf:"bytes,7,rep,name=comments" json:"comments,omitempty"` } func (m *Silence) Reset() { *m = Silence{} } func (m *Silence) String() string { return proto.CompactTextString(m) } func (*Silence) ProtoMessage() {} func (*Silence) Descriptor() ([]byte, []int) { return fileDescriptorSilence, []int{2} } // MeshSilence wraps a regular silence with an expiration timestamp // after which the silence may be garbage collected. type MeshSilence struct { Silence *Silence `protobuf:"bytes,1,opt,name=silence" json:"silence,omitempty"` ExpiresAt time.Time `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt,stdtime" json:"expires_at"` } func (m *MeshSilence) Reset() { *m = MeshSilence{} } func (m *MeshSilence) String() string { return proto.CompactTextString(m) } func (*MeshSilence) ProtoMessage() {} func (*MeshSilence) Descriptor() ([]byte, []int) { return fileDescriptorSilence, []int{3} } func init() { proto.RegisterType((*Matcher)(nil), "silencepb.Matcher") proto.RegisterType((*Comment)(nil), "silencepb.Comment") proto.RegisterType((*Silence)(nil), "silencepb.Silence") proto.RegisterType((*MeshSilence)(nil), "silencepb.MeshSilence") proto.RegisterEnum("silencepb.Matcher_Type", Matcher_Type_name, Matcher_Type_value) } func (m *Matcher) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Matcher) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if m.Type != 0 { dAtA[i] = 0x8 i++ i = encodeVarintSilence(dAtA, i, uint64(m.Type)) } if len(m.Name) > 0 { dAtA[i] = 0x12 i++ i = encodeVarintSilence(dAtA, i, uint64(len(m.Name))) i += copy(dAtA[i:], m.Name) } if len(m.Pattern) > 0 { dAtA[i] = 0x1a i++ i = encodeVarintSilence(dAtA, i, uint64(len(m.Pattern))) i += copy(dAtA[i:], m.Pattern) } return i, nil } func (m *Comment) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Comment) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if len(m.Author) > 0 { dAtA[i] = 0xa i++ i = encodeVarintSilence(dAtA, i, uint64(len(m.Author))) i += copy(dAtA[i:], m.Author) } if len(m.Comment) > 0 { dAtA[i] = 0x12 i++ i = encodeVarintSilence(dAtA, i, uint64(len(m.Comment))) i += copy(dAtA[i:], m.Comment) } dAtA[i] = 0x1a i++ i = encodeVarintSilence(dAtA, i, uint64(github_com_gogo_protobuf_types.SizeOfStdTime(m.Timestamp))) n1, err := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.Timestamp, dAtA[i:]) if err != nil { return 0, err } i += n1 return i, nil } func (m *Silence) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *Silence) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if len(m.Id) > 0 { dAtA[i] = 0xa i++ i = encodeVarintSilence(dAtA, i, uint64(len(m.Id))) i += copy(dAtA[i:], m.Id) } if len(m.Matchers) > 0 { for _, msg := range m.Matchers { dAtA[i] = 0x12 i++ i = encodeVarintSilence(dAtA, i, uint64(msg.Size())) n, err := msg.MarshalTo(dAtA[i:]) if err != nil { return 0, err } i += n } } dAtA[i] = 0x1a i++ i = encodeVarintSilence(dAtA, i, uint64(github_com_gogo_protobuf_types.SizeOfStdTime(m.StartsAt))) n2, err := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.StartsAt, dAtA[i:]) if err != nil { return 0, err } i += n2 dAtA[i] = 0x22 i++ i = encodeVarintSilence(dAtA, i, uint64(github_com_gogo_protobuf_types.SizeOfStdTime(m.EndsAt))) n3, err := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.EndsAt, dAtA[i:]) if err != nil { return 0, err } i += n3 dAtA[i] = 0x2a i++ i = encodeVarintSilence(dAtA, i, uint64(github_com_gogo_protobuf_types.SizeOfStdTime(m.UpdatedAt))) n4, err := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.UpdatedAt, dAtA[i:]) if err != nil { return 0, err } i += n4 if len(m.Comments) > 0 { for _, msg := range m.Comments { dAtA[i] = 0x3a i++ i = encodeVarintSilence(dAtA, i, uint64(msg.Size())) n, err := msg.MarshalTo(dAtA[i:]) if err != nil { return 0, err } i += n } } return i, nil } func (m *MeshSilence) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) if err != nil { return nil, err } return dAtA[:n], nil } func (m *MeshSilence) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l if m.Silence != nil { dAtA[i] = 0xa i++ i = encodeVarintSilence(dAtA, i, uint64(m.Silence.Size())) n5, err := m.Silence.MarshalTo(dAtA[i:]) if err != nil { return 0, err } i += n5 } dAtA[i] = 0x12 i++ i = encodeVarintSilence(dAtA, i, uint64(github_com_gogo_protobuf_types.SizeOfStdTime(m.ExpiresAt))) n6, err := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.ExpiresAt, dAtA[i:]) if err != nil { return 0, err } i += n6 return i, nil } func encodeFixed64Silence(dAtA []byte, offset int, v uint64) int { dAtA[offset] = uint8(v) dAtA[offset+1] = uint8(v >> 8) dAtA[offset+2] = uint8(v >> 16) dAtA[offset+3] = uint8(v >> 24) dAtA[offset+4] = uint8(v >> 32) dAtA[offset+5] = uint8(v >> 40) dAtA[offset+6] = uint8(v >> 48) dAtA[offset+7] = uint8(v >> 56) return offset + 8 } func encodeFixed32Silence(dAtA []byte, offset int, v uint32) int { dAtA[offset] = uint8(v) dAtA[offset+1] = uint8(v >> 8) dAtA[offset+2] = uint8(v >> 16) dAtA[offset+3] = uint8(v >> 24) return offset + 4 } func encodeVarintSilence(dAtA []byte, offset int, v uint64) int { for v >= 1<<7 { dAtA[offset] = uint8(v&0x7f | 0x80) v >>= 7 offset++ } dAtA[offset] = uint8(v) return offset + 1 } func (m *Matcher) Size() (n int) { var l int _ = l if m.Type != 0 { n += 1 + sovSilence(uint64(m.Type)) } l = len(m.Name) if l > 0 { n += 1 + l + sovSilence(uint64(l)) } l = len(m.Pattern) if l > 0 { n += 1 + l + sovSilence(uint64(l)) } return n } func (m *Comment) Size() (n int) { var l int _ = l l = len(m.Author) if l > 0 { n += 1 + l + sovSilence(uint64(l)) } l = len(m.Comment) if l > 0 { n += 1 + l + sovSilence(uint64(l)) } l = github_com_gogo_protobuf_types.SizeOfStdTime(m.Timestamp) n += 1 + l + sovSilence(uint64(l)) return n } func (m *Silence) Size() (n int) { var l int _ = l l = len(m.Id) if l > 0 { n += 1 + l + sovSilence(uint64(l)) } if len(m.Matchers) > 0 { for _, e := range m.Matchers { l = e.Size() n += 1 + l + sovSilence(uint64(l)) } } l = github_com_gogo_protobuf_types.SizeOfStdTime(m.StartsAt) n += 1 + l + sovSilence(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdTime(m.EndsAt) n += 1 + l + sovSilence(uint64(l)) l = github_com_gogo_protobuf_types.SizeOfStdTime(m.UpdatedAt) n += 1 + l + sovSilence(uint64(l)) if len(m.Comments) > 0 { for _, e := range m.Comments { l = e.Size() n += 1 + l + sovSilence(uint64(l)) } } return n } func (m *MeshSilence) Size() (n int) { var l int _ = l if m.Silence != nil { l = m.Silence.Size() n += 1 + l + sovSilence(uint64(l)) } l = github_com_gogo_protobuf_types.SizeOfStdTime(m.ExpiresAt) n += 1 + l + sovSilence(uint64(l)) return n } func sovSilence(x uint64) (n int) { for { n++ x >>= 7 if x == 0 { break } } return n } func sozSilence(x uint64) (n int) { return sovSilence(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } func (m *Matcher) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Matcher: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Matcher: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field Type", wireType) } m.Type = 0 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ m.Type |= (Matcher_Type(b) & 0x7F) << shift if b < 0x80 { break } } case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + intStringLen if postIndex > l { return io.ErrUnexpectedEOF } m.Name = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Pattern", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + intStringLen if postIndex > l { return io.ErrUnexpectedEOF } m.Pattern = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipSilence(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthSilence } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *Comment) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Comment: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Comment: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Author", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + intStringLen if postIndex > l { return io.ErrUnexpectedEOF } m.Author = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Comment", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + intStringLen if postIndex > l { return io.ErrUnexpectedEOF } m.Comment = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.Timestamp, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipSilence(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthSilence } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *Silence) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: Silence: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: Silence: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ stringLen |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } intStringLen := int(stringLen) if intStringLen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + intStringLen if postIndex > l { return io.ErrUnexpectedEOF } m.Id = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Matchers", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } m.Matchers = append(m.Matchers, &Matcher{}) if err := m.Matchers[len(m.Matchers)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field StartsAt", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.StartsAt, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 4: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field EndsAt", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.EndsAt, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 5: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field UpdatedAt", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.UpdatedAt, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 7: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Comments", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } m.Comments = append(m.Comments, &Comment{}) if err := m.Comments[len(m.Comments)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipSilence(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthSilence } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func (m *MeshSilence) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { preIndex := iNdEx var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { return fmt.Errorf("proto: MeshSilence: wiretype end group for non-group") } if fieldNum <= 0 { return fmt.Errorf("proto: MeshSilence: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Silence", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if m.Silence == nil { m.Silence = &Silence{} } if err := m.Silence.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex case 2: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field ExpiresAt", wireType) } var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowSilence } if iNdEx >= l { return io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } if msglen < 0 { return ErrInvalidLengthSilence } postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.ExpiresAt, dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipSilence(dAtA[iNdEx:]) if err != nil { return err } if skippy < 0 { return ErrInvalidLengthSilence } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } iNdEx += skippy } } if iNdEx > l { return io.ErrUnexpectedEOF } return nil } func skipSilence(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 for iNdEx < l { var wire uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowSilence } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ wire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } wireType := int(wire & 0x7) switch wireType { case 0: for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowSilence } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } iNdEx++ if dAtA[iNdEx-1] < 0x80 { break } } return iNdEx, nil case 1: iNdEx += 8 return iNdEx, nil case 2: var length int for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowSilence } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ length |= (int(b) & 0x7F) << shift if b < 0x80 { break } } iNdEx += length if length < 0 { return 0, ErrInvalidLengthSilence } return iNdEx, nil case 3: for { var innerWire uint64 var start int = iNdEx for shift := uint(0); ; shift += 7 { if shift >= 64 { return 0, ErrIntOverflowSilence } if iNdEx >= l { return 0, io.ErrUnexpectedEOF } b := dAtA[iNdEx] iNdEx++ innerWire |= (uint64(b) & 0x7F) << shift if b < 0x80 { break } } innerWireType := int(innerWire & 0x7) if innerWireType == 4 { break } next, err := skipSilence(dAtA[start:]) if err != nil { return 0, err } iNdEx = start + next } return iNdEx, nil case 4: return iNdEx, nil case 5: iNdEx += 4 return iNdEx, nil default: return 0, fmt.Errorf("proto: illegal wireType %d", wireType) } } panic("unreachable") } var ( ErrInvalidLengthSilence = fmt.Errorf("proto: negative length found during unmarshaling") ErrIntOverflowSilence = fmt.Errorf("proto: integer overflow") ) func init() { proto.RegisterFile("silence.proto", fileDescriptorSilence) } var fileDescriptorSilence = []byte{ // 421 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x91, 0xcf, 0xaa, 0xd3, 0x40, 0x14, 0xc6, 0x3b, 0xb9, 0xbd, 0x49, 0x73, 0x8a, 0x97, 0x32, 0x88, 0x86, 0x82, 0x69, 0xc9, 0xaa, 0xa0, 0x4c, 0xa1, 0xae, 0x5d, 0xa4, 0x97, 0xe2, 0xc6, 0x0b, 0x3a, 0x5e, 0xc1, 0x9d, 0x4c, 0x9b, 0x31, 0x0d, 0xdc, 0x64, 0x86, 0xe4, 0x14, 0xbc, 0x2b, 0x05, 0x5f, 0xc0, 0x47, 0x72, 0xd9, 0xa5, 0x4f, 0xe0, 0x9f, 0x3e, 0x89, 0x64, 0x32, 0x89, 0x48, 0x57, 0xd9, 0x9d, 0x33, 0xf9, 0x7d, 0xe7, 0x9c, 0xef, 0x0b, 0x3c, 0xa8, 0xb2, 0x3b, 0x59, 0xec, 0x24, 0xd3, 0xa5, 0x42, 0x45, 0x7d, 0xdb, 0xea, 0xed, 0x74, 0x96, 0x2a, 0x95, 0xde, 0xc9, 0xa5, 0xf9, 0xb0, 0x3d, 0x7c, 0x5c, 0x62, 0x96, 0xcb, 0x0a, 0x45, 0xae, 0x1b, 0x76, 0xfa, 0x30, 0x55, 0xa9, 0x32, 0xe5, 0xb2, 0xae, 0x9a, 0xd7, 0xe8, 0x2b, 0x01, 0xef, 0x46, 0xe0, 0x6e, 0x2f, 0x4b, 0xfa, 0x14, 0x86, 0x78, 0xaf, 0x65, 0x40, 0xe6, 0x64, 0x71, 0xb5, 0x7a, 0xcc, 0xba, 0xe1, 0xcc, 0x12, 0xec, 0xf6, 0x5e, 0x4b, 0x6e, 0x20, 0x4a, 0x61, 0x58, 0x88, 0x5c, 0x06, 0xce, 0x9c, 0x2c, 0x7c, 0x6e, 0x6a, 0x1a, 0x80, 0xa7, 0x05, 0xa2, 0x2c, 0x8b, 0xe0, 0xc2, 0x3c, 0xb7, 0x6d, 0xf4, 0x04, 0x86, 0xb5, 0x96, 0xfa, 0x70, 0xb9, 0x79, 0xf3, 0x2e, 0x7e, 0x35, 0x19, 0x50, 0x00, 0x97, 0x6f, 0x5e, 0x6e, 0xde, 0xbf, 0x9e, 0x90, 0xe8, 0x33, 0x78, 0xd7, 0x2a, 0xcf, 0x65, 0x81, 0xf4, 0x11, 0xb8, 0xe2, 0x80, 0x7b, 0x55, 0x9a, 0x33, 0x7c, 0x6e, 0xbb, 0x7a, 0xf6, 0xae, 0x41, 0xec, 0xca, 0xb6, 0xa5, 0x6b, 0xf0, 0x3b, 0xaf, 0x66, 0xef, 0x78, 0x35, 0x65, 0x4d, 0x1a, 0xac, 0x4d, 0x83, 0xdd, 0xb6, 0xc4, 0x7a, 0x74, 0xfc, 0x39, 0x1b, 0x7c, 0xfb, 0x35, 0x23, 0xfc, 0x9f, 0x2c, 0xfa, 0xee, 0x80, 0xf7, 0xb6, 0xb1, 0x4b, 0xaf, 0xc0, 0xc9, 0x12, 0xbb, 0xdd, 0xc9, 0x12, 0xca, 0x60, 0x94, 0x37, 0xfe, 0xab, 0xc0, 0x99, 0x5f, 0x2c, 0xc6, 0x2b, 0x7a, 0x1e, 0x0d, 0xef, 0x18, 0x1a, 0x83, 0x5f, 0xa1, 0x28, 0xb1, 0xfa, 0x20, 0xb0, 0xd7, 0x3d, 0xa3, 0x46, 0x16, 0x23, 0x7d, 0x01, 0x9e, 0x2c, 0x12, 0x33, 0x60, 0xd8, 0x63, 0x80, 0x5b, 0x8b, 0x62, 0xa4, 0xd7, 0x00, 0x07, 0x9d, 0x08, 0x94, 0x49, 0x3d, 0xe1, 0xb2, 0x4f, 0x24, 0x56, 0x17, 0x63, 0x6d, 0xdb, 0x26, 0x5c, 0x05, 0xde, 0x99, 0x6d, 0xfb, 0xbb, 0x78, 0xc7, 0x44, 0x5f, 0x08, 0x8c, 0x6f, 0x64, 0xb5, 0x6f, 0x63, 0x7c, 0x06, 0x9e, 0xc5, 0x4d, 0x96, 0xff, 0xcb, 0x2d, 0xc4, 0x5b, 0xa4, 0x3e, 0x59, 0x7e, 0xd2, 0x59, 0x29, 0x8d, 0x69, 0xa7, 0xcf, 0xc9, 0x56, 0x17, 0xe3, 0x7a, 0x72, 0xfc, 0x13, 0x0e, 0x8e, 0xa7, 0x90, 0xfc, 0x38, 0x85, 0xe4, 0xf7, 0x29, 0x24, 0x5b, 0xd7, 0x48, 0x9f, 0xff, 0x0d, 0x00, 0x00, 0xff, 0xff, 0x4e, 0x2e, 0x01, 0x5f, 0x38, 0x03, 0x00, 0x00, } prometheus-alertmanager-0.6.2+ds/silence/silencepb/silence.proto000066400000000000000000000037311314512360300250530ustar00rootroot00000000000000syntax = "proto3"; package silencepb; import "google/protobuf/timestamp.proto"; import "gogoproto/gogo.proto"; option (gogoproto.marshaler_all) = true; option (gogoproto.sizer_all) = true; option (gogoproto.unmarshaler_all) = true; option (gogoproto.goproto_getters_all) = false; // Matcher specifies a rule, which can match or set of labels or not. message Matcher { // Type specifies how the given name and pattern are matched // against a label set. enum Type { EQUAL = 0; REGEXP = 1; }; Type type = 1; // The label name in a label set to against which the matcher // checks the pattern. string name = 2; // The pattern being checked according to the matcher's type. string pattern = 3; } // A comment can be attached to a silence. message Comment { string author = 1; string comment = 2; google.protobuf.Timestamp timestamp = 3 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; } // Silence specifies an object that ignores alerts based // on a set of matchers during a given time frame. message Silence { // A globally unique identifier. string id = 1; // A set of matchers all of which have to be true for a silence // to affect a given label set. repeated Matcher matchers = 2; // The time range during which the silence is active. google.protobuf.Timestamp starts_at = 3 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; google.protobuf.Timestamp ends_at = 4 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; // The last motification made to the silence. google.protobuf.Timestamp updated_at = 5 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; // A set of comments made on the silence. repeated Comment comments = 7; } // MeshSilence wraps a regular silence with an expiration timestamp // after which the silence may be garbage collected. message MeshSilence { Silence silence = 1; google.protobuf.Timestamp expires_at = 2 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false]; }prometheus-alertmanager-0.6.2+ds/template/000077500000000000000000000000001314512360300205655ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/template/default.tmpl000066400000000000000000000376071314512360300231240ustar00rootroot00000000000000{{ define "__alertmanager" }}AlertManager{{ end }} {{ define "__alertmanagerURL" }}{{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }}{{ end }} {{ 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 }} {{ define "__description" }}{{ end }} {{ define "__text_alert_list" }}{{ range . }}Labels: {{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }} {{ end }}Annotations: {{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }} {{ end }}Source: {{ .GeneratorURL }} {{ end }}{{ end }} {{ define "slack.default.title" }}{{ template "__subject" . }}{{ end }} {{ define "slack.default.username" }}{{ template "__alertmanager" . }}{{ end }} {{ define "slack.default.fallback" }}{{ template "slack.default.title" . }} | {{ template "slack.default.titlelink" . }}{{ end }} {{ define "slack.default.pretext" }}{{ end }} {{ define "slack.default.titlelink" }}{{ template "__alertmanagerURL" . }}{{ end }} {{ define "slack.default.iconemoji" }}{{ end }} {{ define "slack.default.iconurl" }}{{ end }} {{ define "slack.default.text" }}{{ end }} {{ define "hipchat.default.from" }}{{ template "__alertmanager" . }}{{ end }} {{ define "hipchat.default.message" }}{{ template "__subject" . }}{{ end }} {{ define "pagerduty.default.description" }}{{ template "__subject" . }}{{ end }} {{ define "pagerduty.default.client" }}{{ template "__alertmanager" . }}{{ end }} {{ define "pagerduty.default.clientURL" }}{{ template "__alertmanagerURL" . }}{{ end }} {{ define "pagerduty.default.instances" }}{{ template "__text_alert_list" . }}{{ end }} {{ define "opsgenie.default.message" }}{{ template "__subject" . }}{{ end }} {{ define "opsgenie.default.description" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }} {{ if gt (len .Alerts.Firing) 0 -}} Alerts Firing: {{ template "__text_alert_list" .Alerts.Firing }} {{- end }} {{ if gt (len .Alerts.Resolved) 0 -}} Alerts Resolved: {{ template "__text_alert_list" .Alerts.Resolved }} {{- end }} {{- end }} {{ define "opsgenie.default.source" }}{{ template "__alertmanagerURL" . }}{{ end }} {{ define "victorops.default.state_message" }}{{ template "__subject" . }} | {{ template "__alertmanagerURL" . }}{{ end }} {{ define "victorops.default.monitoring_tool" }}{{ template "__alertmanager" . }}{{ end }} {{ define "email.default.subject" }}{{ template "__subject" . }}{{ end }} {{ define "email.default.html" }} {{ template "__subject" . }}
{{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }} {{ .Name }}={{ .Value }} {{ end }}
{{ if gt (len .Alerts.Firing) 0 }} {{ end }} {{ range .Alerts.Firing }} {{ end }} {{ if gt (len .Alerts.Resolved) 0 }} {{ if gt (len .Alerts.Firing) 0 }} {{ end }} {{ end }} {{ range .Alerts.Resolved }} {{ end }}
View in {{ template "__alertmanager" . }}
[{{ .Alerts.Firing | len }}] Firing
Labels
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} {{ if gt (len .Annotations) 0 }}Annotations
{{ end }} {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} Source



[{{ .Alerts.Resolved | len }}] Resolved
Labels
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} {{ if gt (len .Annotations) 0 }}Annotations
{{ end }} {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} Source
{{ end }} {{ define "pushover.default.title" }}{{ template "__subject" . }}{{ end }} {{ define "pushover.default.message" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }} {{ if gt (len .Alerts.Firing) 0 }} Alerts Firing: {{ template "__text_alert_list" .Alerts.Firing }} {{ end }} {{ if gt (len .Alerts.Resolved) 0 }} Alerts Resolved: {{ template "__text_alert_list" .Alerts.Resolved }} {{ end }} {{ end }} {{ define "pushover.default.url" }}{{ template "__alertmanagerURL" . }}{{ end }} prometheus-alertmanager-0.6.2+ds/template/email.html000066400000000000000000000234001314512360300225410ustar00rootroot00000000000000 {{ template "__subject" . }}
{{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }} {{ .Name }}={{ .Value }} {{ end }}
{{ if gt (len .Alerts.Firing) 0 }} {{ end }} {{ range .Alerts.Firing }} {{ end }} {{ if gt (len .Alerts.Resolved) 0 }} {{ if gt (len .Alerts.Firing) 0 }} {{ end }} {{ end }} {{ range .Alerts.Resolved }} {{ end }}
View in {{ template "__alertmanager" . }}
[{{ .Alerts.Firing | len }}] Firing
Labels
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} {{ if gt (len .Annotations) 0 }}Annotations
{{ end }} {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} Source



[{{ .Alerts.Resolved | len }}] Resolved
Labels
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} {{ if gt (len .Annotations) 0 }}Annotations
{{ end }} {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} Source
prometheus-alertmanager-0.6.2+ds/template/internal/000077500000000000000000000000001314512360300224015ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/template/internal/deftmpl/000077500000000000000000000000001314512360300240345ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/template/internal/deftmpl/bindata.go000066400000000000000000000377611314512360300260030ustar00rootroot00000000000000// Code generated by go-bindata. // sources: // template/default.tmpl // DO NOT EDIT! package deftmpl import ( "bytes" "compress/gzip" "fmt" "io" "io/ioutil" "os" "path/filepath" "strings" "time" ) func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { return nil, fmt.Errorf("Read %q: %v", name, err) } var buf bytes.Buffer _, err = io.Copy(&buf, gz) clErr := gz.Close() if err != nil { return nil, fmt.Errorf("Read %q: %v", name, err) } if clErr != nil { return nil, err } return buf.Bytes(), nil } type asset struct { bytes []byte info os.FileInfo } type bindataFileInfo struct { name string size int64 mode os.FileMode modTime time.Time } func (fi bindataFileInfo) Name() string { return fi.name } func (fi bindataFileInfo) Size() int64 { return fi.size } func (fi bindataFileInfo) Mode() os.FileMode { return fi.mode } func (fi bindataFileInfo) ModTime() time.Time { return fi.modTime } func (fi bindataFileInfo) IsDir() bool { return false } func (fi bindataFileInfo) Sys() interface{} { return nil } var _templateDefaultTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x3b\x6b\x6f\xdb\x36\xbb\xdf\xf5\x2b\x9e\x69\x38\x58\x03\x58\x96\x93\x6e\xc5\xe2\xd8\x39\x70\x1d\xa5\x11\x8e\x23\x07\xb2\xd2\xae\x18\x86\x82\x96\x68\x9b\xad\x44\x6a\x24\x95\xc4\xcb\xfc\xdf\x0f\x48\xc9\x17\xd9\x72\xea\x14\x5d\xe2\xf7\x5d\x12\xb4\x91\x28\x3e\xf7\x2b\x45\xea\xfe\x1e\x22\x3c\x22\x14\x83\xf9\xe9\x13\x8a\x31\x97\x09\xa2\x68\x8c\xb9\x09\xb3\x59\x47\xdd\x5f\xe6\xf7\xf7\xf7\x80\x69\x04\xb3\x99\xb1\x15\xe4\xda\xef\x29\xa8\xfb\x7b\xa8\x3b\x77\x12\x73\x8a\xe2\x6b\xbf\x07\xb3\x99\xfd\xa3\xad\xe7\x89\xff\xe5\x38\xc4\xe4\x06\xf3\xb6\x9a\xe4\x17\x37\x39\x4c\x81\xbd\x8c\x5e\x64\xc3\xcf\x38\x94\x0a\xed\xef\x0a\x64\x20\x91\xcc\x04\xfc\x0d\x92\x5d\xa7\xe9\x1c\x94\x8c\x00\xff\xb9\x78\x68\x8e\x08\x27\x74\xac\x60\x9a\x0a\x46\x4b\x21\xea\xe7\x7a\x14\xfe\x86\x18\xd3\x55\x8a\x7f\x80\x9a\xf4\x8e\xb3\x2c\xed\xa1\x21\x8e\x45\x7d\xc0\xb8\xc4\xd1\x15\x22\x5c\xd4\xdf\xa3\x38\xc3\x8a\xe0\x67\x46\x28\x98\xa0\xb0\x42\x4e\x72\x2c\xe1\x95\xc2\x55\xef\xb2\x24\x61\x34\x07\x3e\x28\xc6\x56\xf0\x1d\xc0\x6c\xf6\xea\xfe\x1e\x6e\x89\x9c\x94\x27\xd7\x7d\x9c\xb0\x1b\x5c\xa6\xee\xa1\x04\x8b\x42\x8d\x55\xd4\x17\x8c\x1f\x2c\xae\xb6\xd8\x26\xc2\x22\xe4\x24\x95\x84\x51\xf3\x01\x1d\x4b\x7c\x27\x73\x3b\x7e\x8a\x89\x90\xc5\x54\x8e\xe8\x18\x43\x1d\x66\xb3\x9c\xaf\xa6\xb1\x1c\xdc\xd4\x93\xd2\x8a\xa5\x15\xa9\xd8\x57\x77\x6d\x58\x08\x50\x30\x96\x13\xef\x50\xca\x24\x52\x3c\x95\x50\xae\x0c\x7f\x1b\xde\x01\xcb\x78\x88\x9b\xb9\x31\x31\xc5\x1c\x49\xc6\x73\xf7\x33\x2a\x14\x55\xd2\x81\x88\x51\xf8\xa5\x1e\xe1\x11\xca\x62\x59\x97\x44\xc6\xb8\xd0\x82\xc4\x49\x1a\x23\x59\xf6\xc5\xfa\x36\x95\x97\xf1\x64\x42\x85\x40\x52\x85\xaa\x1c\x68\x3b\xe2\x1b\xa1\x38\x1e\xa2\xf0\xcb\x06\xbe\x4a\xf6\x15\x52\xf8\x1b\xbe\x36\x31\x26\xf4\xcb\xce\x1c\xa4\x1c\x2b\x67\x31\x77\x9b\xbd\x82\xff\x41\x05\xe8\xb4\xb1\x23\x07\x24\x64\x14\x27\xec\x33\xd9\x91\x07\x35\x3f\xe3\xf1\xae\x1c\x6f\x08\x57\x72\x93\x09\x49\xc3\x09\x92\x4b\x83\x70\x96\x7c\xbb\x71\xd7\xb1\x25\x58\x08\x34\x7e\x84\xe3\x95\x78\x4b\x15\xb5\x28\x93\xd3\x05\xbe\xcd\xe8\x7f\x9c\x33\x6f\x62\x0c\x63\x82\xa9\xfc\x76\x89\xb7\x61\x5c\xd6\x8d\x6f\x73\x91\x4d\xbc\x84\x0a\x89\x68\x88\x45\x05\xde\x8d\x74\xf7\x80\x56\x59\x2a\xc6\x98\x12\xfc\xed\x46\x7a\x08\xd9\xa6\x85\x8a\xea\xb0\x25\x19\x56\x96\x03\x63\xad\x18\x95\xaa\xdd\x01\x34\xc0\x9a\xcd\x8c\x7c\x10\xf2\x41\x9d\x76\x1f\xd6\x48\xb9\x64\x6a\x22\xd6\x8a\x44\x15\xf4\x7c\x2c\x58\x7c\x83\xa3\x35\x8a\xf3\xe1\xdd\x69\xce\x21\x36\xa8\x5a\xbb\xa8\x54\xe8\x2a\xf0\x78\x6f\x2a\x59\xfd\x86\x84\x92\x71\x96\x8a\x25\x5a\x89\x24\xfe\xb4\xa3\xf1\xd7\xb3\xee\x63\x5c\x79\x93\x74\xc2\x28\x91\x4c\xd9\xe1\x93\x64\x2c\x7e\x64\xf4\x95\xe4\xc2\x09\x22\xf1\x52\xa6\x65\x6b\xf5\x68\x57\x2e\x63\x9a\xc8\x44\xf3\x65\xb4\x7e\x38\xeb\x77\x83\x8f\x57\x0e\xa8\x21\xb8\xba\x7e\xdb\x73\xbb\x60\x5a\xb6\xfd\xe1\x75\xd7\xb6\xcf\x82\x33\xf8\xed\x22\xb8\xec\xc1\x61\xbd\x01\x01\x47\x54\x10\xe5\xe4\x28\xb6\x6d\xc7\x33\xc1\x9c\x48\x99\x36\x6d\xfb\xf6\xf6\xb6\x7e\xfb\xba\xce\xf8\xd8\x0e\x7c\xfb\x4e\xe1\x3a\x54\xc0\xc5\xa5\x25\x57\x20\xeb\x91\x8c\xcc\x53\xa3\xf5\x83\x65\x19\x03\x39\x8d\x31\x20\x1a\x81\x26\x12\x61\x4e\x94\x23\xa9\x34\x0d\x0a\xb5\x68\xda\xf6\x98\xc8\x49\x36\xac\x87\x2c\xb1\x95\x0c\xe3\x8c\xda\x1a\x1d\x0a\x73\x7c\x96\x16\xcd\x9a\xab\x43\x18\x86\x11\x4c\x30\x5c\xba\x01\xf4\x48\x88\xa9\xc0\xf0\xea\xd2\x0d\x0e\x0c\xa3\xcb\xd2\x29\x27\xe3\x89\x84\x57\xe1\x01\x1c\x35\x0e\x7f\x86\xcb\x1c\xa3\x61\x5c\x61\x9e\x10\x21\x08\xa3\x40\x04\x4c\x30\xc7\xc3\x29\x8c\x39\xa2\x12\x47\x35\x18\x71\x8c\x81\x8d\x20\x9c\x20\x3e\xc6\x35\x90\x0c\x10\x9d\x42\x8a\xb9\x60\x14\xd8\x50\x22\x42\x55\xdc\x21\x08\x59\x3a\x35\xd8\x08\xe4\x84\x08\x10\x6c\x24\x6f\x11\xcf\x25\x44\x42\xb0\x90\x20\x89\x23\x88\x58\x98\x25\x98\xe6\x09\x03\x46\x24\xc6\x02\x5e\xc9\x09\x06\x73\x50\x40\x98\x07\x9a\x48\x84\x51\x6c\x10\x0a\xea\xd9\xfc\x91\xee\x4a\x59\x26\x81\x63\x21\x39\xd1\x5a\xa8\x01\xa1\x61\x9c\x45\x8a\x87\xf9\xe3\x98\x24\xa4\xa0\xa0\xc0\xb5\xe0\xc2\x90\x0c\x32\x81\x6b\x9a\xcf\x1a\x24\x2c\x22\x23\xf5\x17\x6b\xb1\xd2\x6c\x18\x13\x31\xa9\x41\x44\x14\xea\x61\x26\x71\x0d\x84\x1a\xd4\x7a\xac\x29\x39\x6c\xc6\x41\xe0\x38\x36\x42\x96\x12\x2c\x40\xcb\xba\xe4\x4e\xcf\x51\xac\xa7\x4a\xa1\xb2\x50\x91\x50\x23\xb7\x13\x96\x94\x25\x21\xc2\x18\x65\x9c\x12\x31\xc1\x1a\x26\x62\x20\x98\xa6\xa8\xbc\x59\x8d\xa8\xe9\x23\x16\xc7\xec\x56\x89\x16\x32\x1a\x91\xa2\x11\xd5\x46\x46\x43\xd5\x8c\x87\x0b\xbb\x52\x26\x49\x98\xab\x5b\x1b\x20\x5d\x5a\xb5\x78\x24\x26\x28\x8e\x61\x88\x0b\x85\xe1\x08\x08\x05\xb4\x22\x0e\x57\xe4\x55\x2d\x92\x04\xc5\x90\x32\xae\xe9\xad\x8b\x59\x37\x8c\xe0\xc2\x81\x41\xff\x3c\xf8\xd0\xf1\x1d\x70\x07\x70\xe5\xf7\xdf\xbb\x67\xce\x19\x98\x9d\x01\xb8\x03\xb3\x06\x1f\xdc\xe0\xa2\x7f\x1d\xc0\x87\x8e\xef\x77\xbc\xe0\x23\xf4\xcf\xa1\xe3\x7d\x84\xff\x73\xbd\xb3\x1a\x38\xbf\x5d\xf9\xce\x60\x00\x7d\xdf\x70\x2f\xaf\x7a\xae\x73\x56\x03\xd7\xeb\xf6\xae\xcf\x5c\xef\x1d\xbc\xbd\x0e\xc0\xeb\x07\xd0\x73\x2f\xdd\xc0\x39\x83\xa0\x0f\x8a\x60\x81\xca\x75\x06\x0a\xd9\xa5\xe3\x77\x2f\x3a\x5e\xd0\x79\xeb\xf6\xdc\xe0\x63\xcd\x38\x77\x03\x4f\xe1\x3c\xef\xfb\xd0\x81\xab\x8e\x1f\xb8\xdd\xeb\x5e\xc7\x87\xab\x6b\xff\xaa\x3f\x70\xa0\xe3\x9d\x81\xd7\xf7\x5c\xef\xdc\x77\xbd\x77\xce\xa5\xe3\x05\x75\x70\x3d\xf0\xfa\xe0\xbc\x77\xbc\x00\x06\x17\x9d\x5e\x4f\x91\x32\x3a\xd7\xc1\x45\xdf\x57\xfc\x41\xb7\x7f\xf5\xd1\x77\xdf\x5d\x04\x70\xd1\xef\x9d\x39\xfe\x00\xde\x3a\xd0\x73\x3b\x6f\x7b\x4e\x4e\xca\xfb\x08\xdd\x5e\xc7\xbd\xac\xc1\x59\xe7\xb2\xf3\xce\xd1\x50\xfd\xe0\xc2\xf1\x0d\x35\x2d\xe7\x0e\x3e\x5c\x38\x6a\x48\xd1\xeb\x78\xd0\xe9\x06\x6e\xdf\x53\x62\x74\xfb\x5e\xe0\x77\xba\x41\x0d\x82\xbe\x1f\x2c\x40\x3f\xb8\x03\xa7\x06\x1d\xdf\x1d\x28\x85\x9c\xfb\xfd\xcb\x9a\xa1\xd4\xd9\x3f\x57\x53\x5c\x4f\xc1\x79\x4e\x8e\x45\xa9\x1a\x4a\x16\xe9\xfb\xfa\xfe\x7a\xe0\x2c\x10\xc2\x99\xd3\xe9\xb9\xde\xbb\x81\x02\x56\x22\xce\x27\xd7\x0d\xcb\x3a\x35\x5a\x3a\x05\xde\x25\x31\x15\xed\x8a\xc4\x76\x78\x7c\x7c\x9c\xe7\x33\x73\xb7\x49\x42\x25\xb7\xb6\x39\x62\x54\x5a\x23\x94\x90\x78\xda\x84\x9f\x2e\x70\x7c\x83\x25\x09\x11\x78\x38\xc3\x3f\xd5\x60\x31\x50\x83\x0e\x27\x28\xae\x81\x40\x54\x58\x02\x73\x32\x3a\x81\x21\xbb\xb3\x04\xf9\x4b\xf5\x00\x30\x64\x3c\xc2\xdc\x1a\xb2\xbb\x13\xd0\x48\x05\xf9\x0b\x37\xe1\xf0\xe7\xf4\xee\x04\x12\xc4\xc7\x84\x36\xa1\x71\xa2\x72\xeb\x04\xa3\xe8\x39\xe9\x27\x58\x22\x50\x2b\xa9\xb6\x79\x43\xf0\xad\x8a\x22\x53\x45\xaf\xc4\x54\xb6\xcd\x5b\x12\xc9\x49\x3b\xc2\x37\x24\xc4\x96\xbe\x79\x3e\x65\x81\x3d\x67\x57\x19\xd3\xc2\x7f\x66\xe4\xa6\x6d\x76\x73\x56\xad\x60\x9a\xe2\x15\xc6\x55\x0b\x64\x2b\xe3\x9e\xe8\x4a\x20\xb0\x6c\x5f\x07\xe7\xd6\xaf\xcf\xcc\xbe\x5e\xb6\x3d\x9f\xb9\x1f\xea\x45\x5a\xb6\x66\xee\xd4\x30\x5a\xb6\x72\x4a\x75\x31\x64\xd1\x14\x88\xc4\x89\x08\x59\x8a\xdb\xa6\xa9\x6f\xe4\x54\x5d\x17\x11\x25\xc2\x09\x4e\x90\x8e\x28\x47\x55\xf7\xcb\x79\x1f\xf7\xa4\x42\x5a\xb7\x78\xf8\x85\x48\x2b\x7f\x90\x30\x26\x27\x1a\x28\xaf\x0d\x04\x09\x1c\x2d\x27\x29\xdf\xd0\xd0\x16\x8a\x3e\x67\x42\x36\x81\x32\x8a\x4f\x60\x82\x55\x65\x6a\xc2\x61\xa3\xf1\x3f\x27\x10\x13\x8a\xad\xc5\x50\xfd\x0d\x4e\x4e\x40\x47\x40\x3e\x01\x7e\x20\x89\x0a\x16\x44\xe5\x09\x0c\x51\xf8\x65\xcc\x59\x46\x23\x2b\x64\x31\xe3\x4d\xf8\x71\xf4\x46\xfd\xae\xaa\x1f\x52\x14\x45\x9a\x2b\xe5\x0d\xc3\xb1\x9e\xd9\x36\x8b\x99\xa6\xd2\xb7\x44\xc3\xa7\x76\x8f\x15\x91\x76\x94\xa3\x92\x77\x80\x96\xe4\xcf\x98\xc7\x00\x14\x07\x4f\x9c\x49\x6f\x30\x57\x48\x62\x0b\xc5\x64\x4c\x9b\x20\x59\x5a\x56\xd4\x8d\x7e\xd0\x36\x25\x4b\xcd\xd3\x96\x2d\xa3\x25\xa3\x79\x66\x35\xdf\x34\x1a\x4f\x1c\x2a\x95\x4c\x47\x44\xa4\x31\x9a\x36\x61\x18\xb3\xf0\x4b\xc9\xb7\x13\x74\x67\x15\x4e\xf2\xa6\xd1\x48\xef\x4a\x0f\xc3\x18\x23\xae\x08\xca\x49\x69\x7c\x5b\xa0\x2c\x94\x03\x28\x93\x6c\x2d\x24\x4a\xda\xd2\x8a\x02\x68\x45\xe4\xe6\xa9\xdd\xaa\x2c\xef\xba\x72\x1e\x16\x62\xce\xb7\x32\xb2\x0e\xe6\xc2\xce\x4a\x13\x26\x84\x38\x8e\x8b\xd9\x6d\xb3\x91\xdf\x8b\x14\x85\xf3\xfb\x27\x15\xb4\x78\xc8\x51\x44\x32\xd1\x84\xd7\x7a\xac\x22\x01\x8c\x46\xa5\x2c\x96\x83\x35\xe1\x30\xbd\x03\xc1\x62\x12\xc1\x8f\xf8\x58\xfd\x96\x13\xc3\x68\xb4\xa2\x8b\x7d\xc8\x0e\x4b\x4e\x9e\x2e\x4b\xbc\xd9\x1a\x70\x25\xed\x6a\x90\xdb\xa2\xd4\xfc\xd2\x68\x9c\x80\x2e\x51\xc5\xfc\x10\x53\x89\x79\x95\xbd\xf4\xbf\x86\x36\xca\xa6\xdd\x9c\x37\xbf\x1c\x1d\x75\xab\x0b\xd0\x91\xf2\x6b\x13\x8a\x78\xcb\x09\xac\x5a\x2f\x87\xad\x8e\xc8\xf9\xcf\x72\xf7\x67\xb1\xed\x03\xfa\x6d\x49\xe5\x3b\xac\x03\x38\x84\xd9\x4c\x2c\x5e\x78\xc0\x88\x71\x58\xee\x50\x6c\xd9\x21\x82\xd9\x6c\x8d\x2a\xac\xee\x57\xb4\x4b\xbb\x15\x1b\xd3\x8a\x57\x2b\x25\xe3\x2f\x72\xf0\xe2\x9e\xbf\xb8\xe9\x2e\xc5\x6c\xe9\x3c\x87\xb9\xf3\x3c\xe4\x1b\x7b\x9f\xfb\xb6\xaa\x7d\xbf\x9c\x60\xdf\x5d\xa1\x01\x8d\x79\x2e\x79\xc8\x1d\x0a\x31\x10\x4c\x38\x1e\xb5\xcd\x5d\x5e\xe0\x3e\xb1\x3f\xcc\x93\xe6\xf9\xf9\x79\x91\x7c\x23\x1c\x32\xae\xdf\xc9\xcd\x97\x07\xa5\x05\xc1\x91\x5a\x0e\x94\xf2\xf6\x90\xc5\x51\x75\xe2\x0e\x33\x2e\x14\xf6\x94\x91\x7c\x60\xd1\x50\x10\xaa\x91\x16\x7d\xc5\x5a\x82\xff\x45\x31\xa6\xf1\xe9\x97\xa8\x23\xc6\x93\x26\x84\x28\x25\x12\xc5\xe4\x2f\x5c\x99\xf4\x5f\xff\xfc\x2b\x8e\x50\x45\xbd\xde\x98\x51\x0c\x6b\x2d\x37\xf3\x42\xbe\x18\x5c\x74\x6f\xe9\x5d\x61\xde\xd3\xf7\x04\xdf\x02\xa1\x0f\xbd\x7c\x9f\x2f\x23\x51\xa5\x0f\xaf\x25\xde\xea\xf4\x9b\xff\x7c\x6d\xd3\xa5\xa2\x28\xbc\x84\xec\x3f\x13\xb2\x42\x72\x46\xc7\xcf\xa7\xda\xdf\xb7\x9f\x31\xf9\xa3\xd8\x71\x6b\xd9\x39\x93\xdf\xc1\xeb\x2a\x1a\x86\xe2\xc9\xfc\x20\xc5\xfa\xd6\xdd\x8b\x1f\xfe\x3b\xfc\x30\x6f\x4d\x17\xae\xd6\x1a\x3e\x9f\x99\xc1\xae\xd6\xd1\x57\x4e\x10\x6d\x3f\xe6\xf3\xcc\xc2\x6c\x8f\x3b\xa8\xa8\x05\xcb\xcd\xfb\xbc\x12\x3c\xbb\x67\xac\x70\xb4\x2f\xee\xf1\x55\x8d\x7e\xf5\x58\xd8\x7f\xa8\xb3\xac\x76\x98\xeb\xe7\xd4\x9e\xa9\xa1\x9c\xb7\x5b\x1b\x3d\x65\x46\x23\xcc\x55\xf7\x57\x76\xa7\xfc\xa4\x9d\x6a\xa2\xf6\x2f\xc7\x7c\x5b\x35\xdd\xb1\xbd\x5b\x3d\xe3\x52\x69\xde\x97\xae\x70\x6f\xaa\xf1\xde\x79\x26\x40\x6b\xb2\x87\x3c\xed\x9d\x9e\x1e\x13\xc1\x0f\x75\xc4\x2f\x81\xf5\xdf\xd9\xe6\xae\x2e\xb7\x16\x67\x05\x97\x0b\xae\xf9\xd0\x33\x2c\xb9\x56\x4f\x2e\xbe\x78\xe3\xbf\xc3\x1b\x5f\x16\x5d\x2f\x8b\xae\x97\x45\xd7\xbe\x3b\xcb\xcb\xa2\x6b\x6f\x5a\xb6\x6d\x86\x6a\xd9\x7a\x3f\xee\xf4\x11\x5b\xa1\x0b\x90\xe5\xc8\x93\x9f\xc4\x28\x1d\x4d\x5a\x39\x69\xb2\x34\xf4\xf1\xf1\xf1\x43\x1b\xdc\xe5\x9d\xdd\xcd\x2d\xc9\xfd\x68\x1a\xf6\xa9\x7d\x79\xca\xd6\xe5\x68\x6b\xeb\x52\xb9\x89\xf6\x35\x93\xaf\xf4\x36\x6b\xe7\x1a\xca\xa7\xb0\x56\xd3\x55\xf9\x4b\xda\xa7\x73\x88\xa3\xd5\x6c\xa5\x25\xda\x39\x55\x61\x2a\x61\x38\xdd\x6d\x1f\x6e\x33\x77\x6c\x9c\x77\x58\xcf\x0c\x2d\x3b\x22\x37\xa7\xf9\xff\x46\x39\x4d\xec\x5b\x5b\xbb\xe5\x78\x5d\x2e\xe2\x32\x7f\xb5\xec\x21\x8b\xa6\x6a\x64\x22\x93\xf8\xd4\x30\xaa\x3f\xd5\x4d\x33\x31\x61\x37\x98\x7f\x87\x2f\x55\x37\x50\x95\xbf\x6d\xfa\x27\xbe\x43\xfb\x3e\x9f\xa1\xed\xfe\x15\xda\xf7\xfb\x08\x6d\x85\xe6\x0e\x9a\x5c\x7e\x6e\xfa\x88\x4f\xc0\xfe\x3f\x00\x00\xff\xff\x3c\xf7\xad\xcb\x87\x3f\x00\x00") func templateDefaultTmplBytes() ([]byte, error) { return bindataRead( _templateDefaultTmpl, "template/default.tmpl", ) } func templateDefaultTmpl() (*asset, error) { bytes, err := templateDefaultTmplBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "template/default.tmpl", size: 16263, mode: os.FileMode(420), modTime: time.Unix(1491400526, 0)} a := &asset{bytes: bytes, info: info} return a, nil } // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. func Asset(name string) ([]byte, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) } return a.bytes, nil } return nil, fmt.Errorf("Asset %s not found", name) } // MustAsset is like Asset but panics when Asset would return an error. // It simplifies safe initialization of global variables. func MustAsset(name string) []byte { a, err := Asset(name) if err != nil { panic("asset: Asset(" + name + "): " + err.Error()) } return a } // AssetInfo loads and returns the asset info for the given name. // It returns an error if the asset could not be found or // could not be loaded. func AssetInfo(name string) (os.FileInfo, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) } return a.info, nil } return nil, fmt.Errorf("AssetInfo %s not found", name) } // AssetNames returns the names of the assets. func AssetNames() []string { names := make([]string, 0, len(_bindata)) for name := range _bindata { names = append(names, name) } return names } // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "template/default.tmpl": templateDefaultTmpl, } // AssetDir returns the file names below a certain // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: // data/ // foo.txt // img/ // a.png // b.png // then AssetDir("data") would return []string{"foo.txt", "img"} // AssetDir("data/img") would return []string{"a.png", "b.png"} // AssetDir("foo.txt") and AssetDir("notexist") would return an error // AssetDir("") will return []string{"data"}. func AssetDir(name string) ([]string, error) { node := _bintree if len(name) != 0 { cannonicalName := strings.Replace(name, "\\", "/", -1) pathList := strings.Split(cannonicalName, "/") for _, p := range pathList { node = node.Children[p] if node == nil { return nil, fmt.Errorf("Asset %s not found", name) } } } if node.Func != nil { return nil, fmt.Errorf("Asset %s not found", name) } rv := make([]string, 0, len(node.Children)) for childName := range node.Children { rv = append(rv, childName) } return rv, nil } type bintree struct { Func func() (*asset, error) Children map[string]*bintree } var _bintree = &bintree{nil, map[string]*bintree{ "template": &bintree{nil, map[string]*bintree{ "default.tmpl": &bintree{templateDefaultTmpl, map[string]*bintree{}}, }}, }} // RestoreAsset restores an asset under the given directory func RestoreAsset(dir, name string) error { data, err := Asset(name) if err != nil { return err } info, err := AssetInfo(name) if err != nil { return err } err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) if err != nil { return err } err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) if err != nil { return err } err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) if err != nil { return err } return nil } // RestoreAssets restores an asset under the given directory recursively func RestoreAssets(dir, name string) error { children, err := AssetDir(name) // File if err != nil { return RestoreAsset(dir, name) } // Dir for _, child := range children { err = RestoreAssets(dir, filepath.Join(name, child)) if err != nil { return err } } return nil } func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } prometheus-alertmanager-0.6.2+ds/template/template.go000066400000000000000000000202561314512360300227340ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package template import ( "bytes" "net/url" "path/filepath" "regexp" "sort" "strings" "time" tmplhtml "html/template" tmpltext "text/template" "github.com/prometheus/common/model" "github.com/prometheus/alertmanager/template/internal/deftmpl" "github.com/prometheus/alertmanager/types" ) // Template bundles a text and a html template instance. type Template struct { text *tmpltext.Template html *tmplhtml.Template ExternalURL *url.URL } // FromGlobs calls ParseGlob on all path globs provided and returns the // resulting Template. func FromGlobs(paths ...string) (*Template, error) { t := &Template{ text: tmpltext.New("").Option("missingkey=zero"), html: tmplhtml.New("").Option("missingkey=zero"), } var err error t.text = t.text.Funcs(tmpltext.FuncMap(DefaultFuncs)) t.html = t.html.Funcs(tmplhtml.FuncMap(DefaultFuncs)) b, err := deftmpl.Asset("template/default.tmpl") if err != nil { return nil, err } if t.text, err = t.text.Parse(string(b)); err != nil { return nil, err } if t.html, err = t.html.Parse(string(b)); err != nil { return nil, err } for _, tp := range paths { // ParseGlob in the template packages errors if not at least one file is // matched. We want to allow empty matches that may be populated later on. p, err := filepath.Glob(tp) if err != nil { return nil, err } if len(p) > 0 { if t.text, err = t.text.ParseGlob(tp); err != nil { return nil, err } if t.html, err = t.html.ParseGlob(tp); err != nil { return nil, err } } } return t, nil } // ExecuteTextString needs a meaningful doc comment (TODO(fabxc)). func (t *Template) ExecuteTextString(text string, data interface{}) (string, error) { if text == "" { return "", nil } tmpl, err := t.text.Clone() if err != nil { return "", err } tmpl, err = tmpl.New("").Option("missingkey=zero").Parse(text) if err != nil { return "", err } var buf bytes.Buffer err = tmpl.Execute(&buf, data) return buf.String(), err } // ExecuteHTMLString needs a meaningful doc comment (TODO(fabxc)). func (t *Template) ExecuteHTMLString(html string, data interface{}) (string, error) { if html == "" { return "", nil } tmpl, err := t.html.Clone() if err != nil { return "", err } tmpl, err = tmpl.New("").Option("missingkey=zero").Parse(html) if err != nil { return "", err } var buf bytes.Buffer err = tmpl.Execute(&buf, data) return buf.String(), err } type FuncMap map[string]interface{} var DefaultFuncs = FuncMap{ "toUpper": strings.ToUpper, "toLower": strings.ToLower, "title": strings.Title, // join is equal to strings.Join but inverts the argument order // for easier pipelining in templates. "join": func(sep string, s []string) string { return strings.Join(s, sep) }, "safeHtml": func(text string) tmplhtml.HTML { return tmplhtml.HTML(text) }, "reReplaceAll": func(pattern, repl, text string) string { re := regexp.MustCompile(pattern) return re.ReplaceAllString(text, repl) }, } // Pair is a key/value string pair. type Pair struct { Name, Value string } // Pairs is a list of key/value string pairs. type Pairs []Pair // Names returns a list of names of the pairs. func (ps Pairs) Names() []string { ns := make([]string, 0, len(ps)) for _, p := range ps { ns = append(ns, p.Name) } return ns } // Values returns a list of values of the pairs. func (ps Pairs) Values() []string { vs := make([]string, 0, len(ps)) for _, p := range ps { vs = append(vs, p.Value) } return vs } // KV is a set of key/value string pairs. type KV map[string]string // SortedPairs returns a sorted list of key/value pairs. func (kv KV) SortedPairs() Pairs { var ( pairs = make([]Pair, 0, len(kv)) keys = make([]string, 0, len(kv)) sortStart = 0 ) for k := range kv { if k == string(model.AlertNameLabel) { keys = append([]string{k}, keys...) sortStart = 1 } else { keys = append(keys, k) } } sort.Strings(keys[sortStart:]) for _, k := range keys { pairs = append(pairs, Pair{k, kv[k]}) } return pairs } // Remove returns a copy of the key/value set without the given keys. func (kv KV) Remove(keys []string) KV { keySet := make(map[string]struct{}, len(keys)) for _, k := range keys { keySet[k] = struct{}{} } res := KV{} for k, v := range kv { if _, ok := keySet[k]; !ok { res[k] = v } } return res } // Names returns the names of the label names in the LabelSet. func (kv KV) Names() []string { return kv.SortedPairs().Names() } // Values returns a list of the values in the LabelSet. func (kv KV) Values() []string { return kv.SortedPairs().Values() } // Data is the data passed to notification templates and webhook pushes. // // End-users should not be exposed to Go's type system, as this will confuse them and prevent // simple things like simple equality checks to fail. Map everything to float64/string. type Data struct { Receiver string `json:"receiver"` Status string `json:"status"` Alerts Alerts `json:"alerts"` GroupLabels KV `json:"groupLabels"` CommonLabels KV `json:"commonLabels"` CommonAnnotations KV `json:"commonAnnotations"` ExternalURL string `json:"externalURL"` } // Alert holds one alert for notification templates. type Alert struct { Status string `json:"status"` Labels KV `json:"labels"` Annotations KV `json:"annotations"` StartsAt time.Time `json:"startsAt"` EndsAt time.Time `json:"endsAt"` GeneratorURL string `json:"generatorURL"` } // Alerts is a list of Alert objects. type Alerts []Alert // Firing returns the subset of alerts that are firing. func (as Alerts) Firing() []Alert { res := []Alert{} for _, a := range as { if a.Status == string(model.AlertFiring) { res = append(res, a) } } return res } // Resolved returns the subset of alerts that are resolved. func (as Alerts) Resolved() []Alert { res := []Alert{} for _, a := range as { if a.Status == string(model.AlertResolved) { res = append(res, a) } } return res } // Data assembles data for template expansion. func (t *Template) Data(recv string, groupLabels model.LabelSet, alerts ...*types.Alert) *Data { data := &Data{ Receiver: strings.SplitN(recv, "/", 2)[0], Status: string(types.Alerts(alerts...).Status()), Alerts: make(Alerts, 0, len(alerts)), GroupLabels: KV{}, CommonLabels: KV{}, CommonAnnotations: KV{}, ExternalURL: t.ExternalURL.String(), } // The call to types.Alert is necessary to correctly resolve the internal // representation to the user representation. for _, a := range types.Alerts(alerts...) { alert := Alert{ Status: string(a.Status()), Labels: make(KV, len(a.Labels)), Annotations: make(KV, len(a.Annotations)), StartsAt: a.StartsAt, EndsAt: a.EndsAt, GeneratorURL: a.GeneratorURL, } for k, v := range a.Labels { alert.Labels[string(k)] = string(v) } for k, v := range a.Annotations { alert.Annotations[string(k)] = string(v) } data.Alerts = append(data.Alerts, alert) } for k, v := range groupLabels { data.GroupLabels[string(k)] = string(v) } if len(alerts) >= 1 { var ( commonLabels = alerts[0].Labels.Clone() commonAnnotations = alerts[0].Annotations.Clone() ) for _, a := range alerts[1:] { for ln, lv := range commonLabels { if a.Labels[ln] != lv { delete(commonLabels, ln) } } for an, av := range commonAnnotations { if a.Annotations[an] != av { delete(commonAnnotations, an) } } } for k, v := range commonLabels { data.CommonLabels[string(k)] = string(v) } for k, v := range commonAnnotations { data.CommonAnnotations[string(k)] = string(v) } } return data } prometheus-alertmanager-0.6.2+ds/test/000077500000000000000000000000001314512360300177315ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/test/acceptance.go000066400000000000000000000221751314512360300223550ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package test import ( "bytes" "encoding/json" "fmt" "io/ioutil" "net" "net/http" "os" "os/exec" "path/filepath" "sync" "syscall" "testing" "time" "github.com/prometheus/client_golang/api/alertmanager" "github.com/prometheus/common/model" "golang.org/x/net/context" ) // AcceptanceTest provides declarative definition of given inputs and expected // output of an Alertmanager setup. type AcceptanceTest struct { *testing.T opts *AcceptanceOpts ams []*Alertmanager collectors []*Collector actions map[float64][]func() } // AcceptanceOpts defines configuration paramters for an acceptance test. type AcceptanceOpts struct { Tolerance time.Duration baseTime time.Time } func (opts *AcceptanceOpts) alertString(a *model.Alert) string { if a.EndsAt.IsZero() { return fmt.Sprintf("%s[%v:]", a, opts.relativeTime(a.StartsAt)) } return fmt.Sprintf("%s[%v:%v]", a, opts.relativeTime(a.StartsAt), opts.relativeTime(a.EndsAt)) } // expandTime returns the absolute time for the relative time // calculated from the test's base time. func (opts *AcceptanceOpts) expandTime(rel float64) time.Time { return opts.baseTime.Add(time.Duration(rel * float64(time.Second))) } // expandTime returns the relative time for the given time // calculated from the test's base time. func (opts *AcceptanceOpts) relativeTime(act time.Time) float64 { return float64(act.Sub(opts.baseTime)) / float64(time.Second) } // NewAcceptanceTest returns a new acceptance test with the base time // set to the current time. func NewAcceptanceTest(t *testing.T, opts *AcceptanceOpts) *AcceptanceTest { test := &AcceptanceTest{ T: t, opts: opts, actions: map[float64][]func(){}, } opts.baseTime = time.Now() return test } // freeAddress returns a new listen address not currently in use. func freeAddress() string { // Let the OS allocate a free address, close it and hope // it is still free when starting Alertmanager. l, err := net.Listen("tcp4", "localhost:0") if err != nil { panic(err) } defer l.Close() return l.Addr().String() } // Do sets the given function to be executed at the given time. func (t *AcceptanceTest) Do(at float64, f func()) { t.actions[at] = append(t.actions[at], f) } // Alertmanager returns a new structure that allows starting an instance // of Alertmanager on a random port. func (t *AcceptanceTest) Alertmanager(conf string) *Alertmanager { am := &Alertmanager{ t: t, opts: t.opts, } dir, err := ioutil.TempDir("", "am_test") if err != nil { t.Fatal(err) } am.dir = dir cf, err := os.Create(filepath.Join(dir, "config.yml")) if err != nil { t.Fatal(err) } am.confFile = cf am.UpdateConfig(conf) am.addr = freeAddress() am.mesh = freeAddress() am.hwaddr = "00:00:00:00:00:01" am.nickname = "1" t.Logf("AM on %s", am.addr) client, err := alertmanager.New(alertmanager.Config{ Address: fmt.Sprintf("http://%s", am.addr), }) if err != nil { t.Fatal(err) } am.client = client t.ams = append(t.ams, am) return am } // Collector returns a new collector bound to the test instance. func (t *AcceptanceTest) Collector(name string) *Collector { co := &Collector{ t: t.T, name: name, opts: t.opts, collected: map[float64][]model.Alerts{}, expected: map[Interval][]model.Alerts{}, } t.collectors = append(t.collectors, co) return co } // Run starts all Alertmanagers and runs queries against them. It then checks // whether all expected notifications have arrived at the expected receiver. func (t *AcceptanceTest) Run() { errc := make(chan error) for _, am := range t.ams { am.errc = errc am.Start() defer func(am *Alertmanager) { am.Terminate() am.cleanup() }(am) } go t.runActions() var latest float64 for _, coll := range t.collectors { if l := coll.latest(); l > latest { latest = l } } deadline := t.opts.expandTime(latest) select { case <-time.After(deadline.Sub(time.Now())): // continue case err := <-errc: t.Error(err) } for _, coll := range t.collectors { report := coll.check() t.Log(report) } for _, am := range t.ams { t.Logf("stdout:\n%v", am.cmd.Stdout) t.Logf("stderr:\n%v", am.cmd.Stderr) } } // runActions performs the stored actions at the defined times. func (t *AcceptanceTest) runActions() { var wg sync.WaitGroup for at, fs := range t.actions { ts := t.opts.expandTime(at) wg.Add(len(fs)) for _, f := range fs { go func(f func()) { time.Sleep(ts.Sub(time.Now())) f() wg.Done() }(f) } } wg.Wait() } // Alertmanager encapsulates an Alertmanager process and allows // declaring alerts being pushed to it at fixed points in time. type Alertmanager struct { t *AcceptanceTest opts *AcceptanceOpts addr string mesh, hwaddr, nickname string client alertmanager.Client cmd *exec.Cmd confFile *os.File dir string errc chan<- error } // Start the alertmanager and wait until it is ready to receive. func (am *Alertmanager) Start() { cmd := exec.Command("../../alertmanager", "-config.file", am.confFile.Name(), "-log.level", "debug", "-web.listen-address", am.addr, "-storage.path", am.dir, "-mesh.listen-address", am.mesh, "-mesh.peer-id", am.hwaddr, "-mesh.nickname", am.nickname, ) if am.cmd == nil { var outb, errb bytes.Buffer cmd.Stdout = &outb cmd.Stderr = &errb } else { cmd.Stdout = am.cmd.Stdout cmd.Stderr = am.cmd.Stderr } am.cmd = cmd if err := am.cmd.Start(); err != nil { am.t.Fatalf("Starting alertmanager failed: %s", err) } go func() { if err := am.cmd.Wait(); err != nil { am.errc <- err } }() time.Sleep(50 * time.Millisecond) for i := 0; i < 10; i++ { resp, err := http.Get(fmt.Sprintf("http://%s/status", am.addr)) if err == nil { _, err := ioutil.ReadAll(resp.Body) if err != nil { am.t.Fatalf("Starting alertmanager failed: %s", err) } resp.Body.Close() return } time.Sleep(500 * time.Millisecond) } am.t.Fatalf("Starting alertmanager failed: timeout") } // Terminate kills the underlying Alertmanager process and remove intermediate // data. func (am *Alertmanager) Terminate() { syscall.Kill(am.cmd.Process.Pid, syscall.SIGTERM) } // Reload sends the reloading signal to the Alertmanager process. func (am *Alertmanager) Reload() { syscall.Kill(am.cmd.Process.Pid, syscall.SIGHUP) } func (am *Alertmanager) cleanup() { os.RemoveAll(am.confFile.Name()) } // Push declares alerts that are to be pushed to the Alertmanager // server at a relative point in time. func (am *Alertmanager) Push(at float64, alerts ...*TestAlert) { var nas model.Alerts for _, a := range alerts { nas = append(nas, a.nativeAlert(am.opts)) } alertAPI := alertmanager.NewAlertAPI(am.client) am.t.Do(at, func() { if err := alertAPI.Push(context.Background(), nas...); err != nil { am.t.Errorf("Error pushing %v: %s", nas, err) } }) } // SetSilence updates or creates the given Silence. func (am *Alertmanager) SetSilence(at float64, sil *TestSilence) { am.t.Do(at, func() { var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(sil.nativeSilence(am.opts)); err != nil { am.t.Errorf("Error setting silence %v: %s", sil, err) return } resp, err := http.Post(fmt.Sprintf("http://%s/api/v1/silences", am.addr), "application/json", &buf) if err != nil { am.t.Errorf("Error setting silence %v: %s", sil, err) return } defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) if err != nil { panic(err) } var v struct { Status string `json:"status"` Data struct { SilenceID string `json:"silenceId"` } `json:"data"` } if err := json.Unmarshal(b, &v); err != nil || resp.StatusCode/100 != 2 { am.t.Errorf("error setting silence %v: %s", sil, err) return } sil.ID = v.Data.SilenceID }) } // DelSilence deletes the silence with the sid at the given time. func (am *Alertmanager) DelSilence(at float64, sil *TestSilence) { am.t.Do(at, func() { req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/api/v1/silence/%s", am.addr, sil.ID), nil) if err != nil { am.t.Errorf("Error deleting silence %v: %s", sil, err) return } resp, err := http.DefaultClient.Do(req) if err != nil || resp.StatusCode/100 != 2 { am.t.Errorf("Error deleting silence %v: %s", sil, err) return } }) } // UpdateConfig rewrites the configuration file for the Alertmanager. It does not // initiate config reloading. func (am *Alertmanager) UpdateConfig(conf string) { if _, err := am.confFile.WriteString(conf); err != nil { am.t.Fatal(err) return } if err := am.confFile.Sync(); err != nil { am.t.Fatal(err) return } } prometheus-alertmanager-0.6.2+ds/test/acceptance/000077500000000000000000000000001314512360300220175ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/test/acceptance/inhibit_test.go000066400000000000000000000042641314512360300250410ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package test import ( "fmt" "testing" "time" . "github.com/prometheus/alertmanager/test" ) func TestInhibiting(t *testing.T) { t.Parallel() conf := ` route: receiver: "default" group_by: [] group_wait: 1s group_interval: 1s repeat_interval: 1s receivers: - name: "default" webhook_configs: - url: 'http://%s' inhibit_rules: - source_match: alertname: JobDown target_match: alertname: InstanceDown equal: - job - zone ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co := at.Collector("webhook") wh := NewWebhook(co) am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) am.Push(At(1), Alert("alertname", "test1", "job", "testjob", "zone", "aa")) am.Push(At(1), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "aa")) am.Push(At(1), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "ab")) // This JobDown in zone aa should inhibit InstanceDown in zone aa in the // second batch of notifications. am.Push(At(2.2), Alert("alertname", "JobDown", "job", "testjob", "zone", "aa")) co.Want(Between(2, 2.5), Alert("alertname", "test1", "job", "testjob", "zone", "aa").Active(1), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "aa").Active(1), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "ab").Active(1), ) co.Want(Between(3, 3.5), Alert("alertname", "test1", "job", "testjob", "zone", "aa").Active(1), Alert("alertname", "InstanceDown", "job", "testjob", "zone", "ab").Active(1), Alert("alertname", "JobDown", "job", "testjob", "zone", "aa").Active(2.2), ) at.Run() } prometheus-alertmanager-0.6.2+ds/test/acceptance/send_test.go000066400000000000000000000272651314512360300243520ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package test import ( "fmt" "sync" "testing" "time" . "github.com/prometheus/alertmanager/test" ) // This file contains acceptance tests around the basic sending logic // for notifications, which includes batching and ensuring that each // notification is eventually sent at least once and ideally exactly // once. func TestMergeAlerts(t *testing.T) { t.Parallel() conf := ` route: receiver: "default" group_by: [] group_wait: 1s group_interval: 1s repeat_interval: 0s receivers: - name: "default" webhook_configs: - url: 'http://%s' send_resolved: true ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co := at.Collector("webhook") wh := NewWebhook(co) am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) // Refresh an alert several times. The starting time must remain at the earliest // point in time. am.Push(At(1), Alert("alertname", "test").Active(1.1)) // Another Prometheus server might be sending later but with an earlier start time. am.Push(At(1.2), Alert("alertname", "test").Active(1)) co.Want(Between(2, 2.5), Alert("alertname", "test").Active(1)) am.Push(At(2.1), Alert("alertname", "test").Annotate("ann", "v1").Active(2)) co.Want(Between(3, 3.5), Alert("alertname", "test").Annotate("ann", "v1").Active(1)) // Annotations are always overwritten by the alert that arrived most recently. am.Push(At(3.6), Alert("alertname", "test").Annotate("ann", "v2").Active(1.5)) co.Want(Between(4, 4.5), Alert("alertname", "test").Annotate("ann", "v2").Active(1)) // If an alert is marked resolved twice, the latest point in time must be // set as the eventual resolve time. am.Push(At(4.6), Alert("alertname", "test").Annotate("ann", "v2").Active(3, 4.5)) am.Push(At(4.8), Alert("alertname", "test").Annotate("ann", "v3").Active(2.9, 4.8)) am.Push(At(4.8), Alert("alertname", "test").Annotate("ann", "v3").Active(2.9, 4.1)) co.Want(Between(5, 5.5), Alert("alertname", "test").Annotate("ann", "v3").Active(1, 4.8)) // Reactivate an alert after a previous occurrence has been resolved. // No overlap, no merge must occur. am.Push(At(5.3), Alert("alertname", "test")) co.Want(Between(6, 6.5), Alert("alertname", "test").Active(5.3)) // Test against a bug which ocurrec after a restart. The previous occurrence of // the alert was sent rather than the most recent one. // // XXX(fabxc) disabled as notification info won't be persisted. Thus, with a mesh // notifier we lose the state in this single-node setup. //at.Do(At(6.7), func() { // am.Terminate() // am.Start() //}) // On restart the alert is flushed right away as the group_wait has already passed. // However, it must be caught in the deduplication stage. // The next attempt will be 1s later and won't be filtered in deduping. //co.Want(Between(7.7, 8), Alert("alertname", "test").Active(5.3)) at.Run() } func TestRepeat(t *testing.T) { t.Parallel() conf := ` route: receiver: "default" group_by: [] group_wait: 1s group_interval: 1s repeat_interval: 0s receivers: - name: "default" webhook_configs: - url: 'http://%s' ` // Create a new acceptance test that instantiates new Alertmanagers // with the given configuration and verifies times with the given // tollerance. at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) // Create a collector to which alerts can be written and verified // against a set of expected alert notifications. co := at.Collector("webhook") // Run something that satisfies the webhook interface to which the // Alertmanager pushes as defined by its configuration. wh := NewWebhook(co) // Create a new Alertmanager process listening to a random port am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) // Declare pushes to be made to the Alertmanager at the given time. // Times are provided in fractions of seconds. am.Push(At(1), Alert("alertname", "test").Active(1)) // XXX(fabxc): disabled as long as alerts are not persisted. // at.Do(At(1.2), func() { // am.Terminate() // am.Start() // }) am.Push(At(3.5), Alert("alertname", "test").Active(1, 3)) // Declare which alerts are expected to arrive at the collector within // the defined time intervals. co.Want(Between(2, 2.5), Alert("alertname", "test").Active(1)) co.Want(Between(3, 3.5), Alert("alertname", "test").Active(1)) co.Want(Between(4, 4.5), Alert("alertname", "test").Active(1, 3)) // Start the flow as defined above and run the checks afterwards. at.Run() } func TestRetry(t *testing.T) { t.Parallel() // We create a notification config that fans out into two different // webhooks. // The succeeding one must still only receive the first successful // notifications. Sending to the succeeding one must eventually succeed. conf := ` route: receiver: "default" group_by: [] group_wait: 1s group_interval: 1s repeat_interval: 3s receivers: - name: "default" webhook_configs: - url: 'http://%s' - url: 'http://%s' ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co1 := at.Collector("webhook") wh1 := NewWebhook(co1) co2 := at.Collector("webhook_failing") wh2 := NewWebhook(co2) wh2.Func = func(ts float64) bool { // Fail the first two interval periods but eventually // succeed in the third interval after a few failed attempts. return ts < 4.5 } am := at.Alertmanager(fmt.Sprintf(conf, wh1.Address(), wh2.Address())) am.Push(At(1), Alert("alertname", "test1")) co1.Want(Between(2, 2.5), Alert("alertname", "test1").Active(1)) co1.Want(Between(5, 5.5), Alert("alertname", "test1").Active(1)) co2.Want(Between(4.5, 5), Alert("alertname", "test1").Active(1)) } func TestBatching(t *testing.T) { t.Parallel() conf := ` route: receiver: "default" group_by: [] group_wait: 1s group_interval: 1s repeat_interval: 5s receivers: - name: "default" webhook_configs: - url: 'http://%s' ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co := at.Collector("webhook") wh := NewWebhook(co) am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) am.Push(At(1.1), Alert("alertname", "test1").Active(1)) am.Push(At(1.7), Alert("alertname", "test5").Active(1)) co.Want(Between(2.0, 2.5), Alert("alertname", "test1").Active(1), Alert("alertname", "test5").Active(1), ) am.Push(At(3.3), Alert("alertname", "test2").Active(1.5), Alert("alertname", "test3").Active(1.5), Alert("alertname", "test4").Active(1.6), ) co.Want(Between(4.1, 4.5), Alert("alertname", "test1").Active(1), Alert("alertname", "test5").Active(1), Alert("alertname", "test2").Active(1.5), Alert("alertname", "test3").Active(1.5), Alert("alertname", "test4").Active(1.6), ) // While no changes happen expect no additional notifications // until the 5s repeat interval has ended. co.Want(Between(9.1, 9.5), Alert("alertname", "test1").Active(1), Alert("alertname", "test5").Active(1), Alert("alertname", "test2").Active(1.5), Alert("alertname", "test3").Active(1.5), Alert("alertname", "test4").Active(1.6), ) at.Run() } func TestResolved(t *testing.T) { t.Parallel() var wg sync.WaitGroup wg.Add(10) for i := 0; i < 10; i++ { go func() { conf := ` global: resolve_timeout: 10s route: receiver: "default" group_by: [alertname] group_wait: 1s group_interval: 5s receivers: - name: "default" webhook_configs: - url: 'http://%s' ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co := at.Collector("webhook") wh := NewWebhook(co) am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) am.Push(At(1), Alert("alertname", "test", "lbl", "v1"), Alert("alertname", "test", "lbl", "v2"), Alert("alertname", "test", "lbl", "v3"), ) co.Want(Between(2, 2.5), Alert("alertname", "test", "lbl", "v1").Active(1), Alert("alertname", "test", "lbl", "v2").Active(1), Alert("alertname", "test", "lbl", "v3").Active(1), ) co.Want(Between(12, 13), Alert("alertname", "test", "lbl", "v1").Active(1, 11), Alert("alertname", "test", "lbl", "v2").Active(1, 11), Alert("alertname", "test", "lbl", "v3").Active(1, 11), ) at.Run() wg.Done() }() } wg.Wait() } func TestResolvedFilter(t *testing.T) { t.Parallel() // This integration test ensures that even though resolved alerts may not be // notified about, they must be set as notified. Resolved alerts, even when // filtered, have to end up in the SetNotifiesStage, otherwise when an alert // fires again it is ambiguous whether it was resolved in between or not. conf := ` global: resolve_timeout: 10s route: receiver: "default" group_by: [alertname] group_wait: 1s group_interval: 5s receivers: - name: "default" webhook_configs: - url: 'http://%s' send_resolved: true - url: 'http://%s' send_resolved: false ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co1 := at.Collector("webhook1") wh1 := NewWebhook(co1) co2 := at.Collector("webhook2") wh2 := NewWebhook(co2) am := at.Alertmanager(fmt.Sprintf(conf, wh1.Address(), wh2.Address())) am.Push(At(1), Alert("alertname", "test", "lbl", "v1"), Alert("alertname", "test", "lbl", "v2"), ) am.Push(At(3), Alert("alertname", "test", "lbl", "v1").Active(1, 4), Alert("alertname", "test", "lbl", "v3"), ) am.Push(At(8), Alert("alertname", "test", "lbl", "v3").Active(3), ) co1.Want(Between(2, 2.5), Alert("alertname", "test", "lbl", "v1").Active(1), Alert("alertname", "test", "lbl", "v2").Active(1), ) co1.Want(Between(7, 7.5), Alert("alertname", "test", "lbl", "v1").Active(1, 4), Alert("alertname", "test", "lbl", "v2").Active(1), Alert("alertname", "test", "lbl", "v3").Active(3), ) co1.Want(Between(12, 12.5), Alert("alertname", "test", "lbl", "v2").Active(1, 11), Alert("alertname", "test", "lbl", "v3").Active(3), ) co2.Want(Between(2, 2.5), Alert("alertname", "test", "lbl", "v1").Active(1), Alert("alertname", "test", "lbl", "v2").Active(1), ) co2.Want(Between(7, 7.5), Alert("alertname", "test", "lbl", "v2").Active(1), Alert("alertname", "test", "lbl", "v3").Active(3), ) at.Run() } func TestReload(t *testing.T) { t.Parallel() // We create a notification config that fans out into two different // webhooks. // The succeeding one must still only receive the first successful // notifications. Sending to the succeeding one must eventually succeed. conf := ` route: receiver: "default" group_by: [] group_wait: 1s group_interval: 6s repeat_interval: 10m receivers: - name: "default" webhook_configs: - url: 'http://%s' ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co := at.Collector("webhook") wh := NewWebhook(co) am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) am.Push(At(1), Alert("alertname", "test1")) at.Do(At(3), am.Reload) am.Push(At(4), Alert("alertname", "test2")) co.Want(Between(2, 2.5), Alert("alertname", "test1").Active(1)) // Timers are reset on reload regardless, so we count the 6 second group // interval from 3 onwards. co.Want(Between(9, 9.5), Alert("alertname", "test1").Active(1), Alert("alertname", "test2").Active(4), ) at.Run() } prometheus-alertmanager-0.6.2+ds/test/acceptance/silence_test.go000066400000000000000000000054611314512360300250350ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package test import ( "fmt" "testing" "time" . "github.com/prometheus/alertmanager/test" ) func TestSilencing(t *testing.T) { t.Parallel() conf := ` route: receiver: "default" group_by: [] group_wait: 1s group_interval: 1s repeat_interval: 0s receivers: - name: "default" webhook_configs: - url: 'http://%s' ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co := at.Collector("webhook") wh := NewWebhook(co) am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) // No repeat interval is configured. Thus, we receive an alert // notification every second. am.Push(At(1), Alert("alertname", "test1").Active(1)) am.Push(At(1), Alert("alertname", "test2").Active(1)) co.Want(Between(2, 2.5), Alert("alertname", "test1").Active(1), Alert("alertname", "test2").Active(1), ) // Add a silence that affects the first alert. am.SetSilence(At(2.3), Silence(2.5, 4.5).Match("alertname", "test1")) co.Want(Between(3, 3.5), Alert("alertname", "test2").Active(1)) co.Want(Between(4, 4.5), Alert("alertname", "test2").Active(1)) // Silence should be over now and we receive both alerts again. co.Want(Between(5, 5.5), Alert("alertname", "test1").Active(1), Alert("alertname", "test2").Active(1), ) at.Run() } func TestSilenceDelete(t *testing.T) { t.Parallel() conf := ` route: receiver: "default" group_by: [] group_wait: 1s group_interval: 1s repeat_interval: 0s receivers: - name: "default" webhook_configs: - url: 'http://%s' ` at := NewAcceptanceTest(t, &AcceptanceOpts{ Tolerance: 150 * time.Millisecond, }) co := at.Collector("webhook") wh := NewWebhook(co) am := at.Alertmanager(fmt.Sprintf(conf, wh.Address())) // No repeat interval is configured. Thus, we receive an alert // notification every second. am.Push(At(1), Alert("alertname", "test1").Active(1)) am.Push(At(1), Alert("alertname", "test2").Active(1)) // Silence everything for a long time and delete the silence after // two iterations. sil := Silence(1.5, 100).MatchRE("alertname", ".*") am.SetSilence(At(1.3), sil) am.DelSilence(At(3.5), sil) co.Want(Between(3.5, 4.5), Alert("alertname", "test1").Active(1), Alert("alertname", "test2").Active(1), ) at.Run() } prometheus-alertmanager-0.6.2+ds/test/collector.go000066400000000000000000000067751314512360300222650ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package test import ( "fmt" "testing" "time" "github.com/prometheus/common/model" ) // Collector gathers alerts received by a notification receiver // and verifies whether all arrived and within the correct time boundaries. type Collector struct { t *testing.T name string opts *AcceptanceOpts collected map[float64][]model.Alerts expected map[Interval][]model.Alerts } func (c *Collector) String() string { return c.name } func batchesEqual(as, bs model.Alerts, opts *AcceptanceOpts) bool { if len(as) != len(bs) { return false } for _, a := range as { found := false for _, b := range bs { if equalAlerts(a, b, opts) { found = true break } } if !found { return false } } return true } // latest returns the latest relative point in time where a notification is // expected. func (c *Collector) latest() float64 { var latest float64 for iv := range c.expected { if iv.end > latest { latest = iv.end } } return latest } // Want declares that the Collector expects to receive the given alerts // within the given time boundaries. func (c *Collector) Want(iv Interval, alerts ...*TestAlert) { var nas model.Alerts for _, a := range alerts { nas = append(nas, a.nativeAlert(c.opts)) } c.expected[iv] = append(c.expected[iv], nas) } // add the given alerts to the collected alerts. func (c *Collector) add(alerts ...*model.Alert) { arrival := c.opts.relativeTime(time.Now()) c.collected[arrival] = append(c.collected[arrival], model.Alerts(alerts)) } func (c *Collector) check() string { report := fmt.Sprintf("\ncollector %q:\n\n", c) for iv, expected := range c.expected { report += fmt.Sprintf("interval %v\n", iv) for _, exp := range expected { var found model.Alerts report += fmt.Sprintf("---\n") for _, e := range exp { report += fmt.Sprintf("- %v\n", c.opts.alertString(e)) } for at, got := range c.collected { if !iv.contains(at) { continue } for _, a := range got { if batchesEqual(exp, a, c.opts) { found = a break } } if found != nil { break } } if found != nil { report += fmt.Sprintf(" [ ✓ ]\n") } else { c.t.Fail() report += fmt.Sprintf(" [ ✗ ]\n") } } } // Detect unexpected notifications. var totalExp, totalAct int for _, exp := range c.expected { for _, e := range exp { totalExp += len(e) } } for _, act := range c.collected { for _, a := range act { if len(a) == 0 { c.t.Error("received empty notifications") } totalAct += len(a) } } if totalExp != totalAct { c.t.Fail() report += fmt.Sprintf("\nExpected total of %d alerts, got %d", totalExp, totalAct) } if c.t.Failed() { report += "\nreceived:\n" for at, col := range c.collected { for _, alerts := range col { report += fmt.Sprintf("@ %v\n", at) for _, a := range alerts { report += fmt.Sprintf("- %v\n", c.opts.alertString(a)) } } } } return report } prometheus-alertmanager-0.6.2+ds/test/mock.go000066400000000000000000000151521314512360300212150ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package test import ( "encoding/json" "fmt" "net" "net/http" "reflect" "time" "github.com/prometheus/common/model" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/types" ) // At is a convenience method to allow for declarative syntax of Acceptance // test definitions. func At(ts float64) float64 { return ts } type Interval struct { start, end float64 } func (iv Interval) String() string { return fmt.Sprintf("[%v,%v]", iv.start, iv.end) } func (iv Interval) contains(f float64) bool { return f >= iv.start && f <= iv.end } // Between is a convenience constructor for an interval for declarative syntax // of Acceptance test definitions. func Between(start, end float64) Interval { return Interval{start: start, end: end} } // TestSilence models a model.Silence with relative times. type TestSilence struct { ID string match []string matchRE []string startsAt, endsAt float64 } // Silence creates a new TestSilence active for the relative interval given // by start and end. func Silence(start, end float64) *TestSilence { return &TestSilence{ startsAt: start, endsAt: end, } } // Match adds a new plain matcher to the silence. func (s *TestSilence) Match(v ...string) *TestSilence { s.match = append(s.match, v...) return s } // MatchRE adds a new regex matcher to the silence func (s *TestSilence) MatchRE(v ...string) *TestSilence { if len(v)%2 == 1 { panic("bad key/values") } s.matchRE = append(s.matchRE, v...) return s } // nativeSilence converts the declared test silence into a regular // silence with resolved times. func (s *TestSilence) nativeSilence(opts *AcceptanceOpts) *types.Silence { nsil := &types.Silence{} for i := 0; i < len(s.match); i += 2 { nsil.Matchers = append(nsil.Matchers, &types.Matcher{ Name: s.match[i], Value: s.match[i+1], }) } for i := 0; i < len(s.matchRE); i += 2 { nsil.Matchers = append(nsil.Matchers, &types.Matcher{ Name: s.matchRE[i], Value: s.matchRE[i+1], IsRegex: true, }) } if s.startsAt > 0 { nsil.StartsAt = opts.expandTime(s.startsAt) } if s.endsAt > 0 { nsil.EndsAt = opts.expandTime(s.endsAt) } nsil.Comment = "some comment" nsil.CreatedBy = "admin@example.com" return nsil } // TestAlert models a model.Alert with relative times. type TestAlert struct { labels model.LabelSet annotations model.LabelSet startsAt, endsAt float64 } // Alert creates a new alert declaration with the given key/value pairs // as identifying labels. func Alert(keyval ...interface{}) *TestAlert { if len(keyval)%2 == 1 { panic("bad key/values") } a := &TestAlert{ labels: model.LabelSet{}, annotations: model.LabelSet{}, } for i := 0; i < len(keyval); i += 2 { ln := model.LabelName(keyval[i].(string)) lv := model.LabelValue(keyval[i+1].(string)) a.labels[ln] = lv } return a } // nativeAlert converts the declared test alert into a full alert based // on the given paramters. func (a *TestAlert) nativeAlert(opts *AcceptanceOpts) *model.Alert { na := &model.Alert{ Labels: a.labels, Annotations: a.annotations, } if a.startsAt > 0 { na.StartsAt = opts.expandTime(a.startsAt) } if a.endsAt > 0 { na.EndsAt = opts.expandTime(a.endsAt) } return na } // Annotate the alert with the given key/value pairs. func (a *TestAlert) Annotate(keyval ...interface{}) *TestAlert { if len(keyval)%2 == 1 { panic("bad key/values") } for i := 0; i < len(keyval); i += 2 { ln := model.LabelName(keyval[i].(string)) lv := model.LabelValue(keyval[i+1].(string)) a.annotations[ln] = lv } return a } // Active declares the relative activity time for this alert. It // must be a single starting value or two values where the second value // declares the resolved time. func (a *TestAlert) Active(tss ...float64) *TestAlert { if len(tss) > 2 || len(tss) == 0 { panic("only one or two timestamps allowed") } if len(tss) == 2 { a.endsAt = tss[1] } a.startsAt = tss[0] return a } func equalAlerts(a, b *model.Alert, opts *AcceptanceOpts) bool { if !reflect.DeepEqual(a.Labels, b.Labels) { return false } if !reflect.DeepEqual(a.Annotations, b.Annotations) { return false } if !equalTime(a.StartsAt, b.StartsAt, opts) { return false } if !equalTime(a.EndsAt, b.EndsAt, opts) { return false } return true } func equalTime(a, b time.Time, opts *AcceptanceOpts) bool { if a.IsZero() != b.IsZero() { return false } diff := a.Sub(b) if diff < 0 { diff = -diff } return diff <= opts.Tolerance } type MockWebhook struct { opts *AcceptanceOpts collector *Collector listener net.Listener Func func(timestamp float64) bool } func NewWebhook(c *Collector) *MockWebhook { l, err := net.Listen("tcp4", "localhost:0") if err != nil { // TODO(fabxc): if shutdown of mock destinations ever becomes a concern // we want to shut them down after test completion. Then we might want to // log the error properly, too. panic(err) } wh := &MockWebhook{ listener: l, collector: c, opts: c.opts, } go http.Serve(l, wh) return wh } func (ws *MockWebhook) ServeHTTP(w http.ResponseWriter, req *http.Request) { // Inject Func if it exists. if ws.Func != nil { if ws.Func(ws.opts.relativeTime(time.Now())) { return } } dec := json.NewDecoder(req.Body) defer req.Body.Close() var v notify.WebhookMessage if err := dec.Decode(&v); err != nil { panic(err) } // Transform the webhook message alerts back into model.Alerts. var alerts model.Alerts for _, a := range v.Alerts { var ( labels = model.LabelSet{} annotations = model.LabelSet{} ) for k, v := range a.Labels { labels[model.LabelName(k)] = model.LabelValue(v) } for k, v := range a.Annotations { annotations[model.LabelName(k)] = model.LabelValue(v) } alerts = append(alerts, &model.Alert{ Labels: labels, Annotations: annotations, StartsAt: a.StartsAt, EndsAt: a.EndsAt, GeneratorURL: a.GeneratorURL, }) } ws.collector.add(alerts...) } func (ws *MockWebhook) Address() string { return ws.listener.Addr().String() } prometheus-alertmanager-0.6.2+ds/types/000077500000000000000000000000001314512360300201165ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/types/match.go000066400000000000000000000100121314512360300215330ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package types import ( "fmt" "regexp" "sort" "bytes" "github.com/prometheus/common/model" ) // Matcher defines a matching rule for the value of a given label. type Matcher struct { Name string `json:"name"` Value string `json:"value"` IsRegex bool `json:"isRegex"` regex *regexp.Regexp } // Init internals of the Matcher. Must be called before using Match. func (m *Matcher) Init() error { if !m.IsRegex { return nil } re, err := regexp.Compile("^(?:" + m.Value + ")$") if err == nil { m.regex = re } return err } func (m *Matcher) String() string { if m.IsRegex { return fmt.Sprintf("%s=~%q", m.Name, m.Value) } return fmt.Sprintf("%s=%q", m.Name, m.Value) } // Validate returns true iff all fields of the matcher have valid values. func (m *Matcher) Validate() error { if !model.LabelName(m.Name).IsValid() { return fmt.Errorf("invalid name %q", m.Name) } if m.IsRegex { if _, err := regexp.Compile(m.Value); err != nil { return fmt.Errorf("invalid regular expression %q", m.Value) } } else if !model.LabelValue(m.Value).IsValid() || len(m.Value) == 0 { return fmt.Errorf("invalid value %q", m.Value) } return nil } // Match checks whether the label of the matcher has the specified // matching value. func (m *Matcher) Match(lset model.LabelSet) bool { // Unset labels are treated as unset labels globally. Thus, if a // label is not set we retrieve the empty label which is correct // for the comparison below. v := lset[model.LabelName(m.Name)] if m.IsRegex { return m.regex.MatchString(string(v)) } return string(v) == m.Value } // NewMatcher returns a new matcher that compares against equality of // the given value. func NewMatcher(name model.LabelName, value string) *Matcher { return &Matcher{ Name: string(name), Value: value, IsRegex: false, } } // NewRegexMatcher returns a new matcher that compares values against // a regular expression. The matcher is already initialized. // // TODO(fabxc): refactor usage. func NewRegexMatcher(name model.LabelName, re *regexp.Regexp) *Matcher { return &Matcher{ Name: string(name), Value: re.String(), IsRegex: true, regex: re, } } // Matchers provides the Match and Fingerprint methods for a slice of Matchers. // Matchers must always be sorted. type Matchers []*Matcher // NewMatchers returns the given Matchers sorted. func NewMatchers(ms ...*Matcher) Matchers { m := Matchers(ms) sort.Sort(m) return m } func (ms Matchers) Len() int { return len(ms) } func (ms Matchers) Swap(i, j int) { ms[i], ms[j] = ms[j], ms[i] } func (ms Matchers) Less(i, j int) bool { if ms[i].Name > ms[j].Name { return false } if ms[i].Name < ms[j].Name { return true } if ms[i].Value > ms[j].Value { return false } if ms[i].Value < ms[j].Value { return true } return !ms[i].IsRegex && ms[j].IsRegex } // Equal returns whether both Matchers are equal. func (ms Matchers) Equal(o Matchers) bool { if len(ms) != len(o) { return false } for i, a := range ms { if *a != *o[i] { return false } } return true } // Match checks whether all matchers are fulfilled against the given label set. func (ms Matchers) Match(lset model.LabelSet) bool { for _, m := range ms { if !m.Match(lset) { return false } } return true } func (ms Matchers) String() string { var buf bytes.Buffer buf.WriteByte('{') for i, m := range ms { if i > 0 { buf.WriteByte(',') } buf.WriteString(m.String()) } buf.WriteByte('}') return buf.String() } prometheus-alertmanager-0.6.2+ds/types/types.go000066400000000000000000000246371314512360300216250ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package types import ( "fmt" "sort" "strings" "sync" "time" "github.com/prometheus/common/model" ) type AlertState string const ( AlertStateUnprocessed AlertState = "unprocessed" AlertStateActive = "active" AlertStateSuppressed = "suppressed" ) // AlertStatus stores the state and values associated with an Alert. type AlertStatus struct { State AlertState `json:"state"` SilencedBy []string `json:"silencedBy"` InhibitedBy []string `json:"inhibitedBy"` } // Marker helps to mark alerts as silenced and/or inhibited. // All methods are goroutine-safe. type Marker interface { SetActive(alert model.Fingerprint) SetInhibited(alert model.Fingerprint, ids ...string) SetSilenced(alert model.Fingerprint, ids ...string) Status(model.Fingerprint) AlertStatus Delete(model.Fingerprint) Unprocessed(model.Fingerprint) bool Active(model.Fingerprint) bool Silenced(model.Fingerprint) ([]string, bool) Inhibited(model.Fingerprint) ([]string, bool) } // NewMarker returns an instance of a Marker implementation. func NewMarker() Marker { return &memMarker{ m: map[model.Fingerprint]*AlertStatus{}, } } type memMarker struct { m map[model.Fingerprint]*AlertStatus mtx sync.RWMutex } // SetSilenced sets the AlertStatus to suppressed and stores the associated silence IDs. func (m *memMarker) SetSilenced(alert model.Fingerprint, ids ...string) { m.mtx.Lock() s, found := m.m[alert] if !found { s = &AlertStatus{} m.m[alert] = s } // If there are any silence or alert IDs associated with the // fingerprint, it is suppressed. Otherwise, set it to // AlertStateUnprocessed. if len(ids) == 0 && len(s.InhibitedBy) == 0 { m.mtx.Unlock() m.SetActive(alert) return } s.State = AlertStateSuppressed s.SilencedBy = ids m.mtx.Unlock() } // SetInhibited sets the AlertStatus to suppressed and stores the associated alert IDs. func (m *memMarker) SetInhibited(alert model.Fingerprint, ids ...string) { m.mtx.Lock() s, found := m.m[alert] if !found { s = &AlertStatus{} m.m[alert] = s } // If there are any silence or alert IDs associated with the // fingerprint, it is suppressed. Otherwise, set it to // AlertStateUnprocessed. if len(ids) == 0 && len(s.SilencedBy) == 0 { m.mtx.Unlock() m.SetActive(alert) return } s.State = AlertStateSuppressed s.InhibitedBy = ids m.mtx.Unlock() } func (m *memMarker) SetActive(alert model.Fingerprint) { m.mtx.Lock() defer m.mtx.Unlock() s, found := m.m[alert] if !found { s = &AlertStatus{ SilencedBy: []string{}, InhibitedBy: []string{}, } m.m[alert] = s } s.State = AlertStateActive s.SilencedBy = []string{} s.InhibitedBy = []string{} } // Status returns the AlertStatus for the given Fingerprint. func (m *memMarker) Status(alert model.Fingerprint) AlertStatus { m.mtx.RLock() defer m.mtx.RUnlock() s, found := m.m[alert] if !found { s = &AlertStatus{ State: AlertStateUnprocessed, SilencedBy: []string{}, InhibitedBy: []string{}, } } return *s } // Delete deletes the given Fingerprint from the internal cache. func (m *memMarker) Delete(alert model.Fingerprint) { m.mtx.Lock() defer m.mtx.Unlock() delete(m.m, alert) } // Unprocessed returns whether the alert for the given Fingerprint is in the // Unprocessed state. func (m *memMarker) Unprocessed(alert model.Fingerprint) bool { return m.Status(alert).State == AlertStateUnprocessed } // Active returns whether the alert for the given Fingerprint is in the Active // state. func (m *memMarker) Active(alert model.Fingerprint) bool { return m.Status(alert).State == AlertStateActive } // Inhibited returns whether the alert for the given Fingerprint is in the // Inhibited state and any associated alert IDs. func (m *memMarker) Inhibited(alert model.Fingerprint) ([]string, bool) { s := m.Status(alert) return s.InhibitedBy, s.State == AlertStateSuppressed && len(s.InhibitedBy) > 0 } // Silenced returns whether the alert for the given Fingerprint is in the // Silenced state and any associated silence IDs. func (m *memMarker) Silenced(alert model.Fingerprint) ([]string, bool) { s := m.Status(alert) return s.SilencedBy, s.State == AlertStateSuppressed && len(s.SilencedBy) > 0 } // MultiError contains multiple errors and implements the error interface. Its // zero value is ready to use. All its methods are goroutine safe. type MultiError struct { mtx sync.Mutex errors []error } // Add adds an error to the MultiError. func (e *MultiError) Add(err error) { e.mtx.Lock() defer e.mtx.Unlock() e.errors = append(e.errors, err) } // Len returns the number of errors added to the MultiError. func (e *MultiError) Len() int { e.mtx.Lock() defer e.mtx.Unlock() return len(e.errors) } // Errors returns the errors added to the MuliError. The returned slice is a // copy of the internal slice of errors. func (e *MultiError) Errors() []error { e.mtx.Lock() defer e.mtx.Unlock() return append(make([]error, 0, len(e.errors)), e.errors...) } func (e *MultiError) Error() string { e.mtx.Lock() defer e.mtx.Unlock() es := make([]string, 0, len(e.errors)) for _, err := range e.errors { es = append(es, err.Error()) } return strings.Join(es, "; ") } // Alert wraps a model.Alert with additional information relevant // to internal of the Alertmanager. // The type is never exposed to external communication and the // embedded alert has to be sanitized beforehand. type Alert struct { model.Alert // The authoritative timestamp. UpdatedAt time.Time Timeout bool WasSilenced bool `json:"-"` WasInhibited bool `json:"-"` } // AlertSlice is a sortable slice of Alerts. type AlertSlice []*Alert func (as AlertSlice) Less(i, j int) bool { return as[i].UpdatedAt.Before(as[j].UpdatedAt) } func (as AlertSlice) Swap(i, j int) { as[i], as[j] = as[j], as[i] } func (as AlertSlice) Len() int { return len(as) } // Alerts turns a sequence of internal alerts into a list of // exposable model.Alert structures. func Alerts(alerts ...*Alert) model.Alerts { res := make(model.Alerts, 0, len(alerts)) for _, a := range alerts { v := a.Alert // If the end timestamp was set as the expected value in case // of a timeout but is not reached yet, do not expose it. if a.Timeout && !a.Resolved() { v.EndsAt = time.Time{} } res = append(res, &v) } return res } // Merge merges the timespan of two alerts based and overwrites annotations // based on the authoritative timestamp. A new alert is returned, the labels // are assumed to be equal. func (a *Alert) Merge(o *Alert) *Alert { // Let o always be the younger alert. if o.UpdatedAt.Before(a.UpdatedAt) { return o.Merge(a) } res := *o // Always pick the earliest starting time. if a.StartsAt.Before(o.StartsAt) { res.StartsAt = a.StartsAt } // A non-timeout resolved timestamp always rules. // The latest explicit resolved timestamp wins. if a.EndsAt.After(o.EndsAt) && !a.Timeout { res.EndsAt = a.EndsAt } return &res } // A Muter determines whether a given label set is muted. type Muter interface { Mutes(model.LabelSet) bool } // A MuteFunc is a function that implements the Muter interface. type MuteFunc func(model.LabelSet) bool // Mutes implements the Muter interface. func (f MuteFunc) Mutes(lset model.LabelSet) bool { return f(lset) } // A Silence determines whether a given label set is muted. type Silence struct { // A unique identifier across all connected instances. ID string `json:"id"` // A set of matchers determining if a label set is affect // by the silence. Matchers Matchers `json:"matchers"` // Time range of the silence. // // * StartsAt must not be before creation time // * EndsAt must be after StartsAt // * Deleting a silence means to set EndsAt to now // * Time range must not be modified in different ways // // TODO(fabxc): this may potentially be extended by // creation and update timestamps. StartsAt time.Time `json:"startsAt"` EndsAt time.Time `json:"endsAt"` // The last time the silence was updated. UpdatedAt time.Time `json:"updatedAt"` // Information about who created the silence for which reason. CreatedBy string `json:"createdBy"` Comment string `json:"comment,omitempty"` // timeFunc provides the time against which to evaluate // the silence. Used for test injection. now func() time.Time } // Validate returns true iff all fields of the silence have valid values. func (s *Silence) Validate() error { if s.ID == "" { return fmt.Errorf("ID missing") } if len(s.Matchers) == 0 { return fmt.Errorf("at least one matcher required") } for _, m := range s.Matchers { if err := m.Validate(); err != nil { return fmt.Errorf("invalid matcher: %s", err) } } if s.StartsAt.IsZero() { return fmt.Errorf("start time missing") } if s.EndsAt.IsZero() { return fmt.Errorf("end time missing") } if s.EndsAt.Before(s.StartsAt) { return fmt.Errorf("start time must be before end time") } if s.CreatedBy == "" { return fmt.Errorf("creator information missing") } if s.Comment == "" { return fmt.Errorf("comment missing") } // if s.CreatedAt.IsZero() { // return fmt.Errorf("creation timestamp missing") // } return nil } // Init initializes a silence. Must be called before using Mutes. func (s *Silence) Init() error { for _, m := range s.Matchers { if err := m.Init(); err != nil { return err } } sort.Sort(s.Matchers) return nil } // Mutes implements the Muter interface. // // TODO(fabxc): consider making this a function accepting a // timestamp and returning a Muter, i.e. s.Muter(ts).Mutes(lset). func (s *Silence) Mutes(lset model.LabelSet) bool { var now time.Time if s.now != nil { now = s.now() } else { now = time.Now() } if now.Before(s.StartsAt) || now.After(s.EndsAt) { return false } return s.Matchers.Match(lset) } // Deleted returns whether a silence is deleted. Semantically this means it had no effect // on history at any point. func (s *Silence) Deleted() bool { return s.StartsAt.Equal(s.EndsAt) } prometheus-alertmanager-0.6.2+ds/types/types_test.go000066400000000000000000000027211314512360300226520ustar00rootroot00000000000000// Copyright 2015 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package types import ( "reflect" "testing" "time" "github.com/prometheus/common/model" ) func TestAlertMerge(t *testing.T) { now := time.Now() pairs := []struct { A, B, Res *Alert }{ { A: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-time.Minute), EndsAt: now.Add(2 * time.Minute), }, UpdatedAt: now, Timeout: true, }, B: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-time.Minute), EndsAt: now.Add(3 * time.Minute), }, UpdatedAt: now.Add(time.Minute), Timeout: true, }, Res: &Alert{ Alert: model.Alert{ StartsAt: now.Add(-time.Minute), EndsAt: now.Add(3 * time.Minute), }, UpdatedAt: now.Add(time.Minute), Timeout: true, }, }, } for _, p := range pairs { if res := p.A.Merge(p.B); !reflect.DeepEqual(p.Res, res) { t.Errorf("unexpected merged alert %#v", res) } } } prometheus-alertmanager-0.6.2+ds/ui/000077500000000000000000000000001314512360300173675ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/ui/app/000077500000000000000000000000001314512360300201475ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/ui/app/css/000077500000000000000000000000001314512360300207375ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/ui/app/css/main.css000066400000000000000000000103651314512360300224020ustar00rootroot00000000000000body { overflow-y: scroll; background: #fafafa; } header { padding: 1em 2.5em 1.4em 2.5em; background: #404040; } header #logo { color: #fff; float: left; margin: 0 80px 0 0; } header #logo a { color: inherit; text-decoration: none; } a.gen-link button { color: #fff !important; } #top-nav a { color: #bbb; text-decoration: none } #top-nav .selected a { color: #fff; } #top-nav a:hover { color: #fff; } #top-nav ul { float: left; list-style: none; margin: 8px 0 0 0 ; } #top-nav li { display: inline-block; margin-right: 2.2em; } #content { padding: 2.5em; } #silence-create, #filter-alerts { background: #fff; width: 100%; min-width: 400px; } #alert-groups.hide-silenced .alert-item.silenced { display: none; } .alert-group { margin-bottom: 5px; } .alert-item .details { float: left; margin-right: 12px; } .alert-item .expand, .silence-item .expand { opacity: .4; } .alert-item:hover .expand, .silence-item:hover .expand { opacity: 1; } .silence-item .labels, .alert-item .labels { } .silence-item .labels, .alert-item .labels { width: 65%; } .alert-group-header { border-bottom: 1px solid #fff; background: #bfbfbf; padding: .8em; } .silence-item .delete-button, .silence-item .edit-button, .alert-item .silence-button { opacity: 0.25; } .silence-item:hover .delete-button, .silence-item:hover .edit-button, .alert-item:hover .silence-button { opacity: 1.0; } .active-silences, .pending-silences, .elapsed-silences { margin-bottom: 12px; } .alert-item .overview { background: #f0f0f0; padding: .8em; } .silence-item .overview { background: #bfbfbf; padding: .8em; } .silence-item.highlight .overview { background: #dfdfdf; border: 1px solid #2f77d1; } .silence-item .detail, .alert-item .detail { background: #fff; padding: .8em; } .silence-matchers { margin-bottom: 5px; } .silence-matchers .is-regex { font-family: monospace; } #silences-query { margin-bottom: 24px; } .alert-item .silence-alert, .silence-item .edit-silence { padding: 1em 2em; } .list-item { background: #fff; margin-bottom: 1px; } .list-item .container-left { width: 650px; padding: 1em; float: left; background: #777; color: #eee; } .lbl { display: inline-block; font-size: 0.7em; padding: 0 6px; margin: 0 2px 2px 0; font-family: Menlo, Monaco, Consolas, sans-serif; border: 1px solid #ccc; border-radius: 2px; background: #555; color: #fff; } .lbl-highlight { background: #e6522c; } .lbl-outline { color: #555 !important; background: rgba(255, 255, 255, 0.5) !important; border: 1px solid #555 !important; } .muted-lbl { background: #ffe47a; color: #555 !important; } .muted-lbl a { color: #555 !important; } .annotations td { min-width: 200px; white-space: pre-wrap !important; word-break: break-all; word-wrap: break-word; } .table-normal.annotations td { padding: .4em !important; text-overflow: clip; } .list-item .container-right { border-left: 650px solid #777; padding: 1em; } .active-interval { color: #aaa; font-style: italic; } .silence-alert { background: #f0f0f0; border-top: 1px solid #fff; border-bottom: 1px solid #fff; } input.ng-invalid.ng-touched { border-color: #de2c3b; box-shadow: 0 0 0 2px rgba(222, 44, 59, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2) inset; } input.collapse[type=checkbox] { -webkit-appearance: none; width: 1em; height: 1em; border: none; } input.collapse[type=checkbox]:after { content: '▹'; } input.collapse.open[type=checkbox]:after { content: '▿'; } .label.alertname { background: #fff !important; } /**/ input[type="datetime-local"] { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 1.4rem; padding: 0.9rem 1rem; line-height: 1; height: 40px; outline: none; background: #ffffff; border: 1px solid #dfdfdf; border-radius: 2px; margin-bottom: 0; color: rgba(0, 0, 0, 0.85); } /* Routing Tree */ .sans-serif { font: 12px sans-serif; } .node circle { stroke: steelblue; stroke-width: 1.5px; } .node text { font: 10px sans-serif; } .link { fill: none; stroke: #ccc; stroke-width: 1.5px; } .label-input { padding: 2px 0; width: 500px; } .config-yml { border-color: #ddd; height: 760px; padding: 2px 0; width: 450px; font-family: monospace; } .block { display: block; } .inline-block { display: inline-block; } prometheus-alertmanager-0.6.2+ds/ui/app/img/000077500000000000000000000000001314512360300207235ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/ui/app/img/favicon.ico000066400000000000000000000353561314512360300230600ustar00rootroot00000000000000 h6  00 %F(  IDPoQq]zRqRq`}JTrWuWuWuWuSr@4X0U+Q+Q+Q+Q2W3XNn*Q,R,R*Ph5Z+R,R,R*PLl1W+R,R,R*QDfGh-S+Q8\1Vb~Gzd2WTsLl>Ce^{D@( @ *ng$80kPoQpq80U(O*P*P(O4X,D.T(O)O)O)O)O'N5Y68_|b~b~b~b~b~b~`}+GhJkKkKkKkKkKkKkKkKkKkKkJkIiw:#K'N'N'N'N'N'N'N'N'N'N'N'N%L*fhhhhhhhhhhhhg+n|Yw[x[x[x[x[x[x[x[x[x[x[x[xYwV+Q)O)O)P)P)P)P)P)P)P)P)P)P)P)O,R8\'N+Q1V-S,R,R,R,R,R,R,R,R/U/T)P'N?aRqy}-S,R,R,R,R,R,R,R+QDelMmWu*P,R,R,R,R,R,R,R,R-S/T,R,R,R,R,R,R,R,R,R)P\y)P,R,R,R,R,R,R,R,R,R+QCe)O,R,R,R,R,R,R,R,R,R+Q?b+Q,R,R,R,R,R,R,R,R,R*PHij6[+Q,R,R,R,R+R9\2W,R)OeR(Zx)P/U.T,R,R*QVt?a+Q,R(OTs]z*P,R*PsMm(OPo~55Yv-R,R*Qd*P%Qp:^+Q3XAdp{Kl(NUtg1Vu'N$a]z)P,R,R,R,R,R,R,R,R,R,R,R,R,R,R,R+Q<_i)O,R,R,R,R,R,R,R,R,R,R,R,R,R,R,R+Q@czk)O,R,R,R,R,R,R,R,R,R,R,R,R,R,R,R*PNnI4.S,R,R,R,R,R,R,R,R,R,R8\0V,R,R,R)Ok Ce+Q,R,R,R,R,R,R,R,R+QYw:]+Q,R,R+Qos)O,R-S3X,R,R,R,R,R,RwBd+Q,R*QDeO%/T+R4X|5Z,R,R,R,R-SOn*P,R*PJj)P9\_|)P,R,R,R0U`})P*QIik*z'NGh*P,R,R+Q8\{(O+Qv)Oi3X,R,R*PPo)P=`X1VBd+Q,R)O2W_|5`}Sr*P+R3XSr{!U_|)P)PZx<hg)O+QNg~'N?aOS,Rb~>2[yq#n\ $sdY)ZR"???prometheus-alertmanager-0.6.2+ds/ui/app/index.html000066400000000000000000000021341314512360300221440ustar00rootroot00000000000000 AlertManager – Prometheus

Alertmanager

prometheus-alertmanager-0.6.2+ds/ui/app/js/000077500000000000000000000000001314512360300205635ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/ui/app/js/app.js000066400000000000000000000247651314512360300217170ustar00rootroot00000000000000'use strict'; angular.module('am.directives', []); angular.module('am.directives').directive('route', function(RecursionHelper) { return { restrict: 'E', scope: { route: '=' }, templateUrl: 'app/partials/route.html', compile: function(element) { // Use the compile function from the RecursionHelper, // And return the linking function(s) which it returns return RecursionHelper.compile(element); } }; } ); angular.module('am.directives').directive('alert', function() { return { restrict: 'E', scope: { alert: '=', group: '=' }, templateUrl: 'app/partials/alert.html' }; } ); angular.module('am.directives').directive('silence', function() { return { restrict: 'E', scope: { sil: '=' }, templateUrl: 'app/partials/silence.html' }; } ); angular.module('am.directives').directive('silenceForm', function() { return { restrict: 'E', scope: { silence: '=' }, templateUrl: 'app/partials/silence-form.html' }; } ); angular.module('am.services', ['ngResource']); angular.module('am.services').factory('Silence', function($resource) { return $resource('', { id: '@id' }, { 'query': { method: 'GET', url: 'api/v1/silences' }, 'create': { method: 'POST', url: 'api/v1/silences' }, 'get': { method: 'GET', url: 'api/v1/silence/:id' }, 'delete': { method: 'DELETE', url: 'api/v1/silence/:id' } }); } ); angular.module('am.services').factory('Alert', function($resource) { return $resource('', {}, { 'query': { method: 'GET', url: 'api/v1/alerts' } }); } ); angular.module('am.services').factory('AlertGroups', function($resource) { return $resource('', {}, { 'query': { method: 'GET', url: 'api/v1/alerts/groups' } }); } ); angular.module('am.services').factory('Alert', function($resource) { return $resource('', {}, { 'query': { method: 'GET', url: 'api/v1/alerts' } }); } ); angular.module('am.controllers', []); angular.module('am.controllers').controller('NavCtrl', function($scope, $location) { $scope.items = [{ name: 'Silences', url: 'silences' }, { name: 'Alerts', url: 'alerts' }, { name: 'Status', url: 'status' }]; $scope.selected = function(item) { return item.url == $location.path() } } ); angular.module('am.controllers').controller('AlertCtrl', function($scope) { $scope.showDetails = false; $scope.toggleDetails = function() { $scope.showDetails = !$scope.showDetails } $scope.showSilenceForm = false; $scope.toggleSilenceForm = function() { $scope.showSilenceForm = !$scope.showSilenceForm } $scope.silence = { matchers: [] } angular.forEach($scope.alert.labels, function(value, key) { this.push({ name: key, value: value, isRegex: false }); }, $scope.silence.matchers); $scope.$on('silence-created', function(evt) { $scope.toggleSilenceForm(); }); } ); angular.module('am.controllers').controller('AlertsCtrl', function($scope, $location, AlertGroups) { $scope.groups = null; $scope.allReceivers = []; $scope.$watch('receivers', function(recvs) { if (recvs === undefined || angular.equals(recvs, $scope.allReceivers)) { return; } if (recvs) { $location.search('receiver', recvs); } else { $location.search('receiver', null); } }); $scope.notEmpty = function(group) { var ret = false angular.forEach(group.blocks, function(blk) { if (this.indexOf(blk.routeOpts.receiver) >= 0) { var unsilencedAlerts = blk.alerts.filter(function (a) { return (a.status.state != 'suppressed'); }); if (!$scope.hideSilenced && blk.alerts.length > 0 || $scope.hideSilenced && unsilencedAlerts.length > 0) { ret = true } } }, $scope.receivers); return ret; }; $scope.refresh = function() { AlertGroups.query({}, function(data) { $scope.groups = data.data; $scope.allReceivers = []; angular.forEach($scope.groups, function(group) { angular.forEach(group.blocks, function(blk) { if (this.indexOf(blk.routeOpts.receiver) < 0) { this.push(blk.routeOpts.receiver); } }, this); }, $scope.allReceivers); if (!$scope.receivers) { var recvs = angular.copy($scope.allReceivers); if ($location.search()['receiver']) { recvs = angular.copy($location.search()['receiver']); // The selected items must always be an array for multi-option selects. if (!angular.isArray(recvs)) { recvs = [recvs]; } } $scope.receivers = recvs; } }, function(data) { $scope.error = data.data; } ); }; $scope.refresh(); } ); angular.module('am.controllers').controller('SilenceCtrl', function($scope, $location, Silence) { $scope.highlight = $location.search()['hl'] == $scope.sil.id; $scope.showDetails = false; $scope.showSilenceForm = false; $scope.toggleSilenceForm = function() { $scope.showSilenceForm = !$scope.showSilenceForm } $scope.toggleDetails = function() { $scope.showDetails = !$scope.showDetails } var silCopy = angular.copy($scope.sil); $scope.delete = function(id) { Silence.delete({id: id}, function(data) { $scope.$emit('silence-deleted'); }, function(data) { $scope.error = data.data; }); }; } ); angular.module('am.controllers').controller('SilencesCtrl', function($scope, Silence) { $scope.silences = []; $scope.order = "endsAt"; $scope.showForm = false; $scope.toggleForm = function() { $scope.showForm = !$scope.showForm } $scope.refresh = function() { Silence.query({}, function(data) { $scope.silences = data.data || []; var now = new Date; angular.forEach($scope.silences, function(value) { value.endsAt = new Date(value.endsAt); value.startsAt = new Date(value.startsAt); value.updatedAt = new Date(value.updatedAt); value.elapsed = value.endsAt < now; value.pending = value.startsAt > now; value.active = value.startsAt <= now && value.endsAt > now; }); }, function(data) { $scope.error = data.data; } ); }; $scope.$on('silence-created', function(evt) { $scope.refresh(); }); $scope.$on('silence-deleted', function(evt) { $scope.refresh(); }); $scope.refresh(); } ); angular.module('am.controllers').controller('SilenceCreateCtrl', function($scope, Silence) { $scope.error = null; $scope.silence = $scope.silence || {}; if (!$scope.silence.matchers) { $scope.silence.matchers = [{}]; } var origSilence = angular.copy($scope.silence); $scope.reset = function() { var now = new Date(); var end = new Date(); now.setMilliseconds(0); end.setMilliseconds(0); now.setSeconds(0); end.setSeconds(0); end.setHours(end.getHours() + 4) $scope.silence = angular.copy(origSilence); $scope.silence.createdBy = localStorage.creator; if (!origSilence.startsAt || origSilence.elapsed) { $scope.silence.startsAt = now; } if (!origSilence.endsAt || origSilence.elapsed) { $scope.silence.endsAt = end; } }; $scope.reset(); $scope.addMatcher = function() { $scope.silence.matchers.push({}); }; $scope.delMatcher = function(i) { $scope.silence.matchers.splice(i, 1); }; $scope.create = function() { var now = new Date; localStorage.creator = $scope.silence.createdBy; // Go through conditions that go against immutability of historic silences. var createNew = !angular.equals(origSilence.matchers, $scope.silence.matchers); console.log(origSilence, $scope.silence); createNew = createNew || $scope.silence.elapsed; createNew = createNew || ($scope.silence.active && (origSilence.startsAt == $scope.silence.startsAt || origSilence.endsAt == $scope.silence.endsAt)); if (createNew) { $scope.silence.id = undefined; } Silence.create($scope.silence, function(data) { // If the modifications require creating a new silence, // we expire/delete the old one. if (createNew && origSilence.id && !$scope.silence.elapsed) { Silence.delete({id: origSilence.id}, function(data) { // Only trigger reload after after old silence was deleted. $scope.$emit('silence-created'); }, function(data) { console.warn("deleting silence failed", data); $scope.$emit('silence-created'); }); } else { $scope.$emit('silence-created'); } }, function(data) { $scope.error = data.data.error; } ); }; } ); angular.module('am.services').factory('Status', function($resource) { return $resource('', {}, { 'get': { method: 'GET', url: 'api/v1/status' } }); } ); angular.module('am.controllers').controller('StatusCtrl', function($scope, Status) { Status.get({}, function(data) { $scope.config = data.data.config; $scope.versionInfo = data.data.versionInfo; $scope.uptime = data.data.uptime; }, function(data) { console.log(data.data); }) } ); angular.module('am', [ 'ngRoute', 'ngSanitize', 'angularMoment', 'am.controllers', 'am.services', 'am.directives' ]); angular.module('am').config( function($routeProvider) { $routeProvider. when('/alerts', { templateUrl: 'app/partials/alerts.html', controller: 'AlertsCtrl', reloadOnSearch: false }). when('/silences', { templateUrl: 'app/partials/silences.html', controller: 'SilencesCtrl', reloadOnSearch: false }). when('/status', { templateUrl: 'app/partials/status.html', controller: 'StatusCtrl' }). otherwise({ redirectTo: '/alerts' }); } ); prometheus-alertmanager-0.6.2+ds/ui/app/partials/000077500000000000000000000000001314512360300217665ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/ui/app/partials/alert.html000066400000000000000000000033371314512360300237710ustar00rootroot00000000000000
{{ name }} = "{{ value }}"
inhibited
{{ name }}
prometheus-alertmanager-0.6.2+ds/ui/app/partials/alerts.html000066400000000000000000000022051314512360300241450ustar00rootroot00000000000000
Filter
{{ ln }} = '{{ lv }}'
prometheus-alertmanager-0.6.2+ds/ui/app/partials/route.html000066400000000000000000000012711314512360300240130ustar00rootroot00000000000000
{{ m.name }} =~ '{{ m.value }}'
{{ ln }} = '{{ lv }}'
prometheus-alertmanager-0.6.2+ds/ui/app/partials/silence-form.html000066400000000000000000000037061314512360300252450ustar00rootroot00000000000000
Create Define a new silence.
{{ error }}
prometheus-alertmanager-0.6.2+ds/ui/app/partials/silence.html000066400000000000000000000036601314512360300243030ustar00rootroot00000000000000
{{ m.name }} =~ "{{ m.value }}"
creator {{ sil.createdBy }}
comment {{ sil.comment }}
active {{ sil.startsAt | date:'yyyy-MM-dd HH:mm' }}{{ sil.endsAt | date:'yyyy-MM-dd HH:mm' }}
prometheus-alertmanager-0.6.2+ds/ui/app/partials/silences.html000066400000000000000000000027571314512360300244740ustar00rootroot00000000000000
No silences configured

Active

Pending

Elapsed

prometheus-alertmanager-0.6.2+ds/ui/app/partials/status.html000066400000000000000000000015531314512360300242030ustar00rootroot00000000000000

Status

Up since {{ uptime | date:'yyyy-MM-dd HH:mm:ss' }}

Build info

{{ key }} {{ val }}

Config

{{ config }}

Routing Tree

prometheus-alertmanager-0.6.2+ds/ui/lib/000077500000000000000000000000001314512360300201355ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/ui/lib/angular-moment/000077500000000000000000000000001314512360300230635ustar00rootroot00000000000000prometheus-alertmanager-0.6.2+ds/ui/lib/angular-moment/.editorconfig000066400000000000000000000007321314512360300255420ustar00rootroot00000000000000# EditorConfig helps developers define and maintain consistent # coding styles between different editors and IDEs # editorconfig.org root = true [*] # Change these settings to your own preference indent_style = tab indent_size = 4 # We recommend you to keep these unchanged end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [{package.json,bower.json}] indent_style=space indent_size=2 [*.md] trim_trailing_whitespace = false prometheus-alertmanager-0.6.2+ds/ui/lib/angular-moment/.gitignore000066400000000000000000000000761314512360300250560ustar00rootroot00000000000000/.idea /bower_components /node_modules /coverage npm-debug.logprometheus-alertmanager-0.6.2+ds/ui/lib/angular-moment/.jshintrc000066400000000000000000000006311314512360300247100ustar00rootroot00000000000000{ "node": true, "browser": true, "esnext": true, "bitwise": true, "camelcase": true, "curly": true, "eqeqeq": true, "immed": true, "indent": 2, "latedef": true, "newcap": true, "noarg": true, "quotmark": "single", "regexp": true, "undef": true, "unused": true, "strict": true, "trailing": true, "smarttabs": true, "maxdepth": 2, "maxcomplexity": 10, "globals": { "angular": false } } prometheus-alertmanager-0.6.2+ds/ui/lib/angular-moment/CHANGELOG.md000066400000000000000000000301201314512360300246700ustar00rootroot00000000000000# Changelog ## 1.0.0-beta.6 - 2016-04-24 - Support for setting the units of the full date threshold of `am-time-ago` ([#237](https://github.com/urish/angular-moment/pull/237), contributed by [denistrustepain](https://github.com/denistrustepain)) - Add optional arguments `referenceTime` and `formats` to the `amCalendar` filter ([#241](https://github.com/urish/angular-moment/pull/241), contributed by [Nitro-N](https://github.com/Nitro-N)) - Support moment 2.13.x and above ## 1.0.0-beta.5 - 2016-03-18 - Bugfix: `amTimeAgo` shouldn't convert the time to local timezone on the element's `title` attribute ([#226](https://github.com/urish/angular-moment/pull/226), contributed by [stackia](https://github.com/stackia)) - Support moment 2.12.x ## 1.0.0-beta.4 - 2016-02-09 - Add amStartOf and amEndOf filter ([#203](https://github.com/urish/angular-moment/pull/203), contributed by [pratik14](https://github.com/pratik14)) - Support Moment 2.11.x - Happy Year of the Monkey! ## 1.0.0-beta.3 - 2015-11-10 - Support AngularJS 1.5.x - Support for nw.js ([#196](https://github.com/urish/angular-moment/pull/196), contributed by [makkesk8](https://github.com/makkesk8)) - Bugfix: `title` attribute does update when model changes ([#201](https://github.com/urish/angular-moment/pull/201), contributed by [stackia](https://github.com/stackia)) ## 1.0.0-beta.2 - 2015-09-20 - Bugfix: Infinite digest loop when combining `am-time-ago` and `amTimezone` ([#178](https://github.com/urish/angular-moment/issues/178)) - Bugfix: Cannot use angular-moment under webpack ([#108](https://github.com/urish/angular-moment/issues/108)) - Add `amLocal` filter (see [#114](https://github.com/urish/angular-moment/issues/114)) ## 1.0.0-beta.1 - 2015-09-14 !!! BREAKING CHANGE !!! Preprocessors, timezones and input format were removed from am-time-ago and all filters. Use the new `amFromUnix`, `amUtc`, `amUtcOffset`, `amTimezone`, and `amParse` filters instead. Examples: * `