Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add auto-generated connected components in documentation #5791

Merged
merged 5 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ Main (unreleased)

- `pyroscope.ebpf` support python on arm64 platforms. (@korniltsev)

- Added links between compatible components in the documentation to make it
easier to discover them. (@thampiotr)

### Bugfixes

- Update `pyroscope.ebpf` to fix a logical bug causing to profile to many kthreads instead of regular processes https://github.com/grafana/pyroscope/pull/2778 (@korniltsev)
Expand Down
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ smoke-image:
#

.PHONY: generate generate-crds generate-drone generate-helm-docs generate-helm-tests generate-manifests generate-dashboards generate-protos generate-ui generate-versioned-files
generate: generate-crds generate-drone generate-helm-docs generate-helm-tests generate-manifests generate-dashboards generate-protos generate-ui generate-versioned-files
generate: generate-crds generate-drone generate-helm-docs generate-helm-tests generate-manifests generate-dashboards generate-protos generate-ui generate-versioned-files generate-docs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generate-versioned-files also generates docs-related content. It might be a bit confusing to have a generate-docs alongside it.

Copy link
Contributor Author

@thampiotr thampiotr Nov 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, I am not a fan of "versioned files", cause I'm not sure what it is by reading the name. It could be a lot of things. Do you have suggestions? I'm trying not to refactor existing code here though, as it's already a big PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH I cannot think of more appropriate names... generate-docs literally runs go generate on the docs, and generate-versioned-files is not docs specific. It updates a template file which is in pkg/operator

└─▪ find . -type f -name "*.t"
./docs/sources/_index.md.t
./pkg/operator/defaults.go.t

I'm ok with leaving the names as they are, but I think it might be a bit confusing since people will expect a step called "generate docs" to also update the Agent version in the docs.


generate-crds:
ifeq ($(USE_CONTAINER),1)
Expand Down Expand Up @@ -350,6 +350,12 @@ else
sh ./tools/gen-versioned-files/gen-versioned-files.sh
endif

generate-docs:
ifeq ($(USE_CONTAINER),1)
$(RERUN_IN_CONTAINER)
else
go generate ./docs
endif
#
# Other targets
#
Expand Down
193 changes: 193 additions & 0 deletions component/metadata/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package metadata

import (
"fmt"
"reflect"

"github.com/grafana/agent/component"
_ "github.com/grafana/agent/component/all"
"github.com/grafana/agent/component/common/loki"
"github.com/grafana/agent/component/discovery"
"github.com/grafana/agent/component/otelcol"
"github.com/grafana/agent/component/pyroscope"
"github.com/prometheus/prometheus/storage"
)

//TODO(thampiotr): Instead of metadata package reaching into registry, we'll migrate to using a YAML schema file that
// contains information about all the available components. This file will be generated separately and
// can be used by other tools.

type Type struct {
Name string
// Returns true if provided args include this type (including nested structs)
existsInArgsFn func(args component.Arguments) bool
// Returns true if provided exports include this type (including nested structs)
existsInExportsFn func(exports component.Exports) bool
}

func (t Type) String() string {
return fmt.Sprintf("Type[%s]", t.Name)
}

var (
TypeTargets = Type{
Name: "Targets",
existsInArgsFn: func(args component.Arguments) bool {
return hasFieldOfType(args, reflect.TypeOf([]discovery.Target{}))
},
existsInExportsFn: func(exports component.Exports) bool {
return hasFieldOfType(exports, reflect.TypeOf([]discovery.Target{}))
},
}

TypeLokiLogs = Type{
Name: "Loki `LogsReceiver`",
existsInArgsFn: func(args component.Arguments) bool {
return hasFieldOfType(args, reflect.TypeOf([]loki.LogsReceiver{}))
},
existsInExportsFn: func(exports component.Exports) bool {
return hasFieldOfType(exports, reflect.TypeOf(loki.NewLogsReceiver()))
},
}

TypePromMetricsReceiver = Type{
Name: "Prometheus `MetricsReceiver`",
existsInArgsFn: func(args component.Arguments) bool {
return hasFieldOfType(args, reflect.TypeOf([]storage.Appendable{}))
},
existsInExportsFn: func(exports component.Exports) bool {
var a *storage.Appendable = nil
return hasFieldOfType(exports, reflect.TypeOf(a).Elem())
},
}

TypePyroProfilesReceiver = Type{
Name: "Pyroscope `ProfilesReceiver`",
existsInArgsFn: func(args component.Arguments) bool {
return hasFieldOfType(args, reflect.TypeOf([]pyroscope.Appendable{}))
},
existsInExportsFn: func(exports component.Exports) bool {
var a *pyroscope.Appendable = nil
return hasFieldOfType(exports, reflect.TypeOf(a).Elem())
},
}

TypeOTELReceiver = Type{
Name: "OpenTelemetry `otelcol.Consumer`",
existsInArgsFn: func(args component.Arguments) bool {
return hasFieldOfType(args, reflect.TypeOf([]otelcol.Consumer{}))
},
existsInExportsFn: func(exports component.Exports) bool {
var a *otelcol.Consumer = nil
return hasFieldOfType(exports, reflect.TypeOf(a).Elem())
},
}

AllTypes = []Type{
TypeTargets,
TypeLokiLogs,
TypePromMetricsReceiver,
TypePyroProfilesReceiver,
TypeOTELReceiver,
}
)

type Metadata struct {
accepts []Type
exports []Type
}

func (m Metadata) Empty() bool {
return len(m.accepts) == 0 && len(m.exports) == 0
}

func (m Metadata) AllTypesAccepted() []Type {
return m.accepts
}

func (m Metadata) AllTypesExported() []Type {
return m.exports
}

func (m Metadata) AcceptsType(t Type) bool {
for _, a := range m.accepts {
if a.Name == t.Name {
return true
}
}
return false
}

func (m Metadata) ExportsType(t Type) bool {
for _, o := range m.exports {
if o.Name == t.Name {
return true
}
}
return false
}

func ForComponent(name string) (Metadata, error) {
reg, ok := component.Get(name)
if !ok {
return Metadata{}, fmt.Errorf("could not find component %q", name)
}
return inferMetadata(reg.Args, reg.Exports), nil
}

func inferMetadata(args component.Arguments, exports component.Exports) Metadata {
m := Metadata{}
for _, t := range AllTypes {
if t.existsInArgsFn(args) {
m.accepts = append(m.accepts, t)
}
if t.existsInExportsFn(exports) {
m.exports = append(m.exports, t)
}
}
return m
}

func hasFieldOfType(obj interface{}, fieldType reflect.Type) bool {
objValue := reflect.ValueOf(obj)

// If the object is a pointer, dereference it
for objValue.Kind() == reflect.Ptr {
objValue = objValue.Elem()
}

// If the object is not a struct or interface, return false
if objValue.Kind() != reflect.Struct && objValue.Kind() != reflect.Interface {
return false
}

for i := 0; i < objValue.NumField(); i++ {
fv := objValue.Field(i)
ft := fv.Type()

// If the field type matches the given type, return true
if ft == fieldType {
return true
}

if fv.Kind() == reflect.Interface && fieldType.AssignableTo(ft) {
return true
}

// If the field is a struct, recursively check its fields
if fv.Kind() == reflect.Struct {
if hasFieldOfType(fv.Interface(), fieldType) {
return true
}
}

// If the field is a pointer, create a new instance of the pointer type and recursively check its fields
if fv.Kind() == reflect.Ptr {
if hasFieldOfType(reflect.New(ft.Elem()).Interface(), fieldType) {
return true
}
}
}

return false
}
94 changes: 94 additions & 0 deletions component/metadata/metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package metadata

import (
"testing"

"github.com/stretchr/testify/require"
)

func Test_inferMetadata(t *testing.T) {
tests := []struct {
name string
expected Metadata
}{
{
name: "discovery.dns",
expected: Metadata{exports: []Type{TypeTargets}},
},
{
name: "discovery.relabel",
expected: Metadata{
accepts: []Type{TypeTargets},
exports: []Type{TypeTargets},
},
},
{
name: "loki.echo",
expected: Metadata{exports: []Type{TypeLokiLogs}},
},
{
name: "loki.source.file",
expected: Metadata{
accepts: []Type{TypeTargets, TypeLokiLogs},
},
},
{
name: "loki.process",
expected: Metadata{
accepts: []Type{TypeLokiLogs},
exports: []Type{TypeLokiLogs},
},
},
{
name: "prometheus.relabel",
expected: Metadata{
accepts: []Type{TypePromMetricsReceiver},
exports: []Type{TypePromMetricsReceiver},
},
},
{
name: "prometheus.remote_write",
expected: Metadata{
accepts: []Type{},
exports: []Type{TypePromMetricsReceiver},
},
},
{
name: "otelcol.exporter.otlp",
expected: Metadata{
accepts: []Type{},
exports: []Type{TypeOTELReceiver},
},
},
{
name: "otelcol.processor.filter",
expected: Metadata{
accepts: []Type{TypeOTELReceiver},
exports: []Type{TypeOTELReceiver},
},
},
{
name: "faro.receiver",
expected: Metadata{
accepts: []Type{TypeLokiLogs, TypeOTELReceiver},
exports: []Type{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual, err := ForComponent(tt.name)
require.NoError(t, err)

compareSlices := func(expected, actual []Type, name string) {
require.Equal(t, len(expected), len(actual), "expected %d %s types, got %d; expected: %v, actual: %v", len(expected), name, len(actual), expected, actual)
for i := range expected {
require.Equal(t, expected[i].Name, actual[i].Name, "expected %s type at %d to be %q, got %q", name, i, expected[i].Name, actual[i].Name)
}
}

compareSlices(tt.expected.AllTypesAccepted(), actual.AllTypesAccepted(), "accepted")
compareSlices(tt.expected.AllTypesExported(), actual.AllTypesExported(), "exported")
})
}
}
4 changes: 2 additions & 2 deletions component/pyroscope/ebpf/ebpf_placeholder.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build linux && !arm64 && !amd64
//go:build !(linux && (arm64 || amd64))

package ebpf

Expand Down Expand Up @@ -26,7 +26,7 @@ type Component struct {
}

func New(opts component.Options, args Arguments) (component.Component, error) {
level.Warn(opts.Logger).Log("msg", "the pyroscope.ebpf component only works on linux; enabling it otherwise will do nothing")
level.Warn(opts.Logger).Log("msg", "the pyroscope.ebpf component only works on ARM64 and AMD64 Linux platforms; enabling it otherwise will do nothing")
return &Component{}, nil
}

Expand Down
8 changes: 8 additions & 0 deletions component/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/grafana/regexp"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/trace"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)

// The parsedName of a component is the parts of its name ("remote.http") split
Expand Down Expand Up @@ -202,3 +204,9 @@ func Get(name string) (Registration, bool) {
r, ok := registered[name]
return r, ok
}

func AllNames() []string {
keys := maps.Keys(registered)
slices.Sort(keys)
return keys
}
6 changes: 5 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ First, inside the `docs/` folder run `make check-cloudwatch-integration` to veri

If the check fails, then the doc supported services list should be updated. For that, run `make generate-cloudwatch-integration` to get the updated list, which should replace the old one in [the docs](./sources/static/configuration/integrations/cloudwatch-exporter-config.md).

## Update generated reference docs

Some sections of Grafana Agent Flow reference documentation are automatically generated. To update them, run `make generate-docs`.

### Community Projects

Below is a list of community-led projects for working with Grafana Agent. These projects are not maintained or supported by Grafana Labs.
The following is a list of community-led projects for working with Grafana Agent. These projects are not maintained or supported by Grafana Labs.

#### Helm (Kubernetes Deployment)

Expand Down
Loading