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

Support E&S #146

Merged
merged 6 commits into from
Apr 14, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
- Do not fail the build if `watch` fails to fetch Honeycomb URL (#126) | [@asdvalenzuela](https://github.com/asdvalenzuela)

### Maintenance

- Create draft gh release during publish (#124) | [@MikeGoldsmith](https://github.com/MikeGoldsmith)

## 0.7.0 - 2021-11-03
Expand Down
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,17 +408,17 @@ send_failure_trace:
# Positional argument reference

All the arguments to the various `buildevents` modes are listed above, but for
convenience, here is a summary of the three modes and the arguments that each
convenience, here is a summary of the modes and the arguments that each
requires.

The first argument is the running mode for this invocation of buildevents:
`build`, `step`, or `cmd` The remaining arguments differ depending on the
`build`, `watch`, `step`, or `cmd` The remaining arguments differ depending on the
mode.

arguments for the `build` mode:
1. `build_id` this is used as both the trace ID and to generate a URL to link back to the build
1. `start_time` used to calculate the total duration of the build
1. `status` should be `success` or `failure` and indicates whether the overall build succeeeded or failed
2. `start_time` used to calculate the total duration of the build
3. `status` should be `success` or `failure` and indicates whether the overall build succeeeded or failed

arguments for the `watch` mode:
1. `build_id` this is used as the trace ID
Expand All @@ -434,3 +434,18 @@ arguments for the `cmd` mode:
1. `step_id` buildevents expects a build to contain steps, and each step to have commands. The step ID is used to help construct this tree
1. `name` the name for this command, used in the Honeycomb UI
1. `--` double hyphen indicates the rest of the line will be the command to run

## Note
`name` is most useful if it is a low-cardinality value, usually something like the name of a step in your process. Using a low-cardinality value makes it valuable to do things like `GROUP BY name` in your queries.

## Differences between Classic and non-Classic environments

For "Honeycomb Classic", `buildevents` works almost the same as it always has. It has added service.name in addition to service_name; both fields have the same value.

In a non-Classic environment, there are several differences:
* Service Name, if specified, is used as the dataset as well as both `service_name` and `service.name` fields.
* if dataset is specified and service name is not, it will be used but will generate a warning.
* if both are specified, service name will be used, dataset is ignored, and a warning will be emitted (except in quiet mode)
* the command name is now sent as command_name (in classic it is sent as service_name)
* the watch command now sets the `name` field to merely `watch` rather than a high-cardinality value, making it easier to aggregate queries across different builds

8 changes: 5 additions & 3 deletions cmd_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@ a URL pointing to the generated trace in Honeycomb to STDOUT.`,
providerInfo(*ciProvider, ev)

ev.Add(map[string]interface{}{
"service_name": "build",
"service_name": ifClassic(cfg.APIKey, "build", cfg.Dataset),
"service.name": ifClassic(cfg.APIKey, "build", cfg.Dataset),
"command_name": "build",
"trace.span_id": traceID,
"name": "build " + traceID,
"status": outcome,
"duration_ms": time.Since(startTime) / time.Millisecond,
"source": "buildevents",
})
ev.Timestamp = startTime

Expand All @@ -56,9 +59,8 @@ a URL pointing to the generated trace in Honeycomb to STDOUT.`,
return buildCmd
}

// composer allows combining several PositionalArgs to work in concert.
func argOptions(pos int, opts ...string) cobra.PositionalArgs {
return composer(
return cobra.MatchAll(
cobra.MinimumNArgs(pos+1),
func(cmd *cobra.Command, args []string) error {
for _, opt := range opts {
Expand Down
9 changes: 6 additions & 3 deletions cmd_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ expressed as a single shell command - either a process like "go test" or a
shell script. The command to run is the final argument to buildevents and
will be launched via "bash -c" using "exec". The shell can be changed with the
-s/--shell flag.`,
Args: composer(
Args: cobra.MatchAll(
cobra.MinimumNArgs(4),
func(cmd *cobra.Command, args []string) error {
if cmd.ArgsLenAtDash() != 3 {
Expand Down Expand Up @@ -81,10 +81,13 @@ will be launched via "bash -c" using "exec". The shell can be changed with the
ev.Add(map[string]interface{}{
"trace.parent_id": stepID,
"trace.span_id": spanID,
"service_name": "cmd",
"service_name": ifClassic(cfg.APIKey, "cmd", cfg.Dataset),
"service.name": ifClassic(cfg.APIKey, "cmd", cfg.Dataset),
"command_name": "cmd",
"name": name,
"duration_ms": dur / time.Millisecond,
"cmd": subcmd,
"source": "buildevents",
})
ev.Timestamp = start

Expand Down Expand Up @@ -112,7 +115,7 @@ will be launched via "bash -c" using "exec". The shell can be changed with the
}

func runCommand(subcmd string, prop *propagation.PropagationContext, quiet bool, shell string) error {
if quiet == false {
if !quiet {
fmt.Println("running", shell, "-c", subcmd)
}
cmd := exec.Command(shell, "-c", subcmd)
Expand Down
46 changes: 45 additions & 1 deletion cmd_root.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,60 @@
package main

import (
"fmt"
"os"
"strings"

"github.com/spf13/cobra"

libhoney "github.com/honeycombio/libhoney-go"
)

func commandRoot(cfg *libhoney.Config, filename *string, ciProvider *string) *cobra.Command {
func commandRoot(cfg *libhoney.Config, filename *string, ciProvider *string, serviceName *string) *cobra.Command {
root := &cobra.Command{
Version: Version,
Use: "buildevents",
Short: "buildevents creates events for your CI builds",
Long: `
The buildevents executable creates Honeycomb events and tracing information
about your Continuous Integration builds.`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
quiet, _ := cmd.Flags().GetBool("quiet")
if isClassic(cfg.APIKey) {
// if we're in classic mode, we want to behave the same as we always have
if cfg.Dataset == "" {
cfg.Dataset = "buildevents"
}
if *serviceName != "" && !quiet {
fmt.Fprintf(os.Stderr, "WARN: classic mode ignores the service name parameter.\n")
}
} else {
// This is the non-classic behavior
if *serviceName != "" {
// service name was specified, so use it as the dataset
if cfg.Dataset != "" && !quiet {
// warn if we're going to ignore a specified dataset
fmt.Fprintf(os.Stderr, "WARN: service name was specified, dataset is ignored.\n")
}
trimmed := strings.TrimSpace(*serviceName)
if trimmed != *serviceName && !quiet {
fmt.Fprintf(os.Stderr, "WARN: service name contained leading or trailing whitespace, sending to '%s'.\n", trimmed)
}
cfg.Dataset = trimmed
} else {
// service_name was not specified
if cfg.Dataset != "" {
// dataset was, so use it but warn
if !quiet {
fmt.Fprintf(os.Stderr, "WARN: dataset is deprecated, please use service_name.\n")
}
} else {
// neither was specified, so just use the default
cfg.Dataset = "buildevents"
}
}
}
},
}

root.PersistentFlags().StringVarP(&cfg.APIKey, "apikey", "k", "", "[env.BUILDEVENT_APIKEY] the Honeycomb authentication token")
Expand All @@ -29,6 +68,11 @@ about your Continuous Integration builds.`,
root.PersistentFlags().Lookup("dataset").Value.Set(dataset)
}

root.PersistentFlags().StringVarP(serviceName, "service_name", "n", "", "[env.BUILDEVENT_SERVICE_NAME] the name of the service to which to send these events; overrides dataset")
if service_name, ok := os.LookupEnv("BUILDEVENT_SERVICE_NAME"); ok {
root.PersistentFlags().Lookup("service_name").Value.Set(service_name)
}

root.PersistentFlags().StringVarP(&cfg.APIHost, "apihost", "a", "https://api.honeycomb.io", "[env.BUILDEVENT_APIHOST] the hostname for the Honeycomb API server to which to send this event")
if apihost, ok := os.LookupEnv("BUILDEVENT_APIHOST"); ok {
root.PersistentFlags().Lookup("apihost").Value.Set(apihost)
Expand Down
5 changes: 4 additions & 1 deletion cmd_step.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ most closely maps to a single job. It should be run at the end of the step.`,
ev.Add(map[string]interface{}{
"trace.parent_id": traceID,
"trace.span_id": stepID,
"service_name": "step",
"service_name": ifClassic(cfg.APIKey, "step", cfg.Dataset),
"service.name": ifClassic(cfg.APIKey, "step", cfg.Dataset),
"command_name": "step",
"name": name,
"duration_ms": time.Since(startTime) / time.Millisecond,
"source": "buildevents",
})
ev.Timestamp = startTime

Expand Down
9 changes: 6 additions & 3 deletions cmd_watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func commandWatch(cfg *libhoney.Config, filename *string, ciProvider *string, wc
Polls the CircleCI API and waits until all jobs have finished (either
succeeded, failed, or are blocked). It then reports the final status of the
build with the appropriate timers.`,
Args: composer(
Args: cobra.MatchAll(
cobra.ExactArgs(1),
func(cmd *cobra.Command, args []string) error {
if *ciProvider != providerCircle {
Expand Down Expand Up @@ -69,11 +69,14 @@ build with the appropriate timers.`,
}

ev.Add(map[string]interface{}{
"service_name": "watch",
"service_name": ifClassic(cfg.APIKey, "watch", cfg.Dataset),
"service.name": ifClassic(cfg.APIKey, "watch", cfg.Dataset),
"command_name": "watch",
"trace.span_id": traceID,
"name": "watch " + traceID,
"name": ifClassic(cfg.APIKey, "watch "+traceID, "watch"),
"status": status,
"duration_ms": endTime.Sub(startTime) / time.Millisecond,
"source": "buildevents",
})
ev.Timestamp = startTime

Expand Down
41 changes: 26 additions & 15 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import (
"net/url"
"os"
"path"
"regexp"
"strconv"
"strings"
"time"

"github.com/kr/logfmt"
"github.com/spf13/cobra"

libhoney "github.com/honeycombio/libhoney-go"
"github.com/honeycombio/libhoney-go/transmission"
Expand Down Expand Up @@ -186,6 +186,12 @@ func parseUnix(ts string) time.Time {
return unix
}

// slugify turns a name into a slug. It is idempotent to things that are already slugs.
func slugify(name string) string {
slugReplaceRegex := regexp.MustCompile(`[^a-z0-9_~\.-]`)
return slugReplaceRegex.ReplaceAllString(strings.ToLower(name), "-")
}

func buildURL(cfg *libhoney.Config, traceID string, ts int64) (string, error) {
teamName, err := libhoney.VerifyAPIKey(*cfg)
if err != nil {
Expand All @@ -196,22 +202,27 @@ func buildURL(cfg *libhoney.Config, traceID string, ts int64) (string, error) {
if err != nil {
return "", fmt.Errorf("unable to infer UI host: %s", uiHost)
}
u.Path = path.Join(teamName, "datasets", strings.Replace(cfg.Dataset, " ", "-", -1), "trace")
u.Path = path.Join(teamName, "datasets", slugify(cfg.Dataset), "trace")
endTime := time.Now().Add(10 * time.Minute).Unix()
return fmt.Sprintf(
"%s?trace_id=%s&trace_start_ts=%d&trace_end_ts=%d",
u.String(), traceID, ts, endTime,
), nil

v := url.Values{}
v.Set("trace_id", traceID)
v.Set("trace_start_ts", strconv.FormatInt(ts, 10))
v.Set("trace_end_ts", strconv.FormatInt(endTime, 10))
u.RawQuery = v.Encode()

return u.String(), nil
}

// composer allows combining several PositionalArgs to work in concert.
func composer(pargs ...cobra.PositionalArgs) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
for _, parg := range pargs {
if err := parg(cmd, args); err != nil {
return err
}
}
return nil
// classic keys are always 32 bytes, non-classic are less than that
// classic keys are also hex but that doesn't matter here
func isClassic(key string) bool {
return len(key) == 32
}

func ifClassic(key, classicVal, elseVal string) string {
if isClassic(key) {
return classicVal
}
return elseVal
}
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ module github.com/honeycombio/buildevents
go 1.15

require (
github.com/honeycombio/beeline-go v1.7.0
github.com/honeycombio/beeline-go v1.8.0
github.com/honeycombio/libhoney-go v1.15.8
github.com/jszwedko/go-circleci v0.3.0
github.com/klauspost/compress v1.15.1 // indirect
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515
github.com/spf13/cobra v1.2.1
github.com/spf13/cobra v1.4.0
)

replace github.com/jszwedko/go-circleci => github.com/maplebed/go-circleci v0.0.0-20191121000249-089ef54587e5
Loading