internal/cli/log.go
package cli
import (
"context"
"ct/internal/report"
"ct/internal/store"
"database/sql"
"fmt"
"time"
"github.com/gdamore/tcell/v2"
_ "github.com/mattn/go-sqlite3" //nolint
"github.com/rivo/tview"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func logCmd(cli *cli) *cobra.Command {
var cmd = &cobra.Command{
Use: "log",
}
cmd.AddCommand(promptLogCmd(cli))
cmd.AddCommand(createLogCmd(cli))
cmd.AddCommand(editLogCmd(cli))
return cmd
}
func promptLogCmd(cli *cli) *cobra.Command {
var flags struct {
Timestamp string
}
var cmd = &cobra.Command{
Use: "prompt",
Short: "Prompt a value for all metrics that havent been logged for a timestamp",
Long: `
Prompt a value for all metrics that havent been logged for a timestamp
EXAMPLES
- Prompt for all metrics with the current timestamp
$ ct log prompt
- Prompt for all metrics with the timestamp of 2020-01-01
$ ct log prompt --timestamp 2020-01-01
`,
PreRun: func(cmd *cobra.Command, args []string) {
_ = viper.BindPFlag("timestamp", cmd.Flags().Lookup("timestamp"))
},
RunE: func(cmd *cobra.Command, args []string) error {
db, err := sql.Open("sqlite3", cli.config.DBFile)
if err != nil {
return err
}
defer db.Close()
timestamp, err := parseTimestamp(flags.Timestamp)
if err != nil {
return err
}
s := store.NewStore(db)
ctx := context.Background()
metrics, err := s.Metric.SelectLimit(ctx, 0)
if err != nil {
return err
}
for _, metric := range metrics {
valueText, err := s.Config.SelectOne(ctx, metric.MetricID, "value_text")
if err != nil && err != store.ErrNotFound {
return err
}
if err != nil && err == store.ErrNotFound {
continue
}
_, err = s.Log.SelectOne(ctx, metric.MetricID, timestamp)
if err != nil && err != store.ErrNotFound {
return err
}
if err != nil && err == store.ErrNotFound {
// TODO: update getValueFromConsole to log an error type when no value is provided. This will allow ct to skip over the prompt when no value is submitted here.
value, _ := getValueFromConsole("", valueText)
if value != "" {
_, err = s.Log.Create(ctx, &store.Log{MetricID: metric.MetricID, Value: value, Timestamp: timestamp})
if err != nil {
return fmt.Errorf("Failed to create log: %s", err)
}
}
}
}
return nil
},
}
cmd.Flags().StringVar(&flags.Timestamp, "timestamp", time.Now().Format("2006-01-02"), "The timestamp of the metric (format: YYYY-MM-DD)")
return cmd
}
func createLogCmd(cli *cli) *cobra.Command {
var flags struct {
Timestamp string
Comment string
Update bool
Quiet bool
Feedback bool
}
var cmd = &cobra.Command{
Use: "create [command]",
Short: "Create a new log entry",
Long: `
Create a new log entry
EXAMPLES
- Create a new log entry for the weight metric and prompt for the value:
$ ct log create weight
- Same as above but specify the value on the command line:
$ ct log create weight 100
- Same as above but with the timestamp of 2020-01-01:
$ ct log create weight 95 --timestamp 2020-01-01
- Same as above but update an existing log entry for the same timestamp:
$ ct log create weight 96 --timestamp 2020-01-01 --update
- Create a new log entry with a comment:
$ ct log create walk 3.0 --comment "Walked to the beach and back"
`,
PreRun: func(cmd *cobra.Command, args []string) {
_ = viper.BindPFlag("timestamp", cmd.Flags().Lookup("timestamp"))
_ = viper.BindPFlag("quiet", cmd.Flags().Lookup("quiet"))
_ = viper.BindPFlag("update", cmd.Flags().Lookup("update"))
_ = viper.BindPFlag("comment", cmd.Flags().Lookup("comment"))
_ = viper.BindPFlag("feedback", cmd.Flags().Lookup("feedback"))
},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 2 {
return fmt.Errorf("Missing metric name and/or value")
}
m := args[0]
v := args[1]
db, err := sql.Open("sqlite3", cli.config.DBFile)
if err != nil {
return err
}
defer db.Close()
s := store.NewStore(db)
ctx := context.Background()
metric, err := s.Metric.SelectOne(ctx, m)
if err != nil && err != store.ErrNotFound {
return err
}
if err != nil && err == store.ErrNotFound {
return fmt.Errorf("Metric not found: %s", m)
}
timestamp, err := parseTimestamp(flags.Timestamp)
if err != nil {
return err
}
log, err := s.Log.SelectOne(ctx, metric.MetricID, timestamp)
if err != nil && err != store.ErrNotFound {
return err
}
if log != nil && !flags.Update {
if flags.Quiet {
return nil
}
return fmt.Errorf("log for %s with timestamp of %s already exists", metric.Name, timestamp.Format("2006-01-02"))
}
valueText, err := s.Config.SelectOne(ctx, metric.MetricID, "value_text")
if err != nil && err != store.ErrNotFound {
return err
}
value := v
if value != "" {
value, err = getValueFromConsole(v, valueText)
if err != nil {
return err
}
}
logFunc := s.Log.Create
if flags.Update {
logFunc = s.Log.Upsert
}
log, err = logFunc(ctx, &store.Log{MetricID: metric.MetricID, Value: value, Timestamp: timestamp})
if err != nil && !flags.Quiet {
return fmt.Errorf("Failed to create log: %s", err)
}
if flags.Comment != "" {
if err = s.LogComment.Upsert(ctx, log, flags.Comment); err != nil {
return fmt.Errorf("Failed to insert/update log comment: %s", err)
}
}
if flags.Feedback {
configMetricType, err := s.Config.SelectOne(ctx, metric.MetricID, "metric_type")
if err != nil && err != store.ErrNotFound {
return err
}
if err != nil && err == store.ErrNotFound {
return fmt.Errorf("Missing config option metric_type: %s", metric.Name)
}
switch configMetricType {
case "gauge":
r := report.NewReport(db, metric)
output, err := r.MonthlyGuage(ctx, report.WithStartTimestamp(time.Now().AddDate(0, -1, 0)))
if err != nil {
return err
}
cmd.Print(output)
case "counter":
r := report.NewReport(db, metric)
output, err := r.MonthlyCounter(ctx, report.WithStartTimestamp(time.Now().AddDate(0, -1, 0)))
if err != nil {
return err
}
cmd.Print(output)
}
}
return nil
},
}
cmd.Flags().BoolVar(&flags.Update, "update", false, "Update an existing log entry")
cmd.Flags().BoolVar(&flags.Quiet, "quiet", false, "Dont print warnings")
cmd.Flags().BoolVar(&flags.Feedback, "feedback", false, "Provide feedback when log created")
cmd.Flags().StringVar(&flags.Timestamp, "timestamp", time.Now().Format("2006-01-02"), "The timestamp of the metric (format: YYYY-MM-DD)")
cmd.Flags().StringVar(&flags.Comment, "comment", "", "A log comment")
return cmd
}
func editLogCmd(cli *cli) *cobra.Command {
var flags struct {
Timestamp string
}
var cmd = &cobra.Command{
Use: "edit [command]",
Long: `
Edit existing log entries
EXAMPLES
- Edit all logs for today
$ ct log edit
`,
PreRun: func(cmd *cobra.Command, args []string) {
_ = viper.BindPFlag("timestamp", cmd.Flags().Lookup("timestamp"))
},
RunE: func(cmd *cobra.Command, args []string) error {
db, err := sql.Open("sqlite3", cli.config.DBFile)
if err != nil {
return err
}
defer db.Close()
s := store.NewStore(db)
ctx := context.Background()
metrics, err := s.Metric.SelectLimit(ctx, 0)
if err != nil {
return err
}
timestamp, err := parseTimestamp(flags.Timestamp)
if err != nil {
return err
}
logs := []*store.Log{}
metricsToEdit := map[int64]store.Metric{}
for _, metric := range metrics {
log, err := s.Log.SelectOne(ctx, metric.MetricID, timestamp)
if err != nil && err != store.ErrNotFound {
return err
}
logs = append(logs, log)
metricsToEdit[metric.MetricID] = metric
}
for _, log := range logs {
metric, ok := metricsToEdit[log.MetricID]
if !ok {
return fmt.Errorf("Failed to find metric from log: %d", log.LogID)
}
valueText, err := s.Config.SelectOne(ctx, metric.MetricID, "value_text")
if err != nil && err != store.ErrNotFound {
return err
}
/*
value, err := getValueFromConsole(log.Value, valueText)
if err != nil {
return err
}
fmt.Printf("%#v, %#v, %s, %s, %s\n", log, metric, valueText, log.Value, value)
*/
app := tview.NewApplication()
inputField := tview.NewInputField()
inputField.SetLabel(fmt.Sprintf("%s ", valueText))
inputField.SetFieldWidth(0)
inputField.SetLabelColor(tcell.ColorDefault)
inputField.SetFieldBackgroundColor(tcell.ColorDefault)
inputField.SetPlaceholder(log.Value)
inputField.SetPlaceholderStyle(tcell.StyleDefault)
inputField.SetDoneFunc(func(key tcell.Key) { app.Stop() })
if err := app.SetRoot(inputField, true).SetFocus(inputField).Run(); err != nil {
panic(err)
}
inputValue := inputField.GetText()
if log.Value == "0" && inputValue != "" && inputValue != log.Value {
log.Value = inputValue
_, err = s.Log.Upsert(cmd.Context(), log)
if err != nil {
return err
}
}
}
return nil
},
}
cmd.Flags().StringVar(&flags.Timestamp, "timestamp", time.Now().Format("2006-01-02"), "The timestamp of the logs to edit (format: YYYY-MM-DD)")
return cmd
}