diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index fa11a253200..a3a4f8f1014 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -320,6 +320,7 @@ https://github.com/elastic/beats/compare/v6.2.3...master[Check the HEAD diff] - Release munin and traefik module as beta. {pull}7660[7660] - Add envoyproxy module. {pull}7569[7569] - Release prometheus collector metricset as GA. {pull}7660[7660] +- Add Elasticsearch `cluster_stats` metricset. {pull}7638[7638] *Packetbeat* diff --git a/metricbeat/docs/fields.asciidoc b/metricbeat/docs/fields.asciidoc index 9dd54a257a6..130ed7bdd86 100644 --- a/metricbeat/docs/fields.asciidoc +++ b/metricbeat/docs/fields.asciidoc @@ -3450,6 +3450,114 @@ type: keyword Elasticsearch state id. +-- + +[float] +== cluster.stats fields + +Cluster stats + + + +*`elasticsearch.cluster.stats.status`*:: ++ +-- +type: keyword + +Cluster status (green, yellow, red). + + +-- + +[float] +== nodes fields + +Nodes statistics. + + + +*`elasticsearch.cluster.stats.nodes.count`*:: ++ +-- +type: long + +Total number of nodes in cluster. + + +-- + +*`elasticsearch.cluster.stats.nodes.master`*:: ++ +-- +type: long + +Number of master-eligible nodes in cluster. + + +-- + +*`elasticsearch.cluster.stats.nodes.data`*:: ++ +-- +type: long + +Number of data nodes in cluster. + + +-- + +[float] +== indices fields + +Indices statistics. + + + +*`elasticsearch.cluster.stats.indices.count`*:: ++ +-- +type: long + +Total number of indices in cluster. + + +-- + +[float] +== shards fields + +Shard statistics. + + + +*`elasticsearch.cluster.stats.indices.shards.count`*:: ++ +-- +type: long + +Total number of shards in cluster. + + +-- + +*`elasticsearch.cluster.stats.indices.shards.primaries`*:: ++ +-- +type: long + +Total number of primary shards in cluster. + + +-- + +*`elasticsearch.cluster.stats.indices.fielddata.memory.bytes`*:: ++ +-- +type: long + +Memory used for fielddata. + + -- [float] diff --git a/metricbeat/docs/modules/elasticsearch.asciidoc b/metricbeat/docs/modules/elasticsearch.asciidoc index 86245f7316b..270b3906f43 100644 --- a/metricbeat/docs/modules/elasticsearch.asciidoc +++ b/metricbeat/docs/modules/elasticsearch.asciidoc @@ -44,6 +44,8 @@ This module supports TLS connection when using `ssl` config field, as described The following metricsets are available: +* <> + * <> * <> @@ -60,6 +62,8 @@ The following metricsets are available: * <> +include::elasticsearch/cluster_stats.asciidoc[] + include::elasticsearch/index.asciidoc[] include::elasticsearch/index_recovery.asciidoc[] diff --git a/metricbeat/docs/modules/elasticsearch/cluster_stats.asciidoc b/metricbeat/docs/modules/elasticsearch/cluster_stats.asciidoc new file mode 100644 index 00000000000..c66c9200692 --- /dev/null +++ b/metricbeat/docs/modules/elasticsearch/cluster_stats.asciidoc @@ -0,0 +1,23 @@ +//// +This file is generated! See scripts/docs_collector.py +//// + +[[metricbeat-metricset-elasticsearch-cluster_stats]] +=== Elasticsearch cluster_stats metricset + +beta[] + +include::../../../module/elasticsearch/cluster_stats/_meta/docs.asciidoc[] + + +==== Fields + +For a description of each field in the metricset, see the +<> section. + +Here is an example document generated by this metricset: + +[source,json] +---- +include::../../../module/elasticsearch/cluster_stats/_meta/data.json[] +---- diff --git a/metricbeat/docs/modules_list.asciidoc b/metricbeat/docs/modules_list.asciidoc index 98dcb24ea39..dcc11eed248 100644 --- a/metricbeat/docs/modules_list.asciidoc +++ b/metricbeat/docs/modules_list.asciidoc @@ -33,7 +33,8 @@ This file is generated! See scripts/docs_collector.py |<> beta[] |image:./images/icon-no.png[No prebuilt dashboards] | .1+| .1+| |<> beta[] |<> beta[] |image:./images/icon-no.png[No prebuilt dashboards] | -.8+| .8+| |<> beta[] +.9+| .9+| |<> beta[] +|<> beta[] |<> beta[] |<> beta[] |<> beta[] diff --git a/metricbeat/include/list.go b/metricbeat/include/list.go index 04eadd6aeb8..4ce91bdc716 100644 --- a/metricbeat/include/list.go +++ b/metricbeat/include/list.go @@ -54,6 +54,7 @@ import ( _ "github.com/elastic/beats/metricbeat/module/dropwizard" _ "github.com/elastic/beats/metricbeat/module/dropwizard/collector" _ "github.com/elastic/beats/metricbeat/module/elasticsearch" + _ "github.com/elastic/beats/metricbeat/module/elasticsearch/cluster_stats" _ "github.com/elastic/beats/metricbeat/module/elasticsearch/index" _ "github.com/elastic/beats/metricbeat/module/elasticsearch/index_recovery" _ "github.com/elastic/beats/metricbeat/module/elasticsearch/index_summary" diff --git a/metricbeat/module/elasticsearch/cluster_stats/_meta/data.json b/metricbeat/module/elasticsearch/cluster_stats/_meta/data.json new file mode 100644 index 00000000000..62e4288c715 --- /dev/null +++ b/metricbeat/module/elasticsearch/cluster_stats/_meta/data.json @@ -0,0 +1,43 @@ +{ + "@timestamp": "2017-10-12T08:05:34.853Z", + "beat": { + "hostname": "host.example.com", + "name": "host.example.com" + }, + "elasticsearch": { + "cluster": { + "id": "6UTQ_iuNSzWP49zv99vxDg", + "name": "elasticsearch", + "stats": { + "indices": { + "fielddata": { + "memory": { + "bytes": 1208 + } + }, + "shards": { + "count": 18, + "primaries": 18 + }, + "total": 18 + }, + "nodes": { + "count": 1, + "data": 1, + "master": 1 + }, + "status": "yellow" + } + } + }, + "metricset": { + "host": "127.0.0.1:9200", + "module": "elasticsearch", + "name": "cluster_stats", + "namespace": "elasticsearch.cluster.stats", + "rtt": 115 + }, + "service": { + "name": "elasticsearch" + } +} \ No newline at end of file diff --git a/metricbeat/module/elasticsearch/cluster_stats/_meta/docs.asciidoc b/metricbeat/module/elasticsearch/cluster_stats/_meta/docs.asciidoc new file mode 100644 index 00000000000..1b685ab5860 --- /dev/null +++ b/metricbeat/module/elasticsearch/cluster_stats/_meta/docs.asciidoc @@ -0,0 +1,3 @@ +This is the `cluster_stats` metricset of the Elasticsearch module. It interrogates the +https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-stats.html[Cluster Stats API endpoint] +to fetch information about the Elasticsearch cluster. diff --git a/metricbeat/module/elasticsearch/cluster_stats/_meta/fields.yml b/metricbeat/module/elasticsearch/cluster_stats/_meta/fields.yml new file mode 100644 index 00000000000..a4271342576 --- /dev/null +++ b/metricbeat/module/elasticsearch/cluster_stats/_meta/fields.yml @@ -0,0 +1,53 @@ +- name: cluster.stats + type: group + description: > + Cluster stats + release: beta + fields: + - name: status + type: keyword + description: > + Cluster status (green, yellow, red). + - name: nodes + type: group + description: > + Nodes statistics. + fields: + - name: count + type: long + description: > + Total number of nodes in cluster. + - name: master + type: long + description: > + Number of master-eligible nodes in cluster. + - name: data + type: long + description: > + Number of data nodes in cluster. + - name: indices + type: group + description: > + Indices statistics. + fields: + - name: count + type: long + description: > + Total number of indices in cluster. + - name: shards + type: group + description: > + Shard statistics. + fields: + - name: count + type: long + description: > + Total number of shards in cluster. + - name: primaries + type: long + description: > + Total number of primary shards in cluster. + - name: fielddata.memory.bytes + type: long + description: > + Memory used for fielddata. diff --git a/metricbeat/module/elasticsearch/cluster_stats/_meta/test/cluster_stats.630.json b/metricbeat/module/elasticsearch/cluster_stats/_meta/test/cluster_stats.630.json new file mode 100644 index 00000000000..82156112343 --- /dev/null +++ b/metricbeat/module/elasticsearch/cluster_stats/_meta/test/cluster_stats.630.json @@ -0,0 +1,170 @@ +{ + "_nodes":{ + "total":1, + "successful":1, + "failed":0 + }, + "cluster_name":"docker-cluster", + "timestamp":1532386151704, + "status":"yellow", + "indices":{ + "count":1, + "shards":{ + "total":4, + "primaries":4, + "replication":0.0, + "index":{ + "shards":{ + "min":4, + "max":4, + "avg":4.0 + }, + "primaries":{ + "min":4, + "max":4, + "avg":4.0 + }, + "replication":{ + "min":0.0, + "max":0.0, + "avg":0.0 + } + } + }, + "docs":{ + "count":2, + "deleted":0 + }, + "store":{ + "size_in_bytes":6024 + }, + "fielddata":{ + "memory_size_in_bytes":0, + "evictions":0 + }, + "query_cache":{ + "memory_size_in_bytes":0, + "total_count":0, + "hit_count":0, + "miss_count":0, + "cache_size":0, + "cache_count":0, + "evictions":0 + }, + "completion":{ + "size_in_bytes":0 + }, + "segments":{ + "count":2, + "memory_in_bytes":1342, + "terms_memory_in_bytes":578, + "stored_fields_memory_in_bytes":624, + "term_vectors_memory_in_bytes":0, + "norms_memory_in_bytes":0, + "points_memory_in_bytes":4, + "doc_values_memory_in_bytes":136, + "index_writer_memory_in_bytes":0, + "version_map_memory_in_bytes":0, + "fixed_bit_set_memory_in_bytes":0, + "max_unsafe_auto_id_timestamp":-1, + "file_sizes":{ + + } + } + }, + "nodes":{ + "count":{ + "total":1, + "data":1, + "coordinating_only":0, + "master":1, + "ingest":1 + }, + "versions":[ + "6.3.0" + ], + "os":{ + "available_processors":4, + "allocated_processors":4, + "names":[ + { + "name":"Linux", + "count":1 + } + ], + "mem":{ + "total_in_bytes":2095771648, + "free_in_bytes":66191360, + "used_in_bytes":2029580288, + "free_percent":3, + "used_percent":97 + } + }, + "process":{ + "cpu":{ + "percent":1 + }, + "open_file_descriptors":{ + "min":256, + "max":256, + "avg":256 + } + }, + "jvm":{ + "max_uptime_in_millis":220179, + "versions":[ + { + "version":"10.0.1", + "vm_name":"OpenJDK 64-Bit Server VM", + "vm_version":"10.0.1+10", + "vm_vendor":"Oracle Corporation", + "count":1 + } + ], + "mem":{ + "heap_used_in_bytes":412775576, + "heap_max_in_bytes":1038876672 + }, + "threads":45 + }, + "fs":{ + "total_in_bytes":62725623808, + "free_in_bytes":40728297472, + "available_in_bytes":37511581696 + }, + "plugins":[ + { + "name":"ingest-user-agent", + "version":"6.3.0", + "elasticsearch_version":"6.3.0", + "java_version":"1.8", + "description":"Ingest processor that extracts information from a user agent", + "classname":"org.elasticsearch.ingest.useragent.IngestUserAgentPlugin", + "extended_plugins":[ + + ], + "has_native_controller":false + }, + { + "name":"ingest-geoip", + "version":"6.3.0", + "elasticsearch_version":"6.3.0", + "java_version":"1.8", + "description":"Ingest processor that uses looksup geo data based on ip adresses using the Maxmind geo database", + "classname":"org.elasticsearch.ingest.geoip.IngestGeoIpPlugin", + "extended_plugins":[ + + ], + "has_native_controller":false + } + ], + "network_types":{ + "transport_types":{ + "security4":1 + }, + "http_types":{ + "security4":1 + } + } + } + } diff --git a/metricbeat/module/elasticsearch/cluster_stats/_meta/test/cluster_stats.700.json b/metricbeat/module/elasticsearch/cluster_stats/_meta/test/cluster_stats.700.json new file mode 100644 index 00000000000..50180bfdc37 --- /dev/null +++ b/metricbeat/module/elasticsearch/cluster_stats/_meta/test/cluster_stats.700.json @@ -0,0 +1,147 @@ +{ + "_nodes":{ + "total":2, + "successful":2, + "failed":0 + }, + "cluster_name":"elasticsearch", + "timestamp":1532433421874, + "status":"yellow", + "indices":{ + "count":4, + "shards":{ + "total":16, + "primaries":8, + "replication":1, + "index":{ + "shards":{ + "min":2, + "max":10, + "avg":4 + }, + "primaries":{ + "min":1, + "max":5, + "avg":2 + }, + "replication":{ + "min":1, + "max":1, + "avg":1 + } + } + }, + "docs":{ + "count":145, + "deleted":0 + }, + "store":{ + "size_in_bytes":1030085 + }, + "fielddata":{ + "memory_size_in_bytes":0, + "evictions":0 + }, + "query_cache":{ + "memory_size_in_bytes":0, + "total_count":0, + "hit_count":0, + "miss_count":0, + "cache_size":0, + "cache_count":0, + "evictions":0 + }, + "completion":{ + "size_in_bytes":0 + }, + "segments":{ + "count":26, + "memory_in_bytes":108394, + "terms_memory_in_bytes":83123, + "stored_fields_memory_in_bytes":8112, + "term_vectors_memory_in_bytes":0, + "norms_memory_in_bytes":0, + "points_memory_in_bytes":3047, + "doc_values_memory_in_bytes":14112, + "index_writer_memory_in_bytes":0, + "version_map_memory_in_bytes":0, + "fixed_bit_set_memory_in_bytes":0, + "max_unsafe_auto_id_timestamp":1532433381676, + "file_sizes":{ + + } + } + }, + "nodes":{ + "count":{ + "total":2, + "data":2, + "coordinating_only":0, + "master":2, + "ingest":2 + }, + "versions":[ + "7.0.0-alpha1" + ], + "os":{ + "available_processors":16, + "allocated_processors":16, + "names":[ + { + "name":"Mac OS X", + "count":2 + } + ], + "mem":{ + "total_in_bytes":34359738368, + "free_in_bytes":735543296, + "used_in_bytes":33624195072, + "free_percent":2, + "used_percent":98 + } + }, + "process":{ + "cpu":{ + "percent":0 + }, + "open_file_descriptors":{ + "min":364, + "max":371, + "avg":367 + } + }, + "jvm":{ + "max_uptime_in_millis":228721, + "versions":[ + { + "version":"10.0.1", + "vm_name":"Java HotSpot(TM) 64-Bit Server VM", + "vm_version":"10.0.1+10", + "vm_vendor":"Oracle Corporation", + "count":2 + } + ], + "mem":{ + "heap_used_in_bytes":420771816, + "heap_max_in_bytes":2075918336 + }, + "threads":164 + }, + "fs":{ + "total_in_bytes":499963170816, + "free_in_bytes":410995167232, + "available_in_bytes":408143978496 + }, + "plugins":[ + + ], + "network_types":{ + "transport_types":{ + "security4":2 + }, + "http_types":{ + "security4":2 + } + } + } + } diff --git a/metricbeat/module/elasticsearch/cluster_stats/cluster_stats.go b/metricbeat/module/elasticsearch/cluster_stats/cluster_stats.go new file mode 100644 index 00000000000..c6d848e7c1a --- /dev/null +++ b/metricbeat/module/elasticsearch/cluster_stats/cluster_stats.go @@ -0,0 +1,77 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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 cluster_stats + +import ( + "fmt" + + "github.com/elastic/beats/libbeat/common/cfgwarn" + "github.com/elastic/beats/libbeat/logp" + "github.com/elastic/beats/metricbeat/mb" + "github.com/elastic/beats/metricbeat/module/elasticsearch" +) + +func init() { + mb.Registry.MustAddMetricSet("elasticsearch", "cluster_stats", New, + mb.WithHostParser(elasticsearch.HostParser), + mb.WithNamespace("elasticsearch.cluster.stats"), + ) +} + +const ( + clusterStatsPath = "/_cluster/stats" +) + +// MetricSet defines all fields of the MetricSet +type MetricSet struct { + *elasticsearch.MetricSet +} + +// New create a new instance of the MetricSet +func New(base mb.BaseMetricSet) (mb.MetricSet, error) { + cfgwarn.Beta("The elasticsearch cluster_stats metricset is beta") + + ms, err := elasticsearch.NewMetricSet(base, clusterStatsPath) + if err != nil { + return nil, err + } + return &MetricSet{MetricSet: ms}, nil +} + +// Fetch methods implements the data gathering and data conversion to the right format +func (m *MetricSet) Fetch(r mb.ReporterV2) { + isMaster, err := elasticsearch.IsMaster(m.HTTP, m.HostData().SanitizedURI+clusterStatsPath) + if err != nil { + r.Error(fmt.Errorf("Error fetching master info: %s", err)) + return + } + + // Not master, no event sent + if !isMaster { + logp.Debug("elasticsearch", "Trying to fetch index recovery stats from a non master node.") + return + } + + content, err := m.HTTP.FetchContent() + if err != nil { + r.Error(err) + return + } + + eventMapping(r, content) +} diff --git a/metricbeat/module/elasticsearch/cluster_stats/data.go b/metricbeat/module/elasticsearch/cluster_stats/data.go new file mode 100644 index 00000000000..9f16b4954d4 --- /dev/null +++ b/metricbeat/module/elasticsearch/cluster_stats/data.go @@ -0,0 +1,96 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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 cluster_stats + +import ( + "encoding/json" + "fmt" + + "github.com/elastic/beats/libbeat/common" + + s "github.com/elastic/beats/libbeat/common/schema" + c "github.com/elastic/beats/libbeat/common/schema/mapstriface" + "github.com/elastic/beats/metricbeat/mb" +) + +var ( + schema = s.Schema{ + "status": c.Str("status"), + "nodes": c.Dict("nodes", s.Schema{ + "count": c.Int("count.total"), + "master": c.Int("count.master"), + "data": c.Int("count.data"), + }), + "indices": c.Dict("indices", s.Schema{ + "total": c.Int("count"), + "shards": c.Dict("shards", s.Schema{ + "count": c.Int("total"), + "primaries": c.Int("primaries"), + }), + "fielddata": c.Dict("fielddata", s.Schema{ + "memory": s.Object{ + "bytes": c.Int("memory_size_in_bytes"), + }, + }), + }), + } +) + +// TODO: Remove this function and use the one implemented (currently) in the kibana +// module, after extracting it into the metricbeat helper package +func reportErrorForMissingField(field string, r mb.ReporterV2) error { + err := fmt.Errorf("Could not find field '%v' in Kibana stats API response", field) + r.Error(err) + return err +} + +func eventMapping(r mb.ReporterV2, content []byte) error { + var data map[string]interface{} + err := json.Unmarshal(content, &data) + if err != nil { + r.Error(err) + return err + } + + metricSetFields, err := schema.Apply(data) + if err != nil { + r.Error(err) + return err + } + + clusterName, ok := data["cluster_name"] + if !ok { + return reportErrorForMissingField("cluster_name", r) + } + + var event mb.Event + event.RootFields = common.MapStr{} + event.RootFields.Put("service.name", "elasticsearch") + + event.ModuleFields = common.MapStr{} + event.ModuleFields.Put("cluster.name", clusterName) + clusterUUID, ok := data["cluster_uuid"] + if ok { + event.ModuleFields.Put("cluster.id", clusterUUID) + } + + event.MetricSetFields = metricSetFields + + r.Event(event) + return nil +} diff --git a/metricbeat/module/elasticsearch/cluster_stats/data_test.go b/metricbeat/module/elasticsearch/cluster_stats/data_test.go new file mode 100644 index 00000000000..3fe38b8d0c9 --- /dev/null +++ b/metricbeat/module/elasticsearch/cluster_stats/data_test.go @@ -0,0 +1,30 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +// +build !integration + +package cluster_stats + +import ( + "testing" + + "github.com/elastic/beats/metricbeat/module/elasticsearch" +) + +func TestMapper(t *testing.T) { + elasticsearch.TestMapper(t, "./_meta/test/cluster_stats.*.json", eventMapping) +} diff --git a/metricbeat/module/elasticsearch/elasticsearch_integration_test.go b/metricbeat/module/elasticsearch/elasticsearch_integration_test.go index f14563806b7..abe20e13b1e 100644 --- a/metricbeat/module/elasticsearch/elasticsearch_integration_test.go +++ b/metricbeat/module/elasticsearch/elasticsearch_integration_test.go @@ -34,6 +34,7 @@ import ( "bytes" + _ "github.com/elastic/beats/metricbeat/module/elasticsearch/cluster_stats" _ "github.com/elastic/beats/metricbeat/module/elasticsearch/index" _ "github.com/elastic/beats/metricbeat/module/elasticsearch/index_recovery" _ "github.com/elastic/beats/metricbeat/module/elasticsearch/index_summary" @@ -44,6 +45,7 @@ import ( ) var metricSets = []string{ + "cluster_stats", "index", "index_recovery", "index_summary", diff --git a/metricbeat/module/elasticsearch/fields.go b/metricbeat/module/elasticsearch/fields.go index b917d9823af..6ad6d79844d 100644 --- a/metricbeat/module/elasticsearch/fields.go +++ b/metricbeat/module/elasticsearch/fields.go @@ -31,5 +31,5 @@ func init() { // Asset returns asset data func Asset() string { - return "eJzsW92O2zYTvfdTDPZ+9QC++G4+NO0WSBo0mwJFUTi0OLa5yx+FpJx1n74gJXklirJkWRs5qHW3sn3OmeHMcIbS3sMzHpaAnBjLUoNEp7sFgGWW4xLufqrfv1sAUDSpZpllSi7hfwsAgMZ3QCiac1wAaORIDC5hjZYsAAxay+TWLOGvO2P43d/u3k5pu0qV3LDtEjaEG/fLDUNOzdKD34MkAtsC3WUPGS5hq1WelXci6ppwdciU58aiTtxfxw8r1Gc8fFOa1u5HsYur6YES17Mki05aRt+ClNETlMYSixMTe0xPG7IySfGlRVVfrh6iECCMqOoKF7guIljcUyb3qHHXg1NULmyMzSpLeJQuNLtLdx2NqtQkqcqlbX2lguVKbiMf9pjhrkcnFWQu1qhBbRxZLlBaA0yC3WHh/eS0OIocLYZOfBN5BdPZMo1VGhPD/sFkfbBozpW6UVoQu4SuHw82xUlwhhw1OwM86gnxuPW2frcgKIQdafuFCRRKH2Z27av+Qg7kBimsD97XldSIu+MVK9GYqj3qw9yli4V51enNHk992hFNoTKrUavrfA5+ulIZkDqkOG2mmSANb78yr5XiSOR5zI86R2CbChiMUxLnNpZsJ7T598pajxsJtDq3JXqLNulY5VH8jx4SpKLYvcoF7U6ZsKAUxCzcpc7gdKBAKNVozEn2aXfluoTm3hxfdJXrFCd1/CcPedrxJe1kjq9z9ju+ZJ/W8XUJTcc3K6rJRZDisxTUoiCw1mZz69AmkXfr0G4dWq/+8zo0uM1Wt8y9Ze4PmLmVNYInT2p9yc4v+NvMUaM6ns+Sfc0RBIcnte5u9SyxE7ZZv6p1ARlno8SSlY9ik2RapWgM0pWbvDRdxaJ77Bz5sQL3lID7diDHNDG5J5zRFSUWJ9XzuMNafBYGG/jG7A6Q2R1qICCYMUxunSAsogSUu+//tjtiIVU5pyCVhTVCRrRBGhkgWoHtmt5Lwjr4/fxnmx/aXXydbI/aMBWO4pfylahxyqe9GLzf96XQH+/hQW5UWHb72oS41X2WDxBUiYo6oK6g3AV2SLKESWZn2w9+QZKBU9DYApwN/Ztt3QhBXua1QZCX8SZIJedfig9K3k+wHJUtc67I0ZThq/I6zvvtKBFcpc+Ex2eDUSeID5sKHBw2UqekdFp0I/BP1syl28EqRBnV7UjK0jOOOIZU6RITQn1dWmCuQQtfmLF+u69Gmesdsq5oNPnRhpJq3us5OYAJZ9VRs6gn7aliT3vhnJpkSvHJstYVz3Jec7ijElfx7rSISRropd84bUtrr1u3vrrGUzsX9C0pDAjJ0KiOrwC8Jy9dIVgXnCF5vhLFH5E8D5W8uh5He9limLddP3Elsj8Xrc3JGnVQeVTIxTn3pwO+Zd01KL5l3bVlncn1nu2VfovE+1Ri33LvGhTfcm/u3OvsgLdpkirOMbVKT9YF//x/OILGs25AD1zpOnUKeGE7/MoA23RsYeiaFWHIog8QeixAoyOrh6TRBb2x14uG6L/t92gubkzkZR24JAnfMY5gDsaigDh0XxL6B/+znDgcvaJxnkfclQCyJ4yTNf++KsJ/H8hQUia3K0vM8yKkPuOs80sM8AukSlrCpAEC5QfgPqgj1ZN03OGoQW1XSlMM+72xDyEfPCS0IWtvnSnNbDyhxjyEjcA13/KLMl3wgl8C75QGfCEi486g3N4LkmUskH7MVyZwxeTqa445Jq26NfppLxP+LM3DtmLUv+R7SVB6gDJ4LoyyN3uj2e6YAWb82eKAt5uLA96p3N942O6VBNTxiJz2VYhHf6pKLIYi/g0AAP//s2UyNg==" + return "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" }