Compare commits

...

14 Commits

Author Message Date
bryanqiu
98eac47127 add AlreadyHaveResult function 2024-02-04 15:34:07 +08:00
bryanqiu
69b0fb05aa add AlreadyHaveResult function 2024-02-04 15:29:11 +08:00
bryanqiu
ba9357e166 add GET 2023-11-24 11:41:57 +08:00
bryanqiu
9df516247c export metricRegistry 2023-05-30 10:46:56 +08:00
bryanqiu
93bdc425e5 add grouping 2023-03-06 11:55:47 +08:00
bryanqiu
426a77f4cf options method buf fixed(record metrics update) 2022-12-22 09:53:14 +08:00
bryanqiu
a098d141c2 add pre middleware 2022-12-21 19:14:14 +08:00
bryanqiu
1d8453af9d surport outside instance 2022-12-21 16:46:56 +08:00
bryanqiu
b70e9ff90d add instancee 2022-12-21 16:01:53 +08:00
bryanqiu
8335b17e89 add prometheus record 2022-12-21 00:31:56 +08:00
bryanqiu
900d2e9789 add prometheus 2022-12-19 21:07:54 +08:00
bryanqiu
92b821a12d add ApiHandler 2022-12-09 14:51:14 +08:00
bryanqiu
136178a1c1 errCodeParameterError = -20000 2022-12-07 15:12:56 +08:00
bryanqiu
f978fa3f87 add bindoutput & rewrite shapeoutput 2022-12-06 19:15:35 +08:00
6 changed files with 429 additions and 39 deletions

129
api.go
View File

@ -1,6 +1,9 @@
package api
import (
"fmt"
"reflect"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"qoobing.com/gomod/log"
@ -10,7 +13,9 @@ import (
var (
validate = validator.New()
requestLogidGetter = defaultLogidGetter
requestHandlerMapper = map[string]gin.HandlerFunc{}
errCodeUnknownError = -10000
errCodeParameterError = -20000
errCodeUnimplementApi = -20401
)
@ -33,15 +38,74 @@ type BaseWithErrCodeOutput struct {
ErrMsg string `json:"errMsg"`
}
func NewEngine() *Engine {
func NewEngine(pre ...gin.HandlerFunc) *Engine {
gin.SetMode(gin.ReleaseMode)
e := gin.New()
for _, m := range pre {
e.Use(m)
}
e.Use(middlewareMyApiEngine())
return &Engine{e}
}
func (e *Engine) POST(uri string, handler HandlerFunc) {
e.Engine.POST(uri, handlerWrapper(handler))
func (e *Engine) POST(uri string, handler Handler) {
h := handlerWrapper(handler)
log.Infof("api handler [%v] registor success", handler)
e.Engine.POST(uri, h)
}
func (e *Engine) GET(uri string, handler Handler) {
h := handlerWrapper(handler)
log.Infof("api handler [%v] registor success", handler)
e.Engine.GET(uri, h)
}
// handlerWrapper
func handlerWrapper(handler Handler) gin.HandlerFunc {
handlerFunc := handler.HandlerFunc()
wrappedHandlerFunc := func(c *gin.Context) {
// Case 1. get request handler
if c.Request == nil && c.Keys != nil {
c.Keys["handler"] = handler
return
}
// Case 2. normal case, user request will go this way
if icc, ok := c.Get("cc"); !ok {
log.Errorf("Unreachable Code: can not get cc(*api.Context) from *gin.Context")
} else if cc, ok := icc.(*Context); !ok {
log.Debugf("Unreachable Code: cc from *gin.Context is type of:[%s]", reflect.TypeOf(icc).String())
log.Errorf("Unreachable Code: cc from *gin.Context is not type of *api.Context")
} else {
// user request should go this way
defer cc.TryRecover()
cc.AppId = c.GetString("appid")
cc.UserId = c.GetUint64("userid")
handlerFunc(cc)
return
}
}
hkey := fmt.Sprintf("%v", wrappedHandlerFunc)
requestHandlerMapper[hkey] = wrappedHandlerFunc
return wrappedHandlerFunc
}
// handlerWrapper
func handlerFuncWrapper(handler HandlerFunc) gin.HandlerFunc {
return func(c *gin.Context) {
if icc, ok := c.Get("cc"); !ok {
log.Errorf("Unreachable Code: can not get cc(*api.Context) from *gin.Context")
} else if cc, ok := icc.(*Context); !ok {
log.Debugf("Unreachable Code: cc from *gin.Context is type of:[%s]", reflect.TypeOf(icc).String())
log.Errorf("Unreachable Code: cc from *gin.Context is not type of *api.Context")
} else {
defer cc.TryRecover()
cc.AppId = c.GetString("appid")
cc.UserId = c.GetUint64("userid")
handler(cc)
return
}
}
}
// middlewareMyApiEngine myapi engine middleware
@ -53,11 +117,16 @@ func middlewareMyApiEngine() gin.HandlerFunc {
defer log.Cleanup()
// Step 2. init *api.Context
req := c.Request
log.DebugfWithDepth(1, "[%s|%s|%s]", req.Method, req.Host, req.RequestURI)
cc := New(c)
c.Set("cc", cc)
// Step 3. do request
c.Next()
// Step 4. recorde metrics
cc.recordMetrics()
}
}
@ -72,6 +141,57 @@ func defaultLogidGetter(c *gin.Context) (logid string) {
panic("unreachable code")
}
func CC(c *gin.Context) *Context {
if icc, ok := c.Get("cc"); !ok {
log.Errorf("Unreachable Code: can not get cc(*api.Context) from *gin.Context")
} else if cc, ok := icc.(*Context); !ok {
log.Debugf("Unreachable Code: cc from *gin.Context is type of:[%s]", reflect.TypeOf(icc).String())
log.Errorf("Unreachable Code: cc from *gin.Context is not type of *api.Context")
} else {
return cc
}
return nil
}
func ApiHandler(c *gin.Context) Handler {
h := c.Handler()
hkey := fmt.Sprintf("%v", h)
if _, ok := requestHandlerMapper[hkey]; !ok {
return nil
}
newc := &gin.Context{
Request: nil,
Keys: map[string]any{},
}
h(newc)
return newc.Keys["handler"].(Handler)
}
func RecordMetrics(api, errcode, appid string, costms float64) {
var (
counter = metricApiCounter.WithLabelValues(api, errcode, appid)
summary = metricApiSummary.WithLabelValues(api, errcode, appid)
)
// Metric 1. api request counter
counter.Inc()
// Metric 2. api request cost/latency
summary.Observe(costms)
}
func DumpHandlerFunc(c *Context) error {
errmsg := "api not implemented"
errcode := errCodeUnimplementApi
return c.RESULT_ERROR(errcode, errmsg)
}
func ErrorHandler(c *gin.Context, errcode int, errmsg string) {
handler := handlerFuncWrapper(func(c *Context) error {
return c.RESULT_ERROR(errcode, errmsg)
})
handler(c)
}
func InitErrCodes(errcodes map[string]int) {
if code, ok := errcodes["ErrCodeUnknownError"]; ok {
errCodeUnknownError = code
@ -79,4 +199,7 @@ func InitErrCodes(errcodes map[string]int) {
if code, ok := errcodes["ErrCodeUnimplementApi"]; ok {
errCodeUnknownError = code
}
if code, ok := errcodes["errCodeParameterError"]; ok {
errCodeParameterError = code
}
}

View File

@ -7,6 +7,7 @@ import (
"net/http"
"reflect"
"runtime/debug"
"strconv"
"strings"
"time"
@ -18,6 +19,7 @@ import (
type Context struct {
*gin.Context
e *gin.Engine
AppId string //current request appid, must not be empty
UserId uint64 //current request userid, maybe 0 if user unknown
@ -25,6 +27,8 @@ type Context struct {
resultentry string
httpstatus int
starttime time.Time
output interface{}
errcode int
}
func New(c *gin.Context) *Context {
@ -56,9 +60,6 @@ func (c *Context) TryRecover() {
}
func (c *Context) BindInput(i interface{}) (err error) {
req := c.Request
log.DebugfWithDepth(1, "[%s|%s|%s]", req.Method, req.Host, req.RequestURI)
logstr := fmt.Sprintf("input:[have not initialized]")
defer func(plogstr *string) {
log.DebugfWithDepth(2, "%s", *plogstr)
@ -89,6 +90,29 @@ func (c *Context) BindInput(i interface{}) (err error) {
return nil
}
func (c *Context) BindOutput(o interface{}) (err error) {
var v = reflect.ValueOf(o)
if v.Kind() != reflect.Pointer {
return fmt.Errorf("output is NOT a pointer")
}
var e = v.Elem()
var t = e.Type()
if _, ok := t.FieldByName("ErrCode"); !ok {
return fmt.Errorf("out have no field ErrCode")
}
if _, ok := t.FieldByName("ErrMsg"); !ok {
return fmt.Errorf("out have no field ErrMsg")
}
if code := e.FieldByName("ErrCode"); !code.CanSet() {
return fmt.Errorf("output.ErrCode can not be set, " +
"output is NOT a pointer or output have no field 'ErrCode'")
}
c.output = o
return nil
}
func (c *Context) SetCookie(cookie *http.Cookie) {
if cookie.Path == "" {
cookie.Path = "/"
@ -96,7 +120,14 @@ func (c *Context) SetCookie(cookie *http.Cookie) {
http.SetCookie(c.Writer, cookie)
}
func (c *Context) AlreadyHaveResult() (done bool, entry string) {
return c.resultentry != "", c.resultentry
}
func (c *Context) RESULT(output interface{}) error {
if c.resultentry == "" {
c.resultentry = "RESULT"
}
var t = reflect.TypeOf(output)
defer func(o *interface{}) {
b, err := json.Marshal(o)
@ -130,15 +161,23 @@ func (c *Context) RESULT(output interface{}) error {
if _, ok := t.FieldByName("ErrCode"); !ok {
errstr := "Result MUST have 'ErrCode' field"
output = BaseWithErrCodeOutput{errCodeUnknownError, errstr}
c.shapeOutput(output)
c.errcode = int(reflect.ValueOf(output).FieldByName("ErrCode").Int())
c.IndentedJSON(c.httpstatus, output)
return errors.New(errstr)
}
if _, ok := t.FieldByName("ErrMsg"); !ok {
errstr := "Result MUST have 'ErrMsg' field"
output = BaseWithErrCodeOutput{errCodeUnknownError, errstr}
c.shapeOutput(output)
c.errcode = int(reflect.ValueOf(output).FieldByName("ErrCode").Int())
c.IndentedJSON(c.httpstatus, output)
return errors.New(errstr)
}
c.shapeOutput(output)
c.errcode = int(reflect.ValueOf(output).FieldByName("ErrCode").Int())
c.IndentedJSON(c.httpstatus, output)
return nil
}
@ -149,6 +188,7 @@ func (c *Context) RESULT_ERROR(eno int, err string) error {
}
result := BaseWithErrCodeOutput{eno, err}
c.errcode = eno
return c.RESULT(result)
}
@ -156,5 +196,44 @@ func (c *Context) RESULT_PARAMETER_ERROR(err string) error {
if c.resultentry == "" {
c.resultentry = "RESULT_PARAMETER_ERROR"
}
return c.RESULT_ERROR(errCodeUnknownError, err)
return c.RESULT_ERROR(errCodeParameterError, err)
}
func (c *Context) ApiHandler() Handler {
return ApiHandler(c.Context)
}
func (c *Context) shapeOutput(o interface{}) {
if c.output == nil {
return
}
var out, ok = o.(BaseWithErrCodeOutput)
if !ok {
return
}
var e = reflect.ValueOf(c.output).Elem()
e.FieldByName("ErrMsg").SetString(out.ErrMsg)
e.FieldByName("ErrCode").SetInt(int64(out.ErrCode))
return
}
func (c *Context) recordMetrics() {
if metricApiCounter == nil ||
metricApiSummary == nil {
return
}
var h = c.ApiHandler()
if h == nil {
h = unimplementHandler{c.Context}
}
var (
api = h.ApiName()
appid = c.AppId
costus = time.Now().Sub(c.starttime).Microseconds()
costms = float64(costus) / 1000.0
errcode = strconv.Itoa(c.errcode)
)
RecordMetrics(api, errcode, appid, costms)
}

2
go.mod
View File

@ -1,6 +1,6 @@
module qoobing.com/gomod/api
go 1.16
go 1.19
require (
github.com/gin-gonic/gin v1.8.1

102
prometheus.go Normal file
View File

@ -0,0 +1,102 @@
package api
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/client_golang/prometheus/push"
"qoobing.com/gomod/log"
)
var (
MetricExporterPusher = func() error { return nil }
MetricExporterHandler Handler = nil
MetricRegistry *prometheus.Registry = nil
metricApiCounter *prometheus.CounterVec = nil
metricApiSummary *prometheus.SummaryVec = nil
instance string = "--unknown--"
)
type metricExporterHandler struct {
name string
promhttpHandler http.Handler
}
func (m *metricExporterHandler) ApiName() string {
return m.name
}
func (m *metricExporterHandler) HandlerFunc() HandlerFunc {
return func(c *Context) (err error) {
m.promhttpHandler.ServeHTTP(c.Writer, c.Request)
return nil
}
}
func dumpMetricExporterPusher() error {
return errors.New("not initilized, forgot call 'SetupMetricsExporterPusher'?")
}
func InitMetrics(instanceValue string) {
if instanceValue == "" {
instance = getInstanceIpAddress()
} else if ap := strings.Split(instanceValue, ":"); len(ap) == 2 &&
ap[0] == "0.0.0.0" || ap[0] == "" {
instance = getInstanceIpAddress() + ":" + ap[1]
}
log.Infof("self instance: [%s]", instance)
metricApiCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "api_requests_total",
Help: "How many HTTP requests processed",
//ConstLabels: prometheus.Labels{"instance": instance},
},
[]string{"api", "errcode", "appid"},
)
metricApiSummary = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Name: "api_requests_summary",
Help: "The api request summary of cost.",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
//ConstLabels: prometheus.Labels{"instance": instance},
},
[]string{"api", "errcode", "appid"},
)
MetricRegistry = prometheus.NewRegistry()
MetricRegistry.MustRegister(metricApiCounter)
MetricRegistry.MustRegister(metricApiSummary)
}
func SetupMetricsExporterHandler(apiname string) {
if apiname == "" {
apiname = "premetheus_metrics_exporter"
}
opt := promhttp.HandlerOpts{Registry: MetricRegistry}
MetricExporterHandler = &metricExporterHandler{
name: apiname,
promhttpHandler: promhttp.HandlerFor(MetricRegistry, opt),
}
}
func SetupMetricsExporterPusher(pushgateway string, jobname string) {
if jobname == "" {
jobname = fmt.Sprintf("api-metrics-job-%s", getExeFilename())
}
var pusher = push.
New(pushgateway, jobname).
Grouping("instance", instance).
Gatherer(MetricRegistry)
MetricExporterPusher = func() error {
RecordMetrics("push_metrics_to_prometheus", "0", "selfmonitoring", 1.0)
return pusher.Push()
}
}

View File

@ -1,49 +1,65 @@
package api
import (
"reflect"
"fmt"
"strings"
"github.com/gin-gonic/gin"
"qoobing.com/gomod/log"
)
type routes struct {
gin.IRoutes
}
// ///////////////////////////////////////////////////////////////////
// IRouter & IRoutes in gin, we redefine it simple
// ///////////////////////////////////////////////////////////////////
//
// // IRouter defines all router handle interface
// // includes single and group router.
// type IRouter interface {
// IRoutes
// Group(string, ...HandlerFunc) *RouterGroup
// }
//
// // IRoutes defines all router handle interface.
// type IRoutes interface {
// Use(...HandlerFunc) IRoutes
//
// Handle(string, string, ...HandlerFunc) IRoutes
// Any(string, ...HandlerFunc) IRoutes
// GET(string, ...HandlerFunc) IRoutes
// POST(string, ...HandlerFunc) IRoutes
// DELETE(string, ...HandlerFunc) IRoutes
// PATCH(string, ...HandlerFunc) IRoutes
// PUT(string, ...HandlerFunc) IRoutes
// OPTIONS(string, ...HandlerFunc) IRoutes
// HEAD(string, ...HandlerFunc) IRoutes
//
// StaticFile(string, string) IRoutes
// StaticFileFS(string, string, http.FileSystem) IRoutes
// Static(string, string) IRoutes
// StaticFS(string, http.FileSystem) IRoutes
// }
//
// ///////////////////////////////////////////////////////////////////
type Handler interface {
ApiName() string
HandlerFunc() HandlerFunc
}
type HandlerFunc func(*Context) error
type IRoutes interface {
POST(uri string, handler HandlerFunc)
GET(uri string, handler Handler)
POST(uri string, handler Handler)
}
func (r routes) POST(uri string, handler HandlerFunc) {
r.IRoutes.POST(uri, handlerWrapper(handler))
type unimplementHandler struct {
c *gin.Context
}
func DumpHandler(c *Context) error {
return c.RESULT_ERROR(errCodeUnimplementApi, "api not implemented")
func (u unimplementHandler) ApiName() string {
req := u.c.Request
return strings.ToLower(fmt.Sprintf("[%s]%s", req.Method, req.RequestURI))
}
func ErrorHandler(c *gin.Context, errcode int, errmsg string) {
handler := handlerWrapper(func(c *Context) error {
return c.RESULT_ERROR(errcode, errmsg)
})
handler(c)
}
func handlerWrapper(handler HandlerFunc) gin.HandlerFunc {
return func(c *gin.Context) {
if icc, ok := c.Get("cc"); !ok {
log.Errorf("Unreachable Code: can not get cc(*api.Context) from *gin.Context")
} else if cc, ok := icc.(*Context); !ok {
log.Debugf("Unreachable Code: cc from *gin.Context is type of:[%s]", reflect.TypeOf(icc).String())
log.Errorf("Unreachable Code: cc from *gin.Context is not type of *api.Context")
} else {
defer cc.TryRecover()
cc.AppId = c.GetString("appid")
cc.UserId = c.GetUint64("userid")
handler(cc)
}
}
func (u unimplementHandler) HandlerFunc() HandlerFunc {
return DumpHandlerFunc
}

70
util.go Normal file
View File

@ -0,0 +1,70 @@
package api
import (
"fmt"
"net"
"os"
"path/filepath"
"regexp"
)
// getExeFilename
func getExeFilename() string {
_, logfilename := filepath.Split(os.Args[0])
if logfilename == "" {
panic("get exe filename failed")
}
return logfilename
}
// preferencesMatch
func preferencesMatch(s string, preferences ...string) bool {
for _, pstr := range preferences {
reg := regexp.MustCompile(pstr)
if reg.Match([]byte(s)) {
return true
}
}
return false
}
// getInstanceIpAddress
func getInstanceIpAddress(preferences ...string) string {
addrs, err := net.InterfaceAddrs()
if err != nil {
panic(fmt.Sprintf("getInstanceIpAddress failed: [%s]", err))
}
prefer := func() func(string) bool {
if len(preferences) <= 0 {
return nil
}
return func(ip string) bool {
return preferencesMatch(ip, preferences...)
}
}()
var ipnets = []*net.IPNet{}
for _, address := range addrs {
if ipnet, ok := address.(*net.IPNet); !ok {
continue
} else if ipnet.IP.IsLoopback() {
continue
} else if ipv4 := ipnet.IP.To4(); ipv4 == nil {
continue
} else if prefer == nil {
var n = *ipnet
ipnets = append(ipnets, &n)
continue
} else if prefer != nil && prefer(ipv4.String()) {
return ipv4.String()
}
}
// TODO: sort & return first
if len(ipnets) > 0 {
return ipnets[0].IP.To4().String()
}
panic(fmt.Sprintf("getInstanceIpAddreass failed,"+
"preferences=[%v], ipnets=[%v]", preferences, ipnets))
}