mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
Connection logs now record how a connection was made (`connection_method`) separately from which app made it (`app_name`). Storage, the HTTP API, search filters, and the UI move to the new fields. Legacy agent reports and the deprecated `type` field keep working. - Add `connection_method` (`ssh`, `reconnecting_pty`, `workspace_app`, `port_forwarding`, `tunnel`) and optional `app_name` and `app_display_name` to `GET /api/v2/connectionlog`. Deprecate `type` and make it a plain string in the API and the Go SDK. - Add `method:` and repeatable `app:` search filters. `type:` keeps its old results but can't be combined with the new filters. - Replace the Type menu with a Method menu, show the app next to the method in each row, and warn when a filter uses `type:`. - Change the Active SSH connections preset to `method:ssh`, so it also includes SSH sessions from VS Code and JetBrains apps. - Generate `codersdk.ConnectionLogMethod` from the database enum with `typegen dbenums`. - Migration 000617 rewrites `connection_logs` under an ACCESS EXCLUSIVE lock, which takes about 50 seconds for 10 million rows. `vscode` and `jetbrains` rows become `ssh` rows with that app. Rolling back folds apps into the old types and loses per-app names. - Document the new filters and how to replace each `type:` value. Closes #27412
146 lines
5.0 KiB
Go
146 lines
5.0 KiB
Go
package codersdk
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/netip"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type ConnectionLog struct {
|
|
ID uuid.UUID `json:"id" format:"uuid"`
|
|
ConnectTime time.Time `json:"connect_time" format:"date-time"`
|
|
Organization MinimalOrganization `json:"organization"`
|
|
WorkspaceOwnerID uuid.UUID `json:"workspace_owner_id" format:"uuid"`
|
|
WorkspaceOwnerUsername string `json:"workspace_owner_username"`
|
|
WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"`
|
|
WorkspaceName string `json:"workspace_name"`
|
|
AgentName string `json:"agent_name"`
|
|
IP *netip.Addr `json:"ip,omitempty"`
|
|
// Deprecated: Use ConnectionMethod and AppName.
|
|
Type string `json:"type"`
|
|
ConnectionMethod ConnectionLogMethod `json:"connection_method"`
|
|
// AppName is the name of the connecting application, when known. The
|
|
// client reports it, so it is a normalized label and not a guarantee.
|
|
// Workspace app slugs and forwarded ports are reported in WebInfo.
|
|
AppName string `json:"app_name,omitempty"`
|
|
// AppDisplayName is the registry display name for AppName, or AppName
|
|
// itself when the app is unregistered.
|
|
AppDisplayName string `json:"app_display_name,omitempty"`
|
|
|
|
// WebInfo is set for connections that coderd records.
|
|
WebInfo *ConnectionLogWebInfo `json:"web_info,omitempty"`
|
|
|
|
// SSHInfo is set for connections that agents report.
|
|
SSHInfo *ConnectionLogSSHInfo `json:"ssh_info,omitempty"`
|
|
}
|
|
|
|
// ConnectionType is a connection method with SSH split by app family. It backs
|
|
// the legacy `type` filter. Remove it when that filter and ConnectionLog.Type
|
|
// are removed.
|
|
type ConnectionType string
|
|
|
|
const (
|
|
ConnectionTypeSSH ConnectionType = "ssh"
|
|
ConnectionTypeVSCode ConnectionType = "vscode"
|
|
ConnectionTypeJetBrains ConnectionType = "jetbrains"
|
|
ConnectionTypeReconnectingPTY ConnectionType = "reconnecting_pty"
|
|
ConnectionTypeWorkspaceApp ConnectionType = "workspace_app"
|
|
ConnectionTypePortForwarding ConnectionType = "port_forwarding"
|
|
// ConnectionTypeTunnel records accepted and denied tailnet tunnel
|
|
// requests made by authenticated users.
|
|
ConnectionTypeTunnel ConnectionType = "tunnel"
|
|
)
|
|
|
|
func (t ConnectionType) Valid() bool {
|
|
switch t {
|
|
case ConnectionTypeSSH, ConnectionTypeVSCode, ConnectionTypeJetBrains,
|
|
ConnectionTypeReconnectingPTY, ConnectionTypeWorkspaceApp,
|
|
ConnectionTypePortForwarding, ConnectionTypeTunnel:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// ConnectionLogStatus is the status of a connection log entry.
|
|
// It's the argument to the `status` filter when fetching connection logs.
|
|
type ConnectionLogStatus string
|
|
|
|
const (
|
|
ConnectionLogStatusOngoing ConnectionLogStatus = "ongoing"
|
|
ConnectionLogStatusCompleted ConnectionLogStatus = "completed"
|
|
)
|
|
|
|
func (s ConnectionLogStatus) Valid() bool {
|
|
switch s {
|
|
case ConnectionLogStatusOngoing, ConnectionLogStatusCompleted:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
type ConnectionLogWebInfo struct {
|
|
UserAgent string `json:"user_agent"`
|
|
// User is omitted if the connection event was unauthenticated.
|
|
User *User `json:"user"`
|
|
SlugOrPort string `json:"slug_or_port"`
|
|
// StatusCode is the HTTP status code or tunnel authorization outcome.
|
|
StatusCode int32 `json:"status_code"`
|
|
}
|
|
|
|
type ConnectionLogSSHInfo struct {
|
|
ConnectionID uuid.UUID `json:"connection_id" format:"uuid"`
|
|
// DisconnectTime is omitted if a disconnect event with the same connection ID
|
|
// has not yet been seen.
|
|
DisconnectTime *time.Time `json:"disconnect_time,omitempty" format:"date-time"`
|
|
// DisconnectReason is omitted if a disconnect event with the same connection ID
|
|
// has not yet been seen.
|
|
DisconnectReason string `json:"disconnect_reason,omitempty"`
|
|
// ExitCode is the exit code of the SSH session. It is omitted if a
|
|
// disconnect event with the same connection ID has not yet been seen.
|
|
ExitCode *int32 `json:"exit_code,omitempty"`
|
|
}
|
|
|
|
type ConnectionLogsRequest struct {
|
|
SearchQuery string `json:"q,omitempty"`
|
|
Pagination
|
|
}
|
|
|
|
type ConnectionLogResponse struct {
|
|
ConnectionLogs []ConnectionLog `json:"connection_logs"`
|
|
Count int64 `json:"count"`
|
|
CountCap int64 `json:"count_cap"`
|
|
}
|
|
|
|
func (c *Client) ConnectionLogs(ctx context.Context, req ConnectionLogsRequest) (ConnectionLogResponse, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, "/api/v2/connectionlog", nil, req.asRequestOption(), func(r *http.Request) {
|
|
q := r.URL.Query()
|
|
var params []string
|
|
if req.SearchQuery != "" {
|
|
params = append(params, req.SearchQuery)
|
|
}
|
|
q.Set("q", strings.Join(params, " "))
|
|
r.URL.RawQuery = q.Encode()
|
|
})
|
|
if err != nil {
|
|
return ConnectionLogResponse{}, err
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
return ConnectionLogResponse{}, ReadBodyAsError(res)
|
|
}
|
|
|
|
var logRes ConnectionLogResponse
|
|
err = ReadBodyAsJSON(res, &logRes)
|
|
if err != nil {
|
|
return ConnectionLogResponse{}, err
|
|
}
|
|
return logRes, nil
|
|
}
|