diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index 6e585dcceec..86da7a16904 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -54,6 +54,7 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff] - Allow metricsets to report their status via control v2 protocol. {pull}40025[40025] - Remove fallback to the node limit for the `kubernetes.pod.cpu.usage.limit.pct` and `kubernetes.pod.memory.usage.limit.pct` metrics calculation - Add support for Kibana status metricset in v8 format {pull}40275[40275] +- Add new metrics for the vSphere Datastore metricset. {pull}40441[40441] - Update metrics for the vSphere Host metricset. {pull}40429[40429] - Added back `elasticsearch.node.stats.jvm.mem.pools.*` to the `node_stats` metricset {pull}40571[40571] diff --git a/metricbeat/docs/fields.asciidoc b/metricbeat/docs/fields.asciidoc index 2ebe8a22783..57357afaadc 100644 --- a/metricbeat/docs/fields.asciidoc +++ b/metricbeat/docs/fields.asciidoc @@ -66861,54 +66861,108 @@ datastore -*`vsphere.datastore.name`*:: +*`vsphere.datastore.capacity.free.bytes`*:: + -- -Datastore name +Free bytes of the datastore. -type: keyword +type: long + +format: bytes + +-- + +*`vsphere.datastore.capacity.total.bytes`*:: ++ +-- +Total bytes of the datastore. + + +type: long + +format: bytes + +-- + +*`vsphere.datastore.capacity.used.bytes`*:: ++ +-- +Used bytes of the datastore. + + +type: long + +format: bytes + +-- + +*`vsphere.datastore.capacity.used.pct`*:: ++ +-- +Percentage of datastore capacity used. + + +type: scaled_float + +format: percent -- *`vsphere.datastore.fstype`*:: + -- -Filesystem type +Filesystem type. type: keyword -- -*`vsphere.datastore.capacity.total.bytes`*:: +*`vsphere.datastore.host.count`*:: + -- -Total bytes of the datastore +Number of hosts. type: long -format: bytes +-- +*`vsphere.datastore.host.names`*:: ++ -- +List of all the host names. -*`vsphere.datastore.capacity.free.bytes`*:: + +type: keyword + +-- + +*`vsphere.datastore.iops`*:: + -- -Free bytes of the datastore +Storage I/O Control aggregated Input/Output Operations Per Second. type: long -format: bytes +-- +*`vsphere.datastore.name`*:: ++ -- +Datastore name. -*`vsphere.datastore.capacity.used.bytes`*:: + +type: keyword + +-- + +*`vsphere.datastore.read.bytes`*:: + -- -Used bytes of the datastore +Rate of reading data from the datastore. type: long @@ -66917,15 +66971,65 @@ format: bytes -- -*`vsphere.datastore.capacity.used.pct`*:: +*`vsphere.datastore.read.latency.total.ms`*:: + -- -Used percent of the datastore +Average amount of time for a read operation from the datastore in milliseconds. -type: scaled_float +type: long -format: percent +-- + +*`vsphere.datastore.status`*:: ++ +-- +Status of the datastore. + + +type: keyword + +-- + +*`vsphere.datastore.vm.count`*:: ++ +-- +Number of VMs. + + +type: long + +-- + +*`vsphere.datastore.vm.names`*:: ++ +-- +List of all the VM names. + + +type: keyword + +-- + +*`vsphere.datastore.write.bytes`*:: ++ +-- +Rate of writing data to the datastore. + + +type: long + +format: bytes + +-- + +*`vsphere.datastore.write.latency.total.ms`*:: ++ +-- +Average amount of time for a write operation from the datastore in milliseconds. + + +type: long -- diff --git a/metricbeat/module/vsphere/datastore/_meta/data.json b/metricbeat/module/vsphere/datastore/_meta/data.json index 944ecb1189f..48d6940708a 100644 --- a/metricbeat/module/vsphere/datastore/_meta/data.json +++ b/metricbeat/module/vsphere/datastore/_meta/data.json @@ -1,34 +1,64 @@ { - "@timestamp": "2017-10-12T08:05:34.853Z", - "event": { - "dataset": "vsphere.datastore", - "duration": 115000, - "module": "vsphere" - }, - "metricset": { - "name": "datastore", - "period": 10000 - }, - "service": { - "address": "127.0.0.1:33365", - "type": "vsphere" - }, - "vsphere": { - "datastore": { - "capacity": { - "free": { - "bytes": 37120094208 + "@timestamp": "2017-10-12T08:05:34.853Z", + "event": { + "dataset": "vsphere.datastore", + "duration": 115000, + "module": "vsphere" + }, + "metricset": { + "name": "datastore", + "period": 10000 + }, + "service": { + "address": "127.0.0.1:33365", + "type": "vsphere" + }, + "vsphere": { + "datastore": { + "iops": 0, + "host": { + "count": 1, + "names": [ + "DC3_H0" + ] + }, + "status": "green", + "vm": { + "count": 6, + "names": [ + "DC3_H0_VM0" + ] + }, + "capacity": { + "free": { + "bytes": 37120094208 + }, + "total": { + "bytes": 74686664704 + }, + "used": { + "bytes": 37566570496, + "pct": 0.502988996026061 + }, + "read": { + "bytes": 0, + "latency": { + "total": { + "ms": 0 + } + } }, - "total": { - "bytes": 74686664704 - }, - "used": { - "bytes": 37566570496, - "pct": 0.502988996026061 + "write": { + "bytes": 337000, + "latency": { + "total": { + "ms": 0 + } + } } - }, - "fstype": "local", - "name": "LocalDS_0" - } - } + }, + "fstype": "local", + "name": "LocalDS_0" + } + } } \ No newline at end of file diff --git a/metricbeat/module/vsphere/datastore/_meta/fields.yml b/metricbeat/module/vsphere/datastore/_meta/fields.yml index 632bb13cfe2..e9fb22a486d 100644 --- a/metricbeat/module/vsphere/datastore/_meta/fields.yml +++ b/metricbeat/module/vsphere/datastore/_meta/fields.yml @@ -4,32 +4,74 @@ datastore release: ga fields: - - name: name - type: keyword - description: > - Datastore name - - name: fstype - type: keyword - description: > - Filesystem type - - name: capacity.total.bytes + - name: capacity.free.bytes type: long description: > - Total bytes of the datastore + Free bytes of the datastore. format: bytes - - name: capacity.free.bytes + - name: capacity.total.bytes type: long description: > - Free bytes of the datastore + Total bytes of the datastore. format: bytes - name: capacity.used.bytes type: long description: > - Used bytes of the datastore + Used bytes of the datastore. format: bytes - name: capacity.used.pct type: scaled_float description: > - Used percent of the datastore + Percentage of datastore capacity used. format: percent + - name: fstype + type: keyword + description: > + Filesystem type. + - name: host.count + type: long + description: > + Number of hosts. + - name: host.names + type: keyword + description: > + List of all the host names. + - name: iops + type: long + description: > + Storage I/O Control aggregated Input/Output Operations Per Second. + - name: name + type: keyword + description: > + Datastore name. + - name: read.bytes + type: long + description: > + Rate of reading data from the datastore. + format: bytes + - name: read.latency.total.ms + type: long + description: > + Average amount of time for a read operation from the datastore in milliseconds. + - name: status + type: keyword + description: > + Status of the datastore. + - name: vm.count + type: long + description: > + Number of VMs. + - name: vm.names + type: keyword + description: > + List of all the VM names. + - name: write.bytes + type: long + description: > + Rate of writing data to the datastore. + format: bytes + - name: write.latency.total.ms + type: long + description: > + Average amount of time for a write operation from the datastore in milliseconds. diff --git a/metricbeat/module/vsphere/datastore/data.go b/metricbeat/module/vsphere/datastore/data.go new file mode 100644 index 00000000000..90820f7f5d5 --- /dev/null +++ b/metricbeat/module/vsphere/datastore/data.go @@ -0,0 +1,86 @@ +// 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 datastore + +import ( + "github.com/vmware/govmomi/vim25/mo" + + "github.com/elastic/elastic-agent-libs/mapstr" +) + +func (m *MetricSet) eventMapping(ds mo.Datastore, data *metricData) mapstr.M { + event := mapstr.M{ + "name": ds.Summary.Name, + "fstype": ds.Summary.Type, + "status": ds.OverallStatus, + "host": mapstr.M{ + "count": len(data.assetNames.outputHsNames), + }, + "vm": mapstr.M{ + "count": len(data.assetNames.outputVmNames), + }, + "capacity": mapstr.M{ + "total": mapstr.M{ + "bytes": ds.Summary.Capacity, + }, + "free": mapstr.M{ + "bytes": ds.Summary.FreeSpace, + }, + "used": mapstr.M{ + "bytes": ds.Summary.Capacity - ds.Summary.FreeSpace, + }, + }, + } + + if ds.Summary.Capacity > 0 { + usedSpacePercent := float64(ds.Summary.Capacity-ds.Summary.FreeSpace) / float64(ds.Summary.Capacity) + event.Put("capacity.used.pct", usedSpacePercent) + } + + if len(data.assetNames.outputHsNames) > 0 { + event.Put("host.names", data.assetNames.outputHsNames) + } + + if len(data.assetNames.outputVmNames) > 0 { + event.Put("vm.names", data.assetNames.outputVmNames) + } + + mapPerfMetricToEvent(event, data.perfMetrics) + + return event +} + +func mapPerfMetricToEvent(event mapstr.M, perfMetricMap map[string]interface{}) { + const bytesMultiplier = 1000 + + if val, exist := perfMetricMap["datastore.read.average"]; exist { + event.Put("read.bytes", val.(int64)*bytesMultiplier) + } + if val, exist := perfMetricMap["datastore.totalReadLatency.average"]; exist { + event.Put("read.latency.total.ms", val) + } + if val, exist := perfMetricMap["datastore.write.average"]; exist { + event.Put("write.bytes", val.(int64)*bytesMultiplier) + } + if val, exist := perfMetricMap["datastore.totalWriteLatency.average"]; exist { + event.Put("write.latency.total.ms", val) + } + if val, exist := perfMetricMap["datastore.datastoreIops.average"]; exist { + event.Put("iops", val) + } +} diff --git a/metricbeat/module/vsphere/datastore/data_test.go b/metricbeat/module/vsphere/datastore/data_test.go new file mode 100644 index 00000000000..d57d2ab1319 --- /dev/null +++ b/metricbeat/module/vsphere/datastore/data_test.go @@ -0,0 +1,108 @@ +// 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 datastore + +import ( + "testing" + + "github.com/elastic/elastic-agent-libs/mapstr" + + "github.com/stretchr/testify/assert" + "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/types" +) + +func TestEventMapping(t *testing.T) { + var m *MetricSet + var datastoreTest = mo.Datastore{ + Summary: types.DatastoreSummary{ + Name: "datastore-test", + Type: "local", + Capacity: 5000000, + FreeSpace: 5000000, + }, + ManagedEntity: mo.ManagedEntity{ + OverallStatus: "green", + }, + Host: []types.DatastoreHostMount{}, + Vm: []types.ManagedObjectReference{ + {Type: "VirtualMachine", Value: "vm-test"}, + }, + } + + var metricDataTest = metricData{ + perfMetrics: map[string]interface{}{ + "datastore.read.average": int64(100), + "datastore.write.average": int64(200), + "datastore.datastoreIops.average": int64(10), + "datastore.totalReadLatency.average": int64(100), + "datastore.totalWriteLatency.average": int64(100), + }, + assetNames: assetNames{ + outputHsNames: []string{"DC3_H0"}, + outputVmNames: []string{"DC3_H0_VM0"}, + }, + } + + outputEvent := m.eventMapping(datastoreTest, &metricDataTest) + testEvent := mapstr.M{ + "fstype": "local", + "status": types.ManagedEntityStatus("green"), + "iops": int64(10), + "name": "datastore-test", + "host": mapstr.M{ + "count": 1, + "names": []string{"DC3_H0"}, + }, + "vm": mapstr.M{ + "count": 1, + "names": []string{"DC3_H0_VM0"}, + }, + "read": mapstr.M{ + "bytes": int64(100000), + "latency": mapstr.M{ + "total": mapstr.M{ + "ms": int64(100), + }, + }, + }, + "write": mapstr.M{ + "bytes": int64(200000), + "latency": mapstr.M{ + "total": mapstr.M{ + "ms": int64(100), + }, + }, + }, + "capacity": mapstr.M{ + "free": mapstr.M{ + "bytes": int64(5000000), + }, + "total": mapstr.M{ + "bytes": int64(5000000), + }, + "used": mapstr.M{ + "bytes": int64(0), + "pct": float64(0), + }, + }, + } + + assert.Exactly(t, outputEvent, testEvent) + +} diff --git a/metricbeat/module/vsphere/datastore/datastore.go b/metricbeat/module/vsphere/datastore/datastore.go index 4ec84b4b74e..280d7da9369 100644 --- a/metricbeat/module/vsphere/datastore/datastore.go +++ b/metricbeat/module/vsphere/datastore/datastore.go @@ -20,14 +20,17 @@ package datastore import ( "context" "fmt" + "strings" "github.com/elastic/beats/v7/metricbeat/mb" "github.com/elastic/beats/v7/metricbeat/module/vsphere" - "github.com/elastic/elastic-agent-libs/mapstr" "github.com/vmware/govmomi" + "github.com/vmware/govmomi/performance" + "github.com/vmware/govmomi/property" "github.com/vmware/govmomi/view" "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/types" ) func init() { @@ -51,6 +54,16 @@ func New(base mb.BaseMetricSet) (mb.MetricSet, error) { return &MetricSet{ms}, nil } +type metricData struct { + perfMetrics map[string]interface{} + assetNames assetNames +} + +type assetNames struct { + outputVmNames []string + outputHsNames []string +} + // Fetch methods implements the data gathering and data conversion to the right // format. It publishes the event which is then forwarded to the output. In case // of an error set the Error field of mb.Event or simply call report.Error(). @@ -62,10 +75,9 @@ func (m *MetricSet) Fetch(ctx context.Context, reporter mb.ReporterV2) error { if err != nil { return fmt.Errorf("error in NewClient: %w", err) } - defer func() { if err := client.Logout(ctx); err != nil { - m.Logger().Debug(fmt.Errorf("error trying to logout from vshphere: %w", err)) + m.Logger().Debugf("error trying to log out from vSphere: %w", err) } }() @@ -81,44 +93,145 @@ func (m *MetricSet) Fetch(ctx context.Context, reporter mb.ReporterV2) error { defer func() { if err := v.Destroy(ctx); err != nil { - m.Logger().Debug(fmt.Errorf("error trying to destroy view from vshphere: %w", err)) + m.Logger().Debugf("error trying to destroy view from vSphere: %w", err) } }() // Retrieve summary property for all datastores var dst []mo.Datastore - if err = v.Retrieve(ctx, []string{"Datastore"}, []string{"summary"}, &dst); err != nil { + err = v.Retrieve(ctx, []string{"Datastore"}, []string{"summary", "host", "vm", "overallStatus"}, &dst) + if err != nil { return fmt.Errorf("error in Retrieve: %w", err) } - for _, ds := range dst { - var usedSpacePercent float64 - if ds.Summary.Capacity > 0 { - usedSpacePercent = float64(ds.Summary.Capacity-ds.Summary.FreeSpace) / float64(ds.Summary.Capacity) - } - usedSpaceBytes := ds.Summary.Capacity - ds.Summary.FreeSpace - - event := mapstr.M{ - "name": ds.Summary.Name, - "fstype": ds.Summary.Type, - "capacity": mapstr.M{ - "total": mapstr.M{ - "bytes": ds.Summary.Capacity, - }, - "free": mapstr.M{ - "bytes": ds.Summary.FreeSpace, - }, - "used": mapstr.M{ - "bytes": usedSpaceBytes, - "pct": usedSpacePercent, - }, - }, + // Create a performance manager + perfManager := performance.NewManager(c) + + // Retrieve all available metrics + metrics, err := perfManager.CounterInfoByName(ctx) + if err != nil { + return fmt.Errorf("failed to retrieve metrics: %w", err) + } + + // Filter for required metrics + var metricIds []types.PerfMetricId + + // Define metrics to be collected + for metricName := range map[string]struct{}{ + "datastore.read.average": {}, + "datastore.write.average": {}, + "datastore.datastoreIops.average": {}, + "datastore.totalReadLatency.average": {}, + "datastore.totalWriteLatency.average": {}, + } { + if metric, ok := metrics[metricName]; ok { + metricIds = append(metricIds, types.PerfMetricId{CounterId: metric.Key}) + continue } + m.Logger().Warnf("Metric %s not found", metricName) + } - reporter.Event(mb.Event{ - MetricSetFields: event, - }) + pc := property.DefaultCollector(c) + for i := range dst { + select { + case <-ctx.Done(): + return ctx.Err() + default: + assetNames, err := getAssetNames(ctx, pc, &dst[i]) + if err != nil { + m.Logger().Errorf("Failed to retrieve object from host %s: %w", dst[i].Name, err) + continue + } + + spec := types.PerfQuerySpec{ + Entity: dst[i].Reference(), + MetricId: metricIds, + MaxSample: 1, + IntervalId: 20, // right now we are only grabbing real time metrics from the performance manager + } + + // Query performance data + samples, err := perfManager.Query(ctx, []types.PerfQuerySpec{spec}) + if err != nil { + m.Logger().Debugf("Failed to query performance data for host %s: %v", dst[i].Name, err) + continue + } + + if len(samples) == 0 { + m.Logger().Debugf("No samples returned from performance manager") + continue + } + + results, err := perfManager.ToMetricSeries(ctx, samples) + if err != nil { + m.Logger().Debugf("Failed to query performance data to metric series for host %s: %v", dst[i].Name, err) + continue + } + + metricMap := make(map[string]interface{}) + for _, result := range results[0].Value { + if len(result.Value) > 0 { + metricMap[result.Name] = result.Value[0] + continue + } + m.Logger().Debugf("For host %s,Metric %v: No result found", dst[i].Name, result.Name) + } + + reporter.Event(mb.Event{ + MetricSetFields: m.eventMapping(dst[i], &metricData{ + perfMetrics: metricMap, + assetNames: *assetNames, + }), + }) + } } return nil } + +func getAssetNames(ctx context.Context, pc *property.Collector, ds *mo.Datastore) (*assetNames, error) { + + outputVmNames := make([]string, 0, len(ds.Vm)) + if len(ds.Vm) > 0 { + var objects []mo.ManagedEntity + if err := pc.Retrieve(ctx, ds.Vm, []string{"name"}, &objects); err != nil { + return nil, err + } + for _, ob := range objects { + if ob.Reference().Type == "VirtualMachine" { + name := strings.ReplaceAll(ob.Name, ".", "_") + outputVmNames = append(outputVmNames, name) + } + } + } + + // calling Host explicitly because of mo.Datastore.hHost has types.DatastoreHostMount instead of mo.ManagedEntity + outputHsNames := make([]string, 0, len(ds.Host)) + if len(ds.Host) > 0 { + hsRefs := make([]types.ManagedObjectReference, 0, len(ds.Host)) + for _, obj := range ds.Host { + if obj.Key.Type == "HostSystem" { + hsRefs = append(hsRefs, obj.Key) + } + } + + // Retrieve Host names + var hosts []mo.HostSystem + if len(hsRefs) > 0 { + err := pc.Retrieve(ctx, hsRefs, []string{"name"}, &hosts) + if err != nil { + return nil, err + } + } + + for _, host := range hosts { + name := strings.ReplaceAll(host.Name, ".", "_") + outputHsNames = append(outputHsNames, name) + } + } + + return &assetNames{ + outputHsNames: outputHsNames, + outputVmNames: outputVmNames, + }, nil +} diff --git a/metricbeat/module/vsphere/datastore/datastore_test.go b/metricbeat/module/vsphere/datastore/datastore_test.go index 1e3387adef8..f860a2d228a 100644 --- a/metricbeat/module/vsphere/datastore/datastore_test.go +++ b/metricbeat/module/vsphere/datastore/datastore_test.go @@ -27,10 +27,12 @@ import ( ) func TestFetchEventContents(t *testing.T) { - model := simulator.ESX() + // Creating a new simulator model with VPX server to collect broad range of data. + model := simulator.VPX() if err := model.Create(); err != nil { t.Fatal(err) } + defer model.Remove() ts := model.Service.NewServer() defer ts.Close() @@ -40,7 +42,6 @@ func TestFetchEventContents(t *testing.T) { if len(errs) > 0 { t.Fatalf("Expected 0 error, had %d. %v\n", len(errs), errs) } - assert.NotEmpty(t, events) event := events[0].MetricSetFields @@ -51,28 +52,29 @@ func TestFetchEventContents(t *testing.T) { assert.EqualValues(t, "OTHER", event["fstype"]) // Values are based on the result 'df -k'. - fields := []string{"capacity.total.bytes", "capacity.free.bytes", - "capacity.used.bytes"} + fields := []string{ + "capacity.total.bytes", + "capacity.free.bytes", + "status", + "host.count", + "vm.count", + "write.bytes", + "capacity.used.bytes", + } for _, field := range fields { value, err := event.GetValue(field) if err != nil { - t.Error(err) - } else { - isNonNegativeInt64(t, field, value) + t.Error(field, err) + return + } + switch field { + case "status": + assert.NotNil(t, value) + case "vm.count", "host.count": + assert.GreaterOrEqual(t, value, 0) + default: + assert.GreaterOrEqual(t, value, int64(0)) } - } -} - -func isNonNegativeInt64(t testing.TB, field string, v interface{}) { - i, ok := v.(int64) - if !ok { - t.Errorf("%v: got %T, but expected int64", field, v) - return - } - - if i < 0 { - t.Errorf("%v: value is negative (%v)", field, i) - return } } diff --git a/metricbeat/module/vsphere/fields.go b/metricbeat/module/vsphere/fields.go index 26d9707a819..20b57227030 100644 --- a/metricbeat/module/vsphere/fields.go +++ b/metricbeat/module/vsphere/fields.go @@ -32,5 +32,5 @@ func init() { // AssetVsphere returns asset data. // This is the base64 encoded zlib format compressed contents of module/vsphere. func AssetVsphere() string { - return "eJzMmc2O2zYQx+9+ikHu8QP4UCDYIt0C3bTAbnJd0NTYYs0PgRzJcJ6+ICkpsi15LZs06sMCkZT5/zicGQ7Jz7DDwwoaV5VocQFAgiSu4FPzGp58WgAU6LgVFQmjV/DbAgCgfQvKFLX0/82iROZwBVu2ANgIlIVbhU8/g2YKhxL+R4fKf2xNXbVPRlSODQ2NFYyYI9ObGzc5abZ9NWLkeBzd7xRjiOL/Hr3oSHZ42BtbnLy7wON/v3dM53Y7wY3z9tNJfhUS3cERKjgz3GlyVjEu6LAkQ0wu1wdCN0ogjd7Ok3/zFiFYBLMBKnF0YvxvY6xitIJz+TPOjUVMivnVIianrB0WSSm/OyzyUFacRhkdZxKL94007PSDa1grtBw1XUvbft6/PSsMpXF0T014No5A6KgnjIaNNaovd6gbYY1WqGl5V9HgVR3dqsqfKaf+6Z/vIDS8PP9cTurGFE4nHBP4GuWQlOmEQ0p+pNtH1NL/ezzPbiqbfwkXwpZJeRy6Qdh9RMNNrcczar4jvtVqjdbD9PYdGB2wfEJMsAi3Ww6SnG3TVsy3EoEpP0xP5qnYFvuqAry2FjXJA6xR6C1wo12tQvkCYzt81OTpZhevMLgCG8FRMkLND0vWoCdYqlQD/BINDgZJQiEIAmI7dEAGuFGVREJgGl6fXv/0DxTTRSwrVXlwgjMJEdTHsRJSCofc6GIqhPzIujG1uZxqRM9iW6IjaM1Dw2SNwLg1zoVA9+IO6rjK9PE1A9wiS7vidZOg+xyIy58Xil4OySncDpDxEiLijQG1t4LSJskUvlci1D6GEvKn795eaxXqjucLLvfBHdwElvmBDKB9mATt+fQKlbG5urqXYPx+uly9cSq+PN3m3XRpN1ChgfMmx4uQRtobu3tP3Ax8i2YvLf2tcu42RF9PkqcFaa1f0YB0HGumi70oqFySZdopX/nyLBK+JAEj2JeCl6FZgj1zMJCFora+G/HkQhPahsklvJXCryiVRYeaXHjbU3d7l2448xPgzA8WOYrmsU7oNP8fHsi2UnX5MZxzv2b1w4/LVlfORr1x8/gqxndI7ijO82RhqzQc5uUk7ND60MvL1ctcBYXWGvtIt+0FlRBV57uwpX2QJ4eot3k1pFpKyNi26Euo1xGqWpLgzNEDpr7Xuj13fuFmnvtz1nkzP/Br9sk/Y70OsbCmqrB4wMQPV4LOnZ36LNTMk96vUHdBPi7bL+I5YlQnbIPfSgTjGxwpoUQmqWwlQm/cn1L4ZfyDw9xfjHVFYmJrcttpWPB9a/aYqz04CRVyLmajcu8pfrxc2k40KlPAN8JSzSQoxkuhp440e4z4dfvx4hRkxh3ApKX55/wBVZy6/97t7YnBIzH/J9OOumvzT+5Ycu3kf7TT/xLnYfpC1CQM/VPRvyu0jPwO4DVekU5S5L7SObH6uBudC8KJL3S+NExItpb4kfbwQGtbo6Nsx1pmA394gX5DOHvvN2QNGZoRtb29vI80RlN6tx4dZibzawjB9LDDg+H7WXntyKj3uFKMEpr1v3h2wx4fvt9Rzp6CMIwIP/JAdPFfAAAA//+hUbzT" + return "eJzMmk1v2zgTx+/5FIPe695zeIAiD3ZTYNMW67TXgJbGFtd8EciRDffTL4Z6sWxLjmWT7voQBJYy/x+HM8MhmY+wxt0jbHxZoMMHAJKk8BE+bObhmw8PADn6zMmSpDWP8L8HAIDmKWibV4r/zKFC4fERVuIBYClR5f4xvPoRjNDYl+AP7Up+2dmqbL4ZUDk01DeWCxKebGdu2OSo2ebRgJHDcbSfY4w+SiZKkUnazZYOcbbYEfqD91owZc3q6MEZNv784RAhGAS7BCpwTzw7enlpnRb0CKfyJ5hkSaionK9sMT5o5TGPyvnDY54Is8xoENJnQmH+tlRWHL/wDux3dBkaEitk2A6004WgO4Jd1n88CL70jDZIu8bd1rp8YpRKhX7nCXWwMhsULaynWWYrM+ym6XP5tdILdOwaNu3PqPKvwxF01XD/kp5YVigVQog1gtwIg7RlrPidk3UcD18+fYMna8hZBWK1crgShDl8MWVFn75VVFYE30p0gg15jiSYY2ZNPkzIP+P55/9dpLLdYUWHIm5m/y0opAkblmYV0gWWzupbszyQKkFosrZy6ljQnzcYplNozotQkqRGhgIRhMG2kzgwFpAGtFRK+jC1I9HnSVAVMfrnwd475bNb8XWilP/5MjLcjU6d7j9fziX71kmK2wO0kc2Wu8gme2tc16C/I7CD8pWRfdIGcvG9pQN85uItTe2vjqZtbtFspLNGo6HZbS1iWdVtgi5+xexlnr7/YGe9PP8aDkfWbeY2mnDd7F2iHPrheMKhG35Pd58SietAfrDMjZSDPU2aOtjZ92BN142MsEi/nvWaVrGKW6hei37C+6ZV6brVrHIODakdLJDrWGaNr3Tox8G6Fh8NMd3kchYGl+NGZtjWNFEXoeRVTRKQWKPnopxZXSokBGFg/jT/wl9oYfK6rJTFzstMKKhBL1vAw8gS1elnuSrQEzTmYSNUhSAyZ70Pgc7iPmw0eJq6bvdy8OiNXjsJpsuBej8X2qX9UiL9GlBkBfiBvvfigIq/mo/hsxKh6Rb2OPzxd/rzSoe6w3zB5Rzc9YruBA+kB81hErSn02vU1qU6UHkJxm+nS3WOEosvzfHJzXRxd5vP7e57ZGuLtLVu/Ra5Gfhamz239DfKqdsQczlJmhaksX5BA9JyLITJtzKnYkZOGK+58qVZJLgkgSDYFjIr6p3TVnjoyUJeOe5GmFwaQrcRagavheQVpXTo0ZAPTzvqdu/bDmd6Apz4wWGGcnNfJ7Sa/w0PJFup2vzozzmvWd3w62WrLWeD3rh6fKXI1kj+IM7TZGGj1B/m+SRs0brQS8vVyVwEhc5Zd0+3bSUVUKtOd2FDeydP9lGv82pItZiQddtizqFeRqgrRTITnu4w9Z3W9bmzx00896es02a+59fkk3/Cehli7mxZYn6Hie+vBK07W/VJqIknvVuhboK8X7afxYt9DfFaIFhucJSCAoWiopEIvXF3SsHL+DuHuXvGqiQ5sjW57jQs+L4xe8jVHJyECjkV83dfcSS71NlIR5VQoEVWSDN2pNlh1G83Lz8cg0y4Axi1NP2cP6DKY/ffur09Mnggxj8S7ajbNv/ojiXVTv5nM/0v9TycWm9lbcTQPxZtbs3NCub1fzSMUqS+0jmyer8bnTPCkS90Pm+EVGKh8D3t/oHWqkJPyY617BL+ZIFuQzh579dnDRmaELW5vbyNtI6m+G49OMyM5tcQgvFh+wfDt7NmlSer3+qVYpDQLv7Bk/8Yq798u6GcPQVhGBC+54How78BAAD//4VlTHc=" }