From b32b696f5fb21e08d10b44a77ddb8f26f824613f Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 11 Jan 2023 15:02:52 -0600 Subject: [PATCH 1/9] feat: add stackdriver and json log options to `coder server` --- cli/deployment/config.go | 20 ++++++ cli/server.go | 78 +++++++++++++++++++++-- cli/testdata/coder_server_--help.golden | 9 +++ coderd/apidoc/docs.go | 17 +++++ coderd/apidoc/swagger.json | 17 +++++ codersdk/deploymentconfig.go | 7 +++ docs/api/general.md | 35 +++++++++++ docs/api/schemas.md | 84 +++++++++++++++++++++++++ go.mod | 2 + go.sum | 2 +- site/src/api/typesGenerated.ts | 8 +++ 11 files changed, 272 insertions(+), 7 deletions(-) diff --git a/cli/deployment/config.go b/cli/deployment/config.go index 69451de3a16e3..6312dbfe28254 100644 --- a/cli/deployment/config.go +++ b/cli/deployment/config.go @@ -471,6 +471,26 @@ func newConfig() *codersdk.DeploymentConfig { Default: false, }, }, + Logging: &codersdk.LoggingConfig{ + Human: &codersdk.DeploymentConfigField[string]{ + Name: "Human Log Location", + Usage: "Output human-readable logs to a given file.", + Flag: "human-log", + Default: "/dev/stderr", + }, + JSON: &codersdk.DeploymentConfigField[string]{ + Name: "JSON Log Location", + Usage: "Output JSON logs to a given file.", + Flag: "json-log", + Default: "", + }, + Stackdriver: &codersdk.DeploymentConfigField[string]{ + Name: "Stackdriver Log Location", + Usage: "Output Stackdriver compatible logs to a given file.", + Flag: "stackdriver-log", + Default: "", + }, + }, } } diff --git a/cli/server.go b/cli/server.go index a543f55a0e8c6..5f38a9b90a4d8 100644 --- a/cli/server.go +++ b/cli/server.go @@ -46,6 +46,8 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" + "cdr.dev/slog/sloggers/slogjson" + "cdr.dev/slog/sloggers/slogstackdriver" "github.com/coder/coder/buildinfo" "github.com/coder/coder/cli/cliui" "github.com/coder/coder/cli/config" @@ -122,13 +124,11 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co } printLogo(cmd) - logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr())) - if ok, _ := cmd.Flags().GetBool(varVerbose); ok { - logger = logger.Leveled(slog.LevelDebug) - } - if cfg.Trace.CaptureLogs.Value { - logger = logger.AppendSinks(tracing.SlogSink{}) + logger, logCloser, err := makeLogger(cmd, cfg) + if err != nil { + return xerrors.Errorf("make logger: %w", err) } + defer logCloser() // Register signals early on so that graceful shutdown can't // be interrupted by additional signals. Note that we avoid @@ -1145,6 +1145,11 @@ func newProvisionerDaemon( // nolint: revive func printLogo(cmd *cobra.Command) { + // Only print the logo in TTYs. + if !isTTYOut(cmd) { + return + } + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s - Software development on your infrastucture\n", cliui.Styles.Bold.Render("Coder "+buildinfo.Version())) } @@ -1512,3 +1517,64 @@ func redirectHTTPToAccessURL(handler http.Handler, accessURL *url.URL) http.Hand func isLocalhost(host string) bool { return host == "localhost" || host == "127.0.0.1" || host == "::1" } + +func makeLogger(cmd *cobra.Command, cfg *codersdk.DeploymentConfig) (slog.Logger, func(), error) { + var ( + sinks = []slog.Sink{} + closers = []func() error{} + ) + + if cfg.Logging.Human.Value != "" { + if cfg.Logging.Human.Value == "/dev/stderr" { + sinks = append(sinks, sloghuman.Sink(cmd.ErrOrStderr())) + } else { + fi, err := os.OpenFile(cfg.Logging.Human.Value, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) + if err != nil { + return slog.Logger{}, nil, xerrors.Errorf("open human log %q: %w", cfg.Logging.Human.Value, err) + } + closers = append(closers, fi.Close) + sinks = append(sinks, sloghuman.Sink(fi)) + } + } + + if cfg.Logging.JSON.Value != "" { + if cfg.Logging.JSON.Value == "/dev/stderr" { + sinks = append(sinks, slogjson.Sink(cmd.ErrOrStderr())) + } else { + fi, err := os.OpenFile(cfg.Logging.JSON.Value, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) + if err != nil { + return slog.Logger{}, nil, xerrors.Errorf("open json log %q: %w", cfg.Logging.JSON.Value, err) + } + closers = append(closers, fi.Close) + sinks = append(sinks, slogjson.Sink(fi)) + } + } + + if cfg.Logging.Stackdriver.Value != "" { + if cfg.Logging.JSON.Value == "/dev/stderr" { + sinks = append(sinks, slogstackdriver.Sink(cmd.ErrOrStderr())) + } else { + fi, err := os.OpenFile(cfg.Logging.Stackdriver.Value, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) + if err != nil { + return slog.Logger{}, nil, xerrors.Errorf("open stackdriver log %q: %w", cfg.Logging.Stackdriver.Value, err) + } + closers = append(closers, fi.Close) + sinks = append(sinks, slogstackdriver.Sink(fi)) + } + } + + if cfg.Trace.CaptureLogs.Value { + sinks = append(sinks, tracing.SlogSink{}) + } + + level := slog.LevelInfo + if ok, _ := cmd.Flags().GetBool(varVerbose); ok { + level = slog.LevelDebug + } + + return slog.Make(sinks...).Leveled(level), func() { + for _, closer := range closers { + _ = closer() + } + }, nil +} diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 49f2da97c55b0..c3dcc70940590 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -70,6 +70,12 @@ Flags: disable the HTTP endpoint. Consumes $CODER_HTTP_ADDRESS (default "127.0.0.1:3000") + --human-log string Output human-readable logs to a given + file. + Consumes $CODER_LOGGING_HUMAN (default + "/dev/stderr") + --json-log string Output JSON logs to a given file. + Consumes $CODER_LOGGING_JSON --max-token-lifetime duration The maximum lifetime duration for any user creating a token. Consumes $CODER_MAX_TOKEN_LIFETIME @@ -176,6 +182,9 @@ Flags: "ecdsa", or "rsa4096". Consumes $CODER_SSH_KEYGEN_ALGORITHM (default "ed25519") + --stackdriver-log string Output Stackdriver compatible logs to a + given file. + Consumes $CODER_LOGGING_STACKDRIVER --swagger-enable Expose the swagger endpoint via /swagger. Consumes $CODER_SWAGGER_ENABLE --telemetry Whether telemetry is enabled or not. diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index c103fa9ca0ec9..b6d9843bd7c92 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -5362,6 +5362,9 @@ const docTemplate = `{ "in_memory_database": { "$ref": "#/definitions/codersdk.DeploymentConfigField-bool" }, + "logging": { + "$ref": "#/definitions/codersdk.LoggingConfig" + }, "max_token_lifetime": { "$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration" }, @@ -5881,6 +5884,20 @@ const docTemplate = `{ "LogSourceProvisioner" ] }, + "codersdk.LoggingConfig": { + "type": "object", + "properties": { + "human": { + "$ref": "#/definitions/codersdk.DeploymentConfigField-string" + }, + "json": { + "$ref": "#/definitions/codersdk.DeploymentConfigField-string" + }, + "stackdriver": { + "$ref": "#/definitions/codersdk.DeploymentConfigField-string" + } + } + }, "codersdk.LoginType": { "type": "string", "enum": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index ae8733e3a44fe..2f78661e39906 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -4757,6 +4757,9 @@ "in_memory_database": { "$ref": "#/definitions/codersdk.DeploymentConfigField-bool" }, + "logging": { + "$ref": "#/definitions/codersdk.LoggingConfig" + }, "max_token_lifetime": { "$ref": "#/definitions/codersdk.DeploymentConfigField-time_Duration" }, @@ -5258,6 +5261,20 @@ "enum": ["provisioner_daemon", "provisioner"], "x-enum-varnames": ["LogSourceProvisionerDaemon", "LogSourceProvisioner"] }, + "codersdk.LoggingConfig": { + "type": "object", + "properties": { + "human": { + "$ref": "#/definitions/codersdk.DeploymentConfigField-string" + }, + "json": { + "$ref": "#/definitions/codersdk.DeploymentConfigField-string" + }, + "stackdriver": { + "$ref": "#/definitions/codersdk.DeploymentConfigField-string" + } + } + }, "codersdk.LoginType": { "type": "string", "enum": ["password", "github", "oidc", "token"], diff --git a/codersdk/deploymentconfig.go b/codersdk/deploymentconfig.go index 6842f15a66c23..477a27b013e6c 100644 --- a/codersdk/deploymentconfig.go +++ b/codersdk/deploymentconfig.go @@ -45,6 +45,7 @@ type DeploymentConfig struct { UpdateCheck *DeploymentConfigField[bool] `json:"update_check" typescript:",notnull"` MaxTokenLifetime *DeploymentConfigField[time.Duration] `json:"max_token_lifetime" typescript:",notnull"` Swagger *SwaggerConfig `json:"swagger" typescript:",notnull"` + Logging *LoggingConfig `json:"logging" typescript:",notnull"` } type DERP struct { @@ -155,6 +156,12 @@ type SwaggerConfig struct { Enable *DeploymentConfigField[bool] `json:"enable" typescript:",notnull"` } +type LoggingConfig struct { + Human *DeploymentConfigField[string] `json:"human" typescript:",notnull"` + JSON *DeploymentConfigField[string] `json:"json" typescript:",notnull"` + Stackdriver *DeploymentConfigField[string] `json:"stackdriver" typescript:",notnull"` +} + type Flaggable interface { string | time.Duration | bool | int | []string | []GitAuthConfig } diff --git a/docs/api/general.md b/docs/api/general.md index 85167d8dd2ee9..578ad62ebc6e7 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -335,6 +335,41 @@ curl -X GET http://coder-server:8080/api/v2/config/deployment \ "usage": "string", "value": true }, + "logging": { + "human": { + "default": "string", + "enterprise": true, + "flag": "string", + "hidden": true, + "name": "string", + "secret": true, + "shorthand": "string", + "usage": "string", + "value": "string" + }, + "json": { + "default": "string", + "enterprise": true, + "flag": "string", + "hidden": true, + "name": "string", + "secret": true, + "shorthand": "string", + "usage": "string", + "value": "string" + }, + "stackdriver": { + "default": "string", + "enterprise": true, + "flag": "string", + "hidden": true, + "name": "string", + "secret": true, + "shorthand": "string", + "usage": "string", + "value": "string" + } + }, "max_token_lifetime": { "default": 0, "enterprise": true, diff --git a/docs/api/schemas.md b/docs/api/schemas.md index ed546daf87ca8..a4d497a05cf59 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1408,6 +1408,41 @@ CreateParameterRequest is a structure used to create a new parameter value for a "usage": "string", "value": true }, + "logging": { + "human": { + "default": "string", + "enterprise": true, + "flag": "string", + "hidden": true, + "name": "string", + "secret": true, + "shorthand": "string", + "usage": "string", + "value": "string" + }, + "json": { + "default": "string", + "enterprise": true, + "flag": "string", + "hidden": true, + "name": "string", + "secret": true, + "shorthand": "string", + "usage": "string", + "value": "string" + }, + "stackdriver": { + "default": "string", + "enterprise": true, + "flag": "string", + "hidden": true, + "name": "string", + "secret": true, + "shorthand": "string", + "usage": "string", + "value": "string" + } + }, "max_token_lifetime": { "default": 0, "enterprise": true, @@ -2022,6 +2057,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a | `gitauth` | [codersdk.DeploymentConfigField-array_codersdk_GitAuthConfig](#codersdkdeploymentconfigfield-array_codersdk_gitauthconfig) | false | | | | `http_address` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | | | `in_memory_database` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | | +| `logging` | [codersdk.LoggingConfig](#codersdkloggingconfig) | false | | | | `max_token_lifetime` | [codersdk.DeploymentConfigField-time_Duration](#codersdkdeploymentconfigfield-time_duration) | false | | | | `metrics_cache_refresh_interval` | [codersdk.DeploymentConfigField-time_Duration](#codersdkdeploymentconfigfield-time_duration) | false | | | | `oauth2` | [codersdk.OAuth2Config](#codersdkoauth2config) | false | | | @@ -2558,6 +2594,54 @@ CreateParameterRequest is a structure used to create a new parameter value for a | `provisioner_daemon` | | `provisioner` | +## codersdk.LoggingConfig + +```json +{ + "human": { + "default": "string", + "enterprise": true, + "flag": "string", + "hidden": true, + "name": "string", + "secret": true, + "shorthand": "string", + "usage": "string", + "value": "string" + }, + "json": { + "default": "string", + "enterprise": true, + "flag": "string", + "hidden": true, + "name": "string", + "secret": true, + "shorthand": "string", + "usage": "string", + "value": "string" + }, + "stackdriver": { + "default": "string", + "enterprise": true, + "flag": "string", + "hidden": true, + "name": "string", + "secret": true, + "shorthand": "string", + "usage": "string", + "value": "string" + } +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------- | ------------------------------------------------------------------------------ | -------- | ------------ | ----------- | +| `human` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | | +| `json` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | | +| `stackdriver` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | | + ## codersdk.LoginType ```json diff --git a/go.mod b/go.mod index 393efd50842a5..2711bd2cdf93b 100644 --- a/go.mod +++ b/go.mod @@ -167,6 +167,8 @@ require ( tailscale.com v1.32.2 ) +require cloud.google.com/go/longrunning v0.1.1 // indirect + require ( cloud.google.com/go/compute v1.12.1 // indirect filippo.io/edwards25519 v1.0.0-rc.1 // indirect diff --git a/go.sum b/go.sum index 03f2351aeabd4..2c22b89fa4d7b 100644 --- a/go.sum +++ b/go.sum @@ -38,7 +38,6 @@ cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0c cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= -cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -61,6 +60,7 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl cloud.google.com/go/firestore v1.6.0/go.mod h1:afJwI0vaXwAG54kI7A//lP/lSPDkQORQuMkv56TxEPU= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/longrunning v0.1.1 h1:y50CXG4j0+qvEukslYFBCrzaXX0qpFbBzc3PchSu/LE= +cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 78015f4376c43..7570a7d9289de 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -313,6 +313,7 @@ export interface DeploymentConfig { readonly update_check: DeploymentConfigField readonly max_token_lifetime: DeploymentConfigField readonly swagger: SwaggerConfig + readonly logging: LoggingConfig } // From codersdk/deploymentconfig.go @@ -421,6 +422,13 @@ export interface ListeningPortsResponse { readonly ports: ListeningPort[] } +// From codersdk/deploymentconfig.go +export interface LoggingConfig { + readonly human: DeploymentConfigField + readonly json: DeploymentConfigField + readonly stackdriver: DeploymentConfigField +} + // From codersdk/users.go export interface LoginWithPasswordRequest { readonly email: string From 6ebb833ad2ce31e31bb25d123f1c468fe136e8b7 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 11 Jan 2023 18:25:39 -0600 Subject: [PATCH 2/9] rename flags --- cli/deployment/config.go | 6 +++--- cli/testdata/coder_server_--help.golden | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cli/deployment/config.go b/cli/deployment/config.go index 6312dbfe28254..02b24f371ce6f 100644 --- a/cli/deployment/config.go +++ b/cli/deployment/config.go @@ -475,19 +475,19 @@ func newConfig() *codersdk.DeploymentConfig { Human: &codersdk.DeploymentConfigField[string]{ Name: "Human Log Location", Usage: "Output human-readable logs to a given file.", - Flag: "human-log", + Flag: "log-human", Default: "/dev/stderr", }, JSON: &codersdk.DeploymentConfigField[string]{ Name: "JSON Log Location", Usage: "Output JSON logs to a given file.", - Flag: "json-log", + Flag: "log-json", Default: "", }, Stackdriver: &codersdk.DeploymentConfigField[string]{ Name: "Stackdriver Log Location", Usage: "Output Stackdriver compatible logs to a given file.", - Flag: "stackdriver-log", + Flag: "log-stackdriver", Default: "", }, }, diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index c3dcc70940590..0424c2c8dd9f1 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -70,12 +70,15 @@ Flags: disable the HTTP endpoint. Consumes $CODER_HTTP_ADDRESS (default "127.0.0.1:3000") - --human-log string Output human-readable logs to a given + --log-human string Output human-readable logs to a given file. Consumes $CODER_LOGGING_HUMAN (default "/dev/stderr") - --json-log string Output JSON logs to a given file. + --log-json string Output JSON logs to a given file. Consumes $CODER_LOGGING_JSON + --log-stackdriver string Output Stackdriver compatible logs to a + given file. + Consumes $CODER_LOGGING_STACKDRIVER --max-token-lifetime duration The maximum lifetime duration for any user creating a token. Consumes $CODER_MAX_TOKEN_LIFETIME @@ -182,9 +185,6 @@ Flags: "ecdsa", or "rsa4096". Consumes $CODER_SSH_KEYGEN_ALGORITHM (default "ed25519") - --stackdriver-log string Output Stackdriver compatible logs to a - given file. - Consumes $CODER_LOGGING_STACKDRIVER --swagger-enable Expose the swagger endpoint via /swagger. Consumes $CODER_SWAGGER_ENABLE --telemetry Whether telemetry is enabled or not. From c9e38985fc9e59e21dc719bfe11877b5f644a8d5 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 11 Jan 2023 18:28:00 -0600 Subject: [PATCH 3/9] error if no loggers configured --- cli/server.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cli/server.go b/cli/server.go index 5f38a9b90a4d8..59cafec6a1b9b 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1572,6 +1572,10 @@ func makeLogger(cmd *cobra.Command, cfg *codersdk.DeploymentConfig) (slog.Logger level = slog.LevelDebug } + if len(sinks) == 0 { + return slog.Logger{}, nil, xerrors.New("no loggers provided") + } + return slog.Make(sinks...).Leveled(level), func() { for _, closer := range closers { _ = closer() From 404d52278ae0f81c8bb014b090526d3d8954c071 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 12 Jan 2023 17:16:40 -0600 Subject: [PATCH 4/9] add testing --- cli/server.go | 58 +++++++------- cli/server_test.go | 190 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 31 deletions(-) diff --git a/cli/server.go b/cli/server.go index 59cafec6a1b9b..4a3121efa711d 100644 --- a/cli/server.go +++ b/cli/server.go @@ -124,7 +124,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co } printLogo(cmd) - logger, logCloser, err := makeLogger(cmd, cfg) + logger, logCloser, err := buildLogger(cmd, cfg) if err != nil { return xerrors.Errorf("make logger: %w", err) } @@ -1518,49 +1518,45 @@ func isLocalhost(host string) bool { return host == "localhost" || host == "127.0.0.1" || host == "::1" } -func makeLogger(cmd *cobra.Command, cfg *codersdk.DeploymentConfig) (slog.Logger, func(), error) { +func buildLogger(cmd *cobra.Command, cfg *codersdk.DeploymentConfig) (slog.Logger, func(), error) { var ( sinks = []slog.Sink{} closers = []func() error{} ) - if cfg.Logging.Human.Value != "" { - if cfg.Logging.Human.Value == "/dev/stderr" { - sinks = append(sinks, sloghuman.Sink(cmd.ErrOrStderr())) - } else { + addSinkIfProvided := func(sinkFn func(io.Writer) slog.Sink, loc string) error { + switch loc { + case "": + + case "/dev/stdout": + sinks = append(sinks, sinkFn(cmd.OutOrStdout())) + + case "/dev/stderr": + sinks = append(sinks, sinkFn(cmd.ErrOrStderr())) + + default: fi, err := os.OpenFile(cfg.Logging.Human.Value, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) if err != nil { - return slog.Logger{}, nil, xerrors.Errorf("open human log %q: %w", cfg.Logging.Human.Value, err) + return xerrors.Errorf("open log file %q: %w", cfg.Logging.Human.Value, err) } - closers = append(closers, fi.Close) - sinks = append(sinks, sloghuman.Sink(fi)) - } - } - if cfg.Logging.JSON.Value != "" { - if cfg.Logging.JSON.Value == "/dev/stderr" { - sinks = append(sinks, slogjson.Sink(cmd.ErrOrStderr())) - } else { - fi, err := os.OpenFile(cfg.Logging.JSON.Value, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) - if err != nil { - return slog.Logger{}, nil, xerrors.Errorf("open json log %q: %w", cfg.Logging.JSON.Value, err) - } closers = append(closers, fi.Close) - sinks = append(sinks, slogjson.Sink(fi)) + sinks = append(sinks, sinkFn(fi)) } + return nil } - if cfg.Logging.Stackdriver.Value != "" { - if cfg.Logging.JSON.Value == "/dev/stderr" { - sinks = append(sinks, slogstackdriver.Sink(cmd.ErrOrStderr())) - } else { - fi, err := os.OpenFile(cfg.Logging.Stackdriver.Value, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) - if err != nil { - return slog.Logger{}, nil, xerrors.Errorf("open stackdriver log %q: %w", cfg.Logging.Stackdriver.Value, err) - } - closers = append(closers, fi.Close) - sinks = append(sinks, slogstackdriver.Sink(fi)) - } + err := addSinkIfProvided(sloghuman.Sink, cfg.Logging.Human.Value) + if err != nil { + return slog.Logger{}, nil, xerrors.Errorf("add human sink: %w", err) + } + err = addSinkIfProvided(slogjson.Sink, cfg.Logging.JSON.Value) + if err != nil { + return slog.Logger{}, nil, xerrors.Errorf("add json sink: %w", err) + } + err = addSinkIfProvided(slogstackdriver.Sink, cfg.Logging.Stackdriver.Value) + if err != nil { + return slog.Logger{}, nil, xerrors.Errorf("add stackdriver sink: %w", err) } if cfg.Trace.CaptureLogs.Value { diff --git a/cli/server_test.go b/cli/server_test.go index 32b1c10ceebc9..cbc38787a94c0 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -35,6 +35,7 @@ import ( "github.com/coder/coder/coderd/database/postgres" "github.com/coder/coder/coderd/telemetry" "github.com/coder/coder/codersdk" + "github.com/coder/coder/cryptorand" "github.com/coder/coder/pty/ptytest" "github.com/coder/coder/testutil" ) @@ -1122,6 +1123,195 @@ func TestServer(t *testing.T) { <-serverErr }) }) + + t.Run("Logging", func(t *testing.T) { + t.Parallel() + + t.Run("CreatesFile", func(t *testing.T) { + t.Parallel() + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + + // fi := os.CreateTemp("/tmp", "coder-logging-test-*") + random, err := cryptorand.String(5) + require.NoError(t, err) + fiName := fmt.Sprint("/tmp/coder-logging-test-", random) + defer func() { + _ = os.Remove(fiName) + }() + + root, _ := clitest.New(t, + "server", + "--verbose", + "--in-memory", + "--http-address", ":0", + "--access-url", "http://example.com", + "--log-human", fiName, + ) + serverErr := make(chan error, 1) + go func() { + serverErr <- root.ExecuteContext(ctx) + }() + + require.Eventually(t, func() bool { + stat, err := os.Stat(fiName) + return err == nil && stat.Size() > 0 + }, testutil.IntervalFast, testutil.WaitShort) + cancelFunc() + <-serverErr + }) + + t.Run("Human", func(t *testing.T) { + t.Parallel() + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + + fi, err := os.CreateTemp("/tmp", "coder-logging-test-*") + require.NoError(t, err) + defer func() { + _ = os.Remove(fi.Name()) + }() + + root, _ := clitest.New(t, + "server", + "--verbose", + "--in-memory", + "--http-address", ":0", + "--access-url", "http://example.com", + "--log-human", fi.Name(), + ) + serverErr := make(chan error, 1) + go func() { + serverErr <- root.ExecuteContext(ctx) + }() + + require.Eventually(t, func() bool { + stat, err := os.Stat(fi.Name()) + return err == nil && stat.Size() > 0 + }, testutil.IntervalFast, testutil.WaitShort) + cancelFunc() + <-serverErr + }) + + t.Run("JSON", func(t *testing.T) { + t.Parallel() + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + + fi, err := os.CreateTemp("/tmp", "coder-logging-test-*") + require.NoError(t, err) + defer func() { + _ = os.Remove(fi.Name()) + }() + + root, _ := clitest.New(t, + "server", + "--verbose", + "--in-memory", + "--http-address", ":0", + "--access-url", "http://example.com", + "--log-json", fi.Name(), + ) + serverErr := make(chan error, 1) + go func() { + serverErr <- root.ExecuteContext(ctx) + }() + + require.Eventually(t, func() bool { + stat, err := os.Stat(fi.Name()) + return err == nil && stat.Size() > 0 + }, testutil.IntervalFast, testutil.WaitShort) + cancelFunc() + <-serverErr + }) + + t.Run("Stackdriver", func(t *testing.T) { + t.Parallel() + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + + fi, err := os.CreateTemp("/tmp", "coder-logging-test-*") + require.NoError(t, err) + defer func() { + _ = os.Remove(fi.Name()) + }() + + root, _ := clitest.New(t, + "server", + "--verbose", + "--in-memory", + "--http-address", ":0", + "--access-url", "http://example.com", + "--log-stackdriver", fi.Name(), + ) + serverErr := make(chan error, 1) + go func() { + serverErr <- root.ExecuteContext(ctx) + }() + + require.Eventually(t, func() bool { + stat, err := os.Stat(fi.Name()) + return err == nil && stat.Size() > 0 + }, testutil.IntervalFast, testutil.WaitShort) + cancelFunc() + <-serverErr + }) + + t.Run("Multiple", func(t *testing.T) { + t.Parallel() + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + + fi1, err := os.CreateTemp("/tmp", "coder-logging-test-*") + require.NoError(t, err) + defer func() { + _ = os.Remove(fi1.Name()) + }() + + fi2, err := os.CreateTemp("/tmp", "coder-logging-test-*") + require.NoError(t, err) + defer func() { + _ = os.Remove(fi2.Name()) + }() + + fi3, err := os.CreateTemp("/tmp", "coder-logging-test-*") + require.NoError(t, err) + defer func() { + _ = os.Remove(fi3.Name()) + }() + + root, _ := clitest.New(t, + "server", + "--verbose", + "--in-memory", + "--http-address", ":0", + "--access-url", "http://example.com", + "--log-human", fi1.Name(), + "--log-json", fi2.Name(), + "--log-stackdriver", fi3.Name(), + ) + serverErr := make(chan error, 1) + go func() { + serverErr <- root.ExecuteContext(ctx) + }() + + require.Eventually(t, func() bool { + stat, err := os.Stat(fi1.Name()) + return err == nil && stat.Size() > 0 + }, testutil.IntervalFast, testutil.WaitShort) + require.Eventually(t, func() bool { + stat, err := os.Stat(fi2.Name()) + return err == nil && stat.Size() > 0 + }, testutil.IntervalFast, testutil.WaitShort) + require.Eventually(t, func() bool { + stat, err := os.Stat(fi3.Name()) + return err == nil && stat.Size() > 0 + }, testutil.IntervalFast, testutil.WaitShort) + + cancelFunc() + <-serverErr + }) + }) } func generateTLSCertificate(t testing.TB, commonName ...string) (certPath, keyPath string) { From d929d0fe5d510f93e79b035d64093dc1b5cede1e Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 12 Jan 2023 19:00:13 -0600 Subject: [PATCH 5/9] fixup! add testing --- cli/server.go | 4 ++-- cli/server_test.go | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cli/server.go b/cli/server.go index 4a3121efa711d..bd8b7fcdf4574 100644 --- a/cli/server.go +++ b/cli/server.go @@ -1535,9 +1535,9 @@ func buildLogger(cmd *cobra.Command, cfg *codersdk.DeploymentConfig) (slog.Logge sinks = append(sinks, sinkFn(cmd.ErrOrStderr())) default: - fi, err := os.OpenFile(cfg.Logging.Human.Value, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) + fi, err := os.OpenFile(loc, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) if err != nil { - return xerrors.Errorf("open log file %q: %w", cfg.Logging.Human.Value, err) + return xerrors.Errorf("open log file %q: %w", loc, err) } closers = append(closers, fi.Close) diff --git a/cli/server_test.go b/cli/server_test.go index cbc38787a94c0..d8015d7cbfd8a 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -1156,7 +1156,7 @@ func TestServer(t *testing.T) { require.Eventually(t, func() bool { stat, err := os.Stat(fiName) return err == nil && stat.Size() > 0 - }, testutil.IntervalFast, testutil.WaitShort) + }, testutil.WaitShort, testutil.IntervalFast) cancelFunc() <-serverErr }) @@ -1188,7 +1188,7 @@ func TestServer(t *testing.T) { require.Eventually(t, func() bool { stat, err := os.Stat(fi.Name()) return err == nil && stat.Size() > 0 - }, testutil.IntervalFast, testutil.WaitShort) + }, testutil.WaitShort, testutil.IntervalFast) cancelFunc() <-serverErr }) @@ -1217,10 +1217,10 @@ func TestServer(t *testing.T) { serverErr <- root.ExecuteContext(ctx) }() - require.Eventually(t, func() bool { + assert.Eventually(t, func() bool { stat, err := os.Stat(fi.Name()) return err == nil && stat.Size() > 0 - }, testutil.IntervalFast, testutil.WaitShort) + }, testutil.WaitShort, testutil.IntervalFast) cancelFunc() <-serverErr }) @@ -1252,7 +1252,7 @@ func TestServer(t *testing.T) { require.Eventually(t, func() bool { stat, err := os.Stat(fi.Name()) return err == nil && stat.Size() > 0 - }, testutil.IntervalFast, testutil.WaitShort) + }, testutil.WaitShort, testutil.IntervalFast) cancelFunc() <-serverErr }) @@ -1298,15 +1298,15 @@ func TestServer(t *testing.T) { require.Eventually(t, func() bool { stat, err := os.Stat(fi1.Name()) return err == nil && stat.Size() > 0 - }, testutil.IntervalFast, testutil.WaitShort) + }, testutil.WaitShort, testutil.IntervalFast) require.Eventually(t, func() bool { stat, err := os.Stat(fi2.Name()) return err == nil && stat.Size() > 0 - }, testutil.IntervalFast, testutil.WaitShort) + }, testutil.WaitShort, testutil.IntervalFast) require.Eventually(t, func() bool { stat, err := os.Stat(fi3.Name()) return err == nil && stat.Size() > 0 - }, testutil.IntervalFast, testutil.WaitShort) + }, testutil.WaitShort, testutil.IntervalFast) cancelFunc() <-serverErr From 5d1104202eeaeb35c005df1606f1f82872da133c Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 12 Jan 2023 19:13:50 -0600 Subject: [PATCH 6/9] fixup! add testing --- cli/server_test.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/cli/server_test.go b/cli/server_test.go index d8015d7cbfd8a..24b791ce3ea83 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -1132,10 +1132,9 @@ func TestServer(t *testing.T) { ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() - // fi := os.CreateTemp("/tmp", "coder-logging-test-*") random, err := cryptorand.String(5) require.NoError(t, err) - fiName := fmt.Sprint("/tmp/coder-logging-test-", random) + fiName := fmt.Sprint(os.TempDir(), "/coder-logging-test-", random) defer func() { _ = os.Remove(fiName) }() @@ -1166,7 +1165,7 @@ func TestServer(t *testing.T) { ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() - fi, err := os.CreateTemp("/tmp", "coder-logging-test-*") + fi, err := os.CreateTemp("", "coder-logging-test-*") require.NoError(t, err) defer func() { _ = os.Remove(fi.Name()) @@ -1198,7 +1197,7 @@ func TestServer(t *testing.T) { ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() - fi, err := os.CreateTemp("/tmp", "coder-logging-test-*") + fi, err := os.CreateTemp("", "coder-logging-test-*") require.NoError(t, err) defer func() { _ = os.Remove(fi.Name()) @@ -1230,7 +1229,7 @@ func TestServer(t *testing.T) { ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() - fi, err := os.CreateTemp("/tmp", "coder-logging-test-*") + fi, err := os.CreateTemp("", "coder-logging-test-*") require.NoError(t, err) defer func() { _ = os.Remove(fi.Name()) @@ -1262,19 +1261,19 @@ func TestServer(t *testing.T) { ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() - fi1, err := os.CreateTemp("/tmp", "coder-logging-test-*") + fi1, err := os.CreateTemp("", "coder-logging-test-*") require.NoError(t, err) defer func() { _ = os.Remove(fi1.Name()) }() - fi2, err := os.CreateTemp("/tmp", "coder-logging-test-*") + fi2, err := os.CreateTemp("", "coder-logging-test-*") require.NoError(t, err) defer func() { _ = os.Remove(fi2.Name()) }() - fi3, err := os.CreateTemp("/tmp", "coder-logging-test-*") + fi3, err := os.CreateTemp("", "coder-logging-test-*") require.NoError(t, err) defer func() { _ = os.Remove(fi3.Name()) From 1a56dbe80814930b8578998eea43e034d03fcf7a Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 12 Jan 2023 19:41:10 -0600 Subject: [PATCH 7/9] fixup! add testing --- cli/server_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/server_test.go b/cli/server_test.go index 24b791ce3ea83..b4075628e86b4 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -1152,7 +1152,7 @@ func TestServer(t *testing.T) { serverErr <- root.ExecuteContext(ctx) }() - require.Eventually(t, func() bool { + assert.Eventually(t, func() bool { stat, err := os.Stat(fiName) return err == nil && stat.Size() > 0 }, testutil.WaitShort, testutil.IntervalFast) @@ -1184,7 +1184,7 @@ func TestServer(t *testing.T) { serverErr <- root.ExecuteContext(ctx) }() - require.Eventually(t, func() bool { + assert.Eventually(t, func() bool { stat, err := os.Stat(fi.Name()) return err == nil && stat.Size() > 0 }, testutil.WaitShort, testutil.IntervalFast) @@ -1248,7 +1248,7 @@ func TestServer(t *testing.T) { serverErr <- root.ExecuteContext(ctx) }() - require.Eventually(t, func() bool { + assert.Eventually(t, func() bool { stat, err := os.Stat(fi.Name()) return err == nil && stat.Size() > 0 }, testutil.WaitShort, testutil.IntervalFast) @@ -1294,15 +1294,15 @@ func TestServer(t *testing.T) { serverErr <- root.ExecuteContext(ctx) }() - require.Eventually(t, func() bool { + assert.Eventually(t, func() bool { stat, err := os.Stat(fi1.Name()) return err == nil && stat.Size() > 0 }, testutil.WaitShort, testutil.IntervalFast) - require.Eventually(t, func() bool { + assert.Eventually(t, func() bool { stat, err := os.Stat(fi2.Name()) return err == nil && stat.Size() > 0 }, testutil.WaitShort, testutil.IntervalFast) - require.Eventually(t, func() bool { + assert.Eventually(t, func() bool { stat, err := os.Stat(fi3.Name()) return err == nil && stat.Size() > 0 }, testutil.WaitShort, testutil.IntervalFast) From 21eb106d034dd7dfdca979448986733c2461a42f Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 12 Jan 2023 19:42:26 -0600 Subject: [PATCH 8/9] fixup! add testing --- cli/server_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/server_test.go b/cli/server_test.go index b4075628e86b4..c4f2c77ad2139 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -1297,15 +1297,15 @@ func TestServer(t *testing.T) { assert.Eventually(t, func() bool { stat, err := os.Stat(fi1.Name()) return err == nil && stat.Size() > 0 - }, testutil.WaitShort, testutil.IntervalFast) + }, testutil.WaitLong, testutil.IntervalFast) assert.Eventually(t, func() bool { stat, err := os.Stat(fi2.Name()) return err == nil && stat.Size() > 0 - }, testutil.WaitShort, testutil.IntervalFast) + }, testutil.WaitLong, testutil.IntervalFast) assert.Eventually(t, func() bool { stat, err := os.Stat(fi3.Name()) return err == nil && stat.Size() > 0 - }, testutil.WaitShort, testutil.IntervalFast) + }, testutil.WaitLong, testutil.IntervalFast) cancelFunc() <-serverErr From 3c870114b61f1f10919254ac789e508cd6ba2324 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 12 Jan 2023 19:48:51 -0600 Subject: [PATCH 9/9] fixup! add testing --- cli/server_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/server_test.go b/cli/server_test.go index c4f2c77ad2139..6cd13cb32e2ce 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -1251,7 +1251,7 @@ func TestServer(t *testing.T) { assert.Eventually(t, func() bool { stat, err := os.Stat(fi.Name()) return err == nil && stat.Size() > 0 - }, testutil.WaitShort, testutil.IntervalFast) + }, testutil.WaitLong, testutil.IntervalMedium) cancelFunc() <-serverErr }) @@ -1297,15 +1297,15 @@ func TestServer(t *testing.T) { assert.Eventually(t, func() bool { stat, err := os.Stat(fi1.Name()) return err == nil && stat.Size() > 0 - }, testutil.WaitLong, testutil.IntervalFast) + }, testutil.WaitLong, testutil.IntervalMedium) assert.Eventually(t, func() bool { stat, err := os.Stat(fi2.Name()) return err == nil && stat.Size() > 0 - }, testutil.WaitLong, testutil.IntervalFast) + }, testutil.WaitLong, testutil.IntervalMedium) assert.Eventually(t, func() bool { stat, err := os.Stat(fi3.Name()) return err == nil && stat.Size() > 0 - }, testutil.WaitLong, testutil.IntervalFast) + }, testutil.WaitLong, testutil.IntervalMedium) cancelFunc() <-serverErr