// This is middleware we're using for the entire server. package server import ( "context" "fmt" "strings" "strconv" "time" "github.com/fnproject/fn/api" "github.com/fnproject/fn/api/common" "github.com/fnproject/fn/api/models" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "go.opencensus.io/stats" "go.opencensus.io/stats/view" "go.opencensus.io/tag" "go.opencensus.io/trace" ) var ( pathKey = common.MakeKey("path") methodKey = common.MakeKey("method") statusKey = common.MakeKey("status") apiRequestCountMeasure = common.MakeMeasure("api/request_count", "Count of API requests started", stats.UnitDimensionless) apiResponseCountMeasure = common.MakeMeasure("api/response_count", "API response count", stats.UnitDimensionless) apiLatencyMeasure = common.MakeMeasure("api/latency", "Latency distribution of API requests", stats.UnitMilliseconds) APIViewsGetPath = DefaultAPIViewsGetPath ) func optionalCorsWrap(r *gin.Engine) { // By default no CORS are allowed unless one // or more Origins are defined by the API_CORS // environment variable. corsStr := getEnv(EnvAPICORSOrigins, "") if len(corsStr) > 0 { origins := strings.Split(strings.Replace(corsStr, " ", "", -1), ",") corsConfig := cors.DefaultConfig() if origins[0] == "*" { corsConfig.AllowAllOrigins = true } else { corsConfig.AllowOrigins = origins } corsHeaders := getEnv(EnvAPICORSHeaders, "") if len(corsHeaders) > 0 { headers := strings.Split(strings.Replace(corsHeaders, " ", "", -1), ",") corsConfig.AllowHeaders = headers } corsConfig.AllowMethods = []string{"GET", "POST", "PUT", "HEAD", "DELETE"} logrus.Infof("CORS enabled for domains: %s", origins) r.Use(cors.New(corsConfig)) } } // we should use http grr func traceWrap(c *gin.Context) { appKey, err := tag.NewKey("fn_appname") if err != nil { logrus.Fatal(err) } appIDKey, err := tag.NewKey("fn_app_id") if err != nil { logrus.Fatal(err) } fnKey, err := tag.NewKey("fn_fn_id") if err != nil { logrus.Fatal(err) } ctx, err := tag.New(c.Request.Context(), tag.Insert(appKey, c.Param(api.ParamAppName)), tag.Insert(appIDKey, c.Param(api.ParamAppID)), tag.Insert(fnKey, c.Param(api.ParamFnID)), ) if err != nil { logrus.Fatal(err) } // TODO inspect opencensus more and see if we need to define a header ourselves // to trigger per-request spans (we will want this), we can set sampler here per request. ctx, serverSpan := trace.StartSpan(ctx, "serve_http") defer serverSpan.End() c.Request = c.Request.WithContext(ctx) c.Next() } func RegisterAPIViews(tagKeys []string, dist []float64) { // default tags for request and response reqTags := []tag.Key{pathKey, methodKey} respTags := []tag.Key{pathKey, methodKey, statusKey} // add extra tags if not already in default tags for req/resp for _, key := range tagKeys { if key != "path" && key != "method" && key != "status" { respTags = append(respTags, common.MakeKey(key)) } if key != "path" && key != "method" { reqTags = append(reqTags, common.MakeKey(key)) } } err := view.Register( common.CreateViewWithTags(apiRequestCountMeasure, view.Count(), reqTags), common.CreateViewWithTags(apiResponseCountMeasure, view.Count(), respTags), common.CreateViewWithTags(apiLatencyMeasure, view.Distribution(dist...), respTags), ) if err != nil { logrus.WithError(err).Fatal("cannot register view") } } func DefaultAPIViewsGetPath(routes gin.RoutesInfo, c *gin.Context) string { // get the handler url, example: /v1/apps/:app url := "invalid" for _, r := range routes { if r.Handler == c.HandlerName() { url = r.Path break } } return url } func apiMetricsWrap(s *Server) { measure := func(engine *gin.Engine) func(*gin.Context) { var routes gin.RoutesInfo return func(c *gin.Context) { if routes == nil { routes = engine.Routes() } start := time.Now() ctx, err := tag.New(c.Request.Context(), tag.Upsert(pathKey, APIViewsGetPath(routes, c)), tag.Upsert(methodKey, c.Request.Method), ) if err != nil { logrus.Fatal(err) } stats.Record(ctx, apiRequestCountMeasure.M(0)) c.Next() status := strconv.Itoa(c.Writer.Status()) ctx, err = tag.New(ctx, tag.Upsert(statusKey, status), ) if err != nil { logrus.Fatal(err) } stats.Record(ctx, apiResponseCountMeasure.M(0)) stats.Record(ctx, apiLatencyMeasure.M(int64(time.Since(start)/time.Millisecond))) } } r := s.Router r.Use(measure(r)) if s.svcConfigs[WebServer].Addr != s.svcConfigs[AdminServer].Addr { a := s.AdminRouter a.Use(measure(a)) } } func panicWrap(c *gin.Context) { defer func(c *gin.Context) { if rec := recover(); rec != nil { err, ok := rec.(error) if !ok { err = fmt.Errorf("fn: %v", rec) } handleErrorResponse(c, err) c.Abort() } }(c) c.Next() } func loggerWrap(c *gin.Context) { ctx, _ := common.LoggerWithFields(c.Request.Context(), extractFields(c)) if appName := c.Param(api.ParamAppName); appName != "" { c.Set(api.AppName, appName) ctx = ContextWithApp(ctx, appName) } if appID := c.Param(api.ParamAppID); appID != "" { c.Set(api.ParamAppID, appID) ctx = ContextWithAppID(ctx, appID) } if fnID := c.Param(api.ParamFnID); fnID != "" { c.Set(api.ParamFnID, fnID) ctx = ContextWithFnID(ctx, fnID) } c.Request = c.Request.WithContext(ctx) c.Next() } type ctxFnIDKey string func ContextWithFnID(ctx context.Context, fnID string) context.Context { return context.WithValue(ctx, ctxFnIDKey(api.ParamFnID), fnID) } // FnIDFromContext returns the app from a context, if set. func FnIDFromContext(ctx context.Context) string { r, _ := ctx.Value(ctxFnIDKey(api.ParamFnID)).(string) return r } type ctxAppIDKey string func ContextWithAppID(ctx context.Context, appID string) context.Context { return context.WithValue(ctx, ctxAppIDKey(api.ParamAppID), appID) } // AppIDFromContext returns the app from a context, if set. func AppIDFromContext(ctx context.Context) string { r, _ := ctx.Value(ctxAppIDKey(api.ParamAppID)).(string) return r } type ctxAppKey string // ContextWithApp sets the app name value on a context, it may be retrieved // using AppFromContext. // TODO this is also used as a gin.Key -- stop one of these two things. func ContextWithApp(ctx context.Context, app string) context.Context { return context.WithValue(ctx, ctxAppKey(api.AppName), app) } // AppFromContext returns the app from a context, if set. func AppFromContext(ctx context.Context) string { r, _ := ctx.Value(ctxAppKey(api.AppName)).(string) return r } func (s *Server) checkAppPresenceByName() gin.HandlerFunc { return func(c *gin.Context) { ctx, _ := common.LoggerWithFields(c.Request.Context(), extractFields(c)) appName := c.MustGet(api.AppName).(string) if appName != "" { appID, err := s.datastore.GetAppID(ctx, appName) if err != nil { handleErrorResponse(c, err) c.Abort() return } c.Set(api.AppID, appID) } c.Request = c.Request.WithContext(ctx) c.Next() } } func setAppIDInCtx(c *gin.Context) { // add appName to context appID := c.Param(api.ParamAppID) if appID != "" { c.Set(api.AppID, appID) c.Request = c.Request.WithContext(c) } c.Next() } func appIDCheck(c *gin.Context) { appID := c.GetString(api.ParamAppID) if appID == "" { handleErrorResponse(c, models.ErrAppsMissingID) c.Abort() return } c.Next() }