Commit 53365c46 authored by Abiyan Bagus Baskoro's avatar Abiyan Bagus Baskoro

Initialize Ulfsaar Base Go

parent 95d58c82
Pipeline #427 failed with stages
[url "ssh://git@github.com/"]
insteadOf = https://github.com/
\ No newline at end of file
# These are some examples of commonly ignored file patterns.
# You should customize this list as applicable to your project.
# Learn more about .gitignore:
# https://www.atlassian.com/git/tutorials/saving-changes/gitignore
# Node artifact files
node_modules/
dist/
# Compiled Java class files
*.class
# Compiled Python bytecode
*.py[cod]
# Log files
*.log
# Package files
*.jar
# Maven
target/
dist/
# JetBrains IDE
.idea/
# Unit test reports
TEST*.xml
# Generated by MacOS
.DS_Store
# Generated by Windows
Thumbs.db
# Applications
*.app
*.exe
*.war
# Large media files
*.mp4
*.tiff
*.avi
*.flv
*.mov
*.wmv
# ulfsaar-go
## Getting started
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin https://gitlab.ursabyte.com/abiyan89/ulfsaar-base-go.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools
- [ ] [Set up project integrations](https://gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
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"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/errors"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/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 "gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/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("ursa")
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/abiyan89/ulfsaar-base-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)
SummaryInfo(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 (l *impl) SummaryInfo(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"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/context"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/crypto"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/logger"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/session"
"strings"
)
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"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/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"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/logger"
pkgLogger "gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/pkg/logger"
"time"
)
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.SummaryInfo(model)
//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"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/errors"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/pkg/context"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/pkg/logger"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/session"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/utilities"
"net/http"
)
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) SuccessWithBodyMasking(parameterBody []string, data any) error {
hc := http.StatusOK
if data == nil {
data = struct{}{}
}
maskDataBody := utilities.ConvertToFieldMask(parameterBody, data)
res := Success{
Code: "00",
Message: "success",
Data: maskDataBody,
}
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) SuccessWithAllMasking(parameterBody, parameterHeader []string, data any) error {
hc := http.StatusOK
if data == nil {
data = struct{}{}
}
maskDataBody := utilities.ConvertToFieldMask(parameterBody, data)
utilities.CheckFieldMaskHeader(parameterHeader, sc.UlfsaarContext)
res := Success{
Code: "00",
Message: "success",
Data: maskDataBody,
}
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) SuccessWithHeaderMasking(parameterHeader []string, data interface{}) error {
hc := http.StatusOK
if data == nil {
data = struct{}{}
}
utilities.CheckFieldMaskHeader(parameterHeader, sc.UlfsaarContext)
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) AddUlfsaarContext(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"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/logger"
ucontext "gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/pkg/context"
uecho "gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/pkg/echo"
pkgLogger "gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/pkg/logger"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/utilities"
"io/ioutil"
"strings"
)
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"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/pkg/logger"
"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 logger.ConvertLogTime(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"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/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)
}
func (c *fileLogger) SummaryInfo(tdr logger.LogSummary) {
c.defaultLogger.SummaryInfo(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"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/logger"
"go.uber.org/zap"
"time"
)
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)
ctxLogTime := ConvertLogTime(logTime.Format("2006-01-02 15:04:05.000"))
logRecord = append(logRecord, zap.String(logTimeKey, ctxLogTime))
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 ConvertLogTime(date string) string {
t, err := time.Parse("2006-01-02 15:04:05.000", date)
if err != nil {
fmt.Println("Error parsing timestamp:", err)
return ""
}
// Format the timestamp in the desired format
return t.Format("2006-01-02T15:04:05.000Z")
}
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
}
This diff is collapsed.
package session
import (
"github.com/go-playground/validator/v10"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/crypto"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/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"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/crypto"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/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 (
"encoding/json"
"gitlab.ursabyte.com/abiyan89/ulfsaar-base-go/pkg/context"
"net/http"
"reflect"
"slices"
)
type Masking struct {
ParameterData []string `json:"parameter_data"`
ParameterHeader []string `json:"parameter_header"`
}
func ConvertToFieldMask(config []string, s any) interface{} {
valueOf := reflect.ValueOf(s)
if valueOf.Kind() == reflect.Ptr {
valueOf = valueOf.Elem()
}
if valueOf.Kind() == reflect.Slice || valueOf.Kind() == reflect.Array {
// array
return handlingArray(config, s, valueOf)
} else {
if valueOf.Kind() == reflect.String {
return "***"
} else {
myMap := make(map[string]interface{})
byteArr, err := json.Marshal(s)
if err == nil {
err = json.Unmarshal(byteArr, &myMap)
if err == nil {
maskingItems(config, myMap)
return myMap
}
}
return s
}
}
}
func handlingArray(config []string, originalValue any, valueOf reflect.Value) any {
_, ok := valueOf.Interface().([]string)
if !ok {
items := convertAnyToArrayOfInterface(valueOf.Interface())
for _, data := range items {
maskingItems(config, data)
}
if items == nil {
return originalValue
}
return items
} else {
// Masking For Array String
//var items []string
//for i := 0; i < valueOf.Len(); i++ {
// items = append(items, "***")
//}
return originalValue
}
}
func handlingObject(config []string, originalValue any, valueOf reflect.Value) any {
if valueOf.Kind() == reflect.String {
return "***"
} else {
myMap := make(map[string]interface{})
byteArr, err := json.Marshal(originalValue)
if err == nil {
err = json.Unmarshal(byteArr, &myMap)
if err == nil {
maskingItems(config, myMap)
return myMap
}
}
return originalValue
}
}
func checkFieldMask(config []string, s interface{}) interface{} {
valueOf := reflect.ValueOf(s)
if valueOf.Kind() == reflect.Ptr {
valueOf = valueOf.Elem()
}
if valueOf.Kind() != reflect.Struct && valueOf.Kind() != reflect.Map {
return nil
}
if valueOf.CanSet() {
for _, data := range config {
fieldMask := valueOf.FieldByName(data)
if fieldMask.IsValid() && fieldMask.Kind() == reflect.String {
fieldMask.SetString("***")
}
}
}
return valueOf.Interface()
}
func CheckFieldMaskHeader(config []string, ctx *context.UlfsaarContext) {
for _, data := range config {
ctx.Header.(http.Header).Set(data, "***")
}
}
func maskingItems(config []string, item map[string]interface{}) {
for key, _ := range item {
if reflect.Struct == reflect.TypeOf(item[key]).Kind() || reflect.Map == reflect.TypeOf(item[key]).Kind() {
maskingItems(config, item[key].(map[string]interface{}))
} else if reflect.Array == reflect.TypeOf(item[key]).Kind() || reflect.Slice == reflect.TypeOf(item[key]).Kind() {
items := convertAnyToArrayOfInterface(item[key])
if items != nil {
item[key] = items
for _, data := range item[key].([]map[string]interface{}) {
maskingItems(config, data)
}
}
} else {
exist := slices.Contains(config, key)
if exist {
item[key] = "***"
}
}
}
}
func convertAnyToArrayOfInterface(a any) []map[string]interface{} {
var res []map[string]interface{}
value := unPackArray(a)
for i := 0; i < len(value); i++ {
item := value[i]
t := reflect.TypeOf(item).Kind()
if t == reflect.Struct || t == reflect.Map {
var inInterface map[string]interface{}
inRec, _ := json.Marshal(item)
err := json.Unmarshal(inRec, &inInterface)
if err != nil {
continue
}
res = append(res, inInterface)
}
}
return res
}
func unPackArray(a any) []any {
v := reflect.ValueOf(a)
data := make([]any, v.Len())
for i := 0; i < v.Len(); i++ {
data[i] = v.Index(i).Interface()
}
return data
}
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