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

feat: Auto-generate documentation for jaeger-v2 configuration structs via AST #6776

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Changes from 5 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
242 changes: 242 additions & 0 deletions cmd/config-doc-gen/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
// Copyright (c) 2025 The Jaeger Authors.
// SPDX-License-Identifier: Apache-2.0

package main

import (
"bytes"
"encoding/json"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"log"
"os"
"path/filepath"
"reflect"
"strings"
)

// FieldDoc represents documentation for a struct field.
type FieldDoc struct {
Name string `json:"name"`
Type string `json:"type"`
Tag string `json:"tag,omitempty"`
DefaultValue interface{} `json:"default_value,omitempty"`
Comment string `json:"comment,omitempty"`
}

// StructDoc represents documentation for a struct.
type StructDoc struct {
Name string `json:"name"`
Fields []FieldDoc `json:"fields"`
Comment string `json:"comment,omitempty"`
}

func main() {
// List of target directories
targetDirs := []string{
"cmd/jaeger/internal/extension/jaegerquery",
"pkg/es/config",
}

var allStructs []StructDoc

// Iterate over each target directory
for _, dir := range targetDirs {
fmt.Printf("Parsing directory: %s\n", dir)
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
Copy link
Member

Choose a reason for hiding this comment

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

you are using file system access. Doesn't Go AST have other means of traversal? We have some components imported directly from OTEL, with their own configs, and they will not be easily accessible via local file system (they would be somewhere in Go cache).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that would require more dependencies to be added to go.mod

Copy link
Member

Choose a reason for hiding this comment

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

Like what?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

golang.org/x/tools/go/packages would be added to traverse AST based on package

Copy link
Member

Choose a reason for hiding this comment

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

+1

if err != nil {
return err
}
// Process only Go source files
if !info.IsDir() && filepath.Ext(path) == ".go" {
structs, err := parseFile(path)
if err != nil {
log.Printf("Error parsing file %s: %v", path, err)
return err
}
allStructs = append(allStructs, structs...)
}
return nil
})
if err != nil {
log.Fatalf("Error walking the path %s: %v", dir, err)
}
}

// Serialize the collected struct documentation to JSON
outputFile, err := os.Create("struct_docs.json")
if err != nil {
log.Fatalf("Error creating output file: %v", err)
}
defer outputFile.Close()

encoder := json.NewEncoder(outputFile)
encoder.SetIndent("", " ")
if err := encoder.Encode(allStructs); err != nil {
log.Fatalf("Error encoding JSON: %v", err)
}

fmt.Println("Struct documentation has been written to struct_docs.json")
}

// processField processes a struct field and handles nested structs.
func processField(fset *token.FileSet, field *ast.Field, structs map[string]StructDoc) []FieldDoc {
var fieldDocs []FieldDoc
fieldType := exprToString(fset, field.Type)
fieldTag := extractTag(field.Tag)
defaultValue := extractDefaultValue(field.Tag, fieldType)

for _, name := range field.Names {
fieldDoc := FieldDoc{
Name: name.Name,
Type: fieldType,
Tag: fieldTag,
DefaultValue: defaultValue,
Comment: extractComment(field.Doc),
}
fieldDocs = append(fieldDocs, fieldDoc)
}

// Handle nested struct
if ident, ok := field.Type.(*ast.Ident); ok && ident.Obj != nil {
if typeSpec, ok := ident.Obj.Decl.(*ast.TypeSpec); ok {
if structType, isStruct := typeSpec.Type.(*ast.StructType); isStruct {
nestedStructDoc := StructDoc{
Name: ident.Name,
Comment: extractComment(typeSpec.Doc),
}
for _, nestedField := range structType.Fields.List {
nestedStructDoc.Fields = append(nestedStructDoc.Fields, processField(fset, nestedField, structs)...)
}
structs[ident.Name] = nestedStructDoc
}
}
}

return fieldDocs
}

// parseFile parses a Go source file and extracts struct information.
func parseFile(filePath string) ([]StructDoc, error) {
var structs []StructDoc
structMap := make(map[string]StructDoc)
// Create a new token file set
fset := token.NewFileSet()

// Parse the Go source file
node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("failed to parse file %s: %w", filePath, err)
}

// Traverse the AST to find struct type declarations
ast.Inspect(node, func(n ast.Node) bool {
// Check for type declarations
genDecl, ok := n.(*ast.GenDecl)
if !ok || genDecl.Tok != token.TYPE {
return true
}

// Iterate over the type specifications
for _, spec := range genDecl.Specs {
typeSpec, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}

// Check if the type is a struct
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
continue
}

structDoc := StructDoc{
Copy link
Member

Choose a reason for hiding this comment

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

not every struct you encounter is a config

Copy link
Contributor Author

Choose a reason for hiding this comment

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

are config structs only in config.go file?

Copy link
Member

Choose a reason for hiding this comment

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

not guaranteed. Top level configs usually are, but the nested subconfigs they contain could be anywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do we use any specific convention to identify config structs?

Copy link
Member

Choose a reason for hiding this comment

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

sadly, there is no required interface that configs implement. The factories typically have this function

func createDefaultConfig() component.Config {
	return &Config{}
}

so you could use it as an indication that Config struct is really an interesting config.

Copy link
Contributor Author

@AnmolxSingh AnmolxSingh Feb 28, 2025

Choose a reason for hiding this comment

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

For this i am planning to use ast.Inspect two times one for filtering the config structs returned by above function and second for printing them

Name: typeSpec.Name.Name,
Comment: extractComment(genDecl.Doc),
}

// Iterate over the struct fields
for _, field := range structType.Fields.List {
Copy link
Preview

Copilot AI Feb 26, 2025

Choose a reason for hiding this comment

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

The current loop iterates over field.Names without handling fields that are embedded (anonymous fields). Adding logic to process embedded fields could improve the robustness of the documentation generation.

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options
Copy link
Member

Choose a reason for hiding this comment

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

This is correct observation

structDoc.Fields = append(structDoc.Fields, processField(fset, field, structMap)...)
}
// Store processed struct in the map
structMap[structDoc.Name] = structDoc

}
return false
})

// Convert map to slice
for _, structDoc := range structMap {
structs = append(structs, structDoc)
}
return structs, nil
}

// extractComment retrieves the text from a CommentGroup.
func extractComment(cg *ast.CommentGroup) string {
if cg != nil {
return strings.TrimSpace(cg.Text())
}
return ""
}

// exprToString converts an ast.Expr to its string representation.
func exprToString(fset *token.FileSet, expr ast.Expr) string {
var buf bytes.Buffer
if err := printer.Fprint(&buf, fset, expr); err != nil {
return ""
}
return buf.String()
}

// extractTag extracts the tag value from a BasicLit.
func extractTag(tag *ast.BasicLit) string {
if tag != nil {
return strings.Trim(tag.Value, "`")
}
return ""
}

// extractDefaultValue parses the tag to find a default value if specified.
func extractDefaultValue(tag *ast.BasicLit, fieldType string) interface{} {
if tag == nil {
return nil
}
tagValue := extractTag(tag)
structTag := reflect.StructTag(tagValue)
defaultValueStr := structTag.Get("default")
if defaultValueStr == "" {
return nil
}

// Convert the default value string to the appropriate type
switch fieldType {
case "string":
return defaultValueStr
case "int", "int8", "int16", "int32", "int64":
var intValue int64
if _, err := fmt.Sscanf(defaultValueStr, "%d", &intValue); err != nil {
return err
}
return intValue
case "uint", "uint8", "uint16", "uint32", "uint64":
var uintValue uint64
fmt.Sscanf(defaultValueStr, "%d", &uintValue)
return uintValue
case "float32", "float64":
var floatValue float64
fmt.Sscanf(defaultValueStr, "%f", &floatValue)
return floatValue
case "bool":
var boolValue bool
fmt.Sscanf(defaultValueStr, "%t", &boolValue)
return boolValue
// Add more types as needed
default:
return nil
}
}
Loading