Skip to content

Commit 9cd7a7e

Browse files
guo0693yurishkuro
authored andcommitted
Update grpc & http endpoint to query operations by service & spanKind (#1943)
* Update grpc & http endpoint to query operations by service & spanKind - legacy http endpoint will stay unchanged: /services/%s/operations Signed-off-by: Jun Guo <[email protected]> * Better format Signed-off-by: Jun Guo <[email protected]> * Fix unit test Signed-off-by: Jun Guo <[email protected]> * Update changelog and address feedback Signed-off-by: Jun Guo <[email protected]> * Make grpc query server backward-compatible Signed-off-by: Jun Guo <[email protected]>
1 parent c57e4b7 commit 9cd7a7e

File tree

14 files changed

+646
-122
lines changed

14 files changed

+646
-122
lines changed

Diff for: CHANGELOG.md

+46
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,52 @@ Changes by Version
88

99
##### Breaking Changes
1010

11+
* Changes to the endpoints that returns a list of operations ([#1943](https://github.com/jaegertracing/jaeger/pull/1943), [@guo0693](https://github.com/guo0693))
12+
* Endpoint changes:
13+
* Both Http & gRPC servers now take new optional parameter `spanKind` in addition to `service`. When spanKind
14+
is absent or empty, operations from all kinds of spans will be returned.
15+
* Instead of returning a list of string, both Http & gRPC servers return a list of operation struct. Please
16+
update your client code to process the new response. Example response:
17+
```
18+
curl 'http://localhost:6686/api/operations?service=UserService&spanKind=server' | jq
19+
{
20+
"data": [{
21+
"name": "UserService::getExtendedUser",
22+
"spanKind": "server"
23+
},
24+
{
25+
"name": "UserService::getUserProfile",
26+
"spanKind": "server"
27+
}],
28+
"total": 2,
29+
"limit": 0,
30+
"offset": 0,
31+
"errors": null
32+
}
33+
```
34+
* The legacy http endpoint stay untouched:
35+
```
36+
/services/{%s}/operations
37+
```
38+
* Storage plugin changes:
39+
* Memory updated to support spanKind on write & read, no migration is required.
40+
* [Badger](https://github.com/jaegertracing/jaeger/issues/1922) & [ElasticSearch](https://github.com/jaegertracing/jaeger/issues/1923)
41+
to be implemented:
42+
For now `spanKind` will be set as empty string during read & write, only `name` will be valid operation name.
43+
* Cassandra updated to support spanKind on write & read ([#1937](https://github.com/jaegertracing/jaeger/pull/1937), [@guo0693](https://github.com/guo0693)):
44+
If you don't run the migration script, nothing will break, the system will used the old table
45+
`operation_names` and set empty `spanKind` in the response.
46+
Steps to get the updated functionality:
47+
1. You will need to run below command on the host you can use `cqlsh` to connect the the cassandra contact
48+
point
49+
```
50+
KEYSPACE=test_keyspace TIMEOUT=1000 CQL_CMD='cqlsh host 9042 -u test_user -p test_password' bash
51+
./plugin/storage/cassandra/schema/migration/v002tov003.sh
52+
```
53+
The script will create new table `operation_names_v2` and migrate data from the old table.
54+
`spanKind` column will be empty for those data.
55+
At the end, it will ask you whether you want to drop the old table or not.
56+
2. Restart ingester & query services so that they begin to use the new table
1157
##### New Features
1258

1359
##### Bug fixes, Minor Improvements

Diff for: cmd/query/app/grpc_handler.go

+20-4
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,31 @@ func (g *GRPCHandler) GetServices(ctx context.Context, r *api_v2.GetServicesRequ
128128
}
129129

130130
// GetOperations is the GRPC handler to fetch operations.
131-
func (g *GRPCHandler) GetOperations(ctx context.Context, r *api_v2.GetOperationsRequest) (*api_v2.GetOperationsResponse, error) {
132-
service := r.Service
133-
operations, err := g.queryService.GetOperations(ctx, service)
131+
func (g *GRPCHandler) GetOperations(
132+
ctx context.Context,
133+
r *api_v2.GetOperationsRequest,
134+
) (*api_v2.GetOperationsResponse, error) {
135+
operations, err := g.queryService.GetOperations(ctx, spanstore.OperationQueryParameters{
136+
ServiceName: r.Service,
137+
SpanKind: r.SpanKind,
138+
})
134139
if err != nil {
135140
g.logger.Error("Error fetching operations", zap.Error(err))
136141
return nil, err
137142
}
138143

139-
return &api_v2.GetOperationsResponse{Operations: operations}, nil
144+
result := make([]*api_v2.Operation, len(operations))
145+
for i, operation := range operations {
146+
result[i] = &api_v2.Operation{
147+
Name: operation.Name,
148+
SpanKind: operation.SpanKind,
149+
}
150+
}
151+
return &api_v2.GetOperationsResponse{
152+
Operations: result,
153+
// TODO: remove OperationNames after all clients are updated
154+
OperationNames: getUniqueOperationNames(operations),
155+
}, nil
140156
}
141157

142158
// GetDependencies is the GRPC handler to fetch dependencies.

Diff for: cmd/query/app/grpc_handler_test.go

+9-2
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,12 @@ func TestGetServicesFailureGRPC(t *testing.T) {
415415
func TestGetOperationsSuccessGRPC(t *testing.T) {
416416
withServerAndClient(t, func(server *grpcServer, client *grpcClient) {
417417

418-
expectedOperations := []spanstore.Operation{{Name: ""}, {Name: "get", SpanKind: "server"}}
418+
expectedOperations := []spanstore.Operation{
419+
{Name: ""},
420+
{Name: "get", SpanKind: "server"},
421+
{Name: "get", SpanKind: "client"},
422+
}
423+
expectedNames := []string{"", "get"}
419424
server.spanReader.On("GetOperations",
420425
mock.AnythingOfType("*context.valueCtx"),
421426
spanstore.OperationQueryParameters{ServiceName: "abc/trifle"},
@@ -427,8 +432,10 @@ func TestGetOperationsSuccessGRPC(t *testing.T) {
427432
assert.NoError(t, err)
428433
assert.Equal(t, len(expectedOperations), len(res.Operations))
429434
for i, actualOp := range res.Operations {
430-
assert.Equal(t, expectedOperations[i].Name, actualOp)
435+
assert.Equal(t, expectedOperations[i].Name, actualOp.Name)
436+
assert.Equal(t, expectedOperations[i].SpanKind, actualOp.SpanKind)
431437
}
438+
assert.ElementsMatch(t, expectedNames, res.OperationNames)
432439
})
433440
}
434441

Diff for: cmd/query/app/http_handler.go

+25-5
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,21 @@ func (aH *APIHandler) getOperationsLegacy(w http.ResponseWriter, r *http.Request
157157
vars := mux.Vars(r)
158158
// given how getOperationsLegacy is bound to URL route, serviceParam cannot be empty
159159
service, _ := url.QueryUnescape(vars[serviceParam])
160-
operations, err := aH.queryService.GetOperations(r.Context(), service)
160+
// for backwards compatibility, we will retrieve operations with all span kind
161+
operations, err := aH.queryService.GetOperations(r.Context(),
162+
spanstore.OperationQueryParameters{
163+
ServiceName: service,
164+
// include all kinds
165+
SpanKind: "",
166+
})
167+
161168
if aH.handleError(w, err, http.StatusInternalServerError) {
162169
return
163170
}
171+
operationNames := getUniqueOperationNames(operations)
164172
structuredRes := structuredResponse{
165-
Data: operations,
166-
Total: len(operations),
173+
Data: operationNames,
174+
Total: len(operationNames),
167175
}
168176
aH.writeJSON(w, r, &structuredRes)
169177
}
@@ -175,12 +183,24 @@ func (aH *APIHandler) getOperations(w http.ResponseWriter, r *http.Request) {
175183
return
176184
}
177185
}
178-
operations, err := aH.queryService.GetOperations(r.Context(), service)
186+
spanKind := r.FormValue(spanKindParam)
187+
operations, err := aH.queryService.GetOperations(
188+
r.Context(),
189+
spanstore.OperationQueryParameters{ServiceName: service, SpanKind: spanKind},
190+
)
191+
179192
if aH.handleError(w, err, http.StatusInternalServerError) {
180193
return
181194
}
195+
data := make([]ui.Operation, len(operations))
196+
for i, operation := range operations {
197+
data[i] = ui.Operation{
198+
Name: operation.Name,
199+
SpanKind: operation.SpanKind,
200+
}
201+
}
182202
structuredRes := structuredResponse{
183-
Data: operations,
203+
Data: data,
184204
Total: len(operations),
185205
}
186206
aH.writeJSON(w, r, &structuredRes)

Diff for: cmd/query/app/http_handler_test.go

+24-12
Original file line numberDiff line numberDiff line change
@@ -484,16 +484,27 @@ func TestGetOperationsSuccess(t *testing.T) {
484484
server, readMock, _ := initializeTestServer()
485485
defer server.Close()
486486
expectedOperations := []spanstore.Operation{{Name: ""}, {Name: "get", SpanKind: "server"}}
487-
readMock.On("GetOperations", mock.AnythingOfType("*context.valueCtx"),
488-
spanstore.OperationQueryParameters{ServiceName: "abc/trifle"}).Return(expectedOperations, nil).Once()
487+
readMock.On(
488+
"GetOperations",
489+
mock.AnythingOfType("*context.valueCtx"),
490+
spanstore.OperationQueryParameters{ServiceName: "abc/trifle"},
491+
).Return(expectedOperations, nil).Once()
492+
493+
var response struct {
494+
Operations []ui.Operation `json:"data"`
495+
Total int `json:"total"`
496+
Limit int `json:"limit"`
497+
Offset int `json:"offset"`
498+
Errors []structuredError `json:"errors"`
499+
}
489500

490-
var response structuredResponse
491501
err := getJSON(server.URL+"/api/operations?service=abc%2Ftrifle", &response)
492502
assert.NoError(t, err)
493-
for i, s := range response.Data.([]interface{}) {
494-
assert.Equal(t, expectedOperations[i].Name, s.(string))
503+
assert.Equal(t, len(expectedOperations), len(response.Operations))
504+
for i, op := range response.Operations {
505+
assert.Equal(t, expectedOperations[i].Name, op.Name)
506+
assert.Equal(t, expectedOperations[i].SpanKind, op.SpanKind)
495507
}
496-
497508
}
498509

499510
func TestGetOperationsNoServiceName(t *testing.T) {
@@ -522,20 +533,21 @@ func TestGetOperationsLegacySuccess(t *testing.T) {
522533
server, readMock, _ := initializeTestServer()
523534
defer server.Close()
524535
expectedOperationNames := []string{"", "get"}
525-
expectedOperations := []spanstore.Operation{{Name: ""}, {Name: "get", SpanKind: "server"}}
536+
expectedOperations := []spanstore.Operation{
537+
{Name: ""},
538+
{Name: "get", SpanKind: "server"},
539+
{Name: "get", SpanKind: "client"}}
540+
526541
readMock.On(
527542
"GetOperations",
528543
mock.AnythingOfType("*context.valueCtx"),
529544
mock.AnythingOfType("spanstore.OperationQueryParameters")).Return(expectedOperations, nil).Once()
530545

531546
var response structuredResponse
532547
err := getJSON(server.URL+"/api/services/abc%2Ftrifle/operations", &response)
548+
533549
assert.NoError(t, err)
534-
actualOperations := make([]string, len(expectedOperations))
535-
for i, s := range response.Data.([]interface{}) {
536-
actualOperations[i] = s.(string)
537-
}
538-
assert.Equal(t, expectedOperationNames, actualOperations)
550+
assert.ElementsMatch(t, expectedOperationNames, response.Data.([]interface{}))
539551
}
540552

541553
func TestGetOperationsLegacyStorageFailure(t *testing.T) {

Diff for: cmd/query/app/query_parser.go

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const (
4040
minDurationParam = "minDuration"
4141
maxDurationParam = "maxDuration"
4242
serviceParam = "service"
43+
spanKindParam = "spanKind"
4344
endTimeParam = "end"
4445
prettyPrintParam = "prettyPrint"
4546
)

Diff for: cmd/query/app/querysvc/query_service.go

+5-15
Original file line numberDiff line numberDiff line change
@@ -79,21 +79,11 @@ func (qs QueryService) GetServices(ctx context.Context) ([]string, error) {
7979
}
8080

8181
// GetOperations is the queryService implementation of spanstore.Reader.GetOperations
82-
func (qs QueryService) GetOperations(ctx context.Context, service string) ([]string, error) {
83-
operations, err := qs.spanReader.GetOperations(ctx, spanstore.OperationQueryParameters{
84-
ServiceName: service,
85-
})
86-
87-
// TODO: remove below and simply return the result from query service
88-
if err != nil {
89-
return nil, err
90-
}
91-
92-
names := make([]string, len(operations))
93-
for i, operation := range operations {
94-
names[i] = operation.Name
95-
}
96-
return names, err
82+
func (qs QueryService) GetOperations(
83+
ctx context.Context,
84+
query spanstore.OperationQueryParameters,
85+
) ([]spanstore.Operation, error) {
86+
return qs.spanReader.GetOperations(ctx, query)
9787
}
9888

9989
// FindTraces is the queryService implementation of spanstore.Reader.FindTraces

Diff for: cmd/query/app/querysvc/query_service_test.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -147,17 +147,19 @@ func TestGetServices(t *testing.T) {
147147
// Test QueryService.GetOperations() for success.
148148
func TestGetOperations(t *testing.T) {
149149
qs, readMock, _ := initializeTestService()
150-
expectedOperationNames := []string{"", "get"}
151-
expectedOperations := []spanstore.Operation{{Name: ""}, {Name: "get", SpanKind: "server"}}
152-
readMock.On("GetOperations",
150+
expectedOperations := []spanstore.Operation{{Name: "", SpanKind: ""}, {Name: "get", SpanKind: ""}}
151+
operationQuery := spanstore.OperationQueryParameters{ServiceName: "abc/trifle"}
152+
readMock.On(
153+
"GetOperations",
153154
mock.AnythingOfType("*context.valueCtx"),
154-
mock.AnythingOfType("spanstore.OperationQueryParameters")).Return(expectedOperations, nil).Once()
155+
operationQuery,
156+
).Return(expectedOperations, nil).Once()
155157

156158
type contextKey string
157159
ctx := context.Background()
158-
actualOperations, err := qs.GetOperations(context.WithValue(ctx, contextKey("foo"), "bar"), "abc/trifle")
160+
actualOperations, err := qs.GetOperations(context.WithValue(ctx, contextKey("foo"), "bar"), operationQuery)
159161
assert.NoError(t, err)
160-
assert.Equal(t, expectedOperationNames, actualOperations)
162+
assert.Equal(t, expectedOperations, actualOperations)
161163
}
162164

163165
// Test QueryService.FindTraces() for success.

Diff for: cmd/query/app/util.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) 2019 The Jaeger Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package app
16+
17+
import "github.com/jaegertracing/jaeger/storage/spanstore"
18+
19+
func getUniqueOperationNames(operations []spanstore.Operation) []string {
20+
// only return unique operation names
21+
set := make(map[string]struct{})
22+
for _, operation := range operations {
23+
set[operation.Name] = struct{}{}
24+
}
25+
var operationNames []string
26+
for operation := range set {
27+
operationNames = append(operationNames, operation)
28+
}
29+
return operationNames
30+
}

Diff for: cmd/query/app/util_test.go

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) 2019 The Jaeger Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package app
16+
17+
import (
18+
"testing"
19+
20+
"github.com/stretchr/testify/assert"
21+
22+
"github.com/jaegertracing/jaeger/storage/spanstore"
23+
)
24+
25+
func Test_getUniqueOperationNames(t *testing.T) {
26+
assert.Equal(t, []string(nil), getUniqueOperationNames(nil))
27+
28+
operations := []spanstore.Operation{
29+
{Name: "operation1", SpanKind: "server"},
30+
{Name: "operation1", SpanKind: "client"},
31+
{Name: "operation2"},
32+
}
33+
34+
expNames := []string{"operation1", "operation2"}
35+
names := getUniqueOperationNames(operations)
36+
assert.Equal(t, 2, len(names))
37+
assert.ElementsMatch(t, expNames, names)
38+
}

Diff for: model/json/model.go

+6
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,9 @@ type DependencyLink struct {
109109
Child string `json:"child"`
110110
CallCount uint64 `json:"callCount"`
111111
}
112+
113+
// Operation defines the data in the operation response when query operation by service and span kind
114+
type Operation struct {
115+
Name string `json:"name"`
116+
SpanKind string `json:"spanKind"`
117+
}

Diff for: model/proto/api_v2/query.proto

+8-1
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,17 @@ message GetServicesResponse {
109109

110110
message GetOperationsRequest {
111111
string service = 1;
112+
string span_kind = 2;
113+
}
114+
115+
message Operation {
116+
string name = 1;
117+
string span_kind = 2;
112118
}
113119

114120
message GetOperationsResponse {
115-
repeated string operations = 1;
121+
repeated string operationNames = 1; //deprecated
122+
repeated Operation operations = 2;
116123
}
117124

118125
message GetDependenciesRequest {

0 commit comments

Comments
 (0)