Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
74 changes: 74 additions & 0 deletions cli/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"regexp"
"strings"

"github.com/samber/lo"
"github.com/urfave/cli/v2"

"go.viam.com/rdk/logging"
Expand Down Expand Up @@ -189,6 +190,14 @@ var commonPartFlags = []cli.Flag{
},
}

var commonOtlpFlags = []cli.Flag{
&cli.StringFlag{
Name: "endpoint",
DefaultText: "localhost:4317",
Usage: "OTLP endpoint in host:port format",
},
}

// matches all uppercase characters that follow lowercase chars and aren't at the [0] index of a string.
// This is useful for converting camel case into kabob case when getting values out of a CLI Context
// based on a flag name, and putting them into a struct with a camel cased field name.
Expand Down Expand Up @@ -456,6 +465,71 @@ var app = &cli.App{
},
},
Commands: []*cli.Command{
{
Name: "traces",
Usage: "Work with viam-server traces",
Subcommands: []*cli.Command{
{
Name: "import-local",
Usage: "Import traces from a local viam server trace file to an OTLP endpoint.",
Flags: lo.Flatten([][]cli.Flag{
{&cli.StringFlag{
Name: "path",
TakesFile: true,
Required: true,
Usage: "path to file to import",
}},
commonOtlpFlags,
}),
Action: createCommandWithT(traceImportLocalAction),
},
{
Name: "import-remote",
Usage: "Import traces from a remote viam machine to an OTLP endpoint.",
Flags: lo.Flatten([][]cli.Flag{
commonOtlpFlags,
commonPartFlags,
}),
Action: createCommandWithT(traceImportRemoteAction),
},
{
Name: "print-local",
Usage: "Print traces in a local file to the console",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "path",
TakesFile: true,
Required: true,
Usage: "path to file to import",
},
},
Action: createCommandWithT(tracePrintLocalAction),
},
{
Name: "print-remote",
Usage: "Print traces from a remote viam machine to the console",
Flags: lo.Flatten([][]cli.Flag{
commonPartFlags,
}),
Action: createCommandWithT(tracePrintRemoteAction),
},
{
Name: "fetch-remote",
Usage: "Download a traces from a viam machine and save them to disk",
Flags: lo.Flatten([][]cli.Flag{
commonPartFlags,
{
&cli.PathFlag{
Name: generalFlagDestination,
Required: true,
Usage: "output directory for downloaded traces",
},
},
}),
Action: createCommandWithT(traceFetchRemoteAction),
},
},
},
{
Name: "login",
// NOTE(benjirewis): maintain `auth` as an alias for backward compatibility.
Expand Down
253 changes: 253 additions & 0 deletions cli/traces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
package cli

import (
stderrors "errors"
"os"
"path"
"path/filepath"
"time"

"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
"go.viam.com/utils/perf"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"go.viam.com/rdk/logging"
"go.viam.com/rdk/protoutils"
"go.viam.com/rdk/services/shell"
)

var tracesPath = path.Join("~", ".viam", "trace")

type traceFetchRemoteArgs struct {
Organization string
Location string
Machine string
Part string
Destination string
}

type traceImportRemoteArgs struct {
Organization string
Location string
Machine string
Part string
Endpoint string
}

type traceImportLocalArgs struct {
Path string
Endpoint string
}

type tracePrintLocalArgs struct {
Path string
}

func traceImportRemoteAction(ctx *cli.Context, args traceImportRemoteArgs) error {
client, err := newViamClient(ctx)
if err != nil {
return err
}

globalArgs, err := getGlobalArgs(ctx)
if err != nil {
return err
}
logger := globalArgs.createLogger()
tmp, err := os.MkdirTemp("", "viamtraceimport")
if err != nil {
return err
}
//nolint: errcheck
defer os.RemoveAll(tmp)
if err := client.tracesFetchRemoteAction(
ctx,
traceFetchRemoteArgs{
Organization: args.Organization,
Location: args.Location,
Machine: args.Machine,
Part: args.Part,
Destination: tmp,
},
globalArgs.Debug,
logger,
); err != nil {
return err
}

return traceImportLocalAction(ctx, traceImportLocalArgs{
Path: filepath.Join(tmp, "traces"),
Endpoint: args.Endpoint,
})
}

func (c *viamClient) tracesFetchRemoteAction(
ctx *cli.Context,
flagArgs traceFetchRemoteArgs,
debug bool,
logger logging.Logger,
) error {
part, err := c.robotPart(flagArgs.Organization, flagArgs.Location, flagArgs.Machine, flagArgs.Part)
if err != nil {
return err
}
// Intentional use of path instead of filepath: Windows understands both / and
// \ as path separators, and we don't want a cli running on Windows to send
// a path using \ to a *NIX machine.
src := path.Join(tracesPath, part.Id, "traces")
gArgs, err := getGlobalArgs(ctx)
quiet := err == nil && gArgs != nil && gArgs.Quiet
var startTime time.Time
if !quiet {
startTime = time.Now()
printf(ctx.App.Writer, "Saving to %s ...", path.Join(flagArgs.Destination, part.GetId()))
}
if err := c.copyFilesFromMachine(
flagArgs.Organization,
flagArgs.Location,
flagArgs.Machine,
flagArgs.Part,
debug,
true,
false,
[]string{src},
flagArgs.Destination,
logger,
); err != nil {
if statusErr := status.Convert(err); statusErr != nil &&
statusErr.Code() == codes.InvalidArgument &&
statusErr.Message() == shell.ErrMsgDirectoryCopyRequestNoRecursion {
return errDirectoryCopyRequestNoRecursion
}
return err
}
if !quiet {
printf(ctx.App.Writer, "Download finished in %s.", time.Since(startTime))
}
return nil
}

func tracePrintRemoteAction(
ctx *cli.Context,
args machinesPartGetFTDCArgs,
) error {
client, err := newViamClient(ctx)
if err != nil {
return err
}

globalArgs, err := getGlobalArgs(ctx)
if err != nil {
return err
}
logger := globalArgs.createLogger()
tmp, err := os.MkdirTemp("", "viamtraceimport")
if err != nil {
return err
}
//nolint: errcheck
defer os.RemoveAll(tmp)
if err := client.tracesFetchRemoteAction(
ctx,
traceFetchRemoteArgs{
Organization: args.Organization,
Location: args.Location,
Machine: args.Machine,
Part: args.Part,
Destination: tmp,
},
globalArgs.Debug,
logger,
); err != nil {
return err
}
return tracePrintLocalAction(ctx, tracePrintLocalArgs{Path: filepath.Join(tmp, "traces")})
}

func traceFetchRemoteAction(ctx *cli.Context, args traceFetchRemoteArgs) error {
client, err := newViamClient(ctx)
if err != nil {
return err
}

globalArgs, err := getGlobalArgs(ctx)
if err != nil {
return err
}
logger := globalArgs.createLogger()

return client.tracesFetchRemoteAction(ctx, args, globalArgs.Debug, logger)
}

func tracePrintLocalAction(
ctx *cli.Context,
args tracePrintLocalArgs,
) error {
traceFile, err := os.Open(args.Path)
if err != nil {
if os.IsNotExist(err) {
printf(ctx.App.Writer, "No traces found")
return nil
}
return errors.Wrap(err, "failed to open trace file")
}
traceReader := protoutils.NewDelimitedProtoReader[tracepb.ResourceSpans](traceFile)
//nolint: errcheck
defer traceReader.Close()

devExporter := perf.NewOtelDevelopmentExporter()
if err := devExporter.Start(); err != nil {
return err
}
defer devExporter.Stop()
var msg tracepb.ResourceSpans
err = nil
for resource := range traceReader.AllWithMemory(&msg) {
for _, scope := range resource.ScopeSpans {
err = stderrors.Join(err, devExporter.ExportOTLPSpans(ctx.Context, scope.Spans))
}
}
return err
}

func traceImportLocalAction(
ctx *cli.Context,
args traceImportLocalArgs,
) error {
traceFile, err := os.Open(args.Path)
if err != nil {
if os.IsNotExist(err) {
printf(ctx.App.Writer, "No traces found")
return nil
}
return errors.Wrap(err, "failed to open trace file")
}
traceReader := protoutils.NewDelimitedProtoReader[tracepb.ResourceSpans](traceFile)
//nolint: errcheck
defer traceReader.Close()
endpoint := args.Endpoint
if endpoint == "" {
endpoint = "localhost:4317"
}
otlpClient := otlptracegrpc.NewClient(
otlptracegrpc.WithEndpoint(endpoint),
otlptracegrpc.WithInsecure(),
)
if err := otlpClient.Start(ctx.Context); err != nil {
return err
}
//nolint: errcheck
defer otlpClient.Stop(ctx.Context)
var msg tracepb.ResourceSpans
for span := range traceReader.AllWithMemory(&msg) {
err := otlpClient.UploadTraces(ctx.Context, []*tracepb.ResourceSpans{span})
if err != nil {
printf(ctx.App.Writer, "Error uploading trace: %v", err)
}
}
return nil
}
10 changes: 6 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,17 @@ require (
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
go.opentelemetry.io/proto/otlp v1.7.1
go.opentelemetry.io/proto/otlp v1.9.0
go.uber.org/atomic v1.11.0
go.uber.org/goleak v1.3.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.0
go.viam.com/api v0.1.496
go.viam.com/test v1.2.4
go.viam.com/utils v0.3.5
go.viam.com/utils v0.3.6
goji.io v2.0.2+incompatible
golang.org/x/image v0.25.0
golang.org/x/mobile v0.0.0-20240112133503-c713f31d574b
Expand All @@ -108,9 +109,9 @@ require (
gonum.org/v1/plot v0.15.2
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5
google.golang.org/grpc v1.75.0
google.golang.org/grpc v1.75.1
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1
google.golang.org/protobuf v1.36.8
google.golang.org/protobuf v1.36.10
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/src-d/go-billy.v4 v4.3.2
gorgonia.org/tensor v0.9.24
Expand Down Expand Up @@ -160,6 +161,7 @@ require (
github.com/catppuccin/go v0.2.0 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/bubbles v0.20.0 // indirect
Expand Down
Loading