go.uber.org/cadence@v1.2.9/internal/common/metrics/service_wrapper_test.go (about) 1 // Copyright (c) 2017 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package metrics 22 23 import ( 24 "fmt" 25 "io" 26 "reflect" 27 "strings" 28 "testing" 29 "time" 30 31 "github.com/golang/mock/gomock" 32 "github.com/stretchr/testify/require" 33 "github.com/uber-go/tally" 34 "github.com/uber/tchannel-go/thrift" 35 "go.uber.org/yarpc" 36 37 "go.uber.org/cadence/.gen/go/cadence/workflowserviceclient" 38 "go.uber.org/cadence/.gen/go/cadence/workflowservicetest" 39 s "go.uber.org/cadence/.gen/go/shared" 40 ) 41 42 var ( 43 safeCharacters = []rune{'_'} 44 45 sanitizeOptions = tally.SanitizeOptions{ 46 NameCharacters: tally.ValidCharacters{ 47 Ranges: tally.AlphanumericRange, 48 Characters: safeCharacters, 49 }, 50 KeyCharacters: tally.ValidCharacters{ 51 Ranges: tally.AlphanumericRange, 52 Characters: safeCharacters, 53 }, 54 ValueCharacters: tally.ValidCharacters{ 55 Ranges: tally.AlphanumericRange, 56 Characters: safeCharacters, 57 }, 58 ReplacementCharacter: tally.DefaultReplacementCharacter, 59 } 60 ) 61 62 type testCase struct { 63 serviceMethod string 64 callArgs []interface{} 65 mockReturns []interface{} 66 expectedCounters []string 67 } 68 69 func Test_Wrapper(t *testing.T) { 70 ctx, _ := thrift.NewContext(time.Minute) 71 tests := []testCase{ 72 // one case for each service call 73 {"DeprecateDomain", []interface{}{ctx, &s.DeprecateDomainRequest{}}, []interface{}{nil}, []string{CadenceRequest}}, 74 {"DescribeDomain", []interface{}{ctx, &s.DescribeDomainRequest{}}, []interface{}{&s.DescribeDomainResponse{}, nil}, []string{CadenceRequest}}, 75 {"GetWorkflowExecutionHistory", []interface{}{ctx, &s.GetWorkflowExecutionHistoryRequest{}}, []interface{}{&s.GetWorkflowExecutionHistoryResponse{}, nil}, []string{CadenceRequest}}, 76 {"ListClosedWorkflowExecutions", []interface{}{ctx, &s.ListClosedWorkflowExecutionsRequest{}}, []interface{}{&s.ListClosedWorkflowExecutionsResponse{}, nil}, []string{CadenceRequest}}, 77 {"ListOpenWorkflowExecutions", []interface{}{ctx, &s.ListOpenWorkflowExecutionsRequest{}}, []interface{}{&s.ListOpenWorkflowExecutionsResponse{}, nil}, []string{CadenceRequest}}, 78 {"PollForActivityTask", []interface{}{ctx, &s.PollForActivityTaskRequest{}}, []interface{}{&s.PollForActivityTaskResponse{}, nil}, []string{CadenceRequest}}, 79 {"PollForDecisionTask", []interface{}{ctx, &s.PollForDecisionTaskRequest{}}, []interface{}{&s.PollForDecisionTaskResponse{}, nil}, []string{CadenceRequest}}, 80 {"RecordActivityTaskHeartbeat", []interface{}{ctx, &s.RecordActivityTaskHeartbeatRequest{}}, []interface{}{&s.RecordActivityTaskHeartbeatResponse{}, nil}, []string{CadenceRequest}}, 81 {"RegisterDomain", []interface{}{ctx, &s.RegisterDomainRequest{}}, []interface{}{nil}, []string{CadenceRequest}}, 82 {"RequestCancelWorkflowExecution", []interface{}{ctx, &s.RequestCancelWorkflowExecutionRequest{}}, []interface{}{nil}, []string{CadenceRequest}}, 83 {"RespondActivityTaskCanceled", []interface{}{ctx, &s.RespondActivityTaskCanceledRequest{}}, []interface{}{nil}, []string{CadenceRequest}}, 84 {"RespondActivityTaskCompleted", []interface{}{ctx, &s.RespondActivityTaskCompletedRequest{}}, []interface{}{nil}, []string{CadenceRequest}}, 85 {"RespondActivityTaskFailed", []interface{}{ctx, &s.RespondActivityTaskFailedRequest{}}, []interface{}{nil}, []string{CadenceRequest}}, 86 {"RespondActivityTaskCanceledByID", []interface{}{ctx, &s.RespondActivityTaskCanceledByIDRequest{}}, []interface{}{nil}, []string{CadenceRequest}}, 87 {"RespondActivityTaskCompletedByID", []interface{}{ctx, &s.RespondActivityTaskCompletedByIDRequest{}}, []interface{}{nil}, []string{CadenceRequest}}, 88 {"RespondActivityTaskFailedByID", []interface{}{ctx, &s.RespondActivityTaskFailedByIDRequest{}}, []interface{}{nil}, []string{CadenceRequest}}, 89 {"RespondDecisionTaskCompleted", []interface{}{ctx, &s.RespondDecisionTaskCompletedRequest{}}, []interface{}{nil, nil}, []string{CadenceRequest}}, 90 {"SignalWorkflowExecution", []interface{}{ctx, &s.SignalWorkflowExecutionRequest{}}, []interface{}{nil}, []string{CadenceRequest}}, 91 {"StartWorkflowExecution", []interface{}{ctx, &s.StartWorkflowExecutionRequest{}}, []interface{}{&s.StartWorkflowExecutionResponse{}, nil}, []string{CadenceRequest}}, 92 {"TerminateWorkflowExecution", []interface{}{ctx, &s.TerminateWorkflowExecutionRequest{}}, []interface{}{nil}, []string{CadenceRequest}}, 93 {"ResetWorkflowExecution", []interface{}{ctx, &s.ResetWorkflowExecutionRequest{}}, []interface{}{&s.ResetWorkflowExecutionResponse{}, nil}, []string{CadenceRequest}}, 94 {"UpdateDomain", []interface{}{ctx, &s.UpdateDomainRequest{}}, []interface{}{&s.UpdateDomainResponse{}, nil}, []string{CadenceRequest}}, 95 // one case of invalid request 96 {"PollForActivityTask", []interface{}{ctx, &s.PollForActivityTaskRequest{}}, []interface{}{nil, &s.EntityNotExistsError{}}, []string{CadenceRequest, CadenceInvalidRequest}}, 97 // one case of server error 98 {"PollForActivityTask", []interface{}{ctx, &s.PollForActivityTaskRequest{}}, []interface{}{nil, &s.InternalServiceError{}}, []string{CadenceRequest, CadenceError}}, 99 {"QueryWorkflow", []interface{}{ctx, &s.QueryWorkflowRequest{}}, []interface{}{nil, &s.InternalServiceError{}}, []string{CadenceRequest, CadenceError}}, 100 {"RespondQueryTaskCompleted", []interface{}{ctx, &s.RespondQueryTaskCompletedRequest{}}, []interface{}{&s.InternalServiceError{}}, []string{CadenceRequest, CadenceError}}, 101 } 102 103 // run each test twice - once with the regular scope, once with a sanitized metrics scope 104 for _, test := range tests { 105 runTest(t, test, newService, assertMetrics, fmt.Sprintf("%v_normal", test.serviceMethod)) 106 runTest(t, test, newPromService, assertPromMetrics, fmt.Sprintf("%v_prom_sanitized", test.serviceMethod)) 107 } 108 } 109 110 func runTest( 111 t *testing.T, 112 test testCase, 113 serviceFunc func(*testing.T) (*workflowservicetest.MockClient, workflowserviceclient.Interface, io.Closer, *CapturingStatsReporter), 114 validationFunc func(*testing.T, *CapturingStatsReporter, string, []string), 115 name string, 116 ) { 117 t.Run(name, func(t *testing.T) { 118 t.Parallel() 119 // gomock mutates the returns slice, which leads to different test values between the two runs. 120 // copy the slice until gomock fixes it: https://github.com/golang/mock/issues/353 121 returns := append(make([]interface{}, 0, len(test.mockReturns)), test.mockReturns...) 122 123 mockService, wrapperService, closer, reporter := serviceFunc(t) 124 switch test.serviceMethod { 125 case "DeprecateDomain": 126 mockService.EXPECT().DeprecateDomain(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 127 case "DescribeDomain": 128 mockService.EXPECT().DescribeDomain(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 129 case "GetWorkflowExecutionHistory": 130 mockService.EXPECT().GetWorkflowExecutionHistory(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 131 case "ListClosedWorkflowExecutions": 132 mockService.EXPECT().ListClosedWorkflowExecutions(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 133 case "ListOpenWorkflowExecutions": 134 mockService.EXPECT().ListOpenWorkflowExecutions(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 135 case "PollForActivityTask": 136 mockService.EXPECT().PollForActivityTask(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 137 case "PollForDecisionTask": 138 mockService.EXPECT().PollForDecisionTask(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 139 case "RecordActivityTaskHeartbeat": 140 mockService.EXPECT().RecordActivityTaskHeartbeat(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 141 case "RecordActivityTaskHeartbeatByID": 142 mockService.EXPECT().RecordActivityTaskHeartbeatByID(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 143 case "RegisterDomain": 144 mockService.EXPECT().RegisterDomain(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 145 case "RequestCancelWorkflowExecution": 146 mockService.EXPECT().RequestCancelWorkflowExecution(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 147 case "RespondActivityTaskCanceled": 148 mockService.EXPECT().RespondActivityTaskCanceled(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 149 case "RespondActivityTaskCompleted": 150 mockService.EXPECT().RespondActivityTaskCompleted(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 151 case "RespondActivityTaskFailed": 152 mockService.EXPECT().RespondActivityTaskFailed(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 153 case "RespondActivityTaskCanceledByID": 154 mockService.EXPECT().RespondActivityTaskCanceledByID(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 155 case "RespondActivityTaskCompletedByID": 156 mockService.EXPECT().RespondActivityTaskCompletedByID(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 157 case "RespondActivityTaskFailedByID": 158 mockService.EXPECT().RespondActivityTaskFailedByID(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 159 case "RespondDecisionTaskCompleted": 160 mockService.EXPECT().RespondDecisionTaskCompleted(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 161 case "SignalWorkflowExecution": 162 mockService.EXPECT().SignalWorkflowExecution(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 163 case "SignaWithStartlWorkflowExecution": 164 mockService.EXPECT().SignalWithStartWorkflowExecution(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 165 case "StartWorkflowExecution": 166 mockService.EXPECT().StartWorkflowExecution(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 167 case "TerminateWorkflowExecution": 168 mockService.EXPECT().TerminateWorkflowExecution(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 169 case "ResetWorkflowExecution": 170 mockService.EXPECT().ResetWorkflowExecution(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 171 case "UpdateDomain": 172 mockService.EXPECT().UpdateDomain(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 173 case "QueryWorkflow": 174 mockService.EXPECT().QueryWorkflow(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 175 case "RespondQueryTaskCompleted": 176 mockService.EXPECT().RespondQueryTaskCompleted(gomock.Any(), gomock.Any(), gomock.Any()).Return(returns...) 177 } 178 179 callOption := yarpc.CallOption{} 180 inputs := make([]reflect.Value, len(test.callArgs)) 181 for i, arg := range test.callArgs { 182 inputs[i] = reflect.ValueOf(arg) 183 } 184 inputs = append(inputs, reflect.ValueOf(callOption)) 185 method := reflect.ValueOf(wrapperService).MethodByName(test.serviceMethod) 186 method.Call(inputs) 187 require.NoError(t, closer.Close()) 188 validationFunc(t, reporter, test.serviceMethod, test.expectedCounters) 189 }) 190 } 191 192 func assertMetrics(t *testing.T, reporter *CapturingStatsReporter, methodName string, counterNames []string) { 193 require.Equal(t, len(counterNames), len(reporter.counts)) 194 for _, name := range counterNames { 195 counterName := CadenceMetricsPrefix + methodName + "." + name 196 find := false 197 // counters are not in order 198 for _, counter := range reporter.counts { 199 if counterName == counter.name { 200 find = true 201 break 202 } 203 } 204 require.True(t, find) 205 } 206 require.Equal(t, 1, len(reporter.timers)) 207 require.Equal(t, CadenceMetricsPrefix+methodName+"."+CadenceLatency, reporter.timers[0].name) 208 } 209 210 func assertPromMetrics(t *testing.T, reporter *CapturingStatsReporter, methodName string, counterNames []string) { 211 require.Equal(t, len(counterNames), len(reporter.counts)) 212 for _, name := range counterNames { 213 counterName := makePromCompatible(CadenceMetricsPrefix + methodName + "." + name) 214 find := false 215 // counters are not in order 216 for _, counter := range reporter.counts { 217 if counterName == counter.name { 218 find = true 219 break 220 } 221 } 222 require.True(t, find) 223 } 224 require.Equal(t, 1, len(reporter.timers)) 225 expected := makePromCompatible(CadenceMetricsPrefix + methodName + "." + CadenceLatency) 226 require.Equal(t, expected, reporter.timers[0].name) 227 } 228 229 func makePromCompatible(name string) string { 230 name = strings.Replace(name, "-", "_", -1) 231 name = strings.Replace(name, ".", "_", -1) 232 return name 233 } 234 235 func newService(t *testing.T) ( 236 mockService *workflowservicetest.MockClient, 237 wrapperService workflowserviceclient.Interface, 238 closer io.Closer, 239 reporter *CapturingStatsReporter, 240 ) { 241 mockCtrl := gomock.NewController(t) 242 mockService = workflowservicetest.NewMockClient(mockCtrl) 243 isReplay := false 244 scope, closer, reporter := NewMetricsScope(&isReplay) 245 wrapperService = NewWorkflowServiceWrapper(mockService, scope) 246 return 247 } 248 249 func newPromService(t *testing.T) ( 250 mockService *workflowservicetest.MockClient, 251 wrapperService workflowserviceclient.Interface, 252 closer io.Closer, 253 reporter *CapturingStatsReporter, 254 ) { 255 mockCtrl := gomock.NewController(t) 256 mockService = workflowservicetest.NewMockClient(mockCtrl) 257 isReplay := false 258 scope, closer, reporter := newPromScope(&isReplay) 259 wrapperService = NewWorkflowServiceWrapper(mockService, scope) 260 return 261 } 262 263 func newPromScope(isReplay *bool) (tally.Scope, io.Closer, *CapturingStatsReporter) { 264 reporter := &CapturingStatsReporter{} 265 opts := tally.ScopeOptions{ 266 Reporter: reporter, 267 SanitizeOptions: &sanitizeOptions, 268 } 269 scope, closer := tally.NewRootScope(opts, time.Second) 270 return WrapScope(isReplay, scope, &realClock{}), closer, reporter 271 }