Commit 68d67e83 authored by Faizal Aziz's avatar Faizal Aziz

init ulfssar

parent a76afc5e
Pipeline #424 failed with stages
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
ulfsaar
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ulfsaar.iml" filepath="$PROJECT_DIR$/.idea/ulfsaar.iml" />
</modules>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
\ No newline at end of file
package config
import (
"os"
"github.com/joho/godotenv"
"github.com/kelseyhightower/envconfig"
)
func New(target interface{}) error {
var (
filename = os.Getenv("CONFIG_FILE")
)
if filename == "" {
filename = ".env"
}
if _, err := os.Stat(filename); os.IsNotExist(err) {
if err := envconfig.Process("", target); err != nil {
return err
}
return nil
}
if err := godotenv.Load(filename); err != nil {
return err
}
if err := envconfig.Process("", target); err != nil {
return err
}
return nil
}
package context
import (
"github.com/labstack/echo/v4"
"ulfsaar/errors"
"ulfsaar/session"
)
type (
UlfsaarContext struct {
echo.Context
Session *session.Session
}
Success struct {
Code string `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
Failed struct {
Code string `json:"code"`
Message string `json:"message"`
Error string `json:"error"`
}
FailedWithData struct {
Code string `json:"code"`
Message string `json:"message"`
Error string `json:"error"`
ErrorData interface{} `json:"error_data"`
}
)
func (sc *UlfsaarContext) Success(data interface{}) error {
return sc.JSON(200, Success{
Code: "200",
Message: "success",
Data: data,
})
}
func (sc *UlfsaarContext) SuccessWithMeta(data, meta interface{}) error {
return sc.JSON(200, Success{
Code: "200",
Message: "success",
Data: data,
})
}
func (sc *UlfsaarContext) Fail(err error) error {
var (
ed = errors.ExtractError(err)
)
return sc.JSON(ed.HttpCode, Failed{
Code: ed.Code,
Message: "failed",
Error: ed.Message,
})
}
func (sc *UlfsaarContext) FailWithData(err error, data interface{}) error {
var (
ed = errors.ExtractError(err)
)
return sc.JSON(ed.HttpCode, FailedWithData{
Code: ed.Code,
Message: "failed",
Error: ed.Message,
ErrorData: data,
})
}
func NewEmptyUlfsaarContext(parent echo.Context) *UlfsaarContext {
return &UlfsaarContext{parent, nil}
}
func NewUlfsaarContext(parent echo.Context) (*UlfsaarContext, error) {
pctx, ok := parent.(*UlfsaarContext)
if !ok {
return nil, errors.ErrSession
}
if pctx.Session == nil {
return nil, errors.ErrSession
}
return pctx, nil
}
package crypto
import "github.com/golang-jwt/jwt"
type (
Crypto interface {
Encrypt(claims jwt.Claims) ([]byte, error)
Decrypt(claims jwt.Claims, text string) (jwt.Claims, error)
}
impl struct {
secret []byte
}
)
func (i *impl) Encrypt(claims jwt.Claims) ([]byte, error) {
token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), claims)
ciphertext, err := token.SignedString(i.secret)
if err != nil {
return nil, err
}
return []byte(ciphertext), nil
}
func (i *impl) Decrypt(claims jwt.Claims, tokenString string) (jwt.Claims, error) {
keyFunc := func(token *jwt.Token) (interface{}, error) {
return i.secret, nil
}
token, err := jwt.ParseWithClaims(tokenString, claims, keyFunc)
if err != nil {
return nil, err
}
return token.Claims, nil
}
func New(secret string) (Crypto, error) {
return &impl{[]byte(secret)}, nil
}
package dbcon
import (
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
const (
MySQL string = "mysql"
SQLite string = "sqlite"
PostgreSQL string = "postgresql"
)
type DatabaseJSONType struct {
}
func (t DatabaseJSONType) GormDBDataType(db *gorm.DB, field *schema.Field) string {
switch db.Dialector.Name() {
case MySQL, SQLite:
return "JSON"
case PostgreSQL:
return "JSONB"
}
return ""
}
package dbcon
import (
"context"
"gorm.io/gorm"
)
type ORM interface {
Error() error
Close() error
Begin() ORM
Commit() error
Rollback() error
Offset(offset int64) ORM
Limit(limit int64) ORM
First(object interface{}) error
Last(object interface{}) error
Find(object interface{}) error
Model(value interface{}) ORM
Select(query interface{}, args ...interface{}) ORM
OmitAssoc() ORM
Table(name string, args ...interface{}) ORM
Where(query interface{}, args ...interface{}) ORM
Order(value interface{}) ORM
Create(args interface{}) error
Update(args interface{}) error
UpdateColumns(args interface{}) error
Delete(model interface{}, args ...interface{}) error
WithContext(ctx context.Context) ORM
Raw(query string, args ...interface{}) ORM
Exec(query string, args ...interface{}) ORM
Scan(object interface{}) error
Preload(assoc string, args ...interface{}) ORM
Joins(assoc string) ORM
GetGormInstance() *gorm.DB
Count(count *int64) error
Association(column string) ORMAssociation
Or(query interface{}, args ...interface{}) ORM
Save(data interface{}) error
}
type ORMAssociation interface {
Replace(values ...interface{}) error
Find(out interface{}, conds ...interface{}) error
Clear() error
}
package dbcon
import (
"context"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/logger"
log "ulfsaar/logger"
)
type (
postgresqldb struct {
db *gorm.DB
err error
}
PostgreSqlOption struct {
ConnectionString string
MaxLifeTimeConnection time.Duration
MaxIdleConnection, MaxOpenConnection int
Logger log.Logger
}
)
func (d *postgresqldb) Error() error {
return d.err
}
func (d *postgresqldb) Close() error {
sql, err := d.db.DB()
if err != nil {
return err
}
if err := sql.Close(); err != nil {
return err
}
return nil
}
func (d *postgresqldb) Begin() ORM {
var (
db = d.db.Begin()
err = db.Error
)
return &postgresqldb{db, err}
}
func (d *postgresqldb) Commit() error {
return d.db.Commit().Error
}
func (d *postgresqldb) Rollback() error {
return d.db.Rollback().Error
}
func (d *postgresqldb) Offset(offset int64) ORM {
var (
db = d.db.Offset(int(offset))
err = d.db.Error
)
return &postgresqldb{db, err}
}
func (d *postgresqldb) Limit(limit int64) ORM {
var (
db = d.db.Limit(int(limit))
err = d.db.Error
)
return &postgresqldb{db, err}
}
func (d *postgresqldb) First(object interface{}) error {
var (
res = d.db.First(object)
)
if res.Error != nil {
return res.Error
}
return nil
}
func (d *postgresqldb) Last(object interface{}) error {
var (
res = d.db.Last(object)
)
if res.Error != nil {
return res.Error
}
return nil
}
func (d *postgresqldb) Find(object interface{}) error {
var (
res = d.db.Find(object)
)
if res.Error != nil {
return res.Error
}
return nil
}
func (d *postgresqldb) Model(value interface{}) ORM {
var (
db = d.db.Model(value)
err = db.Error
)
return &postgresqldb{db, err}
}
func (d *postgresqldb) OmitAssoc() ORM {
var (
db = d.db.Omit(clause.Associations)
err = db.Error
)
return &postgresqldb{db, err}
}
func (d *postgresqldb) Select(query interface{}, args ...interface{}) ORM {
var (
db = d.db.Select(query, args...)
err = db.Error
)
return &postgresqldb{db, err}
}
func (d *postgresqldb) Table(name string, args ...interface{}) ORM {
var (
db = d.db.Table(name, args...)
err = db.Error
)
return &postgresqldb{db, err}
}
func (d *postgresqldb) Where(query interface{}, args ...interface{}) ORM {
var (
db = d.db.Where(query, args...)
err = db.Error
)
return &postgresqldb{db, err}
}
func (d *postgresqldb) Order(value interface{}) ORM {
var (
db = d.db.Order(value)
err = d.db.Error
)
return &postgresqldb{db, err}
}
func (d *postgresqldb) Create(args interface{}) error {
return d.db.Create(args).Error
}
func (d *postgresqldb) Update(args interface{}) error {
return d.db.Updates(args).Error
}
func (d *postgresqldb) UpdateColumns(args interface{}) error {
return d.db.UpdateColumns(args).Error
}
func (d *postgresqldb) Delete(model interface{}, args ...interface{}) error {
return d.db.Delete(model, args...).Error
}
func (d *postgresqldb) WithContext(ctx context.Context) ORM {
var (
db = d.db.WithContext(ctx)
)
return &postgresqldb{db: db, err: nil}
}
func (d *postgresqldb) Raw(query string, args ...interface{}) ORM {
var (
db = d.db.Raw(query, args...)
err = db.Error
)
return &postgresqldb{db, err}
}
func (d *postgresqldb) Exec(query string, args ...interface{}) ORM {
var (
db = d.db.Exec(query, args...)
err = db.Error
)
return &postgresqldb{db, err}
}
func (d *postgresqldb) Scan(object interface{}) error {
var (
db = d.db.Scan(object)
)
return db.Error
}
func (d *postgresqldb) Preload(assoc string, args ...interface{}) ORM {
var (
db = d.db.Preload(assoc, args)
err = db.Error
)
return &postgresqldb{db, err}
}
func (d *postgresqldb) Joins(assoc string) ORM {
var (
db = d.db.Joins(assoc)
err = db.Error
)
return &postgresqldb{db, err}
}
func (d *postgresqldb) GetGormInstance() *gorm.DB {
return d.db
}
func (d *postgresqldb) Count(count *int64) error {
var (
res = d.db.Count(count)
)
if res.Error != nil {
return res.Error
}
return nil
}
func (d *postgresqldb) Association(column string) ORMAssociation {
return d.db.Association(column)
}
func (d *postgresqldb) Or(query interface{}, args ...interface{}) ORM {
var (
db = d.db.Or(query, args...)
err = db.Error
)
return &postgresqldb{db, err}
}
func (d *postgresqldb) Save(data interface{}) error {
var (
db = d.db.Save(data)
err = db.Error
)
return err
}
func NewPostgreSql(option *PostgreSqlOption) (ORM, error) {
var (
opts = &gorm.Config{
QueryFields: true,
}
)
if option.Logger != nil {
opts.Logger = logger.New(option.Logger, logger.Config{
SlowThreshold: time.Second,
LogLevel: logger.Info,
Colorful: false,
IgnoreRecordNotFoundError: false,
})
}
db, err := gorm.Open(postgres.Open(option.ConnectionString), opts)
if err != nil {
return nil, err
}
sql, err := db.DB()
if err != nil {
return nil, err
}
sql.SetConnMaxLifetime(option.MaxLifeTimeConnection)
sql.SetMaxOpenConns(option.MaxOpenConnection)
sql.SetMaxIdleConns(option.MaxIdleConnection)
return &postgresqldb{db: db}, nil
}
package errors
import "github.com/joomcode/errorx"
type (
ErrorDescription struct {
Code string
HttpCode int
Message, FullMessage, Source string
}
)
var (
ErrCodeProperty = errorx.RegisterProperty("code")
ErrHttpCodeProperty = errorx.RegisterProperty("httpcode")
ErrSourceProperty = errorx.RegisterProperty("source")
ErrMessage = errorx.RegisterProperty("message")
ErrNamespace = errorx.NewNamespace("nobu")
ErrBase = errorx.NewType(ErrNamespace, "base")
ErrSessionHeader = ErrBase.New("Authorization header is empty").WithProperty(ErrCodeProperty, "401").WithProperty(ErrHttpCodeProperty, 401)
// - session
ErrExpiredSession = ErrBase.New("session is already expired").WithProperty(ErrCodeProperty, "1000").WithProperty(ErrHttpCodeProperty, 401)
ErrSession = ErrBase.New("unauthorized").WithProperty(ErrCodeProperty, "1002").WithProperty(ErrHttpCodeProperty, 401)
// - json
ErrJsonMarshal = ErrBase.New("failed marshal to json").WithProperty(ErrCodeProperty, "1003").WithProperty(ErrHttpCodeProperty, 400)
ErrJsonUnmarshal = ErrBase.New("failed unmarshal from json").WithProperty(ErrCodeProperty, "1003").WithProperty(ErrHttpCodeProperty, 400)
// - validation
ErrValidation = ErrBase.New("failed to validate request body").WithProperty(ErrCodeProperty, "1004").WithProperty(ErrHttpCodeProperty, 400)
)
func WrapErr(err error, message string) *errorx.Error {
return errorx.Decorate(err, message)
}
func ExtractError(err error) ErrorDescription {
var (
e, ok = err.(*errorx.Error)
)
if ok {
if ErrNamespace.IsNamespaceOf(e.Type()) {
code, source, httpcode := "0", "internal", 0
c, ok := errorx.ExtractProperty(e, ErrCodeProperty)
if ok {
code = c.(string)
} else {
code = "500"
}
hc, ok := errorx.ExtractProperty(e, ErrHttpCodeProperty)
if ok {
httpcode = hc.(int)
} else {
httpcode = 500
}
s, ok := errorx.ExtractProperty(e, ErrSourceProperty)
if ok {
source = s.(string)
}
return ErrorDescription{code, httpcode, e.Message(), e.Error(), source}
}
}
return ErrorDescription{
Code: "500",
HttpCode: 500,
Message: "internal server error",
FullMessage: err.Error(),
Source: "internal",
}
}
func New(hc int, code, message string) *errorx.Error {
return ErrBase.New(message).
WithProperty(ErrCodeProperty, code).
WithProperty(ErrHttpCodeProperty, hc)
}
func NewWithSource(hc int, code, message, source string) *errorx.Error {
return ErrBase.New(message).
WithProperty(ErrCodeProperty, code).
WithProperty(ErrHttpCodeProperty, hc).
WithProperty(ErrSourceProperty, source)
}
module gitlab.ursabyte.com/faizal.aziz/ulfssar-go
go 1.19
require (
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5
github.com/go-playground/validator/v10 v10.10.0
github.com/joho/godotenv v1.4.0
github.com/joomcode/errorx v1.1.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/labstack/echo/v4 v4.10.2
github.com/labstack/gommon v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.6.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.7.0 // indirect
gorm.io/driver/postgres v1.3.8
gorm.io/gorm v1.23.8
)
require (
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/uuid v1.3.0
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.4.2
go.uber.org/zap v1.21.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
)
require (
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-resty/resty/v2 v2.2.0
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.12.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.11.0 // indirect
github.com/jackc/pgx/v4 v4.16.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lestrrat-go/strftime v1.0.6 // indirect
github.com/smartystreets/goconvey v1.8.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
)
This diff is collapsed.
package http
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"sync"
"time"
)
const (
// This is the default timeout set into the default HTTP client (if a client is not supplied)
defaultTimeout = 3 * time.Second
// This is the default connect timeout set into the
defaultConnectTimeout = 1 * time.Second
)
var (
// ErrConnectTimeout indicates that we were unable to connect to the destination host and by extension the destination host cannot have
// processed this request in any way
ErrConnectTimeout = errors.New("connection timeout")
// ErrConnection indicates that there were errors (other than timeout) connecting to the destination host
ErrConnection = errors.New("error initiating connection")
// ErrTimeout indicates that we succeeded to connect to the destination host but failed to receive the response before the Timeout or
// context timeout expired.
// By extension this error implies that the destination received the request and may have partial processed it.
ErrTimeout = errors.New("timeout")
)
// Client is a drop-in replacement for the standard http.Client that provides additional features.
// Please note, as with the http.Client it is strongly recommended that a single instance of this client is created and then
// shared amongst the goroutines that make this request type. Allowing for connection pooling and other performance optimizations.
type Client struct {
// Name is the unique name for this client.
// This name is used to track errors, emit telemetry, etc.
// It is recommended to use an identifiable name link the service or endpoint being called.
Name string
// Client is the underlying HTTP client that will be used to make the requests.
// User are encouraged to populate this and explicitly set timeouts.
// If users do not populate this field, it will be automatically populated with this package's default settings.
// Users must not change or access this client after initial creation and a data race may result.
Client *http.Client
clientInitOnce sync.Once
// Timeout is the total timeout (including connection and read timeout) of a particular request
Timeout time.Duration
// ConnectTimeout is the timeout for the connection initiation phase.
// Note: ConnectTimeout should be lesser than timeout. Else, ErrConnectTimeout cannot be caught
ConnectTimeout time.Duration
// Instrumentation allows reporting and logging of internal events and statistics
Instrumentation Instrumentation
// CircuitBreaker defines the (optional) circuit breaker configuration for this client.
CircuitBreaker CircuitBreaker
}
// Do performs the HTTP request provided.
//
// Note: This method does not take a context as it uses the context inside the Request parameter.
// Note: Timeouts should be set using the context.Context in the Request.
// For more information see https://godoc.org/net/http#Client.Do
// nolint:funlen
func (c *Client) Do(req *http.Request) (*http.Response, error) {
start := time.Now()
path := c.getInstrumentation().SanitizePath(req.URL.Path)
endpointTag := generateEndpointTag(req.Method, path)
defer c.getInstrumentation().DoDuration(start, endpointTag)
// base request
doRequestFunc := func(req *http.Request) (*http.Response, error) {
resp, err := c.getClient().Do(req)
if err != nil {
c.getInstrumentation().BaseDoDuration(start, 0, endpointTag)
var urlErr *url.Error
switch {
case errors.As(err, &urlErr) && urlErr.Timeout():
c.getInstrumentation().BaseDoErr(err, endpointTag, "timeout")
return resp, err
case errors.Is(err, context.DeadlineExceeded):
c.getInstrumentation().BaseDoErr(err, endpointTag, "ctxTimeout")
return resp, err
case errors.Is(err, context.Canceled):
c.getInstrumentation().BaseDoErr(err, endpointTag, "ctxCanceled")
return resp, err
default:
c.getInstrumentation().BaseDoErr(err, endpointTag, "na")
return resp, err
}
}
c.getInstrumentation().BaseDoDuration(start, resp.StatusCode, endpointTag)
return resp, nil
}
// add middleware (note: be wary of the ordering here)
// retries are inside the circuit; this means the circuit only see complete failure
doRequestFunc = (&c.CircuitBreaker).addMiddleware(doRequestFunc)
// perform request + middleware
resp, err := doRequestFunc(req)
if err != nil {
return resp, err
}
return resp, nil
}
// all access to the http.Client by this struct should be via this method.
func (c *Client) getClient() *http.Client {
c.clientInitOnce.Do(c.doInitOnce)
return c.Client
}
// all access to the Instrumentation by this struct should be via this method.
func (c *Client) getInstrumentation() Instrumentation {
c.clientInitOnce.Do(c.doInitOnce)
return c.Instrumentation
}
func (c *Client) doInitOnce() {
if c.Instrumentation == nil {
c.Instrumentation = &noopInstrumentation{}
}
if c.Timeout == 0 {
c.Timeout = defaultTimeout
}
if c.ConnectTimeout == 0 {
c.ConnectTimeout = defaultConnectTimeout
}
if c.Client == nil {
c.Client = buildClient(c.Timeout, c.ConnectTimeout)
}
if c.Name == "" {
c.Instrumentation.InitWarning("name was not supplied. Use of unique and informative names is strongly recommended")
c.Name = fmt.Sprintf("smart-http-%d", time.Now().UnixNano())
}
c.Instrumentation.Init(c.Name)
(&c.CircuitBreaker).doInitOnce(c.Instrumentation, c.Name)
}
// GetTransportWithCustomDialer is used internally to assist with detecting connection timeouts during Dial().
// It is provided here so others can use it with their own http.Transport.
func GetTransportWithCustomDialer(connectionTimeout time.Duration) *http.Transport {
return &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (conn net.Conn, err error) {
dialer := net.Dialer{
Timeout: connectionTimeout,
}
conn, err = dialer.DialContext(ctx, network, addr)
if err != nil {
if netError, ok := err.(net.Error); ok {
if netError.Timeout() {
return nil, ErrConnectTimeout
}
return nil, fmt.Errorf("%w %v", ErrConnection, err)
}
return nil, err
}
return conn, nil
},
}
}
func buildClient(timeout, connectTimeout time.Duration) *http.Client {
return &http.Client{
Timeout: timeout,
Transport: GetTransportWithCustomDialer(connectTimeout),
}
}
func generateEndpointTag(method, path string) string {
return method + "::" + path
}
type requestClosure func(*http.Request) (*http.Response, error)
package http
import (
"github.com/afex/hystrix-go/hystrix"
"github.com/pkg/errors"
"net/http"
"time"
)
const (
defaultErrorThreshold = 80
minErrorThreshold = 50
)
var (
defaultMaxConcurrentRequests = hystrix.DefaultMaxConcurrent
// see `getTimeout()` for more details
defaultCircuitBreakerTimeout = 1 * time.Hour
// This indicates a HTTP response code that should be tracked by the circuit
errTrackableStatusCodeError = errors.New("response code is tracked by the circuit")
// ErrCircuitIsOpen indicates that the circuit is open and any available fallback should be used
ErrCircuitIsOpen = errors.New("the circuit is open")
// ErrCircuitMaxConcurrencyReached indicates that there are more concurrent requests than configured going through
// the circuit
ErrCircuitMaxConcurrencyReached = errors.New("the circuit's max concurrency is reached")
// ErrCircuitTimeout indicates that the circuit timed-out the request
ErrCircuitTimeout = errors.New("the circuit timed out the request")
)
// CircuitBreaker defines the circuit breaker configuration
type CircuitBreaker struct {
// Default value is 80 (cannot be set below 50)
ErrorPercentThreshold int
// Default value is 10 (setting above 100 is not advisable)
MaxConcurrentRequests int
name string
instrumentation Instrumentation
// used for testing only
trackError func(cb *CircuitBreaker)
totalTrackedErrors int
}
func (b *CircuitBreaker) getTimeout() int {
// Set a timeout that is so long that all other timeouts will trigger first
// We are essentially disabling this timeout
return int(defaultCircuitBreakerTimeout.Milliseconds())
}
func (b *CircuitBreaker) getMaxConcurrent() int {
if b.MaxConcurrentRequests > 0 {
return b.MaxConcurrentRequests
}
b.instrumentation.InitWarning("using default 'max concurrent requests' setting for circuit breaker")
return defaultMaxConcurrentRequests
}
func (b *CircuitBreaker) getErrorPercent() int {
if b.ErrorPercentThreshold > minErrorThreshold {
return b.ErrorPercentThreshold
}
b.instrumentation.InitWarning("using default 'error threshold' setting for circuit breaker")
return defaultErrorThreshold
}
//nolint:bodyclose
func (b *CircuitBreaker) buildMiddleware(doFunc requestClosure) requestClosure {
return func(req *http.Request) (*http.Response, error) {
var resp *http.Response
err := hystrix.Do(b.name, func() error {
var innerErr error
resp, innerErr = doFunc(req)
if innerErr != nil {
return innerErr
}
return b.outErrorBasedOnResponseCode(req, resp)
}, nil)
switch err {
case hystrix.ErrCircuitOpen:
b.instrumentation.CBCircuitOpen(req)
return resp, ErrCircuitIsOpen
case hystrix.ErrMaxConcurrency:
return resp, ErrCircuitMaxConcurrencyReached
case hystrix.ErrTimeout:
return resp, ErrCircuitTimeout
case nil, errTrackableStatusCodeError:
return resp, nil
default:
return resp, err
}
}
}
func (b *CircuitBreaker) outErrorBasedOnResponseCode(req *http.Request, resp *http.Response) error {
// process HTTP response codes (and throw errors that we should track)
switch resp.StatusCode {
case http.StatusRequestTimeout,
http.StatusTooManyRequests,
http.StatusInternalServerError,
http.StatusNotImplemented,
http.StatusBadGateway,
http.StatusServiceUnavailable,
http.StatusGatewayTimeout,
http.StatusHTTPVersionNotSupported,
http.StatusVariantAlsoNegotiates,
http.StatusInsufficientStorage,
http.StatusLoopDetected,
http.StatusNotExtended,
http.StatusNetworkAuthenticationRequired:
// these HTTP response codes should be tracked by the circuit breaker
b.trackError(b)
b.instrumentation.CBTrackedStatusCode(req, resp.StatusCode)
return errTrackableStatusCodeError
default:
// do not track these HTTP response codes (they are success codes or user errors)
return nil
}
}
func (b *CircuitBreaker) addMiddleware(doFunc requestClosure) requestClosure {
if b == nil {
return doFunc
}
return b.buildMiddleware(doFunc)
}
func (b *CircuitBreaker) doInitOnce(instrumentation Instrumentation, name string) {
if b == nil {
instrumentation.InitWarning("no circuit breaker has been configured. CB use is strongly recommended")
return
}
b.name = name
b.instrumentation = instrumentation
hystrix.ConfigureCommand(b.name, hystrix.CommandConfig{
Timeout: b.getTimeout(),
MaxConcurrentRequests: b.getMaxConcurrent(),
ErrorPercentThreshold: b.getErrorPercent(),
})
if b.trackError == nil {
b.trackError = func(_ *CircuitBreaker) {
// noop
}
}
}
package http
import (
"net/http"
"time"
)
type Instrumentation interface {
// Init is called once during initialization
Init(name string)
// InitWarning is called during init for warnings
InitWarning(message string)
// SanitizePath sanitizes the url path that can be sent to DataDog as a tag
SanitizePath(urlPath string) string
// DoDuration is the total time taken to complete the request (includes retries)
DoDuration(start time.Time, endpointTag string)
// BaseDoDuration is the time taken to make a single http.Client.Do() request
BaseDoDuration(start time.Time, statusCode int, endpointTag string)
// BaseDoErr is called when the underlying http.Client.Do() request returns an error
BaseDoErr(err error, endpointTag, errTag string)
// CBCircuitOpen is called when the circuit breaker circuit is open
CBCircuitOpen(req *http.Request)
// CBTrackedStatusCode is called when the response code is tracked by the circuit breaker as an error
CBTrackedStatusCode(req *http.Request, code int)
// RetryNonRetriable is called when a non-retriable HTTP status code or error has been returned
RetryNonRetriable(req *http.Request, code int)
// RetryRetriable is called when a retriable HTTP status code or error has been returned
// NOTE: when errors occur status code is set to 666
RetryRetriable(req *http.Request, code int)
// SingleflightErr is called when singleflight returns an error
SingleflightErr(req *http.Request, err error)
}
type noopInstrumentation struct{}
func (n *noopInstrumentation) Init(_ string) {}
func (n *noopInstrumentation) InitWarning(_ string) {}
func (n *noopInstrumentation) SanitizePath(_ string) string { return "" }
func (n *noopInstrumentation) DoDuration(_ time.Time, _ string) {}
func (n *noopInstrumentation) BaseDoDuration(_ time.Time, _ int, _ string) {}
func (n *noopInstrumentation) BaseDoErr(_ error, _, _ string) {}
func (n *noopInstrumentation) CBCircuitOpen(_ *http.Request) {}
func (n *noopInstrumentation) CBTrackedStatusCode(_ *http.Request, _ int) {}
func (n *noopInstrumentation) RetryNonRetriable(_ *http.Request, _ int) {}
func (n *noopInstrumentation) RetryRetriable(_ *http.Request, _ int) {}
func (n *noopInstrumentation) SingleflightErr(_ *http.Request, _ error) {}
package logger
import (
"context"
"github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
)
type (
Logger interface {
Info(...interface{})
Infof(string, ...interface{})
Debug(...interface{})
Debugf(string, ...interface{})
Error(...interface{})
Errorf(string, ...interface{})
Warning(...interface{})
Warningf(string, ...interface{})
Fatal(...interface{})
Fatalf(string, ...interface{})
Print(...interface{})
Printf(string, ...interface{})
Println(...interface{})
Instance() interface{}
DebugWithCtx(context.Context, string, ...Field)
DebugfWithCtx(context.Context, string, ...interface{})
InfoWithCtx(context.Context, string, ...Field)
InfofWithCtx(context.Context, string, ...interface{})
WarnWithCtx(context.Context, string, ...Field)
WarnfWithCtx(context.Context, string, ...interface{})
ErrorWithCtx(context.Context, string, ...Field)
ErrorfWithCtx(context.Context, string, ...interface{})
FatalWithCtx(context.Context, string, ...Field)
FatalfWithCtx(context.Context, string, ...interface{})
Summary(tdr LogSummary)
}
LogSummary struct {
ExternalID string `json:"external_id"`
JourneyID string `json:"journey_id"`
ChainID string `json:"chain_id"`
RespTime int64 `json:"rt"`
Error string `json:"error"`
URI string `json:"uri"`
Header interface{} `json:"header"`
Request interface{} `json:"req"`
Response interface{} `json:"resp"`
AdditionalData interface{} `json:"additional_data"`
}
Field struct {
Key string
Val interface{}
}
Level string
Formatter string
Option struct {
Level Level
LogFilePath string
Formatter Formatter
MaxSize, MaxBackups, MaxAge int
Compress bool
}
lumberjackHook struct {
lbj *lumberjack.Logger
logrus *logrus.Logger
}
impl struct {
instance *logrus.Logger
}
)
const (
Info Level = "INFO"
Debug Level = "DEBUG"
Error Level = "ERROR"
JSONFormatter Formatter = "JSON"
)
func (l *impl) Info(args ...interface{}) {
l.instance.Info(args...)
}
func (l *impl) Infof(format string, args ...interface{}) {
l.instance.Infof(format, args...)
}
func (l *impl) Debug(args ...interface{}) {
l.instance.Debug(args...)
}
func (l *impl) Debugf(format string, args ...interface{}) {
l.instance.Debugf(format, args...)
}
func (l *impl) Error(args ...interface{}) {
l.instance.Error(args...)
}
func (l *impl) Errorf(format string, args ...interface{}) {
l.instance.Errorf(format, args...)
}
func (l *impl) Warning(args ...interface{}) {
l.instance.Warning(args...)
}
func (l *impl) Warningf(format string, args ...interface{}) {
l.instance.Warningf(format, args...)
}
func (l *impl) Fatal(args ...interface{}) {
l.instance.Fatal(args...)
}
func (l *impl) Fatalf(format string, args ...interface{}) {
l.instance.Fatalf(format, args...)
}
func (l *impl) Print(args ...interface{}) {
l.instance.Print(args...)
}
func (l *impl) Println(args ...interface{}) {
l.instance.Println(args...)
}
func (l *impl) Printf(format string, args ...interface{}) {
l.instance.Printf(format, args...)
}
func (l *impl) Instance() interface{} {
return l.instance
}
func (l *impl) DebugWithCtx(ctx context.Context, message string, field ...Field) {
l.instance.Debug(message, field)
}
func (l *impl) DebugfWithCtx(ctx context.Context, format string, args ...interface{}) {
l.instance.Debugf(format, args...)
}
func (l *impl) InfoWithCtx(ctx context.Context, message string, field ...Field) {
l.instance.Info(message, field)
}
func (l *impl) InfofWithCtx(ctx context.Context, format string, args ...interface{}) {
l.instance.Infof(format, args...)
}
func (l *impl) WarnWithCtx(ctx context.Context, message string, field ...Field) {
l.instance.Warning(message, field)
}
func (l *impl) WarnfWithCtx(ctx context.Context, format string, args ...interface{}) {
l.instance.Warnf(format, args...)
}
func (l *impl) ErrorWithCtx(ctx context.Context, message string, field ...Field) {
l.instance.Error(message, field)
}
func (l *impl) ErrorfWithCtx(ctx context.Context, format string, args ...interface{}) {
l.instance.Errorf(format, args...)
}
func (l *impl) FatalWithCtx(ctx context.Context, message string, field ...Field) {
l.instance.Fatal(message, field)
}
func (l *impl) FatalfWithCtx(ctx context.Context, format string, args ...interface{}) {
l.instance.Fatalf(format, args...)
}
func (l *impl) Summary(tdr LogSummary) {
l.instance.Info(tdr)
}
func New(option *Option) (Logger, error) {
instance := logrus.New()
if option.Level == Info {
instance.Level = logrus.InfoLevel
}
if option.Level == Debug {
instance.Level = logrus.DebugLevel
}
if option.Level == Error {
instance.Level = logrus.ErrorLevel
}
var formatter logrus.Formatter
if option.Formatter == JSONFormatter {
formatter = &logrus.JSONFormatter{}
} else {
formatter = &logrus.TextFormatter{}
}
instance.Formatter = formatter
// - check if log file path does exists
if option.LogFilePath != "" {
lbj := &lumberjack.Logger{
Filename: option.LogFilePath,
MaxSize: option.MaxSize,
MaxAge: option.MaxAge,
MaxBackups: option.MaxBackups,
LocalTime: true,
Compress: option.Compress,
}
instance.Hooks.Add(&lumberjackHook{
lbj: lbj,
logrus: instance,
})
}
return &impl{instance}, nil
}
func (l *lumberjackHook) Levels() []logrus.Level {
return []logrus.Level{logrus.InfoLevel, logrus.DebugLevel, logrus.ErrorLevel}
}
func (l *lumberjackHook) Fire(entry *logrus.Entry) error {
b, err := l.logrus.Formatter.Format(entry)
if err != nil {
return err
}
if _, err := l.lbj.Write(b); err != nil {
return err
}
return nil
}
package middlewares
import (
"github.com/labstack/echo/v4"
"strings"
"ulfsaar/context"
"ulfsaar/crypto"
"ulfsaar/logger"
"ulfsaar/session"
)
const (
AuthorizationHeader = "authorization"
)
type (
SessionMiddleware interface {
AuthenticateSession(next echo.HandlerFunc) echo.HandlerFunc
}
impl struct {
secret string
crypto crypto.Crypto
logger logger.Logger
prefixSkip []string
}
)
func (i *impl) AuthenticateSession(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
var (
sctx = context.NewEmptyUlfsaarContext(ctx)
token = ctx.Request().Header.Get(AuthorizationHeader)
)
if i.skipper(ctx) {
return next(sctx)
}
NewSession, err := session.NewSession(i.crypto, token)
if err != nil {
return err
}
sctx.Session = NewSession
sctx.Set("Session", NewSession)
return next(sctx)
}
}
func (i *impl) skipper(c echo.Context) (skip bool) {
url := c.Request().URL.String()
if url == "/" {
skip = true
return
}
for _, urlSkip := range i.prefixSkip {
if strings.HasPrefix(url, urlSkip) {
skip = true
return
}
}
return
}
func NewSessionMiddleware(secret string, crypto crypto.Crypto, logger logger.Logger, prefixSkip ...string) (SessionMiddleware, error) {
return &impl{secret, crypto, logger, prefixSkip}, nil
}
package middlewares
import (
"github.com/labstack/echo/v4"
"ulfsaar/errors"
)
type (
BasicAuthorizationMiddleware interface {
BasicAuthenticate(next echo.HandlerFunc) echo.HandlerFunc
}
basicAuthorizationMiddleware struct {
username, password string
}
)
func (i *basicAuthorizationMiddleware) BasicAuthenticate(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
if i.skipper(ctx) {
return next(ctx)
}
username, password, ok := ctx.Request().BasicAuth()
if !ok {
return errors.ErrSession
}
isValid := (username == i.username) && (password == i.password)
if !isValid {
return errors.ErrSession
}
return next(ctx)
}
}
func (i *basicAuthorizationMiddleware) skipper(c echo.Context) (skip bool) {
url := c.Request().URL.String()
if url == "/" {
skip = true
return
}
return
}
func NewBasicAuthorizationMiddleware(username, password string) (BasicAuthorizationMiddleware, error) {
return &basicAuthorizationMiddleware{username, password}, nil
}
package context
import (
"context"
"time"
"ulfsaar/logger"
pkgLogger "ulfsaar/pkg/logger"
)
type UlfsaarContext struct {
Context context.Context
additionalData map[string]interface{}
Logger logger.Logger
RequestTime time.Time
ExternalID string
JourneyID string
ChainID string
URI string
Header interface{}
Request interface{}
Response interface{}
ErrorCode string
ErrorMessage string
}
func NewNUlfsaarContext(logger logger.Logger, xid, jid, cid string) *UlfsaarContext {
return &UlfsaarContext{
Context: context.Background(),
additionalData: map[string]interface{}{},
Logger: logger,
RequestTime: time.Now(),
ExternalID: xid,
JourneyID: jid,
ChainID: cid,
Header: map[string]interface{}{},
Request: struct{}{},
Response: struct{}{},
}
}
func (c *UlfsaarContext) Get(key string) (data interface{}, ok bool) {
data, ok = c.additionalData[key]
return
}
func (c *UlfsaarContext) Put(key string, data interface{}) {
c.additionalData[key] = data
}
func (c *UlfsaarContext) ToContextLogger() (ctx context.Context) {
ctxVal := pkgLogger.Context{
ExternalID: c.ExternalID,
JourneyID: c.JourneyID,
ChainID: c.ChainID,
AdditionalData: c.additionalData,
}
ctx = pkgLogger.InjectCtx(context.Background(), ctxVal)
return
}
func (c *UlfsaarContext) Debug(message string, field ...logger.Field) {
c.Logger.DebugWithCtx(c.ToContextLogger(), message, field...)
}
func (c *UlfsaarContext) Debugf(format string, arg ...interface{}) {
c.Logger.DebugfWithCtx(c.ToContextLogger(), format, arg...)
}
func (c *UlfsaarContext) Info(message string, field ...logger.Field) {
c.Logger.InfoWithCtx(c.ToContextLogger(), message, field...)
}
func (c *UlfsaarContext) Infof(format string, arg ...interface{}) {
c.Logger.InfofWithCtx(c.ToContextLogger(), format, arg...)
}
func (c *UlfsaarContext) Warn(message string, field ...logger.Field) {
c.Logger.WarnWithCtx(c.ToContextLogger(), message, field...)
}
func (c *UlfsaarContext) Warnf(format string, arg ...interface{}) {
c.Logger.WarnfWithCtx(c.ToContextLogger(), format, arg...)
}
func (c *UlfsaarContext) Error(message string, field ...logger.Field) {
c.Logger.ErrorWithCtx(c.ToContextLogger(), message, field...)
}
func (c *UlfsaarContext) Errorf(format string, arg ...interface{}) {
c.Logger.ErrorfWithCtx(c.ToContextLogger(), format, arg...)
}
func (c *UlfsaarContext) Fatal(message string, field ...logger.Field) {
c.Logger.FatalWithCtx(c.ToContextLogger(), message, field...)
}
func (c *UlfsaarContext) Fatalf(format string, arg ...interface{}) {
c.Logger.FatalfWithCtx(c.ToContextLogger(), format, arg...)
}
func (c *UlfsaarContext) Summary() {
model := logger.LogSummary{
ExternalID: c.ExternalID,
JourneyID: c.JourneyID,
ChainID: c.ChainID,
RespTime: c.ResponseTime(),
Error: c.ErrorMessage,
URI: c.URI,
Header: c.Header,
Request: c.Request,
Response: c.Response,
AdditionalData: c.additionalData,
}
c.Logger.Summary(model)
}
func (c *UlfsaarContext) ResponseTime() int64 {
return time.Since(c.RequestTime).Milliseconds()
}
func (c *UlfsaarContext) GetAdditionalData() map[string]interface{} {
return c.additionalData
}
package echo
import (
"github.com/labstack/echo/v4"
"net/http"
"ulfsaar/errors"
"ulfsaar/pkg/context"
"ulfsaar/pkg/logger"
"ulfsaar/session"
)
const (
Ulfsaar = "UlfsaarContext"
Session = "Session"
)
type (
ApplicationContext struct {
echo.Context
Session *session.Session
UlfsaarContext *context.UlfsaarContext
}
Success struct {
Code string `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
Failed struct {
Code string `json:"code"`
Message string `json:"message"`
Error string `json:"error"`
Data interface{} `json:"data"`
}
)
func (sc *ApplicationContext) Success(data interface{}) error {
hc := http.StatusOK
if data == nil {
data = struct{}{}
}
res := Success{
Code: "00",
Message: "success",
Data: data,
}
sc.UlfsaarContext.Response = res
sc.UlfsaarContext.Info("Outgoing",
logger.ToField("rt", sc.UlfsaarContext.ResponseTime()),
logger.ToField("response", res),
logger.ToField("http_code", hc))
sc.UlfsaarContext.Summary()
sc.UlfsaarContext.ErrorCode = res.Code
sc.UlfsaarContext.ErrorMessage = res.Message
return sc.JSON(hc, res)
}
func (sc *ApplicationContext) SuccessWithMeta(data, meta interface{}) error {
hc := http.StatusOK
res := Success{
Code: "00",
Message: "success",
Data: data,
}
sc.UlfsaarContext.Response = res
sc.UlfsaarContext.Info("Outgoing",
logger.ToField("rt", sc.UlfsaarContext.ResponseTime()),
logger.ToField("response", res),
logger.ToField("http_code", hc))
sc.UlfsaarContext.Summary()
sc.UlfsaarContext.ErrorCode = res.Code
sc.UlfsaarContext.ErrorMessage = res.Message
return sc.JSON(hc, res)
}
func (sc *ApplicationContext) Fail(err error) error {
return sc.FailWithData(err, nil)
}
func (sc *ApplicationContext) FailWithData(err error, data interface{}) error {
var (
ed = errors.ExtractError(err)
)
if data == nil {
data = struct{}{}
}
res := Failed{
Code: ed.Code,
Message: ed.Message,
Error: ed.FullMessage,
Data: data,
}
sc.UlfsaarContext.Response = res
sc.UlfsaarContext.Info("Outgoing",
logger.ToField("rt", sc.UlfsaarContext.ResponseTime()),
logger.ToField("response", res),
logger.ToField("http_code", ed.HttpCode))
sc.UlfsaarContext.Summary()
sc.UlfsaarContext.ErrorCode = res.Code
sc.UlfsaarContext.ErrorMessage = res.Message
return sc.JSON(ed.HttpCode, res)
}
func (sc *ApplicationContext) Raw(hc int, data interface{}) error {
if data == nil {
data = struct{}{}
}
sc.UlfsaarContext.Response = data
sc.UlfsaarContext.Info("Outgoing",
logger.ToField("rt", sc.UlfsaarContext.ResponseTime()),
logger.ToField("response", data),
logger.ToField("http_code", hc))
sc.UlfsaarContext.Summary()
return sc.JSON(hc, data)
}
func (sc *ApplicationContext) AddSession(session *session.Session) *ApplicationContext {
sc.Set(Session, session)
sc.Session = session
return sc
}
func (sc *ApplicationContext) AddNobuContext(rc *context.UlfsaarContext) *ApplicationContext {
sc.Set(Ulfsaar, rc)
sc.UlfsaarContext = rc
return sc
}
func ParseApplicationContext(c echo.Context) *ApplicationContext {
var (
nc = c.Get(Ulfsaar)
ss = c.Get(Session)
sess *session.Session
ctx *context.UlfsaarContext
)
// request context is mandatory on application context
// force casting
ctx = nc.(*context.UlfsaarContext)
if ss != nil {
sess, _ = ss.(*session.Session)
}
return &ApplicationContext{Context: c, UlfsaarContext: ctx, Session: sess}
}
func NewEmptyApplicationContext(parent echo.Context) *ApplicationContext {
return &ApplicationContext{parent, nil, nil}
}
func NewApplicationContext(parent echo.Context) (*ApplicationContext, error) {
pctx, ok := parent.(*ApplicationContext)
if !ok {
return nil, errors.ErrSession
}
if pctx.Session == nil {
return nil, errors.ErrSession
}
return pctx, nil
}
package middlewares
import (
"bytes"
"github.com/labstack/echo/v4"
"io/ioutil"
"strings"
"ulfsaar/logger"
ucontext "ulfsaar/pkg/context"
uecho "ulfsaar/pkg/echo"
pkgLogger "ulfsaar/pkg/logger"
"ulfsaar/utilities"
)
const (
ExternalID = "X-EXTERNAL-ID"
JourneyID = "X-JOURNEY-ID"
ChainID = "X-CHAIN-ID"
)
type (
ContextInjectorMiddleware interface {
Injector(next echo.HandlerFunc) echo.HandlerFunc
}
contextInjectorMiddleware struct {
logger logger.Logger
prefixSkip []string
}
)
func (i *contextInjectorMiddleware) Injector(h echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
var (
tid = c.Request().Header.Get(ExternalID)
jid = c.Request().Header.Get(JourneyID)
cid = c.Request().Header.Get(ChainID)
)
if len(tid) == 0 {
tid, _ = utilities.NewUtils().GenerateUUID()
}
// - Set session to context
ctx := ucontext.NewNUlfsaarContext(i.logger, tid, jid, cid)
ctx.Context = c.Request().Context()
ctx.Header = c.Request().Header
ctx.URI = c.Request().URL.String()
c.Set(uecho.Ulfsaar, ctx)
// print request time
var bodyBytes []byte
if c.Request().Body != nil {
bodyBytes, _ = ioutil.ReadAll(c.Request().Body)
// Restore the io.ReadCloser to its original state
c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
}
ctx.Request = string(bodyBytes)
if !i.skipper(c) {
ctx.Info("Incoming",
pkgLogger.ToField("url", c.Request().URL.String()),
pkgLogger.ToField("header", ctx.Header),
pkgLogger.ToField("request", ctx.Request))
}
return h(c)
}
}
func (i *contextInjectorMiddleware) skipper(c echo.Context) (skip bool) {
url := c.Request().URL.String()
if url == "/" {
skip = true
return
}
for _, urlSkip := range i.prefixSkip {
if strings.HasPrefix(url, urlSkip) {
skip = true
return
}
}
return
}
func NewContextInjectorMiddleware(logger logger.Logger, prefixSkip ...string) (ContextInjectorMiddleware, error) {
return &contextInjectorMiddleware{logger: logger, prefixSkip: prefixSkip}, nil
}
This diff is collapsed.
package http_client
const (
ContentType = "Content-Type"
ApplicationJSON = "application/json"
UserAgent = "User-Agent"
UserAgentValue = "https://digdayatech.id"
)
const (
startProcessingTimeKey = "start_processing_time"
processingTimeKey = "processing_time"
urlKey = "url"
requestKey = "request"
responseKey = "response"
)
package http_client
import "github.com/go-resty/resty/v2"
type MultipartFileRequest struct {
FieldName string
FileName string
File []byte
}
type MultipartField = resty.MultipartField
package http_client
import "time"
type Options struct {
Address string `json:"address"`
Timeout time.Duration `json:"timeout"`
DebugMode bool `json:"debug_mode"`
WithProxy bool `json:"with_proxy"`
ProxyAddress string `json:"proxy_address"`
SkipTLS bool `json:"skip_tls"`
SkipCheckRedirect bool `json:"skip_check_redirect"`
}
package http_client
import (
"encoding/json"
"time"
)
type (
logRequest struct {
Header interface{} `json:"header"`
Body interface{} `json:"body"`
}
logResponse struct {
StatusCode int `json:"status_code"`
Header interface{} `json:"header"`
Body interface{} `json:"body"`
}
)
func startProcessingTime(start time.Time) string {
return start.Format("2006-01-02 15:04:05.000")
}
func processingTime(start time.Time) int64 {
return time.Since(start).Milliseconds()
}
func toRequest(header, body interface{}) *logRequest {
if body == nil {
body = struct{}{}
}
return &logRequest{
Header: header,
Body: body,
}
}
func toResponse(statusCode int, header interface{}, body []byte) *logResponse {
var data interface{}
if body != nil {
if _err := json.Unmarshal(body, &data); _err != nil {
data = body
}
}
return &logResponse{
StatusCode: statusCode,
Header: header,
Body: data,
}
}
package logger
import (
"context"
"fmt"
"ulfsaar/logger"
)
type (
fileLogger struct {
defaultLogger *zapLogger
}
Option struct {
Stdout bool `json:"stdout"`
FileLocation string `json:"file_location"`
FileMaxAge int `json:"file_max_age"`
Level int8 `json:"level"`
}
)
func NewLogger(config *Option) logger.Logger {
fmt.Println("Try NewLogger File...")
if config == nil {
panic("logger file config is nil")
}
log := &fileLogger{
defaultLogger: createLogger(config.Stdout, config.Level, config.FileLocation, config.FileMaxAge),
}
return log
}
func (c *fileLogger) Info(args ...interface{}) {
c.InfoWithCtx(context.Background(), fmt.Sprint(args...))
}
func (c *fileLogger) Infof(format string, args ...interface{}) {
c.InfofWithCtx(context.Background(), format, args...)
}
func (c *fileLogger) Debug(args ...interface{}) {
c.DebugWithCtx(context.Background(), fmt.Sprint(args...))
}
func (c *fileLogger) Debugf(format string, args ...interface{}) {
c.DebugfWithCtx(context.Background(), format, args...)
}
func (c *fileLogger) Error(args ...interface{}) {
c.ErrorWithCtx(context.Background(), fmt.Sprint(args...))
}
func (c *fileLogger) Errorf(format string, args ...interface{}) {
c.ErrorfWithCtx(context.Background(), format, args...)
}
func (c *fileLogger) Warning(args ...interface{}) {
c.WarnWithCtx(context.Background(), fmt.Sprint(args...))
}
func (c *fileLogger) Warningf(format string, args ...interface{}) {
c.WarnfWithCtx(context.Background(), format, args...)
}
func (c *fileLogger) Fatal(args ...interface{}) {
c.FatalWithCtx(context.Background(), fmt.Sprint(args...))
}
func (c *fileLogger) Fatalf(format string, args ...interface{}) {
c.FatalfWithCtx(context.Background(), format, args...)
}
func (c *fileLogger) Print(args ...interface{}) {
c.defaultLogger.Print(args...)
}
func (c *fileLogger) Printf(format string, args ...interface{}) {
c.defaultLogger.Printf(format, args...)
}
func (c *fileLogger) Println(args ...interface{}) {
c.defaultLogger.Println(args...)
}
func (c *fileLogger) Instance() interface{} {
return c.defaultLogger
}
func (c *fileLogger) DebugWithCtx(ctx context.Context, message string, fields ...logger.Field) {
c.defaultLogger.Debug(ctx, message, fields...)
}
func (c *fileLogger) DebugfWithCtx(ctx context.Context, format string, args ...interface{}) {
c.defaultLogger.Debugf(ctx, format, args...)
}
func (c *fileLogger) InfoWithCtx(ctx context.Context, message string, fields ...logger.Field) {
c.defaultLogger.Info(ctx, message, fields...)
}
func (c *fileLogger) InfofWithCtx(ctx context.Context, format string, args ...interface{}) {
c.defaultLogger.Infof(ctx, format, args...)
}
func (c *fileLogger) WarnWithCtx(ctx context.Context, message string, fields ...logger.Field) {
c.defaultLogger.Warn(ctx, message, fields...)
}
func (c *fileLogger) WarnfWithCtx(ctx context.Context, format string, args ...interface{}) {
c.defaultLogger.Warnf(ctx, format, args...)
}
func (c *fileLogger) ErrorWithCtx(ctx context.Context, message string, fields ...logger.Field) {
c.defaultLogger.Error(ctx, message, fields...)
}
func (c *fileLogger) ErrorfWithCtx(ctx context.Context, format string, args ...interface{}) {
c.defaultLogger.Errorf(ctx, format, args...)
}
func (c *fileLogger) FatalWithCtx(ctx context.Context, message string, fields ...logger.Field) {
c.defaultLogger.Fatal(ctx, message, fields...)
}
func (c *fileLogger) FatalfWithCtx(ctx context.Context, format string, args ...interface{}) {
c.defaultLogger.Fatalf(ctx, format, args...)
}
func (c *fileLogger) Summary(tdr logger.LogSummary) {
c.defaultLogger.Summary(tdr)
}
package logger
type (
Context struct {
ExternalID string `json:"external_id"`
JourneyID string `json:"journey_id"`
ChainID string `json:"chain_id"`
AdditionalData map[string]interface{} `json:"additional_data,omitempty"`
}
ctxKeyLogger struct{}
)
var ctxKey = ctxKeyLogger{}
package logger
import (
"context"
"encoding/json"
"fmt"
"go.uber.org/zap"
"time"
"ulfsaar/logger"
)
func ToField(key string, val interface{}) (field logger.Field) {
field = logger.Field{
Key: key,
Val: val,
}
return
}
func InjectCtx(parent context.Context, ctx Context) context.Context {
if parent == nil {
return InjectCtx(context.Background(), ctx)
}
return context.WithValue(parent, ctxKey, ctx)
}
func ExtractCtx(ctx context.Context) Context {
if ctx == nil {
return Context{}
}
val, ok := ctx.Value(ctxKey).(Context)
if !ok {
return Context{}
}
return val
}
func ctxToLog(ctx context.Context, logTime time.Time) (logRecord []zap.Field) {
ctxVal := ExtractCtx(ctx)
logRecord = append(logRecord, zap.String(logTimeKey, logTime.Format("2006-01-02 15:04:05.000")))
logRecord = append(logRecord, zap.String(externalIDKey, ctxVal.ExternalID))
if len(ctxVal.JourneyID) != 0 {
logRecord = append(logRecord, zap.String(journeyIDKey, ctxVal.JourneyID))
}
if len(ctxVal.ChainID) != 0 {
logRecord = append(logRecord, zap.String(chainIDKey, ctxVal.ChainID))
}
return
}
func appendToLog(fields ...logger.Field) (logRecord []zap.Field) {
for _, field := range fields {
logRecord = append(logRecord, formatLog(field.Key, field.Val))
}
return
}
func formatLog(key string, msg interface{}) (logRecord zap.Field) {
if msg == nil {
logRecord = zap.Any(key, struct{}{})
return
}
// handle string, string is cannot be masked, just write it
// but try to parse as json object if possible
if str, ok := msg.(string); ok {
var data interface{}
if _err := json.Unmarshal([]byte(str), &data); _err != nil {
logRecord = zap.String(key, str)
return
}
logRecord = zap.Any(key, data)
return
}
// not masked since it failed to convert to reflect.Value above
logRecord = zap.Any(key, msg)
return
}
func createLogger(stdout bool, level int8, location string, age int) *zapLogger {
var opt = make([]ZapOption, 0)
if stdout {
opt = append(opt, WithStdout())
} else {
opt = append(opt, WithFileOutput(location, age))
}
opt = append(opt, WithLevel(level))
log, err := NewZapLogger(opt...)
if err != nil {
panic(fmt.Errorf("init logger with error: %w", err))
}
return log
}
package logger
import (
"context"
"fmt"
rotateLogs "github.com/lestrrat-go/file-rotatelogs"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"io"
"os"
"time"
"ulfsaar/logger"
)
const (
logTimeKey = "log_time"
externalIDKey = "external_id"
journeyIDKey = "journey_id"
chainIDKey = "chain_id"
responseTimeKey = "rt"
responseURI = "uri"
headerKey = "header"
requestKey = "req"
responseKey = "resp"
errorKey = "error"
additionalDataKey = "additional_data"
levelKey = "level"
)
type logLevel string
const (
debugLevel = logLevel("Debug")
infoLevel = logLevel("Info")
warnLevel = logLevel("Warn")
errorLevel = logLevel("Error")
panicLevel = logLevel("Panic")
fatalLevel = logLevel("Fatal")
summaryLevel = logLevel("Summary")
)
type (
ZapOption func(*zapLogger) error
zapLogger struct {
writers []io.Writer
closer []io.Closer
zapLog *zap.Logger
level int8
loggerChannel chan logItem
}
logItem struct {
level logLevel
logTime time.Time
ctx context.Context
message string
args []logger.Field
summary logger.LogSummary
}
)
func WithStdout() ZapOption {
return func(logger *zapLogger) error {
// Wire STD output for both type
logger.writers = append(logger.writers, os.Stdout)
return nil
}
}
func WithFileOutput(location string, maxAge int) ZapOption {
return func(logger *zapLogger) error {
output, err := rotateLogs.New(
location+".%Y%m%d",
rotateLogs.WithLinkName(location),
rotateLogs.WithMaxAge(time.Duration(maxAge)*24*time.Hour),
rotateLogs.WithRotationTime(time.Hour),
)
if err != nil {
return fmt.Errorf("sys file error: %w", err)
}
// Wire SYS config only in sys
logger.writers = append(logger.writers, output)
logger.closer = append(logger.closer, output)
return nil
}
}
// WithCustomWriter add custom writer, so you can write using any storage method
// without waiting this package to be updated.
func WithCustomWriter(writer io.WriteCloser) ZapOption {
return func(logger *zapLogger) error {
if writer == nil {
return fmt.Errorf("writer is nil")
}
// wire custom writer to log
logger.writers = append(logger.writers, writer)
logger.closer = append(logger.closer, writer)
return nil
}
}
// WithLevel set level of logger
func WithLevel(level int8) ZapOption {
return func(logger *zapLogger) error {
logger.level = level
return nil
}
}
func NewZapLogger(opts ...ZapOption) (*zapLogger, error) {
defaultLogger := &zapLogger{
writers: make([]io.Writer, 0),
loggerChannel: make(chan logItem, 51200),
}
for _, o := range opts {
if err := o(defaultLogger); err != nil {
return nil, err
}
}
// use stdout only when writer is not specified
if len(defaultLogger.writers) <= 0 {
defaultLogger.writers = append(defaultLogger.writers, zapcore.AddSync(os.Stdout))
}
encoderConfig := zapcore.EncoderConfig{
TimeKey: "time",
MessageKey: "message",
LineEnding: zapcore.DefaultLineEnding,
EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendInt64(d.Nanoseconds() / 1000000)
},
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05.999"))
},
}
encoding := zapcore.NewJSONEncoder(encoderConfig)
// set logger here instead in options to make easy and consistent initiation
// set multiple writer as already set in options
zapWriters := make([]zapcore.WriteSyncer, 0)
for _, writer := range defaultLogger.writers {
if writer == nil {
continue
}
zapWriters = append(zapWriters, zapcore.AddSync(writer))
}
core := zapcore.NewCore(encoding, zapcore.NewMultiWriteSyncer(zapWriters...), zapcore.Level(defaultLogger.level))
defaultLogger.zapLog = zap.New(core)
// single thread for logging
go func() {
for {
select {
case log := <-defaultLogger.loggerChannel:
defaultLogger.logByLevel(log.level, log.logTime, log.ctx, log.message, log.summary, log.args...)
}
}
}()
return defaultLogger, nil
}
func (d *zapLogger) Close() error {
if d.closer == nil {
return nil
}
var err error
for _, closer := range d.closer {
if closer == nil {
continue
}
if e := closer.Close(); e != nil {
err = fmt.Errorf("%w: %q", e, err)
}
}
return err
}
func (d *zapLogger) Debug(ctx context.Context, message string, fields ...logger.Field) {
d.loggerChannel <- logItem{level: debugLevel, logTime: time.Now(), ctx: ctx, message: message, args: fields}
}
func (d *zapLogger) Debugf(ctx context.Context, format string, args ...interface{}) {
d.loggerChannel <- logItem{level: debugLevel, logTime: time.Now(), ctx: ctx, message: fmt.Sprintf(format, args...), args: []logger.Field{}}
}
func (d *zapLogger) Info(ctx context.Context, message string, fields ...logger.Field) {
d.loggerChannel <- logItem{level: infoLevel, logTime: time.Now(), ctx: ctx, message: message, args: fields}
}
func (d *zapLogger) Infof(ctx context.Context, format string, args ...interface{}) {
d.loggerChannel <- logItem{level: infoLevel, logTime: time.Now(), ctx: ctx, message: fmt.Sprintf(format, args...), args: []logger.Field{}}
}
func (d *zapLogger) Warn(ctx context.Context, message string, fields ...logger.Field) {
d.loggerChannel <- logItem{level: warnLevel, logTime: time.Now(), ctx: ctx, message: message, args: fields}
}
func (d *zapLogger) Warnf(ctx context.Context, format string, args ...interface{}) {
d.loggerChannel <- logItem{level: warnLevel, logTime: time.Now(), ctx: ctx, message: fmt.Sprintf(format, args...), args: []logger.Field{}}
}
func (d *zapLogger) Error(ctx context.Context, message string, fields ...logger.Field) {
d.loggerChannel <- logItem{level: errorLevel, logTime: time.Now(), ctx: ctx, message: message, args: fields}
}
func (d *zapLogger) Errorf(ctx context.Context, format string, args ...interface{}) {
d.loggerChannel <- logItem{level: errorLevel, logTime: time.Now(), ctx: ctx, message: fmt.Sprintf(format, args...), args: []logger.Field{}}
}
func (d *zapLogger) Fatal(ctx context.Context, message string, fields ...logger.Field) {
d.loggerChannel <- logItem{level: fatalLevel, logTime: time.Now(), ctx: ctx, message: message, args: fields}
}
func (d *zapLogger) Fatalf(ctx context.Context, format string, args ...interface{}) {
d.loggerChannel <- logItem{level: fatalLevel, logTime: time.Now(), ctx: ctx, message: fmt.Sprintf(format, args...), args: []logger.Field{}}
}
func (d *zapLogger) Print(args ...interface{}) {
d.loggerChannel <- logItem{level: infoLevel, logTime: time.Now(), ctx: nil, message: fmt.Sprint(args...), args: []logger.Field{}}
}
func (d *zapLogger) Printf(format string, args ...interface{}) {
d.loggerChannel <- logItem{level: infoLevel, logTime: time.Now(), ctx: nil, message: fmt.Sprintf(format, args...), args: []logger.Field{}}
}
func (d *zapLogger) Println(args ...interface{}) {
d.loggerChannel <- logItem{level: infoLevel, logTime: time.Now(), ctx: nil, message: fmt.Sprint(args...), args: []logger.Field{}}
}
func (d *zapLogger) Summary(summary logger.LogSummary) {
d.loggerChannel <- logItem{level: summaryLevel, logTime: time.Now(), ctx: nil, message: "Summary", args: nil, summary: summary}
}
func (d *zapLogger) logByLevel(level logLevel, logTime time.Time, ctx context.Context, message string, summary logger.LogSummary, fields ...logger.Field) {
switch level {
case debugLevel:
zapLogs := []zap.Field{
zap.String(levelKey, "debug"),
}
zapLogs = append(zapLogs, ctxToLog(ctx, logTime)...)
zapLogs = append(zapLogs, appendToLog(fields...)...)
d.zapLog.Debug(message, zapLogs...)
case infoLevel:
zapLogs := []zap.Field{
zap.String(levelKey, "info"),
}
zapLogs = append(zapLogs, ctxToLog(ctx, logTime)...)
zapLogs = append(zapLogs, appendToLog(fields...)...)
d.zapLog.Info(message, zapLogs...)
case warnLevel:
zapLogs := []zap.Field{
zap.String(levelKey, "warn"),
}
zapLogs = append(zapLogs, ctxToLog(ctx, logTime)...)
zapLogs = append(zapLogs, appendToLog(fields...)...)
d.zapLog.Warn(message, zapLogs...)
case errorLevel:
zapLogs := []zap.Field{
zap.String(levelKey, "error"),
}
zapLogs = append(zapLogs, ctxToLog(ctx, logTime)...)
zapLogs = append(zapLogs, appendToLog(fields...)...)
d.zapLog.Error(message, zapLogs...)
case panicLevel:
zapLogs := []zap.Field{
zap.String(levelKey, "panic"),
}
zapLogs = append(zapLogs, ctxToLog(ctx, logTime)...)
zapLogs = append(zapLogs, appendToLog(fields...)...)
d.zapLog.Panic(message, zapLogs...)
case fatalLevel:
zapLogs := []zap.Field{
zap.String(levelKey, "fatal"),
}
zapLogs = append(zapLogs, ctxToLog(ctx, logTime)...)
zapLogs = append(zapLogs, appendToLog(fields...)...)
d.zapLog.Fatal(message, zapLogs...)
case summaryLevel:
zapLogs := []zap.Field{
zap.String(levelKey, "summary"),
zap.String(logTimeKey, logTime.Format("2006-01-02 15:04:05.000")),
zap.String(externalIDKey, summary.ExternalID),
zap.String(journeyIDKey, summary.JourneyID),
zap.String(chainIDKey, summary.ChainID),
zap.Int64(responseTimeKey, summary.RespTime),
zap.String(responseURI, summary.URI),
formatLog(headerKey, summary.Header),
formatLog(requestKey, summary.Request),
formatLog(responseKey, summary.Response),
zap.String(errorKey, summary.Error),
formatLog(additionalDataKey, summary.AdditionalData),
}
d.zapLog.Info(message, zapLogs...)
}
}
package session
import (
"github.com/go-playground/validator/v10"
"ulfsaar/crypto"
"ulfsaar/errors"
)
type (
MediaSession struct {
Role string `json:"user_type" validate:"required"`
Directory string `json:"directory" validate:"required"`
Filenames []string `json:"filenames" validate:"required"`
UserID string `json:"user_id" validate:"required"`
}
)
func (ss *MediaSession) Encrypt(cr crypto.Crypto) (string, error) {
enc, _ := cr.Encrypt(ss)
return string(enc), nil
}
func (ss *MediaSession) Valid() error {
return nil
}
func NewMediaSession(cr crypto.Crypto, session string) (*MediaSession, error) {
var (
ss = &MediaSession{}
dec, err = cr.Decrypt(ss, session)
)
if err != nil {
return nil, errors.ErrSession.WithUnderlyingErrors(err)
}
if err := validator.New().Struct(dec); err != nil {
return nil, errors.ErrSession.WithUnderlyingErrors(err)
}
return ss, nil
}
package session
import (
"time"
"github.com/go-playground/validator/v10"
"ulfsaar/crypto"
"ulfsaar/errors"
)
type (
Session struct {
UserId string `json:"user_id" validate:"required"`
Email string `json:"email" validate:"required"`
Name string `json:"name" validate:"required"`
Role string `json:"role" validate:"required"`
Iat int64 `json:"iat" validate:"required"`
Expired int64 `json:"exp" validate:"required"`
}
)
func (ss *Session) IsSessionExpired() error {
if time.Now().After(time.Unix(ss.Expired, 0)) {
return errors.ErrExpiredSession
}
return nil
}
func (ss *Session) ExtendSession(cr crypto.Crypto, duration int64) (string, error) {
ss.Expired = time.Now().Add(time.Duration(duration) * time.Second).Unix()
return ss.Encrypt(cr)
}
func (ss *Session) Encrypt(cr crypto.Crypto) (string, error) {
enc, _ := cr.Encrypt(ss)
return string(enc), nil
}
func (ss *Session) Valid() error {
return nil
}
func NewSession(cr crypto.Crypto, session string) (*Session, error) {
var (
ss = &Session{}
dec, err = cr.Decrypt(ss, session)
)
if err != nil {
return nil, errors.ErrSession.WithUnderlyingErrors(err)
}
if err := validator.New().Struct(dec); err != nil {
return nil, errors.ErrSession.WithUnderlyingErrors(err)
}
if err := ss.IsSessionExpired(); err != nil {
return nil, err
}
return ss, nil
}
package utilities
import "github.com/google/uuid"
type (
Utils interface {
GenerateUUID() (string, error)
}
utilsImpl struct {
}
)
func NewUtils() Utils {
return &utilsImpl{}
}
// GenerateUUID produces random ID based on UUID
func (h *utilsImpl) GenerateUUID() (string, error) {
_uuid, err := uuid.NewRandom()
if err != nil {
return "", err
}
return _uuid.String(), nil
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment