blob: 12444e2761db1b812e58afacf6e1cdfafa364c00 [file] [log] [blame]
// Licensed to Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Apache Software Foundation (ASF) licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package tools
import (
"fmt"
"go/ast"
"go/doc"
"go/parser"
"go/token"
"io/ioutil"
"os"
"reflect"
"sort"
"strings"
"github.com/apache/skywalking-satellite/internal/pkg/log"
"github.com/apache/skywalking-satellite/internal/pkg/plugin"
"github.com/apache/skywalking-satellite/plugins"
fetcher_api "github.com/apache/skywalking-satellite/plugins/fetcher/api"
forwarder_api "github.com/apache/skywalking-satellite/plugins/forwarder/api"
receiver_api "github.com/apache/skywalking-satellite/plugins/receiver/api"
"golang.org/x/mod/modfile"
)
const (
topLevel = "# "
secondLevel = "## "
lf = "\n"
yamlQuoteStart = "```yaml"
yamlQuoteEnd = "```"
markdownSuffix = ".md"
commentPrefix = "/ "
)
func GeneratePluginDoc(outputRootPath, menuFilePath, pluginFilePath string) error {
log.Init(&log.LoggerConfig{})
plugins.RegisterPlugins()
pluginPath := fmt.Sprintf("%s%s", outputRootPath, pluginFilePath)
if err := createDir(pluginPath); err != nil {
return fmt.Errorf("create docs dir error: %v", err)
}
if err := generatePluginListDoc(pluginPath, getSortedCategories()); err != nil {
return err
}
if err := updateMenuPluginListDoc(outputRootPath, menuFilePath, pluginFilePath, getSortedCategories()); err != nil {
return err
}
log.Logger.Info("Successfully generate documentation!")
return nil
}
// sort categories by dictionary sequence
func getSortedCategories() []reflect.Type {
var categories []reflect.Type
for c := range plugin.Reg {
categories = append(categories, c)
}
sort.Slice(categories, func(i, j int) bool {
return strings.Compare(categories[i].String(), categories[j].String()) <= 0
})
return categories
}
func updateMenuPluginListDoc(outputRootPath, menuFilePath, pluginFilePath string, categories []reflect.Type) error {
menuFile := fmt.Sprintf("%s%s", outputRootPath, menuFilePath)
menu, err := LoadCatalog(menuFile)
if err != nil {
return err
}
// find plugin Catalog
pluginCatalog := menu.Find("Setup", "Plugins")
if pluginCatalog == nil {
return fmt.Errorf("cannot find plugins Catalog")
}
// remove path
pluginCatalog.Path = ""
// rebuild all plugins
var allPlugins []*Catalog
for _, category := range categories {
// plugin
implements := []*Catalog{}
curPlugin := &Catalog{
Name: strings.ToLower(category.Name()),
}
// all implements
pluginList := getPluginsByCategory(category)
for _, pluginName := range pluginList {
implements = append(implements, &Catalog{
Name: pluginName,
Path: strings.TrimRight(fmt.Sprintf("%s/%s", pluginFilePath, getPluginDocFileName(category, pluginName)), markdownSuffix),
})
}
curPlugin.Catalog = implements
if len(implements) > 0 {
allPlugins = append(allPlugins, curPlugin)
}
}
pluginCatalog.Catalog = allPlugins
return menu.Save(menuFile)
}
func generatePluginListDoc(docDir string, categories []reflect.Type) error {
fileName := docDir + "/" + "plugin-list" + markdownSuffix
docStr := topLevel + "Plugin List" + lf
for _, category := range categories {
docStr += "- " + category.Name() + lf
pluginList := getPluginsByCategory(category)
for _, pluginName := range pluginList {
docStr += " - [" + pluginName + "](./" + getPluginDocFileName(category, pluginName) + ")" + lf
if err := generatePluginDoc(docDir, category, pluginName); err != nil {
return err
}
}
}
return writeDoc([]byte(docStr), fileName)
}
func generatePluginDoc(docDir string, category reflect.Type, pluginName string) error {
docFileName := docDir + "/" + getPluginDocFileName(category, pluginName)
p := plugin.Get(category, plugin.Config{plugin.NameField: pluginName})
docRes := topLevel + category.Name() + "/" + pluginName + lf
docRes += secondLevel + "Description" + lf
docRes += p.Description() + lf
docRes += generateSupportForwarders(category, p)
docRes += secondLevel + "DefaultConfig" + lf
docRes += yamlQuoteStart + p.DefaultConfig() + yamlQuoteEnd + lf
docRes += secondLevel + "Configuration" + lf
docRes += generateConfiguration(category, p) + lf
return writeDoc([]byte(docRes), docFileName)
}
func GetModuleName() string {
goModBytes, err := ioutil.ReadFile("go.mod")
if err != nil {
return ""
}
modName := modfile.ModulePath(goModBytes)
return modName
}
func generateConfiguration(category reflect.Type, p plugin.Plugin) string {
var content = ""
content += "|Name|Type|Description|" + lf
content += "|----|----|-----------|" + lf
configurations := getConfigurations(category, reflect.TypeOf(p).Elem())
eachConfigurationItem(configurations, "", func(name, dataType, desc string) {
content += fmt.Sprintf("| %s | %s | %s |%s", name, dataType, desc, lf)
})
return content
}
func eachConfigurationItem(items []*pluginConfigurationItem, parentName string, consumer func(name, dataType, desc string)) {
for _, conf := range items {
consumer(parentName+conf.name, conf.dataType, conf.description)
eachConfigurationItem(conf.children, parentName+conf.name+".", consumer)
}
}
type pluginConfigurationItem struct {
name string
description string
dataType string
children []*pluginConfigurationItem
}
type pluginChildrenFinder struct {
childType reflect.Type
squash bool
}
func getConfigurations(category, p reflect.Type) []*pluginConfigurationItem {
pluginDir := strings.TrimPrefix(p.PkgPath(), GetModuleName())
fset := token.NewFileSet()
d, err := parser.ParseDir(fset, "."+pluginDir, nil, parser.ParseComments)
if err != nil {
log.Logger.Warnf("failed to generate plugin [%s] configuration, error: %v", category.Name()+"/"+p.Name(), err)
return make([]*pluginConfigurationItem, 0)
}
result := make([]*pluginConfigurationItem, 0)
for _, f := range d {
pack := doc.New(f, "./", 0)
for _, t := range pack.Types {
if t.Name != p.Name() {
continue
}
for _, spec := range t.Decl.Specs {
typeSpec, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
continue
}
for _, field := range structType.Fields.List {
item, childFinder := parsePluginConfigurationItem(field, p)
if childFinder != nil {
configurations := getConfigurations(category, childFinder.childType)
if childFinder.squash {
result = append(result, configurations...)
} else if item != nil {
item.children = configurations
}
}
if item != nil {
result = append(result, item)
}
}
}
}
}
return result
}
// parse field to configuration item
func parsePluginConfigurationItem(field *ast.Field, pType reflect.Type) (*pluginConfigurationItem, *pluginChildrenFinder) {
if field.Names == nil || field.Tag == nil {
return nil, nil
}
var fieldName = ""
for _, n := range field.Names {
fieldName += n.Name
}
pluginField, find := pType.FieldByName(fieldName)
if !find {
return nil, nil
}
mapStructureValue := pluginField.Tag.Get("mapstructure")
var confName string
var childrenFinder *pluginChildrenFinder
if index := strings.Index(mapStructureValue, ","); index != -1 {
if strings.Contains(mapStructureValue[index+1:], "squash") {
if pluginField.Type.Kind() == reflect.Struct {
return nil, &pluginChildrenFinder{childType: pluginField.Type, squash: true}
}
log.Logger.Warnf("Could not identity plugin field: %v", pluginField)
return nil, nil
}
confName = mapStructureValue[:index]
} else if len(mapStructureValue) > 0 {
confName = mapStructureValue
} else {
confName = fieldName
}
var dataType = pluginField.Type.String()
switch pluginField.Type.Kind() {
case reflect.Struct:
case reflect.Ptr:
if pluginField.Type.Elem().PkgPath() != "" {
childrenFinder = &pluginChildrenFinder{childType: pluginField.Type.Elem()}
}
}
return &pluginConfigurationItem{
name: confName,
dataType: dataType,
description: buildPluginDescription(field),
}, childrenFinder
}
func buildPluginDescription(field *ast.Field) string {
var comments = ""
for _, group := range []*ast.CommentGroup{field.Doc, field.Comment} {
if group != nil {
for _, comment := range group.List {
comments += strings.TrimLeft(comment.Text, commentPrefix)
}
}
}
return comments
}
func generateSupportForwarders(category reflect.Type, p plugin.Plugin) string {
var forwarders []forwarder_api.Forwarder
if category.Name() == "Receiver" {
forwarders = p.(receiver_api.Receiver).SupportForwarders()
} else if category.Name() == "Fetcher" {
forwarders = p.(fetcher_api.Fetcher).SupportForwarders()
}
if len(forwarders) == 0 {
return ""
}
result := secondLevel + "Support Forwarders" + lf
for _, forwarder := range forwarders {
result += " - [" + forwarder.Name() + "](" + getPluginDocFileName(reflect.TypeOf(forwarder).Elem(), forwarder.Name()) + ")" + lf
}
return result
}
func getPluginsByCategory(category reflect.Type) []string {
mapping := plugin.Reg[category]
var keys []string
for k := range mapping {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
func getPluginDocFileName(category reflect.Type, pluginName string) string {
return strings.ToLower(category.Name() + "_" + pluginName + markdownSuffix)
}
func writeDoc(docBytes []byte, docFileName string) error {
if err := ioutil.WriteFile(docFileName, docBytes, os.ModePerm); err != nil {
return fmt.Errorf("cannot init the plugin doc: %v", err)
}
return nil
}
func createDir(path string) error {
err := os.RemoveAll(path)
if err != nil {
return err
}
fileInfo, err := os.Stat(path)
if os.IsNotExist(err) || fileInfo.Size() == 0 {
return os.Mkdir(path, os.ModePerm)
}
return err
}