diff --git a/cmd/maya-apiserver/app/command/start.go b/cmd/maya-apiserver/app/command/start.go index 40d8a7ce2a..14c0a1d29a 100644 --- a/cmd/maya-apiserver/app/command/start.go +++ b/cmd/maya-apiserver/app/command/start.go @@ -184,7 +184,8 @@ func Run(cmd *cobra.Command, c *CmdStartOptions) error { }() if env.Truthy(env.OpenEBSEnableAnalytics) { - usage.New().Build().InstallBuilder().Send() + usage.New().Build().InstallBuilder(true).Send() + go usage.PingCheck() } // Wait for exit diff --git a/cmd/maya-apiserver/app/server/volume_endpoint_v1alpha1.go b/cmd/maya-apiserver/app/server/volume_endpoint_v1alpha1.go index 5eb015fe17..8b541f4952 100644 --- a/cmd/maya-apiserver/app/server/volume_endpoint_v1alpha1.go +++ b/cmd/maya-apiserver/app/server/volume_endpoint_v1alpha1.go @@ -33,13 +33,14 @@ type volumeAPIOpsV1alpha1 struct { resp http.ResponseWriter } +// volumeEvents sends anonymous volume (de)-provision events func volumeEvents(cvol *v1alpha1.CASVolume, method string) { if menv.Truthy(menv.OpenEBSEnableAnalytics) && cvol != nil { usage.New().Build().ApplicationBuilder(). SetApplicationName(cvol.Spec.CasType). SetDocumentTitle(cvol.ObjectMeta.Name). - SetLabel("capacity"). - SetAction("replica:" + cvol.Spec.Replicas). + SetLabel(usage.EventLabelCapacity). + SetReplicaCount(cvol.Spec.Replicas, method). SetCategory(method). SetVolumeCapacity(cvol.Spec.Capacity).Send() } @@ -122,7 +123,7 @@ func (v *volumeAPIOpsV1alpha1) create() (*v1alpha1.CASVolume, error) { return nil, CodedError(400, err.Error()) } cvol, err := vOps.Create() - volumeEvents(vol, "volume-provision") + volumeEvents(vol, usage.VolumeProvision) if err != nil { glog.Errorf("failed to create cas template based volume: error '%s'", err.Error()) return nil, CodedError(500, err.Error()) @@ -213,7 +214,7 @@ func (v *volumeAPIOpsV1alpha1) delete(volumeName string) (*v1alpha1.CASVolume, e } cvol, err := vOps.Delete() - volumeEvents(vol, "volume-deprovision") + volumeEvents(vol, usage.VolumeDeprovision) if err != nil { glog.Errorf("failed to delete cas template based volume: error '%s'", err.Error()) if isNotFound(err) { diff --git a/pkg/env/v1alpha1/env.go b/pkg/env/v1alpha1/env.go index 79e2478fcd..e1ce0e1d8e 100644 --- a/pkg/env/v1alpha1/env.go +++ b/pkg/env/v1alpha1/env.go @@ -184,6 +184,20 @@ func Get(envKey ENVKey) (value string) { return getEnv(string(envKey)) } +// GetOrDefault fetches value from the provided environment variable +// which on empty returns the defaultValue +// NOTE: os.Getenv is used here instead of os.LookupEnv because it is +// not required to know if the environment variable is defined on the system +func GetOrDefault(e ENVKey, defaultValue string) (value string) { + envValue := Get(e) + if len(envValue) == 0 { + // ENV not defined or set to "" + return defaultValue + } else { + return envValue + } +} + // Lookup looks up an environment variable // // NOTE: diff --git a/pkg/usage/const.go b/pkg/usage/const.go new file mode 100644 index 0000000000..3c8a03f2c7 --- /dev/null +++ b/pkg/usage/const.go @@ -0,0 +1,43 @@ +/* +Copyright 2018 The OpenEBS Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package usage + +const ( + // GAclientID is the unique code of OpenEBS project in Google Analytics + GAclientID string = "UA-127388617-1" + + // supported events categories + + // Install event is sent on pod starts + InstallEvent string = "install" + // Ping event is sent periodically + Ping string = "ping" + // VolumeProvision event is sent when a volume is created + VolumeProvision string = "volume-provision" + //VolumeDeprovision event is sent when a volume is deleted + VolumeDeprovision string = "volume-deprovision" + + AppName string = "OpenEBS" + + // Event labels + RunningStatus string = "running" + EventLabelNode string = "nodes" + EventLabelCapacity string = "capacity" + + // Event action + Replica string = "replica:" + DefaultReplicaCount string = "replica:3" +) diff --git a/pkg/usage/ping.go b/pkg/usage/ping.go new file mode 100644 index 0000000000..c562e73f2b --- /dev/null +++ b/pkg/usage/ping.go @@ -0,0 +1,63 @@ +/* +Copyright 2018 The OpenEBS Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package usage + +import ( + "time" + + menv "github.com/openebs/maya/pkg/env/v1alpha1" +) + +var OpenEBSPingPeriod menv.ENVKey = "OPENEBS_IO_ANALYTICS_PING_INTERVAL" + +const ( + // defaultPingPeriod sets the default ping heartbeat interval + defaultPingPeriod time.Duration = 24 * time.Hour + // minimumPingPeriod sets the minimum possible configurable + // heartbeat period, if a value lower than this will be set, the + // defaultPingPeriod will be used + minimumPingPeriod time.Duration = 1 * time.Hour +) + +// PingCheck sends ping events to Google Analytics +func PingCheck() { + // Create a new usage field + u := New() + duration := getPingPeriod() + ticker := time.NewTicker(duration) + for _ = range ticker.C { + u.Build(). + InstallBuilder(true). + SetCategory(Ping). + Send() + } +} + +// getPingPeriod sets the duration of health events, defaults to 24 +func getPingPeriod() time.Duration { + value := menv.GetOrDefault(OpenEBSPingPeriod, string(defaultPingPeriod)) + duration, _ := time.ParseDuration(value) + // Sanitychecks for setting time duration of health events + // This way, we are checking for negative and zero time duration and we + // also have a minimum possible configurable time duration between health events + if duration < minimumPingPeriod { + // Avoid corner case when the ENV value is undesirable + return time.Duration(defaultPingPeriod) + } else { + return time.Duration(duration) + } +} diff --git a/pkg/usage/ping_test.go b/pkg/usage/ping_test.go new file mode 100644 index 0000000000..ae98768ee5 --- /dev/null +++ b/pkg/usage/ping_test.go @@ -0,0 +1,42 @@ +package usage + +import ( + "os" + "testing" + "time" +) + +func TestGetPingPeriod(t *testing.T) { + beforeFunc := func(value string) { + if err := os.Setenv(string(OpenEBSPingPeriod), value); err != nil { + t.Logf("Unable to set environment variable") + } + } + afterFunc := func() { + if err := os.Unsetenv(string(OpenEBSPingPeriod)); err != nil { + t.Logf("Unable to unset environment variable") + } + } + testSuite := map[string]struct { + OpenEBSPingPeriodValue string + ExpectedPeriodValue time.Duration + }{ + "24 seconds": {"24s", 86400000000000}, + "24 minutes": {"24m", 86400000000000}, + "24 hours": {"24h", 86400000000000}, + "Negative 24 hours": {"-24h", 86400000000000}, + "Random string input": {"Apache", 86400000000000}, + "Two hours": {"2h", 7200000000000}, + "Three hundred hours": {"300h", 1080000000000000}, + "Fifty two seconds": {"52000000000ns", 86400000000000}, + "Empty env value": {"", 86400000000000}, + } + for testKey, testData := range testSuite { + beforeFunc(testData.OpenEBSPingPeriodValue) + evaluatedValue := getPingPeriod() + if evaluatedValue != testData.ExpectedPeriodValue { + t.Fatalf("Tests failed for %s, expected=%d, got=%d", testKey, testData.ExpectedPeriodValue, evaluatedValue) + } + afterFunc() + } +} diff --git a/pkg/usage/size.go b/pkg/usage/size.go index f74b4a4604..fe6c37a8da 100644 --- a/pkg/usage/size.go +++ b/pkg/usage/size.go @@ -19,6 +19,7 @@ package usage import units "github.com/docker/go-units" // toGigaUnits converts a size from xB to bytes where x={k,m,g,t,p...} +// and return the number of Gigabytes as an integer // 1 gigabyte=1000 megabyte func toGigaUnits(size string) (int64, error) { sizeInBytes, err := units.FromHumanSize(size) diff --git a/pkg/usage/usage.go b/pkg/usage/usage.go index 4dfe4a4cce..53c6171dd4 100644 --- a/pkg/usage/usage.go +++ b/pkg/usage/usage.go @@ -19,11 +19,6 @@ import ( k8sapi "github.com/openebs/maya/pkg/client/k8s/v1alpha1" ) -const ( - // GAclientID is the unique code of OpenEBS project in Google Analytics - GAclientID = "UA-127388617-1" -) - // Usage struct represents all information about a usage metric sent to // Google Analytics with respect to the application type Usage struct { @@ -178,8 +173,8 @@ func (u *Usage) SetValue(v int64) *Usage { func (u *Usage) Build() *Usage { // Default ApplicationID for openebs project is OpenEBS v := NewVersion() - v.getVersion() - u.SetApplicationID("OpenEBS"). + v.getVersion(false) + u.SetApplicationID(AppName). SetTrackingID(GAclientID). SetClientID(v.id) // TODO: Add condition for version over-ride @@ -191,7 +186,7 @@ func (u *Usage) Build() *Usage { // for non install events func (u *Usage) ApplicationBuilder() *Usage { v := NewVersion() - v.getVersion() + v.getVersion(false) u.SetApplicationVersion(v.openebsVersion). SetApplicationName(v.k8sArch). SetApplicationInstallerID(v.k8sVersion). @@ -206,17 +201,33 @@ func (u *Usage) SetVolumeCapacity(volCapG string) *Usage { return u } +// Wrapper for setting replica count for volume events +// NOTE: This doesn't get the replica count in a volume de-provision event. +// TODO: Pick the current value of replica-count from the CAS-engine +func (u *Usage) SetReplicaCount(count, method string) *Usage { + if method == VolumeProvision && count == "" { + // Case: When volume-provision the replica count isn't specified + // it is set to three by default by the m-apiserver + u.SetAction(DefaultReplicaCount) + } else { + // Catch all case for volume-deprovision event and + // volume-provision event with an overriden replica-count + u.SetAction(Replica + count) + } + return u +} + // InstallBuilder is a concrete builder for install events -func (u *Usage) InstallBuilder() *Usage { +func (u *Usage) InstallBuilder(override bool) *Usage { v := NewVersion() clusterSize, _ := k8sapi.NumberOfNodes() - v.getVersion() + v.getVersion(override) u.SetApplicationVersion(v.openebsVersion). SetApplicationName(v.k8sArch). SetApplicationInstallerID(v.k8sVersion). SetDataSource(v.nodeType). SetDocumentTitle(v.id). - SetApplicationID("OpenEBS"). - NewEvent("install", "running", "nodes", int64(clusterSize)) + SetApplicationID(AppName). + NewEvent(InstallEvent, RunningStatus, EventLabelNode, int64(clusterSize)) return u } diff --git a/pkg/usage/versionset.go b/pkg/usage/versionset.go index 52d6106325..a65626a562 100644 --- a/pkg/usage/versionset.go +++ b/pkg/usage/versionset.go @@ -76,16 +76,16 @@ func (v *versionSet) fetchAndSetVersion() error { } // getVersion is a wrapper over fetchAndSetVersion -func (v *versionSet) getVersion() error { - // If ENVs aren't set, fetch the required values from the - // K8s APIserver - if _, present := env.Lookup(openEBSversion); !present { +func (v *versionSet) getVersion(override bool) error { + // If ENVs aren't set or the override is true, fetch the required + // values from the K8s APIserver + if _, present := env.Lookup(openEBSversion); !present || override { if err := v.fetchAndSetVersion(); err != nil { glog.Error(err.Error()) return err } } - // Fetch data from ENV instead + // Fetch data from ENV v.id = env.Get(clusterUUID) v.k8sArch = env.Get(clusterArch) v.k8sVersion = env.Get(clusterVersion)