Core Go Toolkit is a set of libraries aimed at writing Go code within the Flat ecosystem easier
- Go Language
- Custom Flat Context.
- Logger library for golang.
- Functionality needed for initializing a web server with sane settings based on a given Flat scope.
- Database connector (PostgreSQL) && Database Operations
- Error handling library
- Datadog Custom Metrics (WIP)
- Simple HTTP and REST client library for Go (Resty)
- Secrets management service
In order to use flat.Context package, you should define your HTTP handlers as follow:
func ControllerHandler(c *gin.Context, ctx *flat.Context) {
// ...
}When hooking tour handler to GIN you must encapsulate it in flat.Handler function.
v1 := g.Group("/v1")
v1.POST("/", flat.Handler(ControllerHandler))The flat context gives you typed access to request data, and a configured logger that will automatically add the request_id to any logged lines.
Import and use it.
import (
"github.com/gin-gonic/gin"
"net/http"
"github.com/FlatDigital/core-go-toolkit/v2/core/libs/go/logger"
)
func Ping(c *gin.Context) {
logger := logger.LoggerWithName(c, "Ping")
logger.Info("PingLog", logger.Attrs{
"atts_1": "value 1",
"...": "value ...",
"atts_n": "value n",
})
c.String(http.StatusOK, "pong")
}Methods
LoggerWithName(c *gin.Context, name string) *Logger Returns a logger pointer with gin context information. The name is used for more information.
(l *Logger) LogWithLevel(level string, event string, attrs ...Attrs) *Logger Basic log method. Level INFO, DEBUG, WARN or ERROR, and an event are required.
Optional attrs of type Attrs - map[string]interface{} can be passed.
Direct methods for each level are provided.
(l *Logger) Debug(event string, attrs ...Attrs) *Logger
(l *Logger) Error(event string, attrs ...Attrs) *Logger
(l *Logger) Warning(event string, attrs ...Attrs) *Logger
(l *Logger) Info(event string, attrs ...Attrs) *LoggerThis package is though for using when bootstraping the main package of a new application.
Toolkit uses scopes for defining which functionality of an application a given scaling group should respond to.
For example, given a scope production-read the application should bootstrap itself so that it only accept requests pertaining read functionality and respond 404 for every other endpoint; using server defaults for a production environment.
The scope format used is: environment-role-tag (the tag segment is optional).
package main
import (
"github.com/FlatDigital/core-go-toolkit/v2/core/libs/go/server"
)
routes := server.RoutingGroup{
server.RoleRead: func (g *gin.RouterGroup) {
g.GET("/reader", func (c *gin.Context) {})
},
server.RoleWrite: func (g *gin.RouterGroup) {
g.POST("/writer", func (c *gin.Context) {})
},
}
srv, err := server.NewEngine("develop-read", routes)
if err != nil {
log.Fatal(err)
}
srv.Run(":8080")Alternatively you can use a controller with a method that returns a function with signature func(*gin.RouterGroup) and then use that to register the appropriate routes.
In effect you'll be delegating the knowledge of the routes to the module, instead of bundling it with your main program.
reader := reader.NewReader(container)
routes := server.RoutingGroup{
server.RoleRead: reader.RegisterRoutes(basePath),
// ...
}Import lib and use it.
import (
"github.com/FlatDigital/core-go-toolkit/v2/database"
)
dbConfig := database.ServiceConfig{
DBUsername: config.DBReadUsername(),
DBPassword: config.DBReadPassword(),
DBHost: config.DBHost(),
DBName: config.DBName(),
MaxIdleConns: config.DBPoolMaxIdleConns(),
MaxOpenConns: config.DBPoolMaxOpenConns(),
ConnMaxLifetime: time.Duration(config.DBConnMaxLifetime()),
ConnReadTimeout: config.DBConnectionReadTimeout(),
ConnWriteTimeout: config.DBConnectionWriteTimeout(),
ConnTimeout: config.DBConnectionTimeout(),
// MaxConnectionRetries default = 3
// MaxConnectionRetries: ,
}
// Connect to database
db, err := database.NewService(dbConfig)
if err != nil {
panic(err.Error())
}ConnReadTimeout and ConnWriteTimeout not supported by postgres connetion string. If you are interested in being able to set these values, look at this workarround -> https://github.com/Kount/pq-timeouts
config contains all settings for a given environment.
List of basic operations
Execute(dbc *DBContext, query string, params ...interface{}) (*DBResult, error)
ExecuteEnsuringOneAffectedRow(dbc *DBContext, query string, params ...interface{}) error
Select(dbc *DBContext, query string, forUpdate bool, params ...interface{}) (*DBResult, error)
SelectUniqueValue(dbc *DBContext, query string, forUpdate bool, params ...interface{}) (*DBRow, error)
SelectUniqueValueNonEmpty(dbc *DBContext, query string, forUpdate bool, params ...interface{}) (*DBRow, error)
Begin(dbc *DBContext) (*DBContext, error)
Commit(dbc *DBContext) error
Rollback(dbc *DBContext) error
Connection() (*DBContext, error)
Close(dbc *DBContext) errorFor more information check the Database lib inside the toolkit
Simple example of a SELECT statement:
const (
getPropertyByID = `
SELECT
*
FROM
rs_webproperty
WHERE
id = $1;
`
)
func (repository *propertyListingDatabase) GetPropertyListing(id int64) (*entities.PropertyListing, errorWrapper.Wrapper) {
params := []interface{}{
id,
}
dbRow, err := repository.database.SelectUniqueValue(nil, getPropertyByID, false, params...)
if err != nil {
return &entities.PropertyListing{}, errorWrapper.Wrap(err)
}
// No record found
if dbRow == nil {
return nil, nil
}
propertyListing, errConvert := convertToPropertyListing(dbRow)
if errConvert != nil {
return nil, errConvert
}
// done
return propertyListing, nil
}This lib has everything you need to handle errors in our application.
Import lib and use it.
import (
errorWrapper "github.com/FlatDigital/core-go-toolkit/v2/error"
)Most important methods:
// New returns a new err container from the given error
func New(format string, params ...interface{}) Wrapper {
...
}
// Wrap wraps an error in an error container
func Wrap(err error) Wrapper {
...
}
func ReturnError(c *gin.Context, errWrapped Wrapper) {
...
}
func ReturnWrappedErrorFromStatus(statusCode int, err error) Wrapper {
...
}Most common uses:
Example 1:
// check if id is empty
if len(propertyListingIDStr) == 0 {
return 0, errorWrapper.New("property listing id can not be empty")
}Example 2:
dbRow, err := repository.database.SelectUniqueValue(nil, getPropertyByID, false, params...)
if err != nil {
return &entities.PropertyListing{}, errorWrapper.Wrap(err)
}Example 3:
propertyListing, err := handler.usecase.GetPropertyListing(id)
if err != nil {
errorWrapper.ReturnError(c, err)
return
}Example 4:
status, responseRaw, err := repository.resty.MakeGetRequest(nil, url, http.Header{})
if err != nil {
return weatherMsg, errorWrapper.ReturnWrappedErrorFromStatus(status, fmt.Errorf("error executing GET %s [status:%v][response:%s]", url, status, responseRaw))
}Import lib and use it.
import (
"github.com/FlatDigital/core-go-toolkit/v2/src/api/libs/godog"
)Example:
tags := new(godog.Tags).
Add("core_property_listings_api_env", string(application.Context().Environment())).
Add("property_listing_id", fmt.Sprint(id))
godog.RecordSimpleMetric("application.core_property_listings_api.get_property_listing_by_id", 1, tags.ToArray()...)Import lib and use it.
import (
"github.com/FlatDigital/core-go-toolkit/v2/src/api/libs/rest"
)// Default RestyClient
restyClient := rest.NewRestyService()
// If you need you can create a restyclient with config
// Configure according to your needs
requestConfig := rest.RequestConfig{
DisableTimeout: false,
Timeout: 3 * time.Second,
ConnectTimeout: 1500 * time.Millisecond,
}
serviceConfig := rest.ServiceConfig{
BaseURL: config.FlatURL(),
MaxIdleConnsPerHost: config.MaxIdleConnsPerHost(),
RequestConfig: &requestConfig,
}
restyClient := rest.NewRestyServiceWithConfig(serviceConfig)Most common uses:
// GET
status, responseRaw, err := repository.resty.MakeGetRequest(nil, url, http.Header{})
if err != nil {
//do some stuff
}
// POST
status, responseRaw, err := repository.resty.MakePostRequest(nil, url, body, http.Header{})
if err != nil {
//do some stuff
}
...Check all the operations available inside the rest library
This service retrieves secrets that the application needs. Currently the secrets are obtained from environment variables -> os.Getenv(key)
Import lib and use it.
import (
"github.com/FlatDigital/core-go-toolkit/v2/src/api/libs/secrets"
)
// Example
secrets := secrets.NewService()
scope, err := secrets.Get("SCOPE")
if scope == "" || err != nil {
panic(fmt.Errorf("application initialization error - No scope defined"))
} go test -cover ./... Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature) - Commit your Changes (
git commit -m 'Add some AmazingFeature') - Push to the Branch (
git push origin feature/AmazingFeature) - Open a Pull Request
Slack: #tech-core
Nico Albani: nico@flat.mx
Nico de Lara: nicolas.delara@flat.mx