mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
Default fn server keys should be minimal (empty) since not all stats have associated app name, fn id, etc. API tags for requests should not include "status" as this is part of responses.
290 lines
7.3 KiB
Go
290 lines
7.3 KiB
Go
// 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()
|
|
}
|