feat(metadata): classify errors and add useful context (#3707)

* feat: classify errors, enrich them with useful context, and add failure-path tests

* fix test
diff --git a/metadata/client.go b/metadata/client.go
index 15169d6..791ce0a 100644
--- a/metadata/client.go
+++ b/metadata/client.go
@@ -45,11 +45,12 @@
 func GetMetadataFromMetadataReport(revision string, instance registry.ServiceInstance, registryId string) (*info.MetadataInfo, error) {
 	report := GetMetadataReportByRegistry(registryId)
 	if report == nil {
-		return nil, fmt.Errorf("no metadata report instance found for registryId=%s, please check metadata-report configuration", registryId)
+		return nil, fmt.Errorf("metadata_report failed: operation=get app=%s revision=%s registry_id=%s storage_type=%s: no metadata report instance found, please check metadata-report configuration",
+			instance.GetServiceName(), revision, registryId, constant.RemoteMetadataStorageType)
 	}
 	meta, err := report.GetAppMetadata(instance.GetServiceName(), revision)
 	if err != nil {
-		return nil, fmt.Errorf("failed to get app metadata app=%s revision=%s: %w", instance.GetServiceName(), revision, err)
+		return nil, fmt.Errorf("%w; registry_id=%s", err, registryId)
 	}
 	return meta, nil
 }
@@ -64,18 +65,25 @@
 	if ctx == nil {
 		ctx = context.Background()
 	}
+	storageType := constant.DefaultMetadataStorageType
+	if instanceMetadata := instance.GetMetadata(); instanceMetadata != nil && instanceMetadata[constant.MetadataStorageTypePropertyName] != "" {
+		storageType = instanceMetadata[constant.MetadataStorageTypePropertyName]
+	}
 	if err := ctx.Err(); err != nil {
-		return nil, err
+		return nil, fmt.Errorf("rpc_metadata failed: app=%s revision=%s instance_id=%s host=%s storage_type=%s: %w",
+			instance.GetServiceName(), revision, instance.GetID(), instance.GetHost(), storageType, err)
 	}
 	url, err := buildStandardMetadataServiceURL(instance)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("url_construction failed: app=%s revision=%s instance_id=%s host=%s storage_type=%s: %w",
+			instance.GetServiceName(), revision, instance.GetID(), instance.GetHost(), storageType, err)
 	}
 	url.SetParam(constant.TimeoutKey, defaultTimeout)
 	p := extension.GetProtocol(url.Protocol)
 	invoker := p.Refer(url)
 	if invoker == nil { // can't connect instance
-		return nil, errors.New("can not connect to remote metadata service host: " + url.Ip)
+		return nil, fmt.Errorf("rpc_metadata failed: app=%s revision=%s instance_id=%s host=%s storage_type=%s: can not connect to remote metadata service",
+			instance.GetServiceName(), revision, instance.GetID(), instance.GetHost(), storageType)
 	}
 	var remoteService remoteMetadataService
 	if url.Protocol == constant.TriProtocol && instance.GetMetadata()[constant.MetadataVersion] == constant.MetadataServiceV2Version {
@@ -86,7 +94,12 @@
 	defer func() {
 		invoker.Destroy()
 	}()
-	return remoteService.getMetadataInfo(ctx, revision)
+	metadataInfo, err := remoteService.getMetadataInfo(ctx, revision)
+	if err != nil {
+		return metadataInfo, fmt.Errorf("rpc_metadata failed: app=%s revision=%s instance_id=%s host=%s storage_type=%s: %w",
+			instance.GetServiceName(), revision, instance.GetID(), instance.GetHost(), storageType, err)
+	}
+	return metadataInfo, nil
 }
 
 // remoteMetadataService is the internal interface for fetching MetadataInfo via RPC.
@@ -287,7 +300,8 @@
 	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)
+			logger.Errorf("[Metadata][URL] url_construction failed: app=%s instance_id=%s host=%s: could not parse metadata service URL parameters: %v",
+				ins.GetServiceName(), ins.GetID(), ins.GetHost(), err)
 		}
 	}
 
diff --git a/metadata/client_test.go b/metadata/client_test.go
index c334134..1c15368 100644
--- a/metadata/client_test.go
+++ b/metadata/client_test.go
@@ -91,6 +91,20 @@
 		instances = make(map[string]report.MetadataReport)
 		_, err := GetMetadataFromMetadataReport("1", ins, "default")
 		require.Error(t, err)
+		assert.Contains(t, err.Error(), "metadata_report failed:")
+		assert.Contains(t, err.Error(), "operation=get")
+		assert.Contains(t, err.Error(), "app=dubbo-app")
+		assert.Contains(t, err.Error(), "revision=1")
+		assert.Contains(t, err.Error(), "registry_id=default")
+		assert.Contains(t, err.Error(), "storage_type=remote")
+	})
+
+	t.Run("no report instance with empty registry id", func(t *testing.T) {
+		instances = make(map[string]report.MetadataReport)
+		_, err := GetMetadataFromMetadataReport("1", ins, "")
+		require.Error(t, err)
+		assert.Contains(t, err.Error(), "metadata_report failed:")
+		assert.Contains(t, err.Error(), "registry_id=")
 	})
 
 	t.Run("default registry routes to default report", func(t *testing.T) {
@@ -139,11 +153,19 @@
 		instances = make(map[string]report.MetadataReport)
 		mockReport := new(mockMetadataReport)
 		defer mockReport.AssertExpectations(t)
-		instances["default"] = mockReport
+		instances["default"] = &DelegateMetadataReport{instance: mockReport}
 
-		mockReport.On("GetAppMetadata").Return(metadataInfo, errors.New("mock error")).Once()
+		sourceErr := errors.New("mock error")
+		mockReport.On("GetAppMetadata").Return(metadataInfo, sourceErr).Once()
 		_, err := GetMetadataFromMetadataReport("1", ins, "default")
 		require.Error(t, err)
+		assert.Contains(t, err.Error(), "metadata_report failed:")
+		assert.Contains(t, err.Error(), "operation=get")
+		assert.Contains(t, err.Error(), "app=dubbo-app")
+		assert.Contains(t, err.Error(), "revision=1")
+		assert.Contains(t, err.Error(), "registry_id=default")
+		assert.Contains(t, err.Error(), "storage_type=remote")
+		require.ErrorIs(t, err, sourceErr)
 	})
 }
 
@@ -173,17 +195,31 @@
 		mockProtocol.On("Refer").Return(nil).Once()
 		_, err := GetMetadataFromRpc("111", ins)
 		require.Error(t, err)
+		assert.Contains(t, err.Error(), "rpc_metadata failed:")
+		assert.Contains(t, err.Error(), "app=dubbo-app")
+		assert.Contains(t, err.Error(), "revision=111")
+		assert.Contains(t, err.Error(), "instance_id=1")
+		assert.Contains(t, err.Error(), "host=dubbo.io")
+		assert.Contains(t, err.Error(), "storage_type=local")
 	})
 	t.Run("invoke timeout", func(t *testing.T) {
+		sourceErr := errors.New("timeout error")
 		mockProtocol.On("Refer").Return(mockInvoker).Once()
 		mockInvoker.On("Invoke").Return(&result.RPCResult{
 			Attrs: map[string]any{},
-			Err:   errors.New("timeout error"),
+			Err:   sourceErr,
 			Rest:  metadataInfo,
 		}).Once()
 		mockInvoker.On("Destroy").Once()
 		_, err := GetMetadataFromRpc("111", ins)
 		require.Error(t, err)
+		assert.Contains(t, err.Error(), "rpc_metadata failed:")
+		assert.Contains(t, err.Error(), "app=dubbo-app")
+		assert.Contains(t, err.Error(), "revision=111")
+		assert.Contains(t, err.Error(), "instance_id=1")
+		assert.Contains(t, err.Error(), "host=dubbo.io")
+		assert.Contains(t, err.Error(), "storage_type=local")
+		require.ErrorIs(t, err, sourceErr)
 	})
 }
 
@@ -208,6 +244,21 @@
 	assert.Same(t, ctx, mockInvoker.invokedContext)
 }
 
+func TestGetMetadataFromRpcWithCanceledContext(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	cancel()
+
+	_, err := GetMetadataFromRpcWithContext(ctx, "111", ins)
+	require.Error(t, err)
+	assert.Contains(t, err.Error(), "rpc_metadata failed:")
+	assert.Contains(t, err.Error(), "app=dubbo-app")
+	assert.Contains(t, err.Error(), "revision=111")
+	assert.Contains(t, err.Error(), "instance_id=1")
+	assert.Contains(t, err.Error(), "host=dubbo.io")
+	assert.Contains(t, err.Error(), "storage_type=local")
+	require.ErrorIs(t, err, context.Canceled)
+}
+
 func TestTriMetadataServiceWithContext(t *testing.T) {
 	mockInvoker := new(mockInvoker)
 	mockInvoker.url = common.NewURLWithOptions(common.WithProtocol(constant.TriProtocol))
@@ -230,6 +281,12 @@
 		}
 		_, err := GetMetadataFromRpc("1", insNoProto)
 		require.Error(t, err)
+		assert.Contains(t, err.Error(), "url_construction failed:")
+		assert.Contains(t, err.Error(), "app=dubbo-app")
+		assert.Contains(t, err.Error(), "revision=1")
+		assert.Contains(t, err.Error(), "instance_id=2")
+		assert.Contains(t, err.Error(), "host=dubbo.io")
+		assert.Contains(t, err.Error(), "storage_type=local")
 		assert.Contains(t, err.Error(), "protocol is empty")
 	})
 
@@ -244,6 +301,12 @@
 		}
 		_, err := GetMetadataFromRpc("1", insNoPort)
 		require.Error(t, err)
+		assert.Contains(t, err.Error(), "url_construction failed:")
+		assert.Contains(t, err.Error(), "app=dubbo-app")
+		assert.Contains(t, err.Error(), "revision=1")
+		assert.Contains(t, err.Error(), "instance_id=3")
+		assert.Contains(t, err.Error(), "host=dubbo.io")
+		assert.Contains(t, err.Error(), "storage_type=local")
 		assert.Contains(t, err.Error(), "port is empty")
 	})
 }
diff --git a/metadata/mapping/metadata/service_name_mapping.go b/metadata/mapping/metadata/service_name_mapping.go
index 4202e9d..09d0307 100644
--- a/metadata/mapping/metadata/service_name_mapping.go
+++ b/metadata/mapping/metadata/service_name_mapping.go
@@ -19,6 +19,7 @@
 
 import (
 	"errors"
+	"fmt"
 	"sync"
 	"time"
 )
@@ -74,6 +75,7 @@
 // Map will map the service to this application-level service
 func (d *ServiceNameMapping) Map(url *common.URL) (err error) {
 	serviceInterface := url.GetParam(constant.InterfaceKey, "")
+	serviceKey := url.ServiceKey()
 	appName := url.GetParam(constant.ApplicationKey, "")
 
 	event := metadataMetrics.NewMetadataMetricTimeEvent(metadataMetrics.MetadataMappingRegister)
@@ -90,13 +92,20 @@
 	// if the mapping can hold a report instance, it can write once
 	metadataReports := metadata.GetMetadataReports()
 	if len(metadataReports) == 0 {
-		err = errors.New("can not registering mapping to remote cause no metadata report instance found")
+		err = fmt.Errorf("mapping_register failed: service_key=%s interface=%s application=%s group=%s reports=0: no metadata report instance found",
+			serviceKey, serviceInterface, appName, DefaultGroup)
 		logger.Errorf("[Metadata][Mapping] register failed interface=%s application=%s group=%s reports=0 err=%v", serviceInterface, appName, DefaultGroup, err)
 		return err
 	}
 
 	for _, metadataReport := range metadataReports {
-		if err := registerWithRetry(metadataReport, serviceInterface, DefaultGroup, appName); err != nil {
+		if registerErr := registerWithRetry(metadataReport, serviceInterface, DefaultGroup, appName); registerErr != nil {
+			reportURL := ""
+			if u := metadataReport.URL(); u != nil {
+				reportURL = u.Protocol + "://" + u.Address()
+			}
+			err = fmt.Errorf("mapping_register failed: service_key=%s interface=%s application=%s group=%s reports=%d report_url=%s: %w",
+				serviceKey, serviceInterface, appName, DefaultGroup, len(metadataReports), reportURL, registerErr)
 			logger.Errorf("[Metadata][Mapping] register failed interface=%s application=%s group=%s reports=%d err=%v", serviceInterface, appName, DefaultGroup, len(metadataReports), err)
 			return err
 		}
@@ -136,11 +145,14 @@
 // Get will return the application-level services. If not found, the empty set will be returned.
 func (d *ServiceNameMapping) Get(url *common.URL, listener mapping.MappingListener) (result *gxset.HashSet, err error) {
 	serviceInterface := url.GetParam(constant.InterfaceKey, "")
+	serviceKey := url.ServiceKey()
 
 	operation := "get"
+	errorCategory := "mapping_get"
 	eventName := metadataMetrics.MetadataMappingGet
 	if listener != nil {
 		operation = "listen"
+		errorCategory = "mapping_listen"
 		eventName = metadataMetrics.MetadataMappingListen
 	}
 
@@ -157,7 +169,8 @@
 
 	metadataReports := metadata.GetMetadataReports()
 	if len(metadataReports) == 0 {
-		err = errors.New("can not get mapping in remote cause no metadata report instance found")
+		err = fmt.Errorf("%s failed: service_key=%s interface=%s group=%s reports=0: no metadata report instance found",
+			errorCategory, serviceKey, serviceInterface, DefaultGroup)
 		logger.Warnf("[Metadata][Mapping] get failed interface=%s group=%s reports=0 err=%v", serviceInterface, DefaultGroup, err)
 		return nil, err
 	}
@@ -174,11 +187,13 @@
 		}
 		set, getErr := metadataReport.GetServiceAppMapping(serviceInterface, DefaultGroup, reportListener)
 		if getErr != nil {
-			errs = append(errs, getErr)
 			reportURL := ""
 			if u := metadataReport.URL(); u != nil {
 				reportURL = u.Protocol + "://" + u.Address()
 			}
+			getErr = fmt.Errorf("%s failed: service_key=%s interface=%s group=%s report=%d/%d report_url=%s: %w",
+				errorCategory, serviceKey, serviceInterface, DefaultGroup, i+1, len(metadataReports), reportURL, getErr)
+			errs = append(errs, getErr)
 			logger.Warnf("[Metadata][Mapping] %s report %d/%d failed interface=%s group=%s url=%s err=%v", operation, i+1, len(metadataReports), serviceInterface, DefaultGroup, reportURL, getErr)
 			continue
 		}
@@ -207,6 +222,7 @@
 // error in one of the others.
 func (d *ServiceNameMapping) Remove(url *common.URL) (err error) {
 	serviceInterface := url.GetParam(constant.InterfaceKey, "")
+	serviceKey := url.ServiceKey()
 
 	event := metadataMetrics.NewMetadataMetricTimeEvent(metadataMetrics.MetadataMappingRemove)
 	event.Attachment[constant.InterfaceKey] = serviceInterface
@@ -219,14 +235,21 @@
 
 	metadataReports := metadata.GetMetadataReports()
 	if len(metadataReports) == 0 {
-		err = errors.New("can not remove mapping in remote cause no metadata report instance found")
+		err = fmt.Errorf("mapping_remove failed: service_key=%s interface=%s group=%s reports=0: no metadata report instance found",
+			serviceKey, serviceInterface, DefaultGroup)
 		logger.Warnf("[Metadata][Mapping] remove failed interface=%s group=%s reports=0 err=%v", serviceInterface, DefaultGroup, err)
 		return err
 	}
 
 	var errs []error
-	for _, metadataReport := range metadataReports {
+	for i, metadataReport := range metadataReports {
 		if removeErr := metadataReport.RemoveServiceAppMappingListener(serviceInterface, DefaultGroup); removeErr != nil {
+			reportURL := ""
+			if u := metadataReport.URL(); u != nil {
+				reportURL = u.Protocol + "://" + u.Address()
+			}
+			removeErr = fmt.Errorf("mapping_remove failed: service_key=%s interface=%s group=%s report=%d/%d report_url=%s: %w",
+				serviceKey, serviceInterface, DefaultGroup, i+1, len(metadataReports), reportURL, removeErr)
 			errs = append(errs, removeErr)
 		}
 	}
diff --git a/metadata/mapping/metadata/service_name_mapping_test.go b/metadata/mapping/metadata/service_name_mapping_test.go
index 7c5b384..882b4a2 100644
--- a/metadata/mapping/metadata/service_name_mapping_test.go
+++ b/metadata/mapping/metadata/service_name_mapping_test.go
@@ -61,10 +61,17 @@
 	)
 	_, err := ins.Get(serviceUrl, lis)
 	require.Error(t, err, "test Get no report instance")
+	assert.Contains(t, err.Error(), "mapping_listen failed:")
+	assert.Contains(t, err.Error(), "service_key=org.apache.dubbo.samples.proto.GreetService")
+	assert.Contains(t, err.Error(), "interface=org.apache.dubbo.samples.proto.GreetService")
+	assert.Contains(t, err.Error(), "group=mapping")
 	err = ins.Map(serviceUrl)
 	require.Error(t, err, "test Map with no report instance")
+	assert.Contains(t, err.Error(), "mapping_register failed:")
+	assert.Contains(t, err.Error(), "application=dubbo")
 	err = ins.Remove(serviceUrl)
 	require.Error(t, err, "test Remove with no report instance")
+	assert.Contains(t, err.Error(), "mapping_remove failed:")
 }
 
 func TestServiceNameMappingNoReportMetersPerBusinessOperation(t *testing.T) {
@@ -85,6 +92,7 @@
 
 	err := ins.Map(serviceUrl)
 	require.Error(t, err)
+	assert.Contains(t, err.Error(), "mapping_register failed:")
 	wantAttachment := mappingAttachment("org.example.NoReportService")
 	wantAttachment[constant.ApplicationKey] = "no-report-app"
 	assertMappingMetricEvent(t, <-ch, metricsMetadata.MetadataMappingRegister, false, false, wantAttachment)
@@ -92,16 +100,19 @@
 
 	_, err = ins.Get(serviceUrl, nil)
 	require.Error(t, err)
+	assert.Contains(t, err.Error(), "mapping_get failed:")
 	assertMappingMetricEvent(t, <-ch, metricsMetadata.MetadataMappingGet, false, false, mappingAttachment("org.example.NoReportService"))
 	assert.Empty(t, ch)
 
 	_, err = ins.Get(serviceUrl, &listener{})
 	require.Error(t, err)
+	assert.Contains(t, err.Error(), "mapping_listen failed:")
 	assertMappingMetricEvent(t, <-ch, metricsMetadata.MetadataMappingListen, false, false, mappingAttachment("org.example.NoReportService"))
 	assert.Empty(t, ch)
 
 	err = ins.Remove(serviceUrl)
 	require.Error(t, err)
+	assert.Contains(t, err.Error(), "mapping_remove failed:")
 	assertMappingMetricEvent(t, <-ch, metricsMetadata.MetadataMappingRemove, false, false, mappingAttachment("org.example.NoReportService"))
 	assert.Empty(t, ch)
 }
@@ -122,9 +133,16 @@
 		assert.False(t, apps.Empty())
 	})
 	t.Run("test error", func(t *testing.T) {
-		mockReport.On("GetServiceAppMapping").Return(gxset.NewSet(), errors.New("mock error")).Once()
+		sourceErr := errors.New("mock error")
+		mockReport.On("GetServiceAppMapping").Return(gxset.NewSet(), sourceErr).Once()
 		_, err = ins.Get(serviceUrl, lis)
 		require.Error(t, err)
+		assert.Contains(t, err.Error(), "mapping_listen failed:")
+		assert.Contains(t, err.Error(), "service_key=org.apache.dubbo.samples.proto.GreetService")
+		assert.Contains(t, err.Error(), "interface=org.apache.dubbo.samples.proto.GreetService")
+		assert.Contains(t, err.Error(), "group=mapping")
+		assert.Contains(t, err.Error(), "report_url=mock://127.0.0.1")
+		require.ErrorIs(t, err, sourceErr)
 	})
 	mockReport.AssertExpectations(t)
 }
@@ -144,9 +162,15 @@
 	})
 	t.Run("non-conflict error returns immediately", func(t *testing.T) {
 		// a generic error is not retriable, so RegisterServiceAppMapping is called exactly once
-		mockReport.On("RegisterServiceAppMapping").Return(errors.New("mock error")).Once()
+		sourceErr := errors.New("mock error")
+		mockReport.On("RegisterServiceAppMapping").Return(sourceErr).Once()
 		err = ins.Map(serviceUrl)
 		require.Error(t, err, "test mapping error")
+		assert.Contains(t, err.Error(), "mapping_register failed:")
+		assert.Contains(t, err.Error(), "service_key=org.apache.dubbo.samples.proto.GreetService")
+		assert.Contains(t, err.Error(), "application=dubbo")
+		assert.Contains(t, err.Error(), "report_url=mock://127.0.0.1")
+		require.ErrorIs(t, err, sourceErr)
 	})
 	t.Run("CAS conflict retries up to retryTimes", func(t *testing.T) {
 		const conflictRetries = 3
@@ -154,6 +178,8 @@
 		mockReport.On("RegisterServiceAppMapping").Return(report.ErrMappingCASConflict).Times(conflictRetries)
 		err = ins.Map(serviceUrl)
 		require.Error(t, err, "conflict exhausts the retry budget")
+		assert.Contains(t, err.Error(), "mapping_register failed:")
+		require.ErrorIs(t, err, report.ErrMappingCASConflict)
 	})
 	mockReport.AssertExpectations(t)
 }
@@ -172,9 +198,14 @@
 		require.NoError(t, err)
 	})
 	t.Run("test error", func(t *testing.T) {
-		mockReport.On("RemoveServiceAppMappingListener").Return(errors.New("mock error")).Once()
+		sourceErr := errors.New("mock error")
+		mockReport.On("RemoveServiceAppMappingListener").Return(sourceErr).Once()
 		err = ins.Remove(serviceUrl)
 		require.Error(t, err)
+		assert.Contains(t, err.Error(), "mapping_remove failed:")
+		assert.Contains(t, err.Error(), "service_key=org.apache.dubbo.samples.proto.GreetService")
+		assert.Contains(t, err.Error(), "report_url=mock://127.0.0.1")
+		require.ErrorIs(t, err, sourceErr)
 	})
 	mockReport.AssertExpectations(t)
 }
@@ -255,6 +286,9 @@
 
 	err := ins.Remove(serviceUrl)
 	require.Error(t, err)
+	assert.Contains(t, err.Error(), "mapping_remove failed:")
+	assert.Contains(t, err.Error(), "service_key=org.example.BarService")
+	assert.Contains(t, err.Error(), "report_url=mock://127.0.0.1:8848")
 	// both individual errors must be present in the returned error
 	require.ErrorIs(t, err, err1)
 	require.ErrorIs(t, err, err2)
@@ -343,6 +377,8 @@
 	r2.On("GetServiceAppMapping").Return(gxset.NewSet(), errors.New("r2 failure")).Once()
 	_, err = ins.Get(serviceUrl, &listener{})
 	require.Error(t, err)
+	assert.Contains(t, err.Error(), "mapping_listen failed:")
+	require.ErrorIs(t, err, getErr)
 	assertMappingMetricEvent(t, <-ch, metricsMetadata.MetadataMappingListen, false, false, mappingAttachment("org.example.MeteredService"))
 	assert.Empty(t, ch)
 
diff --git a/metadata/report_instance.go b/metadata/report_instance.go
index 11dffa0..19e973e 100644
--- a/metadata/report_instance.go
+++ b/metadata/report_instance.go
@@ -18,6 +18,7 @@
 package metadata
 
 import (
+	"fmt"
 	"sort"
 	"sync"
 	"time"
@@ -146,6 +147,10 @@
 	event.Succ = err == nil
 	event.End = time.Now()
 	metrics.Publish(event)
+	if err != nil {
+		return fmt.Errorf("metadata_report failed: operation=publish app=%s revision=%s storage_type=%s: %w",
+			application, revision, constant.RemoteMetadataStorageType, err)
+	}
 	return err
 }
 
@@ -156,6 +161,10 @@
 	event.Succ = err == nil
 	event.End = time.Now()
 	metrics.Publish(event)
+	if err != nil {
+		return meta, fmt.Errorf("metadata_report failed: operation=get app=%s revision=%s storage_type=%s: %w",
+			application, revision, constant.RemoteMetadataStorageType, err)
+	}
 	return meta, err
 }
 
@@ -173,10 +182,20 @@
 
 // UnPublishAppMetadata delegate unpublish metadata info
 func (d *DelegateMetadataReport) UnPublishAppMetadata(application, revision string) error {
-	return d.instance.UnPublishAppMetadata(application, revision)
+	err := d.instance.UnPublishAppMetadata(application, revision)
+	if err != nil {
+		return fmt.Errorf("metadata_report failed: operation=unpublish app=%s revision=%s storage_type=%s: %w",
+			application, revision, constant.RemoteMetadataStorageType, err)
+	}
+	return nil
 }
 
 // ListAppRevisions delegate list app revisions
 func (d *DelegateMetadataReport) ListAppRevisions(application string) ([]report.AppRevision, error) {
-	return d.instance.ListAppRevisions(application)
+	revisions, err := d.instance.ListAppRevisions(application)
+	if err != nil {
+		return revisions, fmt.Errorf("metadata_report failed: operation=list_revisions app=%s storage_type=%s: %w",
+			application, constant.RemoteMetadataStorageType, err)
+	}
+	return revisions, nil
 }
diff --git a/metadata/report_instance_test.go b/metadata/report_instance_test.go
index 87f311d..5724b72 100644
--- a/metadata/report_instance_test.go
+++ b/metadata/report_instance_test.go
@@ -66,9 +66,16 @@
 		assert.True(t, event.Succ)
 	})
 	t.Run("error", func(t *testing.T) {
-		mockReport.On("GetAppMetadata").Return(info.NewAppMetadataInfo("dubbo"), errors.New("mock error")).Once()
+		sourceErr := errors.New("mock error")
+		mockReport.On("GetAppMetadata").Return(info.NewAppMetadataInfo("dubbo"), sourceErr).Once()
 		_, err := delegate.GetAppMetadata("dubbo", "1111")
 		require.Error(t, err)
+		assert.Contains(t, err.Error(), "metadata_report failed:")
+		assert.Contains(t, err.Error(), "operation=get")
+		assert.Contains(t, err.Error(), "app=dubbo")
+		assert.Contains(t, err.Error(), "revision=1111")
+		assert.Contains(t, err.Error(), "storage_type=remote")
+		require.ErrorIs(t, err, sourceErr)
 		assert.Len(t, ch, 1)
 		metricEvent := <-ch
 		assert.Equal(t, constant.MetricsMetadata, metricEvent.Type())
@@ -104,9 +111,16 @@
 		assert.True(t, event.Succ)
 	})
 	t.Run("error", func(t *testing.T) {
-		mockReport.On("PublishAppMetadata").Return(errors.New("mock error")).Once()
+		sourceErr := errors.New("mock error")
+		mockReport.On("PublishAppMetadata").Return(sourceErr).Once()
 		err := delegate.PublishAppMetadata("application", "revision", metadataInfo)
 		require.Error(t, err)
+		assert.Contains(t, err.Error(), "metadata_report failed:")
+		assert.Contains(t, err.Error(), "operation=publish")
+		assert.Contains(t, err.Error(), "app=application")
+		assert.Contains(t, err.Error(), "revision=revision")
+		assert.Contains(t, err.Error(), "storage_type=remote")
+		require.ErrorIs(t, err, sourceErr)
 		assert.Len(t, ch, 1)
 		metricEvent := <-ch
 		assert.Equal(t, constant.MetricsMetadata, metricEvent.Type())
@@ -189,9 +203,16 @@
 		require.NoError(t, err)
 	})
 	t.Run("error", func(t *testing.T) {
-		mockReport.On("UnPublishAppMetadata").Return(errors.New("mock error")).Once()
+		sourceErr := errors.New("mock error")
+		mockReport.On("UnPublishAppMetadata").Return(sourceErr).Once()
 		err := delegate.UnPublishAppMetadata("application", "revision")
 		require.Error(t, err)
+		assert.Contains(t, err.Error(), "metadata_report failed:")
+		assert.Contains(t, err.Error(), "operation=unpublish")
+		assert.Contains(t, err.Error(), "app=application")
+		assert.Contains(t, err.Error(), "revision=revision")
+		assert.Contains(t, err.Error(), "storage_type=remote")
+		require.ErrorIs(t, err, sourceErr)
 	})
 }
 
@@ -210,9 +231,15 @@
 		assert.Equal(t, expected, got)
 	})
 	t.Run("error", func(t *testing.T) {
-		mockReport.On("ListAppRevisions").Return([]report.AppRevision(nil), errors.New("mock error")).Once()
+		sourceErr := errors.New("mock error")
+		mockReport.On("ListAppRevisions").Return([]report.AppRevision(nil), sourceErr).Once()
 		_, err := delegate.ListAppRevisions("application")
 		require.Error(t, err)
+		assert.Contains(t, err.Error(), "metadata_report failed:")
+		assert.Contains(t, err.Error(), "operation=list_revisions")
+		assert.Contains(t, err.Error(), "app=application")
+		assert.Contains(t, err.Error(), "storage_type=remote")
+		require.ErrorIs(t, err, sourceErr)
 	})
 }
 
diff --git a/registry/servicediscovery/service_discovery_registry.go b/registry/servicediscovery/service_discovery_registry.go
index dec7eda..df41e54 100644
--- a/registry/servicediscovery/service_discovery_registry.go
+++ b/registry/servicediscovery/service_discovery_registry.go
@@ -145,7 +145,8 @@
 
 	if metadata.GetMetadataType() == constant.RemoteMetadataStorageType {
 		if s.metadataReport == nil {
-			return errors.New("can not publish app metadata cause report instance not found")
+			return fmt.Errorf("metadata_report failed: operation=publish app=%s revision=%s registry_id=%s storage_type=%s: no metadata report instance found",
+				metaInfo.App, metaInfo.Revision, registryId, constant.RemoteMetadataStorageType)
 		}
 		if err := s.metadataReport.PublishAppMetadata(metaInfo.App, metaInfo.Revision, metaInfo); err != nil {
 			return err
@@ -306,7 +307,8 @@
 	}
 	if metadata.GetMetadataType() == constant.RemoteMetadataStorageType {
 		if s.metadataReport == nil {
-			return errors.New("can not publish app metadata cause report instance not found")
+			return fmt.Errorf("metadata_report failed: operation=publish app=%s revision=%s registry_id=%s storage_type=%s: no metadata report instance found",
+				metadataInfo.App, revision, registryId, constant.RemoteMetadataStorageType)
 		}
 		if err := s.metadataReport.PublishAppMetadata(metadataInfo.App, revision, metadataInfo); err != nil {
 			return err
diff --git a/registry/servicediscovery/service_discovery_registry_test.go b/registry/servicediscovery/service_discovery_registry_test.go
index 608e9b2..bc67405 100644
--- a/registry/servicediscovery/service_discovery_registry_test.go
+++ b/registry/servicediscovery/service_discovery_registry_test.go
@@ -329,7 +329,13 @@
 
 	err = sdReg.RegisterService()
 	require.Error(t, err)
-	assert.Contains(t, err.Error(), "report instance not found")
+	assert.Contains(t, err.Error(), "metadata_report failed:")
+	assert.Contains(t, err.Error(), "operation=publish")
+	assert.Contains(t, err.Error(), "app="+testApp)
+	assert.Contains(t, err.Error(), "revision=")
+	assert.Contains(t, err.Error(), "registry_id="+regID)
+	assert.Contains(t, err.Error(), "storage_type=remote")
+	assert.Contains(t, err.Error(), "no metadata report instance found")
 	assert.False(t, mockSD.registerCalled, "no instance should be registered when the metadata report is nil")
 }
 
diff --git a/registry/servicediscovery/service_instances_changed_listener_impl.go b/registry/servicediscovery/service_instances_changed_listener_impl.go
index 18599d1..2c69cf6 100644
--- a/registry/servicediscovery/service_instances_changed_listener_impl.go
+++ b/registry/servicediscovery/service_instances_changed_listener_impl.go
@@ -384,12 +384,14 @@
 		initCache(app)
 	})
 	cacheKey := metadataCacheKey(app, registryId, revision)
-	if metadataInfo, ok := metaCache.Get(cacheKey); ok {
+	if cachedValue, ok := metaCache.Get(cacheKey); ok {
 		logger.Debugf("[Metadata][Cache] app=%s registry=%s revision=%s host=%s result=hit",
 			app, registryId, revision, instance.GetHost())
 		publishMetadataCacheEvent(app, true)
 		publishMetadataFetchEvent(app, metricsMetadata.SourceCache, "", nil)
-		return metadataInfo.(*info.MetadataInfo), nil
+		metadataInfo := cachedValue.(*info.MetadataInfo)
+		logMetadataRevisionMismatch(metadataInfo, metricsMetadata.SourceCache, app, revision, registryId, instance)
+		return metadataInfo, nil
 	}
 	logger.Debugf("[Metadata][Cache] app=%s registry=%s revision=%s host=%s result=miss",
 		app, registryId, revision, instance.GetHost())
@@ -414,11 +416,24 @@
 		publishMetadataFetchEvent(app, source, metricStorageType, err)
 		return nil, err
 	}
+	logMetadataRevisionMismatch(metadataInfo, source, app, revision, registryId, instance)
 	metaCache.Set(cacheKey, metadataInfo)
 	publishMetadataFetchEvent(app, source, metricStorageType, nil)
 	return metadataInfo, nil
 }
 
+func logMetadataRevisionMismatch(metadataInfo *info.MetadataInfo, source, app, expectedRevision, registryId string, instance registry.ServiceInstance) {
+	if metadataInfo == nil || metadataInfo.Revision == expectedRevision {
+		return
+	}
+	storageType := constant.DefaultMetadataStorageType
+	if instanceMetadata := instance.GetMetadata(); instanceMetadata != nil && instanceMetadata[constant.MetadataStorageTypePropertyName] != "" {
+		storageType = instanceMetadata[constant.MetadataStorageTypePropertyName]
+	}
+	logger.Warnf("[Metadata] revision_mismatch failed: source=%s app=%s expected_revision=%q actual_revision=%q registry_id=%s storage_type=%s instance_id=%s host=%s",
+		source, app, expectedRevision, metadataInfo.Revision, registryId, storageType, instance.GetID(), instance.GetHost())
+}
+
 func publishMetadataCacheEvent(app string, hit bool) {
 	event := metricsMetadata.NewMetadataMetricTimeEvent(metricsMetadata.MetadataCache)
 	event.Succ = hit
@@ -566,20 +581,25 @@
 
 	metadataInfo, rpcErr := metadata.GetMetadataFromRpcWithContext(ctx, revision, instance)
 	if rpcErr != nil {
-		return nil, metricsMetadata.SourceRpc, wrapMetadataRPCFallbackError(rpcErr, reportErr)
+		rpcErr = wrapMetadataRPCFallbackError(rpcErr, reportErr)
+		rpcErr = fmt.Errorf("%w; registry_id=%s", rpcErr, registryId)
+		return nil, metricsMetadata.SourceRpc, rpcErr
 	}
 	metadataInfo, rpcErr = requireMetadataInfo(metadataInfo, app, registryId, revision)
+	if rpcErr != nil {
+		return nil, metricsMetadata.SourceRpc, wrapMetadataRPCFallbackError(rpcErr, reportErr)
+	}
 	return metadataInfo, metricsMetadata.SourceRpc, rpcErr
 }
 
 func logMetadataReportFallback(app, registryId, revision string, reportErr error) {
 	if reportErr != nil {
-		logger.Errorf("[Metadata][Fallback] report failed, fallback to RPC app=%s registry=%s revision=%s err=%v",
-			app, registryId, revision, reportErr)
+		logger.Errorf("[Metadata][Fallback] report failed, fallback to RPC app=%s registry=%s revision=%s storage_type=%s err=%v",
+			app, registryId, revision, constant.RemoteMetadataStorageType, reportErr)
 		return
 	}
-	logger.Warnf("[Metadata][Fallback] report returned nil metadata, fallback to RPC app=%s registry=%s revision=%s",
-		app, registryId, revision)
+	logger.Warnf("[Metadata][Fallback] report returned nil metadata, fallback to RPC app=%s registry=%s revision=%s storage_type=%s",
+		app, registryId, revision, constant.RemoteMetadataStorageType)
 }
 
 func wrapMetadataRPCFallbackError(rpcErr, reportErr error) error {
@@ -593,8 +613,8 @@
 
 func requireMetadataInfo(metadataInfo *info.MetadataInfo, app, registryId, revision string) (*info.MetadataInfo, error) {
 	if metadataInfo == nil {
-		return nil, fmt.Errorf("got nil metadata from RPC app=%s registry=%s revision=%s",
-			app, registryId, revision)
+		return nil, fmt.Errorf("rpc_metadata failed: app=%s revision=%s registry_id=%s: metadata is nil",
+			app, revision, registryId)
 	}
 	return metadataInfo, nil
 }
diff --git a/registry/servicediscovery/service_instances_changed_listener_impl_test.go b/registry/servicediscovery/service_instances_changed_listener_impl_test.go
index 232f6b4..ef88f9e 100644
--- a/registry/servicediscovery/service_instances_changed_listener_impl_test.go
+++ b/registry/servicediscovery/service_instances_changed_listener_impl_test.go
@@ -524,6 +524,11 @@
 	require.Error(t, err)
 	// Must be a URL/RPC error, not a report error, confirming the local path
 	// skips the report entirely and goes straight to RPC.
+	assert.Contains(t, err.Error(), "url_construction failed:")
+	assert.Contains(t, err.Error(), "app=test-app")
+	assert.Contains(t, err.Error(), "revision=rev-local-rpc")
+	assert.Contains(t, err.Error(), "registry=default")
+	assert.Contains(t, err.Error(), "storage_type=local")
 	assert.Contains(t, err.Error(), "metadata service URL params missing",
 		"local storage path should go directly to RPC, not touch the metadata report")
 }
@@ -552,8 +557,13 @@
 	require.Error(t, err)
 	// Both report and RPC fail: the combined error proves the fallback path was taken
 	// and includes the RPC/URL failure as the wrapped cause.
-	assert.Contains(t, err.Error(), "both paths failed",
-		"fallback path should produce a combined error mentioning both failures")
+	assert.Contains(t, err.Error(), "url_construction failed:")
+	assert.Contains(t, err.Error(), "both paths failed, reportErr: metadata_report failed:",
+		"fallback path should retain the report failure")
+	assert.Contains(t, err.Error(), "app=test-app")
+	assert.Contains(t, err.Error(), "revision=rev-fallback-to-rpc")
+	assert.Contains(t, err.Error(), "registry_id=default")
+	assert.Contains(t, err.Error(), "storage_type=remote")
 	assert.Contains(t, err.Error(), "metadata service URL params missing",
 		"fallback error should include the RPC/URL failure cause")
 }
@@ -607,8 +617,11 @@
 	require.Error(t, err)
 	// The report returned nil (no error), so the fallback was triggered and then RPC
 	// failed at URL construction. The error must reflect the RPC-after-nil-report path.
+	assert.Contains(t, err.Error(), "url_construction failed:")
 	assert.Contains(t, err.Error(), "RPC fallback failed after report returned nil metadata",
 		"nil report result should trigger fallback and surface an RPC error")
+	assert.Contains(t, err.Error(), "registry_id="+regID)
+	assert.Contains(t, err.Error(), "storage_type=remote")
 	assert.Contains(t, err.Error(), "metadata service URL params missing",
 		"fallback error should include the RPC/URL failure cause")
 	mockReport.AssertExpectations(t)