Skip to content

Commit

Permalink
Compatible components links generation.
Browse files Browse the repository at this point in the history
  • Loading branch information
thampiotr committed Nov 27, 2023
1 parent a2348a0 commit 1460781
Show file tree
Hide file tree
Showing 140 changed files with 2,086 additions and 8 deletions.
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

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
142 changes: 142 additions & 0 deletions component/metadata/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
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"
)

//TODO(thampiotr): Instead of metadata package reaching into registry, we'll migrate to using a YAML metadata file that
// contains information about all the components. This file will be generated separately from the
// registry 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
}

var (
// TypeTargets represents things that need to be scraped.
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 represent logs in Loki format
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()))
},
}

//TODO(thampiotr): add more types
//DataTypeOTELTelemetry = Type("OTEL Telemetry")
//DataTypePromMetrics = Type("Prometheus Metrics")
//DataTypePyroscopeProfiles = Type("Pyroscope Profiles")

AllTypes = []Type{
TypeTargets,
TypeLokiLogs,
}
)

type Metadata struct {
Accepts []Type
Exports []Type
}

func (m Metadata) Empty() bool {
return len(m.Accepts) == 0 && len(m.Exports) == 0
}

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
}
}
}

return false
}
51 changes: 51 additions & 0 deletions component/metadata/metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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{Accepts: []Type{TypeLokiLogs}},
},
{
name: "loki.source.file",
expected: Metadata{
Accepts: []Type{TypeTargets},
Exports: []Type{TypeLokiLogs},
},
},
{
name: "loki.process",
expected: Metadata{
Accepts: []Type{TypeLokiLogs},
Exports: []Type{TypeLokiLogs},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual, err := ForComponent(tt.name)
require.NoError(t, err)
require.Equal(t, tt.expected, actual)
})
}
}
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
}
70 changes: 70 additions & 0 deletions docs/docs_updated_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package docs

import (
"flag"
"github.com/grafana/agent/component/metadata"
"strings"
"testing"

"github.com/grafana/agent/component"
_ "github.com/grafana/agent/component/all"
"github.com/grafana/agent/docs/generator"
"github.com/stretchr/testify/require"
)

// Run the below generate command to automatically update the Markdown docs with generated content
//go:generate go test -fix-tests

var fixTestsFlag = flag.Bool("fix-tests", false, "update the test files with the current generated content")

func TestLinksToTypesSectionsUpdated(t *testing.T) {
for _, name := range component.AllNames() {
t.Run(name, func(t *testing.T) {
runForGenerator(t, generator.NewLinksToTypesGenerator(name))
})
}
}

func TestCompatibleComponentsPageUpdated(t *testing.T) {
path := "sources/flow/reference/compatibility/_index.md"
for _, typ := range metadata.AllTypes {
t.Run(typ.Name, func(t *testing.T) {
t.Run("exporters", func(t *testing.T) {
runForGenerator(t, generator.NewExportersListGenerator(typ, path))
})
t.Run("consumers", func(t *testing.T) {
runForGenerator(t, generator.NewConsumersListGenerator(typ, path))
})
})
}
}

func runForGenerator(t *testing.T, g generator.DocsGenerator) {
if *fixTestsFlag {
err := g.Write()
require.NoError(t, err, "failed to write generated content for: %q", g.Name())
t.Log("updated the docs with generated content", g.Name())
return
}

generated, err := g.Generate()
require.NoError(t, err, "failed to generate: %q", g.Name())

if generated == "" {
actual, err := g.Read()
require.Error(t, err, "expected error when reading existing generated docs for %q", g.Name())
require.Contains(t, err.Error(), "markers not found", "expected error to be about missing markers")
require.Empty(t, actual, "expected empty actual content for %q", g.Name())
return
}

actual, err := g.Read()
require.NoError(t, err, "failed to read existing generated docs for %q, try running 'go generate ./docs'", g.Name())
require.Contains(
t,
actual,
strings.TrimSpace(generated),
"outdated docs detected when running %q, try updating with 'go generate ./docs'",
g.Name(),
)
}
Loading

0 comments on commit 1460781

Please sign in to comment.