Skip to content

Commit

Permalink
Add code and scheme generation
Browse files Browse the repository at this point in the history
Problem:
- Ensure the Attributes method of Exprotable interface are generated
automatically for the telemetry data types.
- Ensure the avro scheme (.avdl) is generated automatically for the
telemetry data types.

Solution:
- Add generator tool in cmd/generator that generates code (Attributes
method) and the scheme.
- Generator can be used in //go:generate annotations in go source files.
- Generator has "generator" build tag so that it is not included into
the telemetry library by default.
- Generator uses golang.org/x/tools/go/packages to parse source files.

Testing:
- Unit tests of parsing.
- Unit tests of code and scheme generation - ensuring non-zero output.
- Unit tests of generated code in cmd/generator/tests package
- Manual validation of the generated scheme
  (cmd/generator/tests/data.avdl) using Apache Avro IDL Scheme Support
  IntelliJ plugin.

CLOSES - #18
  • Loading branch information
pleshakov committed Feb 23, 2024
1 parent 2ec8c67 commit 8951616
Show file tree
Hide file tree
Showing 17 changed files with 1,545 additions and 3 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
*.dylib

# Output of the go coverage tool
cmd-cover.out
cmd-cover.html
*cover.out
*cover.html

# Vim
*.swp
Expand Down
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,5 @@ issues:
max-same-issues: 0
run:
timeout: 3m
build-tags:
- generator
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ help: Makefile ## Display this help
unit-test:
go test ./... -race -coverprofile cmd-cover.out
go tool cover -html=cmd-cover.out -o cmd-cover.html
go test -tags generator ./cmd/generator/... -race -coverprofile generator-cmd-cover.out
go tool cover -html=generator-cmd-cover.out -o generator-cmd-cover.html

.PHONY: clean-go-cache
clean-go-cache: ## Clean go cache
Expand Down Expand Up @@ -36,3 +38,9 @@ dev-all: deps fmt vet lint unit-test ## Run all the development checks
.PHONY: generate
generate: ## Run go generate
go generate ./...
go generate -tags generator ./cmd/generator/...

.PHONY: generator-tests
generator-tests: ## Regenerate the generator generated files and run generator unit tests
go generate -tags generator ./cmd/generator/... # ensure the generated files generated by the generator are up to date
go test -tags generator ./cmd/generator/... -race -coverprofile cmd-cover.out
110 changes: 110 additions & 0 deletions cmd/generator/code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//go:build generator

package main

import (
"fmt"
"go/types"
"io"
"text/template"
)

const codeTemplate = `{{ if .BuildTags }}//go:build {{ .BuildTags }}{{ end }}
package {{ .PackageName }}
/*
This is a generated file. DO NOT EDIT.
*/
import (
"go.opentelemetry.io/otel/attribute"
"github.com/nginxinc/telemetry-exporter/pkg/telemetry"
)
func (d *{{ .StructName }}) Attributes() []attribute.KeyValue {
var attrs []attribute.KeyValue
{{ range .Fields -}}
attrs = append(attrs, {{ .AttributesSource }})
{{ end }}
return attrs
}
var _ telemetry.Exportable = (*{{ .StructName }})(nil)
`

type codeGen struct {
PackageName string
StructName string
BuildTags string
Fields []codeField
}

type codeField struct {
AttributesSource string
}

func getAttributeType(kind types.BasicKind) string {
switch kind {
case types.Int64:
return "Int64"
case types.Float64:
return "Float64"
case types.String:
return "String"
case types.Bool:
return "Bool"
default:
panic(fmt.Sprintf("unexpected kind %v", kind))
}
}

type codeGenConfig struct {
packageName string
typeName string
buildTags string
fields []field
}

func generateCode(writer io.Writer, cfg codeGenConfig) error {
codeFields := make([]codeField, 0, len(cfg.fields))

for _, f := range cfg.fields {
var cf codeField

if f.embeddedStruct {
cf = codeField{
AttributesSource: fmt.Sprintf(`d.%s.Attributes()...`, f.name),
}
} else if f.slice {
cf = codeField{
AttributesSource: fmt.Sprintf(`attribute.%sSlice("%s", d.%s)`, getAttributeType(f.fieldType), f.name, f.name),
}
} else {
cf = codeField{
AttributesSource: fmt.Sprintf(`attribute.%s("%s", d.%s)`, getAttributeType(f.fieldType), f.name, f.name),
}
}

codeFields = append(codeFields, cf)
}

cg := codeGen{
PackageName: cfg.packageName,
StructName: cfg.typeName,
Fields: codeFields,
BuildTags: cfg.buildTags,
}

funcMap := template.FuncMap{
"getAttributeType": getAttributeType,
}

tmpl := template.Must(template.New("scheme").Funcs(funcMap).Parse(codeTemplate))

if err := tmpl.Execute(writer, cg); err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}

return nil
}
42 changes: 42 additions & 0 deletions cmd/generator/code_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//go:build generator

package main

import (
"bytes"
"testing"

. "github.com/onsi/gomega"

"github.com/nginxinc/telemetry-exporter/cmd/generator/tests"
)

func TestGenerateCode(t *testing.T) {
g := NewGomegaWithT(t)

cfg := parsingConfig{
pkgName: "tests",
typeName: "Data",
loadPattern: "github.com/nginxinc/telemetry-exporter/cmd/generator/tests",
buildFlags: []string{"-tags=generator"},
}

_ = tests.Data{} // depends on the type being defined

fields, err := parse(cfg)

g.Expect(err).ToNot(HaveOccurred())

var buf bytes.Buffer

codeCfg := codeGenConfig{
packageName: "tests",
typeName: "Data",
fields: fields,
}

err = generateCode(&buf, codeCfg)

g.Expect(err).ToNot(HaveOccurred())
g.Expect(buf.Bytes()).ToNot(BeEmpty())
}
129 changes: 129 additions & 0 deletions cmd/generator/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//go:build generator

package main

import (
"flag"
"fmt"
"os"
"strings"
)

var (
code = flag.Bool("code", true, "Generate code")
buildTags = flag.String("build-tags", "", "Comma separated list of build tags expected in the source files and that will be added to the generated code") //nolint:lll
scheme = flag.Bool("scheme", false, "Generate Avro scheme")
schemeNamespace = flag.String("scheme-namespace", "gateway.nginx.org", "Scheme namespace; required when -scheme is set") //nolint:lll
schemeProtocol = flag.String("scheme-protocol", "", "Scheme protocol; required when -scheme is set")
schemeDataFabricDataType = flag.String("scheme-df-datatype", "", "Scheme data fabric data type; required when -scheme is set") //nolint:lll
typeName = flag.String("type", "", "Type to generate; required")
)

func exitWithError(err error) {
fmt.Fprintln(os.Stderr, "error: "+err.Error())
os.Exit(1)
}

func existWithUsage() {
flag.Usage()
os.Exit(1)
}

func validateFlags() {
if *typeName == "" {
existWithUsage()
}

if *scheme {
if *schemeNamespace == "" {
existWithUsage()
}
if *schemeProtocol == "" {
existWithUsage()
}
if *schemeDataFabricDataType == "" {
existWithUsage()
}
}
}

func main() {
flag.Parse()

validateFlags()

pkgName := os.Getenv("GOPACKAGE")
if pkgName == "" {
exitWithError(fmt.Errorf("GOPACKAGE is not set"))
}

var buildFlags []string
if *buildTags != "" {
buildFlags = []string{"-tags=" + *buildTags}
}

cfg := parsingConfig{
pkgName: pkgName,
typeName: *typeName,
buildFlags: buildFlags,
}

fields, err := parse(cfg)
if err != nil {
exitWithError(fmt.Errorf("failed to parse struct: %w", err))
}

fmt.Printf("Successfully parsed struct %s\n", *typeName)

if *code {
fmt.Println("Generating code")

fileName := fmt.Sprintf("%s_attributes_generated.go", strings.ToLower(*typeName))

file, err := os.Create(fileName)
if err != nil {
exitWithError(fmt.Errorf("failed to create file: %w", err))
}
defer file.Close()

var codeGenBuildTags string
if *buildTags != "" {
codeGenBuildTags = strings.ReplaceAll(*buildTags, ",", " && ")
}

codeCfg := codeGenConfig{
packageName: pkgName,
typeName: *typeName,
fields: fields,
buildTags: codeGenBuildTags,
}

if err := generateCode(file, codeCfg); err != nil {
exitWithError(fmt.Errorf("failed to generate code: %w", err))
}
}

if *scheme {
fmt.Println("Generating scheme")

fileName := fmt.Sprintf("%s.avdl", strings.ToLower(*typeName))

file, err := os.Create(fileName)
if err != nil {
exitWithError(fmt.Errorf("failed to create file: %w", err))
}
defer file.Close()

schemeCfg := schemeGenConfig{
namespace: *schemeNamespace,
protocol: *schemeProtocol,
dataFabricDataType: *schemeDataFabricDataType,
record: *typeName,
fields: fields,
}

if err := generateScheme(file, schemeCfg); err != nil {
exitWithError(fmt.Errorf("failed to generate scheme: %w", err))
}
}
}
Loading

0 comments on commit 8951616

Please sign in to comment.