Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user agent metrics #492

Merged
merged 5 commits into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions admin/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import (

"google.golang.org/grpc"
"google.golang.org/grpc/metadata"

"github.com/yorkie-team/yorkie/api/types"
"github.com/yorkie-team/yorkie/internal/version"
)

// AuthInterceptor is an interceptor for authentication.
Expand Down Expand Up @@ -52,7 +55,8 @@ func (i *AuthInterceptor) Unary() grpc.UnaryClientInterceptor {
opts ...grpc.CallOption,
) error {
ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs(
"authorization", i.token,
types.AuthorizationKey, i.token,
types.UserAgentKey, types.GoSDKType+"/"+version.Version,
))
return invoker(ctx, method, req, reply, cc, opts...)
}
Expand All @@ -69,7 +73,8 @@ func (i *AuthInterceptor) Stream() grpc.StreamClientInterceptor {
opts ...grpc.CallOption,
) (grpc.ClientStream, error) {
ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs(
"authorization", i.token,
types.AuthorizationKey, i.token,
types.UserAgentKey, types.GoSDKType+"/"+version.Version,
))
return streamer(ctx, desc, cc, method, opts...)
}
Expand Down
30 changes: 30 additions & 0 deletions api/types/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2023 The Yorkie Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package types

// AuthorizationKey is the key of the authorization header.
const AuthorizationKey = "authorization"

// UserAgentKey is the key of the user agent header.
const UserAgentKey = "x-yorkie-user-agent"

// APIKeyKey is the key of the api key header.
const APIKeyKey = "x-api-key"

// GoSDKType is the type part of Go SDK in value of UserAgent.
const GoSDKType = "yorkie-go-sdk"
13 changes: 9 additions & 4 deletions client/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import (

"google.golang.org/grpc"
"google.golang.org/grpc/metadata"

"github.com/yorkie-team/yorkie/api/types"
"github.com/yorkie-team/yorkie/internal/version"
)

// AuthInterceptor is an interceptor for authentication.
Expand Down Expand Up @@ -49,8 +52,9 @@ func (i *AuthInterceptor) Unary() grpc.UnaryClientInterceptor {
opts ...grpc.CallOption,
) error {
ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs(
"x-api-key", i.apiKey,
"authorization", i.token,
types.APIKeyKey, i.apiKey,
types.AuthorizationKey, i.token,
types.UserAgentKey, types.GoSDKType+"/"+version.Version,
))
return invoker(ctx, method, req, reply, cc, opts...)
}
Expand All @@ -67,8 +71,9 @@ func (i *AuthInterceptor) Stream() grpc.StreamClientInterceptor {
opts ...grpc.CallOption,
) (grpc.ClientStream, error) {
ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs(
"x-api-key", i.apiKey,
"authorization", i.token,
types.APIKeyKey, i.apiKey,
types.AuthorizationKey, i.token,
types.UserAgentKey, types.GoSDKType+"/"+version.Version,
))
return streamer(ctx, desc, cc, method, opts...)
}
Expand Down
6 changes: 6 additions & 0 deletions cmd/yorkie/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,12 @@ func init() {
server.DefaultAuthWebhookCacheUnauthTTL,
"TTL value to set when caching unauthorized webhook response.",
)
cmd.Flags().StringVar(
&conf.Backend.Hostname,
"hostname",
server.DefaultHostname,
"Yorkie Server Hostname",
)

rootCmd.AddCommand(cmd)
}
42 changes: 35 additions & 7 deletions server/admin/interceptors/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/yorkie-team/yorkie/api/types"
"github.com/yorkie-team/yorkie/server/admin/auth"
"github.com/yorkie-team/yorkie/server/backend"
"github.com/yorkie-team/yorkie/server/grpchelper"
"github.com/yorkie-team/yorkie/server/users"
)

Expand Down Expand Up @@ -59,11 +60,24 @@ func (i *AuthInterceptor) Unary() grpc.UnaryServerInterceptor {
if err != nil {
return nil, err
}
ctx = users.With(ctx, user)
}

return handler(users.With(ctx, user), req)
resp, err = handler(ctx, req)

// TODO(hackerwins, emplam27): Consider splitting between admin and sdk metrics.
data, ok := grpcmetadata.FromIncomingContext(ctx)
if ok {
sdkType, sdkVersion := grpchelper.SDKTypeAndVersion(data)
i.backend.Metrics.AddUserAgentWithEmptyProject(
i.backend.Config.Hostname,
sdkType,
sdkVersion,
info.FullMethod,
)
}

return handler(ctx, req)
return resp, err
}
}

Expand All @@ -74,20 +88,34 @@ func (i *AuthInterceptor) Stream() grpc.StreamServerInterceptor {
stream grpc.ServerStream,
info *grpc.StreamServerInfo,
handler grpc.StreamHandler,
) error {
) (err error) {
ctx := stream.Context()
if isRequiredAuth(info.FullMethod) {
ctx := stream.Context()
user, err := i.authenticate(ctx, info.FullMethod)
if err != nil {
return err
}

wrapped := grpcmiddleware.WrapServerStream(stream)
wrapped.WrappedContext = users.With(ctx, user)
return handler(srv, wrapped)
stream = wrapped
}

err = handler(srv, stream)

// TODO(hackerwins, emplam27): Consider splitting between admin and sdk metrics.
data, ok := grpcmetadata.FromIncomingContext(ctx)
if ok {
sdkType, sdkVersion := grpchelper.SDKTypeAndVersion(data)
i.backend.Metrics.AddUserAgentWithEmptyProject(
i.backend.Config.Hostname,
sdkType,
sdkVersion,
info.FullMethod,
)
}

return handler(srv, stream)
return err
}
}

Expand All @@ -113,7 +141,7 @@ func (i *AuthInterceptor) authenticate(
return nil, grpcstatus.Errorf(codes.Unauthenticated, "metadata is not provided")
}

authorization := data["authorization"]
authorization := data[types.AuthorizationKey]
if len(authorization) == 0 {
return nil, grpcstatus.Errorf(codes.Unauthenticated, "authorization is not provided")
}
Expand Down
4 changes: 3 additions & 1 deletion server/admin/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ func (s *Server) CreateProject(
if err != nil {
return nil, err
}

return &api.CreateProjectResponse{
Project: pbProject,
}, nil
Expand All @@ -225,7 +226,7 @@ func (s *Server) CreateProject(
// ListProjects lists all projects.
func (s *Server) ListProjects(
ctx context.Context,
req *api.ListProjectsRequest,
_ *api.ListProjectsRequest,
) (*api.ListProjectsResponse, error) {
user := users.From(ctx)
projectList, err := projects.ListProjects(ctx, s.backend, user.ID)
Expand Down Expand Up @@ -258,6 +259,7 @@ func (s *Server) GetProject(
if err != nil {
return nil, err
}

return &api.GetProjectResponse{
Project: pbProject,
}, nil
Expand Down
11 changes: 8 additions & 3 deletions server/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,13 @@ func New(
clusterAddr string,
metrics *prometheus.Metrics,
) (*Backend, error) {
hostname, err := os.Hostname()
if err != nil {
return nil, fmt.Errorf("get hostname: %w", err)
hostname := conf.Hostname
if hostname == "" {
hostname, err := os.Hostname()
Copy link
Member

@krapie krapie Apr 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hostname value inside if statement shadows hostname in line 68.
Therefore hostname will not be updated.

I'll fix this in #521

if err != nil {
return nil, fmt.Errorf("os.Hostname: %w", err)
}
conf.Hostname = hostname
}

serverInfo := &sync.ServerInfo{
Expand All @@ -80,6 +84,7 @@ func New(
bg := background.New()

var db database.Database
var err error
if mongoConf != nil {
db, err = mongo.Dial(mongoConf)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions server/backend/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ type Config struct {

// AuthWebhookCacheUnauthTTL is the TTL value to set when caching the unauthorized result.
AuthWebhookCacheUnauthTTL string `yaml:"AuthWebhookCacheUnauthTTL"`

// Hostname is yorkie server hostname. hostname is used by metrics.
Hostname string `yaml:"Hostname"`
}

// Validate validates this config.
Expand Down
2 changes: 2 additions & 0 deletions server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ const (
DefaultAuthWebhookCacheSize = 5000
DefaultAuthWebhookCacheAuthTTL = 10 * time.Second
DefaultAuthWebhookCacheUnauthTTL = 10 * time.Second

DefaultHostname = ""
)

// Config is the configuration for creating a Yorkie instance.
Expand Down
4 changes: 4 additions & 0 deletions server/config.sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ Backend:
# AuthWebhookCacheUnauthTTL is the TTL value to set when caching the unauthorized result.
AuthWebhookCacheUnauthTTL: "10s"

# Hostname is the hostname of the server. If not provided, the hostname will be
# determined automatically by the OS (Optional, default: os.Hostname()).
Hostname: ""

# Mongo is the MongoDB configuration (Optional).
Mongo:
# ConnectionTimeout is the timeout for connecting to MongoDB.
Expand Down
38 changes: 38 additions & 0 deletions server/grpchelper/useragent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2023 The Yorkie Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package grpchelper

import (
"strings"

grpcmetadata "google.golang.org/grpc/metadata"

"github.com/yorkie-team/yorkie/api/types"
)

// SDKTypeAndVersion returns the type and version of the SDK from the given
// metadata.
func SDKTypeAndVersion(data grpcmetadata.MD) (string, string) {
yorkieUserAgentSlice := data[types.UserAgentKey]
if len(yorkieUserAgentSlice) == 0 {
return "", ""
}

yorkieUserAgent := yorkieUserAgentSlice[0]
agent := strings.Split(yorkieUserAgent, "/")
return agent[0], agent[1]
}
54 changes: 53 additions & 1 deletion server/profiling/prometheus/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,26 @@ import (
"github.com/prometheus/client_golang/prometheus/promauto"
"google.golang.org/grpc"

"github.com/yorkie-team/yorkie/api/types"
"github.com/yorkie-team/yorkie/internal/version"
)

const (
namespace = "yorkie"
namespace = "yorkie"
sdkTypeLabel = "sdk_type"
sdkVersionLabel = "sdk_version"
methodLabel = "grpc_method"
projectIDLabel = "project_id"
projectNameLabel = "project_name"
hostnameLabel = "hostname"
)

var (
// emptyProject is used when the project is not specified.
emptyProject = &types.Project{
Name: "",
ID: types.ID(""),
}
)

// Metrics manages the metric information that Yorkie is trying to measure.
Expand All @@ -47,6 +62,8 @@ type Metrics struct {
pushPullSentOperationsTotal prometheus.Counter
pushPullSnapshotDurationSeconds prometheus.Histogram
pushPullSnapshotBytesTotal prometheus.Counter

userAgentTotal *prometheus.CounterVec
}

// NewMetrics creates a new instance of Metrics.
Expand Down Expand Up @@ -118,6 +135,19 @@ func NewMetrics() (*Metrics, error) {
Name: "snapshot_bytes_total",
Help: "The total bytes of snapshots for response packs in PushPull.",
}),
userAgentTotal: promauto.With(reg).NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: "user_agent",
Name: "total",
Help: "description",
}, []string{
sdkTypeLabel,
sdkVersionLabel,
methodLabel,
projectIDLabel,
projectNameLabel,
hostnameLabel,
}),
}

metrics.serverVersion.With(prometheus.Labels{
Expand Down Expand Up @@ -168,6 +198,28 @@ func (m *Metrics) AddPushPullSnapshotBytes(bytes int) {
m.pushPullSnapshotBytesTotal.Add(float64(bytes))
}

// AddUserAgent adds the number of user agent.
func (m *Metrics) AddUserAgent(
hostname string,
project *types.Project,
sdkType, sdkVersion string,
methodName string,
) {
m.userAgentTotal.With(prometheus.Labels{
sdkTypeLabel: sdkType,
sdkVersionLabel: sdkVersion,
methodLabel: methodName,
projectIDLabel: project.ID.String(),
projectNameLabel: project.Name,
hostnameLabel: hostname,
}).Inc()
}

// AddUserAgentWithEmptyProject adds the number of user agent with empty project.
func (m *Metrics) AddUserAgentWithEmptyProject(hostname string, sdkType, sdkVersion, methodName string) {
m.AddUserAgent(hostname, emptyProject, sdkType, sdkVersion, methodName)
}

// RegisterGRPCServer registers the given gRPC server.
func (m *Metrics) RegisterGRPCServer(server *grpc.Server) {
m.serverMetrics.InitializeMetrics(server)
Expand Down
Loading