Skip to content

Commit

Permalink
Merge pull request #230 from CircleCI-Public/CIRCLE-14938-expose-orb-…
Browse files Browse the repository at this point in the history
…usage-stats

[CIRCLE-14938]: Expose orb usage stats
  • Loading branch information
Zachary Scott authored Dec 17, 2018
2 parents 8bee1e6 + 2b24b06 commit de475a4
Show file tree
Hide file tree
Showing 19 changed files with 932 additions and 439 deletions.
78 changes: 71 additions & 7 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package api

import (
"context"
"encoding/json"
"io/ioutil"
"log"
"os"
"sort"
"strings"

"fmt"
Expand Down Expand Up @@ -211,19 +213,60 @@ type OrbsForListing struct {
Namespace string `json:"namespace,omitempty"`
}

// OrbWithData wraps an orb with select fields for deserializing into JSON.
type OrbWithData struct {
// SortBy allows us to sort a collection of orbs by builds, projects, or orgs from the last 30 days of data.
func (orbs *OrbsForListing) SortBy(sortBy string) {
switch sortBy {
case "builds":
sort.Slice(orbs.Orbs, func(i, j int) bool {
return orbs.Orbs[i].Statistics.Last30DaysBuildCount > orbs.Orbs[j].Statistics.Last30DaysBuildCount
})
case "projects":
sort.Slice(orbs.Orbs, func(i, j int) bool {
return orbs.Orbs[i].Statistics.Last30DaysProjectCount > orbs.Orbs[j].Statistics.Last30DaysProjectCount
})
case "orgs":
sort.Slice(orbs.Orbs, func(i, j int) bool {
return orbs.Orbs[i].Statistics.Last30DaysOrganizationCount > orbs.Orbs[j].Statistics.Last30DaysOrganizationCount
})
}
}

// OrbBase represents the minimum fields we wish to serialize for orbs.
// This type can be embedded for extending orbs with more data. e.g. OrbWithData
type OrbBase struct {
Name string `json:"name"`
HighestVersion string `json:"version"`
Versions []struct {
Version string `json:"version"`
Source string `json:"source"`
} `json:"versions"`
}

// OrbWithData extends the OrbBase type with additional data used for printing.
type OrbWithData struct {
OrbBase

Statistics struct {
Last30DaysBuildCount int
Last30DaysProjectCount int
Last30DaysOrganizationCount int
}

Commands map[string]OrbElement
Jobs map[string]OrbElement
Executors map[string]OrbElement
}

// MarshalJSON allows us to leave out excess fields we don't want to serialize.
// As is the case with commands/jobs/executors and now statistics.
func (orb OrbWithData) MarshalJSON() ([]byte, error) {
orbForJSON := OrbBase{
orb.Name,
orb.HighestVersion,
orb.Versions,
}

// These fields are printing manually when --details flag is added so hidden from JSON output.
Commands map[string]OrbElement `json:"-"`
Jobs map[string]OrbElement `json:"-"`
Executors map[string]OrbElement `json:"-"`
return json.Marshal(orbForJSON)
}

// OrbElementParameter represents the yaml-unmarshled contents of
Expand Down Expand Up @@ -275,6 +318,12 @@ type Orb struct {
Source string
HighestVersion string `json:"version"`

Statistics struct {
Last30DaysBuildCount int
Last30DaysProjectCount int
Last30DaysOrganizationCount int
}

Commands map[string]OrbElement
Jobs map[string]OrbElement
Executors map[string]OrbElement
Expand Down Expand Up @@ -858,6 +907,11 @@ func OrbInfo(opts Options, orbRef string) (*OrbVersion, error) {
id
createdAt
name
statistics {
last30DaysBuildCount,
last30DaysProjectCount,
last30DaysOrganizationCount
}
versions {
createdAt
version
Expand Down Expand Up @@ -910,6 +964,11 @@ query ListOrbs ($after: String!, $certifiedOnly: Boolean!) {
cursor
node {
name
statistics {
last30DaysBuildCount,
last30DaysProjectCount,
last30DaysOrganizationCount
}
versions(count: 1) {
version,
source
Expand All @@ -921,7 +980,7 @@ query ListOrbs ($after: String!, $certifiedOnly: Boolean!) {
}
}
}
`
`

var orbs OrbsForListing

Expand Down Expand Up @@ -986,6 +1045,11 @@ query namespaceOrbs ($namespace: String, $after: String!) {
version
}
name
statistics {
last30DaysBuildCount,
last30DaysProjectCount,
last30DaysOrganizationCount
}
}
}
totalCount
Expand Down
29 changes: 21 additions & 8 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
Expand Down Expand Up @@ -196,9 +197,9 @@ func (cl *Client) Run(ctx context.Context, request *Request, resp interface{}) e
return err
}
defer func() {
err := res.Body.Close()
if err != nil {
l.Printf(err.Error())
responseBodyCloseErr := res.Body.Close()
if responseBodyCloseErr != nil {
l.Printf(responseBodyCloseErr.Error())
}
}()

Expand All @@ -207,10 +208,26 @@ func (cl *Client) Run(ctx context.Context, request *Request, resp interface{}) e
l.Printf("<< result status: %s", res.Status)
}

if res.StatusCode != 200 {
if res.StatusCode != http.StatusOK {
return fmt.Errorf("failure calling GraphQL API: %s", res.Status)
}

// Request.Body is an io.ReadCloser it can only be read once
if cl.Debug {
var bodyBytes []byte
if res.Body != nil {
bodyBytes, err = ioutil.ReadAll(res.Body)
if err != nil {
return errors.Wrap(err, "reading response")
}

l.Printf("<< %s", string(bodyBytes))

// Restore the io.ReadCloser to its original state
res.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
}
}

wrappedResponse := &Response{
Data: resp,
}
Expand All @@ -223,9 +240,5 @@ func (cl *Client) Run(ctx context.Context, request *Request, resp interface{}) e
return wrappedResponse.Errors
}

if cl.Debug {
l.Printf("<< %+v", resp)
}

return nil
}
2 changes: 1 addition & 1 deletion cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"gopkg.in/yaml.v2"
yaml "gopkg.in/yaml.v2"
)

type buildOptions struct {
Expand Down
57 changes: 38 additions & 19 deletions cmd/cmd_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,25 +84,44 @@ func appendPostHandler(server *ghttp.Server, authToken string, combineHandlers .
responseBody = fmt.Sprintf("{ \"data\": %s, \"errors\": %s}", handler.Response, handler.ErrorResponse)
}

server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", "/graphql-unstable"),
ghttp.VerifyHeader(http.Header{
"Authorization": []string{authToken},
}),
ghttp.VerifyContentType("application/json; charset=utf-8"),
// From Gomegas ghttp.VerifyJson to avoid the
// VerifyContentType("application/json") check
// that fails with "application/json; charset=utf-8"
func(w http.ResponseWriter, req *http.Request) {
body, err := ioutil.ReadAll(req.Body)
req.Body.Close()
Expect(err).ShouldNot(HaveOccurred())
Expect(body).Should(MatchJSON(handler.Request), "JSON Mismatch")
},
ghttp.RespondWith(handler.Status, responseBody),
),
)
if authToken == "" {
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", "/graphql-unstable"),
ghttp.VerifyContentType("application/json; charset=utf-8"),
// From Gomegas ghttp.VerifyJson to avoid the
// VerifyContentType("application/json") check
// that fails with "application/json; charset=utf-8"
func(w http.ResponseWriter, req *http.Request) {
body, err := ioutil.ReadAll(req.Body)
req.Body.Close()
Expect(err).ShouldNot(HaveOccurred())
Expect(body).Should(MatchJSON(handler.Request), "JSON Mismatch")
},
ghttp.RespondWith(handler.Status, responseBody),
),
)
} else {
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", "/graphql-unstable"),
ghttp.VerifyHeader(http.Header{
"Authorization": []string{authToken},
}),
ghttp.VerifyContentType("application/json; charset=utf-8"),
// From Gomegas ghttp.VerifyJson to avoid the
// VerifyContentType("application/json") check
// that fails with "application/json; charset=utf-8"
func(w http.ResponseWriter, req *http.Request) {
body, err := ioutil.ReadAll(req.Body)
req.Body.Close()
Expect(err).ShouldNot(HaveOccurred())
Expect(body).Should(MatchJSON(handler.Request), "JSON Mismatch")
},
ghttp.RespondWith(handler.Status, responseBody),
),
)
}
}
}

Expand Down
Loading

0 comments on commit de475a4

Please sign in to comment.