This the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

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 GRPC 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
Custom routes in grpc-gateway Add custom routes to grpc-gateway
File uploads Support API uploading files to server.

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
$ go get github.com/rookie-ninja/rk-grpc

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
grpc:
  - 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-grpc/boot"
	"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

Cheers

4.With REGION=frankfurt

package main

import (
	"context"
	"fmt"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-grpc/boot"
	"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-grpc/boot"
	"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-grpc/boot"
)

// 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 - 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-07-10T03:00:12.153392+08:00
startTime=2021-07-10T03:00:12.153261+08:00
elapsedNano=130727
timezone=CST
ids={"eventId":"c9a1f6b0-b9ec-4e46-9ed4-238c3c6759ab","requestId":"c9a1f6b0-b9ec-4e46-9ed4-238c3c6759ab","traceId":"5441ff5c3855f03b573e95d81139123b"}
app={"appName":"rk-demo","appVersion":"master-f414049","entryName":"greeter","entryType":"GrpcEntry"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.2","os":"darwin","realm":"*","region":"*"}
payloads={"grpcMethod":"Greeter","grpcService":"api.v1.Greeter","grpcType":"unaryServer","gwMethod":"GET","gwPath":"/v1/greeter","gwScheme":"http","gwUserAgent":"curl/7.64.1"}
error={}
counters={}
pairs={}
timing={}
remoteAddr=localhost:59631
operation=/api.v1.Greeter/Greeter
resCode=OK
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-grpc

1.Custom ZapLoggerEntry

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

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

// 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
grpc:
  - name: greeter
    port: 8080
    enabled: true
    logger:
     zapLogger:
        ref: zap-logger

Cheers

3.Custom EventLoggerEntry

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

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

// 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-10T03:02:58.540804+08:00
startTime=2021-07-10T03:02:58.540803+08:00
elapsedNano=568
timezone=CST
ids={"eventId":"55f9dd94-9bb8-42a0-b3ed-9164d5f00a64"}
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
grpc:
  - 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-entry/entry"
	_ "github.com/rookie-ninja/rk-grpc/boot"
)

// 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().

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-grpc

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. ""

Enable TLS on grpc and gateway

---
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
grpc:
  - name: greeter
    port: 8080
    enabled: true
    enableReflection: true
    cert:
      ref: "local-cert"                    # Enable grpc TLS
    commonService: 
      enabled: true
# try call http service
$ curl -X GET --insecure https://localhost:8080/rk/v1/healthy
{"healthy":true}
# try call grpc service
grpcurl -insecure localhost:8080 rk.api.v1.RkCommonService.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
grpc:
  - name: greeter
    port: 8080
    enabled: true
    cert:
      ref: "remote-cert"                   # Enable grpc TLS
    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
grpc:
  - name: greeter
    port: 8080
    enabled: true
    cert:
      ref: "consul-cert"                   # Enable grpc TLS
    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
grpc:
  - name: greeter
    port: 8080
    enabled: true
    cert:
      ref: "etcd-cert"                   # Enable grpc TLS
    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().

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
$ go get github.com/rookie-ninja/rk-grpc

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
grpc:
  - 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-entry/entry"
	_ "github.com/rookie-ninja/rk-grpc/boot"
)

// 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().

Cheers

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
$ go get github.com/rookie-ninja/rk-grpc
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: []
grpc:
  - 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

6 - Multiple entries

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

Overview

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

Quick start

  • Install
$ go get github.com/rookie-ninja/rk-boot
$ go get github.com/rookie-ninja/rk-grpc
grpc:
  - name: alice
    port: 1949
    enabled: true
    commonService:
      enabled: true
  - name: bob
    port: 2008
    enabled: true
    commonService:
      enabled: true

1.Access entries

package main

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

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()
    
    // Get alice
	boot.GetEntry("alice").(*rkgrpc.GrpcEntry)
    // Get bob
    boot.GetEntry("bob").(*rkgrpc.GrpcEntry)

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

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

Cheers

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 (server *GreeterServer) Greeter(ctx context.Context, request *greeter.GreeterRequest) (*greeter.GreeterResponse, error) {
	panic("Panic manually!")

	return &greeter.GreeterResponse{
		Message: fmt.Sprintf("Hello %s!", request.Name),
	}, nil
}
{
    "error":{
        "code":500,
        "status":"Internal Server Error",
        "message":"Panic manually!",
        "details":[]
    }
}

Quick start

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

Return errors

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

func (server *GreeterServer) Greeter(ctx context.Context, request *greeter.GreeterRequest) (*greeter.GreeterResponse, error) {
	return nil, rkerror.Unimplemented("Trigger manually!", errors.New("this is detail")).Err()
}
$ curl "localhost:8080/v1/greeter?name=rk-dev"
{
    "error":{
        "code":501,
        "status":"Not Implemented",
        "message":"Trigger manually!",
        "details":[
            {
                "code":12,
                "status":"Unimplemented",
                "message":"[from-grpc] Trigger manually!"
            },
            {
                "code":2,
                "status":"Unknown",
                "message":"this is detail"
            }
        ]
    }
}

Cheers

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
$ go get github.com/rookie-ninja/rk-grpc
package main

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

// 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

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
$ go get github.com/rookie-ninja/rk-grpc

1.Override bootstrapper config file

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

boot.yaml

---
grpc:
  - name: greeter
    port: 1949
    enabled: true
    enableReflection: true
    commonService:
      enabled: true

boot-override.yaml

---
grpc:
  - name: greeter
    port: 2008
    enabled: true
    enableReflection: true
    commonService:
      enabled: true

main.go

package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-grpc/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())
}

Start server with --rkboot args

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

Send request to 1949

$ grpcurl -plaintext localhost:1949 rk.api.v1.RkCommonService.Healthy
Failed to dial target host "localhost:1949": dial tcp [::1]:1949: connect: connection refused

Send request to 2008

$ grpcurl -plaintext localhost:2008 rk.api.v1.RkCommonService.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 grpc[0].name=“alice”,grpc[0].port=8081

boot.yaml

---
grpc:
  - name: greeter
    port: 1949
    enabled: true
    enableReflection: true
    commonService:
      enabled: true

main.go

package main

import (
	"context"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-grpc/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())
}

Start server with --rkset args

$ go build main.go
$ ./main --rkset "grpc[0].port=2008"

Send request to 1949

$ grpcurl -plaintext localhost:1949 rk.api.v1.RkCommonService.Healthy
Failed to dial target host "localhost:1949": dial tcp [::1]:1949: connect: connection refused

Send request to 2008

$ grpcurl -plaintext localhost:2008 rk.api.v1.RkCommonService.Healthy
{
    "healthy": true
}

Cheers

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:

---
grpc:
  - name: greeter                   # Name of grpc entry
    port: 8080                      # Port of grpc entry
    enabled: true                   # Enable grpc entry
    enableReflection: true
    commonService:
      enabled: true                 # Enable common service for testing
    interceptors:
      loggingZap:
        enabled: true
------------------------------------------------------------------------
...
ids={"eventId":"cd617f0c-2d93-45e1-bef0-95c89972530d"}
...

RequestId

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

---
grpc:
  - name: greeter                   # Name of grpc entry
    port: 8080                      # Port of grpc entry
    enabled: true                   # Enable grpc entry
    enableReflection: true
    commonService:
      enabled: true                 # Enable common service for testing
    interceptors:
      loggingZap:
        enabled: true
      meta:
        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

  rkgrpcctx.AddHeaderToClient(ctx, rkgrpcctx.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:

---
grpc:
  - name: greeter                   # Name of grpc entry
    port: 8080                      # Port of grpc entry
    enabled: true                   # Enable grpc entry
    enableReflection: true
    commonService:
      enabled: true                 # Enable common service for testing
    interceptors:
      loggingZap:
        enabled: true
      meta:
        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-grpc

1.Create ServerA at 1949

bootA.yaml

---
grpc:
  - name: greeter                   # Name of grpc entry
    port: 1949                      # Port of grpc entry
    enabled: true                   # Enable grpc entry
    enableReflection: true
    interceptors:
      loggingZap:
        enabled: true
      meta:
        enabled: true
      tracingTelemetry:
        enabled: true

serverA.go

package main

import (
	"context"
	"fmt"
	"github.com/rookie-ninja/rk-boot"
	"github.com/rookie-ninja/rk-demo/api/gen/v1"
	"github.com/rookie-ninja/rk-grpc/boot"
	"github.com/rookie-ninja/rk-grpc/interceptor/context"
	"google.golang.org/grpc"
)

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

	// Get grpc entry with name
	grpcEntry := boot.GetEntry("greeter").(*rkgrpc.GrpcEntry)
	grpcEntry.AddRegFuncGrpc(registerGreeter)
	grpcEntry.AddRegFuncGw(greeter.RegisterGreeterHandlerFromEndpoint)

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

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

func registerGreeter(server *grpc.Server) {
	greeter.RegisterGreeterServer(server, &GreeterServer{})
}

type GreeterServer struct{}

func (server *GreeterServer) Greeter(ctx context.Context, request *greeter.GreeterRequest) (*greeter.GreeterResponse, error) {
    // Call serverB at 2008 with grpc client
	opts := []grpc.DialOption {
		grpc.WithBlock(),
		grpc.WithInsecure(),
	}
	conn, _ := grpc.Dial("localhost:2008", opts...)
	defer conn.Close()
	client := greeter.NewGreeterClient(conn)
	
    // Inject current trace information into context
	newCtx := rkgrpcctx.InjectSpanToNewContext(ctx)
	client.Greeter(newCtx, &greeter.GreeterRequest{Name: "A"})

	return &greeter.GreeterResponse{
		Message: fmt.Sprintf("Hello %s!", request.Name),
	}, nil
}

2.Create ServerB at 2008

bootB.yaml

---
grpc:
  - name: greeter                   # Name of grpc entry
    port: 2008                      # Port of grpc entry
    enabled: true                   # Enable grpc entry
    interceptors:
      loggingZap:
        enabled: true
      meta:
        enabled: true
      tracingTelemetry:
        enabled: true

serverB.go

package main

import (
	"context"
	"fmt"
	"github.com/rookie-ninja/rk-boot"
	"github.com/rookie-ninja/rk-demo/api/gen/v1"
	"github.com/rookie-ninja/rk-grpc/boot"
	"github.com/rookie-ninja/rk-grpc/interceptor/context"
	"google.golang.org/grpc"
)

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

	// Get grpc entry with name
	grpcEntry := boot.GetEntry("greeter").(*rkgrpc.GrpcEntry)
	grpcEntry.AddRegFuncGrpc(registerGreeterB)
	grpcEntry.AddRegFuncGw(greeter.RegisterGreeterHandlerFromEndpoint)

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

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

func registerGreeterB(server *grpc.Server) {
	greeter.RegisterGreeterServer(server, &GreeterServerB{})
}

type GreeterServerB struct{}

func (server *GreeterServerB) Greeter(ctx context.Context, request *greeter.GreeterRequest) (*greeter.GreeterResponse, error) {
	return &greeter.GreeterResponse{
		Message: fmt.Sprintf("Hello %s!", request.Name),
	}, nil
}

3.Start ServerA and ServerB

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

4.Send request to ServerA

# Call grpc, both grpc and http will have same effect
$ grpcurl -plaintext localhost:1949 api.v1.Greeter.Greeter
# Call http, both grpc and http will have same effect
$ curl "localhost:1949/v1/greeter?name=rk-dev"

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":"05928652-642c-4c4a-829e-b27b81c979c7","requestId":"05928652-642c-4c4a-829e-b27b81c979c7","traceId":"3614ffe216458f445a611a20e41be948"}
...

ServerB

------------------------------------------------------------------------
...
ids={"eventId":"9b12380e-293d-4501-bc55-80a5b1295748","requestId":"9b12380e-293d-4501-bc55-80a5b1295748","traceId":"3614ffe216458f445a611a20e41be948"}
...

Cheers

11 - Custom routes

How to add custom routes in grpc-gateway without grpc?

Overview

grpc-gateway already support custom routing.

Quick start

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

1.Create boot.yaml

---
grpc:
  - name: greeter                   # Name of grpc entry
    port: 8080                      # Port of grpc entry
    enabled: true                   # Enable grpc entry

2.Create main.go

package main

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

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

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

	// Get grpc entry with name
	grpcEntry := boot.GetEntry("greeter").(*rkgrpc.GrpcEntry)
    
    // !!!!!!
    // This codes should be located after Bootstrap()
	grpcEntry.GwMux.HandlePath("GET", "/custom", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
		w.Write([]byte("Custom routes!"))
	})

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

3.Validate

$ curl "localhost:8080/custom"
Custom routes!

Cheers

12 - 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
$ go get github.com/rookie-ninja/rk-grpc
---
grpc:
  - 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"
	// Must be present in order to make pkger load embedded files into memory.
	_ "github.com/rookie-ninja/rk-demo/internal"
	_ "github.com/rookie-ninja/rk-grpc/boot"
)

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

---
grpc:
  - 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

13 - File uploads

How to upload file with grpc-gateway?

Overview

grpc-gateway already support file uploads.

Quick start

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

1.Create boot.yaml

---
grpc:
  - name: greeter                   # Name of grpc entry
    port: 8080                      # Port of grpc entry
    enabled: true                   # Enable grpc entry

2.Create main.go

package main

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

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

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

	// Get grpc entry with name
	grpcEntry := boot.GetEntry("greeter").(*rkgrpc.GrpcEntry)

	// Attachment upload from http/s handled manually
	grpcEntry.GwMux.HandlePath("POST", "/v1/files", handleBinaryFileUpload)

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

func handleBinaryFileUpload(w http.ResponseWriter, req *http.Request, params map[string]string) {
	err := req.ParseForm()
	if err != nil {
		http.Error(w, fmt.Sprintf("failed to parse form: %s", err.Error()), http.StatusBadRequest)
		return
	}

	f, header, err := req.FormFile("attachment")
	if err != nil {
		http.Error(w, fmt.Sprintf("failed to get file 'attachment': %s", err.Error()), http.StatusBadRequest)
		return
	}
	defer f.Close()

	fmt.Println(header)

	//
	// Now do something with the io.Reader in `f`, i.e. read it into a buffer or stream it to a gRPC client side stream.
	// Also `header` will contain the filename, size etc of the original file.
	//
}

3.Validate

$ curl -X POST -F "attachment=@xxx.txt" localhost:8080/v1/files

Cheers