Documentation
¶
Overview ¶
Package devconsole serves development-only JSON APIs under /_dev/ for a local console (ADR-0065): what the app wired, its routes, the environment variables it read (secrets only as set or unset), recent requests and log records with live streams, captured email, migration state and recent job runs.
The console exposes an app's internals, so every request must pass three checks before anything else runs:
- the Host header names localhost, 127.0.0.1 or [::1] with the port the request arrived on, which defeats DNS rebinding: a page on another site that rebinds its own name to 127.0.0.1 still sends its own name;
- the connection comes from a loopback address, so an app listening on every interface doesn't serve the console to its network;
- an Authorization: Bearer header carries the console token (at least MinTokenLength characters; orb dev generates 256 bits per run), compared in constant time.
Responses never carry CORS headers, and all say Cache-Control: no-store. The app decides whether the console exists at all: gorbital apps mount it only when APP_ENV is development and DEV_CONSOLE_TOKEN is set, and refuse to start in production with the token set.
logs, _ := devconsole.NewLogs(devconsole.DefaultMaxLogs) tel, _ := telemetry.Setup(ctx, name, version, telemetry.WithLogTee(logs.Handler())) console, _ := devconsole.New(token, devconsole.WithLogs(logs), devconsole.WithSources(sources)) handler = console.Mount(handler, logger)
Stability: stable (ADR-0015, ADR-0065). The Go API follows the stability promise; the development-only /_dev responses are described by the OpenAPI document served at /_dev/openapi.json (OpenAPI) and aren't API: fields may be added or change between releases.
Index ¶
- Constants
- Variables
- func CheckToken(token string) error
- func LooksSecret(name string) bool
- func MailpitSource(baseURL string, client *http.Client) (func(ctx context.Context) (Mail, error), error)
- func OpenAPI() []byte
- func RecordRoute(router http.Handler) http.Handler
- func SortRoutes(routes []Route)
- type Address
- type App
- type Attr
- type ConfigList
- type Console
- func (c *Console) Close()
- func (c *Console) Middleware() func(http.Handler) http.Handler
- func (c *Console) Mount(next http.Handler, logger *slog.Logger) http.Handler
- func (c *Console) Operator(prefix string, a actor.Actor, logger *slog.Logger) func(http.Handler) http.Handler
- func (c *Console) RecordRequest(r Request)
- type EnvKey
- type EnvKeys
- type Flag
- type Index
- type Job
- type JobRun
- type JobRunList
- type Library
- type Log
- type LogList
- type Logs
- type Mail
- type MailPreview
- type MailPreviewList
- type MailPreviewMessage
- type MailPreviewSent
- type MailPreviewer
- type Message
- type Migrations
- type Option
- type Permission
- type PermissionCatalog
- type Request
- type RequestList
- type Role
- type Route
- type RouteList
- type Setting
- type Sources
- type StreamDropped
- type StreamEnd
Constants ¶
const ( // Prefix is the path every console endpoint starts with. Prefix = "/_dev/" // MinTokenLength is the shortest console token [New] accepts. MinTokenLength = 32 // MaxTokenLength is the longest console token [New] accepts. MaxTokenLength = 512 // DefaultMaxRequests is how many recent requests the console keeps // without [WithMaxRequests]. DefaultMaxRequests = 500 // DefaultMaxLogs is how many recent log records [NewLogs] callers // usually keep. DefaultMaxLogs = 1000 // DefaultMaxStreams is how many live streams the console serves at once // without [WithStreams]. DefaultMaxStreams = 8 // DefaultStreamDuration is how long a live stream lasts at most without // [WithStreams]. DefaultStreamDuration = 30 * time.Minute )
const ( RouteOpenAPI = "openapi" // an operation in the OpenAPI document RouteHandler = "handler" // a plain http.Handler outside the document )
Sources of a Route.
Variables ¶
var ( // ErrInvalidToken reports a console token shorter than // [MinTokenLength], longer than [MaxTokenLength], or holding characters // other than visible ASCII. ErrInvalidToken = fmt.Errorf("devconsole: the token must be %d to %d visible ASCII characters", MinTokenLength, MaxTokenLength) // such as Mailpit: the endpoint answers 503. ErrUnavailable = errors.New("devconsole: unavailable") // ErrStreamsClosed is the reason streams end when [Console.Close] is // called. ErrStreamsClosed = errors.New("devconsole: closed") )
Errors of the console.
var ErrUnknownPreview = errors.New("no such email preview")
ErrUnknownPreview reports a name Previews doesn't have.
Functions ¶
func CheckToken ¶
CheckToken reports whether token is acceptable as a console token.
func LooksSecret ¶
LooksSecret reports whether a variable's name suggests a secret value, such as GITHUB_CLIENT_SECRET, SMTP_PASSWORD or AUTH_ENCRYPTION_KEYS.
func MailpitSource ¶
func MailpitSource(baseURL string, client *http.Client) (func(ctx context.Context) (Mail, error), error)
MailpitSource returns a Sources.Mail reading the newest messages from the Mailpit web interface at baseURL, such as http://127.0.0.1:8025, with client (http.DefaultClient when nil). An unreachable Mailpit is reported with ErrUnavailable.
func OpenAPI ¶
func OpenAPI() []byte
OpenAPI returns the OpenAPI 3.1 document describing the console's endpoints and response shapes, served at GET /_dev/openapi.json. It is separate from the app's own document, which never lists /_dev/.
func RecordRoute ¶
RecordRoute wraps the application's router so Console.Middleware learns the route pattern it matched. Without the middleware it does nothing.
Types ¶
type App ¶
type App struct {
Name string `json:"name"`
Version string `json:"version"`
Commit string `json:"commit,omitempty"`
GoVersion string `json:"go_version"`
// Env is APP_ENV.
Env string `json:"env"`
// Libraries are the gorbital library modules linked into the binary.
Libraries []Library `json:"libraries"`
// Modules are the API's areas: the OpenAPI document's tags.
Modules []string `json:"modules"`
// Jobs, Settings, Flags and Permissions are empty in apps without them.
Jobs []Job `json:"jobs"`
Settings []Setting `json:"settings"`
Flags []Flag `json:"flags"`
Permissions []PermissionCatalog `json:"permissions"`
}
App describes the running app: GET /_dev/app.
type ConfigList ¶
type ConfigList struct {
Variables []EnvKey `json:"variables"`
}
ConfigList is the environment variables the app read: GET /_dev/config.
type Console ¶
type Console struct {
// contains filtered or unexported fields
}
Console serves the development console's APIs. It is safe for concurrent use. A nil *Console serves nothing: Console.Mount returns the handler unchanged, and its other methods do nothing, so apps can hold a nil console when it is off.
func New ¶
New returns a console that accepts token. It returns ErrInvalidToken for a token CheckToken refuses.
func (*Console) Close ¶
func (c *Console) Close()
Close ends every live stream and refuses new ones, for a shutting-down app: HTTP servers wait for long-lived responses.
func (*Console) Middleware ¶
Middleware records every request with Console.RecordRequest, for apps without a request collector. Install it early, after panic recovery, and wrap the router with RecordRoute so the route pattern is known even when middleware in between copies the request. A nil console returns a middleware that does nothing.
func (*Console) Mount ¶
Mount returns a handler serving the console for paths under Prefix (and /_dev itself) and next for every other path. The console runs before next's middleware: its requests aren't recorded, logged as access or subject to CORS, and they reach no application handler. logger receives refused requests and source errors.
func (*Console) Operator ¶
func (c *Console) Operator(prefix string, a actor.Actor, logger *slog.Logger) func(http.Handler) http.Handler
Operator returns middleware that lets a request under prefix (such as "/ops/") act as a, the development operator, when it carries the console token as Authorization: Bearer and passes the console's Host and loopback checks (ADR-0066). Every other request reaches next unchanged, so the app's own authentication still applies to it; a request with the token but a wrong Host or a remote peer is logged as refused and continues without the operator.
The Dev Portal uses it to call the app's operations APIs in development without a signed-in administrator: orb dev adds the token to proxied requests, and the app grants a the permissions it chooses, typically its platform administrator role's. Audit events record a as the actor. A nil console returns middleware that does nothing, so apps can wire it unconditionally: without DEV_CONSOLE_TOKEN there is no console, and in production the token is refused at startup.
func (*Console) RecordRequest ¶
RecordRequest adds a finished request to the console's recent requests and live streams. Apps with a request collector subscribe it (see gorbital.dev/modules/observability's Collector.Subscribe); others use Console.Middleware. It never blocks: a slow stream client misses requests instead.
type EnvKey ¶
type EnvKey struct {
Name string `json:"name"`
// Secret reports a variable read as a secret, or whose name looks like
// one; its value is never kept.
Secret bool `json:"secret"`
// Set reports a non-empty value.
Set bool `json:"set"`
// Value is the value of a variable that isn't secret, with the user
// information and query of URLs replaced by "[redacted]". Empty for
// secrets.
Value string `json:"value,omitempty"`
}
EnvKey is an environment variable the app read at startup.
type EnvKeys ¶
type EnvKeys struct {
// contains filtered or unexported fields
}
EnvKeys records the environment variables an app reads while loading its configuration, so the console can list them without reading the environment itself. Secrets are recorded as set or unset only. It is safe for concurrent use; the zero value is ready.
func (*EnvKeys) Read ¶
Read records a variable read as plain configuration and its value. A name that looks secret (LooksSecret) is recorded like EnvKeys.ReadSecret.
func (*EnvKeys) ReadSecret ¶
ReadSecret records a variable read as a secret: only whether it is set.
type Flag ¶
type Flag struct {
Key string `json:"key"`
Group string `json:"group"`
Description string `json:"description"`
Client bool `json:"client"`
Enabled bool `json:"enabled"`
Default bool `json:"default"`
// Percentage is the rollout percentage; nil without a rollout.
Percentage *int `json:"percentage"`
// Targets counts the organisation and user IDs in allow and deny lists.
Targets int `json:"targets"`
Modified bool `json:"modified"`
}
Flag is a declared feature flag and its current state, with target lists summarised as counts.
type Index ¶
type Index struct {
// Endpoints are the paths this app serves, sorted.
Endpoints []string `json:"endpoints"`
}
Index lists the console's endpoints: GET /_dev/.
type Job ¶
type Job struct {
Name string `json:"name"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
// Schedule is a cron expression or descriptor; empty for on-demand jobs.
Schedule string `json:"schedule"`
Modified bool `json:"modified"`
NextRunAt *time.Time `json:"next_run_at,omitempty"`
}
Job is a declared job definition and its effective configuration.
type JobRun ¶
type JobRun struct {
ID int64 `json:"id"`
Kind string `json:"kind"`
Queue string `json:"queue"`
State string `json:"state"`
Attempt int `json:"attempt"`
MaxAttempts int `json:"max_attempts"`
CreatedAt time.Time `json:"created_at"`
ScheduledAt time.Time `json:"scheduled_at"`
AttemptedAt *time.Time `json:"attempted_at,omitempty"`
FinalizedAt *time.Time `json:"finalized_at,omitempty"`
// Errors are the failed attempts' messages, oldest first.
Errors []string `json:"errors"`
RequestID string `json:"request_id,omitempty"`
}
JobRun is a job, without its arguments: GET /_dev/jobs.
type JobRunList ¶
type JobRunList struct {
Runs []JobRun `json:"runs"`
}
JobRunList is the most recent job runs, newest first: GET /_dev/jobs.
type Library ¶
type Library struct {
Path string `json:"path"`
Version string `json:"version"`
// Replaced reports a module replaced by a local directory or another
// module, as in a gorbital checkout.
Replaced bool `json:"replaced,omitempty"`
}
Library is a linked Go module.
type Log ¶
type Log struct {
Time time.Time `json:"time"`
Level string `json:"level"`
Message string `json:"message"`
// Attrs are the record's attributes in order, groups flattened into
// dotted keys ("request.id"), values as text. DroppedAttrs counts those
// left out: past 50, or past 8 KiB for the whole record.
Attrs []Attr `json:"attrs"`
DroppedAttrs int `json:"dropped_attrs,omitempty"`
}
Log is one log record.
type LogList ¶
type LogList struct {
// Max is how many records the console keeps.
Max int `json:"max"`
Logs []Log `json:"logs"`
}
LogList is the most recent log records, newest first: GET /_dev/logs.
type Logs ¶
type Logs struct {
// contains filtered or unexported fields
}
Logs keeps an app's most recent log records at info level and above, for the console. Its Logs.Handler receives them, usually as a tee of the app's logger (gorbital.dev/modules/telemetry's WithLogTee). It is safe for concurrent use.
type Mail ¶
type Mail struct {
// WebURL is Mailpit's web interface, where messages can be read.
WebURL string `json:"web_url"`
// Total counts every captured message; Messages holds the newest 50.
Total int `json:"total"`
Messages []Message `json:"messages"`
}
Mail is the most recent email captured by Mailpit: GET /_dev/mail.
type MailPreview ¶
type MailPreview struct {
// Name identifies the preview: letters, digits, dots, hyphens and
// underscores, such as auth.verification_code.
Name string `json:"name"`
// Description says when the app sends it.
Description string `json:"description"`
// Category is the message's tag, when the app sets one.
Category string `json:"category,omitempty"`
}
MailPreview is one of the app's emails rendered with sample data, for the Dev Portal's template preview (ADR-0074): GET /_dev/mail/previews lists them, GET /_dev/mail/preview?name= renders one, and POST /_dev/mail/preview/send?name=&to= sends it through the app's mailer to the inbox.
type MailPreviewList ¶
type MailPreviewList struct {
Previews []MailPreview `json:"previews"`
}
MailPreviewList is GET /_dev/mail/previews.
type MailPreviewMessage ¶
type MailPreviewMessage struct {
MailPreview
Subject string `json:"subject"`
Text string `json:"text"`
HTML string `json:"html"`
// To is the sample recipient the message was rendered for.
To string `json:"to"`
}
MailPreviewMessage is a rendered preview: GET /_dev/mail/preview.
type MailPreviewSent ¶
type MailPreviewSent struct {
Name string `json:"name"`
To string `json:"to"`
Sent bool `json:"sent"`
}
MailPreviewSent is POST /_dev/mail/preview/send.
type MailPreviewer ¶
type MailPreviewer struct {
Previews []MailPreview
Build func(ctx context.Context, name, to string) (gmail.Message, error)
Send func(ctx context.Context, m gmail.Message) error
}
MailPreviewer renders the app's email previews. Build returns the message for to with sample data; the console lists Previews and sends what Build returns through Send (the app's mailer, so the message takes the same path as a real one).
type Message ¶
type Message struct {
ID string `json:"id"`
From Address `json:"from"`
To []Address `json:"to"`
Subject string `json:"subject"`
Snippet string `json:"snippet"`
Created time.Time `json:"created"`
Size int `json:"size"`
Attachments int `json:"attachments"`
Read bool `json:"read"`
}
Message is a captured email's summary.
type Migrations ¶
type Migrations struct {
// Current is the highest applied version, Latest the newest migration
// file's, Pending the files not applied yet.
Current int64 `json:"current"`
Latest int64 `json:"latest"`
Pending int `json:"pending"`
}
Migrations is the database's migration state: GET /_dev/migrations.
type Option ¶
type Option func(*options)
Option configures a Console.
func WithAddr ¶
WithAddr sets the address the app listens on, whose port the Host header must name when the connection's local address isn't known, such as in tests calling the handler directly. The local address of a real connection always wins.
func WithMaxRequests ¶
WithMaxRequests keeps the n most recent requests (default DefaultMaxRequests).
func WithStreams ¶
WithStreams serves at most n live streams at once, each for at most duration (defaults DefaultMaxStreams and DefaultStreamDuration).
type Permission ¶
Permission is a declared permission.
type PermissionCatalog ¶
type PermissionCatalog struct {
Name string `json:"name"`
Permissions []Permission `json:"permissions"`
Roles []Role `json:"roles"`
}
PermissionCatalog is a permission catalog: its permissions and the roles granting them.
type Request ¶
type Request struct {
// Time is when the request finished.
Time time.Time `json:"time"`
Method string `json:"method"`
// Route is the pattern the router matched, such as "/v1/projects/{id}";
// empty when no route matched.
Route string `json:"route"`
// Path is the request path, without the query string.
Path string `json:"path"`
Status int `json:"status"`
DurationMS float64 `json:"duration_ms"`
RequestID string `json:"request_id,omitempty"`
TraceID string `json:"trace_id,omitempty"`
}
Request is one finished HTTP request. It never holds the query string, headers, cookies or bodies.
type RequestList ¶
type RequestList struct {
// Max is how many requests the console keeps.
Max int `json:"max"`
Requests []Request `json:"requests"`
}
RequestList is the most recent requests, newest first: GET /_dev/requests.
type Role ¶
type Role struct {
Name string `json:"name"`
Description string `json:"description"`
Permissions []string `json:"permissions"`
}
Role is a declared role.
type Route ¶
type Route struct {
Method string `json:"method"`
// Path is the route's pattern, such as "/v1/projects/{id}".
Path string `json:"path"`
OperationID string `json:"operation_id,omitempty"`
Summary string `json:"summary,omitempty"`
Tags []string `json:"tags"`
// Secured reports an operation that declares a security requirement.
Secured bool `json:"secured"`
// Source is [RouteOpenAPI] or [RouteHandler].
Source string `json:"source"`
}
Route is an HTTP route: GET /_dev/routes.
func RoutesFromOpenAPI ¶
RoutesFromOpenAPI returns the operations of an OpenAPI 3 document in JSON, sorted by path and method.
type RouteList ¶
type RouteList struct {
Routes []Route `json:"routes"`
}
RouteList is the app's routes: GET /_dev/routes.
type Setting ¶
type Setting struct {
Key string `json:"key"`
Group string `json:"group"`
Description string `json:"description"`
Kind string `json:"kind"`
Value json.RawMessage `json:"value"`
Default json.RawMessage `json:"default"`
Modified bool `json:"modified"`
OrgOverridable bool `json:"org_overridable"`
}
Setting is a declared runtime setting and its current value.
type Sources ¶
type Sources struct {
// App describes the running app (GET /_dev/app).
App func(ctx context.Context) (App, error)
// Routes lists the app's HTTP routes (GET /_dev/routes); see
// [RoutesFromOpenAPI].
Routes func(ctx context.Context) ([]Route, error)
// Config lists the environment variables the app read (GET
// /_dev/config); see [EnvKeys].
Config func(ctx context.Context) ([]EnvKey, error)
// Mail lists captured email (GET /_dev/mail); see [MailpitSource].
Mail func(ctx context.Context) (Mail, error)
// Migrations reports the database's migration state (GET
// /_dev/migrations).
Migrations func(ctx context.Context) (Migrations, error)
// Jobs lists recent job runs (GET /_dev/jobs).
Jobs func(ctx context.Context) ([]JobRun, error)
// MailPreviews renders the app's emails with sample data (GET
// /_dev/mail/previews, /_dev/mail/preview, POST /_dev/mail/preview/send);
// see [MailPreviewer].
MailPreviews *MailPreviewer
}
Sources provide the console's app-specific sections. A nil source leaves its endpoint out: it answers 404, and the index doesn't list it.
type StreamDropped ¶
type StreamDropped struct {
Count int `json:"count"`
}
StreamDropped is the data of a "dropped" event: events the client was too slow to receive.