expr

package
v11.1.4-modfix Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 20, 2024 License: AGPL-3.0 Imports: 43 Imported by: 0

Documentation

Index

Constants

View Source
const DatasourceID = -100

DatasourceID is the fake datasource id used in requests to identify it as an expression command.

View Source
const DatasourceType = "__expr__"

DatasourceType is the string constant used as the datasource when the property is in Datasource.Type. Type in requests is used to identify what type of data source plugin the request belongs to.

View Source
const DatasourceUID = DatasourceType

DatasourceUID is the string constant used as the datasource name in requests to identify it as an expression command when use in Datasource.UID.

View Source
const (

	// DatasourceUID is the string constant used as the datasource name in requests
	// to identify it as an expression command when use in Datasource.UID.
	MLDatasourceUID = "__ml__"
)
View Source
const OldDatasourceUID = "-100"

OldDatasourceUID is the datasource uid used in requests to identify it as an expression command. It goes with the query root level datasourceUID property. It was accidentally set to the Id and is now kept for backwards compatibility. The newer Datasource.UID property should be used instead and should be set to "__expr__".

Variables

View Source
var ConversionError = errutil.BadRequest("sse.readDataError").MustTemplate(
	"[{{ .Public.refId }}] got error: {{ .Error }}",
	errutil.WithPublic(
		"failed to read data from from query {{ .Public.refId }}: {{ .Public.error }}",
	),
)
View Source
var DependencyError = errutil.NewBase(
	errutil.StatusBadRequest, "sse.dependencyError").MustTemplate(
	depErrStr,
	errutil.WithPublic(depErrStr))
View Source
var ErrSeriesMustBeWide = errors.New("input data must be a wide series")
View Source
var QueryError = errutil.BadRequest("sse.dataQueryError").MustTemplate(
	"failed to execute query [{{ .Public.refId }}]: {{ .Error }}",
	errutil.WithPublic(
		"failed to execute query [{{ .Public.refId }}]: {{ .Public.error }}",
	))
View Source
var UnexpectedNodeTypeError = errutil.NewBase(
	errutil.StatusBadRequest, "sse.unexpectedNodeType").MustTemplate(
	unexpectedNodeTypeErrString,
	errutil.WithPublic(unexpectedNodeTypeErrString))

Functions

func DataSourceModel

func DataSourceModel() *datasources.DataSource

Deprecated. Use DataSourceModelFromNodeType instead

func DataSourceModelFromNodeType

func DataSourceModelFromNodeType(kind NodeType) (*datasources.DataSource, error)

Create a datasources.DataSource struct from NodeType. Returns error if kind is TypeDatasourceNode or unknown one.

func FingerprintsToFrame

func FingerprintsToFrame(fingerprints Fingerprints) *data.Frame

FingerprintsToFrame converts Fingerprints to data.Frame.

func GetCommandsFromPipeline

func GetCommandsFromPipeline[T Command](pipeline DataPipeline) []T

GetCommandsFromPipeline traverses the pipeline and extracts all CMDNode commands that match the type

func IsDataSource

func IsDataSource(uid string) bool

IsDataSource checks if the uid points to an expression query

func IsHysteresisExpression

func IsHysteresisExpression(query map[string]any) bool

IsHysteresisExpression returns true if the raw model describes a hysteresis command: - field 'type' has value "threshold", - field 'conditions' is array of objects and has exactly one element - field 'conditions[0].unloadEvaluator is not nil

func IsSupportedThresholdFunc

func IsSupportedThresholdFunc(name string) bool

func MakeQueryError

func MakeQueryError(refID, datasourceUID string, err error) error

func QueryTypeDefinitionListJSON

func QueryTypeDefinitionListJSON() ([]byte, error)

func SetLoadedDimensionsToHysteresisCommand

func SetLoadedDimensionsToHysteresisCommand(query map[string]any, fingerprints Fingerprints) error

SetLoadedDimensionsToHysteresisCommand mutates the input map and sets field "conditions[0].loadedMetrics" with the data frame created from the provided fingerprints.

func WideToMany

func WideToMany(frame *data.Frame, fixSeries func(series mathexp.Series, valueField *data.Field)) ([]mathexp.Series, error)

WideToMany converts a data package wide type Frame to one or multiple Series. A series is created for each value type column of wide frame.

This might not be a good idea long term, but works now as an adapter/shim.

Types

type AbsoluteTimeRange

type AbsoluteTimeRange struct {
	From time.Time
	To   time.Time
}

func (AbsoluteTimeRange) AbsoluteTime

func (r AbsoluteTimeRange) AbsoluteTime(_ time.Time) backend.TimeRange

type CMDNode

type CMDNode struct {
	CMDType CommandType
	Command Command
	// contains filtered or unexported fields
}

CMDNode is a DPNode that holds an expression command.

func (*CMDNode) Execute

func (gn *CMDNode) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, s *Service) (mathexp.Results, error)

Execute runs the node and adds the results to vars. If the node requires other nodes they must have already been executed and their results must already by in vars.

func (*CMDNode) ID

func (b *CMDNode) ID() int64

ID returns the id of the node so it can fulfill the gonum's graph Node interface.

func (*CMDNode) NeedsVars

func (gn *CMDNode) NeedsVars() []string

func (*CMDNode) NodeType

func (gn *CMDNode) NodeType() NodeType

NodeType returns the data pipeline node type.

func (*CMDNode) RefID

func (b *CMDNode) RefID() string

RefID returns the refId of the node.

func (*CMDNode) String

func (b *CMDNode) String() string

String returns a string representation of the node. In particular for %v formatting in error messages.

type ClassicQuery

type ClassicQuery struct {
	Conditions []classic.ConditionJSON `json:"conditions"`
}

type Command

type Command interface {
	NeedsVars() []string
	Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error)
	Type() string
}

Command is an interface for all expression commands.

func UnmarshalThresholdCommand

func UnmarshalThresholdCommand(rn *rawNode, features featuremgmt.FeatureToggles) (Command, error)

UnmarshalResampleCommand creates a ResampleCMD from Grafana's frontend query.

type CommandType

type CommandType int

CommandType is the type of the expression command.

const (
	// TypeUnknown is the CMDType for an unrecognized expression type.
	TypeUnknown CommandType = iota
	// TypeMath is the CMDType for a math expression.
	TypeMath
	// TypeReduce is the CMDType for a reduction expression.
	TypeReduce
	// TypeResample is the CMDType for a resampling expression.
	TypeResample
	// TypeClassicConditions is the CMDType for the classic condition operation.
	TypeClassicConditions
	// TypeThreshold is the CMDType for checking if a threshold has been crossed
	TypeThreshold
	// TypeSQL is the CMDType for running SQL expressions
	TypeSQL
)

func GetExpressionCommandType

func GetExpressionCommandType(rawQuery map[string]any) (c CommandType, err error)

func ParseCommandType

func ParseCommandType(s string) (CommandType, error)

ParseCommandType returns a CommandType from its string representation.

func (CommandType) String

func (gt CommandType) String() string

type ConditionEvalJSON

type ConditionEvalJSON struct {
	Params []float64     `json:"params"`
	Type   ThresholdType `json:"type"` // e.g. "gt"
}

type DSNode

type DSNode struct {
	// contains filtered or unexported fields
}

DSNode is a DPNode that holds a datasource request.

func (*DSNode) Execute

func (dn *DSNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s *Service) (r mathexp.Results, e error)

Execute runs the node and adds the results to vars. If the node requires other nodes they must have already been executed and their results must already by in vars.

func (*DSNode) ID

func (b *DSNode) ID() int64

ID returns the id of the node so it can fulfill the gonum's graph Node interface.

func (*DSNode) NeedsVars

func (dn *DSNode) NeedsVars() []string

NodeType returns the data pipeline node type.

func (*DSNode) NodeType

func (dn *DSNode) NodeType() NodeType

NodeType returns the data pipeline node type.

func (*DSNode) RefID

func (b *DSNode) RefID() string

RefID returns the refId of the node.

func (*DSNode) String

func (dn *DSNode) String() string

type DataPipeline

type DataPipeline []Node

DataPipeline is an ordered set of nodes returned from DPGraph processing.

func (*DataPipeline) GetCommandTypes

func (dp *DataPipeline) GetCommandTypes() []string

GetCommandTypes returns a sorted unique list of all server-side expression commands used in the pipeline.

func (*DataPipeline) GetDatasourceTypes

func (dp *DataPipeline) GetDatasourceTypes() []string

GetDatasourceTypes returns an unique list of data source types used in the query. Machine learning node is encoded as `ml_<type>`, e.g. ml_outlier

type ExecutableNode

type ExecutableNode interface {
	Node
	Execute(ctx context.Context, now time.Time, vars mathexp.Vars, s *Service) (mathexp.Results, error)
}

type ExpressionQuery

type ExpressionQuery struct {
	GraphID   int64     `json:"id,omitempty"`
	RefID     string    `json:"refId"`
	QueryType QueryType `json:"type"`

	// The typed query parameters
	Properties any `json:"properties"`

	// Hidden in debug JSON
	Command Command `json:"-"`
}

Once we are comfortable with the parsing logic, this struct will be merged/replace the existing Query struct in grafana/pkg/expr/transform.go

func (ExpressionQuery) ID

func (q ExpressionQuery) ID() int64

ID is used to identify nodes in the directed graph

type ExpressionQueryReader

type ExpressionQueryReader struct {
	// contains filtered or unexported fields
}

func NewExpressionQueryReader

func NewExpressionQueryReader(features featuremgmt.FeatureToggles) *ExpressionQueryReader

func (*ExpressionQueryReader) ReadQuery

func (h *ExpressionQueryReader) ReadQuery(

	common data.DataQuery,

	iter *jsoniter.Iterator,
) (eq ExpressionQuery, err error)

nolint:gocyclo

type Fingerprints

type Fingerprints map[data.Fingerprint]struct{}

func FingerprintsFromFrame

func FingerprintsFromFrame(frame *data.Frame) (Fingerprints, error)

FingerprintsFromFrame converts data.Frame to Fingerprints. The input data frame must have a single field of uint64 type. Returns error if the input data frame has invalid format

type HysteresisCommand

type HysteresisCommand struct {
	RefID                  string
	ReferenceVar           string
	LoadingThresholdFunc   ThresholdCommand
	UnloadingThresholdFunc ThresholdCommand
	LoadedDimensions       Fingerprints
}

HysteresisCommand is a special case of ThresholdCommand that encapsulates two thresholds that are applied depending on the results of the previous evaluations: - first threshold - "loading", is used when the metric is determined as not loaded, i.e. it does not exist in the data provided by the reader. - second threshold - "unloading", is used when the metric is determined as loaded. To determine whether a metric is loaded, the command uses LoadedDimensions that is supposed to contain data.Fingerprint of the metrics that were loaded during the previous evaluation. The result of the execution of the command is the same as ThresholdCommand: 0 or 1 for each metric.

func NewHysteresisCommand

func NewHysteresisCommand(refID string, referenceVar string, loadCondition ThresholdCommand, unloadCondition ThresholdCommand, l Fingerprints) (*HysteresisCommand, error)

func (*HysteresisCommand) Execute

func (h *HysteresisCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error)

func (*HysteresisCommand) NeedsVars

func (h *HysteresisCommand) NeedsVars() []string

func (HysteresisCommand) Type

func (h HysteresisCommand) Type() string

type MLNode

type MLNode struct {
	TimeRange TimeRange
	// contains filtered or unexported fields
}

MLNode is a node of expression tree that evaluates the expression by sending the payload to Machine Learning back-end. See ml.UnmarshalCommand for supported commands.

func (*MLNode) Execute

func (m *MLNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s *Service) (r mathexp.Results, e error)

Execute initializes plugin API client, executes a ml.Command and then converts the result of the execution. Returns non-empty mathexp.Results if evaluation was successful. Returns QueryError if command execution failed

func (*MLNode) ID

func (b *MLNode) ID() int64

ID returns the id of the node so it can fulfill the gonum's graph Node interface.

func (*MLNode) NeedsVars

func (m *MLNode) NeedsVars() []string

NodeType returns the data pipeline node type.

func (*MLNode) NodeType

func (m *MLNode) NodeType() NodeType

NodeType returns the data pipeline node type.

func (*MLNode) RefID

func (b *MLNode) RefID() string

RefID returns the refId of the node.

func (*MLNode) String

func (b *MLNode) String() string

String returns a string representation of the node. In particular for %v formatting in error messages.

type MathCommand

type MathCommand struct {
	RawExpression string
	Expression    *mathexp.Expr
	// contains filtered or unexported fields
}

MathCommand is a command for a math expression such as "1 + $GA / 2"

func NewMathCommand

func NewMathCommand(refID, expr string) (*MathCommand, error)

NewMathCommand creates a new MathCommand. It will return an error if there is an error parsing expr.

func UnmarshalMathCommand

func UnmarshalMathCommand(rn *rawNode) (*MathCommand, error)

UnmarshalMathCommand creates a MathCommand from Grafana's frontend query.

func (*MathCommand) Execute

func (gm *MathCommand) Execute(ctx context.Context, _ time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error)

Execute runs the command and returns the results or an error if the command failed to execute.

func (*MathCommand) NeedsVars

func (gm *MathCommand) NeedsVars() []string

NeedsVars returns the variable names (refIds) that are dependencies to execute the command and allows the command to fulfill the Command interface.

func (*MathCommand) Type

func (gm *MathCommand) Type() string

type MathQuery

type MathQuery struct {
	// General math expression
	Expression string `json:"expression" jsonschema:"minLength=1,example=$A + 1,example=$A/$B"`
}

type Node

type Node interface {
	ID() int64 // ID() allows the gonum graph node interface to be fulfilled
	NodeType() NodeType
	RefID() string
	String() string
	NeedsVars() []string
}

Node is a node in a Data Pipeline. Node is either a expression command or a datasource query.

type NodeType

type NodeType int

NodeType is the type of a DPNode. Currently either a expression command or datasource query.

const (
	// TypeCMDNode is a NodeType for expression commands.
	TypeCMDNode NodeType = iota
	// TypeDatasourceNode is a NodeType for datasource queries.
	TypeDatasourceNode
	// TypeMLNode is a NodeType for Machine Learning queries.
	TypeMLNode
)

func NodeTypeFromDatasourceUID

func NodeTypeFromDatasourceUID(uid string) NodeType

NodeTypeFromDatasourceUID returns NodeType depending on the UID of the data source: TypeCMDNode if UID is DatasourceUID or OldDatasourceUID, and TypeDatasourceNode otherwise.

func (NodeType) String

func (nt NodeType) String() string

type Query

type Query struct {
	RefID         string
	TimeRange     TimeRange
	DataSource    *datasources.DataSource `json:"datasource"`
	JSON          json.RawMessage
	Interval      time.Duration
	QueryType     string
	MaxDataPoints int64
}

Query is like plugins.DataSubQuery, but with a a time range, and only the UID for the data source. Also interval is a time.Duration.

type QueryType

type QueryType string

Supported expression types +enum

const (
	// Apply a mathematical expression to results
	QueryTypeMath QueryType = "math"

	// Reduce query results
	QueryTypeReduce QueryType = "reduce"

	// Resample query results
	QueryTypeResample QueryType = "resample"

	// Classic query
	QueryTypeClassic QueryType = "classic_conditions"

	// Threshold
	QueryTypeThreshold QueryType = "threshold"

	// SQL query via DuckDB
	QueryTypeSQL QueryType = "sql"
)

type ReduceCommand

type ReduceCommand struct {
	Reducer     mathexp.ReducerID
	VarToReduce string
	// contains filtered or unexported fields
}

ReduceCommand is an expression command for reduction of a timeseries such as a min, mean, or max.

func NewReduceCommand

func NewReduceCommand(refID string, reducer mathexp.ReducerID, varToReduce string, mapper mathexp.ReduceMapper) (*ReduceCommand, error)

NewReduceCommand creates a new ReduceCMD.

func UnmarshalReduceCommand

func UnmarshalReduceCommand(rn *rawNode) (*ReduceCommand, error)

UnmarshalReduceCommand creates a MathCMD from Grafana's frontend query.

func (*ReduceCommand) Execute

func (gr *ReduceCommand) Execute(ctx context.Context, _ time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error)

Execute runs the command and returns the results or an error if the command failed to execute.

func (*ReduceCommand) NeedsVars

func (gr *ReduceCommand) NeedsVars() []string

NeedsVars returns the variable names (refIds) that are dependencies to execute the command and allows the command to fulfill the Command interface.

func (*ReduceCommand) Type

func (gr *ReduceCommand) Type() string

type ReduceMode

type ReduceMode string

Non-Number behavior mode +enum

const (
	// Drop non-numbers
	ReduceModeDrop ReduceMode = "dropNN"

	// Replace non-numbers
	ReduceModeReplace ReduceMode = "replaceNN"
)

type ReduceQuery

type ReduceQuery struct {
	// Reference to single query result
	Expression string `json:"expression" jsonschema:"minLength=1,example=$A"`

	// The reducer
	Reducer mathexp.ReducerID `json:"reducer"`

	// Reducer Options
	Settings *ReduceSettings `json:"settings,omitempty"`
}

type ReduceSettings

type ReduceSettings struct {
	// Non-number reduce behavior
	Mode ReduceMode `json:"mode"`

	// Only valid when mode is replace
	ReplaceWithValue *float64 `json:"replaceWithValue,omitempty"`
}

type RelativeTimeRange

type RelativeTimeRange struct {
	From time.Duration
	To   time.Duration
}

RelativeTimeRange is a time range relative to some absolute time.

func (RelativeTimeRange) AbsoluteTime

func (r RelativeTimeRange) AbsoluteTime(t time.Time) backend.TimeRange

type Request

type Request struct {
	Headers map[string]string
	Debug   bool
	OrgId   int64
	Queries []Query
	User    identity.Requester
}

Request is similar to plugins.DataQuery but with the Time Ranges is per Query.

type ResampleCommand

type ResampleCommand struct {
	Window        time.Duration
	VarToResample string
	Downsampler   mathexp.ReducerID
	Upsampler     mathexp.Upsampler
	TimeRange     TimeRange
	// contains filtered or unexported fields
}

ResampleCommand is an expression command for resampling of a timeseries.

func NewResampleCommand

func NewResampleCommand(refID, rawWindow, varToResample string, downsampler mathexp.ReducerID, upsampler mathexp.Upsampler, tr TimeRange) (*ResampleCommand, error)

NewResampleCommand creates a new ResampleCMD.

func UnmarshalResampleCommand

func UnmarshalResampleCommand(rn *rawNode) (*ResampleCommand, error)

UnmarshalResampleCommand creates a ResampleCMD from Grafana's frontend query.

func (*ResampleCommand) Execute

func (gr *ResampleCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error)

Execute runs the command and returns the results or an error if the command failed to execute.

func (*ResampleCommand) NeedsVars

func (gr *ResampleCommand) NeedsVars() []string

NeedsVars returns the variable names (refIds) that are dependencies to execute the command and allows the command to fulfill the Command interface.

func (*ResampleCommand) Type

func (gr *ResampleCommand) Type() string

type ResampleQuery

type ResampleQuery struct {
	// The math expression
	Expression string `json:"expression" jsonschema:"minLength=1,example=$A + 1,example=$A"`

	// The time duration
	Window string `json:"window" jsonschema:"minLength=1,example=1d,example=10m"`

	// The downsample function
	Downsampler mathexp.ReducerID `json:"downsampler"`

	// The upsample function
	Upsampler mathexp.Upsampler `json:"upsampler"`
}

QueryType = resample

type ResultConverter

type ResultConverter struct {
	Features featuremgmt.FeatureToggles
	Tracer   tracing.Tracer
}

func (*ResultConverter) Convert

func (c *ResultConverter) Convert(ctx context.Context,
	datasourceType string,
	frames data.Frames,
	allowLongFrames bool,
) (string, mathexp.Results, error)

type SQLCommand

type SQLCommand struct {
	// contains filtered or unexported fields
}

SQLCommand is an expression to run SQL over results

func NewSQLCommand

func NewSQLCommand(refID, rawSQL string) (*SQLCommand, error)

NewSQLCommand creates a new SQLCommand.

func UnmarshalSQLCommand

func UnmarshalSQLCommand(rn *rawNode) (*SQLCommand, error)

UnmarshalSQLCommand creates a SQLCommand from Grafana's frontend query.

func (*SQLCommand) Execute

func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error)

Execute runs the command and returns the results or an error if the command failed to execute.

func (*SQLCommand) NeedsVars

func (gr *SQLCommand) NeedsVars() []string

NeedsVars returns the variable names (refIds) that are dependencies to execute the command and allows the command to fulfill the Command interface.

func (*SQLCommand) Type

func (gr *SQLCommand) Type() string

type SQLExpression

type SQLExpression struct {
	Expression string `json:"expression" jsonschema:"minLength=1,example=SELECT * FROM A LIMIT 1"`
}

SQLQuery requires the sqlExpression feature flag

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service is service representation for expression handling.

func ProvideService

func ProvideService(cfg *setting.Cfg, pluginClient plugins.Client, pCtxProvider *plugincontext.Provider,
	features featuremgmt.FeatureToggles, registerer prometheus.Registerer, tracer tracing.Tracer) *Service

func (*Service) BuildPipeline

func (s *Service) BuildPipeline(req *Request) (DataPipeline, error)

BuildPipeline builds a pipeline from a request.

func (*Service) ExecutePipeline

func (s *Service) ExecutePipeline(ctx context.Context, now time.Time, pipeline DataPipeline) (*backend.QueryDataResponse, error)

ExecutePipeline executes an expression pipeline and returns all the results.

func (*Service) TransformData

func (s *Service) TransformData(ctx context.Context, now time.Time, req *Request) (r *backend.QueryDataResponse, err error)

TransformData takes Queries which are either expressions nodes or are datasource requests.

type ThresholdCommand

type ThresholdCommand struct {
	ReferenceVar  string
	RefID         string
	ThresholdFunc ThresholdType
	Invert        bool
	// contains filtered or unexported fields
}

func NewThresholdCommand

func NewThresholdCommand(refID, referenceVar string, thresholdFunc ThresholdType, conditions []float64) (*ThresholdCommand, error)

func (*ThresholdCommand) Execute

func (*ThresholdCommand) NeedsVars

func (tc *ThresholdCommand) NeedsVars() []string

NeedsVars returns the variable names (refIds) that are dependencies to execute the command and allows the command to fulfill the Command interface.

func (*ThresholdCommand) Type

func (tc *ThresholdCommand) Type() string

type ThresholdCommandConfig

type ThresholdCommandConfig struct {
	Expression string                   `json:"expression"`
	Conditions []ThresholdConditionJSON `json:"conditions"`
}

type ThresholdConditionJSON

type ThresholdConditionJSON struct {
	Evaluator        ConditionEvalJSON  `json:"evaluator"`
	UnloadEvaluator  *ConditionEvalJSON `json:"unloadEvaluator,omitempty"`
	LoadedDimensions *data.Frame        `json:"loadedDimensions,omitempty"`
}

type ThresholdQuery

type ThresholdQuery struct {
	// Reference to single query result
	Expression string `json:"expression" jsonschema:"minLength=1,example=$A"`

	// Threshold Conditions
	Conditions []ThresholdConditionJSON `json:"conditions"`
}

type ThresholdType

type ThresholdType string

+enum

const (
	ThresholdIsAbove        ThresholdType = "gt"
	ThresholdIsBelow        ThresholdType = "lt"
	ThresholdIsWithinRange  ThresholdType = "within_range"
	ThresholdIsOutsideRange ThresholdType = "outside_range"
)

type TimeRange

type TimeRange interface {
	AbsoluteTime(now time.Time) backend.TimeRange
}

TimeRange is a time.Time based TimeRange.

Directories

Path Synopsis
parse
Package parse builds parse trees for expressions as defined by expr.
Package parse builds parse trees for expressions as defined by expr.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL