1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-11-23 22:34:47 +02:00
Files
opentelemetry-go/internal/tools/semconvkit/main.go
Matthieu MOREL 1bae8f7347 chore: enable extra-rules from gofumpt (#7114)
#### Description

Enable extra rules from
[gofumpt](https://golangci-lint.run/usage/formatters/#gofumpt) that also
fixes paramTypeCombine from go-critic

Also defines `go.opentelemetry.io/otel` as in
https://github.com/open-telemetry/opentelemetry-go-contrib/pull/7637

Signed-off-by: Matthieu MOREL <matthieu.morel35@gmail.com>
Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
2025-08-03 08:24:33 -07:00

287 lines
6.9 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package semconvkit is used to generate opentelemetry-go specific semantic
// convention code.
package main
import (
"embed"
"errors"
"flag"
"fmt"
"go/ast"
"go/token"
"log/slog"
"os"
"path/filepath"
"sort"
"strings"
"text/template"
"github.com/Masterminds/semver"
"go.opentelemetry.io/otel/internal/tools/semconvkit/decls"
)
var (
logLevel = flag.String("log-level", "", `Logging level ("debug", "info", "warn", "error")`)
semconvPkg = flag.String("semconv", "./", "semconv package directory")
tag = flag.String("tag", "", "OpenTelemetry tagged version")
prev = flag.String("prev", "", "previous semconv version")
//go:embed templates/*.tmpl
rootFS embed.FS
)
func main() {
flag.Parse()
slog.SetDefault(newLogger(*logLevel))
if *tag == "" {
slog.Error("invalid tag", "tag", *tag)
os.Exit(1)
}
sc := &SemanticConventions{TagVer: *tag}
out := filepath.Join(*semconvPkg, *tag)
// Render all other files before the MIGRATION file. That file needs the
// full package declaration so it can determine compatibility accurately.
entries, err := rootFS.ReadDir("templates")
if err != nil {
slog.Error("error reading templates", "err", err)
os.Exit(1)
}
for _, entry := range entries {
if entry.Name() == "MIGRATION.md.tmpl" {
continue
}
src := filepath.Join("templates", entry.Name())
err := render(src, out, sc)
if err != nil {
slog.Error("error rendering template", "err", err, "template", entry.Name())
os.Exit(1)
}
}
prevPkg, err := prevVer(*semconvPkg, *tag, *prev)
if err != nil {
slog.Error("previous version not found, skipping migration", "err", err)
os.Exit(1)
}
slog.Debug("previous version found", "prev", prevPkg)
m, err := newMigration(out, filepath.Join(*semconvPkg, prevPkg))
if err != nil {
slog.Error("error getting migration, skipping", "err", err)
os.Exit(1)
}
if err := render("templates/MIGRATION.md.tmpl", out, m); err != nil {
slog.Error("error rendering migration template", "err", err)
os.Exit(1)
}
}
func newLogger(lvlStr string) *slog.Logger {
levelVar := new(slog.LevelVar) // Default value of info.
opts := &slog.HandlerOptions{AddSource: true, Level: levelVar}
h := slog.NewTextHandler(os.Stderr, opts)
logger := slog.New(h)
if lvlStr == "" {
return logger
}
var level slog.Level
if err := level.UnmarshalText([]byte(lvlStr)); err != nil {
logger.Error("failed to parse log level", "error", err, "log-level", lvlStr)
} else {
levelVar.Set(level)
}
return logger
}
// render renders all templates to the dest directory using the data.
func render(src, dest string, data any) error {
tmpls, err := template.ParseFS(rootFS, src)
if err != nil {
return err
}
for _, tmpl := range tmpls.Templates() {
slog.Debug("rendering template", "name", tmpl.Name())
target := filepath.Join(dest, strings.TrimSuffix(tmpl.Name(), ".tmpl"))
wr, err := os.Create(target)
if err != nil {
return err
}
err = tmpl.Execute(wr, data)
if err != nil {
return err
}
}
return nil
}
// prevVer returns the previous version of the semantic conventions package.
// It will first check for hint within root and return that value if found. If
// not found, it will find all directories in root with a version name and
// return the version that is less than and closest to the curr version.
func prevVer(root, cur, hint string) (string, error) {
slog.Debug("prevVer", "root", root, "current", cur, "hint", hint)
info, err := os.Stat(root)
if err != nil {
return "", fmt.Errorf("root directory %q not found: %w", root, err)
}
if !info.IsDir() {
return "", fmt.Errorf("root %q is not a directory", root)
}
if hint != "" {
sub := filepath.Join(root, hint)
slog.Debug("looking for hint", "path", sub)
info, err = os.Stat(sub)
if err == nil && info.IsDir() {
return hint, nil
}
}
v, err := semver.NewVersion(cur)
if err != nil {
return "", fmt.Errorf("invalid current version %q: %w", cur, err)
}
entries, err := os.ReadDir(root)
if err != nil {
return "", fmt.Errorf("error reading root %q: %w", root, err)
}
var prev *semver.Version
for _, entry := range entries {
slog.Debug("root entry", "name", entry.Name())
if !entry.IsDir() {
continue
}
ver, err := semver.NewVersion(entry.Name())
if err != nil {
slog.Debug("not a version dir", "name", entry.Name())
// Ignore errors for non-semver directories.
continue
}
slog.Debug("found version dir", "prev", ver)
if ver.LessThan(v) && (prev == nil || ver.GreaterThan(prev)) {
slog.Debug("new previous version", "version", ver)
prev = ver
}
}
if prev == nil {
return "", errors.New("no previous version found")
}
return prev.Original(), nil
}
// SemanticConventions are information about the semantic conventions being
// generated.
type SemanticConventions struct {
// TagVer is the tagged version (i.e. v.7.0 and not 1.7.0).
TagVer string
}
func (sc SemanticConventions) SemVer() string {
return strings.TrimPrefix(*tag, "v")
}
// Migration contains the details about the migration from the previous
// semantic conventions to the current one.
type Migration struct {
CurVer string
PrevVer string
Removals []string
Renames []Rename
}
// Remove is a semantic convention declaration that has been renamed.
type Rename struct {
Old, New string
}
func newMigration(cur, prev string) (*Migration, error) {
cDecl, err := decls.GetNames(cur, parse)
if err != nil {
return nil, fmt.Errorf("error parsing current version %q: %w", cur, err)
}
pDecl, err := decls.GetNames(prev, parse)
if err != nil {
return nil, fmt.Errorf("error parsing previous version %q: %w", prev, err)
}
m := Migration{
CurVer: filepath.Base(cur),
PrevVer: filepath.Base(prev),
Removals: inAnotB(pDecl, cDecl),
Renames: renames(pDecl, cDecl),
}
sort.Strings(m.Removals)
sort.Slice(m.Renames, func(i, j int) bool {
return m.Renames[i].Old < m.Renames[j].Old
})
return &m, nil
}
func parse(d ast.Decl) []string {
var out []string
switch decl := d.(type) {
case *ast.FuncDecl:
out = []string{decl.Name.Name}
case *ast.GenDecl:
if decl.Tok == token.CONST || decl.Tok == token.VAR {
for _, spec := range decl.Specs {
if valueSpec, ok := spec.(*ast.ValueSpec); ok {
for _, name := range valueSpec.Names {
out = append(out, name.Name)
}
}
}
}
}
return out
}
// inAnotB returns the canonical names in a that are not in b.
func inAnotB(a, b decls.Names) []string {
var diff []string
for key, name := range a {
if _, ok := b[key]; !ok {
diff = append(diff, string(name))
}
}
return diff
}
// renames returns the renames between the old and current names.
func renames(old, current decls.Names) []Rename {
var renames []Rename
for key, name := range old {
if otherName, ok := current[key]; ok && name != otherName {
renames = append(renames, Rename{
Old: string(name),
New: string(otherName),
})
}
}
return renames
}