diff --git a/metadata/client.go b/metadata/client.go index bfd23c803e..4831fa2a35 100644 --- a/metadata/client.go +++ b/metadata/client.go @@ -45,11 +45,25 @@ const defaultTimeout = "5s" // s func GetMetadataFromMetadataReport(revision string, instance registry.ServiceInstance, registryId string) (*info.MetadataInfo, error) { report := GetMetadataReportByRegistry(registryId) if report == nil { - return nil, perrors.Errorf("no metadata report instance found for registryId=%s, please check metadata-report configuration", registryId) + return nil, &MetadataError{ + Kind: MetadataErrorKindReportLoad, + Source: "metadata_report", + App: instance.GetServiceName(), + Revision: revision, + RegistryID: registryId, + Err: perrors.Errorf("no metadata report instance found for registryId=%s, please check metadata-report configuration", registryId), + } } meta, err := report.GetAppMetadata(instance.GetServiceName(), revision) if err != nil { - return nil, perrors.Wrapf(err, "failed to get app metadata app=%s revision=%s", instance.GetServiceName(), revision) + return nil, &MetadataError{ + Kind: MetadataErrorKindReportLoad, + Source: "metadata_report", + App: instance.GetServiceName(), + Revision: revision, + RegistryID: registryId, + Err: perrors.Wrapf(err, "failed to get app metadata app=%s revision=%s", instance.GetServiceName(), revision), + } } return meta, nil } @@ -57,13 +71,19 @@ func GetMetadataFromMetadataReport(revision string, instance registry.ServiceIns func GetMetadataFromRpc(revision string, instance registry.ServiceInstance) (*info.MetadataInfo, error) { url, err := buildStandardMetadataServiceURL(instance) if err != nil { - return nil, err + return nil, withMetadataErrorContext(err, MetadataErrorKindURLBuild, "metadata_url", instance.GetServiceName(), revision, metadataStorageType(instance)) } url.SetParam(constant.TimeoutKey, defaultTimeout) p := extension.GetProtocol(url.Protocol) invoker := p.Refer(url) if invoker == nil { // can't connect instance - return nil, perrors.New("can not connect to remote metadata service host: " + url.Ip) + return nil, &MetadataError{ + Kind: MetadataErrorKindRPCLoad, + Source: "rpc_metadata", + App: instance.GetServiceName(), + Revision: revision, + Err: perrors.New("can not connect to remote metadata service host: " + url.Ip), + } } var remoteService remoteMetadataService if url.Protocol == constant.TriProtocol && instance.GetMetadata()[constant.MetadataVersion] == constant.MetadataServiceV2Version { @@ -74,7 +94,18 @@ func GetMetadataFromRpc(revision string, instance registry.ServiceInstance) (*in defer func() { invoker.Destroy() }() - return remoteService.getMetadataInfo(context.Background(), revision) + metadataInfo, err := remoteService.getMetadataInfo(context.Background(), revision) + if err != nil { + return nil, withMetadataErrorContext(err, MetadataErrorKindRPCLoad, "rpc_metadata", instance.GetServiceName(), revision, metadataStorageType(instance)) + } + return metadataInfo, nil +} + +func metadataStorageType(instance registry.ServiceInstance) string { + if instance.GetMetadata() == nil { + return "" + } + return instance.GetMetadata()[constant.MetadataStorageTypePropertyName] } // remoteMetadataService is the internal interface for fetching MetadataInfo via RPC. @@ -180,7 +211,12 @@ func (m *remoteMetadataServiceV1) getMetadataInfo(_ context.Context, revision st if rawResult == nil { logger.Warnf("[Metadata][RPC] Provider %s returned nil metadata (service may not be ready), revision=%s", m.invoker.GetURL().Location, revision) - return nil, perrors.Errorf("metadata is nil from %s, revision: %s", m.invoker.GetURL().Location, revision) + return nil, &MetadataError{ + Kind: MetadataErrorKindNil, + Source: "rpc_metadata", + Revision: revision, + Err: perrors.Errorf("metadata is nil from %s, revision: %s", m.invoker.GetURL().Location, revision), + } } var metadataInfo *info.MetadataInfo @@ -222,12 +258,25 @@ func truncateString(s string, maxLen int) string { // buildStandardMetadataServiceURL will use standard format to build the metadata service url. // Returns an error if required params (protocol or port) are missing. func buildStandardMetadataServiceURL(ins registry.ServiceInstance) (*common.URL, error) { - ps := getMetadataServiceUrlParams(ins) + ps, err := getMetadataServiceUrlParams(ins) + if err != nil { + return nil, err + } if ps[constant.ProtocolKey] == "" { - return nil, perrors.New("metadata service URL params missing: protocol is empty") + return nil, &MetadataError{ + Kind: MetadataErrorKindURLBuild, + Source: "metadata_url", + App: ins.GetServiceName(), + Err: perrors.New("metadata service URL params missing: protocol is empty"), + } } if ps[constant.PortKey] == "" { - return nil, perrors.New("metadata service URL params missing: port is empty") + return nil, &MetadataError{ + Kind: MetadataErrorKindURLBuild, + Source: "metadata_url", + App: ins.GetServiceName(), + Err: perrors.New("metadata service URL params missing: port is empty"), + } } sn := ins.GetServiceName() @@ -266,15 +315,21 @@ func buildStandardMetadataServiceURL(ins registry.ServiceInstance) (*common.URL, // getMetadataServiceUrlParams this will convertV2 the metadata service url parameters to map structure // it looks like: // {"dubbo":{"timeout":"10000","version":"1.0.0","dubbo":"2.0.2","release":"2.7.6","port":"20880"}} -func getMetadataServiceUrlParams(ins registry.ServiceInstance) map[string]string { +func getMetadataServiceUrlParams(ins registry.ServiceInstance) (map[string]string, error) { ps := ins.GetMetadata() res := make(map[string]string, 2) if str, ok := ps[constant.MetadataServiceURLParamsPropertyName]; ok && len(str) > 0 { err := json.Unmarshal([]byte(str), &res) if err != nil { logger.Errorf("[Metadata][URL] could not parse the metadata service url parameters to map, err=%v", err) + return nil, &MetadataError{ + Kind: MetadataErrorKindURLBuild, + Source: "metadata_url", + App: ins.GetServiceName(), + Err: perrors.Wrap(err, "could not parse metadata service URL params"), + } } } - return res + return res, nil } diff --git a/metadata/client_test.go b/metadata/client_test.go index a2c192e0f5..30928c94f6 100644 --- a/metadata/client_test.go +++ b/metadata/client_test.go @@ -19,11 +19,12 @@ package metadata import ( "context" + stderrors "errors" "testing" ) import ( - "github.com/pkg/errors" + pkgerrors "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -88,8 +89,16 @@ func TestGetMetadataFromMetadataReport(t *testing.T) { t.Run("no report instance", func(t *testing.T) { instances = make(map[string]report.MetadataReport) - _, err := GetMetadataFromMetadataReport("1", ins, "default") + _, err := GetMetadataFromMetadataReport("rev-missing-report", ins, "default") require.Error(t, err) + + var metadataErr *MetadataError + require.True(t, stderrors.As(err, &metadataErr)) + assert.Equal(t, MetadataErrorKindReportLoad, metadataErr.Kind) + assert.Equal(t, "metadata_report", metadataErr.Source) + assert.Equal(t, "dubbo-app", metadataErr.App) + assert.Equal(t, "rev-missing-report", metadataErr.Revision) + assert.Equal(t, "default", metadataErr.RegistryID) }) t.Run("default registry routes to default report", func(t *testing.T) { @@ -140,9 +149,17 @@ func TestGetMetadataFromMetadataReport(t *testing.T) { defer mockReport.AssertExpectations(t) instances["default"] = mockReport - mockReport.On("GetAppMetadata").Return(metadataInfo, errors.New("mock error")).Once() - _, err := GetMetadataFromMetadataReport("1", ins, "default") + mockReport.On("GetAppMetadata").Return(metadataInfo, pkgerrors.New("mock error")).Once() + _, err := GetMetadataFromMetadataReport("rev-report-error", ins, "default") require.Error(t, err) + + var metadataErr *MetadataError + require.True(t, stderrors.As(err, &metadataErr)) + assert.Equal(t, MetadataErrorKindReportLoad, metadataErr.Kind) + assert.Equal(t, "metadata_report", metadataErr.Source) + assert.Equal(t, "dubbo-app", metadataErr.App) + assert.Equal(t, "rev-report-error", metadataErr.Revision) + assert.Equal(t, "default", metadataErr.RegistryID) }) } @@ -170,23 +187,94 @@ func TestGetMetadataFromRpc(t *testing.T) { }) t.Run("refer error", func(t *testing.T) { mockProtocol.On("Refer").Return(nil).Once() - _, err := GetMetadataFromRpc("111", ins) + _, err := GetMetadataFromRpc("rev-refer-error", ins) require.Error(t, err) + + var metadataErr *MetadataError + require.True(t, stderrors.As(err, &metadataErr)) + assert.Equal(t, MetadataErrorKindRPCLoad, metadataErr.Kind) + assert.Equal(t, "rpc_metadata", metadataErr.Source) + assert.Equal(t, "dubbo-app", metadataErr.App) + assert.Equal(t, "rev-refer-error", metadataErr.Revision) }) t.Run("invoke timeout", func(t *testing.T) { mockProtocol.On("Refer").Return(mockInvoker).Once() mockInvoker.On("Invoke").Return(&result.RPCResult{ Attrs: map[string]any{}, - Err: errors.New("timeout error"), + Err: pkgerrors.New("timeout error"), Rest: metadataInfo, }).Once() mockInvoker.On("Destroy").Once() - _, err := GetMetadataFromRpc("111", ins) + _, err := GetMetadataFromRpc("rev-timeout", ins) + require.Error(t, err) + + var metadataErr *MetadataError + require.True(t, stderrors.As(err, &metadataErr)) + assert.Equal(t, MetadataErrorKindRPCLoad, metadataErr.Kind) + assert.Equal(t, "rpc_metadata", metadataErr.Source) + assert.Equal(t, "dubbo-app", metadataErr.App) + assert.Equal(t, "rev-timeout", metadataErr.Revision) + }) + t.Run("nil response", func(t *testing.T) { + mockProtocol.On("Refer").Return(mockInvoker).Once() + mockInvoker.On("Invoke").Return(&result.RPCResult{ + Attrs: map[string]any{}, + Err: nil, + Rest: nil, + }).Once() + mockInvoker.On("Destroy").Once() + _, err := GetMetadataFromRpc("rev-nil", ins) + require.Error(t, err) + + var metadataErr *MetadataError + require.True(t, stderrors.As(err, &metadataErr)) + assert.Equal(t, MetadataErrorKindNil, metadataErr.Kind) + assert.Equal(t, "rpc_metadata", metadataErr.Source) + assert.Equal(t, "dubbo-app", metadataErr.App) + assert.Equal(t, "rev-nil", metadataErr.Revision) + }) + t.Run("unexpected response type", func(t *testing.T) { + mockProtocol.On("Refer").Return(mockInvoker).Once() + mockInvoker.On("Invoke").Return(&result.RPCResult{ + Attrs: map[string]any{}, + Err: nil, + Rest: 123, + }).Once() + mockInvoker.On("Destroy").Once() + _, err := GetMetadataFromRpc("rev-unexpected", ins) require.Error(t, err) + + var metadataErr *MetadataError + require.True(t, stderrors.As(err, &metadataErr)) + assert.Equal(t, MetadataErrorKindRPCLoad, metadataErr.Kind) + assert.Equal(t, "rpc_metadata", metadataErr.Source) + assert.Equal(t, "dubbo-app", metadataErr.App) + assert.Equal(t, "rev-unexpected", metadataErr.Revision) }) } func TestGetMetadataFromRpc_MissingURLParams(t *testing.T) { + t.Run("malformed params", func(t *testing.T) { + insMalformedParams := ®istry.DefaultServiceInstance{ + ID: "4", + ServiceName: "dubbo-app", + Host: "dubbo.io", + Metadata: map[string]string{ + constant.MetadataServiceURLParamsPropertyName: `xxx`, + }, + } + + _, err := GetMetadataFromRpc("rev-malformed-params", insMalformedParams) + require.Error(t, err) + + var metadataErr *MetadataError + require.True(t, stderrors.As(err, &metadataErr)) + assert.Equal(t, MetadataErrorKindURLBuild, metadataErr.Kind) + assert.Equal(t, "metadata_url", metadataErr.Source) + assert.Equal(t, "dubbo-app", metadataErr.App) + assert.Equal(t, "rev-malformed-params", metadataErr.Revision) + }) + t.Run("missing protocol", func(t *testing.T) { insNoProto := ®istry.DefaultServiceInstance{ ID: "2", @@ -197,6 +285,13 @@ func TestGetMetadataFromRpc_MissingURLParams(t *testing.T) { _, err := GetMetadataFromRpc("1", insNoProto) require.Error(t, err) assert.Contains(t, err.Error(), "protocol is empty") + + var metadataErr *MetadataError + require.True(t, stderrors.As(err, &metadataErr)) + assert.Equal(t, MetadataErrorKindURLBuild, metadataErr.Kind) + assert.Equal(t, "metadata_url", metadataErr.Source) + assert.Equal(t, "dubbo-app", metadataErr.App) + assert.Equal(t, "1", metadataErr.Revision) }) t.Run("missing port", func(t *testing.T) { @@ -211,6 +306,13 @@ func TestGetMetadataFromRpc_MissingURLParams(t *testing.T) { _, err := GetMetadataFromRpc("1", insNoPort) require.Error(t, err) assert.Contains(t, err.Error(), "port is empty") + + var metadataErr *MetadataError + require.True(t, stderrors.As(err, &metadataErr)) + assert.Equal(t, MetadataErrorKindURLBuild, metadataErr.Kind) + assert.Equal(t, "metadata_url", metadataErr.Source) + assert.Equal(t, "dubbo-app", metadataErr.App) + assert.Equal(t, "1", metadataErr.Revision) }) } @@ -305,9 +407,10 @@ func Test_getMetadataServiceUrlParams(t *testing.T) { ins registry.ServiceInstance } tests := []struct { - name string - args args - want map[string]string + name string + args args + want map[string]string + wantErr bool }{ { name: "normal", @@ -345,12 +448,21 @@ func Test_getMetadataServiceUrlParams(t *testing.T) { }, }, }, - want: map[string]string{}, + wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, getMetadataServiceUrlParams(tt.args.ins), "getMetadataServiceUrlParams(%v)", tt.args.ins) + got, err := getMetadataServiceUrlParams(tt.args.ins) + if tt.wantErr { + require.Error(t, err) + var metadataErr *MetadataError + require.True(t, stderrors.As(err, &metadataErr)) + assert.Equal(t, MetadataErrorKindURLBuild, metadataErr.Kind) + return + } + require.NoError(t, err) + assert.Equalf(t, tt.want, got, "getMetadataServiceUrlParams(%v)", tt.args.ins) }) } } @@ -398,8 +510,7 @@ func (m *mockInvoker) Invoke(ctx context.Context, inv base.Invocation) result.Re // Handle both *info.MetadataInfo and *interface{} reply types // This supports the new implementation that uses interface{} to handle different return types if replyPtr, ok := inv.Reply().(*any); ok { - // New code path: reply is *interface{}, set it to point to the metadata - *replyPtr = res.Result().(*info.MetadataInfo) + *replyPtr = res.Result() } else if reply, ok := inv.Reply().(*info.MetadataInfo); ok { // Old code path: reply is *info.MetadataInfo, copy fields meta := res.Result().(*info.MetadataInfo) diff --git a/metadata/errors.go b/metadata/errors.go new file mode 100644 index 0000000000..f6ed90a4a3 --- /dev/null +++ b/metadata/errors.go @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 metadata + +// MetadataErrorKind identifies the metadata loading stage that failed. +type MetadataErrorKind string + +const ( + MetadataErrorKindReportLoad MetadataErrorKind = "metadata_report_load" + MetadataErrorKindRPCLoad MetadataErrorKind = "rpc_metadata_load" + MetadataErrorKindURLBuild MetadataErrorKind = "metadata_url_build" + MetadataErrorKindNil MetadataErrorKind = "metadata_nil" +) + +// MetadataError carries categorizable metadata loading failure context. +type MetadataError struct { + Kind MetadataErrorKind + Source string + App string + Revision string + RegistryID string + StorageType string + Err error +} + +func (e *MetadataError) Error() string { + if e == nil { + return "" + } + if e.Err != nil { + return e.Err.Error() + } + return string(e.Kind) +} + +func (e *MetadataError) Unwrap() error { + if e == nil { + return nil + } + return e.Err +} + +func withMetadataErrorContext(err error, fallbackKind MetadataErrorKind, source string, app string, revision string, storageType string) error { + if err == nil { + return nil + } + if metadataErr, ok := err.(*MetadataError); ok { + next := *metadataErr + if next.Kind == "" { + next.Kind = fallbackKind + } + if next.Source == "" { + next.Source = source + } + if next.App == "" { + next.App = app + } + if next.Revision == "" { + next.Revision = revision + } + if next.StorageType == "" { + next.StorageType = storageType + } + return &next + } + return &MetadataError{ + Kind: fallbackKind, + Source: source, + App: app, + Revision: revision, + StorageType: storageType, + Err: err, + } +} diff --git a/metadata/mapping/metadata/service_name_mapping.go b/metadata/mapping/metadata/service_name_mapping.go index c0f730ce7c..2665cc455d 100644 --- a/metadata/mapping/metadata/service_name_mapping.go +++ b/metadata/mapping/metadata/service_name_mapping.go @@ -19,6 +19,7 @@ package metadata import ( "errors" + "fmt" "sync" "time" ) @@ -82,7 +83,7 @@ func (d *ServiceNameMapping) Map(url *common.URL) error { } for _, metadataReport := range metadataReports { if err := registerWithRetry(metadataReport, serviceInterface, DefaultGroup, appName); err != nil { - return err + return fmt.Errorf("mapping_register failed: interface=%s application=%s group=%s: %w", serviceInterface, appName, DefaultGroup, err) } } return nil @@ -147,7 +148,10 @@ func (d *ServiceNameMapping) Get(url *common.URL, listener mapping.MappingListen } } if result == nil { - return nil, errors.Join(errs...) + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("mapping_get failed: interface=%s group=%s: %w", serviceInterface, DefaultGroup, err) + } + return nil, nil } return result, nil } @@ -170,5 +174,8 @@ func (d *ServiceNameMapping) Remove(url *common.URL) error { errs = append(errs, err) } } - return errors.Join(errs...) + if err := errors.Join(errs...); err != nil { + return fmt.Errorf("mapping_remove failed: interface=%s group=%s: %w", serviceInterface, DefaultGroup, err) + } + return nil } diff --git a/metadata/mapping/metadata/service_name_mapping_test.go b/metadata/mapping/metadata/service_name_mapping_test.go index fb40bc56da..49e16b0b6a 100644 --- a/metadata/mapping/metadata/service_name_mapping_test.go +++ b/metadata/mapping/metadata/service_name_mapping_test.go @@ -78,9 +78,14 @@ func TestServiceNameMappingGet(t *testing.T) { assert.False(t, apps.Empty()) }) t.Run("test error", func(t *testing.T) { - mockReport.On("GetServiceAppMapping").Return(gxset.NewSet(), errors.New("mock error")).Once() + getErr := errors.New("mock error") + mockReport.On("GetServiceAppMapping").Return(gxset.NewSet(), getErr).Once() _, err = ins.Get(serviceUrl, lis) require.Error(t, err) + require.ErrorIs(t, err, getErr) + assert.Contains(t, err.Error(), "mapping_get") + assert.Contains(t, err.Error(), "interface=org.apache.dubbo.samples.proto.GreetService") + assert.Contains(t, err.Error(), "group=mapping") }) mockReport.AssertExpectations(t) } @@ -110,6 +115,11 @@ func TestServiceNameMappingMap(t *testing.T) { mockReport.On("RegisterServiceAppMapping").Return(report.ErrMappingCASConflict).Times(conflictRetries) err = ins.Map(serviceUrl) require.Error(t, err, "conflict exhausts the retry budget") + require.ErrorIs(t, err, report.ErrMappingCASConflict) + assert.Contains(t, err.Error(), "mapping_register") + assert.Contains(t, err.Error(), "interface=org.apache.dubbo.samples.proto.GreetService") + assert.Contains(t, err.Error(), "application=dubbo") + assert.Contains(t, err.Error(), "group=mapping") }) mockReport.AssertExpectations(t) } @@ -128,9 +138,14 @@ func TestServiceNameMappingRemove(t *testing.T) { require.NoError(t, err) }) t.Run("test error", func(t *testing.T) { - mockReport.On("RemoveServiceAppMappingListener").Return(errors.New("mock error")).Once() + removeErr := errors.New("mock error") + mockReport.On("RemoveServiceAppMappingListener").Return(removeErr).Once() err = ins.Remove(serviceUrl) require.Error(t, err) + require.ErrorIs(t, err, removeErr) + assert.Contains(t, err.Error(), "mapping_remove") + assert.Contains(t, err.Error(), "interface=org.apache.dubbo.samples.proto.GreetService") + assert.Contains(t, err.Error(), "group=mapping") }) mockReport.AssertExpectations(t) } diff --git a/metadata/report_instance.go b/metadata/report_instance.go index 55cf516df6..4e1fdb0653 100644 --- a/metadata/report_instance.go +++ b/metadata/report_instance.go @@ -142,14 +142,43 @@ func (d *DelegateMetadataReport) GetAppMetadata(application, revision string) (* return meta, err } -func (d *DelegateMetadataReport) GetServiceAppMapping(application string, group string, listener mapping.MappingListener) (*gxset.HashSet, error) { - return d.instance.GetServiceAppMapping(application, group, listener) +func (d *DelegateMetadataReport) GetServiceAppMapping(interfaceName string, group string, listener mapping.MappingListener) (*gxset.HashSet, error) { + event := metadataMetrics.NewMetadataMetricTimeEvent(metadataMetrics.MetadataMappingGet) + event.Attachment[metadataMetrics.MetadataMappingOperationKey] = "get" + event.Attachment[constant.InterfaceKey] = interfaceName + event.Attachment[constant.GroupKey] = group + event.Attachment[metadataMetrics.MetadataMappingListenerRequestedKey] = "false" + if listener != nil { + event.Attachment[metadataMetrics.MetadataMappingListenerRequestedKey] = "true" + } + set, err := d.instance.GetServiceAppMapping(interfaceName, group, listener) + event.Succ = err == nil + event.End = time.Now() + metrics.Publish(event) + return set, err } func (d *DelegateMetadataReport) RegisterServiceAppMapping(interfaceName, group string, application string) error { - return d.instance.RegisterServiceAppMapping(interfaceName, group, application) + event := metadataMetrics.NewMetadataMetricTimeEvent(metadataMetrics.MetadataMappingRegister) + event.Attachment[metadataMetrics.MetadataMappingOperationKey] = "register" + event.Attachment[constant.InterfaceKey] = interfaceName + event.Attachment[constant.GroupKey] = group + event.Attachment[constant.ApplicationKey] = application + err := d.instance.RegisterServiceAppMapping(interfaceName, group, application) + event.Succ = err == nil + event.End = time.Now() + metrics.Publish(event) + return err } func (d *DelegateMetadataReport) RemoveServiceAppMappingListener(interfaceName, group string) error { - return d.instance.RemoveServiceAppMappingListener(interfaceName, group) + event := metadataMetrics.NewMetadataMetricTimeEvent(metadataMetrics.MetadataMappingRemove) + event.Attachment[metadataMetrics.MetadataMappingOperationKey] = "remove" + event.Attachment[constant.InterfaceKey] = interfaceName + event.Attachment[constant.GroupKey] = group + err := d.instance.RemoveServiceAppMappingListener(interfaceName, group) + event.Succ = err == nil + event.End = time.Now() + metrics.Publish(event) + return err } diff --git a/metadata/report_instance_test.go b/metadata/report_instance_test.go index dfe31b78c5..cccbf625b8 100644 --- a/metadata/report_instance_test.go +++ b/metadata/report_instance_test.go @@ -124,16 +124,45 @@ func TestDelegateMetadataReportGetServiceAppMapping(t *testing.T) { mockReport := new(mockMetadataReport) defer mockReport.AssertExpectations(t) delegate := &DelegateMetadataReport{instance: mockReport} + var ch = make(chan metrics.MetricsEvent, 10) + metrics.Subscribe(constant.MetricsMetadata, ch) + defer close(ch) t.Run("normal", func(t *testing.T) { mockReport.On("GetServiceAppMapping").Return(gxset.NewSet(), nil).Once() got, err := delegate.GetServiceAppMapping("dubbo", "dev", &listener{}) require.NoError(t, err) assert.True(t, got.Empty()) + assert.Len(t, ch, 1) + metricEvent := <-ch + assert.Equal(t, constant.MetricsMetadata, metricEvent.Type()) + event, ok := metricEvent.(*metricsMetadata.MetadataMetricEvent) + assert.True(t, ok) + assert.Equal(t, metricsMetadata.MetadataMappingGet, event.Name) + assert.NotNil(t, event.Start) + assert.NotNil(t, event.End) + assert.True(t, event.Succ) + assert.Equal(t, "get", event.Attachment[metricsMetadata.MetadataMappingOperationKey]) + assert.Equal(t, "dubbo", event.Attachment[constant.InterfaceKey]) + assert.Equal(t, "dev", event.Attachment[constant.GroupKey]) + assert.Equal(t, "true", event.Attachment[metricsMetadata.MetadataMappingListenerRequestedKey]) }) t.Run("error", func(t *testing.T) { mockReport.On("GetServiceAppMapping").Return(gxset.NewSet(), errors.New("mock error")).Once() _, err := delegate.GetServiceAppMapping("dubbo", "dev", &listener{}) require.Error(t, err) + assert.Len(t, ch, 1) + metricEvent := <-ch + assert.Equal(t, constant.MetricsMetadata, metricEvent.Type()) + event, ok := metricEvent.(*metricsMetadata.MetadataMetricEvent) + assert.True(t, ok) + assert.Equal(t, metricsMetadata.MetadataMappingGet, event.Name) + assert.NotNil(t, event.Start) + assert.NotNil(t, event.End) + assert.False(t, event.Succ) + assert.Equal(t, "get", event.Attachment[metricsMetadata.MetadataMappingOperationKey]) + assert.Equal(t, "dubbo", event.Attachment[constant.InterfaceKey]) + assert.Equal(t, "dev", event.Attachment[constant.GroupKey]) + assert.Equal(t, "true", event.Attachment[metricsMetadata.MetadataMappingListenerRequestedKey]) }) } @@ -141,15 +170,44 @@ func TestDelegateMetadataReportRegisterServiceAppMapping(t *testing.T) { mockReport := new(mockMetadataReport) defer mockReport.AssertExpectations(t) delegate := &DelegateMetadataReport{instance: mockReport} + var ch = make(chan metrics.MetricsEvent, 10) + metrics.Subscribe(constant.MetricsMetadata, ch) + defer close(ch) t.Run("normal", func(t *testing.T) { mockReport.On("RegisterServiceAppMapping").Return(nil).Once() err := delegate.RegisterServiceAppMapping("interfaceName", "group", "application") require.NoError(t, err) + assert.Len(t, ch, 1) + metricEvent := <-ch + assert.Equal(t, constant.MetricsMetadata, metricEvent.Type()) + event, ok := metricEvent.(*metricsMetadata.MetadataMetricEvent) + assert.True(t, ok) + assert.Equal(t, metricsMetadata.MetadataMappingRegister, event.Name) + assert.NotNil(t, event.Start) + assert.NotNil(t, event.End) + assert.True(t, event.Succ) + assert.Equal(t, "register", event.Attachment[metricsMetadata.MetadataMappingOperationKey]) + assert.Equal(t, "interfaceName", event.Attachment[constant.InterfaceKey]) + assert.Equal(t, "group", event.Attachment[constant.GroupKey]) + assert.Equal(t, "application", event.Attachment[constant.ApplicationKey]) }) t.Run("error", func(t *testing.T) { mockReport.On("RegisterServiceAppMapping").Return(errors.New("mock error")).Once() err := delegate.RegisterServiceAppMapping("interfaceName", "group", "application") require.Error(t, err) + assert.Len(t, ch, 1) + metricEvent := <-ch + assert.Equal(t, constant.MetricsMetadata, metricEvent.Type()) + event, ok := metricEvent.(*metricsMetadata.MetadataMetricEvent) + assert.True(t, ok) + assert.Equal(t, metricsMetadata.MetadataMappingRegister, event.Name) + assert.NotNil(t, event.Start) + assert.NotNil(t, event.End) + assert.False(t, event.Succ) + assert.Equal(t, "register", event.Attachment[metricsMetadata.MetadataMappingOperationKey]) + assert.Equal(t, "interfaceName", event.Attachment[constant.InterfaceKey]) + assert.Equal(t, "group", event.Attachment[constant.GroupKey]) + assert.Equal(t, "application", event.Attachment[constant.ApplicationKey]) }) } @@ -157,15 +215,42 @@ func TestDelegateMetadataReportRemoveServiceAppMappingListener(t *testing.T) { mockReport := new(mockMetadataReport) defer mockReport.AssertExpectations(t) delegate := &DelegateMetadataReport{instance: mockReport} + var ch = make(chan metrics.MetricsEvent, 10) + metrics.Subscribe(constant.MetricsMetadata, ch) + defer close(ch) t.Run("normal", func(t *testing.T) { mockReport.On("RemoveServiceAppMappingListener").Return(nil).Once() err := delegate.RemoveServiceAppMappingListener("interfaceName", "group") require.NoError(t, err) + assert.Len(t, ch, 1) + metricEvent := <-ch + assert.Equal(t, constant.MetricsMetadata, metricEvent.Type()) + event, ok := metricEvent.(*metricsMetadata.MetadataMetricEvent) + assert.True(t, ok) + assert.Equal(t, metricsMetadata.MetadataMappingRemove, event.Name) + assert.NotNil(t, event.Start) + assert.NotNil(t, event.End) + assert.True(t, event.Succ) + assert.Equal(t, "remove", event.Attachment[metricsMetadata.MetadataMappingOperationKey]) + assert.Equal(t, "interfaceName", event.Attachment[constant.InterfaceKey]) + assert.Equal(t, "group", event.Attachment[constant.GroupKey]) }) t.Run("error", func(t *testing.T) { mockReport.On("RemoveServiceAppMappingListener").Return(errors.New("mock error")).Once() err := delegate.RemoveServiceAppMappingListener("interfaceName", "group") require.Error(t, err) + assert.Len(t, ch, 1) + metricEvent := <-ch + assert.Equal(t, constant.MetricsMetadata, metricEvent.Type()) + event, ok := metricEvent.(*metricsMetadata.MetadataMetricEvent) + assert.True(t, ok) + assert.Equal(t, metricsMetadata.MetadataMappingRemove, event.Name) + assert.NotNil(t, event.Start) + assert.NotNil(t, event.End) + assert.False(t, event.Succ) + assert.Equal(t, "remove", event.Attachment[metricsMetadata.MetadataMappingOperationKey]) + assert.Equal(t, "interfaceName", event.Attachment[constant.InterfaceKey]) + assert.Equal(t, "group", event.Attachment[constant.GroupKey]) }) } diff --git a/metrics/metadata/collector.go b/metrics/metadata/collector.go index 8a08e0ff47..3e29e1704f 100644 --- a/metrics/metadata/collector.go +++ b/metrics/metadata/collector.go @@ -58,6 +58,12 @@ func (c *MetadataMetricCollector) start() { c.handleMetadataSub(event) case SubscribeServiceRt: c.handleSubscribeService(event) + case MetadataMappingRegister: + c.handleMetadataMappingRegister(event) + case MetadataMappingGet: + c.handleMetadataMappingGet(event) + case MetadataMappingRemove: + c.handleMetadataMappingRemove(event) default: } } @@ -88,6 +94,24 @@ func (c *MetadataMetricCollector) handleSubscribeService(event *MetadataMetricEv c.R.Rt(metrics.NewMetricId(subscribeServiceRt, level), &metrics.RtOpts{}).Observe(event.CostMs()) } +func (c *MetadataMetricCollector) handleMetadataMappingRegister(event *MetadataMetricEvent) { + level := newMetadataMappingMetricLevel(event.Attachment) + c.StateCount(metadataMappingRegisterNum, metadataMappingRegisterSucceed, metadataMappingRegisterFailed, level, event.Succ) + c.R.Rt(metrics.NewMetricId(metadataMappingRegisterRt, level), &metrics.RtOpts{}).Observe(event.CostMs()) +} + +func (c *MetadataMetricCollector) handleMetadataMappingGet(event *MetadataMetricEvent) { + level := newMetadataMappingMetricLevel(event.Attachment) + c.StateCount(metadataMappingGetNum, metadataMappingGetSucceed, metadataMappingGetFailed, level, event.Succ) + c.R.Rt(metrics.NewMetricId(metadataMappingGetRt, level), &metrics.RtOpts{}).Observe(event.CostMs()) +} + +func (c *MetadataMetricCollector) handleMetadataMappingRemove(event *MetadataMetricEvent) { + level := newMetadataMappingMetricLevel(event.Attachment) + c.StateCount(metadataMappingRemoveNum, metadataMappingRemoveSucceed, metadataMappingRemoveFailed, level, event.Succ) + c.R.Rt(metrics.NewMetricId(metadataMappingRemoveRt, level), &metrics.RtOpts{}).Observe(event.CostMs()) +} + type MetadataMetricEvent struct { Name MetricName Succ bool @@ -107,3 +131,27 @@ func (e *MetadataMetricEvent) CostMs() float64 { func NewMetadataMetricTimeEvent(n MetricName) *MetadataMetricEvent { return &MetadataMetricEvent{Name: n, Start: time.Now(), Attachment: make(map[string]string)} } + +type metadataMappingMetricLevel struct { + *metrics.ApplicationMetricLevel + attachment map[string]string +} + +func newMetadataMappingMetricLevel(attachment map[string]string) metadataMappingMetricLevel { + return metadataMappingMetricLevel{ + ApplicationMetricLevel: metrics.GetApplicationLevel(), + attachment: attachment, + } +} + +func (m metadataMappingMetricLevel) Tags() map[string]string { + tags := m.ApplicationMetricLevel.Tags() + tags[constant.TagInterface] = m.attachment[constant.InterfaceKey] + tags[constant.TagGroup] = m.attachment[constant.GroupKey] + tags[constant.ApplicationKey] = m.attachment[constant.ApplicationKey] + tags[MetadataMappingOperationKey] = m.attachment[MetadataMappingOperationKey] + if listenerRequested, ok := m.attachment[MetadataMappingListenerRequestedKey]; ok { + tags[MetadataMappingListenerRequestedKey] = listenerRequested + } + return tags +} diff --git a/metrics/metadata/collector_test.go b/metrics/metadata/collector_test.go index 59c29ba90c..68829349b9 100644 --- a/metrics/metadata/collector_test.go +++ b/metrics/metadata/collector_test.go @@ -28,6 +28,7 @@ import ( import ( "dubbo.apache.org/dubbo-go/v3/common/constant" + "dubbo.apache.org/dubbo-go/v3/metrics" ) func TestMetadataMetricEventType(t *testing.T) { @@ -62,3 +63,165 @@ func TestNewMetadataMetricTimeEvent(t *testing.T) { assert.NotNil(t, event.Attachment) assert.Empty(t, event.Attachment) } + +func TestMetadataMetricCollectorHandleMappingRegister(t *testing.T) { + registry := newMetadataTestMetricRegistry() + collector := &MetadataMetricCollector{BaseCollector: metrics.BaseCollector{R: registry}} + event := NewMetadataMetricTimeEvent(MetadataMappingRegister) + event.End = event.Start.Add(10 * time.Millisecond) + event.Succ = true + event.Attachment[MetadataMappingOperationKey] = "register" + event.Attachment[constant.InterfaceKey] = "interfaceName" + event.Attachment[constant.GroupKey] = "group" + event.Attachment[constant.ApplicationKey] = "application" + + collector.handleMetadataMappingRegister(event) + + assert.Equal(t, 1.0, registry.counterValue("dubbo_metadata_mapping_register_num_total")) + assert.Equal(t, 1.0, registry.counterValue("dubbo_metadata_mapping_register_num_succeed_total")) + assert.Equal(t, 0.0, registry.counterValue("dubbo_metadata_mapping_register_num_failed_total")) + assert.Equal(t, []float64{10.0}, registry.rtValues("dubbo_metadata_mapping_register_rt_milliseconds")) + id := registry.metricId("dubbo_metadata_mapping_register_num_total") + assert.Equal(t, "register", id.Tags[MetadataMappingOperationKey]) + assert.Equal(t, "interfaceName", id.Tags[constant.TagInterface]) + assert.Equal(t, "group", id.Tags[constant.GroupKey]) + assert.Equal(t, "application", id.Tags[constant.ApplicationKey]) +} + +func TestMetadataMetricCollectorHandleMappingGet(t *testing.T) { + registry := newMetadataTestMetricRegistry() + collector := &MetadataMetricCollector{BaseCollector: metrics.BaseCollector{R: registry}} + event := NewMetadataMetricTimeEvent(MetadataMappingGet) + event.End = event.Start.Add(10 * time.Millisecond) + event.Succ = true + event.Attachment[MetadataMappingOperationKey] = "get" + event.Attachment[constant.InterfaceKey] = "interfaceName" + event.Attachment[constant.GroupKey] = "group" + event.Attachment[MetadataMappingListenerRequestedKey] = "true" + + collector.handleMetadataMappingGet(event) + + assert.Equal(t, 1.0, registry.counterValue("dubbo_metadata_mapping_get_num_total")) + assert.Equal(t, 1.0, registry.counterValue("dubbo_metadata_mapping_get_num_succeed_total")) + assert.Equal(t, []float64{10.0}, registry.rtValues("dubbo_metadata_mapping_get_rt_milliseconds")) + id := registry.metricId("dubbo_metadata_mapping_get_num_total") + assert.Equal(t, "get", id.Tags[MetadataMappingOperationKey]) + assert.Equal(t, "interfaceName", id.Tags[constant.TagInterface]) + assert.Equal(t, "group", id.Tags[constant.GroupKey]) + assert.Equal(t, "true", id.Tags[MetadataMappingListenerRequestedKey]) +} + +func TestMetadataMetricCollectorHandleMappingRemove(t *testing.T) { + registry := newMetadataTestMetricRegistry() + collector := &MetadataMetricCollector{BaseCollector: metrics.BaseCollector{R: registry}} + event := NewMetadataMetricTimeEvent(MetadataMappingRemove) + event.End = event.Start.Add(10 * time.Millisecond) + event.Succ = false + event.Attachment[MetadataMappingOperationKey] = "remove" + event.Attachment[constant.InterfaceKey] = "interfaceName" + event.Attachment[constant.GroupKey] = "group" + + collector.handleMetadataMappingRemove(event) + + assert.Equal(t, 1.0, registry.counterValue("dubbo_metadata_mapping_remove_num_total")) + assert.Equal(t, 1.0, registry.counterValue("dubbo_metadata_mapping_remove_num_failed_total")) + assert.Equal(t, []float64{10.0}, registry.rtValues("dubbo_metadata_mapping_remove_rt_milliseconds")) + id := registry.metricId("dubbo_metadata_mapping_remove_num_total") + assert.Equal(t, "remove", id.Tags[MetadataMappingOperationKey]) + assert.Equal(t, "interfaceName", id.Tags[constant.TagInterface]) + assert.Equal(t, "group", id.Tags[constant.GroupKey]) +} + +type metadataTestMetricRegistry struct { + counters map[string]*metadataTestCounterMetric + rts map[string]*metadataTestObservableMetric + ids map[string]*metrics.MetricId +} + +func newMetadataTestMetricRegistry() *metadataTestMetricRegistry { + return &metadataTestMetricRegistry{ + counters: make(map[string]*metadataTestCounterMetric), + rts: make(map[string]*metadataTestObservableMetric), + ids: make(map[string]*metrics.MetricId), + } +} + +func (m *metadataTestMetricRegistry) Counter(id *metrics.MetricId) metrics.CounterMetric { + m.ids[id.Name] = id + if c, ok := m.counters[id.Name]; ok { + return c + } + c := &metadataTestCounterMetric{} + m.counters[id.Name] = c + return c +} + +func (m *metadataTestMetricRegistry) Gauge(*metrics.MetricId) metrics.GaugeMetric { + return &metadataTestGaugeMetric{} +} + +func (m *metadataTestMetricRegistry) Histogram(*metrics.MetricId) metrics.ObservableMetric { + return &metadataTestObservableMetric{} +} + +func (m *metadataTestMetricRegistry) Summary(*metrics.MetricId) metrics.ObservableMetric { + return &metadataTestObservableMetric{} +} + +func (m *metadataTestMetricRegistry) Rt(id *metrics.MetricId, _ *metrics.RtOpts) metrics.ObservableMetric { + m.ids[id.Name] = id + if rt, ok := m.rts[id.Name]; ok { + return rt + } + rt := &metadataTestObservableMetric{} + m.rts[id.Name] = rt + return rt +} + +func (m *metadataTestMetricRegistry) Export() {} + +func (m *metadataTestMetricRegistry) counterValue(name string) float64 { + if counter, ok := m.counters[name]; ok { + return counter.value + } + return 0 +} + +func (m *metadataTestMetricRegistry) rtValues(name string) []float64 { + if rt, ok := m.rts[name]; ok { + return rt.values + } + return nil +} + +func (m *metadataTestMetricRegistry) metricId(name string) *metrics.MetricId { + return m.ids[name] +} + +type metadataTestCounterMetric struct { + value float64 +} + +func (m *metadataTestCounterMetric) Inc() { + m.value++ +} + +func (m *metadataTestCounterMetric) Add(v float64) { + m.value += v +} + +type metadataTestObservableMetric struct { + values []float64 +} + +func (m *metadataTestObservableMetric) Observe(v float64) { + m.values = append(m.values, v) +} + +type metadataTestGaugeMetric struct{} + +func (*metadataTestGaugeMetric) Set(float64) {} +func (*metadataTestGaugeMetric) Inc() {} +func (*metadataTestGaugeMetric) Dec() {} +func (*metadataTestGaugeMetric) Add(float64) {} +func (*metadataTestGaugeMetric) Sub(float64) {} diff --git a/metrics/metadata/metric_set.go b/metrics/metadata/metric_set.go index 20d76a36f2..751b58e618 100644 --- a/metrics/metadata/metric_set.go +++ b/metrics/metadata/metric_set.go @@ -31,16 +31,25 @@ const ( // SubscribeRt // StoreProviderInterfaceRt SubscribeServiceRt + MetadataMappingRegister + MetadataMappingGet + MetadataMappingRemove ) const ( - dubboMetadataPush = "dubbo_metadata_push_num" - dubboPushRt = "dubbo_push_rt_milliseconds" - dubboMetadataSubscribe = "dubbo_metadata_subscribe_num" - dubboSubscribeRt = "dubbo_subscribe_rt_milliseconds" - dubboMetadataStoreProvider = "dubbo_metadata_store_provider" - dubboStoreProviderInterfaceRt = "dubbo_store_provider_interface_rt_milliseconds" - dubboSubscribeServiceRt = "dubbo_subscribe_service_rt_milliseconds" + dubboMetadataPush = "dubbo_metadata_push_num" + dubboPushRt = "dubbo_push_rt_milliseconds" + dubboMetadataSubscribe = "dubbo_metadata_subscribe_num" + dubboSubscribeRt = "dubbo_subscribe_rt_milliseconds" + dubboMetadataStoreProvider = "dubbo_metadata_store_provider" + dubboStoreProviderInterfaceRt = "dubbo_store_provider_interface_rt_milliseconds" + dubboSubscribeServiceRt = "dubbo_subscribe_service_rt_milliseconds" + dubboMetadataMappingRegister = "dubbo_metadata_mapping_register_num" + dubboMetadataMappingRegisterRt = "dubbo_metadata_mapping_register_rt_milliseconds" + dubboMetadataMappingGet = "dubbo_metadata_mapping_get_num" + dubboMetadataMappingGetRt = "dubbo_metadata_mapping_get_rt_milliseconds" + dubboMetadataMappingRemove = "dubbo_metadata_mapping_remove_num" + dubboMetadataMappingRemoveRt = "dubbo_metadata_mapping_remove_rt_milliseconds" ) const ( @@ -49,6 +58,11 @@ const ( failedSuffix = "_failed_total" ) +const ( + MetadataMappingOperationKey = "operation" + MetadataMappingListenerRequestedKey = "listener_requested" +) + var ( // app level metadataPushNum = metrics.NewMetricKey(dubboMetadataPush+totalSuffix, "Total Num") @@ -84,4 +98,19 @@ var ( storeProviderInterfaceRt = metrics.NewMetricKey(dubboStoreProviderInterfaceRt, "Store Provider Interface Time") subscribeServiceRt = metrics.NewMetricKey(dubboSubscribeServiceRt, "Subscribe Service Time") + + metadataMappingRegisterNum = metrics.NewMetricKey(dubboMetadataMappingRegister+totalSuffix, "Total Metadata Mapping Register Num") + metadataMappingRegisterSucceed = metrics.NewMetricKey(dubboMetadataMappingRegister+succSuffix, "Succeed Metadata Mapping Register Num") + metadataMappingRegisterFailed = metrics.NewMetricKey(dubboMetadataMappingRegister+failedSuffix, "Failed Metadata Mapping Register Num") + metadataMappingRegisterRt = metrics.NewMetricKey(dubboMetadataMappingRegisterRt, "Metadata Mapping Register Time") + + metadataMappingGetNum = metrics.NewMetricKey(dubboMetadataMappingGet+totalSuffix, "Total Metadata Mapping Get Num") + metadataMappingGetSucceed = metrics.NewMetricKey(dubboMetadataMappingGet+succSuffix, "Succeed Metadata Mapping Get Num") + metadataMappingGetFailed = metrics.NewMetricKey(dubboMetadataMappingGet+failedSuffix, "Failed Metadata Mapping Get Num") + metadataMappingGetRt = metrics.NewMetricKey(dubboMetadataMappingGetRt, "Metadata Mapping Get Time") + + metadataMappingRemoveNum = metrics.NewMetricKey(dubboMetadataMappingRemove+totalSuffix, "Total Metadata Mapping Remove Num") + metadataMappingRemoveSucceed = metrics.NewMetricKey(dubboMetadataMappingRemove+succSuffix, "Succeed Metadata Mapping Remove Num") + metadataMappingRemoveFailed = metrics.NewMetricKey(dubboMetadataMappingRemove+failedSuffix, "Failed Metadata Mapping Remove Num") + metadataMappingRemoveRt = metrics.NewMetricKey(dubboMetadataMappingRemoveRt, "Metadata Mapping Remove Time") ) diff --git a/registry/servicediscovery/service_instances_changed_listener_impl_test.go b/registry/servicediscovery/service_instances_changed_listener_impl_test.go index 22f7582ae4..8b19fa7160 100644 --- a/registry/servicediscovery/service_instances_changed_listener_impl_test.go +++ b/registry/servicediscovery/service_instances_changed_listener_impl_test.go @@ -18,6 +18,7 @@ package servicediscovery import ( + stderrors "errors" "fmt" "testing" ) @@ -465,6 +466,13 @@ func TestGetMetadataInfo_FallbackToRPC(t *testing.T) { "fallback path should produce a combined error mentioning both failures") assert.Contains(t, err.Error(), "metadata service URL params missing", "fallback error should include the RPC/URL failure cause") + var metadataErr *metadata.MetadataError + require.True(t, stderrors.As(err, &metadataErr)) + assert.Equal(t, metadata.MetadataErrorKindURLBuild, metadataErr.Kind) + assert.Equal(t, "metadata_url", metadataErr.Source) + assert.Equal(t, testApp, metadataErr.App) + assert.Equal(t, "rev-fallback-to-rpc", metadataErr.Revision) + assert.Equal(t, constant.RemoteMetadataStorageType, metadataErr.StorageType) } // TestGetMetadataInfo_ReportReturnsNil_FallsBackToRPC verifies the path where the metadata @@ -520,6 +528,13 @@ func TestGetMetadataInfo_ReportReturnsNil_FallsBackToRPC(t *testing.T) { "nil report result should trigger fallback and surface an RPC error") assert.Contains(t, err.Error(), "metadata service URL params missing", "fallback error should include the RPC/URL failure cause") + var metadataErr *metadata.MetadataError + require.True(t, stderrors.As(err, &metadataErr)) + assert.Equal(t, metadata.MetadataErrorKindURLBuild, metadataErr.Kind) + assert.Equal(t, "metadata_url", metadataErr.Source) + assert.Equal(t, testApp, metadataErr.App) + assert.Equal(t, revision, metadataErr.Revision) + assert.Equal(t, constant.RemoteMetadataStorageType, metadataErr.StorageType) mockReport.AssertExpectations(t) }