Skip to main content

Manual instrumentation guide

You can manually send monitoring data to the WhaTap agent using the Go library. Example code is available at github.com/whatap/go-api-example.

Note

Automatic instrumentation vs Manual instrumentation

  • Automatic instrumentation (Recommended): Refer to Automatic instrumentation guide for automatic code insertion
  • Manual instrumentation: Add the APIs described in this document directly to your code

Getting started

Install the Go API with the following command:

go get github.com/whatap/go-api@latest

Init, Shutdown

Basic structure for initializing and shutting down the monitoring module:

import "github.com/whatap/go-api/trace"

func main(){
trace.Init(nil)
defer trace.Shutdown()
...
}

Init function configuration options:

  • Can be configured in map[string]string format
  • Can also be configured in whatap.conf file
  • Default TCP connection: 127.0.0.1:6600
m := make(map[string]string)
m["net_ipc_host"] = "127.0.0.1"
m["net_ipc_port"] = "6601"
trace.Init(m)

whatap.conf configuration:

accesskey={access key}
whatap.server.host={collection server IP address}
net_ipc_host=127.0.0.1
net_ipc_port=6600

Context management

The agent distinguishes transactions based on whatap context(trace.TraceCtx). Performance information outside transactions is ignored or only statistics are collected.

Creating transactions

var traceCtx *TraceCtx
traceCtx.Txid = keygen.Next()
ctx = context.WithValue(ctx, "whatap", traceCtx)

Transaction tracing

Web transaction tracing

http.HandleFunc("/index", func(w http.ResponseWriter, r *http.Request) {
ctx, _ := trace.StartWithRequest(r)
defer trace.End(ctx, nil)
})

Wrapping functions:

  • trace.Func(): Sets RequestURI as transaction name
  • trace.HandlerFunc(): Provides same functionality

General transaction tracing

func main() {
ctx := context.Background()
ctx, _ := trace.Start(ctx, "Custom Transaction")
...
trace.End(ctx, nil)
}

Transaction API

func Start(ctx context.Context, name string) (context.Context, error)
func End(ctx context.Context, err error) error
func StartWithRequest(r *http.Request) (context.Context, error)
func Step(ctx context.Context, title, message string, elapsed, value int) error
func HandlerFunc(handler func(http.ResponseWriter, *http.Request)) http.HandlerFunc
func Func(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request)

DB connection and SQL tracing

Limitations:

  • SQL statement: Maximum 32KB
  • Prepared parameters: Maximum 20, 256 bytes each

DB Connection tracing

import whatapsql "github.com/whatap/go-api/sql"

ctx, _ := trace.Start(context.Background(), "Trace Open DB")
defer trace.End(ctx, nil)

sqlCtx, _ := whatapsql.StartOpen(ctx, "id@tcp(x.x.x.x:3306)/test")
db, err := sql.Open("mysql", "id:pwd@tcp(x.x.x.x:3306)/test")
whatapsql.End(sqlCtx, err)
defer db.Close()

SQL Query tracing

query = "select id, subject from tbl_faq limit 10"
sqlCtx, _ = whatapsql.Start(ctx, "id:pwd@tcp(x.x.x.x:3306)/test", query)
rows, err := db.QueryContext(ctx, query)
whatapsql.End(sqlCtx, err)

Prepared Statement tracing

query = "select id, subject from tbl_faq where id = ? limit ?"
stmt, err := db.Prepare(query)
defer stmt.Close()

params := make([]interface{}, 0)
params = append(params, 8)
params = append(params, 1)

sqlCtx, _ := whatapsql.StartWithParamArray(ctx, "id:pwd@tcp(x.x.x.x:3306)/test", query, params)
rows, err := stmt.QueryContext(ctx, params...)
whatapsql.End(sqlCtx, err)

Using database/sql package

import (
_ "github.com/go-sql-driver/mysql"
"github.com/whatap/go-api/instrumentation/database/sql/whatapsql"
)

db, err := whatapsql.OpenContext(ctx, "mysql", dataSource)
defer db.Close()

if rows, err := db.QueryContext(ctx, query); err == nil {
...
}

SQL API

func Start(ctx context.Context, dbhost, sql string) (*SqlCtx, error)
func StartOpen(ctx context.Context, dbhost string) (*SqlCtx, error)
func End(sqlCtx *SqlCtx, err error) error
func StartWithParam(ctx context.Context, dbhost, sql, param ...interface{}) (*SqlCtx, error)
func StartWithParamArray(ctx context.Context, dbhost, sql string, param []interface{}) (*SqlCtx, error)
func Trace(ctx context.Context, dbhost, sql, param string, elapsed int, err error) error

HTTP request tracing

import "github.com/whatap/go-api/httc"

ctx, _ := trace.Start(context.Background(), "Trace Http Call")
defer trace.End(ctx, nil)

httpcCtx, _ := httpc.Start(ctx, callUrl)
resp, err := http.Get(callUrl)
if err == nil {
httpc.End(httpcCtx, resp.StatusCode, "", nil)
} else {
httpc.End(httpcCtx, 0, "", err)
}

HTTP Transport RoundTrip

import "github.com/whatap/go-api/instrumentation/net/http/whataphttp"

ctx, _ := trace.Start(context.Background(), "Http call")
defer trace.End(ctx, nil)

client := http.DefaultClient
client.Transport = whataphttp.NewRoundTrip(ctx, http.DefaultTransport)
resp, err := client.Get(callUrl)
defer resp.Body.Close()

HTTP API

func Start(ctx context.Context, url string) (*HttpcCtx, error)
func End(httpcCtx *HttpcCtx, status int, reason string, err error) error
func Trace(ctx context.Context, host string, port int, url string, elapsed int, status int, reason string, err error) error

Multi-transaction tracing (Distributed tracing)

Trace transactions associated with other agents or projects.

Header key values:

  • x-wtap-po
  • x-wtap-mst
  • x-wtap-sp1

Also supports OpenTrace's traceparent header.

Agent configuration

mtrace_enabled=true
mtrace_rate=100

Request Header processing

func UpdateMtrace(traceCtx *trace.TraceCtx, header http.Header)

trace.StartWithRequest internally calls this function.

ctx, traceCtx := trace.GetTraceContext(ctx)
if traceCtx != nil {
trace.UpdateMtrace(traceCtx, header)
}

Retrieving and adding Header information

func GetMTrace(ctx context.Context) http.Header

This function returns headers needed for distributed tracing:

headers := trace.GetMTrace(wCtx)
for key, _ := range headers {
req.Header.Set(key, headers.Get(key))
}

Automatic Header addition

Already included in WhaTap transport:

client := http.Client{Timeout: timeout}
client.Transport = whataphttp.NewRoundTrip(ctx, http.DefaultTransport)
resp, err := client.Get(callUrl)

Using multiple Transports:

client.Transport = NewAccessLogRoundTrip(whataphttp.NewRoundTrip(ctx, http.DefaultTransport))

Function tracing

Measure execution time of user functions or specific sections:

import "github.com/whatap/go-api/method"

ctx, _ := trace.Start(context.Background(), "Trace Method")
defer trace.End(ctx, nil)

getUser(ctx)

func getUser(ctx context.Context) {
methodCtx, _ := method.Start(ctx, "getUser")
defer method.End(methodCtx, nil)
time.Sleep(time.Duration(1) * time.Second)
}

Function tracing API

func Start(ctx context.Context, name string) (*MethodCtx, error)
func End(methodCtx *MethodCtx, err error) error
func Trace(ctx context.Context, name string, elapsed int, err error) error

Wrap functions (Go 1.18+ Generics)

Wrap functions allow you to write instrumentation code more concisely using closures. Utilizes Go 1.18+ Generics.

Trace Wrap functions (Generic)

Used for general-purpose tracing not belonging to a specific domain.

import "github.com/whatap/go-api/trace"

// Wrap - Generic function returning (T, error)
result, err := trace.Wrap(ctx, "ThirdParty.Calculate", func() (int, error) {
return thirdPartyLib.Calculate(input)
})

// WrapError - Returns only error
err := trace.WrapError(ctx, "FileProcessor.Process", func() error {
return processFile(path)
})

// WrapVoid - No return value
trace.WrapVoid(ctx, "Cleanup.Execute", func() {
cleanup()
})

SQL Wrap functions

Useful for instrumenting DB libraries that don't support hooks (Aerospike, some NoSQL, etc.).

import "github.com/whatap/go-api/sql"

// Wrap - Query returning (T, error)
record, err := sql.Wrap(ctx, "aerospike://host:3000", "GET ns/set", func() (*aero.Record, error) {
return client.Get(policy, key)
})

// WrapError - Query returning only error (INSERT, UPDATE, DELETE)
err := sql.WrapError(ctx, "aerospike://host:3000", "PUT ns/set", func() error {
return client.Put(policy, key, bins)
})

// WrapP - Including parameter tracing
rows, err := sql.WrapP(ctx, "mysql://host:3306", "SELECT * FROM users WHERE id = ?",
[]interface{}{userId},
func() (*sql.Rows, error) {
return db.Query("SELECT * FROM users WHERE id = ?", userId)
},
)

// WrapOpen - DB connection tracing
db, err := sql.WrapOpen(ctx, "mysql://host:3306", func() (*sql.DB, error) {
return sql.Open("mysql", dsn)
})

HTTP Wrap functions

Used for instrumenting external HTTP API calls.

import "github.com/whatap/go-api/httpc"

// Wrap - Returns (T, error)
resp, err := httpc.Wrap(ctx, "https://api.payment.com/charge", func() (*PaymentResp, error) {
return paymentClient.Charge(amount)
})

// WrapError - Returns only error
err := httpc.WrapError(ctx, "https://api.example.com/webhook", func() error {
return sendWebhook(payload)
})

// WrapWithStatus - Including HTTP status code
resp, status, err := httpc.WrapWithStatus(ctx, "https://api.example.com/data",
func() (*Response, int, error) {
resp, err := client.Get(url)
if resp != nil {
return resp, resp.StatusCode, err
}
return nil, 0, err
},
)

Method Wrap functions

Used for tracing custom methods/functions.

import "github.com/whatap/go-api/method"

// Wrap - Returns (T, error)
order, err := method.Wrap(ctx, "OrderService.ProcessOrder", func() (*Order, error) {
return s.processOrderInternal(orderID)
})

// WrapError - Returns only error
err := method.WrapError(ctx, "OrderService.ValidateOrder", func() error {
return s.validateOrder(order)
})

// WrapVoid - No return value
method.WrapVoid(ctx, "CacheService.Invalidate", func() {
s.cache.Delete(key)
})

Wrap function API summary

PackageFunctionDescription
traceWrap[T]Generic (T, error) return
traceWrapErrorGeneric error only return
traceWrapVoidGeneric no return value
sqlWrap[T]Query returning (T, error)
sqlWrapErrorQuery returning only error
sqlWrapP[T]Including parameter tracing
sqlWrapErrorPParameters + error only
sqlWrapOpen[T]DB connection tracing
httpcWrap[T]HTTP call
httpcWrapErrorReturns only error
httpcWrapWithStatus[T]Including status code
methodWrap[T]Method tracing
methodWrapErrorReturns only error
methodWrapVoidNo return value

Log collection

Agent configuration

logsink_enabled=true              # Enable log collection (default: false)
logsink_trace_enabled=true # Transaction linkage (default: true)
logsink_zip_enabled=true # Compressed transmission (default: true)

Using logsink.GetTraceLogWriter() allows you to link logs with transactions. @txid, @mtid, @gid fields are automatically added to logs.

func GetTraceLogWriter(w io.Writer) io.Writer
func GetTraceLogWriterWithCategory(w io.Writer, category string) io.Writer

Change notice: It is recommended to use GetTraceLogWriter(os.Stdout), GetTraceLogWriter(os.Stderr) instead of the existing GetWriterHookStdout(), GetWriterHookStderr().

Existing API (Compatibility maintained)

func GetWriterHookStdout() io.Writer  // → GetTraceLogWriter(os.Stdout) recommended
func GetWriterHookStderr() io.Writer // → GetTraceLogWriter(os.Stderr) recommended

log package example

import (
"log"
"os"
"github.com/whatap/go-api/logsink"
"github.com/whatap/go-api/trace"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

// Recommended: Use TraceLogWriter
log.SetOutput(logsink.GetTraceLogWriter(os.Stderr))

// Existing method (compatibility maintained)
// log.SetOutput(logsink.GetWriterHookStderr())

log.Println("Application started")
}

go.uber.org/zap example

import (
"os"
"github.com/whatap/go-api/logsink"
"github.com/whatap/go-api/trace"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

// Wrap with TraceLogWriter
writer := logsink.GetTraceLogWriter(os.Stdout)

consoleCore := zapcore.NewCore(
zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()),
zapcore.AddSync(writer),
zap.InfoLevel,
)
logger := zap.New(consoleCore)
logger.Info("logger started")
}

sirupsen/logrus example

import (
"os"
log "github.com/sirupsen/logrus"
"github.com/whatap/go-api/logsink"
"github.com/whatap/go-api/trace"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

// Wrap with TraceLogWriter
log.SetOutput(logsink.GetTraceLogWriter(os.Stderr))
log.Info("Log message")
}

Web framework instrumentation

Using the instrumentation package enables automatic tracing for major web frameworks.

Gin

import (
"github.com/gin-gonic/gin"
"github.com/whatap/go-api/instrumentation/github.com/gin-gonic/gin/whatapgin"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

r := gin.Default()
r.Use(whatapgin.Middleware())

r.GET("/hello", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "hello"})
})
r.Run(":8080")
}

Echo v4

import (
"github.com/labstack/echo/v4"
"github.com/whatap/go-api/instrumentation/github.com/labstack/echo/v4/whatapecho"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

e := echo.New()
e.Use(whatapecho.Middleware())

e.GET("/hello", func(c echo.Context) error {
return c.JSON(200, map[string]string{"message": "hello"})
})
e.Start(":8080")
}

Fiber v2

import (
"github.com/gofiber/fiber/v2"
"github.com/whatap/go-api/instrumentation/github.com/gofiber/fiber/v2/whatapfiber"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

app := fiber.New()
app.Use(whatapfiber.Middleware())

app.Get("/hello", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "hello"})
})
app.Listen(":8080")
}

Chi v5

import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/whatap/go-api/instrumentation/github.com/go-chi/chi/whatapchi"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

r := chi.NewRouter()
r.Use(whatapchi.Middleware)

r.Get("/hello", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
})
http.ListenAndServe(":8080", r)
}

Gorilla Mux

import (
"net/http"
"github.com/gorilla/mux"
"github.com/whatap/go-api/instrumentation/github.com/gorilla/mux/whatapmux"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

r := mux.NewRouter()
r.Use(whatapmux.Middleware)

r.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
})
http.ListenAndServe(":8080", r)
}

FastHTTP

import (
"github.com/valyala/fasthttp"
"github.com/whatap/go-api/instrumentation/github.com/valyala/fasthttp/whatapfasthttp"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

handler := func(ctx *fasthttp.RequestCtx) {
ctx.WriteString("hello")
}

fasthttp.ListenAndServe(":8080", whatapfasthttp.Middleware(handler))
}

net/http handler wrapping

import (
"net/http"
"github.com/whatap/go-api/instrumentation/net/http/whataphttp"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

// HandleFunc wrapping
http.HandleFunc("/api", whataphttp.Func(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
}))

// Handler wrapping
http.Handle("/handler", whataphttp.Handler(myHandler))

http.ListenAndServe(":8080", nil)
}

database/sql instrumentation (whatapsql)

Wraps the database/sql package to automatically track SQL queries.

import (
_ "github.com/go-sql-driver/mysql"
"github.com/whatap/go-api/instrumentation/database/sql/whatapsql"
"github.com/whatap/go-api/trace"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

// Use whatapsql.Open instead of sql.Open
db, err := whatapsql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
if err != nil {
panic(err)
}
defer db.Close()

// db.Query, db.Exec, etc. are automatically tracked
rows, err := db.QueryContext(ctx, "SELECT * FROM users WHERE id = ?", 1)
}

OpenContext (Transaction context linkage)

// Link with transaction context
db, err := whatapsql.OpenContext(ctx, "mysql", dsn)

sqlx instrumentation

import (
"github.com/whatap/go-api/instrumentation/github.com/jmoiron/sqlx/whatapsqlx"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

// Use whatapsqlx.Open instead of sqlx.Open
db, err := whatapsqlx.Open("mysql", dsn)

// Use whatapsqlx.Connect instead of sqlx.Connect
db, err := whatapsqlx.Connect("postgres", dsn)
}

GORM instrumentation

gorm.io/gorm (GORM v2)

import (
"gorm.io/driver/mysql"
"github.com/whatap/go-api/instrumentation/github.com/go-gorm/gorm/whatapgorm"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

// Use whatapgorm.Open instead of gorm.Open
db, err := whatapgorm.Open(mysql.Open(dsn), &gorm.Config{})
}

github.com/jinzhu/gorm (GORM v1)

import (
"github.com/whatap/go-api/instrumentation/github.com/jinzhu/gorm/whatapgorm"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

db, err := whatapgorm.Open("mysql", dsn)
}

Redis instrumentation

go-redis v9

import (
"github.com/whatap/go-api/instrumentation/github.com/redis/go-redis/v9/whatapgoredis"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

// Use whatapgoredis.NewClient instead of redis.NewClient
rdb := whatapgoredis.NewClient(&redis.Options{
Addr: "localhost:6379",
})

// redis.NewClusterClient
cluster := whatapgoredis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{"localhost:7000", "localhost:7001"},
})
}

go-redis v8

import (
"github.com/whatap/go-api/instrumentation/github.com/go-redis/redis/v8/whatapgoredis"
)

// Same usage as v9
rdb := whatapgoredis.NewClient(&redis.Options{
Addr: "localhost:6379",
})

Redigo

import (
"github.com/whatap/go-api/instrumentation/github.com/gomodule/redigo/whatapredigo"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

// Use whatapredigo.Dial instead of redis.Dial
conn, err := whatapredigo.Dial("tcp", "localhost:6379")
defer conn.Close()

// Use regular redis commands
conn.Do("SET", "key", "value")
}

MongoDB instrumentation

import (
"go.mongodb.org/mongo-driver/mongo"
"github.com/whatap/go-api/instrumentation/go.mongodb.org/mongo-driver/mongo/whatapmongo"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

// Use whatapmongo.Connect instead of mongo.Connect
client, err := whatapmongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
defer client.Disconnect(ctx)

collection := client.Database("test").Collection("users")
collection.FindOne(ctx, bson.M{"name": "john"})
}

gRPC instrumentation

Server side

import (
"google.golang.org/grpc"
"github.com/whatap/go-api/instrumentation/google.golang.org/grpc/whatapgrpc"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

// Add Interceptor
server := grpc.NewServer(
grpc.UnaryInterceptor(whatapgrpc.UnaryServerInterceptor()),
grpc.StreamInterceptor(whatapgrpc.StreamServerInterceptor()),
)

// Register service and start server
pb.RegisterMyServiceServer(server, &myService{})
server.Serve(lis)
}

Client side

func main() {
trace.Init(nil)
defer trace.Shutdown()

conn, err := grpc.Dial("localhost:50051",
grpc.WithInsecure(),
grpc.WithUnaryInterceptor(whatapgrpc.UnaryClientInterceptor()),
grpc.WithStreamInterceptor(whatapgrpc.StreamClientInterceptor()),
)
defer conn.Close()

client := pb.NewMyServiceClient(conn)
resp, err := client.MyMethod(ctx, &pb.Request{})
}

Kafka instrumentation (Sarama)

import (
"github.com/IBM/sarama"
"github.com/whatap/go-api/instrumentation/github.com/IBM/sarama/whatapsarama"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

config := sarama.NewConfig()

// Producer wrapping
producer, err := sarama.NewSyncProducer(brokers, config)
wrappedProducer := whatapsarama.WrapSyncProducer(producer)

// Consumer wrapping
consumer, err := sarama.NewConsumer(brokers, config)
wrappedConsumer := whatapsarama.WrapConsumer(consumer)
}

Kubernetes client-go instrumentation

import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"github.com/whatap/go-api/instrumentation/k8s.io/client-go/kubernetes/whatapkubernetes"
)

func main() {
trace.Init(nil)
defer trace.Shutdown()

config, err := rest.InClusterConfig()
if err != nil {
panic(err)
}

// config wrapping
config = whatapkubernetes.WrapConfig(config)

clientset, err := kubernetes.NewForConfig(config)
// Kubernetes API calls are automatically tracked
}

Instrumentation package summary

CategoryPackageImport path
Web frameworksGingithub.com/whatap/go-api/instrumentation/github.com/gin-gonic/gin/whatapgin
Echo v4github.com/whatap/go-api/instrumentation/github.com/labstack/echo/v4/whatapecho
Fiber v2github.com/whatap/go-api/instrumentation/github.com/gofiber/fiber/v2/whatapfiber
Chigithub.com/whatap/go-api/instrumentation/github.com/go-chi/chi/whatapchi
Gorilla Muxgithub.com/whatap/go-api/instrumentation/github.com/gorilla/mux/whatapmux
FastHTTPgithub.com/whatap/go-api/instrumentation/github.com/valyala/fasthttp/whatapfasthttp
net/httpgithub.com/whatap/go-api/instrumentation/net/http/whataphttp
Databasesdatabase/sqlgithub.com/whatap/go-api/instrumentation/database/sql/whatapsql
sqlxgithub.com/whatap/go-api/instrumentation/github.com/jmoiron/sqlx/whatapsqlx
GORM v2github.com/whatap/go-api/instrumentation/github.com/go-gorm/gorm/whatapgorm
GORM v1github.com/whatap/go-api/instrumentation/github.com/jinzhu/gorm/whatapgorm
Redisgo-redis v9github.com/whatap/go-api/instrumentation/github.com/redis/go-redis/v9/whatapgoredis
go-redis v8github.com/whatap/go-api/instrumentation/github.com/go-redis/redis/v8/whatapgoredis
Redigogithub.com/whatap/go-api/instrumentation/github.com/gomodule/redigo/whatapredigo
NoSQLMongoDBgithub.com/whatap/go-api/instrumentation/go.mongodb.org/mongo-driver/mongo/whatapmongo
RPCgRPCgithub.com/whatap/go-api/instrumentation/google.golang.org/grpc/whatapgrpc
Message queueSarama (IBM)github.com/whatap/go-api/instrumentation/github.com/IBM/sarama/whatapsarama
Sarama (Shopify)github.com/whatap/go-api/instrumentation/github.com/Shopify/sarama/whatapsarama
CloudKubernetesgithub.com/whatap/go-api/instrumentation/k8s.io/client-go/kubernetes/whatapkubernetes