-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 1ffee63
Showing
7 changed files
with
849 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# Go Infra Api | ||
|
||
Infrastructre methods and types for make api simple | ||
|
||
## Response | ||
|
||
- abstract type ApiResponse which can used as Tipically responses code as functions | ||
- Json Style for ApiError and method for simple constructing this model | ||
|
||
## Request | ||
|
||
- extract body to data class from all sources request with special tag | ||
|
||
## Metrics Prometheus | ||
|
||
- base golden metrics for api with high level methods for sending metrics to prometheus |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
module github.com/kubitre/go_api_infra | ||
|
||
go 1.17 | ||
|
||
require ( | ||
github.com/beorn7/perks v1.0.1 // indirect | ||
github.com/cespare/xxhash/v2 v2.1.2 // indirect | ||
github.com/golang/protobuf v1.5.2 // indirect | ||
github.com/gorilla/mux v1.8.0 | ||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect | ||
github.com/prometheus/client_golang v1.12.1 // indirect | ||
github.com/prometheus/client_model v0.2.0 // indirect | ||
github.com/prometheus/common v0.32.1 // indirect | ||
github.com/prometheus/procfs v0.7.3 // indirect | ||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect | ||
google.golang.org/protobuf v1.26.0 // indirect | ||
) |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
package metricsprometheus | ||
|
||
import ( | ||
"context" | ||
"time" | ||
|
||
"github.com/prometheus/client_golang/prometheus" | ||
) | ||
|
||
type MetricRecorder struct { | ||
httpRequestDurHistogram *prometheus.HistogramVec | ||
httpResponseSizeHistogram *prometheus.HistogramVec | ||
httpRequestsInflight *prometheus.GaugeVec | ||
} | ||
|
||
type MetricConfig struct { | ||
Prefix string | ||
DurationBuckets []float64 | ||
SizeBuckets []float64 | ||
Registry prometheus.Registerer | ||
HandlerIDLabel string | ||
StatusCodeLabel string | ||
MethodLabel string | ||
ServiceLabel string | ||
} | ||
|
||
type HTTPReqProperties struct { | ||
// Service is the service that has served the request. | ||
Service string | ||
// ID is the id of the request handler. | ||
ID string | ||
// Method is the method of the request. | ||
Method string | ||
// Code is the response of the request. | ||
Code string | ||
} | ||
|
||
func (c *MetricConfig) defaults() { | ||
if len(c.DurationBuckets) == 0 { | ||
c.DurationBuckets = prometheus.DefBuckets | ||
} | ||
|
||
if len(c.SizeBuckets) == 0 { | ||
c.SizeBuckets = prometheus.ExponentialBuckets(100, 10, 8) | ||
} | ||
|
||
if c.Registry == nil { | ||
c.Registry = prometheus.DefaultRegisterer | ||
} | ||
|
||
if c.HandlerIDLabel == "" { | ||
c.HandlerIDLabel = "handler" | ||
} | ||
|
||
if c.StatusCodeLabel == "" { | ||
c.StatusCodeLabel = "code" | ||
} | ||
|
||
if c.MethodLabel == "" { | ||
c.MethodLabel = "method" | ||
} | ||
|
||
if c.ServiceLabel == "" { | ||
c.ServiceLabel = "service" | ||
} | ||
} | ||
|
||
func NewRecorder(cfg MetricConfig) *MetricRecorder { | ||
cfg.defaults() | ||
|
||
r := &MetricRecorder{ | ||
httpRequestDurHistogram: prometheus.NewHistogramVec(prometheus.HistogramOpts{ | ||
Namespace: cfg.Prefix, | ||
Subsystem: "http", | ||
Name: "request_duration_seconds", | ||
Help: "The latency of the HTTP requests.", | ||
Buckets: cfg.DurationBuckets, | ||
}, []string{cfg.ServiceLabel, cfg.HandlerIDLabel, cfg.MethodLabel, cfg.StatusCodeLabel}), | ||
|
||
httpResponseSizeHistogram: prometheus.NewHistogramVec(prometheus.HistogramOpts{ | ||
Namespace: cfg.Prefix, | ||
Subsystem: "http", | ||
Name: "response_size_bytes", | ||
Help: "The size of the HTTP responses.", | ||
Buckets: cfg.SizeBuckets, | ||
}, []string{cfg.ServiceLabel, cfg.HandlerIDLabel, cfg.MethodLabel, cfg.StatusCodeLabel}), | ||
|
||
httpRequestsInflight: prometheus.NewGaugeVec(prometheus.GaugeOpts{ | ||
Namespace: cfg.Prefix, | ||
Subsystem: "http", | ||
Name: "requests_inflight", | ||
Help: "The number of inflight requests being handled at the same time.", | ||
}, []string{cfg.ServiceLabel, cfg.HandlerIDLabel}), | ||
} | ||
|
||
cfg.Registry.MustRegister( | ||
r.httpRequestDurHistogram, | ||
r.httpResponseSizeHistogram, | ||
r.httpRequestsInflight, | ||
) | ||
|
||
return r | ||
} | ||
|
||
func (recorder MetricRecorder) ObserveHTTPRequestDuration(_ context.Context, p HTTPReqProperties, duration time.Duration) { | ||
recorder.httpRequestDurHistogram.WithLabelValues(p.Service, p.ID, p.Method, p.Code).Observe(duration.Seconds()) | ||
} | ||
|
||
func (recorder MetricRecorder) ObserveHTTPResponseSize(_ context.Context, p HTTPReqProperties, sizeBytes int64) { | ||
recorder.httpResponseSizeHistogram.WithLabelValues(p.Service, p.ID, p.Method, p.Code).Observe(float64(sizeBytes)) | ||
} | ||
|
||
func (recorder MetricRecorder) AddInflightRequests(_ context.Context, p HTTPReqProperties, quantity int) { | ||
recorder.httpRequestsInflight.WithLabelValues(p.Service, p.ID).Add(float64(quantity)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package request | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"net/http" | ||
"reflect" | ||
) | ||
|
||
const ( | ||
tagParsing = "query" | ||
) | ||
|
||
// TODO: make abstract type interface{} | ||
type extractor func(string, *http.Request) string | ||
|
||
func RequestToType(request *http.Request, data interface{}, parseQuery, parseParams bool) (interface{}, error) { | ||
if err := json.NewDecoder(request.Body).Decode(&data); err != nil { | ||
return nil, errors.New("can not unmarshalled request") | ||
} | ||
|
||
var extractorFunc extractor | ||
|
||
if parseQuery { | ||
extractorFunc = extractFromQuery | ||
} | ||
|
||
if parseParams { | ||
extractorFunc = extractFromPathVariables | ||
} | ||
|
||
value := reflect.ValueOf(data) | ||
|
||
if err := prepareInlineStructFields(request, value, extractorFunc); err != nil { | ||
return nil, err | ||
} | ||
|
||
return data, nil | ||
} | ||
|
||
func extractFromQuery(paramName string, request *http.Request) string { | ||
return request.URL.Query().Get(paramName) | ||
} | ||
|
||
func extractFromPathVariables(paramName string, request *http.Request) string { | ||
vars := request.Context().Value(0).(map[string]interface{}) | ||
return vars[paramName].(string) | ||
} | ||
|
||
func prepareInlineStructFields(request *http.Request, value reflect.Value, preparator extractor) error { | ||
for i := 0; i < value.NumField(); i++ { | ||
val := value.Field(i).Addr() | ||
if val.Kind() == reflect.Struct { | ||
prepareInlineStructFields(request, val, preparator) | ||
} else { | ||
parsedTag := val.Type().Field(i).Tag.Get(tagParsing) | ||
if parsedTag != "" { | ||
dataQuery := request.URL.Query().Get(parsedTag) | ||
if dataQuery != "" { | ||
value.SetString(dataQuery) | ||
} else { | ||
return errors.New("in request does not exist query param with name: " + parsedTag) | ||
} | ||
} | ||
} | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package response | ||
|
||
type ApiError struct { | ||
Code string `json:"code"` | ||
Message string `json:"message,omitempty"` | ||
Target string `json:"target,omitempty"` | ||
Context interface{} `json:"context,omitempty"` | ||
TraceId string `json:"traceId,omitempty"` | ||
Errors []ApiError `json:"errors,omitempty"` | ||
} | ||
|
||
func MakeApiError(code string, message string, target string) ApiError { | ||
return ApiError{ | ||
Code: code, | ||
Message: message, | ||
Target: target, | ||
} | ||
} | ||
|
||
func MakeSimpleApiError(code string, message string) ApiError { | ||
return ApiError{ | ||
Code: code, | ||
Message: message, | ||
} | ||
} | ||
|
||
func (err *ApiError) SetTarget(target string) { | ||
if target != "" { | ||
err.Target = target | ||
} | ||
} | ||
|
||
func (err *ApiError) SetCode(code string) { | ||
if code != "" { | ||
err.Code = code | ||
} | ||
} | ||
|
||
func (err *ApiError) SetTraceID(traceId string) { | ||
if traceId != "" { | ||
err.TraceId = traceId | ||
} | ||
} | ||
|
||
func (err *ApiError) AddContextError(newSubErr ApiError) { | ||
err.Errors = append(err.Errors, newSubErr) | ||
} | ||
|
||
func (err *ApiError) SetContext(newContext string) { | ||
err.Context = newContext | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
package response | ||
|
||
import ( | ||
"encoding/json" | ||
"net/http" | ||
) | ||
|
||
type ApiResponse struct { | ||
ResponseWriter http.ResponseWriter | ||
Headers map[string]string | ||
Target string // service name | ||
} | ||
|
||
type EntityCreated struct { | ||
ID string `json:"id"` | ||
} | ||
|
||
func NewResponseJSON(w http.ResponseWriter, target string) *ApiResponse { | ||
return &ApiResponse{ | ||
ResponseWriter: w, | ||
Headers: map[string]string{ | ||
"Content-Type": "application/json", | ||
}, | ||
Target: target, | ||
} | ||
} | ||
|
||
func NewResponse(w http.ResponseWriter, target string, headers map[string]string) *ApiResponse { | ||
return &ApiResponse{ | ||
ResponseWriter: w, | ||
Headers: map[string]string{}, | ||
Target: target, | ||
} | ||
} | ||
|
||
func (responser *ApiResponse) AddHeader(key string, value string) *ApiResponse { | ||
responser.ResponseWriter.Header().Add(key, value) | ||
return responser | ||
} | ||
|
||
func (responser *ApiResponse) DeleteHeader(key string) *ApiResponse { | ||
responser.ResponseWriter.Header().Del(key) | ||
return responser | ||
} | ||
|
||
func (responser *ApiResponse) writeResponse(status int, body interface{}) { | ||
if body == nil && !isSuccessCode(status) { | ||
body = MakeApiError(getCodeText(status), "unknown error", responser.Target) | ||
} | ||
|
||
respBody, err := json.Marshal(body) | ||
if err != nil { | ||
responser.writeResponse(http.StatusInternalServerError, MakeApiError(getCodeText(status), "unknown error", responser.Target)) | ||
return | ||
} | ||
|
||
responser.ResponseWriter.WriteHeader(status) | ||
responser.ResponseWriter.Write(respBody) | ||
} | ||
|
||
func (responser *ApiResponse) Ok(entity interface{}) { | ||
responser.writeResponse(http.StatusOK, entity) | ||
} | ||
|
||
func (responser *ApiResponse) Created(entity interface{}) { | ||
responser.writeResponse(http.StatusCreated, entity) | ||
} | ||
|
||
func (responser *ApiResponse) Accepted(entity interface{}) { | ||
responser.writeResponse(http.StatusAccepted, entity) | ||
} | ||
|
||
func (responser *ApiResponse) NoContent() { | ||
responser.writeResponse(http.StatusNoContent, nil) | ||
} | ||
|
||
func (responser *ApiResponse) Unauthorized(entity interface{}) { | ||
responser.writeResponse(http.StatusUnauthorized, entity) | ||
} | ||
|
||
func (responser *ApiResponse) BadRequest(entity interface{}) { | ||
responser.writeResponse(http.StatusBadRequest, entity) | ||
} | ||
|
||
func (responser *ApiResponse) Forbidden(entity interface{}) { | ||
responser.writeResponse(http.StatusForbidden, entity) | ||
} | ||
|
||
func (responser *ApiResponse) NotFound(entity interface{}) { | ||
responser.writeResponse(http.StatusNotFound, entity) | ||
} | ||
|
||
func (responser *ApiResponse) MethodNotAllowed(entity interface{}) { | ||
responser.writeResponse(http.StatusMethodNotAllowed, entity) | ||
} | ||
|
||
func (responser *ApiResponse) Conflict(entity interface{}) { | ||
responser.writeResponse(http.StatusConflict, entity) | ||
} | ||
|
||
func (responser *ApiResponse) InternalServerError(entity interface{}) { | ||
responser.writeResponse(http.StatusInternalServerError, entity) | ||
} | ||
|
||
func (responser *ApiResponse) NotImplemented(entity interface{}) { | ||
responser.writeResponse(http.StatusNotImplemented, entity) | ||
} | ||
|
||
func (responser *ApiResponse) ServiceUnavailable(entity interface{}) { | ||
responser.writeResponse(http.StatusServiceUnavailable, entity) | ||
} | ||
|
||
func isSuccessCode(status int) bool { | ||
return status/100 == 2 | ||
} | ||
|
||
func getCodeText(status int) string { | ||
return http.StatusText(status) | ||
} |