diff --git a/.circleci/check-links/config.json b/.circleci/check-links/config.json index 2ea064c4bc1..6119b53355c 100644 --- a/.circleci/check-links/config.json +++ b/.circleci/check-links/config.json @@ -5,6 +5,9 @@ }, { "pattern": "http(s)?://localhost" + }, + { + "pattern": "http(s)?://example.com" } ] } diff --git a/exporter/otlphttpexporter/README.md b/exporter/otlphttpexporter/README.md new file mode 100644 index 00000000000..ddb12e21f91 --- /dev/null +++ b/exporter/otlphttpexporter/README.md @@ -0,0 +1,35 @@ +# OTLP/HTTP Exporter + +Exports traces and/or metrics via HTTP using +[OTLP](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/protocol/otlp.md) format. + +The following settings are required: + +- `endpoint` (no default): The target URL to send data to (e.g.: https://example.com:55681/v1/traces). + +The following settings can be optionally configured: + +- `insecure` (default = false): when set to true disables verifying the server's + certificate chain and host name. The connection is still encrypted but server identity + is not verified. +- `ca_file` path to the CA cert. For a client this verifies the server certificate. Should + only be used if `insecure` is set to false. +- `cert_file` path to the TLS cert to use for TLS required connections. Should + only be used if `insecure` is set to false. +- `key_file` path to the TLS key to use for TLS required connections. Should + only be used if `insecure` is set to false. +- `timeout` (default = 30s): HTTP request time limit. For details see https://golang.org/pkg/net/http/#Client +- `read_buffer_size` (default = 0): ReadBufferSize for HTTP client. +- `write_buffer_size` (default = 512 * 1024): WriteBufferSize for HTTP client. + + +Example: + +```yaml +exporters: + otlphttp: + endpoint: https://example.com:55681/v1/traces +``` + +The full list of settings exposed for this exporter are documented [here](./config.go) +with detailed sample configurations [here](./testdata/config.yaml). diff --git a/exporter/otlphttpexporter/config.go b/exporter/otlphttpexporter/config.go new file mode 100644 index 00000000000..b73902be1fa --- /dev/null +++ b/exporter/otlphttpexporter/config.go @@ -0,0 +1,29 @@ +// Copyright The OpenTelemetry 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. + +package otlphttpexporter + +import ( + "go.opentelemetry.io/collector/config/confighttp" + "go.opentelemetry.io/collector/config/configmodels" + "go.opentelemetry.io/collector/exporter/exporterhelper" +) + +// Config defines configuration for OTLP/HTTP exporter. +type Config struct { + configmodels.ExporterSettings `mapstructure:",squash"` // squash ensures fields are correctly decoded in embedded struct. + confighttp.HTTPClientSettings `mapstructure:",squash"` // squash ensures fields are correctly decoded in embedded struct. + exporterhelper.QueueSettings `mapstructure:"sending_queue"` + exporterhelper.RetrySettings `mapstructure:"retry_on_failure"` +} diff --git a/exporter/otlphttpexporter/config_test.go b/exporter/otlphttpexporter/config_test.go new file mode 100644 index 00000000000..a0d3aa83a3b --- /dev/null +++ b/exporter/otlphttpexporter/config_test.go @@ -0,0 +1,85 @@ +// Copyright The OpenTelemetry 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. + +package otlphttpexporter + +import ( + "path" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/config/confighttp" + "go.opentelemetry.io/collector/config/configmodels" + "go.opentelemetry.io/collector/config/configtest" + "go.opentelemetry.io/collector/config/configtls" + "go.opentelemetry.io/collector/exporter/exporterhelper" +) + +func TestLoadConfig(t *testing.T) { + factories, err := componenttest.ExampleComponents() + assert.NoError(t, err) + + factory := NewFactory() + factories.Exporters[typeStr] = factory + cfg, err := configtest.LoadConfigFile(t, path.Join(".", "testdata", "config.yaml"), factories) + + require.NoError(t, err) + require.NotNil(t, cfg) + + e0 := cfg.Exporters["otlphttp"] + assert.Equal(t, e0, factory.CreateDefaultConfig()) + + e1 := cfg.Exporters["otlphttp/2"] + assert.Equal(t, e1, + &Config{ + ExporterSettings: configmodels.ExporterSettings{ + NameVal: "otlphttp/2", + TypeVal: "otlphttp", + }, + RetrySettings: exporterhelper.RetrySettings{ + Enabled: true, + InitialInterval: 10 * time.Second, + MaxInterval: 1 * time.Minute, + MaxElapsedTime: 10 * time.Minute, + }, + QueueSettings: exporterhelper.QueueSettings{ + Enabled: true, + NumConsumers: 2, + QueueSize: 10, + }, + HTTPClientSettings: confighttp.HTTPClientSettings{ + Headers: map[string]string{ + "can you have a . here?": "F0000000-0000-0000-0000-000000000000", + "header1": "234", + "another": "somevalue", + }, + Endpoint: "https://1.2.3.4:1234", + TLSSetting: configtls.TLSClientSetting{ + TLSSetting: configtls.TLSSetting{ + CAFile: "/var/lib/mycert.pem", + CertFile: "certfile", + KeyFile: "keyfile", + }, + Insecure: true, + }, + ReadBufferSize: 123, + WriteBufferSize: 345, + Timeout: time.Second * 10, + }, + }) +} diff --git a/exporter/otlphttpexporter/factory.go b/exporter/otlphttpexporter/factory.go new file mode 100644 index 00000000000..d130b709d3d --- /dev/null +++ b/exporter/otlphttpexporter/factory.go @@ -0,0 +1,135 @@ +// Copyright The OpenTelemetry 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. + +package otlphttpexporter + +import ( + "context" + "time" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/confighttp" + "go.opentelemetry.io/collector/config/configmodels" + "go.opentelemetry.io/collector/exporter/exporterhelper" +) + +const ( + // The value of "type" key in configuration. + typeStr = "otlphttp" +) + +// NewFactory creates a factory for OTLP exporter. +func NewFactory() component.ExporterFactory { + return exporterhelper.NewFactory( + typeStr, + createDefaultConfig, + exporterhelper.WithTraces(createTraceExporter), + exporterhelper.WithMetrics(createMetricsExporter), + exporterhelper.WithLogs(createLogsExporter)) +} + +func createDefaultConfig() configmodels.Exporter { + return &Config{ + ExporterSettings: configmodels.ExporterSettings{ + TypeVal: typeStr, + NameVal: typeStr, + }, + RetrySettings: exporterhelper.CreateDefaultRetrySettings(), + QueueSettings: exporterhelper.CreateDefaultQueueSettings(), + HTTPClientSettings: confighttp.HTTPClientSettings{ + Endpoint: "", + Timeout: 30 * time.Second, + Headers: map[string]string{}, + // We almost read 0 bytes, so no need to tune ReadBufferSize. + WriteBufferSize: 512 * 1024, + }, + } +} + +func createTraceExporter( + _ context.Context, + _ component.ExporterCreateParams, + cfg configmodels.Exporter, +) (component.TraceExporter, error) { + oce, err := newExporter(cfg) + if err != nil { + return nil, err + } + oCfg := cfg.(*Config) + oexp, err := exporterhelper.NewTraceExporter( + cfg, + oce.pushTraceData, + // explicitly disable since we rely on http.Client timeout logic. + exporterhelper.WithTimeout(exporterhelper.TimeoutSettings{Timeout: 0}), + exporterhelper.WithRetry(oCfg.RetrySettings), + exporterhelper.WithQueue(oCfg.QueueSettings), + exporterhelper.WithShutdown(oce.shutdown)) + if err != nil { + return nil, err + } + + return oexp, nil +} + +func createMetricsExporter( + _ context.Context, + _ component.ExporterCreateParams, + cfg configmodels.Exporter, +) (component.MetricsExporter, error) { + oce, err := newExporter(cfg) + if err != nil { + return nil, err + } + oCfg := cfg.(*Config) + oexp, err := exporterhelper.NewMetricsExporter( + cfg, + oce.pushMetricsData, + // explicitly disable since we rely on http.Client timeout logic. + exporterhelper.WithTimeout(exporterhelper.TimeoutSettings{Timeout: 0}), + exporterhelper.WithRetry(oCfg.RetrySettings), + exporterhelper.WithQueue(oCfg.QueueSettings), + exporterhelper.WithShutdown(oce.shutdown), + ) + if err != nil { + return nil, err + } + + return oexp, nil +} + +func createLogsExporter( + _ context.Context, + _ component.ExporterCreateParams, + cfg configmodels.Exporter, +) (component.LogsExporter, error) { + oce, err := newExporter(cfg) + if err != nil { + return nil, err + } + oCfg := cfg.(*Config) + oexp, err := exporterhelper.NewLogsExporter( + cfg, + oce.pushLogData, + // explicitly disable since we rely on http.Client timeout logic. + exporterhelper.WithTimeout(exporterhelper.TimeoutSettings{Timeout: 0}), + exporterhelper.WithRetry(oCfg.RetrySettings), + exporterhelper.WithQueue(oCfg.QueueSettings), + exporterhelper.WithShutdown(oce.shutdown), + ) + if err != nil { + return nil, err + } + + return oexp, nil +} diff --git a/exporter/otlphttpexporter/factory_test.go b/exporter/otlphttpexporter/factory_test.go new file mode 100644 index 00000000000..e2b9aedfb3b --- /dev/null +++ b/exporter/otlphttpexporter/factory_test.go @@ -0,0 +1,161 @@ +// Copyright The OpenTelemetry 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. + +package otlphttpexporter + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/configcheck" + "go.opentelemetry.io/collector/config/confighttp" + "go.opentelemetry.io/collector/config/configtls" + "go.opentelemetry.io/collector/testutil" +) + +func TestCreateDefaultConfig(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + assert.NotNil(t, cfg, "failed to create default config") + assert.NoError(t, configcheck.ValidateConfig(cfg)) + ocfg, ok := factory.CreateDefaultConfig().(*Config) + assert.True(t, ok) + assert.Equal(t, ocfg.HTTPClientSettings.Endpoint, "") + assert.Equal(t, ocfg.HTTPClientSettings.Timeout, 30*time.Second, "default timeout is 30 second") + assert.Equal(t, ocfg.RetrySettings.Enabled, true, "default retry is enabled") + assert.Equal(t, ocfg.RetrySettings.MaxElapsedTime, 300*time.Second, "default retry MaxElapsedTime") + assert.Equal(t, ocfg.RetrySettings.InitialInterval, 5*time.Second, "default retry InitialInterval") + assert.Equal(t, ocfg.RetrySettings.MaxInterval, 30*time.Second, "default retry MaxInterval") + assert.Equal(t, ocfg.QueueSettings.Enabled, true, "default sending queue is enabled") +} + +func TestCreateMetricsExporter(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig().(*Config) + cfg.HTTPClientSettings.Endpoint = testutil.GetAvailableLocalAddress(t) + + creationParams := component.ExporterCreateParams{Logger: zap.NewNop()} + oexp, err := factory.CreateMetricsExporter(context.Background(), creationParams, cfg) + require.Nil(t, err) + require.NotNil(t, oexp) +} + +func TestCreateTraceExporter(t *testing.T) { + endpoint := testutil.GetAvailableLocalAddress(t) + + tests := []struct { + name string + config Config + mustFail bool + }{ + { + name: "NoEndpoint", + config: Config{ + HTTPClientSettings: confighttp.HTTPClientSettings{ + Endpoint: "", + }, + }, + mustFail: true, + }, + { + name: "UseSecure", + config: Config{ + HTTPClientSettings: confighttp.HTTPClientSettings{ + Endpoint: endpoint, + TLSSetting: configtls.TLSClientSetting{ + Insecure: false, + }, + }, + }, + }, + { + name: "Headers", + config: Config{ + HTTPClientSettings: confighttp.HTTPClientSettings{ + Endpoint: endpoint, + Headers: map[string]string{ + "hdr1": "val1", + "hdr2": "val2", + }, + }, + }, + }, + { + name: "CaCert", + config: Config{ + HTTPClientSettings: confighttp.HTTPClientSettings{ + Endpoint: endpoint, + TLSSetting: configtls.TLSClientSetting{ + TLSSetting: configtls.TLSSetting{ + CAFile: "testdata/test_cert.pem", + }, + }, + }, + }, + }, + { + name: "CertPemFileError", + config: Config{ + HTTPClientSettings: confighttp.HTTPClientSettings{ + Endpoint: endpoint, + TLSSetting: configtls.TLSClientSetting{ + TLSSetting: configtls.TLSSetting{ + CAFile: "nosuchfile", + }, + }, + }, + }, + mustFail: false, // TODO: change to true when error handling is implemented. + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + factory := NewFactory() + creationParams := component.ExporterCreateParams{Logger: zap.NewNop()} + consumer, err := factory.CreateTraceExporter(context.Background(), creationParams, &tt.config) + + if tt.mustFail { + assert.NotNil(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, consumer) + + err = consumer.Shutdown(context.Background()) + if err != nil { + // Since the endpoint of OTLP exporter doesn't actually exist, + // exporter may already stop because it cannot connect. + assert.Equal(t, err.Error(), "rpc error: code = Canceled desc = grpc: the client connection is closing") + } + } + }) + } +} + +func TestCreateLogsExporter(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig().(*Config) + cfg.HTTPClientSettings.Endpoint = testutil.GetAvailableLocalAddress(t) + + creationParams := component.ExporterCreateParams{Logger: zap.NewNop()} + oexp, err := factory.CreateLogsExporter(context.Background(), creationParams, cfg) + require.Nil(t, err) + require.NotNil(t, oexp) +} diff --git a/exporter/otlphttpexporter/otlp.go b/exporter/otlphttpexporter/otlp.go new file mode 100644 index 00000000000..811d19fda43 --- /dev/null +++ b/exporter/otlphttpexporter/otlp.go @@ -0,0 +1,123 @@ +// Copyright The OpenTelemetry 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. + +package otlphttpexporter + +import ( + "context" + "errors" + "fmt" + + "go.opentelemetry.io/collector/config/configmodels" + "go.opentelemetry.io/collector/consumer/pdata" + "go.opentelemetry.io/collector/internal" + otlplogs "go.opentelemetry.io/collector/internal/data/opentelemetry-proto-gen/collector/logs/v1" + otlpmetrics "go.opentelemetry.io/collector/internal/data/opentelemetry-proto-gen/collector/metrics/v1" + otlptrace "go.opentelemetry.io/collector/internal/data/opentelemetry-proto-gen/collector/trace/v1" +) + +type exporterImp struct { + // Input configuration. + config *Config + w sender +} + +type sender interface { + exportTrace(ctx context.Context, request *otlptrace.ExportTraceServiceRequest) error + exportMetrics(ctx context.Context, request *otlpmetrics.ExportMetricsServiceRequest) error + exportLogs(ctx context.Context, request *otlplogs.ExportLogsServiceRequest) error + stop() error +} + +// Crete new exporter. +func newExporter(cfg configmodels.Exporter) (*exporterImp, error) { + oCfg := cfg.(*Config) + + if oCfg.Endpoint == "" { + return nil, errors.New("OTLP exporter config requires an Endpoint") + } + + e := &exporterImp{} + e.config = oCfg + w, err := newHTTPSender(oCfg) + if err != nil { + return nil, err + } + e.w = w + return e, nil +} + +func (e *exporterImp) shutdown(context.Context) error { + return e.w.stop() +} + +func (e *exporterImp) pushTraceData(ctx context.Context, td pdata.Traces) (int, error) { + request := &otlptrace.ExportTraceServiceRequest{ + ResourceSpans: pdata.TracesToOtlp(td), + } + err := e.w.exportTrace(ctx, request) + + if err != nil { + return td.SpanCount(), fmt.Errorf("failed to push trace data via OTLP exporter: %w", err) + } + return 0, nil +} + +func (e *exporterImp) pushMetricsData(ctx context.Context, md pdata.Metrics) (int, error) { + request := &otlpmetrics.ExportMetricsServiceRequest{ + ResourceMetrics: pdata.MetricsToOtlp(md), + } + err := e.w.exportMetrics(ctx, request) + + if err != nil { + return md.MetricCount(), fmt.Errorf("failed to push metrics data via OTLP exporter: %w", err) + } + return 0, nil +} + +func (e *exporterImp) pushLogData(ctx context.Context, logs pdata.Logs) (int, error) { + request := &otlplogs.ExportLogsServiceRequest{ + ResourceLogs: internal.LogsToOtlp(logs.InternalRep()), + } + err := e.w.exportLogs(ctx, request) + + if err != nil { + return logs.LogRecordCount(), fmt.Errorf("failed to push log data via OTLP exporter: %w", err) + } + return 0, nil +} + +type httpSender struct { +} + +func newHTTPSender(config *Config) (sender, error) { + hs := &httpSender{} + return hs, nil +} + +func (hs *httpSender) stop() error { + return nil +} + +func (hs *httpSender) exportTrace(ctx context.Context, request *otlptrace.ExportTraceServiceRequest) error { + return nil +} + +func (hs *httpSender) exportMetrics(ctx context.Context, request *otlpmetrics.ExportMetricsServiceRequest) error { + return nil +} + +func (hs *httpSender) exportLogs(ctx context.Context, request *otlplogs.ExportLogsServiceRequest) error { + return nil +} diff --git a/exporter/otlphttpexporter/otlp_test.go b/exporter/otlphttpexporter/otlp_test.go new file mode 100644 index 00000000000..6a33da65044 --- /dev/null +++ b/exporter/otlphttpexporter/otlp_test.go @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry 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. + +package otlphttpexporter + +// TODO: add exporter tests. diff --git a/exporter/otlphttpexporter/testdata/config.yaml b/exporter/otlphttpexporter/testdata/config.yaml new file mode 100644 index 00000000000..bf6bb642445 --- /dev/null +++ b/exporter/otlphttpexporter/testdata/config.yaml @@ -0,0 +1,37 @@ +receivers: + examplereceiver: + +processors: + exampleprocessor: + +exporters: + otlphttp: + otlphttp/2: + endpoint: "https://1.2.3.4:1234" + insecure: true + ca_file: /var/lib/mycert.pem + cert_file: certfile + key_file: keyfile + timeout: 10s + read_buffer_size: 123 + write_buffer_size: 345 + sending_queue: + enabled: true + num_consumers: 2 + queue_size: 10 + retry_on_failure: + enabled: true + initial_interval: 10s + max_interval: 60s + max_elapsed_time: 10m + headers: + "can you have a . here?": "F0000000-0000-0000-0000-000000000000" + header1: 234 + another: "somevalue" + +service: + pipelines: + traces: + receivers: [examplereceiver] + processors: [exampleprocessor] + exporters: [otlphttp] diff --git a/exporter/otlphttpexporter/testdata/test_cert.pem b/exporter/otlphttpexporter/testdata/test_cert.pem new file mode 100644 index 00000000000..b2e77b89d49 --- /dev/null +++ b/exporter/otlphttpexporter/testdata/test_cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE6jCCAtICCQDVU4PtqpqADTANBgkqhkiG9w0BAQsFADA3MQswCQYDVQQGEwJV +UzETMBEGA1UECAwKY2FsaWZvcm5pYTETMBEGA1UECgwKb3BlbmNlbnN1czAeFw0x +OTAzMDQxODA3MjZaFw0yMDAzMDMxODA3MjZaMDcxCzAJBgNVBAYTAlVTMRMwEQYD +VQQIDApjYWxpZm9ybmlhMRMwEQYDVQQKDApvcGVuY2Vuc3VzMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAy9JQiAOMzArcdiS4szbTuzg5yYijSSY6SvGj +XMs4/LEFLxgGmFfyHXxoVQzV26lTu/AiUFlZi4JY2qlkZyPwmmmSg4fmzikpVPiC +Vv9pvSIojs8gs0sHaOt40Q8ym43bNt3Mh8rYrs+XMERi6Ol9//j4LnfePkNU5uEo +qC8KQamckaMR6UEHFNunyOwvNBsipgTPldQUPGVnCsNKk8olYGAXS7DR25bgbPli +4T9VCSElsSPAODmyo+2MEDagVXa1vVYxKyO2k6oeBS0lsvdRqRTmGggcg0B/dk+a +H1CL9ful0cu9P3dQif+hfGay8udPkwDLPEq1+WnjJFut3Pmbk3SqUCas5iWt76kK +eKFh4k8fCy4yiaZxzvSbm9+bEBHAl0ZXd8pjvAsBfCKe6G9SBzE1DK4FjWiiEGCb +5dGsyTKr33q3DekLvT3LF8ZeON/13d9toucX9PqG2HDwMP/Fb4WjQIzOc/H9wIak +pf7u6QBDGUiCMmoDrp1d8RsI1RPbEhoywH0YlLmwgf+cr1dU7vlISf576EsGxFz4 ++/sZjIBvZBHn/x0MH+bs4J8V3vMujfDoRdhL07bK7q/AkEALUxljKEfoWeqiuVzK +F9BVv3xNhiua2kgPVbMNWPrQ5uotkNp8IykJ3QOuQ3p5pzxdGfpLd6f8gmJDmcbi +AI9dWTcCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAVVi4t/Sumre+AGTaU7np9dl2 +tpllbES5ixe6m2uezt5wAzYNNyuQ2mMG2XrSkMy5gvBZRT9nRNSmLV8VEcxZihG0 +YHS5soXnLL3Jdlwxp98WTDPvM1ntxcHyEyqrrg9YDfKn4sOrr5vo2yZzoKwtxtc7 +lue9JormVx7GxMi7NwaUtCbnwAIcqJJpFjt1EhmJOxGqTJPgUvTBdeGvRj30c6fk +pqpUdPbZ7RKPEtbLoMoCBujKnErv+H0G6Vp9WyCHN+Mi9uTMsGwH14cmJjmfwGDC +8/WF4LdlawFnf/arIp9YcVwcP91d4ywyvbuuo2M7qdosQ7k4uRZ3tyggLYShS3RW +BMEhMRDz9dM0oKGF+HnaS824BIh6O6Hn82Vt8uCKS7IbEX99/kkN1KcqqQe6Lwjq +tG/lm4K5yf+FJVDivpZ9mYTvqTBjhTaOp6m3HYSNJfS0hLQVvEuBNXd8bHiXkcLp +rmFOYUWsjxV1Qku3U5Rner0UpB2Fuw9nJcXuDgWG0gjwzAZ83y3du1VIZp0Ad8Vv +IYpaucbImGJszMtNXn3l72K1wvQVIhm9eRwYc3QteJzweHaDsbytZEoS/GhTrZIT +wRe5ZGrjJBJngRANRSm1BH8j6PjLem9mzPb2eytwJJA0lLhUk4vYproVvXcx0vow +5F+5VB1YB8/tbWePmpo= +-----END CERTIFICATE-----