github.com/arthur-befumo/witchcraft-go-server@v1.12.0/README.md (about) 1 <p align="right"> 2 <a href="https://autorelease.general.dmz.palantir.tech/palantir/witchcraft-go-server"><img src="https://img.shields.io/badge/Perform%20an-Autorelease-success.svg" alt="Autorelease"></a> 3 </p> 4 5 witchcraft-go-server 6 ==================== 7 [![](https://godoc.org/github.com/palantir/witchcraft-go-server?status.svg)](http://godoc.org/github.com/palantir/witchcraft-go-server) 8 9 `witchcraft-go-server` is a Go implementation of a Witchcraft server. It provides a way to quickly and easily create 10 servers that work in the Witchcraft ecosystem. 11 12 Implementation 13 -------------- 14 15 ### Configuration 16 A witchcraft server is provided with install configuration and runtime configuration. Install configuration specifies 17 configuration values that are static -- they are read in once at server startup and are known to never change. Runtime 18 configuration values are considered *refreshable*. When file-based configuration is used, whenever the runtime 19 configuration file is updated its contents are loaded and the corresponding values are refreshed. See the section on 20 refreshable configuration for more information on this. 21 22 The default configuration uses file-based configuration with the install configuration at `var/conf/install.yml` and 23 runtime configuration at `var/conf/runtime.yml`. It is possible to use code to specify different sources of 24 configuration (for example, in-memory providers). 25 26 `witchcraft-server` also supports using `encrypted-config-value` to automatically decrypt encrypted configuration 27 values. The default configuration expects a key file to be at `var/conf/encrypted-config-value.key`. It is possible to 28 use code to specify a different source for the key (or to specify that no key should be used). If the configuration 29 does not contain encrypted values, any specified ECV key will not be read. If the install configuration contains 30 encrypted values but the encryption key is missing or malformed, the server will fail to start. If the runtime config 31 contains encrypted values but fails to decrypt them, a warning will be logged and the encrypted values passed to the 32 server. 33 34 `witchcraft-server` defines base configuration for its install and runtime configuration. Servers that want to provide 35 their own install and/or runtime configuration should embed the base configuration structs within the definition of 36 their configuration structs. 37 38 ### Route registration 39 A witchcraft server is backed by a `wrouter.Router` and allows authors to register route handlers on the server. The 40 router uses a specific format for path templates to specify path parameters and has rules around the kinds of paths that 41 can be matched. witchcraft is opinionated about the path formats and does not support registering paths that cannot be 42 expressed using its template rules. All witchcraft routes are configured to emit request logs and trace logs and update 43 metrics for the requests using built-in middleware. The `context.Context` for the `http.Request` provided to the 44 handlers is configured with all of the standard loggers (service logger, event logger, trace logger, etc.). 45 46 When registering routes on the router, it is also possible to specify path/header/query param keys that should be 47 considered "safe" or "forbidden" when used as parameters in logging. These are combined with the default set of safe and 48 forbidden header parameters defined by the `req2log` package in `witchcraft-go-logging`. 49 50 ### Liveness, readiness, and health 51 `witchcraft-server` registers the endpoints `/status/liveness`, `/status/readiness` and `/status/health` to report the 52 server's liveness, readiness and health. By default, these endpoints use a built-in provider that reports liveness, 53 readiness and health based on the state of the server. It is possible to configure the liveness and readiness providers 54 in code, and health status providers can also be added via code (health supports specifying multiple sources to report 55 health, and the server's built-in health status provider will always be one of them). 56 57 The default behavior serves both the user-registered endpoints and the status endpoints from the same server. However, 58 if a "management port" is specified in the server's install configuration and its value differs from the "port" value in 59 configuration, then `witchcraft-server` starts a second management server on the specified port and serves the status 60 endpoints on that port. This can be useful in scenarios where all of the traffic to the main endpoints require client 61 certificates for TLS but the status endpoints need to be served without requiring client TLS certificates. 62 63 ### Debug Routes 64 The following routes are registered on the management server (if enabled, otherwise the main server) to aid in debugging 65 and telemetry collection: 66 * `/debug/pprof`: Provides an HTML index of the other endpoints at this route. 67 * `/debug/pprof/profile`: Returns the pprof-formatted cpu profile. See [pprof.Profile](https://golang.org/pkg/net/http/pprof/#Profile). 68 * `/debug/pprof/heap`: Returns the pprof-formatted heap profile as of the last GC. See [pprof.Profile](https://golang.org/pkg/runtime/pprof/#Profile). 69 * `/debug/pprof/cmdline`: Returns the process's command line invocation as `text/plain`. See [pprof.Cmdline](https://golang.org/pkg/net/http/pprof/#Cmdline). 70 * `/debug/pprof/symbol`: Looks up the program counters listed in the request, responding with a table mapping program counters to function names See [pprof.Symbol](https://golang.org/pkg/net/http/pprof/#Symbol). 71 * `/debug/pprof/trace`: Returns the execution trace in binary form. See [pprof.Trace](https://golang.org/pkg/net/http/pprof/#Trace). 72 73 ### Context path 74 If `context-path` is specified in the install configuration, all of the routes registered on the server will be prefixed 75 with the specified `context-path`. 76 77 ### Security 78 `witchcraft-server` only supports HTTPS. The TLS client authentication type is configurable in code. The base install 79 configuration has fields to specify the location of server key and certificate material for TLS connections. 80 81 Although it is not possible to run `witchcraft-server` using HTTP, it is possible to configure the server in code to use 82 a generated self-signed certificate on start-up. Running the server in this mode and connecting to it using TLS without 83 server certificate verification (equivalent of `curl -k` or an `http.Transport` with 84 `TLSClientConfig: &tls.Config{InsecureSkipVerify: true}`) provides an analog to using HTTP, with the benefit that the 85 traffic itself is still encrypted. 86 87 ### Logging 88 `witchcraft-server` is configured with service, event, metric, request and trace loggers from the 89 `witchcraft-go-logging` project and emits structured JSON logs using [`zap`](https://github.com/uber-go/zap) as the 90 logger implementation. The default behavior emits logs to the `var/log` directory (`var/log/service.log`, 91 `var/log/request.log`, etc.) unless the server is run in a Docker container, in which case the logs are always emitted 92 to `stdout`. The `use-console-log` property in the install configuration can also be set to "true" to always output logs 93 to `stdout`. The runtime configuration supports configuring the log output level for service logs. 94 95 The `context.Context` provided to request handlers is configured with all of the standard loggers (service logger, event 96 logger, trace logger, etc.). All of the handlers are also configured to emit request logs and trace logs. 97 98 ### Service logger origin 99 By default, the `origin` field of the service logger is set to be the package path of the package in which the 100 `witchcraft-server` is started. For example, if the server is started in the file 101 `github.com/palantir/project/server/server.go`, the origin for all service log lines will be 102 `github.com/palantir/project/server`. 103 104 It is possible to configure the origin to be a different value using code. The origin can be specified to be a string 105 constant or a function can be used that returns a specific package path based on supplied parameters (for example, the 106 function can specify that the caller package's parent package should be used as the origin). The origin can also be set 107 to empty, in which case it is omitted from the log output. 108 109 ### Trace IDs and instrumentation 110 `witchcraft-server` supports zipkin-compatible tracing and ensures that every request is instrumented for tracing. 111 `witchcraft-server` also recognizes that some code will use trace IDs without necessarily using full zipkin-compatible 112 spans, so some allowances are made to support this scenario. 113 114 The built-in `witchcraft-server` middleware that registers loggers on the context also ensures that a zipkin span is 115 started. If the incoming request header has valid zipkin span information (that is, it specifies both a `X-B3-TraceId` 116 and `X-B3-SpanId` in the header), then the span created by the middleware is a child span of the incoming span. If the 117 incoming request does not have a trace ID header, a new root span is created. If the header specifies a trace ID but not 118 a span ID, the middleware creates a new root zipkin span, but ensures that the trace ID of the created span matches what 119 is specified in the header. If an incoming request is routed to a registered endpoint, the built-in router middleware 120 will create another span (which is a child span of the one created by the request middleware) whose span name is the 121 HTTP method and template for the endpoint. 122 123 The trace information generated by the middleware is set on the header and will be visible to subsequent handlers. If 124 the request specifies a `X-B3-Sampled` header, the value specified in that header is used to determine sampling. If this 125 header is not present, whether or not the trace is sampled is determined by the sampling source configured for the 126 `witchcraft-server` (by default, all traces are sampled). If a trace is not sampled, `witchcraft-server` will not 127 generate any trace log output for it. However, the infrastructure will still perform all of the trace-related operations 128 (such as creating child spans and setting span information on headers). The install configuration field 129 `trace-sample-rate` represents a float between 0 and 1 (inclusive) to control the proportion of traces sampled by 130 default. If the `WithTraceSampler` server option is provided, it overrides this configuration. 131 132 `witchcraft-server` also ensures that the context for every request has a trace ID. After the logging middleware 133 executes, the request is guaranteed to have a trace ID (either from the incoming request or from the newly generated 134 root span), and that trace ID is registered on the context. The `witchcraft.TraceIDFromContext(context.Context) string` 135 function can be used to retrieve the trace ID from the context. 136 137 ### Creating new spans/trace log entries 138 Use the `wtracing.StartSpanFromContext` function to start a new span. 139 This function will create a new span that is a child span of the span in the provided context. 140 Defer the `Finish()` function of the returned span to ensure that the span is properly marked as finished (the "finish" 141 operation will also generate a trace log entry if the span is sampled). 142 143 ### Middleware 144 `witchcraft-server` supports registering middleware to perform custom handling/augmenting of incoming requests. There 145 are 2 different kinds of middleware: *request* and *route* middleware. 146 147 Request middleware is executed on every request received by the server. The function signature for request middleware is 148 `func(rw http.ResponseWriter, r *http.Request, next http.Handler)`. Request middleware is the most common kind of 149 middleware. The server has built-in request middleware that adds a panic handler, sets the loggers and trade ID on the 150 request context and updates request-related metrics. Any user-supplied request middleware is run after the built-in 151 request middleware in the order in which they were added (which means that the context has all of the loggers 152 configured). Request middleware is run before the request is handled by the router, which means that it is possible to 153 rewrite the URL and other properties of the request and the router will route the _modified_ request. However, note that 154 the built-in logging middleware extracts the UID, SID, TokenID and TraceID from the request and sets them on the loggers 155 before user-provided middleware is invoked, so if the user-defined middleware modifies the header in a manner that would 156 change any of these values, the middleware should also update the request context to have loggers that use the updated 157 values. 158 159 Route middleware is only executed on the routes that are registered on the router -- they wrap the handler registered on 160 the route, so they are executed after the path has been matched and the handler for the router has been located and the 161 path parameters have been extracted and set on the context. The function signature for route middleware is 162 `func(rw http.ResponseWriter, r *http.Request, reqVals RequestVals, next RouteRequestHandler)`. The `RequestVals` struct 163 stores the path template for the route along with the path parameters and their values. The server has built-in route 164 middleware that records a request log entry after the request has completed and creates a trace log span and logs a 165 trace log entry after the request has completed. Route middleware is run after all of the request middleware has run, 166 and any user-supplied route middleware is run after the built-in route middleware in the order in which they were added. 167 Because router middleware is executed after the routing has been determined, changing the URL of the request will not 168 change the handler that is invoked or the path parameter values that have been extracted/stored (although it may still 169 impact behavior based on the content of the actual handler that is registered). In general, most users will likely use 170 request middleware rather than route middleware. However, if users want to only execute middleware on matched routes and 171 want route-specific information such as the unrendered path template and the path parameter values, then route 172 middleware should be used. 173 174 ### Long-running execution not associated with a route 175 In some instances, a server may want a long-running task not associated with an endpoint. For example, the server may 176 want a long-running goroutine that performs an operation at some interval for the lifetime of the server. 177 178 It is recommended that such goroutines be launched in the initialization function provided to `witchcraft.With` and use 179 the `ctx Context` as its context. This context has the same lifecycle as the server and has all of the configured 180 loggers (service loggers, metric loggers, etc.) already configured on it. 181 182 The provided context does not have a span or trace ID associated with it. If a trace ID is desired, 183 [create a new span](#creating-new-spanstrace-log-entries) with `wtracing.StartSpanFromContext` and the provided context to derive a new context that has a 184 new root span associated with it. This function also updates any loggers in the context to use the new trace ID (for 185 example, service loggers will include the trace ID). 186 187 ### Metrics 188 `witchcraft-server` initializes a metrics registry that uses the `github.com/palantir/pkg/metrics` package (which uses 189 `github.com/rcrowley/go-metrics` internally) to track metrics for the server. All of the tracked metrics are emitted as 190 metric log entries once every metric emit interval, which is 60 seconds by default (and can be configured to be a custom 191 interval in the install configuration). 192 193 By default, `witchcraft-server` captures various Go runtime metrics (such as allocations, number of running goroutines, 194 etc.) at the same frequency as the metric emit frequency. The collection of Go runtime statistics can be disabled with 195 the `WithDisableGoRuntimeMetrics` server method. 196 197 ### SIGQUIT handling 198 `witchcraft-server` sets up a SIGQUIT handler such that, if the program is terminated using a SIGQUIT signal 199 (`kill -3`), a goroutine dump is written as a `diagnostic.1` log. This behavior can be disabled using 200 `server.WithDisableSigQuitHandler`. If `server.WithSigQuitHandlerWriter` is used, the stacks will also be written in 201 their unparsed form to the provided writer. 202 203 ### Shutdown signal handling 204 `witchcraft-server` attempts to drain active connections and gracefully shut down by calling `server.Shutdown` upon receiving a SIGTERM or SIGINT signal. This behavior can be disabled using `server.WithDisableShutdownSignalHandler`. 205 206 Example server initialization 207 ----------------------------- 208 209 ### Basic production server 210 The following is an example program that launches a `witchcraft-server` that registers a `GET /myNum` endpoint that 211 returns a randomly generated number encoded as JSON: 212 213 ```go 214 package main 215 216 import ( 217 "context" 218 "math/rand" 219 "net/http" 220 221 "github.com/palantir/witchcraft-go-server/rest" 222 "github.com/palantir/witchcraft-go-server/witchcraft" 223 "github.com/palantir/witchcraft-go-server/witchcraft/refreshable" 224 "github.com/palantir/witchcraft-go-server/wrouter" 225 ) 226 227 func main() { 228 if err := witchcraft.NewServer(). 229 WithInitFunc(func(ctx context.Context, info witchcraft.InitInfo) (func(), error) { 230 if err := registerMyNumEndpoint(info.Router); err != nil { 231 return nil, err 232 } 233 return nil, nil 234 }). 235 Start(); err != nil { 236 panic(err) 237 } 238 } 239 240 func registerMyNumEndpoint(router wrouter.Router) error { 241 return router.Get("/myNum", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 242 rest.WriteJSONResponse(rw, rand.Intn(100), http.StatusOK) 243 })) 244 } 245 ``` 246 247 Creating a `witchcraft-server` starts with the `witchcraft.NewServer` function, which returns a new witchcraft server 248 with default configuration. The `*witchcraft.Server` struct has various `With*` functions that can be used to configure 249 the server, and the `Start()` function starts the server using the specified configuration. 250 251 The `WithInitFunc(InitFunc)` function is used to register routes on the server. The initialization function provided to 252 `WithInitFunc` is of the type `witchcraft.InitFunc`, which has the following definition: 253 `type InitFunc func(ctx context.Context, info InitInfo) (cleanup func(), rErr error)`. 254 255 The `ctx` provided to the function is valid for the duration of the server and has loggers configured on it. The `info` 256 struct contains fields that can be used to initialize various state and configuration for the server -- refer to the 257 `InitInfo` documentation for more information. 258 259 In this example, a "GET" endpoint is registered on the router using the "/myNum" path, and `rest` package is used to 260 write a JSON response. 261 262 This example server uses all of the `witchcraft` defaults -- it looks for install configuration in 263 `var/conf/install.yml` and uses `config.Install` as its type, looks for runtime configuration in `var/conf/runtime.yml` 264 and uses `config.Runtime` as its type, and looks for an encrypted-config-value key in 265 `var/conf/encrypted-config-value.key`. The install configuration must also specify paths to key and certificate files to 266 use for TLS. 267 268 ### Basic local/test server 269 The defaults for the server make sense for a production environment, but can make running the server locally (or in 270 tests) cumbersome. We can modify the `main` function as follows to configure the witchcraft server to use in-memory 271 defaults: 272 273 ```go 274 func main() { 275 if err := witchcraft.NewServer(). 276 WithInitFunc(func(ctx context.Context, info witchcraft.InitInfo) (func(), error) { 277 if err := registerMyNumEndpoint(info.Router); err != nil { 278 return nil, err 279 } 280 return nil, nil 281 }). 282 WithSelfSignedCertificate(). 283 WithECVKeyProvider(witchcraft.ECVKeyNoOp()). 284 WithRuntimeConfig(config.Runtime{}). 285 WithInstallConfig(config.Install{ 286 ProductName: "example-app", 287 Server: config.Server{ 288 Port: 8100, 289 }, 290 UseConsoleLog: true, 291 }). 292 Start(); err != nil { 293 panic(err) 294 } 295 } 296 ``` 297 298 The `WithSelfSignedCertificate()` function configures the server to start using a generated self-signed certificate, 299 which removes the need to specify server TLS material. The `WithECVKeyProvider(witchcraft.ECVKeyNoOp())` function 300 configures the server to use an empty ECV key source. The `WithRuntimeConfig(config.Runtime{})` function configures the 301 server to use the provided runtime configuration (in this case, it is empty), and the `WithInstallConfig` function 302 specifies the install configuration that should be used (it specifies that port 8100 should be used and that log output 303 should go to STDOUT). 304 305 With this configuration, the program can be run using `go run`: 306 307 ``` 308 ➜ go run main.go 309 {"level":"INFO","time":"2018-11-27T05:47:02.013456Z","message":"Listening to https","type":"service.1","origin":"github.com/palantir/witchcraft-go-server/app_example","params":{"address":":8100","server":"example-app"}} 310 ``` 311 312 Issuing a request to this server using `curl` produces the expected response (note that the `-k` option is used to skip 313 certificate verification because the server is using a self-signed certificate): 314 315 ```go 316 ➜ curl -k https://localhost:8100/myNum 317 81 318 ``` 319 320 You can also observe that the server emits trace and request logs based on receiving this request: 321 322 ``` 323 {"time":"2018-11-27T05:47:28.313585Z","type":"trace.1","span":{"traceId":"7e43bde2647413fc","id":"01228e628b3b3d22","name":"GET /myNum","parentId":"7e43bde2647413fc","timestamp":1543297648313551,"duration":29000}} 324 {"time":"2018-11-27T05:47:28.313719Z","type":"request.2","method":"GET","protocol":"HTTP/2.0","path":"/myNum","status":200,"requestSize":0,"responseSize":3,"duration":146,"traceId":"7e43bde2647413fc","params":{"Accept":"*/*","User-Agent":"curl/7.54.0","X-B3-Parentspanid":"7e43bde2647413fc","X-B3-Sampled":"1","X-B3-Spanid":"01228e628b3b3d22","X-B3-Traceid":"7e43bde2647413fc"}} 325 {"time":"2018-11-27T05:47:28.313802Z","type":"trace.1","span":{"traceId":"7e43bde2647413fc","id":"7e43bde2647413fc","name":"witchcraft-go-server request middleware","timestamp":1543297648313496,"duration":304000}} 326 ``` 327 328 ### Server using install configuration 329 The previous examples used the built-in install configuration. Most real servers will use custom install configuration 330 that specifies configuration for the server. Any struct can be used as install configuration, but it must support being 331 unmarshaled as YAML and must embed the `config.Install` struct. The install configuration is loaded once when the server 332 starts (it is never reloaded), so only values that are static for the lifetime of the server should be specified in this 333 configuration. 334 335 The following example modifies the previous example so that the endpoint returns the number defined in the install 336 configuration instead of a random number: 337 338 ```go 339 package main 340 341 import ( 342 "context" 343 "net/http" 344 345 "github.com/palantir/witchcraft-go-server/config" 346 "github.com/palantir/witchcraft-go-server/rest" 347 "github.com/palantir/witchcraft-go-server/witchcraft" 348 "github.com/palantir/witchcraft-go-server/witchcraft/refreshable" 349 "github.com/palantir/witchcraft-go-server/wrouter" 350 ) 351 352 type AppInstallConfig struct { 353 config.Install `yaml:",inline"` 354 355 MyNum int `yaml:"my-num"` 356 } 357 358 func main() { 359 if err := witchcraft.NewServer(). 360 WithInitFunc(func(ctx context.Context, info witchcraft.InitInfo) (func(), error) { 361 if err := registerMyNumEndpoint(info.Router, info.InstallConfig.(AppInstallConfig).MyNum); err != nil { 362 return nil, err 363 } 364 return nil, nil 365 }, 366 ). 367 WithSelfSignedCertificate(). 368 WithECVKeyProvider(witchcraft.ECVKeyNoOp()). 369 WithRuntimeConfig(config.Runtime{}). 370 WithInstallConfigType(AppInstallConfig{}). 371 WithInstallConfig(AppInstallConfig{ 372 Install: config.Install{ 373 ProductName: "example-app", 374 Server: config.Server{ 375 Port: 8100, 376 }, 377 UseConsoleLog: true, 378 }, 379 MyNum: 13, 380 }). 381 Start(); err != nil { 382 panic(err) 383 } 384 } 385 386 func registerMyNumEndpoint(router wrouter.Router, num int) error { 387 return router.Get("/myNum", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 388 rest.WriteJSONResponse(rw, num, http.StatusOK) 389 })) 390 } 391 ``` 392 393 This example defines the `AppInstallConfig` struct, which embeds `config.Install` and also defines a `MyNum` field. The 394 `WithInstallConfigType(AppInstallConfig{})` function call is added to specify `AppInstallConfig{}` as the install struct 395 and the initialization function logic is modified to convert the provided `installConfig interface{}` into an 396 `AppInstallConfig` and uses the `MyNum` value as the value that is returned by the endpoint. The `WithInstallConfig` 397 function is also updated to use configuration that specifies a value for `MyNum`. 398 399 Running the updated program using `go run main.go` and issuing `curl -k https://localhost:8100/myNum` returns `13`. 400 401 A real program will generally read runtime configuration from disk rather than specifying it directly in code. We can 402 modify the example above to do this by simply removing the `WithInstallConfig` call: 403 404 ```go 405 package main 406 407 import ( 408 "context" 409 "net/http" 410 411 "github.com/palantir/witchcraft-go-server/config" 412 "github.com/palantir/witchcraft-go-server/rest" 413 "github.com/palantir/witchcraft-go-server/witchcraft" 414 "github.com/palantir/witchcraft-go-server/witchcraft/refreshable" 415 "github.com/palantir/witchcraft-go-server/wrouter" 416 ) 417 418 type AppInstallConfig struct { 419 config.Install `yaml:",inline"` 420 421 MyNum int `yaml:"my-num"` 422 } 423 424 func main() { 425 if err := witchcraft.NewServer(). 426 WithInitFunc(func(ctx context.Context, info witchcraft.InitInfo) (func(), error) { 427 if err := registerMyNumEndpoint(info.Router, info.InstallConfig.(AppInstallConfig).MyNum); err != nil { 428 return nil, err 429 } 430 return nil, nil 431 }, 432 ). 433 WithSelfSignedCertificate(). 434 WithECVKeyProvider(witchcraft.ECVKeyNoOp()). 435 WithInstallConfigType(AppInstallConfig{}). 436 Start(); err != nil { 437 panic(err) 438 } 439 } 440 441 func registerMyNumEndpoint(router wrouter.Router, num int) error { 442 return router.Get("/myNum", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 443 rest.WriteJSONResponse(rw, num, http.StatusOK) 444 })) 445 } 446 ``` 447 448 By default, the install configuration is read from `var/conf/install.yml`. Create a file at that path relative to the 449 Go file and provide it with the YAML content for the configuration: 450 451 ```yaml 452 product-name: "example-app" 453 use-console-log: true 454 server: 455 port: 8100 456 my-num: 77 457 ``` 458 459 Running the updated program using `go run main.go` and issuing `curl -k https://localhost:8100/myNum` returns `77`. 460 461 ### Server using runtime configuration 462 Runtime configuration is similar to install configuration. The main difference is that runtime configuration supports 463 reloading configuration. When file-based runtime configuration is used, whenever the configuration file is updated, the 464 associated values are updated as well. 465 466 The following example defines a custom runtime configuration struct and returns the refreshable int value in the runtime 467 from its endpoint (the example uses a basic in-memory install configuration for simplicity): 468 469 ```go 470 package main 471 472 import ( 473 "context" 474 "net/http" 475 476 "github.com/palantir/witchcraft-go-server/config" 477 "github.com/palantir/witchcraft-go-server/rest" 478 "github.com/palantir/witchcraft-go-server/witchcraft" 479 "github.com/palantir/witchcraft-go-server/witchcraft/refreshable" 480 "github.com/palantir/witchcraft-go-server/wrouter" 481 ) 482 483 type AppRuntimeConfig struct { 484 config.Runtime `yaml:",inline"` 485 486 MyNum int `yaml:"my-num"` 487 } 488 489 func main() { 490 if err := witchcraft.NewServer(). 491 WithInitFunc(func(ctx context.Context, info witchcraft.InitInfo) (func(), error) { 492 myNumRefreshable := refreshable.NewInt(info.RuntimeConfig.Map(func(in interface{}) interface{} { 493 return in.(AppRuntimeConfig).MyNum 494 })) 495 if err := registerMyNumEndpoint(info.Router, myNumRefreshable); err != nil { 496 return nil, err 497 } 498 return nil, nil 499 }, 500 ). 501 WithSelfSignedCertificate(). 502 WithECVKeyProvider(witchcraft.ECVKeyNoOp()). 503 WithInstallConfig(config.Install{ 504 ProductName: "example-app", 505 Server: config.Server{ 506 Port: 8100, 507 }, 508 UseConsoleLog: true, 509 }). 510 WithRuntimeConfigType(AppRuntimeConfig{}). 511 Start(); err != nil { 512 panic(err) 513 } 514 } 515 516 func registerMyNumEndpoint(router wrouter.Router, numProvider refreshable.Int) error { 517 return router.Get("/myNum", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 518 rest.WriteJSONResponse(rw, numProvider.CurrentInt(), http.StatusOK) 519 })) 520 } 521 ``` 522 523 The refreshable configuration warrants some closer examination. Note that the `registerMyNumEndpoint` takes a 524 `numProvider refreshable.Int` as an argument rather than an `int` and returns the result of `CurrentInt()`. 525 Conceptually, the `numProvider` is guaranteed to always return the current value of the number specified in the runtime 526 configuration. Using this pattern removes the need for writing code that listens for updates -- the code can simply 527 assume that the provider always returns the most recent value. `refreshable.Int` and `refreshable.String` are helper 528 types that provide functions that return the current value of the correct type. For types without helper functions, the 529 general `refreshable.Refreshable` should be used, and the `interface{}` returned by `Current()` must be explicitly 530 converted to the proper target type (this is required because Go does not support generics/templatization). 531 532 The `numProvider` provided to `registerMyNumEndpoint` is derived by applying a mapping function to the 533 `runtimeConfig refreshable.Refreshable` parameter. `runtimeConfig.Map` is provided with a function that, given an 534 updated runtime configuration, returns the portion of the configuration that is required. The input to the mapping 535 function must be explicitly cast to the runtime configuration type (in this case, `in.(AppRuntimeConfig)`), and then the 536 relevant section can be accessed (or derived) and returned. The result of the `Map` function is a `Refreshable` that 537 returns the mapped portion. In this case, because we know the result will always be an `int`, we wrap the returned 538 `Refreshable` in a `refreshable.NewInt` call, which provides the convenience function `CurrentInt()` that performs the 539 type conversion of the result to an `int`. 540 541 By default, the runtime configuration is read from `var/conf/runtime.yml`. Create a file at that path relative to the 542 Go file and provide it with the YAML content for the configuration: 543 544 ```yaml 545 my-num: 99 546 ``` 547 548 Running the updated program using `go run main.go` and issuing `curl -k https://localhost:8100/myNum` returns `99`. 549 While the program is still running, update the content of the file to be `my-num: 88`, save it, then run the `curl` 550 command again. The output is `88`. 551 552 ### Full server example 553 The following is an example of a server that defines and uses both custom install and runtime configuration: 554 555 ```go 556 package main 557 558 import ( 559 "context" 560 "net/http" 561 562 "github.com/palantir/witchcraft-go-server/config" 563 "github.com/palantir/witchcraft-go-server/rest" 564 "github.com/palantir/witchcraft-go-server/witchcraft" 565 "github.com/palantir/witchcraft-go-server/witchcraft/refreshable" 566 "github.com/palantir/witchcraft-go-server/wrouter" 567 ) 568 569 type AppInstallConfig struct { 570 config.Install `yaml:",inline"` 571 572 MyNum int `yaml:"my-num"` 573 } 574 575 type AppRuntimeConfig struct { 576 config.Runtime `yaml:",inline"` 577 578 MyNum int `yaml:"my-num"` 579 } 580 581 func main() { 582 if err := witchcraft.NewServer(). 583 WithInitFunc(func(ctx context.Context, info witchcraft.InitInfo) (func(), error) { 584 if err := registerInstallNumEndpoint(info.Router, info.InstallConfig.(AppInstallConfig).MyNum); err != nil { 585 return nil, err 586 } 587 588 myNumRefreshable := refreshable.NewInt(info.RuntimeConfig.Map(func(in interface{}) interface{} { 589 return in.(AppRuntimeConfig).MyNum 590 })) 591 if err := registerRuntimeNumEndpoint(info.Router, myNumRefreshable); err != nil { 592 return nil, err 593 } 594 return nil, nil 595 }, 596 ). 597 WithInstallConfigType(AppInstallConfig{}). 598 WithRuntimeConfigType(AppRuntimeConfig{}). 599 WithSelfSignedCertificate(). 600 WithECVKeyProvider(witchcraft.ECVKeyNoOp()). 601 Start(); err != nil { 602 panic(err) 603 } 604 } 605 606 func registerInstallNumEndpoint(router wrouter.Router, num int) error { 607 return router.Get("/installNum", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 608 rest.WriteJSONResponse(rw, num, http.StatusOK) 609 })) 610 } 611 612 func registerRuntimeNumEndpoint(router wrouter.Router, numProvider refreshable.Int) error { 613 return router.Get("/runtimeNum", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 614 rest.WriteJSONResponse(rw, numProvider.CurrentInt(), http.StatusOK) 615 })) 616 } 617 ``` 618 619 With `var/conf/install.yml`: 620 621 ```yaml 622 product-name: "example-app" 623 use-console-log: true 624 server: 625 port: 8100 626 my-num: 7 627 ``` 628 629 And `var/conf/runtime.yml`: 630 631 ```yaml 632 my-num: 13 633 ``` 634 635 Querying `installNum` returns `7`, while querying `runtimeNum` returns `13`: 636 637 ``` 638 ➜ curl -k https://localhost:8100/installNum 639 7 640 ➜ curl -k https://localhost:8100/runtimeNum 641 13 642 ``` 643 644 In a production server, `WithSelfSignedCertificate()` and `WithECVKeyProvider(witchcraft.ECVKeyNoOp())` would not be 645 called and the proper security and key material would exist in their expected locations. 646 647 Refreshable configuration 648 ------------------------- 649 The runtime configuration for `witchcraft-server` uses the `refreshable.Refreshable` interface. Conceptually, a 650 `Refreshable` is a container that holds a value of a specific type that may be updated/refreshed. The following is the 651 interface definition for `Refreshable`: 652 653 ```go 654 type Refreshable interface { 655 // Current returns the most recent value of this Refreshable. 656 Current() interface{} 657 658 // Subscribe subscribes to changes of this Refreshable. The provided function is called with the value of Current() 659 // whenever the value changes. 660 Subscribe(consumer func(interface{})) (unsubscribe func()) 661 662 // Map returns a new Refreshable based on the current one that handles updates based on the current Refreshable. 663 Map(func(interface{}) interface{}) Refreshable 664 } 665 ``` 666 667 The `runtimeConfig refreshable.Refreshable` parameter provided to the initialization function specified using 668 `WithInitFunc` stores the latest unmarshaled runtime configuration as its current value, and the type of the value is 669 specified using the `WithRuntimeConfigType` function (if this function is not called, `config.Runtime` is used as the 670 default type). 671 672 For example, for the call: 673 674 ```go 675 witchcraft.NewServer(). 676 WithInitFunc(func(ctx context.Context, info witchcraft.InitInfo) (func(), error) { 677 return nil, nil 678 }). 679 WithRuntimeConfigType(AppRuntimeConfig{}) 680 ``` 681 682 The `WithRuntimeConfigType(AppRuntimeConfig{})` function specifies that the type of the runtime configuration is 683 `AppRuntimeConfig`, so the value returned by `runtimeConfig.Current()` in `WithInitFunc` will have the type 684 `AppRuntimeConfig`. Because Go does not have a notion of generics, the author must make this association manually and 685 perform the conversion of the current value into the desired type when using it (for example, 686 `runtimeConfig.Current().(AppRuntimeConfig)`). 687 688 The `Refreshable` interface supports using the `Map` function to derive a new refreshable based on the value of the 689 current refreshable. This allows downstream functions that are only interested in a subset of the refreshable to observe 690 just the relevant portion. 691 692 For example, consider the `AppRuntimeConfig` definition: 693 694 ```go 695 type AppRuntimeConfig struct { 696 config.Runtime `yaml:",inline"` 697 698 MyNum int `yaml:"my-num"` 699 } 700 ``` 701 702 A downstream function may only be interested in updates to the `MyNum` variable -- if updates to `config.Runtime` are 703 not relevant to the function, there is no need to subscribe to it. The following code derives a new `Refreshable` from 704 the `runtimeConfig` refreshable: 705 706 ```go 707 myNumRefreshable := runtimeConfig.Map(func(in interface{}) interface{} { 708 return in.(AppRuntimeConfig).MyNum 709 }) 710 ``` 711 712 The `Current()` function for `myNumRefreshable` returns the `MyNum` field of `in.(AppRuntimeConfig)`, and the derived 713 `Refreshable` is only updated when the derived value changes. Accessing a field is the most common usage of `Map`, but 714 any arbitrary logic can be performed in the mapping function. Just note that the mapping will be performed whenever the 715 parent refreshable is updated and the result will be compared using `reflect.DeepEqual`. 716 717 The general `Refreshable` interface returns an `interface{}` and its result must always be converted to the actual 718 underlying type. However, if a `Refreshable` is known to return an `int`, `string` or `bool`, convenience wrapper types 719 are provided to return typed values. For example, `refreshable.NewInt(in Refreshable)` returns a `refreshable.Int`, 720 which is defined as: 721 722 ```go 723 type Int interface { 724 Refreshable 725 CurrentInt() int 726 } 727 ``` 728 729 The `CurrentInt()` function returns the current value converted to an `int`, which makes it easier to use in code and 730 alleviates the need for clients to manually remember the type stored in the `Refreshable`. 731 732 If a `Refreshable` with a particular value/type is used widely throughout a code base, it may make sense to define a 733 similar interface so that clients do not have to manually track the type information. For example, a typed `Refreshable` 734 for `AppRuntimeConfig` can be defined as follows: 735 736 ```go 737 type RefreshableAppRuntimeConfig interface { 738 Refreshable 739 CurrentAppRuntimeConfig() AppRuntimeConfig 740 } 741 742 type refreshableAppRuntimeConfig struct { 743 Refreshable 744 } 745 746 func (r refreshableAppRuntimeConfig) CurrentAppRuntimeConfig() AppRuntimeConfig { 747 return rt.Current().(AppRuntimeConfig) 748 } 749 ``` 750 751 ### Updating refreshable configuration: provider-based vs. push-based 752 The "provider" model of configuration updates takes the philosophy that executing code simply needs the most up-to-date 753 value of a `Refreshable` when it executes. This model makes the most sense when the value is read whenever an endpoint 754 is executed or when a long-running or periodically executed background task executes. In these scenarios, the latest 755 value of the `Refreshable` is only needed when the logic executes. This update model is typically the most common, and 756 is achieved by passing down specific `Refreshable` providers for the required values to the handlers/routines. 757 758 However, in some cases, an application may want to be notified of every update to a field and react to that update 759 immediately -- for example, if updating a specific configuration field triggers an expensive computation that should 760 happen immediately, the logic wants to be notified as soon as the update is made. 761 762 In this scenario, the `Subscribe` function should be used for the `Refreshable` that has the value for which updates are 763 needed. For example, consider the following configuration: 764 765 ```go 766 type AppRuntimeConfig struct { 767 config.Runtime `yaml:",inline"` 768 769 AssetURLs []string `yaml:"asset-urls"` 770 } 771 ``` 772 773 The `AssetURLs` field specifies URLs that should be downloaded by the program whenever the value is updated. This 774 can be handled as follows: 775 776 ```go 777 unsubscribe := runtimeConfig.Map(func(in interface{}) interface{} { 778 return in.(AppRuntimeConfig).AssetURLs 779 }).Subscribe(func(in interface{}) { 780 assetURLs := in.([]string) 781 // perform work 782 }) 783 // unsubscribe should be deferred or stored and run at shutdown 784 ``` 785 786 The `Map` function returns a new `Refreshable` that updates only when the `AssetURLs` field is updated, and the 787 `Subscribe` function subscribes a listener that performs work as soon as the value is updated. This ensures that the 788 logic is run as soon as the value is refreshed every time the value is updated. 789 790 License 791 ------- 792 This project is made available under the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0).