1 - Basic

Basic user guide which could operate by user turn on/off by modifying boot.yaml.

Overview

In basic user guide, we will introduce how to enable bellow functionalities by modifying boot.yaml.

Functionality Description
Swagger UI Enable swagger UI
Common Service Enable common service
RK TV Enable RK TV
Prometheus Client Enable prometheus client
Logging Enable logging middleware
Metrics Enable prometheus middleware
Meta Enable meta middleware
Tracing Enable openTelemetry tracing middleware
Auth Enable basic & ApiKey auth middleware

1.1 - Swagger UI

Enable swagger UI for server.

Prerequisite

We will use swag to generate swagger UI config files.

Option 1: With RK CMD

# Install RK CMD
$ go get -u github.com/rookie-ninja/rk/cmd/rk

# Install swag with rk
$ rk install swag

Option 2: From swag the official repository:

$ go get -u github.com/swaggo/swag/cmd/swag

Installation

go get github.com/rookie-ninja/rk-boot/gin

General options

These are general options to start a gin server with rk-boot

name description type default value
gin.name The name of gin server string N/A
gin.port The port of gin server integer nil, server won’t start
gin.enabled Enable gin entry bool false
gin.description Description of gin entry. string ""

Swagger options

name description type default value
gin.sw.enabled Enable swagger service over gin server boolean false
gin.sw.path The path access swagger service from web string /sw
gin.sw.jsonPath Where the swagger.json files are stored locally string ""
gin.sw.headers Headers would be sent to caller as scheme of [key:value] []string []

Quick start

1.Create boot.yaml

In order to make rk-boot finds out available swagger config files, we need to add gin.sw.jsonPath in boot.yaml file.

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    sw:
      enabled: true
      jsonPath: "docs"
#      path: "sw"        # Default value is "sw", change it as needed
#      headers: []       # Headers that will be set while accessing swagger UI main page.

2.Create main.go

In order to generate swagger config with swag, we need to add comments as bellow.

package main

import (
	"context"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/rookie-ninja/rk-boot"
	"github.com/rookie-ninja/rk-boot/gin"
	"net/http"
)

// @title RK Swagger for Gin
// @version 1.0
// @description This is a greeter service with rk-boot.

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Register handler
	ginEntry := rkbootgin.GetGinEntry("greeter")
	ginEntry.Router.GET("/v1/greeter", Greeter)

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

// @Summary Greeter service
// @Id 1
// @version 1.0
// @produce application/json
// @Param name query string true "Input name"
// @Success 200 {object} GreeterResponse
// @Router /v1/greeter [get]
func Greeter(ctx *gin.Context) {
	ctx.JSON(http.StatusOK, &GreeterResponse{
		Message: fmt.Sprintf("Hello %s!", ctx.Query("name")),
	})
}

// Response.
type GreeterResponse struct {
	Message string
}

3.Generate swagger config

$ swag init

# swagger.json, swagger.yaml, docs.go files will be generated under ./docs folder.
$ tree
.
├── boot.yaml
├── docs
│   ├── docs.go
│   ├── swagger.json
│   └── swagger.yaml
├── go.mod
├── go.sum
└── main.go

1 directory, 7 files

4.Validate

Swagger: http://localhost:8080/sw

Cheers

1.2 - Common Service

Enable common service for server.

Overview

Common service contains builtin API commonly used by user.

Path Description
/rk/v1/apis List APIs in current GinEntry.
/rk/v1/certs List CertEntry.
/rk/v1/configs List ConfigEntry.
/rk/v1/deps List dependencies related application, entire contents of go.mod file would be returned.
/rk/v1/entries List all Entries.
/rk/v1/gc Trigger GC
/rk/v1/healthy Get application healthy status.
/rk/v1/info Get application and process info.
/rk/v1/license Get license related application, entire contents of LICENSE file would be returned.
/rk/v1/logs List logger related entries.
/rk/v1/git Get git information.
/rk/v1/readme Get contents of README file.
/rk/v1/req List prometheus metrics of requests.
/rk/v1/sys Get OS stat.
/rk/v1/tv Get HTML page of /tv.

Installation

go get github.com/rookie-ninja/rk-boot/gin

General options

These are general options to start a gin server with rk-boot

name description type default value
gin.name The name of gin server string N/A
gin.port The port of gin server integer nil, server won’t start
gin.enabled Enable gin entry bool false
gin.description Description of gin entry. string ""

CommonService options

name description type default value
gin.commonService.enabled Enable embedded common service boolean false

Quick start

1.Create boot.yaml

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    commonService:
      enabled: true     # Enable common service

2.Create main.go

package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
    _ "github.com/rookie-ninja/rk-boot/gin"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

3.Validate

$ curl -X GET localhost:8080/rk/v1/healthy
{"healthy":true}

Cheers

4.Enable swagger UI

In order to visualise common service in swagger UI, we need to enable swagger in boot.yaml as bellow.

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    commonService:
      enabled: true     # Enable common service
    sw:
      enabled: true     # Enable swagger UI

Validate

http://localhost:8080/sw

sw-common

Cheers

1.3 - RK TV

Enable RK TV for server.

Overview

RK TV is a web UI contains service information including APIs, process info, metrics etc.

Installation

go get github.com/rookie-ninja/rk-boot
go get github.com/rookie-ninja/rk-gin

General options

These are general options to start a gin server with rk-boot

name description type default value
gin.name The name of gin server string N/A
gin.port The port of gin server integer nil, server won’t start
gin.enabled Enable gin entry bool false
gin.description Description of gin entry. string ""

TV options

name description type default value
gin.tv.enabled Enable RK TV boolean false

Quick start

1.Create boot.yaml

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    tv:
      enabled: true     # Enable TV

2.Create main.go

package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
    _ "github.com/rookie-ninja/rk-gin/boot"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

3.Validate

Validate

http://localhost:8080/rk/v1/tv

tv

Cheers

1.4 - Prometheus client

Enable prometheus client for server.

Installation

go get github.com/rookie-ninja/rk-boot/gin

General options

These are general options to start a gin server with rk-boot

name description type default value
gin.name The name of gin server string N/A
gin.port The port of gin server integer nil, server won’t start
gin.enabled Enable gin entry bool false
gin.description Description of gin entry. string ""

Prometheus options

name description type default value
gin.prom.enabled Enable prometheus boolean false
gin.prom.path Path of prometheus string /metrics
gin.prom.pusher.enabled Enable prometheus pusher bool false
gin.prom.pusher.jobName Job name would be attached as label while pushing to remote pushgateway string ""
gin.prom.pusher.remoteAddress PushGateWay address, could be form of http://x.x.x.x or x.x.x.x string ""
gin.prom.pusher.intervalMs Push interval in milliseconds string 1000
gin.prom.pusher.basicAuth Basic auth used to interact with remote pushgateway, form of [user:pass] string ""
gin.prom.pusher.cert.ref Reference of rkentry.CertEntry string ""

Quick start

1.Create boot.yaml

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    prom:
      enabled: true     # Enable prometheus client
#      path: "metrics"   # Default value is "metrics", set path as needed.

2.Create main.go

package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

3.Validate

Validate

http://localhost:8080/metrics

prom

Cheers

4.Add value to prometheus client

In order to add custom metrics into current prometheus client, we need to clarify bellow concept.

prom

Name Description
MetricsSet RK defined data structure could be used register Counter, Gauge, Histogram and Summary
Prometheus Registerer User need to register MetricsSet into registerer
Prometheus Counter A counter is a cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero on restart.
Prometheus Gauge A gauge is a metric that represents a single numerical value that can arbitrarily go up and down.
Prometheus Histogram A histogram samples observations (usually things like request durations or response sizes) and counts them in configurable buckets. It also provides a sum of all observed values.
Prometheus Summary Similar to a histogram, a summary samples observations (usually things like request durations and response sizes).
Prometheus Namespace Prometheus metrics was consist of namespace_subSystem_metricsName
Prometheus SubSystem Prometheus metrics was consist of namespace_subSystem_metricsName
package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	"github.com/rookie-ninja/rk-boot/gin" 
	"github.com/rookie-ninja/rk-prom"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Create a metrics set into prometheus.Registerer
	ginEntry := rkbootgin.GetGinEntry("greeter")
	set := rkprom.NewMetricsSet("rk", "demo", ginEntry.PromEntry.Registerer)

	// Register counter, gauge, histogram, summary
	set.RegisterCounter("my_counter", "label")
	set.RegisterGauge("my_gauge", "label")
	set.RegisterHistogram("my_histogram", []float64{}, "label")
	set.RegisterSummary("my_summary", rkprom.SummaryObjectives, "label")

	// Increase counter, gauge, histogram, summary with label value
	set.GetCounterWithValues("my_counter", "value").Inc()
	set.GetGaugeWithValues("my_gauge", "value").Add(1.0)
	set.GetHistogramWithValues("my_histogram", "value").Observe(0.1)
	set.GetSummaryWithValues("my_summary", "value").Observe(0.1)

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

5.Validate metrics

Validate

http://localhost:8080/metrics

prom

Cheers

6.Push metrics to Pushgateway

Turn on pusher in boot.yaml.

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    prom:
      enabled: true                         # Enable prometheus client
      pusher:
        enabled : true                      # Enable backend job push metrics to remote pushgateway
        jobName: "demo"                     # Name of current push job
        remoteAddress: "localhost:9091"     # Remote address of pushgateway
        intervalMs: 2000                    # Push interval in milliseconds
#        basicAuth: "user:pass"             # Basic auth of pushgateway
#        cert:
#          ref: "ref"                       # Cert reference defined in CertEntry. Please see advanced user guide for details.

Start pushgateway locally for testing

$ docker run -p 9091:9091 prom/pushgateway

Validate metrics at local pushgateway

http://localhost:9091

pushgateway

Cheers

1.5 - Middleware logging

Enable RPC logging interceptor/middleware for server.

Installation

go get github.com/rookie-ninja/rk-boot/gin

General options

These are general options to start a gin server with rk-boot

name description type default value
gin.name The name of gin server string N/A
gin.port The port of gin server integer nil, server won’t start
gin.enabled Enable gin entry bool false
gin.description Description of gin entry. string ""

Logging options

name description type default value
gin.interceptors.loggingZap.enabled Enable log interceptor boolean false
gin.interceptors.loggingZap.zapLoggerEncoding json or console string console
gin.interceptors.loggingZap.zapLoggerOutputPaths Output paths []string stdout
gin.interceptors.loggingZap.eventLoggerEncoding json or console string console
gin.interceptors.loggingZap.eventLoggerOutputPaths Output paths []string false

Concept

In order to monitor every RPC request, we introduce EventLogger and ZapLogger

ZapLogger

RK bootstrapper adopt zap as the logger. lumberjack as the logger rotation.

Example

2021-07-05T23:35:17.104+0800    INFO    boot/gin_entry.go:631   Bootstrapping GinEntry. {"eventId": "08d11a34-472a-4785-a98c-144a3417d7f0", "entryName": "greeter", "entryType": "GinEntry", "port": 8080, "interceptorsCount": 1, "swEnabled": false, "tlsEnabled": false, "commonServiceEnabled": false, "tvEnabled": false, "promPath": "/metrics", "promPort": 8080}

EventLogger

RK bootstrapper treat RPC request as an Event, and record every RPC request into Event type in rk-query.

Field Description
endTime As name described
startTime As name described
elapsedNano Elapsed time for RPC in nanoseconds
timezone As name described
ids Contains three different ids(eventId, requestId and traceId). If meta interceptor was enabled or event.SetRequestId() was called by user, then requestId would be attached. eventId would be the same as requestId if meta interceptor was enabled. If trace interceptor was enabled, then traceId would be attached.
app Contains appName, appVersion, entryName, entryType.
env Contains arch, az, domain, hostname, localIP, os, realm, region. realm, region, az, domain were retrieved from environment variable named as REALM, REGION, AZ and DOMAIN. “*” means empty environment variable.
payloads Contains RPC related metadata
error Contains errors if occur
counters Set by calling event.SetCounter() by user.
pairs Set by calling event.AddPair() by user.
timing Set by calling event.StartTimer() and event.EndTimer() by user.
remoteAddr As name described
operation RPC method name
resCode Response code of RPC
eventStatus Ended or InProgress

Example

------------------------------------------------------------------------
endTime=2021-06-25T01:30:45.144023+08:00
startTime=2021-06-25T01:30:45.143767+08:00
elapsedNano=255948
timezone=CST
ids={"eventId":"3332e575-43d8-4bfe-84dd-45b5fc5fb104","requestId":"3332e575-43d8-4bfe-84dd-45b5fc5fb104","traceId":"65b9aa7a9705268bba492fdf4a0e5652"}
app={"appName":"rk-gin","appVersion":"master-xxx","entryName":"greeter","entryType":"GinEntry"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={"apiMethod":"GET","apiPath":"/rk/v1/healthy","apiProtocol":"HTTP/1.1","apiQuery":"","userAgent":"curl/7.64.1"}
error={}
counters={}
pairs={}
timing={}
remoteAddr=localhost:60718
operation=/rk/v1/healthy
resCode=200
eventStatus=Ended
EOE

Quick start

1.Create boot.yaml

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    commonService:
      enabled: true          # Enable common service for testing
    interceptors:
      loggingZap:
        enabled: true        # Enable logging interceptor/middleware, by default, stdout would be destination of log

2.Create main.go

package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

3.Validate

Send request

$ curl -X GET localhost:8080/rk/v1/healthy
{"healthy":true}

Log

------------------------------------------------------------------------
endTime=2021-07-05T23:42:35.588164+08:00
startTime=2021-07-05T23:42:35.588095+08:00
elapsedNano=69414
timezone=CST
ids={"eventId":"9b874eea-b16b-4c46-b0f5-d2b7cff6844e"}
app={"appName":"rk-demo","appVersion":"master-f414049","entryName":"greeter","entryType":"GinEntry"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={"apiMethod":"GET","apiPath":"/rk/v1/healthy","apiProtocol":"HTTP/1.1","apiQuery":"","userAgent":"curl/7.64.1"}
error={}
counters={}
pairs={}
timing={}
remoteAddr=localhost:56274
operation=/rk/v1/healthy
resCode=200
eventStatus=Ended
EOE

Cheers

4.JSON format

---
gin:
  - name: greeter
    ...
    interceptors:
      loggingZap:
        ...
        zapLoggerEncoding: "json"          # Override to json format, option: json or console
        eventLoggerEncoding: "json"        # Override to json format, option: json or console

Cheers

5.Output paths

By default, logs will be rotated by 1GB and compressed.

---
gin:
  - name: greeter
    ...
    interceptors:
      loggingZap:
        ...
        zapLoggerOutputPaths: ["logs/app.log"]        # Override output paths, option: json or console
        eventLoggerOutputPaths: ["logs/event.log"]    # Override output paths, option: json or console

Cheers

5.Get RPC scope logger

A new zap logger instance with requestId(if exist enabled by meta interceptor) will be created for every RPC request.

func Greeter(ctx *gin.Context) {
	rkginctx.GetLogger(ctx).Info("Received request")

	ctx.JSON(http.StatusOK, &GreeterResponse{
		Message: fmt.Sprintf("Hello %s!", ctx.Query("name")),
	})
}
2021-07-06T02:04:55.306+0800    INFO    basic/main.go:39        Received request        {"requestId": "b8522178-9fac-47d3-b866-ce43e119f7a3"}

Cheers

6.Modify Event

A new event logger instance will be created for every RPC request.

User can add pairs, counters, errors in event which will be logged as soon as RPC finish.

func Greeter(ctx *gin.Context) {
	event := rkginctx.GetEvent(ctx)
	event.AddPair("key", "value")

	ctx.JSON(http.StatusOK, &GreeterResponse{
		Message: fmt.Sprintf("Hello %s!", ctx.Query("name")),
	})
}
------------------------------------------------------------------------
endTime=2021-07-06T02:08:40.929323+08:00
startTime=2021-07-06T02:08:40.929191+08:00
elapsedNano=132017
timezone=CST
ids={"eventId":"8f5cfe20-12c7-4208-995a-b113c7fc46a1","requestId":"8f5cfe20-12c7-4208-995a-b113c7fc46a1"}
app={"appName":"rk-demo","appVersion":"master-f414049","entryName":"greeter","entryType":"GinEntry"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={"apiMethod":"GET","apiPath":"/v1/greeter","apiProtocol":"HTTP/1.1","apiQuery":"name=rk-dev","userAgent":"curl/7.64.1"}
error={}
counters={}
pairs={"key":"value"}
timing={}
remoteAddr=localhost:57311
operation=/v1/greeter
resCode=200
eventStatus=Ended
EOE

Cheers

1.6 - Middleware metrics

Enable RPC metrics interceptor/middleware for server.

Installation

go get github.com/rookie-ninja/rk-boot/gin

General options

These are general options to start a gin server with rk-boot

name description type default value
gin.name The name of gin server string N/A
gin.port The port of gin server integer nil, server won’t start
gin.enabled Enable gin entry bool false
gin.description Description of gin entry. string ""

Metrics options

name description type default value
gin.interceptors.metricsProm.enabled Enable metrics interceptor boolean false

Concept

By default, metrics interceptor/middleware will record bellow metrics.

Metrics name Metrics type Description
elapsedNano Summary The time elapsed for RPC.
resCode Counter The counter of RPC with resCode.
errors Counter The counter of RPC with errors if occurs.

All of three metrics have the same labels as bellow:

Label name Description
entryName Gin entry name
entryType Gin entry type
realm OS environment variable: REALM, eg: rk
region OS environment variable: REGION, eg: beijing
az OS environment variable: AZ, eg: beijing-1
domain OS environment variable: DOMAIN, eg: prod
instance Hostname
appVersion Retrieved from AppInfoEntry
appName Retrieved from AppInfoEntry
restMethod Restful API method, eg: GET
restPath Restful API path, eg: /rk/v1/healthy
resCode Response code, eg: 200

Quick start

1.Create boot.yaml

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    prom:
      enabled : true         # Enable prometheus client in order to export metrics
    commonService:
      enabled: true          # Enable common service for testing
    interceptors:
      metricsProm:
        enabled: true        # Enable metrics interceptor/middleware

2.Create main.go

package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

3.Validate

Send request

$ curl -X GET localhost:8080/rk/v1/healthy
{"healthy":true}

Prometheus client:

http://localhost:8080/metrics

prom-inter

Cheers

1.7 - Middleware meta

Enable RPC meta interceptor/middleware for server.

Overview

Meta interceptor/middleware will attach bellow headers to server response.

Header key Description
X-Request-Id Request id generated by the interceptor.
X-[Prefix]-App Application name.
X-[Prefix]-App-Version Version of application.
X-[Prefix]-App-Unix-Time Unix time of current application.
X-[Prefix]-Request-Received-Time Time of current request received by application.

Installation

go get github.com/rookie-ninja/rk-boot/gin

General options

These are general options to start a gin server with rk-boot

name description type default value
gin.name The name of gin server string N/A
gin.port The port of gin server integer nil, server won’t start
gin.enabled Enable gin entry bool false
gin.description Description of gin entry. string ""

Meta options

name description type default value
gin.interceptors.meta.enabled Enable meta interceptor boolean false
gin.interceptors.meta.prefix Header key was formed as X--XXX string RK

Quick start

1.Create boot.yaml

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    commonService:
      enabled: true          # Enable common service for testing
    interceptors:
      meta:
        enabled: true        # Enable meta interceptor/middleware

2.Create main.go

package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

3.Validate

Send request

$ curl -vs -X GET localhost:8080/rk/v1/healthy
  ...
  < X-Request-Id: c2ec237e-e141-45af-a4ff-89ce8a9d0636
  < X-Rk-App-Name: rk-demo
  < X-Rk-App-Unix-Time: 2021-07-06T02:46:31.977902+08:00
  < X-Rk-App-Version: master-f414049
  < X-Rk-Received-Time: 2021-07-06T02:46:31.977902+08:00
  ...
  {"healthy":true}

Cheers

4.Override requestId

func Greeter(ctx *gin.Context) {
    // Override request id
	rkginctx.SetHeaderToClient(ctx, rkmid.HeaderRequestId, "request-id-override")
    // We expect new request id attached to logger
	rkginctx.GetLogger(ctx).Info("Received request")

	ctx.JSON(http.StatusOK, &GreeterResponse{
		Message: fmt.Sprintf("Hello %s!", ctx.Query("name")),
	})
}
$ curl -vs -X GET "localhost:8080/v1/greeter?name=rk-dev"
...
< X-Request-Id: request-id-override
< X-Rk-App-Locale: *::*::*::*
< X-Rk-App-Name: rk
< X-Rk-App-Unix-Time: 2022-01-17T14:52:14.111916+08:00
< X-Rk-Received-Time: 2022-01-17T14:52:14.111916+08:00
...
{"Message":"Hello rk-dev!"}

If we enabled logging interceptor/middleware, then we expect request as bellow

2022-01-17T14:52:14.111+0800    INFO    gin/main.go:57  Received request        {"requestId": "request-id-override"}
------------------------------------------------------------------------
endTime=2022-01-17T14:52:14.111979+08:00
startTime=2022-01-17T14:52:14.111911+08:00
elapsedNano=68026
timezone=CST
ids={"eventId":"request-id-override","requestId":"request-id-override"}
app={"appName":"rk","appVersion":"","entryName":"greeter","entryType":"Gin"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={"apiMethod":"GET","apiPath":"/v1/greeter","apiProtocol":"HTTP/1.1","apiQuery":"","userAgent":"curl/7.64.1"}
error={}
counters={}
pairs={}
timing={}
remoteAddr=localhost:62084
operation=/v1/greeter
resCode=200
eventStatus=Ended
EOE

Cheers

5.Override header prefix

---
gin:
  - name: greeter
    ...
    interceptors:
      meta:
        enabled: true        # Enable meta interceptor/middleware
        prefix: "Override"   # Override prefix which formed as X-[Prefix]-xxx
$ curl -vs -X GET localhost:8080/rk/v1/healthy
...
< X-Override-App-Locale: *::*::*::*
< X-Override-App-Name: rk
< X-Override-App-Unix-Time: 2022-01-17T14:53:08.417587+08:00
< X-Override-Received-Time: 2022-01-17T14:53:08.417587+08:00
< X-Request-Id: a1d2d57c-110f-498a-b9a4-f38a27c50e1e

...
{"healthy":true}

Cheers

1.8 - Middleware tracing

Enable RPC tracing interceptor/middleware for server.

Installation

go get github.com/rookie-ninja/rk-boot/gin

General options

These are general options to start a gin server with rk-boot

name description type default value
gin.name The name of gin server string N/A
gin.port The port of gin server integer nil, server won’t start
gin.enabled Enable gin entry bool false
gin.description Description of gin entry. string ""

Tracing options

name description type default value
gin.interceptors.tracingTelemetry.enabled Enable tracing interceptor boolean false
gin.interceptors.tracingTelemetry.exporter.file.enabled Enable file exporter boolean false
gin.interceptors.tracingTelemetry.exporter.file.outputPath Export tracing info to files string stdout
gin.interceptors.tracingTelemetry.exporter.jaeger.agent.enabled Export tracing info to jaeger agent boolean false
gin.interceptors.tracingTelemetry.exporter.jaeger.agent.host As name described string localhost
gin.interceptors.tracingTelemetry.exporter.jaeger.agent.port As name described int 6831
gin.interceptors.tracingTelemetry.exporter.jaeger.collector.enabled Export tracing info to jaeger collector boolean false
gin.interceptors.tracingTelemetry.exporter.jaeger.collector.endpoint As name described string http://localhost:16368/api/trace
gin.interceptors.tracingTelemetry.exporter.jaeger.collector.username As name described string ""
gin.interceptors.tracingTelemetry.exporter.jaeger.collector.password As name described string ""

Quick start

1.Create boot.yaml

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    commonService:
      enabled: true          # Enable common service for testing
    interceptors:
      tracingTelemetry:
        enabled: true        # Enable tracing interceptor/middleware
        exporter:
          file:
            enabled: true
            outputPath: "stdout"

2.Create main.go

package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

3.Validate

Send request

$ curl -X GET localhost:8080/rk/v1/healthy
{"healthy":true}
[
        {
                "SpanContext": {
                        "TraceID": "4b59250ca328450045efe841a443e77d",
                        "SpanID": "4d3545e0c0da05c3",
                        "TraceFlags": "01",
                        "TraceState": null,
                        "Remote": false
                },
                "Parent": {
                        "TraceID": "00000000000000000000000000000000",
                        "SpanID": "0000000000000000",
                        "TraceFlags": "00",
                        "TraceState": null,
                        "Remote": true
                },
                "SpanKind": 1,
                "Name": "/rk/v1/healthy",
                "StartTime": "2021-07-06T03:06:50.871097+08:00",
                "EndTime": "2021-07-06T03:06:50.871167677+08:00",
                ....
                "InstrumentationLibrary": {
                        "Name": "greeter",
                        "Version": "semver:0.20.0"
                }
        }
]

Cheers

4.Export tracing log to file

There will be delay for the couple of seconds before flushing to log file.

---
gin:
  - name: greeter
    ...
    interceptors:
      tracingTelemetry:
        enabled: true                       # Enable tracing interceptor/middleware
        exporter:
          file:
            enabled: true
            outputPath: "logs/tracing.log"  # Log to files

Cheers

5.Export to jaeger

Start jaeger-all-in-one for testing

$ docker run -d --name jaeger \
    -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
    -p 5775:5775/udp \
    -p 6831:6831/udp \
    -p 6832:6832/udp \
    -p 5778:5778 \
    -p 16686:16686 \
    -p 14268:14268 \
    -p 14250:14250 \
    -p 9411:9411 \
    jaegertracing/all-in-one:1.23
---
gin:
  - name: greeter
    ...
    interceptors:
      tracingTelemetry:
        enabled: true                                          # Enable tracing interceptor/middleware
        exporter:
          jaeger:
            agent:
              enabled: true                                    # Export to jaeger agent
#              host: ""                                        # Optional, default: localhost
#              port: 0                                         # Optional, default: 6831
#            collector:
#              enabled: true                                   # Optional, default: false
#              endpoint: ""                                    # Optional, default: http://localhost:14268/api/traces
#              username: ""                                    # Optional, default: ""
#              password: ""                                    # Optional, default: ""

Jaeger:

http://localhost:16686/

jaeger

Cheers

1.9 - Middleware auth

Enable RPC auth interceptor/middleware for server.

Installation

go get github.com/rookie-ninja/rk-boot/gin

General options

These are general options to start a gin server with rk-boot

name description type default value
gin.name The name of gin server string N/A
gin.port The port of gin server integer nil, server won’t start
gin.enabled Enable gin entry bool false
gin.description Description of gin entry. string ""

Auth options

name description type default value
gin.interceptors.auth.enabled Enable auth interceptor boolean false
gin.interceptors.auth.basic Basic auth credentials as scheme of user:pass []string []
gin.interceptors.auth.apiKey API key auth []string []
gin.interceptors.auth.ignorePrefix The paths of prefix that will be ignored by interceptor []string []

Quick start

1.Create boot.yaml

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    commonService:
      enabled: true          # Enable common service for testing
    interceptors:
      auth:
        enabled: true        # Enable auth interceptor/middleware
        basic: ["user:pass"] # Enable basic auth

2.Create main.go

package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

3.Validate

$ curl  -X GET localhost:8080/rk/v1/healthy
# This is RK style error code if unauthorized
{
    "error":{
        "code":401,
        "status":"Unauthorized",
        "message":"Missing authorization, provide one of bellow auth header:[Basic Auth]",
        "details":[]
    }
}

Cheers

4.With X-API-Key auth type

---
gin:
  - name: greeter
    ...
    interceptors:
      auth:
        enabled: true        # Enable auth interceptor/middleware
        apiKey: ["token"]    # Enable X-API-Key auth

Cheers

5.Ignore paths

---
gin:
  - name: greeter
    ...
    interceptors:
      auth:
        enabled: true                     # Enable auth interceptor/middleware
        basic: ["user:pass"]              # Enable basic auth
        ignorePrefix: ["/rk/v1/healthy"]  # Ignoring path with prefix

Cheers

1.10 - Middleware rate limit

Enable rate limit interceptor/middleware for the server.

Installation

go get github.com/rookie-ninja/rk-boot/gin

General options

These are general options to start a gin server with rk-boot

name description type default value
gin.name The name of gin server string N/A
gin.port The port of gin server integer nil, server won’t start
gin.enabled Enable gin entry bool false
gin.description Description of gin entry. string ""

Rate limit options

name description type default value
gin.interceptors.rateLimit.enabled Enable rate limit interceptor boolean false
gin.interceptors.rateLimit.algorithm Provide algorithm, tokenBucket and leakyBucket are available options string tokenBucket
gin.interceptors.rateLimit.reqPerSec Request per second globally int 0
gin.interceptors.rateLimit.paths.path Full path string ""
gin.interceptors.rateLimit.paths.reqPerSec Request per second by full path int 0

Quick start

1.Create boot.yaml

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    commonService:
      enabled: true          # Enable common service for testing
    interceptors:
      rateLimit:
        enabled: true
        algorithm: "leakyBucket"
        reqPerSec: 100
        paths:
          - path: "/rk/v1/healthy"
            reqPerSec: 0

2.Create main.go

package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

3.Validate

Send request

$ curl -X GET localhost:8080/rk/v1/healthy
{
    "error":{
        "code":429,
        "status":"Too Many Requests",
        "message":"",
        "details":[
            "slow down your request"
        ]
    }
}

Cheers

1.11 - Middleware timeout

Enable timeout interceptor/middleware for the server.

Installation

go get github.com/rookie-ninja/rk-boot/gin

General options

These are general options to start a gin server with rk-boot

name description type default value
gin.name The name of gin server string N/A
gin.port The port of gin server integer nil, server won’t start
gin.enabled Enable gin entry bool false
gin.description Description of gin entry. string ""

Timeout options

name description type default value
gin.interceptors.timeout.enabled Enable timeout interceptor boolean false
gin.interceptors.timeout.timeoutMs Global timeout in milliseconds. int 5000
gin.interceptors.timeout.paths.path Full path string ""
gin.interceptors.timeout.paths.timeoutMs Timeout in milliseconds by full path int 5000

Quick start

1.Create boot.yaml

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    commonService:
      enabled: true                                 # Enable common service for testing
    interceptors:
      timeout:
        enabled: true                               # Optional, default: false
        timeoutMs: 5000                             # Optional, default: 5000
        paths: 
          - path: "/rk/v1/gc"                       # Optional, default: ""
            timeoutMs: 1                            # Optional, default: 5000

2.Create main.go

package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

3.Validate

Send request

$ curl -X GET localhost:8080/rk/v1/gc
{
    "error":{
        "code":408,
        "status":"Request Timeout",
        "message":"Request timed out!",
        "details":[]
    }
}

Cheers

1.12 - Middleware gzip

Enable gzip interceptor/middleware for the server.

Installation

go get github.com/rookie-ninja/rk-boot/gin

General options

These are general options to start a gin server with rk-boot

name description type default value
gin.name The name of gin server string N/A
gin.port The port of gin server integer nil, server won’t start
gin.enabled Enable gin entry bool false
gin.description Description of gin entry. string ""

Gzip options

name description type default value
gin.interceptors.gzip.enabled Enable gzip interceptor boolean false
gin.interceptors.gzip.level Provide level of compression, options are noCompression, bestSpeed, bestCompression, defaultCompression, huffmanOnly. string defaultCompression

Quick start

1.Create boot.yaml

---
gin:
  - name: greeter                     # Required
    port: 8080                        # Required
    enabled: true                     # Required
    commonService:
      enabled: true                   # Optional, default: false
    interceptors:
      gzip:
        enabled: true                 # Optional, default: false
        level: bestSpeed              # Optional, options: [noCompression, bestSpeed, bestCompression, defaultCompression, huffmanOnly]

2.Create main.go

// Copyright (c) 2021 rookie-ninja
//
// Use of this source code is governed by an Apache-style
// license that can be found in the LICENSE file.
package main

import (
	"context"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/rookie-ninja/rk-boot"
    "github.com/rookie-ninja/rk-boot/gin"
	"io"
	"net/http"
	"strings"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Register handler
    ginEntry := rkbootgin.GetGinEntry("greeter")
	ginEntry.Router.POST("/v1/post", post)

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

// PostResponse Response of Greeter.
type PostResponse struct {
	ReceivedMessage string
}

// post Handler.
func post(ctx *gin.Context) {
	buf := new(strings.Builder)
	io.Copy(buf, ctx.Request.Body)

	ctx.JSON(http.StatusOK, &PostResponse{
		ReceivedMessage: fmt.Sprintf("Received %s!", buf.String()),
	})
}

3.Validate

Send request

$ echo 'this is message' | gzip | curl --compressed --data-binary @- -H "Content-Encoding: gzip" -H "Accept-Encoding: gzip" localhost:8080/v1/post
{"ReceivedMessage":"this is message\n"}

Cheers

1.13 - Middleware jwt

Enable jwt interceptor/middleware for the server.

Installation

go get github.com/rookie-ninja/rk-boot/gin

General options

These are general options to start a gin server with rk-boot

name description type default value
gin.name The name of gin server string N/A
gin.port The port of gin server integer nil, server won’t start
gin.enabled Enable gin entry bool false
gin.description Description of gin entry. string ""

JWT options

In order to make swagger UI and RK tv work under JWT without JWT token, we need to ignore prefixes of paths as bellow.

jwt:
  ...
  ignorePrefix:
   - "/rk/v1/tv"
   - "/sw"
   - "/rk/v1/assets"
name description type default value
gin.interceptors.jwt.enabled Enable JWT interceptor boolean false
gin.interceptors.jwt.signingKey Required, Provide signing key. string ""
gin.interceptors.jwt.ignorePrefix Provide ignoring path prefix. []string []
gin.interceptors.jwt.signingKeys Provide signing keys as scheme of :. []string []
gin.interceptors.jwt.signingAlgo Provide signing algorithm. string HS256
gin.interceptors.jwt.tokenLookup Provide token lookup scheme, please see bellow description. string “header:Authorization”
gin.interceptors.jwt.authScheme Provide auth scheme. string Bearer

The supported scheme of tokenLookup

// Optional. Default value "header:Authorization".
// Possible values:
// - "header:<name>"
// - "query:<name>"
// - "param:<name>"
// - "cookie:<name>"
// - "form:<name>"
// Multiply sources example:
// - "header: Authorization,cookie: myowncookie"

Quick start

1.Create boot.yaml

---
gin:
  - name: greeter                     # Required
    port: 8080                        # Required
    enabled: true                     # Required
    commonService:
      enabled: true                   # Optional, default: false
    interceptors:
      jwt:
        enabled: true                 # Optional, default: false
        signingKey: "my-secret"       # Required

2.Create main.go

// Copyright (c) 2021 rookie-ninja
//
// Use of this source code is governed by an Apache-style
// license that can be found in the LICENSE file.
package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

3.Validate

  • with valid jwt token
$ curl localhost:8080/rk/v1/healthy -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.EpM5XBzTJZ4J8AfoJEcJrjth8pfH28LWdjLo90sYb9g"
{"healthy":true}
  • with invalid jwt token
$ curl localhost:8080/rk/v1/healthy -H "Authorization: Bearer invalid-jwt-token"
{
    "error":{
        "code":401,
        "status":"Unauthorized",
        "message":"invalid or expired jwt",
        "details":[]
    }
}

Cheers

1.14 - Middleware cors

Enable CORS interceptor/middleware for the server.

Installation

go get github.com/rookie-ninja/rk-boot/gin

General options

These are general options to start a Gin server with rk-boot

name description type default value
gin.name The name of gin server string N/A
gin.port The port of gin server integer nil, server won’t start
gin.enabled Enable gin entry bool false
gin.description Description of gin entry. string ""

CORS options

name description type default value
gin.interceptors.cors.enabled Enable cors interceptor boolean false
gin.interceptors.cors.allowOrigins Provide allowed origins with wildcard enabled. []string *
gin.interceptors.cors.allowMethods Provide allowed methods returns as response header of OPTIONS request. []string All http methods
gin.interceptors.cors.allowHeaders Provide allowed headers returns as response header of OPTIONS request. []string Headers from request
gin.interceptors.cors.allowCredentials Returns as response header of OPTIONS request. bool false
gin.interceptors.cors.exposeHeaders Provide exposed headers returns as response header of OPTIONS request. []string ""
gin.interceptors.cors.maxAge Provide max age returns as response header of OPTIONS request. int 0

Quick start

1.Create boot.yaml

---
gin:
  - name: greeter                     # Required
    port: 8080                        # Required
    enabled: true                     # Required
    commonService:
      enabled: true                   # Optional, default: false
    interceptors:
      cors:
        enabled: true                 # Optional, default: false
        # Accept all origins from localhost
        allowOrigins:
          - "http://localhost:*"      # Optional, default: *

2.Create main.go

// Copyright (c) 2021 rookie-ninja
//
// Use of this source code is governed by an Apache-style
// license that can be found in the LICENSE file.
package rkdemo

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

3.Create cors.html

<!DOCTYPE html>
<html>
<body>

<h1>CORS Test</h1>

<p>Call http://localhost:8080/rk/v1/healthy</p>

<script type="text/javascript">
    window.onload = function() {
        var apiUrl = 'http://localhost:8080/rk/v1/healthy';
        fetch(apiUrl).then(response => response.json()).then(data => {
            document.getElementById("res").innerHTML = data["healthy"]
        }).catch(err => {
            document.getElementById("res").innerHTML = err
        });
    };
</script>

<h4>Response: </h4>
<p id="res"></p>

</body>
</html>

4.Directory hierarchy

.
├── boot.yaml
├── cors.html
├── go.mod
├── go.sum
└── main.go

0 directories, 5 files

5.Validate

Open cors.html

6.With blocking CORS

Set gin.interceptors.cors.allowOrigins to http://localhost:8080 in order to block request.

---
gin:
  - name: greeter                     # Required
    port: 8080                        # Required
    enabled: true                     # Required
    commonService:
      enabled: true                   # Optional, default: false
    interceptors:
      cors:
        enabled: true                 # Optional, default: false
        allowOrigins:
          - "http://localhost:8080"   # Optional, default: *

Cheers

1.15 - Middleware secure

Enable secure interceptor/middleware for the server.

Installation

go get github.com/rookie-ninja/rk-boot/gin

General options

These are general options to start a gin server with rk-boot

name description type default value
gin.name The name of gin server string N/A
gin.port The port of gin server integer nil, server won’t start
gin.enabled Enable gin entry bool false
gin.description Description of gin entry. string ""

Secure options

name description type default value
gin.interceptors.secure.enabled Enable secure interceptor boolean false
gin.interceptors.secure.xssProtection X-XSS-Protection header value. string “1; mode=block”
gin.interceptors.secure.contentTypeNosniff X-Content-Type-Options header value. string nosniff
gin.interceptors.secure.xFrameOptions X-Frame-Options header value. string SAMEORIGIN
gin.interceptors.secure.hstsMaxAge Strict-Transport-Security header value. int 0
gin.interceptors.secure.hstsExcludeSubdomains Excluding subdomains of HSTS. bool false
gin.interceptors.secure.hstsPreloadEnabled Enabling HSTS preload. bool false
gin.interceptors.secure.contentSecurityPolicy Content-Security-Policy header value. string ""
gin.interceptors.secure.cspReportOnly Content-Security-Policy-Report-Only header value. bool false
gin.interceptors.secure.referrerPolicy Referrer-Policy header value. string ""
gin.interceptors.secure.ignorePrefix Ignoring path prefix. []string []

Quick start

1.Create boot.yaml

---
gin:
  - name: greeter                     # Required
    port: 8080                        # Required
    enabled: true                     # Required
    commonService:
      enabled: true                   # Optional, default: false
    interceptors:
      secure:
        enabled: true                 # Optional, default: false

2.Create main.go

// Copyright (c) 2021 rookie-ninja
//
// Use of this source code is governed by an Apache-style
// license that can be found in the LICENSE file.
package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

3.Validate

$ curl -vs localhost:8080/rk/v1/healthy
  ...
  < X-Content-Type-Options: nosniff
  < X-Frame-Options: SAMEORIGIN
  < X-Xss-Protection: 1; mode=block
  < Date: Sun, 05 Dec 2021 18:32:24 GMT
  < Content-Length: 17
  <
  ...

Cheers

1.16 - Middleware CSRF

Enable CSRF interceptor/middleware for the server.

Installation

go get github.com/rookie-ninja/rk-boot/gin

General options

These are general options to start a gin server with rk-boot

name description type default value
gin.name The name of gin server string N/A
gin.port The port of gin server integer nil, server won’t start
gin.enabled Enable gin entry bool false
gin.description Description of gin entry. string ""

CSRF options

name description type default value
gin.interceptors.csrf.enabled Enable csrf interceptor boolean false
gin.interceptors.csrf.tokenLength Provide the length of the generated token. int 32
gin.interceptors.csrf.tokenLookup Provide csrf token lookup rules, please see code comments for details. string “header:X-CSRF-Token”
gin.interceptors.csrf.cookieName Provide name of the CSRF cookie. This cookie will store CSRF token. string _csrf
gin.interceptors.csrf.cookieDomain Domain of the CSRF cookie. string ""
gin.interceptors.csrf.cookiePath Path of the CSRF cookie. string ""
gin.interceptors.csrf.cookieMaxAge Provide max age (in seconds) of the CSRF cookie. int 86400
gin.interceptors.csrf.cookieHttpOnly Indicates if CSRF cookie is HTTP only. bool false
gin.interceptors.csrf.cookieSameSite Indicates SameSite mode of the CSRF cookie. Options: lax, strict, none, default string default
gin.interceptors.csrf.ignorePrefix Ignoring path prefix. []string []

Quick start

1.Create boot.yaml

---
gin:
  - name: greeter                     # Required
    port: 8080                        # Required
    enabled: true                     # Required
    commonService:
      enabled: true                   # Optional, default: false
    interceptors:
      csrf:
        enabled: true                 # Optional, default: false

2.Create main.go

// Copyright (c) 2021 rookie-ninja
//
// Use of this source code is governed by an Apache-style
// license that can be found in the LICENSE file.
package main

import (
	"context"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/rookie-ninja/rk-boot"
	"github.com/rookie-ninja/rk-boot/gin"
	"net/http"
)

// @title RK Swagger for Echo
// @version 1.0
// @description This is a greeter service with rk-boot.

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Register handler
	ginEntry := rkbootgin.GetGinEntry("greeter")
    ginEntry.Router.POST("/v1/greeter", Greeter)
	ginEntry.Router.GET("/v1/greeter", Greeter)

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

// @Summary Greeter service
// @Id 1
// @version 1.0
// @produce application/json
// @Param name query string true "Input name"
// @Success 200 {object} GreeterResponse
// @Router /v1/greeter [get]
func Greeter(ctx echo.Context) error {
	return ctx.JSON(http.StatusOK, &GreeterResponse{
		Message: fmt.Sprintf("Hello %s!", ctx.QueryParam("name")),
	})
}

// Response.
type GreeterResponse struct {
	Message string
}

3.Validate

  • Send GET request, no csrf validation expected and a new cookie should be there.
$ curl -X GET -vs localhost:8080/v1/greeter
  ...
  < HTTP/1.1 200 OK
  < Content-Type: application/json; charset=UTF-8
  < Set-Cookie: _csrf=WyOJLwzhfUGAMDHglkuIRucdpalxolWg; Expires=Mon, 06 Dec 2021 18:38:45 GMT
  < Vary: Cookie
  < Date: Sun, 05 Dec 2021 18:38:45 GMT
  < Content-Length: 22
  <
  {"Message":"Hello !"}
  • POST method with happy case
$ curl -X POST -v --cookie "_csrf=my-test-csrf-token" -H "X-CSRF-Token:my-test-csrf-token" localhost:8080/v1/greeter
  ...
  > Cookie: _csrf=my-test-csrf-token
  > X-CSRF-Token:my-test-csrf-token
  >
  < HTTP/1.1 200 OK
  < Content-Type: application/json; charset=UTF-8
  < Set-Cookie: _csrf=my-test-csrf-token; Expires=Mon, 06 Dec 2021 18:40:13 GMT
  < Vary: Cookie
  < Date: Sun, 05 Dec 2021 18:40:13 GMT
  < Content-Length: 22
  <
  {"Message":"Hello !"}
  • POST method with invalid csrf
$ curl -X POST -v -H "X-CSRF-Token:my-test-csrf-token" localhost:8080/v1/greeter
  ...
  > X-CSRF-Token:my-test-csrf-token
  >
  < HTTP/1.1 403 Forbidden
  < Content-Type: application/json; charset=UTF-8
  < Date: Sun, 05 Dec 2021 18:41:00 GMT
  < Content-Length: 92
  <
  {"error":{"code":403,"status":"Forbidden","message":"invalid csrf token","details":[null]}}

Cheers

2 - Advanced

RK provide bootstrapper for popular frameworks in order to save time for learning complex initializing process.

Overview

In advanced user guide, we will introduce how to enable bellow functionalities by modifying boot.yaml.

Functionality Description
Locale How to distinguish environment based os OS.Environment
Logging Use user defined logging
TLS Enable TLS support
Config Use viper config
AppInfo Use custom application information
Multiple entries Start multiple Gin entries
Shutdown hook Register shutdown hook functions
Error type Use standard error type
Override bootstrapper Override bootstrapper
Trace RPC with logs Trace RPC logs based on traceId

2.1 - Locale

Distinguish entries based on different environment.

Overview

Lets assuming we need to use different config files based on the region like Singapore or Frankfurt.

Since bootstrapper support multiple ConfigEntries in boot.yaml, we need a mechanism to distinguish ConfigEntries.

Concept

How entries selected by bootstrapper?

RK use REALM, REGION, AZ, DOMAIN environment variable to distinguish environments.

Quick start

  • Install
$ go get github.com/rookie-ninja/rk-boot/gin

1.Create config files

config/singapore.yaml

---
region: sinpapore

config/frankfurt.yaml

---
region: frankfurt

config/default.yaml

---
region: default

2.Create boot.yaml

config:
  # Load this config if nothing matched
  - name: my-config
    locale: "*::*::*::*"
    path: config/default.yaml
  # Load this config if REGION=singapore
  - name: my-config
    locale: "*::singapore::*::*"
    path: config/singapore.yaml
  # Load this config if REGION=frankfurt
  - name: my-config
    locale: "*::frankfurt::*::*"
    path: config/frankfurt.yaml
gin:
  - name: greeter
    port: 8080
    enabled: true

3.With REGION=singapore

package main

import (
	"context"
	"fmt"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
	"os"
)

// Application entrance.
func main() {
    // Set REGION=singapore
	os.Setenv("REGION", "singapore")

	// Create a new boot instance.
	boot := rkboot.NewBoot()

	fmt.Println(boot.GetConfigEntry("my-config").GetViper().GetString("region"))

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

Output

singapore

4.With REGION=frankfurt

package main

import (
	"context"
	"fmt"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
	"os"
)

// Application entrance.
func main() {
    // Set REGION=singapore
	os.Setenv("REGION", "frankfurt")

	// Create a new boot instance.
	boot := rkboot.NewBoot()

	fmt.Println(boot.GetConfigEntry("my-config").GetViper().GetString("region"))

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

Output

frankfurt

Cheers

5.With REGION=not-matched

package main

import (
	"context"
	"fmt"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
	"os"
)

// Application entrance.
func main() {
    // Set REGION=singapore
	os.Setenv("REGION", "not-matched")

	// Create a new boot instance.
	boot := rkboot.NewBoot()

	fmt.Println(boot.GetConfigEntry("my-config").GetViper().GetString("region"))

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

Output

default

Cheers

6.With REGION=""

package main

import (
	"context"
	"fmt"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	fmt.Println(boot.GetConfigEntry("my-config").GetViper().GetString("region"))

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

Output

default

Cheers

2.2 - Logging

Customise logging.

Architecture

ZapLoggerEntry

ZapLoggerEntry is used for initializing zap logger.

// ZapLoggerEntry contains bellow fields.
// 1: EntryName: Name of entry.
// 2: EntryType: Type of entry which is ZapLoggerEntryType.
// 3: EntryDescription: Description of ZapLoggerEntry.
// 4: Logger: zap.Logger which was initialized at the beginning.
// 5: LoggerConfig: zap.Logger config which was initialized at the beginning which is not accessible after initialization..
// 6: LumberjackConfig: lumberjack.Logger which was initialized at the beginning.
type ZapLoggerEntry struct {
	EntryName        string             `yaml:"entryName" json:"entryName"`
	EntryType        string             `yaml:"entryType" json:"entryType"`
	EntryDescription string             `yaml:"entryDescription" json:"entryDescription"`
	Logger           *zap.Logger        `yaml:"-" json:"-"`
	LoggerConfig     *zap.Config        `yaml:"zapConfig" json:"zapConfig"`
	LumberjackConfig *lumberjack.Logger `yaml:"lumberjackConfig" json:"lumberjackConfig"`
}

YAML

ZapLoggerEntry follows zap and lumberjack YAML hierarchy.

Please refer to zap and lumberjack site for details.

---
zapLogger:
  - name: zap-logger                      # Required
    description: "Description of entry"   # Optional
    zap:
      level: info                         # Optional, default: info, options: [debug, DEBUG, info, INFO, warn, WARN, dpanic, DPANIC, panic, PANIC, fatal, FATAL]
      development: true                   # Optional, default: true
      disableCaller: false                # Optional, default: false
      disableStacktrace: true             # Optional, default: true
      sampling:                           # Optional, default: empty map
        initial: 0
        thereafter: 0
      encoding: console                   # Optional, default: "console", options: [console, json]
      encoderConfig:
        messageKey: "msg"                 # Optional, default: "msg"
        levelKey: "level"                 # Optional, default: "level"
        timeKey: "ts"                     # Optional, default: "ts"
        nameKey: "logger"                 # Optional, default: "logger"
        callerKey: "caller"               # Optional, default: "caller"
        functionKey: ""                   # Optional, default: ""
        stacktraceKey: "stacktrace"       # Optional, default: "stacktrace"
        lineEnding: "\n"                  # Optional, default: "\n"
        levelEncoder: "capitalColor"      # Optional, default: "capitalColor", options: [capital, capitalColor, color, lowercase]
        timeEncoder: "iso8601"            # Optional, default: "iso8601", options: [rfc3339nano, RFC3339Nano, rfc3339, RFC3339, iso8601, ISO8601, millis, nanos]
        durationEncoder: "string"         # Optional, default: "string", options: [string, nanos, ms]
        callerEncoder: ""                 # Optional, default: ""
        nameEncoder: ""                   # Optional, default: ""
        consoleSeparator: ""              # Optional, default: ""
      outputPaths: [ "stdout" ]           # Optional, default: ["stdout"], stdout would be replaced if specified
      errorOutputPaths: [ "stderr" ]      # Optional, default: ["stderr"], stderr would be replaced if specified
      initialFields:                      # Optional, default: empty map
        key: "value"
    lumberjack:                           # Optional
      filename: "rkapp-event.log"         # Optional, default: It uses <processname>-lumberjack.log in os.TempDir() if empty.
      maxsize: 1024                       # Optional, default: 1024 (MB)
      maxage: 7                           # Optional, default: 7 (days)
      maxbackups: 3                       # Optional, default: 3 (days)
      localtime: true                     # Optional, default: true
      compress: true                      # Optional, default: true

Access

// Access entry
rkentry.GlobalAppCtx.GetZapLoggerEntry("zap-logger")

// Access zap logger
rkentry.GlobalAppCtx.GetZapLoggerEntry("zap-logger").GetLogger()

// Access zap logger config
rkentry.GlobalAppCtx.GetZapLoggerEntry("zap-logger").GetLoggerConfig()

// Access lumberjack config
rkentry.GlobalAppCtx.GetZapLoggerEntry("zap-logger").GetLumberjackConfig()

EventLoggerEntry

RK bootstrapper treat RPC request as an Event, and record every RPC request into Event type in rk-query.

// EventLoggerEntry contains bellow fields.
// 1: EntryName: Name of entry.
// 2: EntryType: Type of entry which is EventLoggerEntryType.
// 3: EntryDescription: Description of EventLoggerEntry.
// 4: EventFactory: rkquery.EventFactory was initialized at the beginning.
// 5: EventHelper: rkquery.EventHelper was initialized at the beginning.
// 6: LoggerConfig: zap.Config which was initialized at the beginning which is not accessible after initialization.
// 7: LumberjackConfig: lumberjack.Logger which was initialized at the beginning.
type EventLoggerEntry struct {
	EntryName        string                `yaml:"entryName" json:"entryName"`
	EntryType        string                `yaml:"entryType" json:"entryType"`
	EntryDescription string                `yaml:"entryDescription" json:"entryDescription"`
	EventFactory     *rkquery.EventFactory `yaml:"-" json:"-"`
	EventHelper      *rkquery.EventHelper  `yaml:"-" json:"-"`
	LoggerConfig     *zap.Config           `yaml:"zapConfig" json:"zapConfig"`
	LumberjackConfig *lumberjack.Logger    `yaml:"lumberjackConfig" json:"lumberjackConfig"`
}

Fields

Field Description
endTime As name described
startTime As name described
elapsedNano Elapsed time for RPC in nanoseconds
timezone As name described
ids Contains three different ids(eventId, requestId and traceId). If meta interceptor was enabled or event.SetRequestId() was called by user, then requestId would be attached. eventId would be the same as requestId if meta interceptor was enabled. If trace interceptor was enabled, then traceId would be attached.
app Contains appName, appVersion, entryName, entryType.
env Contains arch, az, domain, hostname, localIP, os, realm, region. realm, region, az, domain were retrieved from environment variable named as REALM, REGION, AZ and DOMAIN. “*” means empty environment variable.
payloads Contains RPC related metadata
error Contains errors if occur
counters Set by calling event.SetCounter() by user.
pairs Set by calling event.AddPair() by user.
timing Set by calling event.StartTimer() and event.EndTimer() by user.
remoteAddr As name described
operation RPC method name
resCode Response code of RPC
eventStatus Ended or InProgress

Example

------------------------------------------------------------------------
endTime=2021-06-25T01:30:45.144023+08:00
startTime=2021-06-25T01:30:45.143767+08:00
elapsedNano=255948
timezone=CST
ids={"eventId":"3332e575-43d8-4bfe-84dd-45b5fc5fb104","requestId":"3332e575-43d8-4bfe-84dd-45b5fc5fb104","traceId":"65b9aa7a9705268bba492fdf4a0e5652"}
app={"appName":"rk-gin","appVersion":"master-xxx","entryName":"greeter","entryType":"GinEntry"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={"apiMethod":"GET","apiPath":"/rk/v1/healthy","apiProtocol":"HTTP/1.1","apiQuery":"","userAgent":"curl/7.64.1"}
error={}
counters={}
pairs={}
timing={}
remoteAddr=localhost:60718
operation=/rk/v1/healthy
resCode=200
eventStatus=Ended
EOE

YAML

EventLoggerEntry needs application name while creating event log. The application name retrieved from go.mod file. If there is go.mod file missing, then default one would be used.

---
eventLogger:
  - name: event-logger                 # Required
    description: "This is description" # Optional
    encoding: console                  # Optional, default: console, options: console and json
    outputPaths: ["stdout"]            # Optional
    lumberjack:                        # Optional
      filename: "rkapp-event.log"      # Optional, default: It uses <processname>-lumberjack.log in os.TempDir() if empty.
      maxsize: 1024                    # Optional, default: 1024 (MB)
      maxage: 7                        # Optional, default: 7 (days)
      maxbackups: 3                    # Optional, default: 3 (days)
      localtime: true                  # Optional, default: true
      compress: true                   # Optional, default: true

Access

// Access entry
rkentry.GlobalAppCtx.GetEventLoggerEntry("event-logger")

// Access event factory
rkentry.GlobalAppCtx.GetEventLoggerEntry("event-logger").GetEventFactory()

// Access event helper
rkentry.GlobalAppCtx.GetEventLoggerEntry("event-logger").GetEventHelper()

// Access lumberjack config
rkentry.GlobalAppCtx.GetEventLoggerEntry("event-logger").GetLumberjackConfig()

Quick start

  • Install
$ go get github.com/rookie-ninja/rk-boot
$ go get github.com/rookie-ninja/rk-gin

1.Custom ZapLoggerEntry

---
zapLogger:
  - name: zap-logger                      # Required
    description: "Description of entry"   # Optional
    zap:
      encoding: json
gin:
  - name: greeter
    port: 8080
    enabled: true
package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
	"github.com/rookie-ninja/rk-entry/entry"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()
    
    // Get custom ZapLoggerEntry
	rkentry.GlobalAppCtx.GetZapLoggerEntry("zap-logger").GetLogger().Info("Custom zap logger")

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}
{"level":"INFO","ts":"2021-07-06T05:06:16.252+0800","msg":"Custom zap logger"}

Cheers

2.Reference ZapLoggerEntry

Bootstrapper will use this logger by default including interceptor.

---
zapLogger:
  - name: zap-logger
gin:
  - name: greeter
    port: 8080
    enabled: true
    logger:
     zapLogger:
        ref: zap-logger

Cheers

3.Custom EventLoggerEntry

---
eventLogger:
  - name: event-logger
gin:
  - name: greeter
    port: 8080
    enabled: true
package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
	"github.com/rookie-ninja/rk-entry/entry"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()
    
    // Get custom EventLoggerEntry
	helper := rkentry.GlobalAppCtx.GetEventLoggerEntry("event-logger").GetEventHelper()
	event := helper.Start("demo")
	event.AddPair("key", "value")
	helper.Finish(event)

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}
------------------------------------------------------------------------
endTime=2021-07-06T05:10:08.660629+08:00
startTime=2021-07-06T05:10:08.660625+08:00
elapsedNano=4034
timezone=CST
ids={"eventId":"545693bb-6c61-4486-957a-cdbea19649c7"}
app={"appName":"rk-demo","appVersion":"master-f414049","entryName":"","entryType":""}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={}
error={}
counters={}
pairs={"key":"value"}
timing={}
remoteAddr=localhost
operation=demo
resCode=OK
eventStatus=Ended
EOE

Cheers

4.Reference EventLoggerEntry

Bootstrapper will use this logger by default including interceptor.

---
eventLogger:
  - name: event-logger
gin:
  - name: greeter
    port: 8080
    enabled: true
    logger:
      eventLogger:
        ref: event-logger

Cheers

5.Set env at EventLoggerEntry

package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
	"github.com/rookie-ninja/rk-entry/entry"
)

// Application entrance.
func main() {
    // Set env
	os.Setenv("REALM", "my-realm")
	os.Setenv("REGION", "my-region")
	os.Setenv("AZ", "my-az")
	os.Setenv("DOMAIN", "my-domain")

	// Create a new boot instance.
	boot := rkboot.NewBoot()
    
    // Get custom EventLoggerEntry
	helper := rkentry.GlobalAppCtx.GetEventLoggerEntry("event-logger").GetEventHelper()
	event := helper.Start("demo")
	helper.Finish(event)

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}
------------------------------------------------------------------------
...
env={"arch":"amd64","az":"my-az","domain":"my-domain","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"my-realm","region":"my-region"}
...

Cheers

6.Access ZapLoggerEntry & EventLoggerEntry

User can access by calling rkentry.GlobalAppCtx.GetEventLoggerEntry() & rkentry.GlobalAppCtx.GetZapLoggerEntry().

2.3 - TLS/SSL

Enable TLS/SSL for the server.

Overview

In order to enable TLS/SSL on the server, we need server certificate and server key at least.

Bootstrapper will read CertEntry section in boot.yaml file and load certificates into memory.

We support four types of CertEntry retrievers. We will extend retriever future.

  • LocalFs
  • RemoteFs
  • Consul
  • Etcd

Architecture

Locale

CertEntry support concept of locale.

Generate Self-Signed Certificate

There is a convenient way to generate certificates with cfssl

1.Download cfssl & cfssljson

Install cfssl with rk cli

$ go get -u github.com/rookie-ninja/rk/cmd/rk
$ rk install cfssl
$ rk install cfssljson

2.Generate CA

$ cfssl print-defaults config > ca-config.json
$ cfssl print-defaults csr > ca-csr.json

Adjust ca-config.json and ca-csr.json as needed.

$ cfssl gencert -initca ca-csr.json | cfssljson -bare ca -

3.Generate server certificates

server.csr, server.pem and server-key.pem would be created.

$ cfssl gencert -config ca-config.json -ca ca.pem -ca-key ca-key.pem -profile www csr.json | cfssljson -bare server

Quick start

  • Install
$ go get github.com/rookie-ninja/rk-boot
$ go get github.com/rookie-ninja/rk-gin

1.localFs

Name Description Default
cert.localFs.name Name of localFs retriever ""
cert.localFs.locale Represent environment of current process follows schema of <realm>::<region>::<az>::<domain> ""
cert.localFs.serverCertPath Path of server cert in local file system. ""
cert.localFs.serverKeyPath Path of server key in local file system. ""
cert.localFs.clientCertPath Path of client cert in local file system. ""
cert.localFs.clientCertPath Path of client key in local file system. ""
---
cert:
  - name: "local-cert"                     # Required
    description: "Description of entry"    # Optional
    provider: "localFs"                    # Required, etcd, consul, localFs, remoteFs are supported options
    locale: "*::*::*::*"                   # Required, default: ""
    serverCertPath: "cert/server.pem"      # Optional, default: "", path of certificate on local FS
    serverKeyPath: "cert/server-key.pem"   # Optional, default: "", path of certificate on local FS
gin:
  - name: greeter
    port: 8080
    enabled: true
    cert:
      ref: "local-cert"
    commonService: 
      enabled: true
$ curl -X GET --insecure https://localhost:8080/rk/v1/healthy
{"healthy":true}

Cheers

2.remoteFs

Name Description Default
cert.remoteFs.name Name of remoteFileStore retriever ""
cert.remoteFs.locale Represent environment of current process follows schema of <realm>::<region>::<az>::<domain> ""
cert.remoteFs.endpoint Endpoint of remoteFileStore server, http://x.x.x.x or x.x.x.x both acceptable. N/A
cert.remoteFs.basicAuth Basic auth for remoteFileStore server, like user:pass. ""
cert.remoteFs.serverCertPath Path of server cert in remoteFs server. ""
cert.remoteFs.serverKeyPath Path of server key in remoteFs server. ""
cert.remoteFs.clientCertPath Path of client cert in remoteFs server. ""
cert.remoteFs.clientCertPath Path of client key in remoteFs server. ""
---
cert:
  - name: "remote-cert"                    # Required
    description: "Description of entry"    # Optional
    provider: "remoteFs"                   # Required, etcd, consul, localFs, remoteFs are supported options
    endpoint: "localhost:8081"             # Required, both http://x.x.x.x or x.x.x.x are acceptable
    locale: "*::*::*::*"                   # Required, default: ""
    serverCertPath: "cert/server.pem"      # Optional, default: "", path of certificate on local FS
    serverKeyPath: "cert/server-key.pem"   # Optional, default: "", path of certificate on local FS
gin:
  - name: greeter
    port: 8080
    enabled: true
    cert:
      ref: "remote-cert"
    commonService: 
      enabled: true
$ curl -X GET --insecure https://localhost:8080/rk/v1/healthy
{"healthy":true}

Cheers

3.consul

Name Description Default
cert.consul.name Name of consul retriever ""
cert.consul.locale Represent environment of current process follows schema of <realm>::<region>::<az>::<domain> ""
cert.consul.endpoint Endpoint of Consul server, http://x.x.x.x or x.x.x.x both acceptable. N/A
cert.consul.datacenter consul datacenter. ""
cert.consul.token Token for access consul. ""
cert.consul.basicAuth Basic auth for consul server, like user:pass. ""
cert.consul.serverCertPath Path of server cert in Consul server. ""
cert.consul.serverKeyPath Path of server key in Consul server. ""
cert.consul.clientCertPath Path of client cert in Consul server. ""
cert.consul.clientCertPath Path of client key in Consul server. ""
---
cert:
  - name: "consul-cert"                    # Required
    provider: "consul"                     # Required, etcd, consul, localFS, remoteFs are supported options
    description: "Description of entry"    # Optional
    locale: "*::*::*::*"                   # Required, default: ""
    endpoint: "localhost:8500"             # Required, http://x.x.x.x or x.x.x.x both acceptable.
    datacenter: "dc1"                      # Optional, default: "", consul datacenter
    serverCertPath: "server.pem"           # Optional, default: "", key of value in consul
    serverKeyPath: "server-key.pem"        # Optional, default: "", key of value in consul
gin:
  - name: greeter
    port: 8080
    enabled: true
    cert:
      ref: "consul-cert"
    commonService: 
      enabled: true
$ curl -X GET --insecure https://localhost:8080/rk/v1/healthy
{"healthy":true}

Cheers

4.etcd

Name Description Default
cert.etcd.name Name of etcd retriever ""
cert.etcd.locale Represent environment of current process follows schema of <realm>::<region>::<az>::<domain> ""
cert.etcd.endpoint Endpoint of etcd server, http://x.x.x.x or x.x.x.x both acceptable. N/A
cert.etcd.basicAuth Basic auth for etcd server, like user:pass. ""
cert.etcd.serverCertPath Path of server cert in etcd server. ""
cert.etcd.serverKeyPath Path of server key in etcd server. ""
cert.etcd.clientCertPath Path of client cert in etcd server. ""
cert.etcd.clientCertPath Path of client key in etcd server. ""
---
cert:
  - name: "etcd-cert"                      # Required
    description: "Description of entry"    # Optional
    provider: "etcd"                       # Required, etcd, consul, localFs, remoteFs are supported options
    locale: "*::*::*::*"                   # Required, default: ""
    endpoint: "localhost:2379"             # Required, http://x.x.x.x or x.x.x.x both acceptable.
    serverCertPath: "server.pem"           # Optional, default: "", key of value in etcd
    serverKeyPath: "server-key.pem"        # Optional, default: "", key of value in etcd
gin:
  - name: greeter
    port: 8080
    enabled: true
    cert:
      ref: "etcd-cert"
    commonService: 
      enabled: true
$ curl -X GET --insecure https://localhost:8080/rk/v1/healthy
{"healthy":true}

Cheers

5.Access CertEntry

User can access CertEntry by calling rkentry.GlobalAppCtx.GetCertEntry().

2.4 - Config

How to read configs in local file system.

Overview

Bootstrapper will try to read config files described in [config] section in boot.yaml file and load the file content with viper.

As a result, bellow file type can be supported.

  • JSON
  • TOML
  • YAML
  • HCL
  • envfile
  • Java propertie

Architecture

Locale

ConfigEntry support concept of locale.

Quick start

  • Install
$ go get github.com/rookie-ninja/rk-boot/gin

1.Create config

Create config file and specify in boot.yaml file

config/default.yaml

region: default

boot.yaml

config:
  - name: my-config
    locale: "*::*::*::*"
    path: config/default.yaml
gin:
  - name: greeter
    port: 8080
    enabled: true

2.Refer config path

main.go

package main

import (
	"context"
	"fmt"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
	"github.com/rookie-ninja/rk-entry/entry"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Get ConfigEntry and get viper instance
	fmt.Println(rkentry.GlobalAppCtx.GetConfigEntry("my-config").GetViper().GetString("region"))

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

3.Access ConfigEntry

User can access ConfigEntry by calling rkentry.GlobalAppCtx.GetConfigEntry().

2.5 - AppInfo

How to specify application info in boot.yaml?

Overview

Application info consist of bellow fields.

app:
  description: "this is description"  # Optional, default: ""
  keywords: ["rk", "golang"]          # Optional, default: []
  homeUrl: "http://example.com"       # Optional, default: ""
  iconUrl: "http://example.com"       # Optional, default: ""
  docsUrl: ["http://example.com"]     # Optional, default: []
  maintainers: ["rk-dev"]             # Optional, default: []

Quick start

  • Install
$ go get github.com/rookie-ninja/rk-boot/gin
app:
  description: "this is description"  # Optional, default: ""
  keywords: ["rk", "golang"]          # Optional, default: []
  homeUrl: "http://example.com"       # Optional, default: ""
  iconUrl: "http://example.com"       # Optional, default: ""
  docsUrl: ["http://example.com"]     # Optional, default: []
  maintainers: ["rk-dev"]             # Optional, default: []
gin:
  - name: greeter
    port: 8080
    enabled: true
    commonService:
      enabled: true                   # Enable for validation
    tv: 
      enabled: true                   # Enable for validation

1.Access from RK TV

http://localhost:8080/rk/v1/tv/info

Cheers

2.Access from common service

$ curl localhost:8080/rk/v1/info
{
    "appName":"rk-demo",
    "version":"master-f414049",
    "description":"this is description",
    "keywords":[
        "rk",
        "golang"
    ],
    "homeUrl":"http://example.com",
    "iconUrl":"http://example.com",
    "docsUrl":[
        "http://example.com"
    ],
    "maintainers":[
        "rk-dev"
    ],
    "uid":"501",
    "gid":"20",
    "username":"XXX",
    "startTime":"2021-07-08T00:38:51+08:00",
    "upTimeSec":13,
    "upTimeStr":"13 seconds",
    "region":"",
    "az":"",
    "realm":"",
    "domain":""
}

Cheers

3.Access from AppInfoEntry

rkentry.GlobalAppCtx.GetAppInfoEntry()

type AppInfoEntry struct {
	EntryName        string   `json:"entryName" yaml:"entryName"`
	EntryType        string   `json:"entryType" yaml:"entryType"`
	EntryDescription string   `json:"description" yaml:"description"`
	AppName          string   `json:"appName" yaml:"appName"`
	Version          string   `json:"version" yaml:"version"`
	Lang             string   `json:"lang" yaml:"lang"`
	Keywords         []string `json:"keywords" yaml:"keywords"`
	HomeUrl          string   `json:"homeUrl" yaml:"homeUrl"`
	IconUrl          string   `json:"iconUrl" yaml:"iconUrl"`
	DocsUrl          []string `json:"docsUrl" yaml:"docsUrl"`
	Maintainers      []string `json:"maintainers" yaml:"maintainers"`
	License          string   `json:"-" yaml:"-"`
	Readme           string   `json:"-" yaml:"-"`
	GoMod            string   `json:"-" yaml:"-"`
	UtHtml           string   `json:"-" yaml:"-"`
}

Cheers

2.6 - Multiple entries

How to start multiple Gin server with different port in one process?

Overview

With bootstrapper, user can start multiple GinEntry at the same time. Event for multiple different entries like GRPC.

Quick start

  • Install
$ go get github.com/rookie-ninja/rk-boot/gin
gin:
  - name: alice
    port: 8080
    enabled: true
    commonService:
      enabled: true
  - name: bob
    port: 8081
    enabled: true
    commonService:
      enabled: true

1.Access entries

package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	"github.com/rookie-ninja/rk-boot/gin"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()
    
    // Get alice
    rkbootgin.GetGinEntry("alice")
    // Get bob
    rkbootgin.GetGinEntry("bob")

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

Cheers

2.7 - Error type

What is the best way to return an RPC error?

Overview

It is always not easy to define a user-friendly API. However, errors could happen anywhere.

In order to return standard error type to user, it is important to define an error type.

By default, panic middleware/interceptor will be attached which will catch panic and return internal server error as bellow:

Error type used by bootstrapper was defined in rkerror.ErrorResp

func Greeter(ctx *gin.Context) {
	panic("Panic manually!")
}
{
    "error":{
        "code":500,
        "status":"Internal Server Error",
        "message":"Panic manually!",
        "details":[]
    }
}

Quick start

  • Install
$ go get github.com/rookie-ninja/rk-boot/gin

Return errors

Here is the way how to return errors in user RPC implementation.

func Greeter(ctx *gin.Context) {
	err := rkerror.New(
		rkerror.WithHttpCode(http.StatusAlreadyReported),
		rkerror.WithMessage("Trigger manually!"),
		rkerror.WithDetails("This is detail.", false, -1, 0.1))

	ctx.JSON(http.StatusAlreadyReported, err)
}
$ curl "localhost:8080/v1/greeter?name=rk-dev"
{
    "error":{
        "code":208,
        "status":"Already Reported",
        "message":"Trigger manually!",
        "details":[
            "This is detail.",
            false,
            -1,
            0.1
        ]
    }
}

Cheers

2.8 - Shutdown hook

How to add shutdown hook function while receiving shutdown signal?

Overview

We need to understand how bootstrapper stop process.

  1. Add shutdown hook functions by user
  2. Call functions as soon as receive signal from outside

Getting started

  • Install
$ go get github.com/rookie-ninja/rk-boot/gin
package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()
    
    // Add shutdown hook function
	boot.AddShutdownHookFunc("shutdown-hook", func() {
		fmt.Println("shutting down")
	})

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}
...
shutting down
...

Cheers

2.9 - Override bootstrapper

Is there any way to override boot.yaml or values in boot.yaml at start time?

Overview

Bootstrapper support two kinds of ways to override bootstrapper configs.

  • Override config file (by --rkboot)
  • Override values in config file (by --rkset)

Quick start

  • Install
$ go get github.com/rookie-ninja/rk-boot/gin

1.Override bootstrapper config file

In order to override bootstrapper file path, --rkboot needs to be passed.

boot.yaml

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    commonService:
      enabled: true

boot-override.yaml

---
gin:
  - name: greeter
    port: 8081
    enabled: true
    commonService:
      enabled: true

main.go

package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

Start server with --rkboot args

$ go build main.go
$ ./main --rkboot boot-override.yaml

Send request to 8080

$ curl localhost:8080/rk/v1/healthy
curl: (7) Failed to connect to localhost port 8080: Connection refused

Send request to 8081

$ curl localhost:8081/rk/v1/healthy
{"healthy":true}

Cheers

2.Override values in boot.yaml

In order to override bootstrapper file path, --rkset needs to be passed.

Use comma to separate multiple overrides.

Type Example
Map app.description=“This is description”
List gin[0].name=“alice”,gin[0].port=8081

boot.yaml

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    commonService:
      enabled: true

main.go

package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

Start server with --rkset args

$ go build main.go
$ ./main --rkset "gin[0].port=8081"

Send request to 8080

$ curl localhost:8080/rk/v1/healthy
curl: (7) Failed to connect to localhost port 8080: Connection refused

Send request to 8081

$ curl localhost:8081/rk/v1/healthy
{"healthy":true}

Cheers

2.10 - Trace RPC with logs

How to trace RPC with logs?

Overview

Sometimes, we don’t want to install any of tracing service like jaeger because of cost. In that case, we need a way to trace RPC with logs only.

Bootstrapper introduce a way to trace RPC with openTelemetry library but without tracing service.

Concept

RK bootstrapper will use traceId to track each RPC in distributed system which will be logged into RPC log while tracing and log interceptor enabled in bootstrapper.

EventId

EventId would be generated if logging interceptor/middleware was enabled bellow:

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    interceptors:
      loggingZap:
        enabled: true
------------------------------------------------------------------------
...
ids={"eventId":"cd617f0c-2d93-45e1-bef0-95c89972530d"}
...

RequestId

Generated by meta interceptor/middleware automatically if user enabled bellow:

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    interceptors:
      meta:
        enabled: true
      loggingZap:
        enabled: true
------------------------------------------------------------------------
...
ids={"eventId":"8226ba9b-424e-4e19-ba63-d37ca69028b3","requestId":"8226ba9b-424e-4e19-ba63-d37ca69028b3"}
...

If meta interceptor/middleware was enabled or requestId was enabled by user, then eventId will be override by requestId.

Simply, eventId will always the same as requestId

  rkginctx.SetHeaderToClient(ctx, rkginctx.RequestIdKey, "overridden-request-id")
------------------------------------------------------------------------
...
ids={"eventId":"overridden-request-id","requestId":"overridden-request-id"}
...

TraceId

Generated if user enable tracing interceptor/middleware by user as bellow:

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    interceptors:
      meta:
        enabled: true
      loggingZap:
        enabled: true
      tracingTelemetry:
        enabled: true
------------------------------------------------------------------------
...
ids={"eventId":"dd19cf9a-c7be-486c-b29d-7af777a78ebe","requestId":"dd19cf9a-c7be-486c-b29d-7af777a78ebe","traceId":"316a7b475ff500a76bfcd6147036951c"}
...

Quick start

  • Install
$ go get github.com/rookie-ninja/rk-boot
$ go get github.com/rookie-ninja/rk-gin

1.Create ServerA at 8080

bootA.yaml

---
gin:
  - name: greeter
    port: 8080
    enabled: true
    interceptors:
      meta:
        enabled: true
      loggingZap:
        enabled: true
      tracingTelemetry:
        enabled: true

serverA.go

package main

import (
	"context"
	"github.com/gin-gonic/gin"
	"github.com/rookie-ninja/rk-boot"
	"github.com/rookie-ninja/rk-boot/gin"
	"github.com/rookie-ninja/rk-gin/interceptor/context"
	"net/http"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot(rkboot.WithBootConfigPath("bootA.yaml"))

	// Register handler
	ginEntry := rkbootgin.GetGinEntry("greeter")
	ginEntry.Router.GET("/v1/hello", func(ctx *gin.Context) {
		client := http.DefaultClient

        // Construct request point to Server B
		req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost:8081/v1/hello", nil)

		// Inject parent trace info into request header
		rkginctx.InjectSpanToHttpRequest(ctx, req)

        // Send request to Server B
		client.Do(req)

		ctx.JSON(http.StatusOK, "Hello!")
	})

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

2.Create ServerB at 8081

bootB.yaml

---
gin:
  - name: greeter
    port: 8081
    enabled: true
    interceptors:
      meta:
        enabled: true
      loggingZap:
        enabled: true
      tracingTelemetry:
        enabled: true

serverB.go

package main

import (
	"context"
	"github.com/gin-gonic/gin"
	"github.com/rookie-ninja/rk-boot"
	"github.com/rookie-ninja/rk-boot/gin"
	"net/http"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot(rkboot.WithBootConfigPath("bootB.yaml"))

	// Register handler
	ginEntry := rkbootgin.GetGinEntry("greeter")
	ginEntry.Router.GET("/v1/hello", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, "Hello!")
	})

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

3.Start ServerA and ServerB

$ go run serverA.go
$ go run serverB.go

4.Send request to ServerA

$ curl localhost:8080/v1/hello

5.Validate logs

Two server will have same traceId in event log but different requestId and eventId.

What we need to do is grep same traceId.

ServerA

------------------------------------------------------------------------
...
ids={"eventId":"9b2e9278-0e19-4936-9afe-966ff7fce1c9","requestId":"9b2e9278-0e19-4936-9afe-966ff7fce1c9","traceId":"fc3801be6de88116a7c47fafca6a7328"}
...

ServerB

------------------------------------------------------------------------
...
ids={"eventId":"7b9585de-eaa3-4ad5-ae7a-e3be7fdb312e","requestId":"7b9585de-eaa3-4ad5-ae7a-e3be7fdb312e","traceId":"fc3801be6de88116a7c47fafca6a7328"}
...

2.11 - Static file handler with Web UI

How to list download static files via Web UI?

Overview

rk-boot provide an easy way to start a Web UI of downloading static files from server.

Currently, rk-boot support bellow source of static files located. User can implement own http.FileSystem to support other remote repository like S3.

Quick start

  • Install
$ go get github.com/rookie-ninja/rk-boot/gin
---
gin:
  - name: greeter                     # Required
    port: 8080                        # Required
    enabled: true                     # Required
    static:
      enabled: true                   # Optional, default: false
      path: "/rk/v1/static"           # Optional, default: /rk/v1/static
      sourceType: local               # Required, options: pkger, local
      sourcePath: "."                 # Required, full path of source directory

1.Access via static file handler path

http://localhost:8080/rk/v1/static

Cheers

Read from pkger (embeded static files)

pkger is a tool for embedding static files into Go binaries.

1.Download pkger

go get github.com/markbates/pkger/cmd/pkger

2.Add pkger.Include() in main.go

We will embed files in the current directory and move pkger.go file into internal/ folder.

// Copyright (c) 2021 rookie-ninja
//
// Use of this source code is governed by an Apache-style
// license that can be found in the LICENSE file.
package main

import (
	"context"
	"github.com/markbates/pkger"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gin"
	// Must be present in order to make pkger load embedded files into memory.
	_ "github.com/rookie-ninja/rk-demo/internal"
)

func init() {
	// This is used while running pkger CLI
	pkger.Include("./")
}

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

3.Generate pkger.go

pkger -o internal

4.Configure boot.yaml

pkger will use module name as package, so we need to specify sourcePath has the prefix of current module

---
gin:
  - name: greeter                                             # Required
    port: 8080                                                # Required
    enabled: true                                             # Required
    static:
      enabled: true                                           # Optional, default: false
      path: "/rk/v1/static"                                   # Optional, default: /rk/v1/static
      sourceType: pkger                                       # Required, options: pkger, local
      sourcePath: "github.com/rookie-ninja/rk-demo:/"         # Required, full path of source directory

5.Directory hierarchy

.
├── boot.yaml
├── go.mod
├── go.sum
├── internal
│   └── pkged.go
└── main.go

1 directory, 5 files

6.Validate

http://localhost:8080/rk/v1/static

Cheers

3 - Developer

How to implement a custom entry?

Overview

Every entries in builtin bootstrapper implement Entry interface in rk-entry package.

// Entry interface which must be implemented for bootstrapper to bootstrap
type Entry interface {
	// Bootstrap entry
	Bootstrap(context.Context)

	// Interrupt entry
	// Wait for shutdown signal and wait for draining incomplete procedure
	Interrupt(context.Context)

	// Get name of entry
	GetName() string

	// Get type of entry
	GetType() string

	// Get description of entry
	GetDescription() string

	// print entry as string
	String() string
}

Quick start

An entry could be any kinds of services or pieces of codes which needs to be bootstrap/initialized while application starts.

A third party entry could be implemented and inject to rk-boot via boot.yaml file.

How to create a new custom entry?

  1. Construct your own entry YAML struct as needed
  2. Create a struct which implements Entry interface
  3. Implements EntryRegFunc
  4. Register your reg function in init() in order to register your entry while application starts

How entry interact with rk-boot.Bootstrapper?

  1. Entry will be created and registered into rkentry.GlobalAppCtx
  2. Bootstrap will be called from Bootstrapper.Bootstrap() function
  3. Application will wait for shutdown signal
  4. Interrupt will be called from Bootstrapper.Interrupt() function

1.Implement Entry

type MyEntry struct {
	EntryName        string                    `json:"entryName" yaml:"entryName"`
	EntryType        string                    `json:"entryType" yaml:"entryType"`
	EntryDescription string                    `json:"entryDescription" yaml:"entryDescription"`
}

func (entry *MyEntry) Bootstrap(context.Context) {}

func (entry *MyEntry) Interrupt(context.Context) {}

func (entry *MyEntry) GetName() string {
	return entry.EntryName
}

func (entry *MyEntry) GetType() string {
	return entry.EntryType
}

func (entry *MyEntry) GetDescription() string {
	return entry.EntryDescription
}

func (entry *MyEntry) String() string {
	bytes, _ := json.Marshal(entry)
	return string(bytes)
}

2.Create bootstrapper config

This structure will match myEntry section in boot.yaml file

// A struct which is for unmarshalled YAML
type BootConfig struct {
	MyEntry struct {
		Enabled     bool   `yaml:"enabled" json:"enabled"`
		Name        string `yaml:"name" json:"name"`
		Description string `yaml:"description" json:"description"`
	} `yaml:"myEntry" json:"myEntry"`
}
myEntry:
  enabled: true
  name: "xxx"
  descriptin: "xxx"

3.Implements EntryRegFunc

Although we only need RegisterMyEntriesFromConfig() function, we strongly recommend implementing as bellow in order to run our entry from code either.

// An implementation of:
// type EntryRegFunc func(string) map[string]rkentry.Entry
func RegisterMyEntriesFromConfig(configFilePath string) map[string]rkentry.Entry {
	res := make(map[string]rkentry.Entry)

	// 1: decode config map into boot config struct
	config := &BootConfig{}
	rkcommon.UnmarshalBootConfig(configFilePath, config)

	// 3: construct entry
	if config.MyEntry.Enabled {
		entry := RegisterMyEntry(
			WithName(config.MyEntry.Name),
			WithDescription(config.MyEntry.Description),
		res[entry.GetName()] = entry
	}

	return res
}

func RegisterMyEntry(opts ...MyEntryOption) *MyEntry {
	entry := &MyEntry{
		EntryName:        "myEntry",
		EntryType:        "myEntry",
		EntryDescription: "Please contact maintainers to add description of this entry.",
	}

	for i := range opts {
		opts[i](entry)
	}

	if len(entry.EntryName) < 1 {
		entry.EntryName = "my-default"
	}

	if len(entry.EntryDescription) < 1 {
		entry.EntryDescription = "Please contact maintainers to add description of this entry."
	}

	rkentry.GlobalAppCtx.AddEntry(entry)

	return entry
}

type MyEntryOption func(*MyEntry)

func WithName(name string) MyEntryOption {
	return func(entry *MyEntry) {
		entry.EntryName = name
	}
}

func WithDescription(description string) MyEntryOption {
	return func(entry *MyEntry) {
		entry.EntryDescription = description
	}
}

4.Create init() function

Register your reg function in init() in order to register your entry while application starts

func init() {
	rkentry.RegisterEntryRegFunc(RegisterMyEntriesFromConfig)
}

5.Full example

boot.yaml

---
myEntry:
  enabled: true
  name: my-entry
  description: "This is my entry."

main.go

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/rookie-ninja/rk-boot"
	"github.com/rookie-ninja/rk-common/common"
	"github.com/rookie-ninja/rk-entry/entry"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Bootstrap
	boot.Bootstrap(context.Background())

	fmt.Println(rkentry.GlobalAppCtx.GetEntry("my-entry"))

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}


// Register entry, must be in init() function since we need to register entry at beginning
func init() {
	rkentry.RegisterEntryRegFunc(RegisterMyEntriesFromConfig)
}

// A struct which is for unmarshalled YAML
type BootConfig struct {
	MyEntry struct {
		Enabled     bool   `yaml:"enabled" json:"enabled"`
		Name        string `yaml:"name" json:"name"`
		Description string `yaml:"description" json:"description"`
	} `yaml:"myEntry" json:"myEntry"`
}

// An implementation of:
// type EntryRegFunc func(string) map[string]rkentry.Entry
func RegisterMyEntriesFromConfig(configFilePath string) map[string]rkentry.Entry {
	res := make(map[string]rkentry.Entry)

	// 1: decode config map into boot config struct
	config := &BootConfig{}
	rkcommon.UnmarshalBootConfig(configFilePath, config)

	// 3: construct entry
	if config.MyEntry.Enabled {
		entry := RegisterMyEntry(
			WithName(config.MyEntry.Name),
			WithDescription(config.MyEntry.Description))
		res[entry.GetName()] = entry
	}

	return res
}

func RegisterMyEntry(opts ...MyEntryOption) *MyEntry {
	entry := &MyEntry{
		EntryName:        "default",
		EntryType:        "myEntry",
		EntryDescription: "Please contact maintainers to add description of this entry.",
	}

	for i := range opts {
		opts[i](entry)
	}

	if len(entry.EntryName) < 1 {
		entry.EntryName = "my-default"
	}

	if len(entry.EntryDescription) < 1 {
		entry.EntryDescription = "Please contact maintainers to add description of this entry."
	}

	rkentry.GlobalAppCtx.AddEntry(entry)

	return entry
}

type MyEntryOption func(*MyEntry)

func WithName(name string) MyEntryOption {
	return func(entry *MyEntry) {
		entry.EntryName = name
	}
}

func WithDescription(description string) MyEntryOption {
	return func(entry *MyEntry) {
		entry.EntryDescription = description
	}
}

type MyEntry struct {
	EntryName        string                    `json:"entryName" yaml:"entryName"`
	EntryType        string                    `json:"entryType" yaml:"entryType"`
	EntryDescription string                    `json:"entryDescription" yaml:"entryDescription"`
}

func (entry *MyEntry) Bootstrap(context.Context) {}

func (entry *MyEntry) Interrupt(context.Context) {}

func (entry *MyEntry) GetName() string {
	return entry.EntryName
}

func (entry *MyEntry) GetDescription() string {
	return entry.EntryDescription
}

func (entry *MyEntry) GetType() string {
	return entry.EntryType
}

func (entry *MyEntry) String() string {
	bytes, _ := json.Marshal(entry)
	return string(bytes)
}

5.Validate

$ go run main.go
{"entryName":"my-entry","entryType":"myEntry","entryDescription":"This is my entry."}