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 support for distributed tracing #1019

Merged
merged 16 commits into from
Sep 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
328 changes: 301 additions & 27 deletions Gopkg.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,7 @@
[[constraint]]
name = "github.com/prometheus/client_golang"
version = "0.8.0"

[[constraint]]
name = "github.com/opentracing/opentracing-go"
version = "1.0.2"
18 changes: 18 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,24 @@ func initConfig() {
viper.BindEnv("OIDC_SUBJECT_TYPE_PAIRWISE_SALT")
viper.SetDefault("OIDC_SUBJECT_TYPE_PAIRWISE_SALT", "public")

viper.BindEnv("TRACING_PROVIDER")
viper.SetDefault("TRACING_PROVIDER", "")

viper.BindEnv("TRACING_PROVIDER_JAEGER_SAMPLING_SERVER_URL")
viper.SetDefault("TRACING_PROVIDER_JAEGER_SAMPLING_SERVER_URL", "")

viper.BindEnv("TRACING_PROVIDER_JAEGER_SAMPLING_TYPE")
viper.SetDefault("TRACING_PROVIDER_JAEGER_SAMPLING_TYPE", "const")

viper.BindEnv("TRACING_PROVIDER_JAEGER_SAMPLING_VALUE")
viper.SetDefault("TRACING_PROVIDER_JAEGER_SAMPLING_VALUE", float64(1))

viper.BindEnv("TRACING_PROVIDER_JAEGER_LOCAL_AGENT_HOST_PORT")
viper.SetDefault("TRACING_PROVIDER_JAEGER_LOCAL_AGENT_HOST_PORT", "")

viper.BindEnv("TRACING_SERVICE_NAME")
viper.SetDefault("TRACING_SERVICE_NAME", "Ory Hydra")

// If a config file is found, read it in.
if err := viper.ReadInConfig(); err != nil {
fmt.Printf(`Config file not found because "%s"`, err)
Expand Down
9 changes: 9 additions & 0 deletions cmd/server/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ func setup(c *config.Config, cmd *cobra.Command, args []string, name string) (ha
handler = NewHandler(c, w)
handler.RegisterRoutes(frontend, backend)
c.ForceHTTP, _ = cmd.Flags().GetBool("dangerous-force-http")
if tracer, err := c.GetTracer(); err != nil {
c.GetLogger().Fatalf("Failed to initialize tracer: %s", err)
} else if tracer.IsLoaded() {
middlewares = append(middlewares, tracer)
}

if !c.ForceHTTP {
if c.Issuer == "" {
Expand Down Expand Up @@ -208,6 +213,10 @@ func serve(c *config.Config, cmd *cobra.Command, handler http.Handler, address s
},
})

if tracer, err := c.GetTracer(); err == nil && tracer.IsLoaded() {
srv.RegisterOnShutdown(tracer.Close)
}

err := graceful.Graceful(func() error {
var err error
c.GetLogger().Infof("Setting up http server on %s", address)
Expand Down
96 changes: 63 additions & 33 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"github.com/ory/hydra/health"
"github.com/ory/hydra/metrics/prometheus"
"github.com/ory/hydra/pkg"
"github.com/ory/hydra/tracing"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
Expand All @@ -53,43 +54,50 @@ type Config struct {
EndpointURL string `mapstructure:"HYDRA_URL" yaml:"-"`

// These are used by the host command
FrontendBindPort int `mapstructure:"PUBLIC_PORT" yaml:"-"`
FrontendBindHost string `mapstructure:"PUBLIC_HOST" yaml:"-"`
BackendBindPort int `mapstructure:"ADMIN_PORT" yaml:"-"`
BackendBindHost string `mapstructure:"ADMIN_HOST" yaml:"-"`
Issuer string `mapstructure:"OAUTH2_ISSUER_URL" yaml:"-"`
SystemSecret string `mapstructure:"SYSTEM_SECRET" yaml:"-"`
RotatedSystemSecret string `mapstructure:"ROTATED_SYSTEM_SECRET" yaml:"-"`
DatabaseURL string `mapstructure:"DATABASE_URL" yaml:"-"`
DatabasePlugin string `mapstructure:"DATABASE_PLUGIN" yaml:"-"`
ConsentURL string `mapstructure:"OAUTH2_CONSENT_URL" yaml:"-"`
LoginURL string `mapstructure:"OAUTH2_LOGIN_URL" yaml:"-"`
LogoutRedirectURL string `mapstructure:"OAUTH2_LOGOUT_REDIRECT_URL" yaml:"-"`
DefaultClientScope string `mapstructure:"OIDC_DYNAMIC_CLIENT_REGISTRATION_DEFAULT_SCOPE" yaml:"-"`
ErrorURL string `mapstructure:"OAUTH2_ERROR_URL" yaml:"-"`
AllowTLSTermination string `mapstructure:"HTTPS_ALLOW_TERMINATION_FROM" yaml:"-"`
BCryptWorkFactor int `mapstructure:"BCRYPT_COST" yaml:"-"`
AccessTokenLifespan string `mapstructure:"ACCESS_TOKEN_LIFESPAN" yaml:"-"`
ScopeStrategy string `mapstructure:"SCOPE_STRATEGY" yaml:"-"`
AuthCodeLifespan string `mapstructure:"AUTH_CODE_LIFESPAN" yaml:"-"`
IDTokenLifespan string `mapstructure:"ID_TOKEN_LIFESPAN" yaml:"-"`
ChallengeTokenLifespan string `mapstructure:"CHALLENGE_TOKEN_LIFESPAN" yaml:"-"`
CookieSecret string `mapstructure:"COOKIE_SECRET" yaml:"-"`
LogLevel string `mapstructure:"LOG_LEVEL" yaml:"-"`
LogFormat string `mapstructure:"LOG_FORMAT" yaml:"-"`
AccessControlResourcePrefix string `mapstructure:"RESOURCE_NAME_PREFIX" yaml:"-"`
SubjectTypesSupported string `mapstructure:"OIDC_SUBJECT_TYPES_SUPPORTED" yaml:"-"`
SubjectIdentifierAlgorithmSalt string `mapstructure:"OIDC_SUBJECT_TYPE_PAIRWISE_SALT" yaml:"-"`
OpenIDDiscoveryClaimsSupported string `mapstructure:"OIDC_DISCOVERY_CLAIMS_SUPPORTED" yaml:"-"`
OpenIDDiscoveryScopesSupported string `mapstructure:"OIDC_DISCOVERY_SCOPES_SUPPORTED" yaml:"-"`
OpenIDDiscoveryUserinfoEndpoint string `mapstructure:"OIDC_DISCOVERY_USERINFO_ENDPOINT" yaml:"-"`
SendOAuth2DebugMessagesToClients bool `mapstructure:"OAUTH2_SHARE_ERROR_DEBUG" yaml:"-"`
OAuth2AccessTokenStrategy string `mapstructure:"OAUTH2_ACCESS_TOKEN_STRATEGY" yaml:"-"`
ForceHTTP bool `yaml:"-"`
FrontendBindPort int `mapstructure:"PUBLIC_PORT" yaml:"-"`
FrontendBindHost string `mapstructure:"PUBLIC_HOST" yaml:"-"`
BackendBindPort int `mapstructure:"ADMIN_PORT" yaml:"-"`
BackendBindHost string `mapstructure:"ADMIN_HOST" yaml:"-"`
Issuer string `mapstructure:"OAUTH2_ISSUER_URL" yaml:"-"`
SystemSecret string `mapstructure:"SYSTEM_SECRET" yaml:"-"`
RotatedSystemSecret string `mapstructure:"ROTATED_SYSTEM_SECRET" yaml:"-"`
DatabaseURL string `mapstructure:"DATABASE_URL" yaml:"-"`
DatabasePlugin string `mapstructure:"DATABASE_PLUGIN" yaml:"-"`
ConsentURL string `mapstructure:"OAUTH2_CONSENT_URL" yaml:"-"`
LoginURL string `mapstructure:"OAUTH2_LOGIN_URL" yaml:"-"`
LogoutRedirectURL string `mapstructure:"OAUTH2_LOGOUT_REDIRECT_URL" yaml:"-"`
DefaultClientScope string `mapstructure:"OIDC_DYNAMIC_CLIENT_REGISTRATION_DEFAULT_SCOPE" yaml:"-"`
ErrorURL string `mapstructure:"OAUTH2_ERROR_URL" yaml:"-"`
AllowTLSTermination string `mapstructure:"HTTPS_ALLOW_TERMINATION_FROM" yaml:"-"`
BCryptWorkFactor int `mapstructure:"BCRYPT_COST" yaml:"-"`
AccessTokenLifespan string `mapstructure:"ACCESS_TOKEN_LIFESPAN" yaml:"-"`
ScopeStrategy string `mapstructure:"SCOPE_STRATEGY" yaml:"-"`
AuthCodeLifespan string `mapstructure:"AUTH_CODE_LIFESPAN" yaml:"-"`
IDTokenLifespan string `mapstructure:"ID_TOKEN_LIFESPAN" yaml:"-"`
ChallengeTokenLifespan string `mapstructure:"CHALLENGE_TOKEN_LIFESPAN" yaml:"-"`
CookieSecret string `mapstructure:"COOKIE_SECRET" yaml:"-"`
LogLevel string `mapstructure:"LOG_LEVEL" yaml:"-"`
LogFormat string `mapstructure:"LOG_FORMAT" yaml:"-"`
AccessControlResourcePrefix string `mapstructure:"RESOURCE_NAME_PREFIX" yaml:"-"`
SubjectTypesSupported string `mapstructure:"OIDC_SUBJECT_TYPES_SUPPORTED" yaml:"-"`
SubjectIdentifierAlgorithmSalt string `mapstructure:"OIDC_SUBJECT_TYPE_PAIRWISE_SALT" yaml:"-"`
OpenIDDiscoveryClaimsSupported string `mapstructure:"OIDC_DISCOVERY_CLAIMS_SUPPORTED" yaml:"-"`
OpenIDDiscoveryScopesSupported string `mapstructure:"OIDC_DISCOVERY_SCOPES_SUPPORTED" yaml:"-"`
OpenIDDiscoveryUserinfoEndpoint string `mapstructure:"OIDC_DISCOVERY_USERINFO_ENDPOINT" yaml:"-"`
SendOAuth2DebugMessagesToClients bool `mapstructure:"OAUTH2_SHARE_ERROR_DEBUG" yaml:"-"`
OAuth2AccessTokenStrategy string `mapstructure:"OAUTH2_ACCESS_TOKEN_STRATEGY" yaml:"-"`
TracingProvider string `mapstructure:"TRACING_PROVIDER" yaml:"-"`
TracingServiceName string `mapstructure:"TRACING_SERVICE_NAME" yaml:"-"`
JaegerSamplingServerUrl string `mapstructure:"TRACING_PROVIDER_JAEGER_SAMPLING_SERVER_URL" yaml:"-"`
JaegerLocalAgentHostPort string `mapstructure:"TRACING_PROVIDER_JAEGER_LOCAL_AGENT_HOST_PORT" yaml:"-"`
JaegerSamplingType string `mapstructure:"TRACING_PROVIDER_JAEGER_SAMPLING_TYPE" yaml:"-"`
JaegerSamplingValue float64 `mapstructure:"TRACING_PROVIDER_JAEGER_SAMPLING_VALUE" yaml:"-"`
ForceHTTP bool `yaml:"-"`

BuildVersion string `yaml:"-"`
BuildHash string `yaml:"-"`
BuildTime string `yaml:"-"`
tracer *tracing.Tracer `yaml:"-"`
logger *logrus.Logger `yaml:"-"`
prometheus *prometheus.MetricsManager `yaml:"-"`
cluster *url.URL `yaml:"-"`
Expand Down Expand Up @@ -187,6 +195,28 @@ func (c *Config) GetLogger() *logrus.Logger {
return c.logger
}

func (c *Config) GetTracer() (*tracing.Tracer, error) {
if c.tracer == nil {
c.GetLogger().Info("Setting up tracing middleware")

c.tracer = &tracing.Tracer{
ServiceName: c.TracingServiceName,
JaegerConfig: &tracing.JaegerConfig{
LocalAgentHostPort: c.JaegerLocalAgentHostPort,
SamplerType: c.JaegerSamplingType,
SamplerValue: c.JaegerSamplingValue,
SamplerServerUrl: c.JaegerSamplingServerUrl,
},
Provider: c.TracingProvider,
Logger: c.GetLogger(),
}

return c.tracer, c.tracer.Setup()
}

return c.tracer, nil
}

func (c *Config) GetPrometheusMetrics() *prometheus.MetricsManager {
c.GetLogger().Info("Setting up Prometheus middleware")

Expand Down
22 changes: 22 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,28 @@ func TestDoesRequestSatisfyTermination(t *testing.T) {
assert.NoError(t, c.DoesRequestSatisfyTermination(r))
}

func TestTracingSetup(t *testing.T) {
// tracer is not loaded if an unknown tracing provider is specified
c := &Config{TracingProvider: "some_unsupported_tracing_provider"}
tracer, _ := c.GetTracer()
assert.False(t, tracer.IsLoaded())

// tracer is not loaded if no tracing provider is specified
c = &Config{TracingProvider: ""}
tracer, _ = c.GetTracer()
assert.False(t, tracer.IsLoaded())

// tracer is loaded if configured properly
c = &Config{
TracingProvider: "jaeger",
TracingServiceName: "Ory Hydra",
JaegerSamplingServerUrl: "http://localhost:5778/sampling",
JaegerLocalAgentHostPort: "127.0.0.1:6831",
}
tracer, _ = c.GetTracer()
assert.True(t, tracer.IsLoaded())
}

func TestSystemSecret(t *testing.T) {
c3 := &Config{}
assert.EqualValues(t, c3.GetSystemSecret(), c3.GetSystemSecret())
Expand Down
42 changes: 42 additions & 0 deletions tracing/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package tracing

import (
"net/http"

"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/urfave/negroni"
)

func (t *Tracer) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
var span opentracing.Span
opName := r.URL.Path

// It's very possible that Hydra is fronted by a proxy which could have initiated a trace.
// If so, we should attempt to join it.
remoteContext, err := opentracing.GlobalTracer().Extract(
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(r.Header),
)

if err != nil {
span = opentracing.StartSpan(opName)
} else {
span = opentracing.StartSpan(opName, opentracing.ChildOf(remoteContext))
}

defer span.Finish()

r = r.WithContext(opentracing.ContextWithSpan(r.Context(), span))

next(rw, r)

ext.HTTPMethod.Set(span, r.Method)
if negroniWriter, ok := rw.(negroni.ResponseWriter); ok {
Copy link
Member

Choose a reason for hiding this comment

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

Not sure about this line, are you sure this is negroni? I think it would be better do define your own interface here or use one from the standard library

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am sure that this is the negroni response writer because of https://github.com/urfave/negroni/blob/master/negroni.go#L96

https://github.com/urfave/negroni/blob/master/response_writer.go#L31

With that said, to be extra defensive, I capture the boolean returned from the type assertion to verify if the ResponseWriter is indeed of type negroini.ResponseWriter. All of this is so we can add the tag in our span for the http status that was returned:

image

Copy link
Member

Choose a reason for hiding this comment

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

Ok I see how this works now. This is a negroni middleware so we're getting the correct RW here. I though it worked differently, my bad :)

statusCode := uint16(negroniWriter.Status())
if statusCode >= 400 {
ext.Error.Set(span, true)
}
ext.HTTPStatusCode.Set(span, statusCode)
}
}
97 changes: 97 additions & 0 deletions tracing/middleware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package tracing_test

import (
"net/http"
"net/http/httptest"

"github.com/opentracing/opentracing-go/ext"
"github.com/opentracing/opentracing-go/mocktracer"

"testing"

"github.com/opentracing/opentracing-go"
"github.com/ory/hydra/tracing"
"github.com/stretchr/testify/assert"
"github.com/urfave/negroni"
)

var mockedTracer *mocktracer.MockTracer
var tracer *tracing.Tracer = &tracing.Tracer{
ServiceName: "Ory Hydra Test",
Provider: "Mock Provider",
}

func init() {
mockedTracer = mocktracer.New()
opentracing.SetGlobalTracer(mockedTracer)
}

func TestTracingServeHttp(t *testing.T) {
expectedTagsSuccess := map[string]interface{}{
string(ext.HTTPStatusCode): uint16(200),
string(ext.HTTPMethod): "GET",
}

expectedTagsError := map[string]interface{}{
string(ext.HTTPStatusCode): uint16(400),
string(ext.HTTPMethod): "GET",
"error": true,
}

testCases := []struct {
httpStatus int
testDescription string
expectedTags map[string]interface{}
}{
{
testDescription: "success http response",
httpStatus: http.StatusOK,
expectedTags: expectedTagsSuccess,
},
{
testDescription: "error http response",
httpStatus: http.StatusBadRequest,
expectedTags: expectedTagsError,
},
}

for _, test := range testCases {
t.Run(test.testDescription, func(t *testing.T) {
defer mockedTracer.Reset()
request := httptest.NewRequest(http.MethodGet, "https://apis.somecompany.com/endpoint", nil)
next := func(rw http.ResponseWriter, _ *http.Request) {
rw.WriteHeader(test.httpStatus)
}

tracer.ServeHTTP(negroni.NewResponseWriter(httptest.NewRecorder()), request, next)

spans := mockedTracer.FinishedSpans()
assert.Len(t, spans, 1)
span := spans[0]

assert.Equal(t, test.expectedTags, span.Tags())
})
}
}

func TestShouldContinueTraceIfAlreadyPresent(t *testing.T) {
defer mockedTracer.Reset()
parentSpan := mockedTracer.StartSpan("some-operation").(*mocktracer.MockSpan)
ext.SpanKindRPCClient.Set(parentSpan)
request := httptest.NewRequest(http.MethodGet, "https://apis.somecompany.com/endpoint", nil)
carrier := opentracing.HTTPHeadersCarrier(request.Header)
// this request now contains a trace initiated by another service/process (e.g. an edge proxy that fronts Hydra)
mockedTracer.Inject(parentSpan.Context(), opentracing.HTTPHeaders, carrier)

next := func(rw http.ResponseWriter, _ *http.Request) {
rw.WriteHeader(http.StatusOK)
}

tracer.ServeHTTP(negroni.NewResponseWriter(httptest.NewRecorder()), request, next)

spans := mockedTracer.FinishedSpans()
assert.Len(t, spans, 1)
span := spans[0]

assert.Equal(t, parentSpan.SpanContext.SpanID, span.ParentID)
}
Loading