Skip to content

Commit

Permalink
initial tools
Browse files Browse the repository at this point in the history
  • Loading branch information
kubitre committed Feb 1, 2022
0 parents commit 1ffee63
Show file tree
Hide file tree
Showing 7 changed files with 849 additions and 0 deletions.
16 changes: 16 additions & 0 deletions Readme.md
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
17 changes: 17 additions & 0 deletions go.mod
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
)
463 changes: 463 additions & 0 deletions go.sum

Large diffs are not rendered by default.

115 changes: 115 additions & 0 deletions metrics_prometheus/golden_metrics.go
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))
}
68 changes: 68 additions & 0 deletions request/extract_request.go
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
}
51 changes: 51 additions & 0 deletions response/api_error.go
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
}
119 changes: 119 additions & 0 deletions response/response.go
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)
}

0 comments on commit 1ffee63

Please sign in to comment.