provider/newrelic: Add new provider for New Relic
This commit is contained in:
parent
f39cfe61ce
commit
3dfe5a47f1
|
@ -0,0 +1,29 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/logging"
|
||||||
|
newrelic "github.com/paultyng/go-newrelic/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config contains New Relic provider settings
|
||||||
|
type Config struct {
|
||||||
|
APIKey string
|
||||||
|
APIURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client returns a new client for accessing New Relic
|
||||||
|
func (c *Config) Client() (*newrelic.Client, error) {
|
||||||
|
nrConfig := newrelic.Config{
|
||||||
|
APIKey: c.APIKey,
|
||||||
|
Debug: logging.IsDebugOrHigher(),
|
||||||
|
BaseURL: c.APIURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
client := newrelic.New(nrConfig)
|
||||||
|
|
||||||
|
log.Printf("[INFO] New Relic client configured")
|
||||||
|
|
||||||
|
return &client, nil
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
newrelic "github.com/paultyng/go-newrelic/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func dataSourceNewRelicApplication() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Read: dataSourceNewRelicApplicationRead,
|
||||||
|
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"name": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"instance_ids": {
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Elem: &schema.Schema{Type: schema.TypeInt},
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"host_ids": {
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Elem: &schema.Schema{Type: schema.TypeInt},
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dataSourceNewRelicApplicationRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*newrelic.Client)
|
||||||
|
|
||||||
|
log.Printf("[INFO] Reading New Relic applications")
|
||||||
|
|
||||||
|
applications, err := client.ListApplications()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var application *newrelic.Application
|
||||||
|
name := d.Get("name").(string)
|
||||||
|
|
||||||
|
for _, a := range applications {
|
||||||
|
if a.Name == name {
|
||||||
|
application = &a
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if application == nil {
|
||||||
|
return fmt.Errorf("The name '%s' does not match any New Relic applications.", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId(strconv.Itoa(application.ID))
|
||||||
|
d.Set("name", application.Name)
|
||||||
|
d.Set("instance_ids", application.Links.InstanceIDs)
|
||||||
|
d.Set("host_ids", application.Links.HostIDs)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccNewRelicApplication_Basic(t *testing.T) {
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccNewRelicApplicationConfig(),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccNewRelicApplication("data.newrelic_application.app"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccNewRelicApplication(n string) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
r := s.RootModule().Resources[n]
|
||||||
|
a := r.Primary.Attributes
|
||||||
|
|
||||||
|
if a["id"] == "" {
|
||||||
|
return fmt.Errorf("Expected to get an application from New Relic")
|
||||||
|
}
|
||||||
|
|
||||||
|
if a["name"] != testAccExpectedApplicationName {
|
||||||
|
return fmt.Errorf("Expected the application name to be: %s, but got: %s", testAccExpectedApplicationName, a["name"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The test application for this data source is created in provider_test.go
|
||||||
|
func testAccNewRelicApplicationConfig() string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
data "newrelic_application" "app" {
|
||||||
|
name = "%s"
|
||||||
|
}
|
||||||
|
`, testAccExpectedApplicationName)
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseIDs(serializedID string, count int) ([]int, error) {
|
||||||
|
rawIDs := strings.SplitN(serializedID, ":", count)
|
||||||
|
if len(rawIDs) != count {
|
||||||
|
return []int{}, fmt.Errorf("Unable to parse ID %v", serializedID)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]int, count)
|
||||||
|
|
||||||
|
for i, rawID := range rawIDs {
|
||||||
|
id, err := strconv.ParseInt(rawID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return ids, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ids[i] = int(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func serializeIDs(ids []int) string {
|
||||||
|
idStrings := make([]string, len(ids))
|
||||||
|
|
||||||
|
for i, id := range ids {
|
||||||
|
idStrings[i] = strconv.Itoa(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(idStrings, ":")
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestParseIDs_Basic(t *testing.T) {
|
||||||
|
ids, err := parseIDs("1:2", 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ids) != 2 {
|
||||||
|
t.Fatal(len(ids))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ids[0] != 1 || ids[1] != 2 {
|
||||||
|
t.Fatal(ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSerializeIDs_Basic(t *testing.T) {
|
||||||
|
id := serializeIDs([]int{1, 2})
|
||||||
|
|
||||||
|
if id != "1:2" {
|
||||||
|
t.Fatal(id)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/acctest"
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccNewRelicAlertChannel_import(t *testing.T) {
|
||||||
|
resourceName := "newrelic_alert_channel.foo"
|
||||||
|
rName := acctest.RandString(5)
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckNewRelicAlertChannelDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckNewRelicAlertChannelConfig(rName),
|
||||||
|
},
|
||||||
|
|
||||||
|
resource.TestStep{
|
||||||
|
ResourceName: resourceName,
|
||||||
|
ImportState: true,
|
||||||
|
ImportStateVerify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/acctest"
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccNewRelicAlertCondition_import(t *testing.T) {
|
||||||
|
resourceName := "newrelic_alert_condition.foo"
|
||||||
|
rName := acctest.RandString(5)
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckNewRelicAlertConditionDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckNewRelicAlertConditionConfig(rName),
|
||||||
|
},
|
||||||
|
|
||||||
|
resource.TestStep{
|
||||||
|
ResourceName: resourceName,
|
||||||
|
ImportState: true,
|
||||||
|
ImportStateVerify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/acctest"
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccNewRelicAlertPolicy_import(t *testing.T) {
|
||||||
|
resourceName := "newrelic_alert_policy.foo"
|
||||||
|
rName := acctest.RandString(5)
|
||||||
|
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckNewRelicAlertPolicyDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckNewRelicAlertPolicyConfig(rName),
|
||||||
|
},
|
||||||
|
|
||||||
|
resource.TestStep{
|
||||||
|
ResourceName: resourceName,
|
||||||
|
ImportState: true,
|
||||||
|
ImportStateVerify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provider represents a resource provider in Terraform
|
||||||
|
func Provider() terraform.ResourceProvider {
|
||||||
|
return &schema.Provider{
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"api_key": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
DefaultFunc: schema.EnvDefaultFunc("NEWRELIC_API_KEY", nil),
|
||||||
|
Sensitive: true,
|
||||||
|
},
|
||||||
|
"api_url": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
DefaultFunc: schema.EnvDefaultFunc("NEWRELIC_API_URL", "https://api.newrelic.com/v2"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
DataSourcesMap: map[string]*schema.Resource{
|
||||||
|
"newrelic_application": dataSourceNewRelicApplication(),
|
||||||
|
},
|
||||||
|
|
||||||
|
ResourcesMap: map[string]*schema.Resource{
|
||||||
|
"newrelic_alert_channel": resourceNewRelicAlertChannel(),
|
||||||
|
"newrelic_alert_condition": resourceNewRelicAlertCondition(),
|
||||||
|
"newrelic_alert_policy": resourceNewRelicAlertPolicy(),
|
||||||
|
"newrelic_alert_policy_channel": resourceNewRelicAlertPolicyChannel(),
|
||||||
|
},
|
||||||
|
|
||||||
|
ConfigureFunc: providerConfigure,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func providerConfigure(data *schema.ResourceData) (interface{}, error) {
|
||||||
|
config := Config{
|
||||||
|
APIKey: data.Get("api_key").(string),
|
||||||
|
APIURL: data.Get("api_url").(string),
|
||||||
|
}
|
||||||
|
log.Println("[INFO] Initializing New Relic client")
|
||||||
|
return config.Client()
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/acctest"
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
newrelic "github.com/newrelic/go-agent"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testAccExpectedApplicationName string
|
||||||
|
testAccProviders map[string]terraform.ResourceProvider
|
||||||
|
testAccProvider *schema.Provider
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
testAccExpectedApplicationName = fmt.Sprintf("tf_test_%s", acctest.RandString(10))
|
||||||
|
testAccProvider = Provider().(*schema.Provider)
|
||||||
|
testAccProviders = map[string]terraform.ResourceProvider{
|
||||||
|
"newrelic": testAccProvider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider(t *testing.T) {
|
||||||
|
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProviderImpl(t *testing.T) {
|
||||||
|
var _ terraform.ResourceProvider = Provider()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccPreCheck(t *testing.T) {
|
||||||
|
if v := os.Getenv("NEWRELIC_API_KEY"); v == "" {
|
||||||
|
t.Log(v)
|
||||||
|
t.Fatal("NEWRELIC_API_KEY must be set for acceptance tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup fake application by logging some metrics
|
||||||
|
if v := os.Getenv("NEWRELIC_LICENSE_KEY"); len(v) > 0 {
|
||||||
|
config := newrelic.NewConfig(testAccExpectedApplicationName, v)
|
||||||
|
app, err := newrelic.NewApplication(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.Fatal("Error setting up New Relic application")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.WaitForConnection(30 * time.Second); err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.Fatal("Unable to setup New Relic application connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.RecordCustomEvent("terraform test", nil); err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
t.Fatal("Unable to record custom event in New Relic")
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Shutdown(30 * time.Second)
|
||||||
|
} else {
|
||||||
|
t.Log(v)
|
||||||
|
t.Fatal("NEWRELIC_LICENSE_KEY must be set for acceptance tests")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/hashicorp/terraform/helper/validation"
|
||||||
|
newrelic "github.com/paultyng/go-newrelic/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var alertChannelTypes = map[string][]string{
|
||||||
|
"campfire": []string{
|
||||||
|
"room",
|
||||||
|
"subdomain",
|
||||||
|
"token",
|
||||||
|
},
|
||||||
|
"email": []string{
|
||||||
|
"include_json_attachment",
|
||||||
|
"recipients",
|
||||||
|
},
|
||||||
|
"hipchat": []string{
|
||||||
|
"auth_token",
|
||||||
|
"base_url",
|
||||||
|
"room_id",
|
||||||
|
},
|
||||||
|
"opsgenie": []string{
|
||||||
|
"api_key",
|
||||||
|
"recipients",
|
||||||
|
"tags",
|
||||||
|
"teams",
|
||||||
|
},
|
||||||
|
"pagerduty": []string{
|
||||||
|
"service_key",
|
||||||
|
},
|
||||||
|
"slack": []string{
|
||||||
|
"channel",
|
||||||
|
"url",
|
||||||
|
},
|
||||||
|
"user": []string{
|
||||||
|
"user_id",
|
||||||
|
},
|
||||||
|
"victorops": []string{
|
||||||
|
"key",
|
||||||
|
"route_key",
|
||||||
|
},
|
||||||
|
"webhook": []string{
|
||||||
|
"auth_password",
|
||||||
|
"auth_type",
|
||||||
|
"auth_username",
|
||||||
|
"base_url",
|
||||||
|
"headers",
|
||||||
|
"payload_type",
|
||||||
|
"payload",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNewRelicAlertChannel() *schema.Resource {
|
||||||
|
validAlertChannelTypes := make([]string, 0, len(alertChannelTypes))
|
||||||
|
for k := range alertChannelTypes {
|
||||||
|
validAlertChannelTypes = append(validAlertChannelTypes, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceNewRelicAlertChannelCreate,
|
||||||
|
Read: resourceNewRelicAlertChannelRead,
|
||||||
|
// Update: Not currently supported in API
|
||||||
|
Delete: resourceNewRelicAlertChannelDelete,
|
||||||
|
Importer: &schema.ResourceImporter{
|
||||||
|
State: schema.ImportStatePassthrough,
|
||||||
|
},
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"name": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
ValidateFunc: validation.StringInSlice(validAlertChannelTypes, false),
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
Type: schema.TypeMap,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
//TODO: ValidateFunc: (use list of keys from map above)
|
||||||
|
Sensitive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAlertChannelStruct(d *schema.ResourceData) *newrelic.AlertChannel {
|
||||||
|
channel := newrelic.AlertChannel{
|
||||||
|
Name: d.Get("name").(string),
|
||||||
|
Type: d.Get("type").(string),
|
||||||
|
Configuration: d.Get("configuration").(map[string]interface{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &channel
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNewRelicAlertChannelCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*newrelic.Client)
|
||||||
|
channel := buildAlertChannelStruct(d)
|
||||||
|
|
||||||
|
log.Printf("[INFO] Creating New Relic alert channel %s", channel.Name)
|
||||||
|
|
||||||
|
channel, err := client.CreateAlertChannel(*channel)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId(strconv.Itoa(channel.ID))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNewRelicAlertChannelRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*newrelic.Client)
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(d.Id(), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[INFO] Reading New Relic alert channel %v", id)
|
||||||
|
|
||||||
|
channel, err := client.GetAlertChannel(int(id))
|
||||||
|
if err != nil {
|
||||||
|
if err == newrelic.ErrNotFound {
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Set("name", channel.Name)
|
||||||
|
d.Set("type", channel.Type)
|
||||||
|
if err := d.Set("configuration", channel.Configuration); err != nil {
|
||||||
|
return fmt.Errorf("[DEBUG] Error setting Alert Channel Configuration: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNewRelicAlertChannelDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*newrelic.Client)
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(d.Id(), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[INFO] Deleting New Relic alert channel %v", id)
|
||||||
|
|
||||||
|
if err := client.DeleteAlertChannel(int(id)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId("")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/acctest"
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
newrelic "github.com/paultyng/go-newrelic/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccNewRelicAlertChannel_Basic(t *testing.T) {
|
||||||
|
rName := acctest.RandString(5)
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckNewRelicAlertChannelDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckNewRelicAlertChannelConfig(rName),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckNewRelicAlertChannelExists("newrelic_alert_channel.foo"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_channel.foo", "name", fmt.Sprintf("tf-test-%s", rName)),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_channel.foo", "type", "email"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_channel.foo", "configuration.recipients", "foo@example.com"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_channel.foo", "configuration.include_json_attachment", "1"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckNewRelicAlertChannelConfigUpdated(rName),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckNewRelicAlertChannelExists("newrelic_alert_channel.foo"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_channel.foo", "name", fmt.Sprintf("tf-test-updated-%s", rName)),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_channel.foo", "type", "email"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_channel.foo", "configuration.recipients", "bar@example.com"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_channel.foo", "configuration.include_json_attachment", "0"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNewRelicAlertChannelDestroy(s *terraform.State) error {
|
||||||
|
client := testAccProvider.Meta().(*newrelic.Client)
|
||||||
|
for _, r := range s.RootModule().Resources {
|
||||||
|
if r.Type != "newrelic_alert_channel" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(r.Primary.ID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.GetAlertChannel(int(id))
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("Alert channel still exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNewRelicAlertChannelExists(n string) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
rs, ok := s.RootModule().Resources[n]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Not found: %s", n)
|
||||||
|
}
|
||||||
|
if rs.Primary.ID == "" {
|
||||||
|
return fmt.Errorf("No channel ID is set")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := testAccProvider.Meta().(*newrelic.Client)
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(rs.Primary.ID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
found, err := client.GetAlertChannel(int(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if strconv.Itoa(found.ID) != rs.Primary.ID {
|
||||||
|
return fmt.Errorf("Channel not found: %v - %v", rs.Primary.ID, found)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNewRelicAlertChannelConfig(rName string) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "newrelic_alert_channel" "foo" {
|
||||||
|
name = "tf-test-%s"
|
||||||
|
type = "email"
|
||||||
|
|
||||||
|
configuration = {
|
||||||
|
recipients = "foo@example.com"
|
||||||
|
include_json_attachment = "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, rName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNewRelicAlertChannelConfigUpdated(rName string) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "newrelic_alert_channel" "foo" {
|
||||||
|
name = "tf-test-updated-%s"
|
||||||
|
type = "email"
|
||||||
|
|
||||||
|
configuration = {
|
||||||
|
recipients = "bar@example.com"
|
||||||
|
include_json_attachment = "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, rName)
|
||||||
|
}
|
|
@ -0,0 +1,342 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/hashicorp/terraform/helper/validation"
|
||||||
|
newrelic "github.com/paultyng/go-newrelic/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var alertConditionTypes = map[string][]string{
|
||||||
|
"apm_app_metric": []string{
|
||||||
|
"apdex",
|
||||||
|
"error_percentage",
|
||||||
|
"response_time_background",
|
||||||
|
"response_time_web",
|
||||||
|
"throughput_background",
|
||||||
|
"throughput_web",
|
||||||
|
"user_defined",
|
||||||
|
},
|
||||||
|
"apm_kt_metric": []string{
|
||||||
|
"apdex",
|
||||||
|
"error_count",
|
||||||
|
"error_percentage",
|
||||||
|
"response_time",
|
||||||
|
"throughput",
|
||||||
|
},
|
||||||
|
"browser_metric": []string{
|
||||||
|
"ajax_response_time",
|
||||||
|
"ajax_throughput",
|
||||||
|
"dom_processing",
|
||||||
|
"end_user_apdex",
|
||||||
|
"network",
|
||||||
|
"page_rendering",
|
||||||
|
"page_view_throughput",
|
||||||
|
"page_views_with_js_errors",
|
||||||
|
"request_queuing",
|
||||||
|
"total_page_load",
|
||||||
|
"user_defined",
|
||||||
|
"web_application",
|
||||||
|
},
|
||||||
|
"mobile_metric": []string{
|
||||||
|
"database",
|
||||||
|
"images",
|
||||||
|
"json",
|
||||||
|
"mobile_crash_rate",
|
||||||
|
"network_error_percentage",
|
||||||
|
"network",
|
||||||
|
"status_error_percentage",
|
||||||
|
"user_defined",
|
||||||
|
"view_loading",
|
||||||
|
},
|
||||||
|
"servers_metric": []string{
|
||||||
|
"cpu_percentage",
|
||||||
|
"disk_io_percentage",
|
||||||
|
"fullest_disk_percentage",
|
||||||
|
"load_average_one_minute",
|
||||||
|
"memory_percentage",
|
||||||
|
"user_defined",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNewRelicAlertCondition() *schema.Resource {
|
||||||
|
validAlertConditionTypes := make([]string, 0, len(alertConditionTypes))
|
||||||
|
for k := range alertConditionTypes {
|
||||||
|
validAlertConditionTypes = append(validAlertConditionTypes, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceNewRelicAlertConditionCreate,
|
||||||
|
Read: resourceNewRelicAlertConditionRead,
|
||||||
|
Update: resourceNewRelicAlertConditionUpdate,
|
||||||
|
Delete: resourceNewRelicAlertConditionDelete,
|
||||||
|
Importer: &schema.ResourceImporter{
|
||||||
|
State: schema.ImportStatePassthrough,
|
||||||
|
},
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"policy_id": {
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ValidateFunc: validation.StringInSlice(validAlertConditionTypes, false),
|
||||||
|
},
|
||||||
|
"entities": {
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Elem: &schema.Schema{Type: schema.TypeInt},
|
||||||
|
Required: true,
|
||||||
|
MinItems: 1,
|
||||||
|
},
|
||||||
|
"metric": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
//TODO: ValidateFunc from map
|
||||||
|
},
|
||||||
|
"runbook_url": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"term": {
|
||||||
|
Type: schema.TypeList,
|
||||||
|
Elem: &schema.Resource{
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"duration": {
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Required: true,
|
||||||
|
ValidateFunc: intInSlice([]int{5, 10, 15, 30, 60, 120}),
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Default: "equal",
|
||||||
|
ValidateFunc: validation.StringInSlice([]string{"above", "below", "equal"}, false),
|
||||||
|
},
|
||||||
|
"priority": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Default: "critical",
|
||||||
|
ValidateFunc: validation.StringInSlice([]string{"critical", "warning"}, false),
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
Type: schema.TypeFloat,
|
||||||
|
Required: true,
|
||||||
|
ValidateFunc: float64Gte(0.0),
|
||||||
|
},
|
||||||
|
"time_function": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ValidateFunc: validation.StringInSlice([]string{"all", "any"}, false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: true,
|
||||||
|
MinItems: 1,
|
||||||
|
},
|
||||||
|
"user_defined_metric": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"user_defined_value_function": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
ValidateFunc: validation.StringInSlice([]string{"average", "min", "max", "total", "sample_size"}, false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAlertConditionStruct(d *schema.ResourceData) *newrelic.AlertCondition {
|
||||||
|
entitySet := d.Get("entities").([]interface{})
|
||||||
|
entities := make([]string, len(entitySet))
|
||||||
|
|
||||||
|
for i, entity := range entitySet {
|
||||||
|
entities[i] = strconv.Itoa(entity.(int))
|
||||||
|
}
|
||||||
|
|
||||||
|
termSet := d.Get("term").([]interface{})
|
||||||
|
terms := make([]newrelic.AlertConditionTerm, len(termSet))
|
||||||
|
|
||||||
|
for i, termI := range termSet {
|
||||||
|
termM := termI.(map[string]interface{})
|
||||||
|
|
||||||
|
terms[i] = newrelic.AlertConditionTerm{
|
||||||
|
Duration: termM["duration"].(int),
|
||||||
|
Operator: termM["operator"].(string),
|
||||||
|
Priority: termM["priority"].(string),
|
||||||
|
Threshold: termM["threshold"].(float64),
|
||||||
|
TimeFunction: termM["time_function"].(string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
condition := newrelic.AlertCondition{
|
||||||
|
Type: d.Get("type").(string),
|
||||||
|
Name: d.Get("name").(string),
|
||||||
|
Enabled: true,
|
||||||
|
Entities: entities,
|
||||||
|
Metric: d.Get("metric").(string),
|
||||||
|
Terms: terms,
|
||||||
|
PolicyID: d.Get("policy_id").(int),
|
||||||
|
}
|
||||||
|
|
||||||
|
if attr, ok := d.GetOk("runbook_url"); ok {
|
||||||
|
condition.RunbookURL = attr.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if attrM, ok := d.GetOk("user_defined_metric"); ok {
|
||||||
|
if attrVF, ok := d.GetOk("user_defined_value_function"); ok {
|
||||||
|
condition.UserDefined = newrelic.AlertConditionUserDefined{
|
||||||
|
Metric: attrM.(string),
|
||||||
|
ValueFunction: attrVF.(string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &condition
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAlertConditionStruct(condition *newrelic.AlertCondition, d *schema.ResourceData) error {
|
||||||
|
ids, err := parseIDs(d.Id(), 2)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
policyID := ids[0]
|
||||||
|
|
||||||
|
entities := make([]int, len(condition.Entities))
|
||||||
|
for i, entity := range condition.Entities {
|
||||||
|
v, err := strconv.ParseInt(entity, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
entities[i] = int(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Set("policy_id", policyID)
|
||||||
|
d.Set("name", condition.Name)
|
||||||
|
d.Set("type", condition.Type)
|
||||||
|
d.Set("metric", condition.Metric)
|
||||||
|
d.Set("runbook_url", condition.RunbookURL)
|
||||||
|
d.Set("user_defined_metric", condition.UserDefined.Metric)
|
||||||
|
d.Set("user_defined_value_function", condition.UserDefined.ValueFunction)
|
||||||
|
if err := d.Set("entities", entities); err != nil {
|
||||||
|
return fmt.Errorf("[DEBUG] Error setting alert condition entities: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var terms []map[string]interface{}
|
||||||
|
|
||||||
|
for _, src := range condition.Terms {
|
||||||
|
dst := map[string]interface{}{
|
||||||
|
"duration": src.Duration,
|
||||||
|
"operator": src.Operator,
|
||||||
|
"priority": src.Priority,
|
||||||
|
"threshold": src.Threshold,
|
||||||
|
"time_function": src.TimeFunction,
|
||||||
|
}
|
||||||
|
terms = append(terms, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.Set("term", terms); err != nil {
|
||||||
|
return fmt.Errorf("[DEBUG] Error setting alert condition terms: %#v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNewRelicAlertConditionCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*newrelic.Client)
|
||||||
|
condition := buildAlertConditionStruct(d)
|
||||||
|
|
||||||
|
log.Printf("[INFO] Creating New Relic alert condition %s", condition.Name)
|
||||||
|
|
||||||
|
condition, err := client.CreateAlertCondition(*condition)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId(serializeIDs([]int{condition.PolicyID, condition.ID}))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNewRelicAlertConditionRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*newrelic.Client)
|
||||||
|
|
||||||
|
log.Printf("[INFO] Reading New Relic alert condition %s", d.Id())
|
||||||
|
|
||||||
|
ids, err := parseIDs(d.Id(), 2)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
policyID := ids[0]
|
||||||
|
id := ids[1]
|
||||||
|
|
||||||
|
condition, err := client.GetAlertCondition(policyID, id)
|
||||||
|
if err != nil {
|
||||||
|
if err == newrelic.ErrNotFound {
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return readAlertConditionStruct(condition, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNewRelicAlertConditionUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*newrelic.Client)
|
||||||
|
condition := buildAlertConditionStruct(d)
|
||||||
|
|
||||||
|
ids, err := parseIDs(d.Id(), 2)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
policyID := ids[0]
|
||||||
|
id := ids[1]
|
||||||
|
|
||||||
|
condition.PolicyID = policyID
|
||||||
|
condition.ID = id
|
||||||
|
|
||||||
|
log.Printf("[INFO] Updating New Relic alert condition %d", id)
|
||||||
|
|
||||||
|
updatedCondition, err := client.UpdateAlertCondition(*condition)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return readAlertConditionStruct(updatedCondition, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNewRelicAlertConditionDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*newrelic.Client)
|
||||||
|
|
||||||
|
ids, err := parseIDs(d.Id(), 2)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
policyID := ids[0]
|
||||||
|
id := ids[1]
|
||||||
|
|
||||||
|
log.Printf("[INFO] Deleting New Relic alert condition %d", id)
|
||||||
|
|
||||||
|
if err := client.DeleteAlertCondition(policyID, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId("")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,189 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/acctest"
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
newrelic "github.com/paultyng/go-newrelic/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccNewRelicAlertCondition_Basic(t *testing.T) {
|
||||||
|
rName := acctest.RandString(5)
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckNewRelicAlertConditionDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckNewRelicAlertConditionConfig(rName),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckNewRelicAlertConditionExists("newrelic_alert_condition.foo"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "name", fmt.Sprintf("tf-test-%s", rName)),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "type", "apm_app_metric"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "runbook_url", "https://foo.example.com"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "entities.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "entities.0", "12345"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "term.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "term.0.duration", "5"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "term.0.operator", "below"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "term.0.priority", "critical"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "term.0.threshold", "0.75"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "term.0.time_function", "all"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckNewRelicAlertConditionConfigUpdated(rName),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckNewRelicAlertConditionExists("newrelic_alert_condition.foo"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "name", fmt.Sprintf("tf-test-updated-%s", rName)),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "runbook_url", "https://bar.example.com"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "entities.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "entities.0", "67890"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "term.#", "1"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "term.0.duration", "10"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "term.0.operator", "below"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "term.0.priority", "critical"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "term.0.threshold", "0.65"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_condition.foo", "term.0.time_function", "all"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: func TestAccNewRelicAlertCondition_Multi(t *testing.T) {
|
||||||
|
|
||||||
|
func testAccCheckNewRelicAlertConditionDestroy(s *terraform.State) error {
|
||||||
|
client := testAccProvider.Meta().(*newrelic.Client)
|
||||||
|
for _, r := range s.RootModule().Resources {
|
||||||
|
if r.Type != "newrelic_alert_condition" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ids, err := parseIDs(r.Primary.ID, 2)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
policyID := ids[0]
|
||||||
|
id := ids[1]
|
||||||
|
|
||||||
|
_, err = client.GetAlertCondition(policyID, id)
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("Alert condition still exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNewRelicAlertConditionExists(n string) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
rs, ok := s.RootModule().Resources[n]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Not found: %s", n)
|
||||||
|
}
|
||||||
|
if rs.Primary.ID == "" {
|
||||||
|
return fmt.Errorf("No alert condition ID is set")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := testAccProvider.Meta().(*newrelic.Client)
|
||||||
|
|
||||||
|
ids, err := parseIDs(rs.Primary.ID, 2)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
policyID := ids[0]
|
||||||
|
id := ids[1]
|
||||||
|
|
||||||
|
found, err := client.GetAlertCondition(policyID, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if found.ID != id {
|
||||||
|
return fmt.Errorf("Alert condition not found: %v - %v", id, found)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNewRelicAlertConditionConfig(rName string) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "newrelic_alert_policy" "foo" {
|
||||||
|
name = "tf-test-%[1]s"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "newrelic_alert_condition" "foo" {
|
||||||
|
policy_id = "${newrelic_alert_policy.foo.id}"
|
||||||
|
|
||||||
|
name = "tf-test-%[1]s"
|
||||||
|
type = "apm_app_metric"
|
||||||
|
entities = ["12345"]
|
||||||
|
metric = "apdex"
|
||||||
|
runbook_url = "https://foo.example.com"
|
||||||
|
|
||||||
|
term {
|
||||||
|
duration = 5
|
||||||
|
operator = "below"
|
||||||
|
priority = "critical"
|
||||||
|
threshold = "0.75"
|
||||||
|
time_function = "all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, rName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNewRelicAlertConditionConfigUpdated(rName string) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "newrelic_alert_policy" "foo" {
|
||||||
|
name = "tf-test-updated-%[1]s"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "newrelic_alert_condition" "foo" {
|
||||||
|
policy_id = "${newrelic_alert_policy.foo.id}"
|
||||||
|
|
||||||
|
name = "tf-test-updated-%[1]s"
|
||||||
|
type = "apm_app_metric"
|
||||||
|
entities = ["67890"]
|
||||||
|
metric = "apdex"
|
||||||
|
runbook_url = "https://bar.example.com"
|
||||||
|
|
||||||
|
term {
|
||||||
|
duration = 10
|
||||||
|
operator = "below"
|
||||||
|
priority = "critical"
|
||||||
|
threshold = "0.65"
|
||||||
|
time_function = "all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, rName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: const testAccCheckNewRelicAlertConditionConfigMulti = `
|
|
@ -0,0 +1,119 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/hashicorp/terraform/helper/validation"
|
||||||
|
newrelic "github.com/paultyng/go-newrelic/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resourceNewRelicAlertPolicy() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceNewRelicAlertPolicyCreate,
|
||||||
|
Read: resourceNewRelicAlertPolicyRead,
|
||||||
|
// Update: Not currently supported in API
|
||||||
|
Delete: resourceNewRelicAlertPolicyDelete,
|
||||||
|
Importer: &schema.ResourceImporter{
|
||||||
|
State: schema.ImportStatePassthrough,
|
||||||
|
},
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"name": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"incident_preference": {
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Default: "PER_POLICY",
|
||||||
|
ForceNew: true,
|
||||||
|
ValidateFunc: validation.StringInSlice([]string{"PER_POLICY", "PER_CONDITION", "PER_CONDITION_AND_TARGET"}, false),
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAlertPolicyStruct(d *schema.ResourceData) *newrelic.AlertPolicy {
|
||||||
|
policy := newrelic.AlertPolicy{
|
||||||
|
Name: d.Get("name").(string),
|
||||||
|
}
|
||||||
|
|
||||||
|
if attr, ok := d.GetOk("incident_preference"); ok {
|
||||||
|
policy.IncidentPreference = attr.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &policy
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNewRelicAlertPolicyCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*newrelic.Client)
|
||||||
|
policy := buildAlertPolicyStruct(d)
|
||||||
|
|
||||||
|
log.Printf("[INFO] Creating New Relic alert policy %s", policy.Name)
|
||||||
|
|
||||||
|
policy, err := client.CreateAlertPolicy(*policy)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId(strconv.Itoa(policy.ID))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNewRelicAlertPolicyRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*newrelic.Client)
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(d.Id(), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[INFO] Reading New Relic alert policy %v", id)
|
||||||
|
|
||||||
|
policy, err := client.GetAlertPolicy(int(id))
|
||||||
|
if err != nil {
|
||||||
|
if err == newrelic.ErrNotFound {
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Set("name", policy.Name)
|
||||||
|
d.Set("incident_preference", policy.IncidentPreference)
|
||||||
|
d.Set("created_at", policy.CreatedAt)
|
||||||
|
d.Set("updated_at", policy.UpdatedAt)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNewRelicAlertPolicyDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*newrelic.Client)
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(d.Id(), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[INFO] Deleting New Relic alert policy %v", id)
|
||||||
|
|
||||||
|
if err := client.DeleteAlertPolicy(int(id)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId("")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
newrelic "github.com/paultyng/go-newrelic/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func policyChannelExists(client *newrelic.Client, policyID int, channelID int) (bool, error) {
|
||||||
|
channel, err := client.GetAlertChannel(channelID)
|
||||||
|
if err != nil {
|
||||||
|
if err == newrelic.ErrNotFound {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range channel.Links.PolicyIDs {
|
||||||
|
if id == policyID {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNewRelicAlertPolicyChannel() *schema.Resource {
|
||||||
|
return &schema.Resource{
|
||||||
|
Create: resourceNewRelicAlertPolicyChannelCreate,
|
||||||
|
Read: resourceNewRelicAlertPolicyChannelRead,
|
||||||
|
// Update: Not currently supported in API
|
||||||
|
Delete: resourceNewRelicAlertPolicyChannelDelete,
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"policy_id": {
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
"channel_id": {
|
||||||
|
Type: schema.TypeInt,
|
||||||
|
Required: true,
|
||||||
|
ForceNew: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNewRelicAlertPolicyChannelCreate(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*newrelic.Client)
|
||||||
|
|
||||||
|
policyID := d.Get("policy_id").(int)
|
||||||
|
channelID := d.Get("channel_id").(int)
|
||||||
|
|
||||||
|
serializedID := serializeIDs([]int{policyID, channelID})
|
||||||
|
|
||||||
|
log.Printf("[INFO] Creating New Relic alert policy channel %s", serializedID)
|
||||||
|
|
||||||
|
exists, err := policyChannelExists(client, policyID, channelID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
err = client.UpdateAlertPolicyChannels(policyID, []int{channelID})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId(serializedID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNewRelicAlertPolicyChannelRead(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*newrelic.Client)
|
||||||
|
|
||||||
|
ids, err := parseIDs(d.Id(), 2)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
policyID := ids[0]
|
||||||
|
channelID := ids[1]
|
||||||
|
|
||||||
|
log.Printf("[INFO] Reading New Relic alert policy channel %s", d.Id())
|
||||||
|
|
||||||
|
exists, err := policyChannelExists(client, policyID, channelID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
d.SetId("")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Set("policy_id", policyID)
|
||||||
|
d.Set("channel_id", channelID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceNewRelicAlertPolicyChannelDelete(d *schema.ResourceData, meta interface{}) error {
|
||||||
|
client := meta.(*newrelic.Client)
|
||||||
|
|
||||||
|
ids, err := parseIDs(d.Id(), 2)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
policyID := ids[0]
|
||||||
|
channelID := ids[1]
|
||||||
|
|
||||||
|
log.Printf("[INFO] Deleting New Relic alert policy channel %s", d.Id())
|
||||||
|
|
||||||
|
exists, err := policyChannelExists(client, policyID, channelID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
if err := client.DeleteAlertPolicyChannel(policyID, channelID); err != nil {
|
||||||
|
switch err {
|
||||||
|
case newrelic.ErrNotFound:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetId("")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/acctest"
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
newrelic "github.com/paultyng/go-newrelic/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccNewRelicAlertPolicyChannel_Basic(t *testing.T) {
|
||||||
|
rName := acctest.RandString(5)
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckNewRelicAlertPolicyChannelDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckNewRelicAlertPolicyChannelConfig(rName),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckNewRelicAlertPolicyChannelExists("newrelic_alert_policy_channel.foo"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckNewRelicAlertPolicyChannelConfigUpdated(rName),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckNewRelicAlertPolicyChannelExists("newrelic_alert_policy_channel.foo"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNewRelicAlertPolicyChannelDestroy(s *terraform.State) error {
|
||||||
|
client := testAccProvider.Meta().(*newrelic.Client)
|
||||||
|
for _, r := range s.RootModule().Resources {
|
||||||
|
if r.Type != "newrelic_alert_policy_channel" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ids, err := parseIDs(r.Primary.ID, 2)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
policyID := ids[0]
|
||||||
|
channelID := ids[1]
|
||||||
|
|
||||||
|
exists, err := policyChannelExists(client, policyID, channelID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return fmt.Errorf("Resource still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNewRelicAlertPolicyChannelExists(n string) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
rs, ok := s.RootModule().Resources[n]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Not found: %s", n)
|
||||||
|
}
|
||||||
|
if rs.Primary.ID == "" {
|
||||||
|
return fmt.Errorf("No resource ID is set")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := testAccProvider.Meta().(*newrelic.Client)
|
||||||
|
|
||||||
|
ids, err := parseIDs(rs.Primary.ID, 2)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
policyID := ids[0]
|
||||||
|
channelID := ids[1]
|
||||||
|
|
||||||
|
exists, err := policyChannelExists(client, policyID, channelID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("Resource not found: %v", rs.Primary.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNewRelicAlertPolicyChannelConfig(rName string) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "newrelic_alert_policy" "foo" {
|
||||||
|
name = "tf-test-%[1]s"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "newrelic_alert_channel" "foo" {
|
||||||
|
name = "tf-test-%[1]s"
|
||||||
|
type = "email"
|
||||||
|
|
||||||
|
configuration = {
|
||||||
|
recipients = "foo@example.com"
|
||||||
|
include_json_attachment = "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "newrelic_alert_policy_channel" "foo" {
|
||||||
|
policy_id = "${newrelic_alert_policy.foo.id}"
|
||||||
|
channel_id = "${newrelic_alert_channel.foo.id}"
|
||||||
|
}
|
||||||
|
`, rName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNewRelicAlertPolicyChannelConfigUpdated(rName string) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "newrelic_alert_policy" "bar" {
|
||||||
|
name = "tf-test-updated-%[1]s"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "newrelic_alert_channel" "foo" {
|
||||||
|
name = "tf-test-updated-%[1]s"
|
||||||
|
type = "email"
|
||||||
|
|
||||||
|
configuration = {
|
||||||
|
recipients = "bar@example.com"
|
||||||
|
include_json_attachment = "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "newrelic_alert_policy_channel" "foo" {
|
||||||
|
policy_id = "${newrelic_alert_policy.bar.id}"
|
||||||
|
channel_id = "${newrelic_alert_channel.foo.id}"
|
||||||
|
}
|
||||||
|
`, rName)
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/acctest"
|
||||||
|
"github.com/hashicorp/terraform/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
newrelic "github.com/paultyng/go-newrelic/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccNewRelicAlertPolicy_Basic(t *testing.T) {
|
||||||
|
rName := acctest.RandString(5)
|
||||||
|
resource.Test(t, resource.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Providers: testAccProviders,
|
||||||
|
CheckDestroy: testAccCheckNewRelicAlertPolicyDestroy,
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckNewRelicAlertPolicyConfig(rName),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckNewRelicAlertPolicyExists("newrelic_alert_policy.foo"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_policy.foo", "name", fmt.Sprintf("tf-test-%s", rName)),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_policy.foo", "incident_preference", "PER_POLICY"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
resource.TestStep{
|
||||||
|
Config: testAccCheckNewRelicAlertPolicyConfigUpdated(rName),
|
||||||
|
Check: resource.ComposeTestCheckFunc(
|
||||||
|
testAccCheckNewRelicAlertPolicyExists("newrelic_alert_policy.foo"),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_policy.foo", "name", fmt.Sprintf("tf-test-updated-%s", rName)),
|
||||||
|
resource.TestCheckResourceAttr(
|
||||||
|
"newrelic_alert_policy.foo", "incident_preference", "PER_CONDITION"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNewRelicAlertPolicyDestroy(s *terraform.State) error {
|
||||||
|
client := testAccProvider.Meta().(*newrelic.Client)
|
||||||
|
for _, r := range s.RootModule().Resources {
|
||||||
|
if r.Type != "newrelic_alert_policy" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(r.Primary.ID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.GetAlertPolicy(int(id))
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("Policy still exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNewRelicAlertPolicyExists(n string) resource.TestCheckFunc {
|
||||||
|
return func(s *terraform.State) error {
|
||||||
|
rs, ok := s.RootModule().Resources[n]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Not found: %s", n)
|
||||||
|
}
|
||||||
|
if rs.Primary.ID == "" {
|
||||||
|
return fmt.Errorf("No policy ID is set")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := testAccProvider.Meta().(*newrelic.Client)
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(rs.Primary.ID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
found, err := client.GetAlertPolicy(int(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if strconv.Itoa(found.ID) != rs.Primary.ID {
|
||||||
|
return fmt.Errorf("Policy not found: %v - %v", rs.Primary.ID, found)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNewRelicAlertPolicyConfig(rName string) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "newrelic_alert_policy" "foo" {
|
||||||
|
name = "tf-test-%s"
|
||||||
|
}
|
||||||
|
`, rName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckNewRelicAlertPolicyConfigUpdated(rName string) string {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
resource "newrelic_alert_policy" "foo" {
|
||||||
|
name = "tf-test-updated-%s"
|
||||||
|
incident_preference = "PER_CONDITION"
|
||||||
|
}
|
||||||
|
`, rName)
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func float64Gte(gte float64) schema.SchemaValidateFunc {
|
||||||
|
return func(i interface{}, k string) (s []string, es []error) {
|
||||||
|
v, ok := i.(float64)
|
||||||
|
if !ok {
|
||||||
|
es = append(es, fmt.Errorf("expected type of %s to be float64", k))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if v >= gte {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
es = append(es, fmt.Errorf("expected %s to be greater than or equal to %v, got %v", k, gte, v))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func intInSlice(valid []int) schema.SchemaValidateFunc {
|
||||||
|
return func(i interface{}, k string) (s []string, es []error) {
|
||||||
|
v, ok := i.(int)
|
||||||
|
if !ok {
|
||||||
|
es = append(es, fmt.Errorf("expected type of %s to be int", k))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range valid {
|
||||||
|
if v == p {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
es = append(es, fmt.Errorf("expected %s to be one of %v, got %v", k, valid, v))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
val interface{}
|
||||||
|
f schema.SchemaValidateFunc
|
||||||
|
expectedErr *regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidationIntInInSlice(t *testing.T) {
|
||||||
|
runTestCases(t, []testCase{
|
||||||
|
{
|
||||||
|
val: 2,
|
||||||
|
f: intInSlice([]int{1, 2, 3}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
val: 4,
|
||||||
|
f: intInSlice([]int{1, 2, 3}),
|
||||||
|
expectedErr: regexp.MustCompile("expected [\\w]+ to be one of \\[1 2 3\\], got 4"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
val: "foo",
|
||||||
|
f: intInSlice([]int{1, 2, 3}),
|
||||||
|
expectedErr: regexp.MustCompile("expected type of [\\w]+ to be int"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidationFloat64Gte(t *testing.T) {
|
||||||
|
runTestCases(t, []testCase{
|
||||||
|
{
|
||||||
|
val: 1.1,
|
||||||
|
f: float64Gte(1.1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
val: 1.2,
|
||||||
|
f: float64Gte(1.1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
val: "foo",
|
||||||
|
f: float64Gte(1.1),
|
||||||
|
expectedErr: regexp.MustCompile("expected type of [\\w]+ to be float64"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
val: 0.1,
|
||||||
|
f: float64Gte(1.1),
|
||||||
|
expectedErr: regexp.MustCompile("expected [\\w]+ to be greater than or equal to 1.1, got 0.1"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTestCases(t *testing.T, cases []testCase) {
|
||||||
|
matchErr := func(errs []error, r *regexp.Regexp) bool {
|
||||||
|
// err must match one provided
|
||||||
|
for _, err := range errs {
|
||||||
|
if r.MatchString(err.Error()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
_, errs := tc.f(tc.val, "test_property")
|
||||||
|
|
||||||
|
if len(errs) == 0 && tc.expectedErr == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matchErr(errs, tc.expectedErr) {
|
||||||
|
t.Fatalf("expected test case %d to produce error matching \"%s\", got %v", i, tc.expectedErr, errs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ import (
|
||||||
logentriesprovider "github.com/hashicorp/terraform/builtin/providers/logentries"
|
logentriesprovider "github.com/hashicorp/terraform/builtin/providers/logentries"
|
||||||
mailgunprovider "github.com/hashicorp/terraform/builtin/providers/mailgun"
|
mailgunprovider "github.com/hashicorp/terraform/builtin/providers/mailgun"
|
||||||
mysqlprovider "github.com/hashicorp/terraform/builtin/providers/mysql"
|
mysqlprovider "github.com/hashicorp/terraform/builtin/providers/mysql"
|
||||||
|
newrelicprovider "github.com/hashicorp/terraform/builtin/providers/newrelic"
|
||||||
nomadprovider "github.com/hashicorp/terraform/builtin/providers/nomad"
|
nomadprovider "github.com/hashicorp/terraform/builtin/providers/nomad"
|
||||||
nullprovider "github.com/hashicorp/terraform/builtin/providers/null"
|
nullprovider "github.com/hashicorp/terraform/builtin/providers/null"
|
||||||
openstackprovider "github.com/hashicorp/terraform/builtin/providers/openstack"
|
openstackprovider "github.com/hashicorp/terraform/builtin/providers/openstack"
|
||||||
|
@ -99,6 +100,7 @@ var InternalProviders = map[string]plugin.ProviderFunc{
|
||||||
"logentries": logentriesprovider.Provider,
|
"logentries": logentriesprovider.Provider,
|
||||||
"mailgun": mailgunprovider.Provider,
|
"mailgun": mailgunprovider.Provider,
|
||||||
"mysql": mysqlprovider.Provider,
|
"mysql": mysqlprovider.Provider,
|
||||||
|
"newrelic": newrelicprovider.Provider,
|
||||||
"nomad": nomadprovider.Provider,
|
"nomad": nomadprovider.Provider,
|
||||||
"null": nullprovider.Provider,
|
"null": nullprovider.Provider,
|
||||||
"openstack": openstackprovider.Provider,
|
"openstack": openstackprovider.Provider,
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
## ChangeLog
|
||||||
|
|
||||||
|
## 1.5.0
|
||||||
|
|
||||||
|
* Added support for Windows. Thanks to @ianomad and @lvxv for the contributions.
|
||||||
|
|
||||||
|
* The number of heap objects allocated is recorded in the
|
||||||
|
`Memory/Heap/AllocatedObjects` metric. This will soon be displayed on the "Go
|
||||||
|
runtime" page.
|
||||||
|
|
||||||
|
* If the [DatastoreSegment](https://godoc.org/github.com/newrelic/go-agent#DatastoreSegment)
|
||||||
|
fields `Host` and `PortPathOrID` are not provided, they will no longer appear
|
||||||
|
as `"unknown"` in transaction traces and slow query traces.
|
||||||
|
|
||||||
|
* Stack traces will now be nicely aligned in the APM UI.
|
||||||
|
|
||||||
|
## 1.4.0
|
||||||
|
|
||||||
|
* Added support for slow query traces. Slow datastore segments will now
|
||||||
|
generate slow query traces viewable on the datastore tab. These traces include
|
||||||
|
a stack trace and help you to debug slow datastore activity.
|
||||||
|
[Slow Query Documentation](https://docs.newrelic.com/docs/apm/applications-menu/monitoring/viewing-slow-query-details)
|
||||||
|
|
||||||
|
* Added new
|
||||||
|
[DatastoreSegment](https://godoc.org/github.com/newrelic/go-agent#DatastoreSegment)
|
||||||
|
fields `ParameterizedQuery`, `QueryParameters`, `Host`, `PortPathOrID`, and
|
||||||
|
`DatabaseName`. These fields will be shown in transaction traces and in slow
|
||||||
|
query traces.
|
||||||
|
|
||||||
|
## 1.3.0
|
||||||
|
|
||||||
|
* Breaking Change: Added a timeout parameter to the `Application.Shutdown` method.
|
||||||
|
|
||||||
|
## 1.2.0
|
||||||
|
|
||||||
|
* Added support for instrumenting short-lived processes:
|
||||||
|
* The new `Application.Shutdown` method allows applications to report
|
||||||
|
data to New Relic without waiting a full minute.
|
||||||
|
* The new `Application.WaitForConnection` method allows your process to
|
||||||
|
defer instrumentation until the application is connected and ready to
|
||||||
|
gather data.
|
||||||
|
* Full documentation here: [application.go](application.go)
|
||||||
|
* Example short-lived process: [examples/short-lived-process/main.go](examples/short-lived-process/main.go)
|
||||||
|
|
||||||
|
* Error metrics are no longer created when `ErrorCollector.Enabled = false`.
|
||||||
|
|
||||||
|
* Added support for [github.com/mgutz/logxi](github.com/mgutz/logxi). See
|
||||||
|
[_integrations/nrlogxi/v1/nrlogxi.go](_integrations/nrlogxi/v1/nrlogxi.go).
|
||||||
|
|
||||||
|
* Fixed bug where Transaction Trace thresholds based upon Apdex were not being
|
||||||
|
applied to background transactions.
|
||||||
|
|
||||||
|
## 1.1.0
|
||||||
|
|
||||||
|
* Added support for Transaction Traces.
|
||||||
|
|
||||||
|
* Stack trace filenames have been shortened: Any thing preceding the first
|
||||||
|
`/src/` is now removed.
|
||||||
|
|
||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
* Removed `BetaToken` from the `Config` structure.
|
||||||
|
|
||||||
|
* Breaking Datastore Change: `datastore` package contents moved to top level
|
||||||
|
`newrelic` package. `datastore.MySQL` has become `newrelic.DatastoreMySQL`.
|
||||||
|
|
||||||
|
* Breaking Attributes Change: `attributes` package contents moved to top
|
||||||
|
level `newrelic` package. `attributes.ResponseCode` has become
|
||||||
|
`newrelic.AttributeResponseCode`. Some attribute name constants have been
|
||||||
|
shortened.
|
||||||
|
|
||||||
|
* Added "runtime.NumCPU" to the environment tab. Thanks sergeylanzman for the
|
||||||
|
contribution.
|
||||||
|
|
||||||
|
* Prefixed the environment tab values "Compiler", "GOARCH", "GOOS", and
|
||||||
|
"Version" with "runtime.".
|
||||||
|
|
||||||
|
## 0.8.0
|
||||||
|
|
||||||
|
* Breaking Segments API Changes: The segments API has been rewritten with the
|
||||||
|
goal of being easier to use and to avoid nil Transaction checks. See:
|
||||||
|
|
||||||
|
* [segments.go](segments.go)
|
||||||
|
* [examples/server/main.go](examples/server/main.go)
|
||||||
|
* [GUIDE.md#segments](GUIDE.md#segments)
|
||||||
|
|
||||||
|
* Updated LICENSE.txt with contribution information.
|
||||||
|
|
||||||
|
## 0.7.1
|
||||||
|
|
||||||
|
* Fixed a bug causing the `Config` to fail to serialize into JSON when the
|
||||||
|
`Transport` field was populated.
|
||||||
|
|
||||||
|
## 0.7.0
|
||||||
|
|
||||||
|
* Eliminated `api`, `version`, and `log` packages. `Version`, `Config`,
|
||||||
|
`Application`, and `Transaction` now live in the top level `newrelic` package.
|
||||||
|
If you imported the `attributes` or `datastore` packages then you will need
|
||||||
|
to remove `api` from the import path.
|
||||||
|
|
||||||
|
* Breaking Logging Changes
|
||||||
|
|
||||||
|
Logging is no longer controlled though a single global. Instead, logging is
|
||||||
|
configured on a per-application basis with the new `Config.Logger` field. The
|
||||||
|
logger is an interface described in [log.go](log.go). See
|
||||||
|
[GUIDE.md#logging](GUIDE.md#logging).
|
||||||
|
|
||||||
|
## 0.6.1
|
||||||
|
|
||||||
|
* No longer create "GC/System/Pauses" metric if no GC pauses happened.
|
||||||
|
|
||||||
|
## 0.6.0
|
||||||
|
|
||||||
|
* Introduced beta token to support our beta program.
|
||||||
|
|
||||||
|
* Rename `Config.Development` to `Config.Enabled` (and change boolean
|
||||||
|
direction).
|
||||||
|
|
||||||
|
* Fixed a bug where exclusive time could be incorrect if segments were not
|
||||||
|
ended.
|
||||||
|
|
||||||
|
* Fix unit tests broken in 1.6.
|
||||||
|
|
||||||
|
* In `Config.Enabled = false` mode, the license must be the proper length or empty.
|
||||||
|
|
||||||
|
* Added runtime statistics for CPU/memory usage, garbage collection, and number
|
||||||
|
of goroutines.
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
* Added segment timing methods to `Transaction`. These methods must only be
|
||||||
|
used in a single goroutine.
|
||||||
|
|
||||||
|
* The license length check will not be performed in `Development` mode.
|
||||||
|
|
||||||
|
* Rename `SetLogFile` to `SetFile` to reduce redundancy.
|
||||||
|
|
||||||
|
* Added `DebugEnabled` logging guard to reduce overhead.
|
||||||
|
|
||||||
|
* `Transaction` now implements an `Ignore` method which will prevent
|
||||||
|
any of the transaction's data from being recorded.
|
||||||
|
|
||||||
|
* `Transaction` now implements a subset of the interfaces
|
||||||
|
`http.CloseNotifier`, `http.Flusher`, `http.Hijacker`, and `io.ReaderFrom`
|
||||||
|
to match the behavior of its wrapped `http.ResponseWriter`.
|
||||||
|
|
||||||
|
* Changed project name from `go-sdk` to `go-agent`.
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
* Queue time support added: if the inbound request contains an
|
||||||
|
`"X-Request-Start"` or `"X-Queue-Start"` header with a unix timestamp, the
|
||||||
|
agent will report queue time metrics. Queue time will appear on the
|
||||||
|
application overview chart. The timestamp may fractional seconds,
|
||||||
|
milliseconds, or microseconds: the agent will deduce the correct units.
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
You are welcome to send pull requests to us. By doing so you agree that you are
|
||||||
|
granting New Relic a non-exclusive, non-revokable, no-cost license to use the
|
||||||
|
code, algorithms, patents, and ideas in that code in our products if we so
|
||||||
|
choose. You also agree the code is provided as-is and you provide no warranties
|
||||||
|
as to its fitness or correctness for any purpose.
|
||||||
|
|
||||||
|
* [LICENSE.txt](LICENSE.txt)
|
|
@ -0,0 +1,325 @@
|
||||||
|
# New Relic Go Agent Guide
|
||||||
|
|
||||||
|
* [Installation](#installation)
|
||||||
|
* [Config and Application](#config-and-application)
|
||||||
|
* [Logging](#logging)
|
||||||
|
* [logrus](#logrus)
|
||||||
|
* [Transactions](#transactions)
|
||||||
|
* [Segments](#segments)
|
||||||
|
* [Datastore Segments](#datastore-segments)
|
||||||
|
* [External Segments](#external-segments)
|
||||||
|
* [Attributes](#attributes)
|
||||||
|
* [Request Queuing](#request-queuing)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Installing the Go Agent is the same as installing any other Go library. The
|
||||||
|
simplest way is to run:
|
||||||
|
|
||||||
|
```
|
||||||
|
go get github.com/newrelic/go-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
Then import the `github.com/newrelic/go-agent` package in your application.
|
||||||
|
|
||||||
|
## Config and Application
|
||||||
|
|
||||||
|
* [config.go](config.go)
|
||||||
|
* [application.go](application.go)
|
||||||
|
|
||||||
|
In your `main` function or in an `init` block:
|
||||||
|
|
||||||
|
```go
|
||||||
|
config := newrelic.NewConfig("Your Application Name", "__YOUR_NEW_RELIC_LICENSE_KEY__")
|
||||||
|
app, err := newrelic.NewApplication(config)
|
||||||
|
```
|
||||||
|
|
||||||
|
Find your application in the New Relic UI. Click on it to see the Go runtime
|
||||||
|
tab that shows information about goroutine counts, garbage collection, memory,
|
||||||
|
and CPU usage.
|
||||||
|
|
||||||
|
If you are working in a development environment or running unit tests, you may
|
||||||
|
not want the Go Agent to spawn goroutines or report to New Relic. You're in
|
||||||
|
luck! Set the config's `Enabled` field to false. This makes the license key
|
||||||
|
optional.
|
||||||
|
|
||||||
|
```go
|
||||||
|
config := newrelic.NewConfig("Your Application Name", "")
|
||||||
|
config.Enabled = false
|
||||||
|
app, err := newrelic.NewApplication(config)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
* [log.go](log.go)
|
||||||
|
|
||||||
|
The agent's logging system is designed to be easily extensible. By default, no
|
||||||
|
logging will occur. To enable logging, assign the `Config.Logger` field to
|
||||||
|
something implementing the `Logger` interface. A basic logging
|
||||||
|
implementation is included.
|
||||||
|
|
||||||
|
To log at debug level to standard out, set:
|
||||||
|
|
||||||
|
```go
|
||||||
|
config.Logger = newrelic.NewDebugLogger(os.Stdout)
|
||||||
|
```
|
||||||
|
|
||||||
|
To log at info level to a file, set:
|
||||||
|
|
||||||
|
```go
|
||||||
|
w, err := os.OpenFile("my_log_file", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
|
if nil == err {
|
||||||
|
config.Logger = newrelic.NewLogger(w)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### logrus
|
||||||
|
|
||||||
|
* [_integrations/nrlogrus/nrlogrus.go](_integrations/nrlogrus/nrlogrus.go)
|
||||||
|
|
||||||
|
If you are using `logrus` and would like to send the agent's log messages to its
|
||||||
|
standard logger, import the
|
||||||
|
`github.com/newrelic/go-agent/_integrations/nrlogrus` package, then set:
|
||||||
|
|
||||||
|
```go
|
||||||
|
config.Logger = nrlogrus.StandardLogger()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transactions
|
||||||
|
|
||||||
|
* [transaction.go](transaction.go)
|
||||||
|
* [More info on Transactions](https://docs.newrelic.com/docs/apm/applications-menu/monitoring/transactions-page)
|
||||||
|
|
||||||
|
Transactions time requests and background tasks. Each transaction should only
|
||||||
|
be used in a single goroutine. Start a new transaction when you spawn a new
|
||||||
|
goroutine.
|
||||||
|
|
||||||
|
The simplest way to create transactions is to use
|
||||||
|
`Application.StartTransaction` and `Transaction.End`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
txn := app.StartTransaction("transactionName", responseWriter, request)
|
||||||
|
defer txn.End()
|
||||||
|
```
|
||||||
|
|
||||||
|
The response writer and request parameters are optional. Leave them `nil` to
|
||||||
|
instrument a background task.
|
||||||
|
|
||||||
|
```go
|
||||||
|
txn := app.StartTransaction("backgroundTask", nil, nil)
|
||||||
|
defer txn.End()
|
||||||
|
```
|
||||||
|
|
||||||
|
The transaction has helpful methods like `NoticeError` and `SetName`.
|
||||||
|
See more in [transaction.go](transaction.go).
|
||||||
|
|
||||||
|
If you are using the `http` standard library package, use `WrapHandle` and
|
||||||
|
`WrapHandleFunc`. These wrappers automatically start and end transactions with
|
||||||
|
the request and response writer. See [instrumentation.go](instrumentation.go).
|
||||||
|
|
||||||
|
```go
|
||||||
|
http.HandleFunc(newrelic.WrapHandleFunc(app, "/users", usersHandler))
|
||||||
|
```
|
||||||
|
|
||||||
|
To access the transaction in your handler, use type assertion on the response
|
||||||
|
writer passed to the handler.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func myHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if txn, ok := w.(newrelic.Transaction); ok {
|
||||||
|
txn.NoticeError(errors.New("my error message"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Segments
|
||||||
|
|
||||||
|
* [segments.go](segments.go)
|
||||||
|
|
||||||
|
Find out where the time in your transactions is being spent! Each transaction
|
||||||
|
should only track segments in a single goroutine.
|
||||||
|
|
||||||
|
`Segment` is used to instrument functions, methods, and blocks of code. A
|
||||||
|
segment begins when its `StartTime` field is populated, and finishes when its
|
||||||
|
`End` method is called.
|
||||||
|
|
||||||
|
```go
|
||||||
|
segment := newrelic.Segment{}
|
||||||
|
segment.Name = "mySegmentName"
|
||||||
|
segment.StartTime = newrelic.StartSegmentNow(txn)
|
||||||
|
// ... code you want to time here ...
|
||||||
|
segment.End()
|
||||||
|
```
|
||||||
|
|
||||||
|
`StartSegment` is a convenient helper. It creates a segment and starts it:
|
||||||
|
|
||||||
|
```go
|
||||||
|
segment := newrelic.StartSegment(txn, "mySegmentName")
|
||||||
|
// ... code you want to time here ...
|
||||||
|
segment.End()
|
||||||
|
```
|
||||||
|
|
||||||
|
Timing a function is easy using `StartSegment` and `defer`. Just add the
|
||||||
|
following line to the beginning of that function:
|
||||||
|
|
||||||
|
```go
|
||||||
|
defer newrelic.StartSegment(txn, "mySegmentName").End()
|
||||||
|
```
|
||||||
|
|
||||||
|
Segments may be nested. The segment being ended must be the most recently
|
||||||
|
started segment.
|
||||||
|
|
||||||
|
```go
|
||||||
|
s1 := newrelic.StartSegment(txn, "outerSegment")
|
||||||
|
s2 := newrelic.StartSegment(txn, "innerSegment")
|
||||||
|
// s2 must be ended before s1
|
||||||
|
s2.End()
|
||||||
|
s1.End()
|
||||||
|
```
|
||||||
|
|
||||||
|
A zero value segment may safely be ended. Therefore, the following code
|
||||||
|
is safe even if the conditional fails:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var s newrelic.Segment
|
||||||
|
if txn, ok := w.(newrelic.Transaction); ok {
|
||||||
|
s.StartTime = newrelic.StartSegmentNow(txn),
|
||||||
|
}
|
||||||
|
// ... code you wish to time here ...
|
||||||
|
s.End()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Datastore Segments
|
||||||
|
|
||||||
|
Datastore segments appear in the transaction "Breakdown table" and in the
|
||||||
|
"Databases" tab.
|
||||||
|
|
||||||
|
* [datastore.go](datastore.go)
|
||||||
|
* [More info on Databases tab](https://docs.newrelic.com/docs/apm/applications-menu/monitoring/databases-slow-queries-page)
|
||||||
|
|
||||||
|
Datastore segments are instrumented using `DatastoreSegment`. Just like basic
|
||||||
|
segments, datastore segments begin when the `StartTime` field is populated and
|
||||||
|
finish when the `End` method is called. Here is an example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
s := newrelic.DatastoreSegment{
|
||||||
|
// Product is the datastore type. See the constants in datastore.go.
|
||||||
|
Product: newrelic.DatastoreMySQL,
|
||||||
|
// Collection is the table or group.
|
||||||
|
Collection: "my_table",
|
||||||
|
// Operation is the relevant action, e.g. "SELECT" or "GET".
|
||||||
|
Operation: "SELECT",
|
||||||
|
}
|
||||||
|
s.StartTime = newrelic.StartSegmentNow(txn)
|
||||||
|
// ... make the datastore call
|
||||||
|
s.End()
|
||||||
|
```
|
||||||
|
|
||||||
|
This may be combined into a single line when instrumenting a datastore call
|
||||||
|
that spans an entire function call:
|
||||||
|
|
||||||
|
```go
|
||||||
|
defer newrelic.DatastoreSegment{
|
||||||
|
StartTime: newrelic.StartSegmentNow(txn),
|
||||||
|
Product: newrelic.DatastoreMySQL,
|
||||||
|
Collection: "my_table",
|
||||||
|
Operation: "SELECT",
|
||||||
|
}.End()
|
||||||
|
```
|
||||||
|
|
||||||
|
### External Segments
|
||||||
|
|
||||||
|
External segments appear in the transaction "Breakdown table" and in the
|
||||||
|
"External services" tab.
|
||||||
|
|
||||||
|
* [More info on External Services tab](https://docs.newrelic.com/docs/apm/applications-menu/monitoring/external-services-page)
|
||||||
|
|
||||||
|
External segments are instrumented using `ExternalSegment`. Populate either the
|
||||||
|
`URL` or `Request` field to indicate the endpoint. Here is an example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func external(txn newrelic.Transaction, url string) (*http.Response, error) {
|
||||||
|
defer newrelic.ExternalSegment{
|
||||||
|
StartTime: newrelic.StartSegmentNow(txn),
|
||||||
|
URL: url,
|
||||||
|
}.End()
|
||||||
|
|
||||||
|
return http.Get(url)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We recommend using the `Request` and `Response` fields since they provide more
|
||||||
|
information about the external call. The `StartExternalSegment` helper is
|
||||||
|
useful when the request is available. This function may be modified in the
|
||||||
|
future to add headers that will trace activity between applications that are
|
||||||
|
instrumented by New Relic.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func external(txn newrelic.Transaction, req *http.Request) (*http.Response, error) {
|
||||||
|
s := newrelic.StartExternalSegment(txn, req)
|
||||||
|
response, err := http.DefaultClient.Do(req)
|
||||||
|
s.Response = response
|
||||||
|
s.End()
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`NewRoundTripper` is another useful helper. As with all segments, the round
|
||||||
|
tripper returned **must** only be used in the same goroutine as the transaction.
|
||||||
|
|
||||||
|
```go
|
||||||
|
client := &http.Client{}
|
||||||
|
client.Transport = newrelic.NewRoundTripper(txn, nil)
|
||||||
|
resp, err := client.Get("http://example.com/")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
Attributes add context to errors and allow you to filter performance data
|
||||||
|
in Insights.
|
||||||
|
|
||||||
|
You may add them using the `Transaction.AddAttribute` method.
|
||||||
|
|
||||||
|
```go
|
||||||
|
txn.AddAttribute("key", "value")
|
||||||
|
txn.AddAttribute("product", "widget")
|
||||||
|
txn.AddAttribute("price", 19.99)
|
||||||
|
txn.AddAttribute("importantCustomer", true)
|
||||||
|
```
|
||||||
|
|
||||||
|
* [More info on Custom Attributes](https://docs.newrelic.com/docs/insights/new-relic-insights/decorating-events/insights-custom-attributes)
|
||||||
|
|
||||||
|
Some attributes are recorded automatically. These are called agent attributes.
|
||||||
|
They are listed here:
|
||||||
|
|
||||||
|
* [attributes.go](attributes.go)
|
||||||
|
|
||||||
|
To disable one of these agents attributes, `AttributeResponseCode` for
|
||||||
|
example, modify the config like this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
config.Attributes.Exclude = append(config.Attributes.Exclude, newrelic.AttributeResponseCode)
|
||||||
|
```
|
||||||
|
|
||||||
|
* [More info on Agent Attributes](https://docs.newrelic.com/docs/agents/manage-apm-agents/agent-metrics/agent-attributes)
|
||||||
|
|
||||||
|
## Custom Events
|
||||||
|
|
||||||
|
You may track arbitrary events using custom Insights events.
|
||||||
|
|
||||||
|
```go
|
||||||
|
app.RecordCustomEvent("MyEventType", map[string]interface{}{
|
||||||
|
"myString": "hello",
|
||||||
|
"myFloat": 0.603,
|
||||||
|
"myInt": 123,
|
||||||
|
"myBool": true,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Request Queuing
|
||||||
|
|
||||||
|
If you are running a load balancer or reverse web proxy then you may configure
|
||||||
|
it to add a `X-Queue-Start` header with a Unix timestamp. This will create a
|
||||||
|
band on the application overview chart showing queue time.
|
||||||
|
|
||||||
|
* [More info on Request Queuing](https://docs.newrelic.com/docs/apm/applications-menu/features/request-queuing-tracking-front-end-time)
|
|
@ -0,0 +1,50 @@
|
||||||
|
This product includes source derived from 'go' by The Go Authors, distributed
|
||||||
|
under the following BSD license:
|
||||||
|
|
||||||
|
https://github.com/golang/go/blob/master/LICENSE
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
All components of this product are Copyright (c) 2016 New Relic, Inc. All
|
||||||
|
rights reserved.
|
||||||
|
|
||||||
|
Certain inventions disclosed in this file may be claimed within patents owned or
|
||||||
|
patent applications filed by New Relic, Inc. or third parties.
|
||||||
|
|
||||||
|
Subject to the terms of this notice, New Relic grants you a nonexclusive,
|
||||||
|
nontransferable license, without the right to sublicense, to (a) install and
|
||||||
|
execute one copy of these files on any number of workstations owned or
|
||||||
|
controlled by you and (b) distribute verbatim copies of these files to third
|
||||||
|
parties. You may install, execute, and distribute these files and their
|
||||||
|
contents only in conjunction with your direct use of New Relic’s services.
|
||||||
|
These files and their contents shall not be used in conjunction with any other
|
||||||
|
product or software, including but not limited to those that may compete with
|
||||||
|
any New Relic product, feature, or software. As a condition to the foregoing
|
||||||
|
grant, you must provide this notice along with each copy you distribute and you
|
||||||
|
must not remove, alter, or obscure this notice. In the event you submit or
|
||||||
|
provide any feedback, code, pull requests, or suggestions to New Relic you
|
||||||
|
hereby grant New Relic a worldwide, non-exclusive, irrevocable, transferrable,
|
||||||
|
fully paid-up license to use the code, algorithms, patents, and ideas therein in
|
||||||
|
our products.
|
||||||
|
|
||||||
|
All other use, reproduction, modification, distribution, or other exploitation
|
||||||
|
of these files is strictly prohibited, except as may be set forth in a separate
|
||||||
|
written license agreement between you and New Relic. The terms of any such
|
||||||
|
license agreement will control over this notice. The license stated above will
|
||||||
|
be automatically terminated and revoked if you exceed its scope or violate any
|
||||||
|
of the terms of this notice.
|
||||||
|
|
||||||
|
This License does not grant permission to use the trade names, trademarks,
|
||||||
|
service marks, or product names of New Relic, except as required for reasonable
|
||||||
|
and customary use in describing the origin of this file and reproducing the
|
||||||
|
content of this notice. You may not mark or brand this file with any trade
|
||||||
|
name, trademarks, service marks, or product names other than the original brand
|
||||||
|
(if any) provided by New Relic.
|
||||||
|
|
||||||
|
Unless otherwise expressly agreed by New Relic in a separate written license
|
||||||
|
agreement, these files are provided AS IS, WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
including without any implied warranties of MERCHANTABILITY, FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE, TITLE, or NON-INFRINGEMENT. As a condition to your use of
|
||||||
|
these files, you are solely responsible for such use. New Relic will have no
|
||||||
|
liability to you for direct, indirect, consequential, incidental, special, or
|
||||||
|
punitive damages or for lost profits or data.
|
|
@ -0,0 +1,157 @@
|
||||||
|
# New Relic Go Agent
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The New Relic Go Agent allows you to monitor your Go applications with New
|
||||||
|
Relic. It helps you track transactions, outbound requests, database calls, and
|
||||||
|
other parts of your Go application's behavior and provides a running overview of
|
||||||
|
garbage collection, goroutine activity, and memory use.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
Go 1.3+ is required, due to the use of http.Client's Timeout field.
|
||||||
|
|
||||||
|
Linux, OS X, and Windows (Vista, Server 2008 and later) are supported.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Here are the basic steps to instrumenting your application. For more
|
||||||
|
information, see [GUIDE.md](GUIDE.md).
|
||||||
|
|
||||||
|
#### Step 0: Installation
|
||||||
|
|
||||||
|
Installing the Go Agent is the same as installing any other Go library. The
|
||||||
|
simplest way is to run:
|
||||||
|
|
||||||
|
```
|
||||||
|
go get github.com/newrelic/go-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
Then import the `github.com/newrelic/go-agent` package in your application.
|
||||||
|
|
||||||
|
#### Step 1: Create a Config and an Application
|
||||||
|
|
||||||
|
In your `main` function or an `init` block:
|
||||||
|
|
||||||
|
```go
|
||||||
|
config := newrelic.NewConfig("Your Application Name", "__YOUR_NEW_RELIC_LICENSE_KEY__")
|
||||||
|
app, err := newrelic.NewApplication(config)
|
||||||
|
```
|
||||||
|
|
||||||
|
[more info](GUIDE.md#config-and-application), [application.go](application.go),
|
||||||
|
[config.go](config.go)
|
||||||
|
|
||||||
|
#### Step 2: Add Transactions
|
||||||
|
|
||||||
|
Transactions time requests and background tasks. Use `WrapHandle` and
|
||||||
|
`WrapHandleFunc` to create transactions for requests handled by the `http`
|
||||||
|
standard library package.
|
||||||
|
|
||||||
|
```go
|
||||||
|
http.HandleFunc(newrelic.WrapHandleFunc(app, "/users", usersHandler))
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, create transactions directly using the application's
|
||||||
|
`StartTransaction` method:
|
||||||
|
|
||||||
|
```go
|
||||||
|
txn := app.StartTransaction("myTxn", optionalResponseWriter, optionalRequest)
|
||||||
|
defer txn.End()
|
||||||
|
```
|
||||||
|
|
||||||
|
[more info](GUIDE.md#transactions), [transaction.go](transaction.go)
|
||||||
|
|
||||||
|
#### Step 3: Instrument Segments
|
||||||
|
|
||||||
|
Segments show you where time in your transactions is being spent. At the
|
||||||
|
beginning of important functions, add:
|
||||||
|
|
||||||
|
```go
|
||||||
|
defer newrelic.StartSegment(txn, "mySegmentName").End()
|
||||||
|
```
|
||||||
|
|
||||||
|
[more info](GUIDE.md#segments), [segments.go](segments.go)
|
||||||
|
|
||||||
|
## Runnable Example
|
||||||
|
|
||||||
|
[examples/server/main.go](./examples/server/main.go) is an example that will appear as "My Go
|
||||||
|
Application" in your New Relic applications list. To run it:
|
||||||
|
|
||||||
|
```
|
||||||
|
env NEW_RELIC_LICENSE_KEY=__YOUR_NEW_RELIC_LICENSE_KEY__LICENSE__ \
|
||||||
|
go run examples/server/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
Some endpoints exposed are [http://localhost:8000/](http://localhost:8000/)
|
||||||
|
and [http://localhost:8000/notice_error](http://localhost:8000/notice_error)
|
||||||
|
|
||||||
|
|
||||||
|
## Basic Example
|
||||||
|
|
||||||
|
Before Instrumentation
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func helloHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
io.WriteString(w, "hello, world")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", helloHandler)
|
||||||
|
http.ListenAndServe(":8000", nil)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After Instrumentation
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/newrelic/go-agent"
|
||||||
|
)
|
||||||
|
|
||||||
|
func helloHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
io.WriteString(w, "hello, world")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create a config. You need to provide the desired application name
|
||||||
|
// and your New Relic license key.
|
||||||
|
cfg := newrelic.NewConfig("My Go Application", "__YOUR_NEW_RELIC_LICENSE_KEY__")
|
||||||
|
|
||||||
|
// Create an application. This represents an application in the New
|
||||||
|
// Relic UI.
|
||||||
|
app, err := newrelic.NewApplication(cfg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap helloHandler. The performance of this handler will be recorded.
|
||||||
|
http.HandleFunc(newrelic.WrapHandleFunc(app, "/", helloHandler))
|
||||||
|
http.ListenAndServe(":8000", nil)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
You can find more detailed documentation [in the guide](GUIDE.md).
|
||||||
|
|
||||||
|
If you can't find what you're looking for there, reach out to us on our [support
|
||||||
|
site](http://support.newrelic.com/) or our [community
|
||||||
|
forum](http://forum.newrelic.com) and we'll be happy to help you.
|
||||||
|
|
||||||
|
Find a bug? Contact us via [support.newrelic.com](http://support.newrelic.com/),
|
||||||
|
or email support@newrelic.com.
|
|
@ -0,0 +1,58 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Application represents your application.
|
||||||
|
type Application interface {
|
||||||
|
// StartTransaction begins a Transaction.
|
||||||
|
// * The Transaction should only be used in a single goroutine.
|
||||||
|
// * This method never returns nil.
|
||||||
|
// * If an http.Request is provided then the Transaction is considered
|
||||||
|
// a web transaction.
|
||||||
|
// * If an http.ResponseWriter is provided then the Transaction can be
|
||||||
|
// used in its place. This allows instrumentation of the response
|
||||||
|
// code and response headers.
|
||||||
|
StartTransaction(name string, w http.ResponseWriter, r *http.Request) Transaction
|
||||||
|
|
||||||
|
// RecordCustomEvent adds a custom event to the application. This
|
||||||
|
// feature is incompatible with high security mode.
|
||||||
|
//
|
||||||
|
// eventType must consist of alphanumeric characters, underscores, and
|
||||||
|
// colons, and must contain fewer than 255 bytes.
|
||||||
|
//
|
||||||
|
// Each value in the params map must be a number, string, or boolean.
|
||||||
|
// Keys must be less than 255 bytes. The params map may not contain
|
||||||
|
// more than 64 attributes. For more information, and a set of
|
||||||
|
// restricted keywords, see:
|
||||||
|
//
|
||||||
|
// https://docs.newrelic.com/docs/insights/new-relic-insights/adding-querying-data/inserting-custom-events-new-relic-apm-agents
|
||||||
|
RecordCustomEvent(eventType string, params map[string]interface{}) error
|
||||||
|
|
||||||
|
// WaitForConnection blocks until the application is connected, is
|
||||||
|
// incapable of being connected, or the timeout has been reached. This
|
||||||
|
// method is useful for short-lived processes since the application will
|
||||||
|
// not gather data until it is connected. nil is returned if the
|
||||||
|
// application is connected successfully.
|
||||||
|
WaitForConnection(timeout time.Duration) error
|
||||||
|
|
||||||
|
// Shutdown flushes data to New Relic's servers and stops all
|
||||||
|
// agent-related goroutines managing this application. After Shutdown
|
||||||
|
// is called, the application is disabled and no more data will be
|
||||||
|
// collected. This method will block until all final data is sent to
|
||||||
|
// New Relic or the timeout has elapsed.
|
||||||
|
Shutdown(timeout time.Duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApplication creates an Application and spawns goroutines to manage the
|
||||||
|
// aggregation and harvesting of data. On success, a non-nil Application and a
|
||||||
|
// nil error are returned. On failure, a nil Application and a non-nil error
|
||||||
|
// are returned.
|
||||||
|
//
|
||||||
|
// Applications do not share global state (other than the shared log.Logger).
|
||||||
|
// Therefore, it is safe to create multiple applications.
|
||||||
|
func NewApplication(c Config) (Application, error) {
|
||||||
|
return newApp(c)
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
// This file contains the names of the automatically captured attributes.
|
||||||
|
// Attributes are key value pairs attached to transaction events, error events,
|
||||||
|
// and traced errors. You may add your own attributes using the
|
||||||
|
// Transaction.AddAttribute method (see transaction.go).
|
||||||
|
//
|
||||||
|
// These attribute names are exposed here to facilitate configuration.
|
||||||
|
//
|
||||||
|
// For more information, see:
|
||||||
|
// https://docs.newrelic.com/docs/agents/manage-apm-agents/agent-metrics/agent-attributes
|
||||||
|
|
||||||
|
// Attributes destined for Transaction Events and Errors:
|
||||||
|
const (
|
||||||
|
// AttributeResponseCode is the response status code for a web request.
|
||||||
|
AttributeResponseCode = "httpResponseCode"
|
||||||
|
// AttributeRequestMethod is the request's method.
|
||||||
|
AttributeRequestMethod = "request.method"
|
||||||
|
// AttributeRequestAccept is the request's "Accept" header.
|
||||||
|
AttributeRequestAccept = "request.headers.accept"
|
||||||
|
// AttributeRequestContentType is the request's "Content-Type" header.
|
||||||
|
AttributeRequestContentType = "request.headers.contentType"
|
||||||
|
// AttributeRequestContentLength is the request's "Content-Length" header.
|
||||||
|
AttributeRequestContentLength = "request.headers.contentLength"
|
||||||
|
// AttributeRequestHost is the request's "Host" header.
|
||||||
|
AttributeRequestHost = "request.headers.host"
|
||||||
|
// AttributeResponseContentType is the response "Content-Type" header.
|
||||||
|
AttributeResponseContentType = "response.headers.contentType"
|
||||||
|
// AttributeResponseContentLength is the response "Content-Length" header.
|
||||||
|
AttributeResponseContentLength = "response.headers.contentLength"
|
||||||
|
// AttributeHostDisplayName contains the value of Config.HostDisplayName.
|
||||||
|
AttributeHostDisplayName = "host.displayName"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Attributes destined for Errors:
|
||||||
|
const (
|
||||||
|
// AttributeRequestUserAgent is the request's "User-Agent" header.
|
||||||
|
AttributeRequestUserAgent = "request.headers.User-Agent"
|
||||||
|
// AttributeRequestReferer is the request's "Referer" header. Query
|
||||||
|
// string parameters are removed.
|
||||||
|
AttributeRequestReferer = "request.headers.referer"
|
||||||
|
)
|
|
@ -0,0 +1,257 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config contains Application and Transaction behavior settings.
|
||||||
|
// Use NewConfig to create a Config with proper defaults.
|
||||||
|
type Config struct {
|
||||||
|
// AppName is used by New Relic to link data across servers.
|
||||||
|
//
|
||||||
|
// https://docs.newrelic.com/docs/apm/new-relic-apm/installation-configuration/naming-your-application
|
||||||
|
AppName string
|
||||||
|
|
||||||
|
// License is your New Relic license key.
|
||||||
|
//
|
||||||
|
// https://docs.newrelic.com/docs/accounts-partnerships/accounts/account-setup/license-key
|
||||||
|
License string
|
||||||
|
|
||||||
|
// Logger controls go-agent logging. See log.go.
|
||||||
|
Logger Logger
|
||||||
|
|
||||||
|
// Enabled determines whether the agent will communicate with the New
|
||||||
|
// Relic servers and spawn goroutines. Setting this to be false can be
|
||||||
|
// useful in testing and staging situations.
|
||||||
|
Enabled bool
|
||||||
|
|
||||||
|
// Labels are key value pairs used to roll up applications into specific
|
||||||
|
// categories.
|
||||||
|
//
|
||||||
|
// https://docs.newrelic.com/docs/apm/new-relic-apm/maintenance/labels-categories-organizing-your-apps-servers
|
||||||
|
Labels map[string]string
|
||||||
|
|
||||||
|
// HighSecurity guarantees that certain agent settings can not be made
|
||||||
|
// more permissive. This setting must match the corresponding account
|
||||||
|
// setting in the New Relic UI.
|
||||||
|
//
|
||||||
|
// https://docs.newrelic.com/docs/accounts-partnerships/accounts/security/high-security
|
||||||
|
HighSecurity bool
|
||||||
|
|
||||||
|
// CustomInsightsEvents controls the behavior of
|
||||||
|
// Application.RecordCustomEvent.
|
||||||
|
//
|
||||||
|
// https://docs.newrelic.com/docs/insights/new-relic-insights/adding-querying-data/inserting-custom-events-new-relic-apm-agents
|
||||||
|
CustomInsightsEvents struct {
|
||||||
|
// Enabled controls whether RecordCustomEvent will collect
|
||||||
|
// custom analytics events. High security mode overrides this
|
||||||
|
// setting.
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionEvents controls the behavior of transaction analytics
|
||||||
|
// events.
|
||||||
|
TransactionEvents struct {
|
||||||
|
// Enabled controls whether transaction events are captured.
|
||||||
|
Enabled bool
|
||||||
|
// Attributes controls the attributes included with transaction
|
||||||
|
// events.
|
||||||
|
Attributes AttributeDestinationConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorCollector controls the capture of errors.
|
||||||
|
ErrorCollector struct {
|
||||||
|
// Enabled controls whether errors are captured. This setting
|
||||||
|
// affects both traced errors and error analytics events.
|
||||||
|
Enabled bool
|
||||||
|
// CaptureEvents controls whether error analytics events are
|
||||||
|
// captured.
|
||||||
|
CaptureEvents bool
|
||||||
|
// IgnoreStatusCodes controls which http response codes are
|
||||||
|
// automatically turned into errors. By default, response codes
|
||||||
|
// greater than or equal to 400, with the exception of 404, are
|
||||||
|
// turned into errors.
|
||||||
|
IgnoreStatusCodes []int
|
||||||
|
// Attributes controls the attributes included with errors.
|
||||||
|
Attributes AttributeDestinationConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionTracer controls the capture of transaction traces.
|
||||||
|
TransactionTracer struct {
|
||||||
|
// Enabled controls whether transaction traces are captured.
|
||||||
|
Enabled bool
|
||||||
|
// Threshold controls whether a transaction trace will be
|
||||||
|
// considered for capture. Of the traces exceeding the
|
||||||
|
// threshold, the slowest trace every minute is captured.
|
||||||
|
Threshold struct {
|
||||||
|
// If IsApdexFailing is true then the trace threshold is
|
||||||
|
// four times the apdex threshold.
|
||||||
|
IsApdexFailing bool
|
||||||
|
// If IsApdexFailing is false then this field is the
|
||||||
|
// threshold, otherwise it is ignored.
|
||||||
|
Duration time.Duration
|
||||||
|
}
|
||||||
|
// SegmentThreshold is the threshold at which segments will be
|
||||||
|
// added to the trace. Lowering this setting may increase
|
||||||
|
// overhead.
|
||||||
|
SegmentThreshold time.Duration
|
||||||
|
// StackTraceThreshold is the threshold at which segments will
|
||||||
|
// be given a stack trace in the transaction trace. Lowering
|
||||||
|
// this setting will drastically increase overhead.
|
||||||
|
StackTraceThreshold time.Duration
|
||||||
|
// Attributes controls the attributes included with transaction
|
||||||
|
// traces.
|
||||||
|
Attributes AttributeDestinationConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// HostDisplayName gives this server a recognizable name in the New
|
||||||
|
// Relic UI. This is an optional setting.
|
||||||
|
HostDisplayName string
|
||||||
|
|
||||||
|
// UseTLS controls whether http or https is used to send data to New
|
||||||
|
// Relic servers.
|
||||||
|
UseTLS bool
|
||||||
|
|
||||||
|
// Transport customizes http.Client communication with New Relic
|
||||||
|
// servers. This may be used to configure a proxy.
|
||||||
|
Transport http.RoundTripper
|
||||||
|
|
||||||
|
// Utilization controls the detection and gathering of system
|
||||||
|
// information.
|
||||||
|
Utilization struct {
|
||||||
|
// DetectAWS controls whether the Application attempts to detect
|
||||||
|
// AWS.
|
||||||
|
DetectAWS bool
|
||||||
|
// DetectDocker controls whether the Application attempts to
|
||||||
|
// detect Docker.
|
||||||
|
DetectDocker bool
|
||||||
|
|
||||||
|
// These settings provide system information when custom values
|
||||||
|
// are required.
|
||||||
|
LogicalProcessors int
|
||||||
|
TotalRAMMIB int
|
||||||
|
BillingHostname string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DatastoreTracer controls behavior relating to datastore segments.
|
||||||
|
DatastoreTracer struct {
|
||||||
|
InstanceReporting struct {
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
DatabaseNameReporting struct {
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
QueryParameters struct {
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
// SlowQuery controls the capture of slow query traces. Slow
|
||||||
|
// query traces show you instances of your slowest datastore
|
||||||
|
// segments.
|
||||||
|
SlowQuery struct {
|
||||||
|
Enabled bool
|
||||||
|
Threshold time.Duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attributes controls the attributes included with errors and
|
||||||
|
// transaction events.
|
||||||
|
Attributes AttributeDestinationConfig
|
||||||
|
|
||||||
|
// RuntimeSampler controls the collection of runtime statistics like
|
||||||
|
// CPU/Memory usage, goroutine count, and GC pauses.
|
||||||
|
RuntimeSampler struct {
|
||||||
|
// Enabled controls whether runtime statistics are captured.
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttributeDestinationConfig controls the attributes included with errors and
|
||||||
|
// transaction events.
|
||||||
|
type AttributeDestinationConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
Include []string
|
||||||
|
Exclude []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfig creates an Config populated with the given appname, license,
|
||||||
|
// and expected default values.
|
||||||
|
func NewConfig(appname, license string) Config {
|
||||||
|
c := Config{}
|
||||||
|
|
||||||
|
c.AppName = appname
|
||||||
|
c.License = license
|
||||||
|
c.Enabled = true
|
||||||
|
c.Labels = make(map[string]string)
|
||||||
|
c.CustomInsightsEvents.Enabled = true
|
||||||
|
c.TransactionEvents.Enabled = true
|
||||||
|
c.TransactionEvents.Attributes.Enabled = true
|
||||||
|
c.HighSecurity = false
|
||||||
|
c.UseTLS = true
|
||||||
|
c.ErrorCollector.Enabled = true
|
||||||
|
c.ErrorCollector.CaptureEvents = true
|
||||||
|
c.ErrorCollector.IgnoreStatusCodes = []int{
|
||||||
|
http.StatusNotFound, // 404
|
||||||
|
}
|
||||||
|
c.ErrorCollector.Attributes.Enabled = true
|
||||||
|
c.Utilization.DetectAWS = true
|
||||||
|
c.Utilization.DetectDocker = true
|
||||||
|
c.Attributes.Enabled = true
|
||||||
|
c.RuntimeSampler.Enabled = true
|
||||||
|
|
||||||
|
c.TransactionTracer.Enabled = true
|
||||||
|
c.TransactionTracer.Threshold.IsApdexFailing = true
|
||||||
|
c.TransactionTracer.Threshold.Duration = 500 * time.Millisecond
|
||||||
|
c.TransactionTracer.SegmentThreshold = 2 * time.Millisecond
|
||||||
|
c.TransactionTracer.StackTraceThreshold = 500 * time.Millisecond
|
||||||
|
c.TransactionTracer.Attributes.Enabled = true
|
||||||
|
|
||||||
|
c.DatastoreTracer.InstanceReporting.Enabled = true
|
||||||
|
c.DatastoreTracer.DatabaseNameReporting.Enabled = true
|
||||||
|
c.DatastoreTracer.QueryParameters.Enabled = true
|
||||||
|
c.DatastoreTracer.SlowQuery.Enabled = true
|
||||||
|
c.DatastoreTracer.SlowQuery.Threshold = 10 * time.Millisecond
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
licenseLength = 40
|
||||||
|
appNameLimit = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// The following errors will be returned if your Config fails to validate.
|
||||||
|
var (
|
||||||
|
errLicenseLen = fmt.Errorf("license length is not %d", licenseLength)
|
||||||
|
errHighSecurityTLS = errors.New("high security requires TLS")
|
||||||
|
errAppNameMissing = errors.New("AppName required")
|
||||||
|
errAppNameLimit = fmt.Errorf("max of %d rollup application names", appNameLimit)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate checks the config for improper fields. If the config is invalid,
|
||||||
|
// newrelic.NewApplication returns an error.
|
||||||
|
func (c Config) Validate() error {
|
||||||
|
if c.Enabled {
|
||||||
|
if len(c.License) != licenseLength {
|
||||||
|
return errLicenseLen
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The License may be empty when the agent is not enabled.
|
||||||
|
if len(c.License) != licenseLength && len(c.License) != 0 {
|
||||||
|
return errLicenseLen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.HighSecurity && !c.UseTLS {
|
||||||
|
return errHighSecurityTLS
|
||||||
|
}
|
||||||
|
if "" == c.AppName {
|
||||||
|
return errAppNameMissing
|
||||||
|
}
|
||||||
|
if strings.Count(c.AppName, ";") >= appNameLimit {
|
||||||
|
return errAppNameLimit
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
// DatastoreProduct encourages consistent metrics across New Relic agents. You
|
||||||
|
// may create your own if your datastore is not listed below.
|
||||||
|
type DatastoreProduct string
|
||||||
|
|
||||||
|
// Datastore names used across New Relic agents:
|
||||||
|
const (
|
||||||
|
DatastoreCassandra DatastoreProduct = "Cassandra"
|
||||||
|
DatastoreDerby = "Derby"
|
||||||
|
DatastoreElasticsearch = "Elasticsearch"
|
||||||
|
DatastoreFirebird = "Firebird"
|
||||||
|
DatastoreIBMDB2 = "IBMDB2"
|
||||||
|
DatastoreInformix = "Informix"
|
||||||
|
DatastoreMemcached = "Memcached"
|
||||||
|
DatastoreMongoDB = "MongoDB"
|
||||||
|
DatastoreMySQL = "MySQL"
|
||||||
|
DatastoreMSSQL = "MSSQL"
|
||||||
|
DatastoreOracle = "Oracle"
|
||||||
|
DatastorePostgres = "Postgres"
|
||||||
|
DatastoreRedis = "Redis"
|
||||||
|
DatastoreSolr = "Solr"
|
||||||
|
DatastoreSQLite = "SQLite"
|
||||||
|
DatastoreCouchDB = "CouchDB"
|
||||||
|
DatastoreRiak = "Riak"
|
||||||
|
DatastoreVoltDB = "VoltDB"
|
||||||
|
)
|
|
@ -0,0 +1,68 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// instrumentation.go contains helpers built on the lower level api.
|
||||||
|
|
||||||
|
// WrapHandle facilitates instrumentation of handlers registered with an
|
||||||
|
// http.ServeMux. For example, to instrument this code:
|
||||||
|
//
|
||||||
|
// http.Handle("/foo", fooHandler)
|
||||||
|
//
|
||||||
|
// Perform this replacement:
|
||||||
|
//
|
||||||
|
// http.Handle(newrelic.WrapHandle(app, "/foo", fooHandler))
|
||||||
|
//
|
||||||
|
// The Transaction is passed to the handler in place of the original
|
||||||
|
// http.ResponseWriter, so it can be accessed using type assertion.
|
||||||
|
// For example, to rename the transaction:
|
||||||
|
//
|
||||||
|
// // 'w' is the variable name of the http.ResponseWriter.
|
||||||
|
// if txn, ok := w.(newrelic.Transaction); ok {
|
||||||
|
// txn.SetName("other-name")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
func WrapHandle(app Application, pattern string, handler http.Handler) (string, http.Handler) {
|
||||||
|
return pattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
txn := app.StartTransaction(pattern, w, r)
|
||||||
|
defer txn.End()
|
||||||
|
|
||||||
|
handler.ServeHTTP(txn, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapHandleFunc serves the same purpose as WrapHandle for functions registered
|
||||||
|
// with ServeMux.HandleFunc.
|
||||||
|
func WrapHandleFunc(app Application, pattern string, handler func(http.ResponseWriter, *http.Request)) (string, func(http.ResponseWriter, *http.Request)) {
|
||||||
|
p, h := WrapHandle(app, pattern, http.HandlerFunc(handler))
|
||||||
|
return p, func(w http.ResponseWriter, r *http.Request) { h.ServeHTTP(w, r) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRoundTripper creates an http.RoundTripper to instrument external requests.
|
||||||
|
// This RoundTripper must be used in same the goroutine as the other uses of the
|
||||||
|
// Transaction's SegmentTracer methods. http.DefaultTransport is used if an
|
||||||
|
// http.RoundTripper is not provided.
|
||||||
|
//
|
||||||
|
// client := &http.Client{}
|
||||||
|
// client.Transport = newrelic.NewRoundTripper(txn, nil)
|
||||||
|
// resp, err := client.Get("http://example.com/")
|
||||||
|
//
|
||||||
|
func NewRoundTripper(txn Transaction, original http.RoundTripper) http.RoundTripper {
|
||||||
|
return roundTripperFunc(func(request *http.Request) (*http.Response, error) {
|
||||||
|
segment := StartExternalSegment(txn, request)
|
||||||
|
|
||||||
|
if nil == original {
|
||||||
|
original = http.DefaultTransport
|
||||||
|
}
|
||||||
|
response, err := original.RoundTrip(request)
|
||||||
|
|
||||||
|
segment.Response = response
|
||||||
|
segment.End()
|
||||||
|
|
||||||
|
return response, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type roundTripperFunc func(*http.Request) (*http.Response, error)
|
||||||
|
|
||||||
|
func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
|
|
@ -0,0 +1,122 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"container/heap"
|
||||||
|
|
||||||
|
"github.com/newrelic/go-agent/internal/jsonx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// eventStamp allows for uniform random sampling of events. When an event is
|
||||||
|
// created it is given an eventStamp. Whenever an event pool is full and events
|
||||||
|
// need to be dropped, the events with the lowest stamps are dropped.
|
||||||
|
type eventStamp float32
|
||||||
|
|
||||||
|
func eventStampCmp(a, b eventStamp) bool {
|
||||||
|
return a < b
|
||||||
|
}
|
||||||
|
|
||||||
|
type analyticsEvent struct {
|
||||||
|
stamp eventStamp
|
||||||
|
jsonWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
type analyticsEventHeap []analyticsEvent
|
||||||
|
|
||||||
|
type analyticsEvents struct {
|
||||||
|
numSeen int
|
||||||
|
events analyticsEventHeap
|
||||||
|
failedHarvests int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (events *analyticsEvents) NumSeen() float64 { return float64(events.numSeen) }
|
||||||
|
func (events *analyticsEvents) NumSaved() float64 { return float64(len(events.events)) }
|
||||||
|
|
||||||
|
func (h analyticsEventHeap) Len() int { return len(h) }
|
||||||
|
func (h analyticsEventHeap) Less(i, j int) bool { return eventStampCmp(h[i].stamp, h[j].stamp) }
|
||||||
|
func (h analyticsEventHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
||||||
|
|
||||||
|
// Push and Pop are unused: only heap.Init and heap.Fix are used.
|
||||||
|
func (h analyticsEventHeap) Push(x interface{}) {}
|
||||||
|
func (h analyticsEventHeap) Pop() interface{} { return nil }
|
||||||
|
|
||||||
|
func newAnalyticsEvents(max int) *analyticsEvents {
|
||||||
|
return &analyticsEvents{
|
||||||
|
numSeen: 0,
|
||||||
|
events: make(analyticsEventHeap, 0, max),
|
||||||
|
failedHarvests: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (events *analyticsEvents) addEvent(e analyticsEvent) {
|
||||||
|
events.numSeen++
|
||||||
|
|
||||||
|
if len(events.events) < cap(events.events) {
|
||||||
|
events.events = append(events.events, e)
|
||||||
|
if len(events.events) == cap(events.events) {
|
||||||
|
// Delay heap initialization so that we can have
|
||||||
|
// deterministic ordering for integration tests (the max
|
||||||
|
// is not being reached).
|
||||||
|
heap.Init(events.events)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if eventStampCmp(e.stamp, events.events[0].stamp) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
events.events[0] = e
|
||||||
|
heap.Fix(events.events, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (events *analyticsEvents) mergeFailed(other *analyticsEvents) {
|
||||||
|
fails := other.failedHarvests + 1
|
||||||
|
if fails >= failedEventsAttemptsLimit {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
events.failedHarvests = fails
|
||||||
|
events.Merge(other)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (events *analyticsEvents) Merge(other *analyticsEvents) {
|
||||||
|
allSeen := events.numSeen + other.numSeen
|
||||||
|
|
||||||
|
for _, e := range other.events {
|
||||||
|
events.addEvent(e)
|
||||||
|
}
|
||||||
|
events.numSeen = allSeen
|
||||||
|
}
|
||||||
|
|
||||||
|
func (events *analyticsEvents) CollectorJSON(agentRunID string) ([]byte, error) {
|
||||||
|
if 0 == events.numSeen {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
estimate := 256 * len(events.events)
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, estimate))
|
||||||
|
|
||||||
|
buf.WriteByte('[')
|
||||||
|
jsonx.AppendString(buf, agentRunID)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
buf.WriteByte('{')
|
||||||
|
buf.WriteString(`"reservoir_size":`)
|
||||||
|
jsonx.AppendUint(buf, uint64(cap(events.events)))
|
||||||
|
buf.WriteByte(',')
|
||||||
|
buf.WriteString(`"events_seen":`)
|
||||||
|
jsonx.AppendUint(buf, uint64(events.numSeen))
|
||||||
|
buf.WriteByte('}')
|
||||||
|
buf.WriteByte(',')
|
||||||
|
buf.WriteByte('[')
|
||||||
|
for i, e := range events.events {
|
||||||
|
if i > 0 {
|
||||||
|
buf.WriteByte(',')
|
||||||
|
}
|
||||||
|
e.WriteJSON(buf)
|
||||||
|
}
|
||||||
|
buf.WriteByte(']')
|
||||||
|
buf.WriteByte(']')
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// ApdexZone is a transaction classification.
|
||||||
|
type ApdexZone int
|
||||||
|
|
||||||
|
// https://en.wikipedia.org/wiki/Apdex
|
||||||
|
const (
|
||||||
|
ApdexNone ApdexZone = iota
|
||||||
|
ApdexSatisfying
|
||||||
|
ApdexTolerating
|
||||||
|
ApdexFailing
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApdexFailingThreshold calculates the threshold at which the transaction is
|
||||||
|
// considered a failure.
|
||||||
|
func ApdexFailingThreshold(threshold time.Duration) time.Duration {
|
||||||
|
return 4 * threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateApdexZone calculates the apdex based on the transaction duration and
|
||||||
|
// threshold.
|
||||||
|
//
|
||||||
|
// Note that this does not take into account whether or not the transaction
|
||||||
|
// had an error. That is expected to be done by the caller.
|
||||||
|
func CalculateApdexZone(threshold, duration time.Duration) ApdexZone {
|
||||||
|
if duration <= threshold {
|
||||||
|
return ApdexSatisfying
|
||||||
|
}
|
||||||
|
if duration <= ApdexFailingThreshold(threshold) {
|
||||||
|
return ApdexTolerating
|
||||||
|
}
|
||||||
|
return ApdexFailing
|
||||||
|
}
|
||||||
|
|
||||||
|
func (zone ApdexZone) label() string {
|
||||||
|
switch zone {
|
||||||
|
case ApdexSatisfying:
|
||||||
|
return "S"
|
||||||
|
case ApdexTolerating:
|
||||||
|
return "T"
|
||||||
|
case ApdexFailing:
|
||||||
|
return "F"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,572 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New agent attributes must be added in the following places:
|
||||||
|
// * Constants here.
|
||||||
|
// * Top level attributes.go file.
|
||||||
|
// * agentAttributes
|
||||||
|
// * agentAttributeDests
|
||||||
|
// * calculateAgentAttributeDests
|
||||||
|
// * writeAgentAttributes
|
||||||
|
const (
|
||||||
|
responseCode = "httpResponseCode"
|
||||||
|
requestMethod = "request.method"
|
||||||
|
requestAccept = "request.headers.accept"
|
||||||
|
requestContentType = "request.headers.contentType"
|
||||||
|
requestContentLength = "request.headers.contentLength"
|
||||||
|
requestHost = "request.headers.host"
|
||||||
|
responseContentType = "response.headers.contentType"
|
||||||
|
responseContentLength = "response.headers.contentLength"
|
||||||
|
hostDisplayName = "host.displayName"
|
||||||
|
requestUserAgent = "request.headers.User-Agent"
|
||||||
|
requestReferer = "request.headers.referer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://source.datanerd.us/agents/agent-specs/blob/master/Agent-Attributes-PORTED.md
|
||||||
|
|
||||||
|
// AttributeDestinationConfig matches newrelic.AttributeDestinationConfig to
|
||||||
|
// avoid circular dependency issues.
|
||||||
|
type AttributeDestinationConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
Include []string
|
||||||
|
Exclude []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type destinationSet int
|
||||||
|
|
||||||
|
const (
|
||||||
|
destTxnEvent destinationSet = 1 << iota
|
||||||
|
destError
|
||||||
|
destTxnTrace
|
||||||
|
destBrowser
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
destNone destinationSet = 0
|
||||||
|
// DestAll contains all destinations.
|
||||||
|
DestAll destinationSet = destTxnEvent | destTxnTrace | destError | destBrowser
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
attributeWildcardSuffix = '*'
|
||||||
|
)
|
||||||
|
|
||||||
|
type attributeModifier struct {
|
||||||
|
match string // This will not contain a trailing '*'.
|
||||||
|
includeExclude
|
||||||
|
}
|
||||||
|
|
||||||
|
type byMatch []*attributeModifier
|
||||||
|
|
||||||
|
func (m byMatch) Len() int { return len(m) }
|
||||||
|
func (m byMatch) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
|
||||||
|
func (m byMatch) Less(i, j int) bool { return m[i].match < m[j].match }
|
||||||
|
|
||||||
|
// AttributeConfig is created at application creation and shared between all
|
||||||
|
// transactions.
|
||||||
|
type AttributeConfig struct {
|
||||||
|
disabledDestinations destinationSet
|
||||||
|
exactMatchModifiers map[string]*attributeModifier
|
||||||
|
// Once attributeConfig is constructed, wildcardModifiers is sorted in
|
||||||
|
// lexicographical order. Modifiers appearing later have precedence
|
||||||
|
// over modifiers appearing earlier.
|
||||||
|
wildcardModifiers []*attributeModifier
|
||||||
|
agentDests agentAttributeDests
|
||||||
|
}
|
||||||
|
|
||||||
|
type includeExclude struct {
|
||||||
|
include destinationSet
|
||||||
|
exclude destinationSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func modifierApply(m *attributeModifier, d destinationSet) destinationSet {
|
||||||
|
// Include before exclude, since exclude has priority.
|
||||||
|
d |= m.include
|
||||||
|
d &^= m.exclude
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyAttributeConfig(c *AttributeConfig, key string, d destinationSet) destinationSet {
|
||||||
|
// Important: The wildcard modifiers must be applied before the exact
|
||||||
|
// match modifiers, and the slice must be iterated in a forward
|
||||||
|
// direction.
|
||||||
|
for _, m := range c.wildcardModifiers {
|
||||||
|
if strings.HasPrefix(key, m.match) {
|
||||||
|
d = modifierApply(m, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m, ok := c.exactMatchModifiers[key]; ok {
|
||||||
|
d = modifierApply(m, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
d &^= c.disabledDestinations
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func addModifier(c *AttributeConfig, match string, d includeExclude) {
|
||||||
|
if "" == match {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exactMatch := true
|
||||||
|
if attributeWildcardSuffix == match[len(match)-1] {
|
||||||
|
exactMatch = false
|
||||||
|
match = match[0 : len(match)-1]
|
||||||
|
}
|
||||||
|
mod := &attributeModifier{
|
||||||
|
match: match,
|
||||||
|
includeExclude: d,
|
||||||
|
}
|
||||||
|
|
||||||
|
if exactMatch {
|
||||||
|
if m, ok := c.exactMatchModifiers[mod.match]; ok {
|
||||||
|
m.include |= mod.include
|
||||||
|
m.exclude |= mod.exclude
|
||||||
|
} else {
|
||||||
|
c.exactMatchModifiers[mod.match] = mod
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, m := range c.wildcardModifiers {
|
||||||
|
// Important: Duplicate entries for the same match
|
||||||
|
// string would not work because exclude needs
|
||||||
|
// precedence over include.
|
||||||
|
if m.match == mod.match {
|
||||||
|
m.include |= mod.include
|
||||||
|
m.exclude |= mod.exclude
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.wildcardModifiers = append(c.wildcardModifiers, mod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processDest(c *AttributeConfig, dc *AttributeDestinationConfig, d destinationSet) {
|
||||||
|
if !dc.Enabled {
|
||||||
|
c.disabledDestinations |= d
|
||||||
|
}
|
||||||
|
for _, match := range dc.Include {
|
||||||
|
addModifier(c, match, includeExclude{include: d})
|
||||||
|
}
|
||||||
|
for _, match := range dc.Exclude {
|
||||||
|
addModifier(c, match, includeExclude{exclude: d})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttributeConfigInput is used as the input to CreateAttributeConfig: it
|
||||||
|
// transforms newrelic.Config settings into an AttributeConfig.
|
||||||
|
type AttributeConfigInput struct {
|
||||||
|
Attributes AttributeDestinationConfig
|
||||||
|
ErrorCollector AttributeDestinationConfig
|
||||||
|
TransactionEvents AttributeDestinationConfig
|
||||||
|
browserMonitoring AttributeDestinationConfig
|
||||||
|
TransactionTracer AttributeDestinationConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
sampleAttributeConfigInput = AttributeConfigInput{
|
||||||
|
Attributes: AttributeDestinationConfig{Enabled: true},
|
||||||
|
ErrorCollector: AttributeDestinationConfig{Enabled: true},
|
||||||
|
TransactionEvents: AttributeDestinationConfig{Enabled: true},
|
||||||
|
TransactionTracer: AttributeDestinationConfig{Enabled: true},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateAttributeConfig creates a new AttributeConfig.
|
||||||
|
func CreateAttributeConfig(input AttributeConfigInput) *AttributeConfig {
|
||||||
|
c := &AttributeConfig{
|
||||||
|
exactMatchModifiers: make(map[string]*attributeModifier),
|
||||||
|
wildcardModifiers: make([]*attributeModifier, 0, 64),
|
||||||
|
}
|
||||||
|
|
||||||
|
processDest(c, &input.Attributes, DestAll)
|
||||||
|
processDest(c, &input.ErrorCollector, destError)
|
||||||
|
processDest(c, &input.TransactionEvents, destTxnEvent)
|
||||||
|
processDest(c, &input.TransactionTracer, destTxnTrace)
|
||||||
|
processDest(c, &input.browserMonitoring, destBrowser)
|
||||||
|
|
||||||
|
sort.Sort(byMatch(c.wildcardModifiers))
|
||||||
|
|
||||||
|
c.agentDests = calculateAgentAttributeDests(c)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
type userAttribute struct {
|
||||||
|
value interface{}
|
||||||
|
dests destinationSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attributes are key value pairs attached to the various collected data types.
|
||||||
|
type Attributes struct {
|
||||||
|
config *AttributeConfig
|
||||||
|
user map[string]userAttribute
|
||||||
|
Agent agentAttributes
|
||||||
|
}
|
||||||
|
|
||||||
|
type agentAttributes struct {
|
||||||
|
HostDisplayName string
|
||||||
|
RequestMethod string
|
||||||
|
RequestAcceptHeader string
|
||||||
|
RequestContentType string
|
||||||
|
RequestContentLength int
|
||||||
|
RequestHeadersHost string
|
||||||
|
RequestHeadersUserAgent string
|
||||||
|
RequestHeadersReferer string
|
||||||
|
ResponseHeadersContentType string
|
||||||
|
ResponseHeadersContentLength int
|
||||||
|
ResponseCode string
|
||||||
|
}
|
||||||
|
|
||||||
|
type agentAttributeDests struct {
|
||||||
|
HostDisplayName destinationSet
|
||||||
|
RequestMethod destinationSet
|
||||||
|
RequestAcceptHeader destinationSet
|
||||||
|
RequestContentType destinationSet
|
||||||
|
RequestContentLength destinationSet
|
||||||
|
RequestHeadersHost destinationSet
|
||||||
|
RequestHeadersUserAgent destinationSet
|
||||||
|
RequestHeadersReferer destinationSet
|
||||||
|
ResponseHeadersContentType destinationSet
|
||||||
|
ResponseHeadersContentLength destinationSet
|
||||||
|
ResponseCode destinationSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateAgentAttributeDests(c *AttributeConfig) agentAttributeDests {
|
||||||
|
usual := DestAll &^ destBrowser
|
||||||
|
traces := destTxnTrace | destError
|
||||||
|
return agentAttributeDests{
|
||||||
|
HostDisplayName: applyAttributeConfig(c, hostDisplayName, usual),
|
||||||
|
RequestMethod: applyAttributeConfig(c, requestMethod, usual),
|
||||||
|
RequestAcceptHeader: applyAttributeConfig(c, requestAccept, usual),
|
||||||
|
RequestContentType: applyAttributeConfig(c, requestContentType, usual),
|
||||||
|
RequestContentLength: applyAttributeConfig(c, requestContentLength, usual),
|
||||||
|
RequestHeadersHost: applyAttributeConfig(c, requestHost, usual),
|
||||||
|
RequestHeadersUserAgent: applyAttributeConfig(c, requestUserAgent, traces),
|
||||||
|
RequestHeadersReferer: applyAttributeConfig(c, requestReferer, traces),
|
||||||
|
ResponseHeadersContentType: applyAttributeConfig(c, responseContentType, usual),
|
||||||
|
ResponseHeadersContentLength: applyAttributeConfig(c, responseContentLength, usual),
|
||||||
|
ResponseCode: applyAttributeConfig(c, responseCode, usual),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type agentAttributeWriter struct {
|
||||||
|
jsonFieldsWriter
|
||||||
|
d destinationSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *agentAttributeWriter) writeString(name string, val string, d destinationSet) {
|
||||||
|
if "" != val && 0 != w.d&d {
|
||||||
|
w.stringField(name, truncateStringValueIfLong(val))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *agentAttributeWriter) writeInt(name string, val int, d destinationSet) {
|
||||||
|
if val >= 0 && 0 != w.d&d {
|
||||||
|
w.intField(name, int64(val))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAgentAttributes(buf *bytes.Buffer, d destinationSet, values agentAttributes, dests agentAttributeDests) {
|
||||||
|
w := &agentAttributeWriter{
|
||||||
|
jsonFieldsWriter: jsonFieldsWriter{buf: buf},
|
||||||
|
d: d,
|
||||||
|
}
|
||||||
|
buf.WriteByte('{')
|
||||||
|
w.writeString(hostDisplayName, values.HostDisplayName, dests.HostDisplayName)
|
||||||
|
w.writeString(requestMethod, values.RequestMethod, dests.RequestMethod)
|
||||||
|
w.writeString(requestAccept, values.RequestAcceptHeader, dests.RequestAcceptHeader)
|
||||||
|
w.writeString(requestContentType, values.RequestContentType, dests.RequestContentType)
|
||||||
|
w.writeInt(requestContentLength, values.RequestContentLength, dests.RequestContentLength)
|
||||||
|
w.writeString(requestHost, values.RequestHeadersHost, dests.RequestHeadersHost)
|
||||||
|
w.writeString(requestUserAgent, values.RequestHeadersUserAgent, dests.RequestHeadersUserAgent)
|
||||||
|
w.writeString(requestReferer, values.RequestHeadersReferer, dests.RequestHeadersReferer)
|
||||||
|
w.writeString(responseContentType, values.ResponseHeadersContentType, dests.ResponseHeadersContentType)
|
||||||
|
w.writeInt(responseContentLength, values.ResponseHeadersContentLength, dests.ResponseHeadersContentLength)
|
||||||
|
w.writeString(responseCode, values.ResponseCode, dests.ResponseCode)
|
||||||
|
buf.WriteByte('}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAttributes creates a new Attributes.
|
||||||
|
func NewAttributes(config *AttributeConfig) *Attributes {
|
||||||
|
return &Attributes{
|
||||||
|
config: config,
|
||||||
|
Agent: agentAttributes{
|
||||||
|
RequestContentLength: -1,
|
||||||
|
ResponseHeadersContentLength: -1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrInvalidAttribute is returned when the value is not valid.
|
||||||
|
type ErrInvalidAttribute struct{ typeString string }
|
||||||
|
|
||||||
|
func (e ErrInvalidAttribute) Error() string {
|
||||||
|
return fmt.Sprintf("attribute value type %s is invalid", e.typeString)
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueIsValid(val interface{}) error {
|
||||||
|
switch val.(type) {
|
||||||
|
case string, bool, nil,
|
||||||
|
uint8, uint16, uint32, uint64, int8, int16, int32, int64,
|
||||||
|
float32, float64, uint, int, uintptr:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return ErrInvalidAttribute{
|
||||||
|
typeString: fmt.Sprintf("%T", val),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type invalidAttributeKeyErr struct{ key string }
|
||||||
|
|
||||||
|
func (e invalidAttributeKeyErr) Error() string {
|
||||||
|
return fmt.Sprintf("attribute key '%.32s...' exceeds length limit %d",
|
||||||
|
e.key, attributeKeyLengthLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
type userAttributeLimitErr struct{ key string }
|
||||||
|
|
||||||
|
func (e userAttributeLimitErr) Error() string {
|
||||||
|
return fmt.Sprintf("attribute '%s' discarded: limit of %d reached", e.key,
|
||||||
|
attributeUserLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validAttributeKey(key string) error {
|
||||||
|
// Attributes whose keys are excessively long are dropped rather than
|
||||||
|
// truncated to avoid worrying about the application of configuration to
|
||||||
|
// truncated values or performing the truncation after configuration.
|
||||||
|
if len(key) > attributeKeyLengthLimit {
|
||||||
|
return invalidAttributeKeyErr{key: key}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateStringValueIfLong(val string) string {
|
||||||
|
if len(val) > attributeValueLengthLimit {
|
||||||
|
return StringLengthByteLimit(val, attributeValueLengthLimit)
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateStringValueIfLongInterface(val interface{}) interface{} {
|
||||||
|
if str, ok := val.(string); ok {
|
||||||
|
val = interface{}(truncateStringValueIfLong(str))
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddUserAttribute adds a user attribute.
|
||||||
|
func AddUserAttribute(a *Attributes, key string, val interface{}, d destinationSet) error {
|
||||||
|
val = truncateStringValueIfLongInterface(val)
|
||||||
|
if err := valueIsValid(val); nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := validAttributeKey(key); nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dests := applyAttributeConfig(a.config, key, d)
|
||||||
|
if destNone == dests {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if nil == a.user {
|
||||||
|
a.user = make(map[string]userAttribute)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := a.user[key]; !exists && len(a.user) >= attributeUserLimit {
|
||||||
|
return userAttributeLimitErr{key}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Duplicates are overridden: last attribute in wins.
|
||||||
|
a.user[key] = userAttribute{
|
||||||
|
value: val,
|
||||||
|
dests: dests,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAttributeValueJSON(w *jsonFieldsWriter, key string, val interface{}) {
|
||||||
|
switch v := val.(type) {
|
||||||
|
case nil:
|
||||||
|
w.rawField(key, `null`)
|
||||||
|
case string:
|
||||||
|
w.stringField(key, v)
|
||||||
|
case bool:
|
||||||
|
if v {
|
||||||
|
w.rawField(key, `true`)
|
||||||
|
} else {
|
||||||
|
w.rawField(key, `false`)
|
||||||
|
}
|
||||||
|
case uint8:
|
||||||
|
w.intField(key, int64(v))
|
||||||
|
case uint16:
|
||||||
|
w.intField(key, int64(v))
|
||||||
|
case uint32:
|
||||||
|
w.intField(key, int64(v))
|
||||||
|
case uint64:
|
||||||
|
w.intField(key, int64(v))
|
||||||
|
case uint:
|
||||||
|
w.intField(key, int64(v))
|
||||||
|
case uintptr:
|
||||||
|
w.intField(key, int64(v))
|
||||||
|
case int8:
|
||||||
|
w.intField(key, int64(v))
|
||||||
|
case int16:
|
||||||
|
w.intField(key, int64(v))
|
||||||
|
case int32:
|
||||||
|
w.intField(key, int64(v))
|
||||||
|
case int64:
|
||||||
|
w.intField(key, v)
|
||||||
|
case int:
|
||||||
|
w.intField(key, int64(v))
|
||||||
|
case float32:
|
||||||
|
w.floatField(key, float64(v))
|
||||||
|
case float64:
|
||||||
|
w.floatField(key, v)
|
||||||
|
default:
|
||||||
|
w.stringField(key, fmt.Sprintf("%T", v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type agentAttributesJSONWriter struct {
|
||||||
|
attributes *Attributes
|
||||||
|
dest destinationSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w agentAttributesJSONWriter) WriteJSON(buf *bytes.Buffer) {
|
||||||
|
if nil == w.attributes {
|
||||||
|
buf.WriteString("{}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeAgentAttributes(buf, w.dest, w.attributes.Agent, w.attributes.config.agentDests)
|
||||||
|
}
|
||||||
|
|
||||||
|
func agentAttributesJSON(a *Attributes, buf *bytes.Buffer, d destinationSet) {
|
||||||
|
agentAttributesJSONWriter{
|
||||||
|
attributes: a,
|
||||||
|
dest: d,
|
||||||
|
}.WriteJSON(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
type userAttributesJSONWriter struct {
|
||||||
|
attributes *Attributes
|
||||||
|
dest destinationSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u userAttributesJSONWriter) WriteJSON(buf *bytes.Buffer) {
|
||||||
|
buf.WriteByte('{')
|
||||||
|
if nil != u.attributes {
|
||||||
|
w := jsonFieldsWriter{buf: buf}
|
||||||
|
for name, atr := range u.attributes.user {
|
||||||
|
if 0 != atr.dests&u.dest {
|
||||||
|
writeAttributeValueJSON(&w, name, atr.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.WriteByte('}')
|
||||||
|
}
|
||||||
|
|
||||||
|
func userAttributesJSON(a *Attributes, buf *bytes.Buffer, d destinationSet) {
|
||||||
|
userAttributesJSONWriter{
|
||||||
|
attributes: a,
|
||||||
|
dest: d,
|
||||||
|
}.WriteJSON(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userAttributesStringJSON(a *Attributes, d destinationSet) JSONString {
|
||||||
|
if nil == a {
|
||||||
|
return JSONString("{}")
|
||||||
|
}
|
||||||
|
estimate := len(a.user) * 128
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, estimate))
|
||||||
|
userAttributesJSON(a, buf, d)
|
||||||
|
bs := buf.Bytes()
|
||||||
|
return JSONString(bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func agentAttributesStringJSON(a *Attributes, d destinationSet) JSONString {
|
||||||
|
if nil == a {
|
||||||
|
return JSONString("{}")
|
||||||
|
}
|
||||||
|
estimate := 1024
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, estimate))
|
||||||
|
agentAttributesJSON(a, buf, d)
|
||||||
|
return JSONString(buf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserAttributes(a *Attributes, d destinationSet) map[string]interface{} {
|
||||||
|
v := make(map[string]interface{})
|
||||||
|
json.Unmarshal([]byte(userAttributesStringJSON(a, d)), &v)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAgentAttributes(a *Attributes, d destinationSet) map[string]interface{} {
|
||||||
|
v := make(map[string]interface{})
|
||||||
|
json.Unmarshal([]byte(agentAttributesStringJSON(a, d)), &v)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestAgentAttributes gathers agent attributes out of the request.
|
||||||
|
func RequestAgentAttributes(a *Attributes, r *http.Request) {
|
||||||
|
a.Agent.RequestMethod = r.Method
|
||||||
|
|
||||||
|
h := r.Header
|
||||||
|
if nil == h {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.Agent.RequestAcceptHeader = h.Get("Accept")
|
||||||
|
a.Agent.RequestContentType = h.Get("Content-Type")
|
||||||
|
a.Agent.RequestHeadersHost = h.Get("Host")
|
||||||
|
a.Agent.RequestHeadersUserAgent = h.Get("User-Agent")
|
||||||
|
a.Agent.RequestHeadersReferer = SafeURLFromString(h.Get("Referer"))
|
||||||
|
|
||||||
|
if cl := h.Get("Content-Length"); "" != cl {
|
||||||
|
if x, err := strconv.Atoi(cl); nil == err {
|
||||||
|
a.Agent.RequestContentLength = x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseHeaderAttributes gather agent attributes from the response headers.
|
||||||
|
func ResponseHeaderAttributes(a *Attributes, h http.Header) {
|
||||||
|
if nil == h {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.Agent.ResponseHeadersContentType = h.Get("Content-Type")
|
||||||
|
if val := h.Get("Content-Length"); "" != val {
|
||||||
|
if x, err := strconv.Atoi(val); nil == err {
|
||||||
|
a.Agent.ResponseHeadersContentLength = x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// statusCodeLookup avoids a strconv.Itoa call.
|
||||||
|
statusCodeLookup = map[int]string{
|
||||||
|
100: "100", 101: "101",
|
||||||
|
200: "200", 201: "201", 202: "202", 203: "203", 204: "204", 205: "205", 206: "206",
|
||||||
|
300: "300", 301: "301", 302: "302", 303: "303", 304: "304", 305: "305", 307: "307",
|
||||||
|
400: "400", 401: "401", 402: "402", 403: "403", 404: "404", 405: "405", 406: "406",
|
||||||
|
407: "407", 408: "408", 409: "409", 410: "410", 411: "411", 412: "412", 413: "413",
|
||||||
|
414: "414", 415: "415", 416: "416", 417: "417", 418: "418", 428: "428", 429: "429",
|
||||||
|
431: "431", 451: "451",
|
||||||
|
500: "500", 501: "501", 502: "502", 503: "503", 504: "504", 505: "505", 511: "511",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResponseCodeAttribute sets the response code agent attribute.
|
||||||
|
func ResponseCodeAttribute(a *Attributes, code int) {
|
||||||
|
a.Agent.ResponseCode = statusCodeLookup[code]
|
||||||
|
if a.Agent.ResponseCode == "" {
|
||||||
|
a.Agent.ResponseCode = strconv.Itoa(code)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,267 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/newrelic/go-agent/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
procotolVersion = "14"
|
||||||
|
userAgentPrefix = "NewRelic-Go-Agent/"
|
||||||
|
|
||||||
|
// Methods used in collector communication.
|
||||||
|
cmdRedirect = "get_redirect_host"
|
||||||
|
cmdConnect = "connect"
|
||||||
|
cmdMetrics = "metric_data"
|
||||||
|
cmdCustomEvents = "custom_event_data"
|
||||||
|
cmdTxnEvents = "analytic_event_data"
|
||||||
|
cmdErrorEvents = "error_event_data"
|
||||||
|
cmdErrorData = "error_data"
|
||||||
|
cmdTxnTraces = "transaction_sample_data"
|
||||||
|
cmdSlowSQLs = "sql_trace_data"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrPayloadTooLarge is created in response to receiving a 413 response
|
||||||
|
// code.
|
||||||
|
ErrPayloadTooLarge = errors.New("payload too large")
|
||||||
|
// ErrUnsupportedMedia is created in response to receiving a 415
|
||||||
|
// response code.
|
||||||
|
ErrUnsupportedMedia = errors.New("unsupported media")
|
||||||
|
)
|
||||||
|
|
||||||
|
// RpmCmd contains fields specific to an individual call made to RPM.
|
||||||
|
type RpmCmd struct {
|
||||||
|
Name string
|
||||||
|
Collector string
|
||||||
|
RunID string
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// RpmControls contains fields which will be the same for all calls made
|
||||||
|
// by the same application.
|
||||||
|
type RpmControls struct {
|
||||||
|
UseTLS bool
|
||||||
|
License string
|
||||||
|
Client *http.Client
|
||||||
|
Logger logger.Logger
|
||||||
|
AgentVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpmURL(cmd RpmCmd, cs RpmControls) string {
|
||||||
|
var u url.URL
|
||||||
|
|
||||||
|
u.Host = cmd.Collector
|
||||||
|
u.Path = "agent_listener/invoke_raw_method"
|
||||||
|
|
||||||
|
if cs.UseTLS {
|
||||||
|
u.Scheme = "https"
|
||||||
|
} else {
|
||||||
|
u.Scheme = "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
query := url.Values{}
|
||||||
|
query.Set("marshal_format", "json")
|
||||||
|
query.Set("protocol_version", procotolVersion)
|
||||||
|
query.Set("method", cmd.Name)
|
||||||
|
query.Set("license_key", cs.License)
|
||||||
|
|
||||||
|
if len(cmd.RunID) > 0 {
|
||||||
|
query.Set("run_id", cmd.RunID)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.RawQuery = query.Encode()
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type unexpectedStatusCodeErr struct {
|
||||||
|
code int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e unexpectedStatusCodeErr) Error() string {
|
||||||
|
return fmt.Sprintf("unexpected HTTP status code: %d", e.code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectorRequestInternal(url string, data []byte, cs RpmControls) ([]byte, error) {
|
||||||
|
deflated, err := compress(data)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(deflated))
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Accept-Encoding", "identity, deflate")
|
||||||
|
req.Header.Add("Content-Type", "application/octet-stream")
|
||||||
|
req.Header.Add("User-Agent", userAgentPrefix+cs.AgentVersion)
|
||||||
|
req.Header.Add("Content-Encoding", "deflate")
|
||||||
|
|
||||||
|
resp, err := cs.Client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if 413 == resp.StatusCode {
|
||||||
|
return nil, ErrPayloadTooLarge
|
||||||
|
}
|
||||||
|
|
||||||
|
if 415 == resp.StatusCode {
|
||||||
|
return nil, ErrUnsupportedMedia
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the response code is not 200, then the collector may not return
|
||||||
|
// valid JSON.
|
||||||
|
if 200 != resp.StatusCode {
|
||||||
|
return nil, unexpectedStatusCodeErr{code: resp.StatusCode}
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parseResponse(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectorRequest makes a request to New Relic.
|
||||||
|
func CollectorRequest(cmd RpmCmd, cs RpmControls) ([]byte, error) {
|
||||||
|
url := rpmURL(cmd, cs)
|
||||||
|
|
||||||
|
if cs.Logger.DebugEnabled() {
|
||||||
|
cs.Logger.Debug("rpm request", map[string]interface{}{
|
||||||
|
"command": cmd.Name,
|
||||||
|
"url": url,
|
||||||
|
"payload": JSONString(cmd.Data),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := collectorRequestInternal(url, cmd.Data, cs)
|
||||||
|
if err != nil {
|
||||||
|
cs.Logger.Debug("rpm failure", map[string]interface{}{
|
||||||
|
"command": cmd.Name,
|
||||||
|
"url": url,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if cs.Logger.DebugEnabled() {
|
||||||
|
cs.Logger.Debug("rpm response", map[string]interface{}{
|
||||||
|
"command": cmd.Name,
|
||||||
|
"url": url,
|
||||||
|
"response": JSONString(resp),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type rpmException struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
ErrorType string `json:"error_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *rpmException) Error() string {
|
||||||
|
return fmt.Sprintf("%s: %s", e.ErrorType, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasType(e error, expected string) bool {
|
||||||
|
rpmErr, ok := e.(*rpmException)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return rpmErr.ErrorType == expected
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
forceRestartType = "NewRelic::Agent::ForceRestartException"
|
||||||
|
disconnectType = "NewRelic::Agent::ForceDisconnectException"
|
||||||
|
licenseInvalidType = "NewRelic::Agent::LicenseException"
|
||||||
|
runtimeType = "RuntimeError"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsRestartException indicates if the error was a restart exception.
|
||||||
|
func IsRestartException(e error) bool { return hasType(e, forceRestartType) }
|
||||||
|
|
||||||
|
// IsLicenseException indicates if the error was an invalid exception.
|
||||||
|
func IsLicenseException(e error) bool { return hasType(e, licenseInvalidType) }
|
||||||
|
|
||||||
|
// IsRuntime indicates if the error was a runtime exception.
|
||||||
|
func IsRuntime(e error) bool { return hasType(e, runtimeType) }
|
||||||
|
|
||||||
|
// IsDisconnect indicates if the error was a disconnect exception.
|
||||||
|
func IsDisconnect(e error) bool { return hasType(e, disconnectType) }
|
||||||
|
|
||||||
|
func parseResponse(b []byte) ([]byte, error) {
|
||||||
|
var r struct {
|
||||||
|
ReturnValue json.RawMessage `json:"return_value"`
|
||||||
|
Exception *rpmException `json:"exception"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal(b, &r)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if nil != r.Exception {
|
||||||
|
return nil, r.Exception
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.ReturnValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectAttempt tries to connect an application.
|
||||||
|
func ConnectAttempt(js []byte, redirectHost string, cs RpmControls) (*AppRun, error) {
|
||||||
|
call := RpmCmd{
|
||||||
|
Name: cmdRedirect,
|
||||||
|
Collector: redirectHost,
|
||||||
|
Data: []byte("[]"),
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := CollectorRequest(call, cs)
|
||||||
|
if nil != err {
|
||||||
|
// err is intentionally unmodified: We do not want to change
|
||||||
|
// the type of these collector errors.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var host string
|
||||||
|
err = json.Unmarshal(out, &host)
|
||||||
|
if nil != err {
|
||||||
|
return nil, fmt.Errorf("unable to parse redirect reply: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
call.Collector = host
|
||||||
|
call.Data = js
|
||||||
|
call.Name = cmdConnect
|
||||||
|
|
||||||
|
rawReply, err := CollectorRequest(call, cs)
|
||||||
|
if nil != err {
|
||||||
|
// err is intentionally unmodified: We do not want to change
|
||||||
|
// the type of these collector errors.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reply := ConnectReplyDefaults()
|
||||||
|
err = json.Unmarshal(rawReply, reply)
|
||||||
|
if nil != err {
|
||||||
|
return nil, fmt.Errorf("unable to parse connect reply: %v", err)
|
||||||
|
}
|
||||||
|
// Note: This should never happen. It would mean the collector
|
||||||
|
// response is malformed. This exists merely as extra defensiveness.
|
||||||
|
if "" == reply.RunID {
|
||||||
|
return nil, errors.New("connect reply missing agent run id")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AppRun{reply, host}, nil
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/zlib"
|
||||||
|
"encoding/base64"
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func compress(b []byte) ([]byte, error) {
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
w := zlib.NewWriter(&buf)
|
||||||
|
_, err := w.Write(b)
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uncompress(b []byte) ([]byte, error) {
|
||||||
|
buf := bytes.NewBuffer(b)
|
||||||
|
r, err := zlib.NewReader(buf)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
return ioutil.ReadAll(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func compressEncode(b []byte) (string, error) {
|
||||||
|
compressed, err := compress(b)
|
||||||
|
|
||||||
|
if nil != err {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(compressed), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uncompressDecode(s string) ([]byte, error) {
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(s)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return uncompress(decoded)
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AgentRunID identifies the current connection with the collector.
|
||||||
|
type AgentRunID string
|
||||||
|
|
||||||
|
func (id AgentRunID) String() string {
|
||||||
|
return string(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppRun contains information regarding a single connection session with the
|
||||||
|
// collector. It is created upon application connect and is afterwards
|
||||||
|
// immutable.
|
||||||
|
type AppRun struct {
|
||||||
|
*ConnectReply
|
||||||
|
Collector string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectReply contains all of the settings and state send down from the
|
||||||
|
// collector. It should not be modified after creation.
|
||||||
|
type ConnectReply struct {
|
||||||
|
RunID AgentRunID `json:"agent_run_id"`
|
||||||
|
|
||||||
|
// Transaction Name Modifiers
|
||||||
|
SegmentTerms segmentRules `json:"transaction_segment_terms"`
|
||||||
|
TxnNameRules metricRules `json:"transaction_name_rules"`
|
||||||
|
URLRules metricRules `json:"url_rules"`
|
||||||
|
MetricRules metricRules `json:"metric_name_rules"`
|
||||||
|
|
||||||
|
// Cross Process
|
||||||
|
EncodingKey string `json:"encoding_key"`
|
||||||
|
CrossProcessID string `json:"cross_process_id"`
|
||||||
|
TrustedAccounts []int `json:"trusted_account_ids"`
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
KeyTxnApdex map[string]float64 `json:"web_transactions_apdex"`
|
||||||
|
ApdexThresholdSeconds float64 `json:"apdex_t"`
|
||||||
|
CollectAnalyticsEvents bool `json:"collect_analytics_events"`
|
||||||
|
CollectCustomEvents bool `json:"collect_custom_events"`
|
||||||
|
CollectTraces bool `json:"collect_traces"`
|
||||||
|
CollectErrors bool `json:"collect_errors"`
|
||||||
|
CollectErrorEvents bool `json:"collect_error_events"`
|
||||||
|
|
||||||
|
// RUM
|
||||||
|
AgentLoader string `json:"js_agent_loader"`
|
||||||
|
Beacon string `json:"beacon"`
|
||||||
|
BrowserKey string `json:"browser_key"`
|
||||||
|
AppID string `json:"application_id"`
|
||||||
|
ErrorBeacon string `json:"error_beacon"`
|
||||||
|
JSAgentFile string `json:"js_agent_file"`
|
||||||
|
|
||||||
|
Messages []struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
} `json:"messages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectReplyDefaults returns a newly allocated ConnectReply with the proper
|
||||||
|
// default settings. A pointer to a global is not used to prevent consumers
|
||||||
|
// from changing the default settings.
|
||||||
|
func ConnectReplyDefaults() *ConnectReply {
|
||||||
|
return &ConnectReply{
|
||||||
|
ApdexThresholdSeconds: 0.5,
|
||||||
|
CollectAnalyticsEvents: true,
|
||||||
|
CollectCustomEvents: true,
|
||||||
|
CollectTraces: true,
|
||||||
|
CollectErrors: true,
|
||||||
|
CollectErrorEvents: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateApdexThreshold calculates the apdex threshold.
|
||||||
|
func CalculateApdexThreshold(c *ConnectReply, txnName string) time.Duration {
|
||||||
|
if t, ok := c.KeyTxnApdex[txnName]; ok {
|
||||||
|
return floatSecondsToDuration(t)
|
||||||
|
}
|
||||||
|
return floatSecondsToDuration(c.ApdexThresholdSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateFullTxnName uses collector rules and the appropriate metric prefix to
|
||||||
|
// construct the full transaction metric name from the name given by the
|
||||||
|
// consumer.
|
||||||
|
func CreateFullTxnName(input string, reply *ConnectReply, isWeb bool) string {
|
||||||
|
var afterURLRules string
|
||||||
|
if "" != input {
|
||||||
|
afterURLRules = reply.URLRules.Apply(input)
|
||||||
|
if "" == afterURLRules {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := backgroundMetricPrefix
|
||||||
|
if isWeb {
|
||||||
|
prefix = webMetricPrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
var beforeNameRules string
|
||||||
|
if strings.HasPrefix(afterURLRules, "/") {
|
||||||
|
beforeNameRules = prefix + afterURLRules
|
||||||
|
} else {
|
||||||
|
beforeNameRules = prefix + "/" + afterURLRules
|
||||||
|
}
|
||||||
|
|
||||||
|
afterNameRules := reply.TxnNameRules.Apply(beforeNameRules)
|
||||||
|
if "" == afterNameRules {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.SegmentTerms.apply(afterNameRules)
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://newrelic.atlassian.net/wiki/display/eng/Custom+Events+in+New+Relic+Agents
|
||||||
|
|
||||||
|
var (
|
||||||
|
eventTypeRegexRaw = `^[a-zA-Z0-9:_ ]+$`
|
||||||
|
eventTypeRegex = regexp.MustCompile(eventTypeRegexRaw)
|
||||||
|
|
||||||
|
errEventTypeLength = fmt.Errorf("event type exceeds length limit of %d",
|
||||||
|
attributeKeyLengthLimit)
|
||||||
|
// ErrEventTypeRegex will be returned to caller of app.RecordCustomEvent
|
||||||
|
// if the event type is not valid.
|
||||||
|
ErrEventTypeRegex = fmt.Errorf("event type must match %s", eventTypeRegexRaw)
|
||||||
|
errNumAttributes = fmt.Errorf("maximum of %d attributes exceeded",
|
||||||
|
customEventAttributeLimit)
|
||||||
|
)
|
||||||
|
|
||||||
|
// CustomEvent is a custom event.
|
||||||
|
type CustomEvent struct {
|
||||||
|
eventType string
|
||||||
|
timestamp time.Time
|
||||||
|
truncatedParams map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteJSON prepares JSON in the format expected by the collector.
|
||||||
|
func (e *CustomEvent) WriteJSON(buf *bytes.Buffer) {
|
||||||
|
w := jsonFieldsWriter{buf: buf}
|
||||||
|
buf.WriteByte('[')
|
||||||
|
buf.WriteByte('{')
|
||||||
|
w.stringField("type", e.eventType)
|
||||||
|
w.floatField("timestamp", timeToFloatSeconds(e.timestamp))
|
||||||
|
buf.WriteByte('}')
|
||||||
|
|
||||||
|
buf.WriteByte(',')
|
||||||
|
buf.WriteByte('{')
|
||||||
|
w = jsonFieldsWriter{buf: buf}
|
||||||
|
for key, val := range e.truncatedParams {
|
||||||
|
writeAttributeValueJSON(&w, key, val)
|
||||||
|
}
|
||||||
|
buf.WriteByte('}')
|
||||||
|
|
||||||
|
buf.WriteByte(',')
|
||||||
|
buf.WriteByte('{')
|
||||||
|
buf.WriteByte('}')
|
||||||
|
buf.WriteByte(']')
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON is used for testing.
|
||||||
|
func (e *CustomEvent) MarshalJSON() ([]byte, error) {
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, 256))
|
||||||
|
|
||||||
|
e.WriteJSON(buf)
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func eventTypeValidate(eventType string) error {
|
||||||
|
if len(eventType) > attributeKeyLengthLimit {
|
||||||
|
return errEventTypeLength
|
||||||
|
}
|
||||||
|
if !eventTypeRegex.MatchString(eventType) {
|
||||||
|
return ErrEventTypeRegex
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCustomEvent creates a custom event.
|
||||||
|
func CreateCustomEvent(eventType string, params map[string]interface{}, now time.Time) (*CustomEvent, error) {
|
||||||
|
if err := eventTypeValidate(eventType); nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(params) > customEventAttributeLimit {
|
||||||
|
return nil, errNumAttributes
|
||||||
|
}
|
||||||
|
|
||||||
|
truncatedParams := make(map[string]interface{})
|
||||||
|
for key, val := range params {
|
||||||
|
if err := validAttributeKey(key); nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
val = truncateStringValueIfLongInterface(val)
|
||||||
|
|
||||||
|
if err := valueIsValid(val); nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
truncatedParams[key] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CustomEvent{
|
||||||
|
eventType: eventType,
|
||||||
|
timestamp: now,
|
||||||
|
truncatedParams: truncatedParams,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeIntoHarvest implements Harvestable.
|
||||||
|
func (e *CustomEvent) MergeIntoHarvest(h *Harvest) {
|
||||||
|
h.CustomEvents.Add(e)
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type customEvents struct {
|
||||||
|
events *analyticsEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCustomEvents(max int) *customEvents {
|
||||||
|
return &customEvents{
|
||||||
|
events: newAnalyticsEvents(max),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *customEvents) Add(e *CustomEvent) {
|
||||||
|
stamp := eventStamp(rand.Float32())
|
||||||
|
cs.events.addEvent(analyticsEvent{stamp, e})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *customEvents) MergeIntoHarvest(h *Harvest) {
|
||||||
|
h.CustomEvents.events.mergeFailed(cs.events)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *customEvents) Data(agentRunID string, harvestStart time.Time) ([]byte, error) {
|
||||||
|
return cs.events.CollectorJSON(agentRunID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *customEvents) numSeen() float64 { return cs.events.NumSeen() }
|
||||||
|
func (cs *customEvents) numSaved() float64 { return cs.events.NumSaved() }
|
|
@ -0,0 +1,61 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Environment describes the application's environment.
|
||||||
|
type Environment struct {
|
||||||
|
Compiler string `env:"runtime.Compiler"`
|
||||||
|
GOARCH string `env:"runtime.GOARCH"`
|
||||||
|
GOOS string `env:"runtime.GOOS"`
|
||||||
|
Version string `env:"runtime.Version"`
|
||||||
|
NumCPU int `env:"runtime.NumCPU"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// SampleEnvironment is useful for testing.
|
||||||
|
SampleEnvironment = Environment{
|
||||||
|
Compiler: "comp",
|
||||||
|
GOARCH: "arch",
|
||||||
|
GOOS: "goos",
|
||||||
|
Version: "vers",
|
||||||
|
NumCPU: 8,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewEnvironment returns a new Environment.
|
||||||
|
func NewEnvironment() Environment {
|
||||||
|
return Environment{
|
||||||
|
Compiler: runtime.Compiler,
|
||||||
|
GOARCH: runtime.GOARCH,
|
||||||
|
GOOS: runtime.GOOS,
|
||||||
|
Version: runtime.Version(),
|
||||||
|
NumCPU: runtime.NumCPU(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON prepares Environment JSON in the format expected by the collector
|
||||||
|
// during the connect command.
|
||||||
|
func (e Environment) MarshalJSON() ([]byte, error) {
|
||||||
|
var arr [][]interface{}
|
||||||
|
|
||||||
|
val := reflect.ValueOf(e)
|
||||||
|
numFields := val.NumField()
|
||||||
|
|
||||||
|
arr = make([][]interface{}, numFields)
|
||||||
|
|
||||||
|
for i := 0; i < numFields; i++ {
|
||||||
|
v := val.Field(i)
|
||||||
|
t := val.Type().Field(i).Tag.Get("env")
|
||||||
|
|
||||||
|
arr[i] = []interface{}{
|
||||||
|
t,
|
||||||
|
v.Interface(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(arr)
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrorEvent is an error event.
|
||||||
|
type ErrorEvent struct {
|
||||||
|
Klass string
|
||||||
|
Msg string
|
||||||
|
When time.Time
|
||||||
|
TxnName string
|
||||||
|
Duration time.Duration
|
||||||
|
Queuing time.Duration
|
||||||
|
Attrs *Attributes
|
||||||
|
DatastoreExternalTotals
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON is used for testing.
|
||||||
|
func (e *ErrorEvent) MarshalJSON() ([]byte, error) {
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, 256))
|
||||||
|
|
||||||
|
e.WriteJSON(buf)
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteJSON prepares JSON in the format expected by the collector.
|
||||||
|
// https://source.datanerd.us/agents/agent-specs/blob/master/Error-Events.md
|
||||||
|
func (e *ErrorEvent) WriteJSON(buf *bytes.Buffer) {
|
||||||
|
w := jsonFieldsWriter{buf: buf}
|
||||||
|
buf.WriteByte('[')
|
||||||
|
buf.WriteByte('{')
|
||||||
|
w.stringField("type", "TransactionError")
|
||||||
|
w.stringField("error.class", e.Klass)
|
||||||
|
w.stringField("error.message", e.Msg)
|
||||||
|
w.floatField("timestamp", timeToFloatSeconds(e.When))
|
||||||
|
w.stringField("transactionName", e.TxnName)
|
||||||
|
w.floatField("duration", e.Duration.Seconds())
|
||||||
|
if e.Queuing > 0 {
|
||||||
|
w.floatField("queueDuration", e.Queuing.Seconds())
|
||||||
|
}
|
||||||
|
if e.externalCallCount > 0 {
|
||||||
|
w.intField("externalCallCount", int64(e.externalCallCount))
|
||||||
|
w.floatField("externalDuration", e.externalDuration.Seconds())
|
||||||
|
}
|
||||||
|
if e.datastoreCallCount > 0 {
|
||||||
|
// Note that "database" is used for the keys here instead of
|
||||||
|
// "datastore" for historical reasons.
|
||||||
|
w.intField("databaseCallCount", int64(e.datastoreCallCount))
|
||||||
|
w.floatField("databaseDuration", e.datastoreDuration.Seconds())
|
||||||
|
}
|
||||||
|
buf.WriteByte('}')
|
||||||
|
buf.WriteByte(',')
|
||||||
|
userAttributesJSON(e.Attrs, buf, destError)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
agentAttributesJSON(e.Attrs, buf, destError)
|
||||||
|
buf.WriteByte(']')
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorEvents struct {
|
||||||
|
events *analyticsEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
func newErrorEvents(max int) *errorEvents {
|
||||||
|
return &errorEvents{
|
||||||
|
events: newAnalyticsEvents(max),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (events *errorEvents) Add(e *ErrorEvent) {
|
||||||
|
stamp := eventStamp(rand.Float32())
|
||||||
|
events.events.addEvent(analyticsEvent{stamp, e})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (events *errorEvents) MergeIntoHarvest(h *Harvest) {
|
||||||
|
h.ErrorEvents.events.mergeFailed(events.events)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (events *errorEvents) Data(agentRunID string, harvestStart time.Time) ([]byte, error) {
|
||||||
|
return events.events.CollectorJSON(agentRunID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (events *errorEvents) numSeen() float64 { return events.events.NumSeen() }
|
||||||
|
func (events *errorEvents) numSaved() float64 { return events.events.NumSaved() }
|
|
@ -0,0 +1,179 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/newrelic/go-agent/internal/jsonx"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PanicErrorKlass is the error klass used for errors generated by
|
||||||
|
// recovering panics in txn.End.
|
||||||
|
PanicErrorKlass = "panic"
|
||||||
|
)
|
||||||
|
|
||||||
|
func panicValueMsg(v interface{}) string {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case error:
|
||||||
|
return val.Error()
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TxnErrorFromPanic creates a new TxnError from a panic.
|
||||||
|
func TxnErrorFromPanic(now time.Time, v interface{}) TxnError {
|
||||||
|
return TxnError{
|
||||||
|
When: now,
|
||||||
|
Msg: panicValueMsg(v),
|
||||||
|
Klass: PanicErrorKlass,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TxnErrorFromError creates a new TxnError from an error.
|
||||||
|
func TxnErrorFromError(now time.Time, err error) TxnError {
|
||||||
|
return TxnError{
|
||||||
|
When: now,
|
||||||
|
Msg: err.Error(),
|
||||||
|
Klass: reflect.TypeOf(err).String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TxnErrorFromResponseCode creates a new TxnError from an http response code.
|
||||||
|
func TxnErrorFromResponseCode(now time.Time, code int) TxnError {
|
||||||
|
return TxnError{
|
||||||
|
When: now,
|
||||||
|
Msg: http.StatusText(code),
|
||||||
|
Klass: strconv.Itoa(code),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TxnError is an error captured in a Transaction.
|
||||||
|
type TxnError struct {
|
||||||
|
When time.Time
|
||||||
|
Stack *StackTrace
|
||||||
|
Msg string
|
||||||
|
Klass string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TxnErrors is a set of errors captured in a Transaction.
|
||||||
|
type TxnErrors []*TxnError
|
||||||
|
|
||||||
|
// NewTxnErrors returns a new empty TxnErrors.
|
||||||
|
func NewTxnErrors(max int) TxnErrors {
|
||||||
|
return make([]*TxnError, 0, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds a TxnError.
|
||||||
|
func (errors *TxnErrors) Add(e TxnError) {
|
||||||
|
if len(*errors) < cap(*errors) {
|
||||||
|
*errors = append(*errors, &e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *harvestError) WriteJSON(buf *bytes.Buffer) {
|
||||||
|
buf.WriteByte('[')
|
||||||
|
jsonx.AppendFloat(buf, timeToFloatMilliseconds(h.When))
|
||||||
|
buf.WriteByte(',')
|
||||||
|
jsonx.AppendString(buf, h.txnName)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
jsonx.AppendString(buf, h.Msg)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
jsonx.AppendString(buf, h.Klass)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
buf.WriteByte('{')
|
||||||
|
w := jsonFieldsWriter{buf: buf}
|
||||||
|
if nil != h.Stack {
|
||||||
|
w.writerField("stack_trace", h.Stack)
|
||||||
|
}
|
||||||
|
w.writerField("agentAttributes", agentAttributesJSONWriter{
|
||||||
|
attributes: h.attrs,
|
||||||
|
dest: destError,
|
||||||
|
})
|
||||||
|
w.writerField("userAttributes", userAttributesJSONWriter{
|
||||||
|
attributes: h.attrs,
|
||||||
|
dest: destError,
|
||||||
|
})
|
||||||
|
w.rawField("intrinsics", JSONString("{}"))
|
||||||
|
if h.requestURI != "" {
|
||||||
|
w.stringField("request_uri", h.requestURI)
|
||||||
|
}
|
||||||
|
buf.WriteByte('}')
|
||||||
|
buf.WriteByte(']')
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON is used for testing.
|
||||||
|
func (h *harvestError) MarshalJSON() ([]byte, error) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
h.WriteJSON(buf)
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type harvestError struct {
|
||||||
|
TxnError
|
||||||
|
txnName string
|
||||||
|
requestURI string
|
||||||
|
attrs *Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
type harvestErrors struct {
|
||||||
|
errors []*harvestError
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHarvestErrors(max int) *harvestErrors {
|
||||||
|
return &harvestErrors{
|
||||||
|
errors: make([]*harvestError, 0, max),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func harvestErrorFromTxnError(e *TxnError, txnName string, requestURI string, attrs *Attributes) *harvestError {
|
||||||
|
return &harvestError{
|
||||||
|
TxnError: *e,
|
||||||
|
txnName: txnName,
|
||||||
|
requestURI: requestURI,
|
||||||
|
attrs: attrs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addTxnError(errors *harvestErrors, e *TxnError, txnName string, requestURI string, attrs *Attributes) {
|
||||||
|
he := harvestErrorFromTxnError(e, txnName, requestURI, attrs)
|
||||||
|
errors.errors = append(errors.errors, he)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeTxnErrors merges a transaction's errors into the harvest's errors.
|
||||||
|
func MergeTxnErrors(errors *harvestErrors, errs TxnErrors, txnName string, requestURI string, attrs *Attributes) {
|
||||||
|
for _, e := range errs {
|
||||||
|
if len(errors.errors) == cap(errors.errors) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addTxnError(errors, e, txnName, requestURI, attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (errors *harvestErrors) Data(agentRunID string, harvestStart time.Time) ([]byte, error) {
|
||||||
|
if 0 == len(errors.errors) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
estimate := 1024 * len(errors.errors)
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, estimate))
|
||||||
|
buf.WriteByte('[')
|
||||||
|
jsonx.AppendString(buf, agentRunID)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
buf.WriteByte('[')
|
||||||
|
for i, e := range errors.errors {
|
||||||
|
if i > 0 {
|
||||||
|
buf.WriteByte(',')
|
||||||
|
}
|
||||||
|
e.WriteJSON(buf)
|
||||||
|
}
|
||||||
|
buf.WriteByte(']')
|
||||||
|
buf.WriteByte(']')
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (errors *harvestErrors) MergeIntoHarvest(h *Harvest) {}
|
|
@ -0,0 +1,402 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Unfortunately, the resolution of time.Now() on Windows is coarse: Two
|
||||||
|
// sequential calls to time.Now() may return the same value, and tests
|
||||||
|
// which expect non-zero durations may fail. To avoid adding sleep
|
||||||
|
// statements or mocking time.Now(), those tests are skipped on Windows.
|
||||||
|
doDurationTests = runtime.GOOS != `windows`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validator is used for testing.
|
||||||
|
type Validator interface {
|
||||||
|
Error(...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateStringField(v Validator, fieldName, v1, v2 string) {
|
||||||
|
if v1 != v2 {
|
||||||
|
v.Error(fieldName, v1, v2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type addValidatorField struct {
|
||||||
|
field interface{}
|
||||||
|
original Validator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a addValidatorField) Error(fields ...interface{}) {
|
||||||
|
fields = append([]interface{}{a.field}, fields...)
|
||||||
|
a.original.Error(fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtendValidator is used to add more context to a validator.
|
||||||
|
func ExtendValidator(v Validator, field interface{}) Validator {
|
||||||
|
return addValidatorField{
|
||||||
|
field: field,
|
||||||
|
original: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WantMetric is a metric expectation. If Data is nil, then any data values are
|
||||||
|
// acceptable.
|
||||||
|
type WantMetric struct {
|
||||||
|
Name string
|
||||||
|
Scope string
|
||||||
|
Forced interface{} // true, false, or nil
|
||||||
|
Data []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// WantCustomEvent is a custom event expectation.
|
||||||
|
type WantCustomEvent struct {
|
||||||
|
Type string
|
||||||
|
Params map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WantError is a traced error expectation.
|
||||||
|
type WantError struct {
|
||||||
|
TxnName string
|
||||||
|
Msg string
|
||||||
|
Klass string
|
||||||
|
Caller string
|
||||||
|
URL string
|
||||||
|
UserAttributes map[string]interface{}
|
||||||
|
AgentAttributes map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WantErrorEvent is an error event expectation.
|
||||||
|
type WantErrorEvent struct {
|
||||||
|
TxnName string
|
||||||
|
Msg string
|
||||||
|
Klass string
|
||||||
|
Queuing bool
|
||||||
|
ExternalCallCount uint64
|
||||||
|
DatastoreCallCount uint64
|
||||||
|
UserAttributes map[string]interface{}
|
||||||
|
AgentAttributes map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WantTxnEvent is a transaction event expectation.
|
||||||
|
type WantTxnEvent struct {
|
||||||
|
Name string
|
||||||
|
Zone string
|
||||||
|
Queuing bool
|
||||||
|
ExternalCallCount uint64
|
||||||
|
DatastoreCallCount uint64
|
||||||
|
UserAttributes map[string]interface{}
|
||||||
|
AgentAttributes map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WantTxnTrace is a transaction trace expectation.
|
||||||
|
type WantTxnTrace struct {
|
||||||
|
MetricName string
|
||||||
|
CleanURL string
|
||||||
|
NumSegments int
|
||||||
|
UserAttributes map[string]interface{}
|
||||||
|
AgentAttributes map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WantSlowQuery is a slowQuery expectation.
|
||||||
|
type WantSlowQuery struct {
|
||||||
|
Count int32
|
||||||
|
MetricName string
|
||||||
|
Query string
|
||||||
|
TxnName string
|
||||||
|
TxnURL string
|
||||||
|
DatabaseName string
|
||||||
|
Host string
|
||||||
|
PortPathOrID string
|
||||||
|
Params map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect exposes methods that allow for testing whether the correct data was
|
||||||
|
// captured.
|
||||||
|
type Expect interface {
|
||||||
|
ExpectCustomEvents(t Validator, want []WantCustomEvent)
|
||||||
|
ExpectErrors(t Validator, want []WantError)
|
||||||
|
ExpectErrorEvents(t Validator, want []WantErrorEvent)
|
||||||
|
ExpectTxnEvents(t Validator, want []WantTxnEvent)
|
||||||
|
ExpectMetrics(t Validator, want []WantMetric)
|
||||||
|
ExpectTxnTraces(t Validator, want []WantTxnTrace)
|
||||||
|
ExpectSlowQueries(t Validator, want []WantSlowQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectMetricField(t Validator, id metricID, v1, v2 float64, fieldName string) {
|
||||||
|
if v1 != v2 {
|
||||||
|
t.Error("metric fields do not match", id, v1, v2, fieldName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectMetrics allows testing of metrics.
|
||||||
|
func ExpectMetrics(t Validator, mt *metricTable, expect []WantMetric) {
|
||||||
|
if len(mt.metrics) != len(expect) {
|
||||||
|
t.Error("metric counts do not match expectations", len(mt.metrics), len(expect))
|
||||||
|
}
|
||||||
|
expectedIds := make(map[metricID]struct{})
|
||||||
|
for _, e := range expect {
|
||||||
|
id := metricID{Name: e.Name, Scope: e.Scope}
|
||||||
|
expectedIds[id] = struct{}{}
|
||||||
|
m := mt.metrics[id]
|
||||||
|
if nil == m {
|
||||||
|
t.Error("unable to find metric", id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if b, ok := e.Forced.(bool); ok {
|
||||||
|
if b != (forced == m.forced) {
|
||||||
|
t.Error("metric forced incorrect", b, m.forced, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nil != e.Data {
|
||||||
|
expectMetricField(t, id, e.Data[0], m.data.countSatisfied, "countSatisfied")
|
||||||
|
expectMetricField(t, id, e.Data[1], m.data.totalTolerated, "totalTolerated")
|
||||||
|
expectMetricField(t, id, e.Data[2], m.data.exclusiveFailed, "exclusiveFailed")
|
||||||
|
expectMetricField(t, id, e.Data[3], m.data.min, "min")
|
||||||
|
expectMetricField(t, id, e.Data[4], m.data.max, "max")
|
||||||
|
expectMetricField(t, id, e.Data[5], m.data.sumSquares, "sumSquares")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id := range mt.metrics {
|
||||||
|
if _, ok := expectedIds[id]; !ok {
|
||||||
|
t.Error("expected metrics does not contain", id.Name, id.Scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectAttributes(v Validator, exists map[string]interface{}, expect map[string]interface{}) {
|
||||||
|
// TODO: This params comparison can be made smarter: Alert differences
|
||||||
|
// based on sub/super set behavior.
|
||||||
|
if len(exists) != len(expect) {
|
||||||
|
v.Error("attributes length difference", exists, expect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for key, val := range expect {
|
||||||
|
found, ok := exists[key]
|
||||||
|
if !ok {
|
||||||
|
v.Error("missing key", key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v1 := fmt.Sprint(found)
|
||||||
|
v2 := fmt.Sprint(val)
|
||||||
|
if v1 != v2 {
|
||||||
|
v.Error("value difference", fmt.Sprintf("key=%s", key),
|
||||||
|
v1, v2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectCustomEvent(v Validator, event *CustomEvent, expect WantCustomEvent) {
|
||||||
|
if event.eventType != expect.Type {
|
||||||
|
v.Error("type mismatch", event.eventType, expect.Type)
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
diff := absTimeDiff(now, event.timestamp)
|
||||||
|
if diff > time.Hour {
|
||||||
|
v.Error("large timestamp difference", event.eventType, now, event.timestamp)
|
||||||
|
}
|
||||||
|
expectAttributes(v, event.truncatedParams, expect.Params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectCustomEvents allows testing of custom events.
|
||||||
|
func ExpectCustomEvents(v Validator, cs *customEvents, expect []WantCustomEvent) {
|
||||||
|
if len(cs.events.events) != len(expect) {
|
||||||
|
v.Error("number of custom events does not match", len(cs.events.events),
|
||||||
|
len(expect))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, e := range expect {
|
||||||
|
event, ok := cs.events.events[i].jsonWriter.(*CustomEvent)
|
||||||
|
if !ok {
|
||||||
|
v.Error("wrong custom event")
|
||||||
|
} else {
|
||||||
|
expectCustomEvent(v, event, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectErrorEvent(v Validator, err *ErrorEvent, expect WantErrorEvent) {
|
||||||
|
validateStringField(v, "txnName", expect.TxnName, err.TxnName)
|
||||||
|
validateStringField(v, "klass", expect.Klass, err.Klass)
|
||||||
|
validateStringField(v, "msg", expect.Msg, err.Msg)
|
||||||
|
if (0 != err.Queuing) != expect.Queuing {
|
||||||
|
v.Error("queuing", err.Queuing)
|
||||||
|
}
|
||||||
|
if nil != expect.UserAttributes {
|
||||||
|
expectAttributes(v, getUserAttributes(err.Attrs, destError), expect.UserAttributes)
|
||||||
|
}
|
||||||
|
if nil != expect.AgentAttributes {
|
||||||
|
expectAttributes(v, getAgentAttributes(err.Attrs, destError), expect.AgentAttributes)
|
||||||
|
}
|
||||||
|
if expect.ExternalCallCount != err.externalCallCount {
|
||||||
|
v.Error("external call count", expect.ExternalCallCount, err.externalCallCount)
|
||||||
|
}
|
||||||
|
if doDurationTests && (0 == expect.ExternalCallCount) != (err.externalDuration == 0) {
|
||||||
|
v.Error("external duration", err.externalDuration)
|
||||||
|
}
|
||||||
|
if expect.DatastoreCallCount != err.datastoreCallCount {
|
||||||
|
v.Error("datastore call count", expect.DatastoreCallCount, err.datastoreCallCount)
|
||||||
|
}
|
||||||
|
if doDurationTests && (0 == expect.DatastoreCallCount) != (err.datastoreDuration == 0) {
|
||||||
|
v.Error("datastore duration", err.datastoreDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectErrorEvents allows testing of error events.
|
||||||
|
func ExpectErrorEvents(v Validator, events *errorEvents, expect []WantErrorEvent) {
|
||||||
|
if len(events.events.events) != len(expect) {
|
||||||
|
v.Error("number of custom events does not match",
|
||||||
|
len(events.events.events), len(expect))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, e := range expect {
|
||||||
|
event, ok := events.events.events[i].jsonWriter.(*ErrorEvent)
|
||||||
|
if !ok {
|
||||||
|
v.Error("wrong error event")
|
||||||
|
} else {
|
||||||
|
expectErrorEvent(v, event, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectTxnEvent(v Validator, e *TxnEvent, expect WantTxnEvent) {
|
||||||
|
validateStringField(v, "apdex zone", expect.Zone, e.Zone.label())
|
||||||
|
validateStringField(v, "name", expect.Name, e.Name)
|
||||||
|
if doDurationTests && 0 == e.Duration {
|
||||||
|
v.Error("zero duration", e.Duration)
|
||||||
|
}
|
||||||
|
if (0 != e.Queuing) != expect.Queuing {
|
||||||
|
v.Error("queuing", e.Queuing)
|
||||||
|
}
|
||||||
|
if nil != expect.UserAttributes {
|
||||||
|
expectAttributes(v, getUserAttributes(e.Attrs, destTxnEvent), expect.UserAttributes)
|
||||||
|
}
|
||||||
|
if nil != expect.AgentAttributes {
|
||||||
|
expectAttributes(v, getAgentAttributes(e.Attrs, destTxnEvent), expect.AgentAttributes)
|
||||||
|
}
|
||||||
|
if expect.ExternalCallCount != e.externalCallCount {
|
||||||
|
v.Error("external call count", expect.ExternalCallCount, e.externalCallCount)
|
||||||
|
}
|
||||||
|
if doDurationTests && (0 == expect.ExternalCallCount) != (e.externalDuration == 0) {
|
||||||
|
v.Error("external duration", e.externalDuration)
|
||||||
|
}
|
||||||
|
if expect.DatastoreCallCount != e.datastoreCallCount {
|
||||||
|
v.Error("datastore call count", expect.DatastoreCallCount, e.datastoreCallCount)
|
||||||
|
}
|
||||||
|
if doDurationTests && (0 == expect.DatastoreCallCount) != (e.datastoreDuration == 0) {
|
||||||
|
v.Error("datastore duration", e.datastoreDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectTxnEvents allows testing of txn events.
|
||||||
|
func ExpectTxnEvents(v Validator, events *txnEvents, expect []WantTxnEvent) {
|
||||||
|
if len(events.events.events) != len(expect) {
|
||||||
|
v.Error("number of txn events does not match",
|
||||||
|
len(events.events.events), len(expect))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, e := range expect {
|
||||||
|
event, ok := events.events.events[i].jsonWriter.(*TxnEvent)
|
||||||
|
if !ok {
|
||||||
|
v.Error("wrong txn event")
|
||||||
|
} else {
|
||||||
|
expectTxnEvent(v, event, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectError(v Validator, err *harvestError, expect WantError) {
|
||||||
|
caller := topCallerNameBase(err.TxnError.Stack)
|
||||||
|
validateStringField(v, "caller", expect.Caller, caller)
|
||||||
|
validateStringField(v, "txnName", expect.TxnName, err.txnName)
|
||||||
|
validateStringField(v, "klass", expect.Klass, err.TxnError.Klass)
|
||||||
|
validateStringField(v, "msg", expect.Msg, err.TxnError.Msg)
|
||||||
|
validateStringField(v, "URL", expect.URL, err.requestURI)
|
||||||
|
if nil != expect.UserAttributes {
|
||||||
|
expectAttributes(v, getUserAttributes(err.attrs, destError), expect.UserAttributes)
|
||||||
|
}
|
||||||
|
if nil != expect.AgentAttributes {
|
||||||
|
expectAttributes(v, getAgentAttributes(err.attrs, destError), expect.AgentAttributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectErrors allows testing of errors.
|
||||||
|
func ExpectErrors(v Validator, errors *harvestErrors, expect []WantError) {
|
||||||
|
if len(errors.errors) != len(expect) {
|
||||||
|
v.Error("number of errors mismatch", len(errors.errors), len(expect))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, e := range expect {
|
||||||
|
expectError(v, errors.errors[i], e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectTxnTrace(v Validator, trace *HarvestTrace, expect WantTxnTrace) {
|
||||||
|
if doDurationTests && 0 == trace.Duration {
|
||||||
|
v.Error("zero trace duration")
|
||||||
|
}
|
||||||
|
validateStringField(v, "metric name", expect.MetricName, trace.MetricName)
|
||||||
|
validateStringField(v, "request url", expect.CleanURL, trace.CleanURL)
|
||||||
|
if nil != expect.UserAttributes {
|
||||||
|
expectAttributes(v, getUserAttributes(trace.Attrs, destTxnTrace), expect.UserAttributes)
|
||||||
|
}
|
||||||
|
if nil != expect.AgentAttributes {
|
||||||
|
expectAttributes(v, getAgentAttributes(trace.Attrs, destTxnTrace), expect.AgentAttributes)
|
||||||
|
}
|
||||||
|
if expect.NumSegments != len(trace.Trace.nodes) {
|
||||||
|
v.Error("wrong number of segments", expect.NumSegments, len(trace.Trace.nodes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectTxnTraces allows testing of transaction traces.
|
||||||
|
func ExpectTxnTraces(v Validator, traces *harvestTraces, want []WantTxnTrace) {
|
||||||
|
if len(want) == 0 {
|
||||||
|
if nil != traces.trace {
|
||||||
|
v.Error("trace exists when not expected")
|
||||||
|
}
|
||||||
|
} else if len(want) > 1 {
|
||||||
|
v.Error("too many traces expected")
|
||||||
|
} else {
|
||||||
|
if nil == traces.trace {
|
||||||
|
v.Error("missing expected trace")
|
||||||
|
} else {
|
||||||
|
expectTxnTrace(v, traces.trace, want[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectSlowQuery(t Validator, slowQuery *slowQuery, want WantSlowQuery) {
|
||||||
|
if slowQuery.Count != want.Count {
|
||||||
|
t.Error("wrong Count field", slowQuery.Count, want.Count)
|
||||||
|
}
|
||||||
|
validateStringField(t, "MetricName", slowQuery.DatastoreMetric, want.MetricName)
|
||||||
|
validateStringField(t, "Query", slowQuery.ParameterizedQuery, want.Query)
|
||||||
|
validateStringField(t, "TxnName", slowQuery.TxnName, want.TxnName)
|
||||||
|
validateStringField(t, "TxnURL", slowQuery.TxnURL, want.TxnURL)
|
||||||
|
validateStringField(t, "DatabaseName", slowQuery.DatabaseName, want.DatabaseName)
|
||||||
|
validateStringField(t, "Host", slowQuery.Host, want.Host)
|
||||||
|
validateStringField(t, "PortPathOrID", slowQuery.PortPathOrID, want.PortPathOrID)
|
||||||
|
expectAttributes(t, map[string]interface{}(slowQuery.QueryParameters), want.Params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectSlowQueries allows testing of slow queries.
|
||||||
|
func ExpectSlowQueries(t Validator, slowQueries *slowQueries, want []WantSlowQuery) {
|
||||||
|
if len(want) != len(slowQueries.priorityQueue) {
|
||||||
|
t.Error("wrong number of slow queries",
|
||||||
|
"expected", len(want), "got", len(slowQueries.priorityQueue))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, s := range want {
|
||||||
|
idx, ok := slowQueries.lookup[s.Query]
|
||||||
|
if !ok {
|
||||||
|
t.Error("unable to find slow query", s.Query)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
expectSlowQuery(t, slowQueries.priorityQueue[idx], s)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Harvestable is something that can be merged into a Harvest.
|
||||||
|
type Harvestable interface {
|
||||||
|
MergeIntoHarvest(h *Harvest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Harvest contains collected data.
|
||||||
|
type Harvest struct {
|
||||||
|
Metrics *metricTable
|
||||||
|
CustomEvents *customEvents
|
||||||
|
TxnEvents *txnEvents
|
||||||
|
ErrorEvents *errorEvents
|
||||||
|
ErrorTraces *harvestErrors
|
||||||
|
TxnTraces *harvestTraces
|
||||||
|
SlowSQLs *slowQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payloads returns a map from expected collector method name to data type.
|
||||||
|
func (h *Harvest) Payloads() map[string]PayloadCreator {
|
||||||
|
return map[string]PayloadCreator{
|
||||||
|
cmdMetrics: h.Metrics,
|
||||||
|
cmdCustomEvents: h.CustomEvents,
|
||||||
|
cmdTxnEvents: h.TxnEvents,
|
||||||
|
cmdErrorEvents: h.ErrorEvents,
|
||||||
|
cmdErrorData: h.ErrorTraces,
|
||||||
|
cmdTxnTraces: h.TxnTraces,
|
||||||
|
cmdSlowSQLs: h.SlowSQLs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHarvest returns a new Harvest.
|
||||||
|
func NewHarvest(now time.Time) *Harvest {
|
||||||
|
return &Harvest{
|
||||||
|
Metrics: newMetricTable(maxMetrics, now),
|
||||||
|
CustomEvents: newCustomEvents(maxCustomEvents),
|
||||||
|
TxnEvents: newTxnEvents(maxTxnEvents),
|
||||||
|
ErrorEvents: newErrorEvents(maxErrorEvents),
|
||||||
|
ErrorTraces: newHarvestErrors(maxHarvestErrors),
|
||||||
|
TxnTraces: newHarvestTraces(),
|
||||||
|
SlowSQLs: newSlowQueries(maxHarvestSlowSQLs),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
trackMutex sync.Mutex
|
||||||
|
trackMetrics []string
|
||||||
|
)
|
||||||
|
|
||||||
|
// TrackUsage helps track which integration packages are used.
|
||||||
|
func TrackUsage(s ...string) {
|
||||||
|
trackMutex.Lock()
|
||||||
|
defer trackMutex.Unlock()
|
||||||
|
|
||||||
|
m := "Supportability/" + strings.Join(s, "/")
|
||||||
|
trackMetrics = append(trackMetrics, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTrackUsageMetrics(metrics *metricTable) {
|
||||||
|
trackMutex.Lock()
|
||||||
|
defer trackMutex.Unlock()
|
||||||
|
|
||||||
|
for _, m := range trackMetrics {
|
||||||
|
metrics.addSingleCount(m, forced)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateFinalMetrics creates extra metrics at harvest time.
|
||||||
|
func (h *Harvest) CreateFinalMetrics() {
|
||||||
|
h.Metrics.addSingleCount(instanceReporting, forced)
|
||||||
|
|
||||||
|
h.Metrics.addCount(customEventsSeen, h.CustomEvents.numSeen(), forced)
|
||||||
|
h.Metrics.addCount(customEventsSent, h.CustomEvents.numSaved(), forced)
|
||||||
|
|
||||||
|
h.Metrics.addCount(txnEventsSeen, h.TxnEvents.numSeen(), forced)
|
||||||
|
h.Metrics.addCount(txnEventsSent, h.TxnEvents.numSaved(), forced)
|
||||||
|
|
||||||
|
h.Metrics.addCount(errorEventsSeen, h.ErrorEvents.numSeen(), forced)
|
||||||
|
h.Metrics.addCount(errorEventsSent, h.ErrorEvents.numSaved(), forced)
|
||||||
|
|
||||||
|
if h.Metrics.numDropped > 0 {
|
||||||
|
h.Metrics.addCount(supportabilityDropped, float64(h.Metrics.numDropped), forced)
|
||||||
|
}
|
||||||
|
|
||||||
|
createTrackUsageMetrics(h.Metrics)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PayloadCreator is a data type in the harvest.
|
||||||
|
type PayloadCreator interface {
|
||||||
|
// In the event of a rpm request failure (hopefully simply an
|
||||||
|
// intermittent collector issue) the payload may be merged into the next
|
||||||
|
// time period's harvest.
|
||||||
|
Harvestable
|
||||||
|
// Data prepares JSON in the format expected by the collector endpoint.
|
||||||
|
// This method should return (nil, nil) if the payload is empty and no
|
||||||
|
// rpm request is necessary.
|
||||||
|
Data(agentRunID string, harvestStart time.Time) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTxnMetricsArgs contains the parameters to CreateTxnMetrics.
|
||||||
|
type CreateTxnMetricsArgs struct {
|
||||||
|
IsWeb bool
|
||||||
|
Duration time.Duration
|
||||||
|
Exclusive time.Duration
|
||||||
|
Name string
|
||||||
|
Zone ApdexZone
|
||||||
|
ApdexThreshold time.Duration
|
||||||
|
HasErrors bool
|
||||||
|
Queueing time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTxnMetrics creates metrics for a transaction.
|
||||||
|
func CreateTxnMetrics(args CreateTxnMetricsArgs, metrics *metricTable) {
|
||||||
|
// Duration Metrics
|
||||||
|
rollup := backgroundRollup
|
||||||
|
if args.IsWeb {
|
||||||
|
rollup = webRollup
|
||||||
|
metrics.addDuration(dispatcherMetric, "", args.Duration, 0, forced)
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics.addDuration(args.Name, "", args.Duration, args.Exclusive, forced)
|
||||||
|
metrics.addDuration(rollup, "", args.Duration, args.Exclusive, forced)
|
||||||
|
|
||||||
|
// Apdex Metrics
|
||||||
|
if args.Zone != ApdexNone {
|
||||||
|
metrics.addApdex(apdexRollup, "", args.ApdexThreshold, args.Zone, forced)
|
||||||
|
|
||||||
|
mname := apdexPrefix + removeFirstSegment(args.Name)
|
||||||
|
metrics.addApdex(mname, "", args.ApdexThreshold, args.Zone, unforced)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error Metrics
|
||||||
|
if args.HasErrors {
|
||||||
|
metrics.addSingleCount(errorsAll, forced)
|
||||||
|
if args.IsWeb {
|
||||||
|
metrics.addSingleCount(errorsWeb, forced)
|
||||||
|
} else {
|
||||||
|
metrics.addSingleCount(errorsBackground, forced)
|
||||||
|
}
|
||||||
|
metrics.addSingleCount(errorsPrefix+args.Name, forced)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queueing Metrics
|
||||||
|
if args.Queueing > 0 {
|
||||||
|
metrics.addDuration(queueMetric, "", args.Queueing, args.Queueing, forced)
|
||||||
|
}
|
||||||
|
}
|
52
vendor/github.com/newrelic/go-agent/internal/json_object_writer.go
generated
vendored
Normal file
52
vendor/github.com/newrelic/go-agent/internal/json_object_writer.go
generated
vendored
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
"github.com/newrelic/go-agent/internal/jsonx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type jsonWriter interface {
|
||||||
|
WriteJSON(buf *bytes.Buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonFieldsWriter struct {
|
||||||
|
buf *bytes.Buffer
|
||||||
|
needsComma bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *jsonFieldsWriter) addKey(key string) {
|
||||||
|
if w.needsComma {
|
||||||
|
w.buf.WriteByte(',')
|
||||||
|
} else {
|
||||||
|
w.needsComma = true
|
||||||
|
}
|
||||||
|
// defensively assume that the key needs escaping:
|
||||||
|
jsonx.AppendString(w.buf, key)
|
||||||
|
w.buf.WriteByte(':')
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *jsonFieldsWriter) stringField(key string, val string) {
|
||||||
|
w.addKey(key)
|
||||||
|
jsonx.AppendString(w.buf, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *jsonFieldsWriter) intField(key string, val int64) {
|
||||||
|
w.addKey(key)
|
||||||
|
jsonx.AppendInt(w.buf, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *jsonFieldsWriter) floatField(key string, val float64) {
|
||||||
|
w.addKey(key)
|
||||||
|
jsonx.AppendFloat(w.buf, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *jsonFieldsWriter) rawField(key string, val JSONString) {
|
||||||
|
w.addKey(key)
|
||||||
|
w.buf.WriteString(string(val))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *jsonFieldsWriter) writerField(key string, val jsonWriter) {
|
||||||
|
w.addKey(key)
|
||||||
|
val.WriteJSON(w.buf)
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
// Copyright 2010 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package jsonx extends the encoding/json package to encode JSON
|
||||||
|
// incrementally and without requiring reflection.
|
||||||
|
package jsonx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
var hex = "0123456789abcdef"
|
||||||
|
|
||||||
|
// AppendString escapes s appends it to buf.
|
||||||
|
func AppendString(buf *bytes.Buffer, s string) {
|
||||||
|
buf.WriteByte('"')
|
||||||
|
start := 0
|
||||||
|
for i := 0; i < len(s); {
|
||||||
|
if b := s[i]; b < utf8.RuneSelf {
|
||||||
|
if 0x20 <= b && b != '\\' && b != '"' && b != '<' && b != '>' && b != '&' {
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if start < i {
|
||||||
|
buf.WriteString(s[start:i])
|
||||||
|
}
|
||||||
|
switch b {
|
||||||
|
case '\\', '"':
|
||||||
|
buf.WriteByte('\\')
|
||||||
|
buf.WriteByte(b)
|
||||||
|
case '\n':
|
||||||
|
buf.WriteByte('\\')
|
||||||
|
buf.WriteByte('n')
|
||||||
|
case '\r':
|
||||||
|
buf.WriteByte('\\')
|
||||||
|
buf.WriteByte('r')
|
||||||
|
case '\t':
|
||||||
|
buf.WriteByte('\\')
|
||||||
|
buf.WriteByte('t')
|
||||||
|
default:
|
||||||
|
// This encodes bytes < 0x20 except for \n and \r,
|
||||||
|
// as well as <, > and &. The latter are escaped because they
|
||||||
|
// can lead to security holes when user-controlled strings
|
||||||
|
// are rendered into JSON and served to some browsers.
|
||||||
|
buf.WriteString(`\u00`)
|
||||||
|
buf.WriteByte(hex[b>>4])
|
||||||
|
buf.WriteByte(hex[b&0xF])
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
start = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c, size := utf8.DecodeRuneInString(s[i:])
|
||||||
|
if c == utf8.RuneError && size == 1 {
|
||||||
|
if start < i {
|
||||||
|
buf.WriteString(s[start:i])
|
||||||
|
}
|
||||||
|
buf.WriteString(`\ufffd`)
|
||||||
|
i += size
|
||||||
|
start = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// U+2028 is LINE SEPARATOR.
|
||||||
|
// U+2029 is PARAGRAPH SEPARATOR.
|
||||||
|
// They are both technically valid characters in JSON strings,
|
||||||
|
// but don't work in JSONP, which has to be evaluated as JavaScript,
|
||||||
|
// and can lead to security holes there. It is valid JSON to
|
||||||
|
// escape them, so we do so unconditionally.
|
||||||
|
// See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion.
|
||||||
|
if c == '\u2028' || c == '\u2029' {
|
||||||
|
if start < i {
|
||||||
|
buf.WriteString(s[start:i])
|
||||||
|
}
|
||||||
|
buf.WriteString(`\u202`)
|
||||||
|
buf.WriteByte(hex[c&0xF])
|
||||||
|
i += size
|
||||||
|
start = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i += size
|
||||||
|
}
|
||||||
|
if start < len(s) {
|
||||||
|
buf.WriteString(s[start:])
|
||||||
|
}
|
||||||
|
buf.WriteByte('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendStringArray appends an array of string literals to buf.
|
||||||
|
func AppendStringArray(buf *bytes.Buffer, a ...string) {
|
||||||
|
buf.WriteByte('[')
|
||||||
|
for i, s := range a {
|
||||||
|
if i > 0 {
|
||||||
|
buf.WriteByte(',')
|
||||||
|
}
|
||||||
|
AppendString(buf, s)
|
||||||
|
}
|
||||||
|
buf.WriteByte(']')
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendFloat appends a numeric literal representing the value to buf.
|
||||||
|
func AppendFloat(buf *bytes.Buffer, x float64) error {
|
||||||
|
var scratch [64]byte
|
||||||
|
|
||||||
|
if math.IsInf(x, 0) || math.IsNaN(x) {
|
||||||
|
return &json.UnsupportedValueError{
|
||||||
|
Value: reflect.ValueOf(x),
|
||||||
|
Str: strconv.FormatFloat(x, 'g', -1, 64),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.Write(strconv.AppendFloat(scratch[:0], x, 'g', -1, 64))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendFloatArray appends an array of numeric literals to buf.
|
||||||
|
func AppendFloatArray(buf *bytes.Buffer, a ...float64) error {
|
||||||
|
buf.WriteByte('[')
|
||||||
|
for i, x := range a {
|
||||||
|
if i > 0 {
|
||||||
|
buf.WriteByte(',')
|
||||||
|
}
|
||||||
|
if err := AppendFloat(buf, x); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.WriteByte(']')
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendInt appends a numeric literal representing the value to buf.
|
||||||
|
func AppendInt(buf *bytes.Buffer, x int64) {
|
||||||
|
var scratch [64]byte
|
||||||
|
buf.Write(strconv.AppendInt(scratch[:0], x, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendIntArray appends an array of numeric literals to buf.
|
||||||
|
func AppendIntArray(buf *bytes.Buffer, a ...int64) {
|
||||||
|
var scratch [64]byte
|
||||||
|
|
||||||
|
buf.WriteByte('[')
|
||||||
|
for i, x := range a {
|
||||||
|
if i > 0 {
|
||||||
|
buf.WriteByte(',')
|
||||||
|
}
|
||||||
|
buf.Write(strconv.AppendInt(scratch[:0], x, 10))
|
||||||
|
}
|
||||||
|
buf.WriteByte(']')
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendUint appends a numeric literal representing the value to buf.
|
||||||
|
func AppendUint(buf *bytes.Buffer, x uint64) {
|
||||||
|
var scratch [64]byte
|
||||||
|
buf.Write(strconv.AppendUint(scratch[:0], x, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendUintArray appends an array of numeric literals to buf.
|
||||||
|
func AppendUintArray(buf *bytes.Buffer, a ...uint64) {
|
||||||
|
var scratch [64]byte
|
||||||
|
|
||||||
|
buf.WriteByte('[')
|
||||||
|
for i, x := range a {
|
||||||
|
if i > 0 {
|
||||||
|
buf.WriteByte(',')
|
||||||
|
}
|
||||||
|
buf.Write(strconv.AppendUint(scratch[:0], x, 10))
|
||||||
|
}
|
||||||
|
buf.WriteByte(']')
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
// Labels is used for connect JSON formatting.
|
||||||
|
type Labels map[string]string
|
||||||
|
|
||||||
|
// MarshalJSON requires a comment for golint?
|
||||||
|
func (l Labels) MarshalJSON() ([]byte, error) {
|
||||||
|
ls := make([]struct {
|
||||||
|
Key string `json:"label_type"`
|
||||||
|
Value string `json:"label_value"`
|
||||||
|
}, len(l))
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for key, val := range l {
|
||||||
|
ls[i].Key = key
|
||||||
|
ls[i].Value = val
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(ls)
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// app behavior
|
||||||
|
|
||||||
|
// ConnectBackoff is the wait time between unsuccessful connect
|
||||||
|
// attempts.
|
||||||
|
ConnectBackoff = 20 * time.Second
|
||||||
|
// HarvestPeriod is the period that collected data is sent to New Relic.
|
||||||
|
HarvestPeriod = 60 * time.Second
|
||||||
|
// CollectorTimeout is the timeout used in the client for communication
|
||||||
|
// with New Relic's servers.
|
||||||
|
CollectorTimeout = 20 * time.Second
|
||||||
|
// AppDataChanSize is the size of the channel that contains data sent
|
||||||
|
// the app processor.
|
||||||
|
AppDataChanSize = 200
|
||||||
|
failedMetricAttemptsLimit = 5
|
||||||
|
failedEventsAttemptsLimit = 10
|
||||||
|
|
||||||
|
// transaction behavior
|
||||||
|
maxStackTraceFrames = 100
|
||||||
|
// MaxTxnErrors is the maximum number of errors captured per
|
||||||
|
// transaction.
|
||||||
|
MaxTxnErrors = 5
|
||||||
|
maxTxnTraceNodes = 256
|
||||||
|
maxTxnSlowQueries = 10
|
||||||
|
|
||||||
|
// harvest data
|
||||||
|
maxMetrics = 2 * 1000
|
||||||
|
maxCustomEvents = 10 * 1000
|
||||||
|
maxTxnEvents = 10 * 1000
|
||||||
|
maxErrorEvents = 100
|
||||||
|
maxHarvestErrors = 20
|
||||||
|
maxHarvestSlowSQLs = 10
|
||||||
|
|
||||||
|
// attributes
|
||||||
|
attributeKeyLengthLimit = 255
|
||||||
|
attributeValueLengthLimit = 255
|
||||||
|
attributeUserLimit = 64
|
||||||
|
attributeAgentLimit = 255 - attributeUserLimit
|
||||||
|
customEventAttributeLimit = 64
|
||||||
|
|
||||||
|
// Limits affecting Config validation are found in the config package.
|
||||||
|
|
||||||
|
// RuntimeSamplerPeriod is the period of the runtime sampler. Runtime
|
||||||
|
// metrics should not depend on the sampler period, but the period must
|
||||||
|
// be the same across instances. For that reason, this value should not
|
||||||
|
// be changed without notifying customers that they must update all
|
||||||
|
// instance simultaneously for valid runtime metrics.
|
||||||
|
RuntimeSamplerPeriod = 60 * time.Second
|
||||||
|
)
|
|
@ -0,0 +1,89 @@
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger matches newrelic.Logger to allow implementations to be passed to
|
||||||
|
// internal packages.
|
||||||
|
type Logger interface {
|
||||||
|
Error(msg string, context map[string]interface{})
|
||||||
|
Warn(msg string, context map[string]interface{})
|
||||||
|
Info(msg string, context map[string]interface{})
|
||||||
|
Debug(msg string, context map[string]interface{})
|
||||||
|
DebugEnabled() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShimLogger implements Logger and does nothing.
|
||||||
|
type ShimLogger struct{}
|
||||||
|
|
||||||
|
// Error allows ShimLogger to implement Logger.
|
||||||
|
func (s ShimLogger) Error(string, map[string]interface{}) {}
|
||||||
|
|
||||||
|
// Warn allows ShimLogger to implement Logger.
|
||||||
|
func (s ShimLogger) Warn(string, map[string]interface{}) {}
|
||||||
|
|
||||||
|
// Info allows ShimLogger to implement Logger.
|
||||||
|
func (s ShimLogger) Info(string, map[string]interface{}) {}
|
||||||
|
|
||||||
|
// Debug allows ShimLogger to implement Logger.
|
||||||
|
func (s ShimLogger) Debug(string, map[string]interface{}) {}
|
||||||
|
|
||||||
|
// DebugEnabled allows ShimLogger to implement Logger.
|
||||||
|
func (s ShimLogger) DebugEnabled() bool { return false }
|
||||||
|
|
||||||
|
type logFile struct {
|
||||||
|
l *log.Logger
|
||||||
|
doDebug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a basic Logger.
|
||||||
|
func New(w io.Writer, doDebug bool) Logger {
|
||||||
|
return &logFile{
|
||||||
|
l: log.New(w, logPid, logFlags),
|
||||||
|
doDebug: doDebug,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logFlags = log.Ldate | log.Ltime | log.Lmicroseconds
|
||||||
|
|
||||||
|
var (
|
||||||
|
logPid = fmt.Sprintf("(%d) ", os.Getpid())
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f *logFile) fire(level, msg string, ctx map[string]interface{}) {
|
||||||
|
js, err := json.Marshal(struct {
|
||||||
|
Level string `json:"level"`
|
||||||
|
Event string `json:"msg"`
|
||||||
|
Context map[string]interface{} `json:"context"`
|
||||||
|
}{
|
||||||
|
level,
|
||||||
|
msg,
|
||||||
|
ctx,
|
||||||
|
})
|
||||||
|
if nil == err {
|
||||||
|
f.l.Printf(string(js))
|
||||||
|
} else {
|
||||||
|
f.l.Printf("unable to marshal log entry: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *logFile) Error(msg string, ctx map[string]interface{}) {
|
||||||
|
f.fire("error", msg, ctx)
|
||||||
|
}
|
||||||
|
func (f *logFile) Warn(msg string, ctx map[string]interface{}) {
|
||||||
|
f.fire("warn", msg, ctx)
|
||||||
|
}
|
||||||
|
func (f *logFile) Info(msg string, ctx map[string]interface{}) {
|
||||||
|
f.fire("info", msg, ctx)
|
||||||
|
}
|
||||||
|
func (f *logFile) Debug(msg string, ctx map[string]interface{}) {
|
||||||
|
if f.doDebug {
|
||||||
|
f.fire("debug", msg, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (f *logFile) DebugEnabled() bool { return f.doDebug }
|
|
@ -0,0 +1,145 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
const (
|
||||||
|
apdexRollup = "Apdex"
|
||||||
|
apdexPrefix = "Apdex/"
|
||||||
|
|
||||||
|
webRollup = "WebTransaction"
|
||||||
|
backgroundRollup = "OtherTransaction/all"
|
||||||
|
|
||||||
|
errorsAll = "Errors/all"
|
||||||
|
errorsWeb = "Errors/allWeb"
|
||||||
|
errorsBackground = "Errors/allOther"
|
||||||
|
errorsPrefix = "Errors/"
|
||||||
|
|
||||||
|
// "HttpDispatcher" metric is used for the overview graph, and
|
||||||
|
// therefore should only be made for web transactions.
|
||||||
|
dispatcherMetric = "HttpDispatcher"
|
||||||
|
|
||||||
|
queueMetric = "WebFrontend/QueueTime"
|
||||||
|
|
||||||
|
webMetricPrefix = "WebTransaction/Go"
|
||||||
|
backgroundMetricPrefix = "OtherTransaction/Go"
|
||||||
|
|
||||||
|
instanceReporting = "Instance/Reporting"
|
||||||
|
|
||||||
|
// https://newrelic.atlassian.net/wiki/display/eng/Custom+Events+in+New+Relic+Agents
|
||||||
|
customEventsSeen = "Supportability/Events/Customer/Seen"
|
||||||
|
customEventsSent = "Supportability/Events/Customer/Sent"
|
||||||
|
|
||||||
|
// https://source.datanerd.us/agents/agent-specs/blob/master/Transaction-Events-PORTED.md
|
||||||
|
txnEventsSeen = "Supportability/AnalyticsEvents/TotalEventsSeen"
|
||||||
|
txnEventsSent = "Supportability/AnalyticsEvents/TotalEventsSent"
|
||||||
|
|
||||||
|
// https://source.datanerd.us/agents/agent-specs/blob/master/Error-Events.md
|
||||||
|
errorEventsSeen = "Supportability/Events/TransactionError/Seen"
|
||||||
|
errorEventsSent = "Supportability/Events/TransactionError/Sent"
|
||||||
|
|
||||||
|
supportabilityDropped = "Supportability/MetricsDropped"
|
||||||
|
|
||||||
|
// source.datanerd.us/agents/agent-specs/blob/master/Datastore-Metrics-PORTED.md
|
||||||
|
datastoreAll = "Datastore/all"
|
||||||
|
datastoreWeb = "Datastore/allWeb"
|
||||||
|
datastoreOther = "Datastore/allOther"
|
||||||
|
|
||||||
|
// source.datanerd.us/agents/agent-specs/blob/master/APIs/external_segment.md
|
||||||
|
// source.datanerd.us/agents/agent-specs/blob/master/APIs/external_cat.md
|
||||||
|
// source.datanerd.us/agents/agent-specs/blob/master/Cross-Application-Tracing-PORTED.md
|
||||||
|
externalAll = "External/all"
|
||||||
|
externalWeb = "External/allWeb"
|
||||||
|
externalOther = "External/allOther"
|
||||||
|
|
||||||
|
// Runtime/System Metrics
|
||||||
|
memoryPhysical = "Memory/Physical"
|
||||||
|
heapObjectsAllocated = "Memory/Heap/AllocatedObjects"
|
||||||
|
cpuUserUtilization = "CPU/User/Utilization"
|
||||||
|
cpuSystemUtilization = "CPU/System/Utilization"
|
||||||
|
cpuUserTime = "CPU/User Time"
|
||||||
|
cpuSystemTime = "CPU/System Time"
|
||||||
|
runGoroutine = "Go/Runtime/Goroutines"
|
||||||
|
gcPauseFraction = "GC/System/Pause Fraction"
|
||||||
|
gcPauses = "GC/System/Pauses"
|
||||||
|
)
|
||||||
|
|
||||||
|
func customSegmentMetric(s string) string {
|
||||||
|
return "Custom/" + s
|
||||||
|
}
|
||||||
|
|
||||||
|
// DatastoreMetricKey contains the fields by which datastore metrics are
|
||||||
|
// aggregated.
|
||||||
|
type DatastoreMetricKey struct {
|
||||||
|
Product string
|
||||||
|
Collection string
|
||||||
|
Operation string
|
||||||
|
Host string
|
||||||
|
PortPathOrID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type externalMetricKey struct {
|
||||||
|
Host string
|
||||||
|
ExternalCrossProcessID string
|
||||||
|
ExternalTransactionName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type datastoreProductMetrics struct {
|
||||||
|
All string // Datastore/{datastore}/all
|
||||||
|
Web string // Datastore/{datastore}/allWeb
|
||||||
|
Other string // Datastore/{datastore}/allOther
|
||||||
|
}
|
||||||
|
|
||||||
|
func datastoreScopedMetric(key DatastoreMetricKey) string {
|
||||||
|
if "" != key.Collection {
|
||||||
|
return datastoreStatementMetric(key)
|
||||||
|
}
|
||||||
|
return datastoreOperationMetric(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func datastoreProductMetric(key DatastoreMetricKey) datastoreProductMetrics {
|
||||||
|
d, ok := datastoreProductMetricsCache[key.Product]
|
||||||
|
if ok {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
return datastoreProductMetrics{
|
||||||
|
All: "Datastore/" + key.Product + "/all",
|
||||||
|
Web: "Datastore/" + key.Product + "/allWeb",
|
||||||
|
Other: "Datastore/" + key.Product + "/allOther",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datastore/operation/{datastore}/{operation}
|
||||||
|
func datastoreOperationMetric(key DatastoreMetricKey) string {
|
||||||
|
return "Datastore/operation/" + key.Product +
|
||||||
|
"/" + key.Operation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datastore/statement/{datastore}/{table}/{operation}
|
||||||
|
func datastoreStatementMetric(key DatastoreMetricKey) string {
|
||||||
|
return "Datastore/statement/" + key.Product +
|
||||||
|
"/" + key.Collection +
|
||||||
|
"/" + key.Operation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datastore/instance/{datastore}/{host}/{port_path_or_id}
|
||||||
|
func datastoreInstanceMetric(key DatastoreMetricKey) string {
|
||||||
|
return "Datastore/instance/" + key.Product +
|
||||||
|
"/" + key.Host +
|
||||||
|
"/" + key.PortPathOrID
|
||||||
|
}
|
||||||
|
|
||||||
|
// External/{host}/all
|
||||||
|
func externalHostMetric(key externalMetricKey) string {
|
||||||
|
return "External/" + key.Host + "/all"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExternalApp/{host}/{external_id}/all
|
||||||
|
func externalAppMetric(key externalMetricKey) string {
|
||||||
|
return "ExternalApp/" + key.Host +
|
||||||
|
"/" + key.ExternalCrossProcessID + "/all"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExternalTransaction/{host}/{external_id}/{external_txnname}
|
||||||
|
func externalTransactionMetric(key externalMetricKey) string {
|
||||||
|
return "ExternalTransaction/" + key.Host +
|
||||||
|
"/" + key.ExternalCrossProcessID +
|
||||||
|
"/" + key.ExternalTransactionName
|
||||||
|
}
|
96
vendor/github.com/newrelic/go-agent/internal/metric_names_datastore.go
generated
vendored
Normal file
96
vendor/github.com/newrelic/go-agent/internal/metric_names_datastore.go
generated
vendored
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
var (
|
||||||
|
datastoreProductMetricsCache = map[string]datastoreProductMetrics{
|
||||||
|
"Cassandra": {
|
||||||
|
All: "Datastore/Cassandra/all",
|
||||||
|
Web: "Datastore/Cassandra/allWeb",
|
||||||
|
Other: "Datastore/Cassandra/allOther",
|
||||||
|
},
|
||||||
|
"Derby": {
|
||||||
|
All: "Datastore/Derby/all",
|
||||||
|
Web: "Datastore/Derby/allWeb",
|
||||||
|
Other: "Datastore/Derby/allOther",
|
||||||
|
},
|
||||||
|
"Elasticsearch": {
|
||||||
|
All: "Datastore/Elasticsearch/all",
|
||||||
|
Web: "Datastore/Elasticsearch/allWeb",
|
||||||
|
Other: "Datastore/Elasticsearch/allOther",
|
||||||
|
},
|
||||||
|
"Firebird": {
|
||||||
|
All: "Datastore/Firebird/all",
|
||||||
|
Web: "Datastore/Firebird/allWeb",
|
||||||
|
Other: "Datastore/Firebird/allOther",
|
||||||
|
},
|
||||||
|
"IBMDB2": {
|
||||||
|
All: "Datastore/IBMDB2/all",
|
||||||
|
Web: "Datastore/IBMDB2/allWeb",
|
||||||
|
Other: "Datastore/IBMDB2/allOther",
|
||||||
|
},
|
||||||
|
"Informix": {
|
||||||
|
All: "Datastore/Informix/all",
|
||||||
|
Web: "Datastore/Informix/allWeb",
|
||||||
|
Other: "Datastore/Informix/allOther",
|
||||||
|
},
|
||||||
|
"Memcached": {
|
||||||
|
All: "Datastore/Memcached/all",
|
||||||
|
Web: "Datastore/Memcached/allWeb",
|
||||||
|
Other: "Datastore/Memcached/allOther",
|
||||||
|
},
|
||||||
|
"MongoDB": {
|
||||||
|
All: "Datastore/MongoDB/all",
|
||||||
|
Web: "Datastore/MongoDB/allWeb",
|
||||||
|
Other: "Datastore/MongoDB/allOther",
|
||||||
|
},
|
||||||
|
"MySQL": {
|
||||||
|
All: "Datastore/MySQL/all",
|
||||||
|
Web: "Datastore/MySQL/allWeb",
|
||||||
|
Other: "Datastore/MySQL/allOther",
|
||||||
|
},
|
||||||
|
"MSSQL": {
|
||||||
|
All: "Datastore/MSSQL/all",
|
||||||
|
Web: "Datastore/MSSQL/allWeb",
|
||||||
|
Other: "Datastore/MSSQL/allOther",
|
||||||
|
},
|
||||||
|
"Oracle": {
|
||||||
|
All: "Datastore/Oracle/all",
|
||||||
|
Web: "Datastore/Oracle/allWeb",
|
||||||
|
Other: "Datastore/Oracle/allOther",
|
||||||
|
},
|
||||||
|
"Postgres": {
|
||||||
|
All: "Datastore/Postgres/all",
|
||||||
|
Web: "Datastore/Postgres/allWeb",
|
||||||
|
Other: "Datastore/Postgres/allOther",
|
||||||
|
},
|
||||||
|
"Redis": {
|
||||||
|
All: "Datastore/Redis/all",
|
||||||
|
Web: "Datastore/Redis/allWeb",
|
||||||
|
Other: "Datastore/Redis/allOther",
|
||||||
|
},
|
||||||
|
"Solr": {
|
||||||
|
All: "Datastore/Solr/all",
|
||||||
|
Web: "Datastore/Solr/allWeb",
|
||||||
|
Other: "Datastore/Solr/allOther",
|
||||||
|
},
|
||||||
|
"SQLite": {
|
||||||
|
All: "Datastore/SQLite/all",
|
||||||
|
Web: "Datastore/SQLite/allWeb",
|
||||||
|
Other: "Datastore/SQLite/allOther",
|
||||||
|
},
|
||||||
|
"CouchDB": {
|
||||||
|
All: "Datastore/CouchDB/all",
|
||||||
|
Web: "Datastore/CouchDB/allWeb",
|
||||||
|
Other: "Datastore/CouchDB/allOther",
|
||||||
|
},
|
||||||
|
"Riak": {
|
||||||
|
All: "Datastore/Riak/all",
|
||||||
|
Web: "Datastore/Riak/allWeb",
|
||||||
|
Other: "Datastore/Riak/allOther",
|
||||||
|
},
|
||||||
|
"VoltDB": {
|
||||||
|
All: "Datastore/VoltDB/all",
|
||||||
|
Web: "Datastore/VoltDB/allWeb",
|
||||||
|
Other: "Datastore/VoltDB/allOther",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
|
@ -0,0 +1,163 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ruleResult int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ruleChanged ruleResult = iota
|
||||||
|
ruleUnchanged
|
||||||
|
ruleIgnore
|
||||||
|
)
|
||||||
|
|
||||||
|
type metricRule struct {
|
||||||
|
// 'Ignore' indicates if the entire transaction should be discarded if
|
||||||
|
// there is a match. This field is only used by "url_rules" and
|
||||||
|
// "transaction_name_rules", not "metric_name_rules".
|
||||||
|
Ignore bool `json:"ignore"`
|
||||||
|
EachSegment bool `json:"each_segment"`
|
||||||
|
ReplaceAll bool `json:"replace_all"`
|
||||||
|
Terminate bool `json:"terminate_chain"`
|
||||||
|
Order int `json:"eval_order"`
|
||||||
|
OriginalReplacement string `json:"replacement"`
|
||||||
|
RawExpr string `json:"match_expression"`
|
||||||
|
|
||||||
|
// Go's regexp backreferences use '${1}' instead of the Perlish '\1', so
|
||||||
|
// we transform the replacement string into the Go syntax and store it
|
||||||
|
// here.
|
||||||
|
TransformedReplacement string
|
||||||
|
re *regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
type metricRules []*metricRule
|
||||||
|
|
||||||
|
// Go's regexp backreferences use `${1}` instead of the Perlish `\1`, so we must
|
||||||
|
// transform the replacement string. This is non-trivial: `\1` is a
|
||||||
|
// backreference but `\\1` is not. Rather than count the number of back slashes
|
||||||
|
// preceding the digit, we simply skip rules with tricky replacements.
|
||||||
|
var (
|
||||||
|
transformReplacementAmbiguous = regexp.MustCompile(`\\\\([0-9]+)`)
|
||||||
|
transformReplacementRegex = regexp.MustCompile(`\\([0-9]+)`)
|
||||||
|
transformReplacementReplacement = "$${${1}}"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (rules *metricRules) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
var raw []*metricRule
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &raw); nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
valid := make(metricRules, 0, len(raw))
|
||||||
|
|
||||||
|
for _, r := range raw {
|
||||||
|
re, err := regexp.Compile("(?i)" + r.RawExpr)
|
||||||
|
if err != nil {
|
||||||
|
// TODO
|
||||||
|
// Warn("unable to compile rule", {
|
||||||
|
// "match_expression": r.RawExpr,
|
||||||
|
// "error": err.Error(),
|
||||||
|
// })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if transformReplacementAmbiguous.MatchString(r.OriginalReplacement) {
|
||||||
|
// TODO
|
||||||
|
// Warn("unable to transform replacement", {
|
||||||
|
// "match_expression": r.RawExpr,
|
||||||
|
// "replacement": r.OriginalReplacement,
|
||||||
|
// })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r.re = re
|
||||||
|
r.TransformedReplacement = transformReplacementRegex.ReplaceAllString(r.OriginalReplacement,
|
||||||
|
transformReplacementReplacement)
|
||||||
|
valid = append(valid, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(valid)
|
||||||
|
|
||||||
|
*rules = valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rules metricRules) Len() int {
|
||||||
|
return len(rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rules should be applied in increasing order
|
||||||
|
func (rules metricRules) Less(i, j int) bool {
|
||||||
|
return rules[i].Order < rules[j].Order
|
||||||
|
}
|
||||||
|
func (rules metricRules) Swap(i, j int) {
|
||||||
|
rules[i], rules[j] = rules[j], rules[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceFirst(re *regexp.Regexp, s string, replacement string) string {
|
||||||
|
// Note that ReplaceAllStringFunc cannot be used here since it does
|
||||||
|
// not replace $1 placeholders.
|
||||||
|
loc := re.FindStringIndex(s)
|
||||||
|
if nil == loc {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
firstMatch := s[loc[0]:loc[1]]
|
||||||
|
firstMatchReplaced := re.ReplaceAllString(firstMatch, replacement)
|
||||||
|
return s[0:loc[0]] + firstMatchReplaced + s[loc[1]:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *metricRule) apply(s string) (ruleResult, string) {
|
||||||
|
// Rules are strange, and there is no spec.
|
||||||
|
// This code attempts to duplicate the logic of the PHP agent.
|
||||||
|
// Ambiguity abounds.
|
||||||
|
|
||||||
|
if r.Ignore {
|
||||||
|
if r.re.MatchString(s) {
|
||||||
|
return ruleIgnore, ""
|
||||||
|
}
|
||||||
|
return ruleUnchanged, s
|
||||||
|
}
|
||||||
|
|
||||||
|
var out string
|
||||||
|
|
||||||
|
if r.ReplaceAll {
|
||||||
|
out = r.re.ReplaceAllString(s, r.TransformedReplacement)
|
||||||
|
} else if r.EachSegment {
|
||||||
|
segments := strings.Split(string(s), "/")
|
||||||
|
applied := make([]string, len(segments))
|
||||||
|
for i, segment := range segments {
|
||||||
|
applied[i] = replaceFirst(r.re, segment, r.TransformedReplacement)
|
||||||
|
}
|
||||||
|
out = strings.Join(applied, "/")
|
||||||
|
} else {
|
||||||
|
out = replaceFirst(r.re, s, r.TransformedReplacement)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out == s {
|
||||||
|
return ruleUnchanged, out
|
||||||
|
}
|
||||||
|
return ruleChanged, out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rules metricRules) Apply(input string) string {
|
||||||
|
var res ruleResult
|
||||||
|
s := input
|
||||||
|
|
||||||
|
for _, rule := range rules {
|
||||||
|
res, s = rule.apply(s)
|
||||||
|
|
||||||
|
if ruleIgnore == res {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if (ruleChanged == res) && rule.Terminate {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
|
@ -0,0 +1,258 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/newrelic/go-agent/internal/jsonx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type metricForce int
|
||||||
|
|
||||||
|
const (
|
||||||
|
forced metricForce = iota
|
||||||
|
unforced
|
||||||
|
)
|
||||||
|
|
||||||
|
type metricID struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Scope string `json:"scope,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type metricData struct {
|
||||||
|
// These values are in the units expected by the collector.
|
||||||
|
countSatisfied float64 // Seconds, or count for Apdex
|
||||||
|
totalTolerated float64 // Seconds, or count for Apdex
|
||||||
|
exclusiveFailed float64 // Seconds, or count for Apdex
|
||||||
|
min float64 // Seconds
|
||||||
|
max float64 // Seconds
|
||||||
|
sumSquares float64 // Seconds**2, or 0 for Apdex
|
||||||
|
}
|
||||||
|
|
||||||
|
func metricDataFromDuration(duration, exclusive time.Duration) metricData {
|
||||||
|
ds := duration.Seconds()
|
||||||
|
return metricData{
|
||||||
|
countSatisfied: 1,
|
||||||
|
totalTolerated: ds,
|
||||||
|
exclusiveFailed: exclusive.Seconds(),
|
||||||
|
min: ds,
|
||||||
|
max: ds,
|
||||||
|
sumSquares: ds * ds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type metric struct {
|
||||||
|
forced metricForce
|
||||||
|
data metricData
|
||||||
|
}
|
||||||
|
|
||||||
|
type metricTable struct {
|
||||||
|
metricPeriodStart time.Time
|
||||||
|
failedHarvests int
|
||||||
|
maxTableSize int // After this max is reached, only forced metrics are added
|
||||||
|
numDropped int // Number of unforced metrics dropped due to full table
|
||||||
|
metrics map[metricID]*metric
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMetricTable(maxTableSize int, now time.Time) *metricTable {
|
||||||
|
return &metricTable{
|
||||||
|
metricPeriodStart: now,
|
||||||
|
metrics: make(map[metricID]*metric),
|
||||||
|
maxTableSize: maxTableSize,
|
||||||
|
failedHarvests: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *metricTable) full() bool {
|
||||||
|
return len(mt.metrics) >= mt.maxTableSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *metricData) aggregate(src metricData) {
|
||||||
|
data.countSatisfied += src.countSatisfied
|
||||||
|
data.totalTolerated += src.totalTolerated
|
||||||
|
data.exclusiveFailed += src.exclusiveFailed
|
||||||
|
|
||||||
|
if src.min < data.min {
|
||||||
|
data.min = src.min
|
||||||
|
}
|
||||||
|
if src.max > data.max {
|
||||||
|
data.max = src.max
|
||||||
|
}
|
||||||
|
|
||||||
|
data.sumSquares += src.sumSquares
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *metricTable) mergeMetric(id metricID, m metric) {
|
||||||
|
if to := mt.metrics[id]; nil != to {
|
||||||
|
to.data.aggregate(m.data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if mt.full() && (unforced == m.forced) {
|
||||||
|
mt.numDropped++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// NOTE: `new` is used in place of `&m` since the latter will make `m`
|
||||||
|
// get heap allocated regardless of whether or not this line gets
|
||||||
|
// reached (running go version go1.5 darwin/amd64). See
|
||||||
|
// BenchmarkAddingSameMetrics.
|
||||||
|
alloc := new(metric)
|
||||||
|
*alloc = m
|
||||||
|
mt.metrics[id] = alloc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *metricTable) mergeFailed(from *metricTable) {
|
||||||
|
fails := from.failedHarvests + 1
|
||||||
|
if fails >= failedMetricAttemptsLimit {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if from.metricPeriodStart.Before(mt.metricPeriodStart) {
|
||||||
|
mt.metricPeriodStart = from.metricPeriodStart
|
||||||
|
}
|
||||||
|
mt.failedHarvests = fails
|
||||||
|
mt.merge(from, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *metricTable) merge(from *metricTable, newScope string) {
|
||||||
|
if "" == newScope {
|
||||||
|
for id, m := range from.metrics {
|
||||||
|
mt.mergeMetric(id, *m)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for id, m := range from.metrics {
|
||||||
|
mt.mergeMetric(metricID{Name: id.Name, Scope: newScope}, *m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *metricTable) add(name, scope string, data metricData, force metricForce) {
|
||||||
|
mt.mergeMetric(metricID{Name: name, Scope: scope}, metric{data: data, forced: force})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *metricTable) addCount(name string, count float64, force metricForce) {
|
||||||
|
mt.add(name, "", metricData{countSatisfied: count}, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *metricTable) addSingleCount(name string, force metricForce) {
|
||||||
|
mt.addCount(name, float64(1), force)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *metricTable) addDuration(name, scope string, duration, exclusive time.Duration, force metricForce) {
|
||||||
|
mt.add(name, scope, metricDataFromDuration(duration, exclusive), force)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *metricTable) addValueExclusive(name, scope string, total, exclusive float64, force metricForce) {
|
||||||
|
data := metricData{
|
||||||
|
countSatisfied: 1,
|
||||||
|
totalTolerated: total,
|
||||||
|
exclusiveFailed: exclusive,
|
||||||
|
min: total,
|
||||||
|
max: total,
|
||||||
|
sumSquares: total * total,
|
||||||
|
}
|
||||||
|
mt.add(name, scope, data, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *metricTable) addValue(name, scope string, total float64, force metricForce) {
|
||||||
|
mt.addValueExclusive(name, scope, total, total, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *metricTable) addApdex(name, scope string, apdexThreshold time.Duration, zone ApdexZone, force metricForce) {
|
||||||
|
apdexSeconds := apdexThreshold.Seconds()
|
||||||
|
data := metricData{min: apdexSeconds, max: apdexSeconds}
|
||||||
|
|
||||||
|
switch zone {
|
||||||
|
case ApdexSatisfying:
|
||||||
|
data.countSatisfied = 1
|
||||||
|
case ApdexTolerating:
|
||||||
|
data.totalTolerated = 1
|
||||||
|
case ApdexFailing:
|
||||||
|
data.exclusiveFailed = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
mt.add(name, scope, data, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *metricTable) CollectorJSON(agentRunID string, now time.Time) ([]byte, error) {
|
||||||
|
if 0 == len(mt.metrics) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
estimatedBytesPerMetric := 128
|
||||||
|
estimatedLen := len(mt.metrics) * estimatedBytesPerMetric
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, estimatedLen))
|
||||||
|
buf.WriteByte('[')
|
||||||
|
|
||||||
|
jsonx.AppendString(buf, agentRunID)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
jsonx.AppendInt(buf, mt.metricPeriodStart.Unix())
|
||||||
|
buf.WriteByte(',')
|
||||||
|
jsonx.AppendInt(buf, now.Unix())
|
||||||
|
buf.WriteByte(',')
|
||||||
|
|
||||||
|
buf.WriteByte('[')
|
||||||
|
first := true
|
||||||
|
for id, metric := range mt.metrics {
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
buf.WriteByte(',')
|
||||||
|
}
|
||||||
|
buf.WriteByte('[')
|
||||||
|
buf.WriteByte('{')
|
||||||
|
buf.WriteString(`"name":`)
|
||||||
|
jsonx.AppendString(buf, id.Name)
|
||||||
|
if id.Scope != "" {
|
||||||
|
buf.WriteString(`,"scope":`)
|
||||||
|
jsonx.AppendString(buf, id.Scope)
|
||||||
|
}
|
||||||
|
buf.WriteByte('}')
|
||||||
|
buf.WriteByte(',')
|
||||||
|
|
||||||
|
jsonx.AppendFloatArray(buf,
|
||||||
|
metric.data.countSatisfied,
|
||||||
|
metric.data.totalTolerated,
|
||||||
|
metric.data.exclusiveFailed,
|
||||||
|
metric.data.min,
|
||||||
|
metric.data.max,
|
||||||
|
metric.data.sumSquares)
|
||||||
|
|
||||||
|
buf.WriteByte(']')
|
||||||
|
}
|
||||||
|
buf.WriteByte(']')
|
||||||
|
|
||||||
|
buf.WriteByte(']')
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *metricTable) Data(agentRunID string, harvestStart time.Time) ([]byte, error) {
|
||||||
|
return mt.CollectorJSON(agentRunID, harvestStart)
|
||||||
|
}
|
||||||
|
func (mt *metricTable) MergeIntoHarvest(h *Harvest) {
|
||||||
|
h.Metrics.mergeFailed(mt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *metricTable) ApplyRules(rules metricRules) *metricTable {
|
||||||
|
if nil == rules {
|
||||||
|
return mt
|
||||||
|
}
|
||||||
|
if len(rules) == 0 {
|
||||||
|
return mt
|
||||||
|
}
|
||||||
|
|
||||||
|
applied := newMetricTable(mt.maxTableSize, mt.metricPeriodStart)
|
||||||
|
cache := make(map[string]string)
|
||||||
|
|
||||||
|
for id, m := range mt.metrics {
|
||||||
|
out, ok := cache[id.Name]
|
||||||
|
if !ok {
|
||||||
|
out = rules.Apply(id.Name)
|
||||||
|
cache[id.Name] = out
|
||||||
|
}
|
||||||
|
|
||||||
|
if "" != out {
|
||||||
|
applied.mergeMetric(metricID{Name: out, Scope: id.Scope}, *m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return applied
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
xRequestStart = "X-Request-Start"
|
||||||
|
xQueueStart = "X-Queue-Start"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
earliestAcceptableSeconds = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC).Unix()
|
||||||
|
latestAcceptableSeconds = time.Date(2050, time.January, 1, 0, 0, 0, 0, time.UTC).Unix()
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkQueueTimeSeconds(secondsFloat float64) time.Time {
|
||||||
|
seconds := int64(secondsFloat)
|
||||||
|
nanos := int64((secondsFloat - float64(seconds)) * (1000.0 * 1000.0 * 1000.0))
|
||||||
|
if seconds > earliestAcceptableSeconds && seconds < latestAcceptableSeconds {
|
||||||
|
return time.Unix(seconds, nanos)
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseQueueTime(s string) time.Time {
|
||||||
|
f, err := strconv.ParseFloat(s, 64)
|
||||||
|
if nil != err {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
if f <= 0 {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// try microseconds
|
||||||
|
if t := checkQueueTimeSeconds(f / (1000.0 * 1000.0)); !t.IsZero() {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
// try milliseconds
|
||||||
|
if t := checkQueueTimeSeconds(f / (1000.0)); !t.IsZero() {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
// try seconds
|
||||||
|
if t := checkQueueTimeSeconds(f); !t.IsZero() {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueueDuration TODO
|
||||||
|
func QueueDuration(hdr http.Header, txnStart time.Time) time.Duration {
|
||||||
|
s := hdr.Get(xQueueStart)
|
||||||
|
if "" == s {
|
||||||
|
s = hdr.Get(xRequestStart)
|
||||||
|
}
|
||||||
|
if "" == s {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
s = strings.TrimPrefix(s, "t=")
|
||||||
|
qt := parseQueueTime(s)
|
||||||
|
if qt.IsZero() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if qt.After(txnStart) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return txnStart.Sub(qt)
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/newrelic/go-agent/internal/logger"
|
||||||
|
"github.com/newrelic/go-agent/internal/sysinfo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sample is a system/runtime snapshot.
|
||||||
|
type Sample struct {
|
||||||
|
when time.Time
|
||||||
|
memStats runtime.MemStats
|
||||||
|
usage sysinfo.Usage
|
||||||
|
numGoroutine int
|
||||||
|
numCPU int
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesToMebibytesFloat(bts uint64) float64 {
|
||||||
|
return float64(bts) / (1024 * 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSample gathers a new Sample.
|
||||||
|
func GetSample(now time.Time, lg logger.Logger) *Sample {
|
||||||
|
s := Sample{
|
||||||
|
when: now,
|
||||||
|
numGoroutine: runtime.NumGoroutine(),
|
||||||
|
numCPU: runtime.NumCPU(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if usage, err := sysinfo.GetUsage(); err == nil {
|
||||||
|
s.usage = usage
|
||||||
|
} else {
|
||||||
|
lg.Warn("unable to usage", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.ReadMemStats(&s.memStats)
|
||||||
|
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
|
type cpuStats struct {
|
||||||
|
used time.Duration
|
||||||
|
fraction float64 // used / (elapsed * numCPU)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats contains system information for a period of time.
|
||||||
|
type Stats struct {
|
||||||
|
numGoroutine int
|
||||||
|
allocBytes uint64
|
||||||
|
heapObjects uint64
|
||||||
|
user cpuStats
|
||||||
|
system cpuStats
|
||||||
|
gcPauseFraction float64
|
||||||
|
deltaNumGC uint32
|
||||||
|
deltaPauseTotal time.Duration
|
||||||
|
minPause time.Duration
|
||||||
|
maxPause time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Samples is used as the parameter to GetStats to avoid mixing up the previous
|
||||||
|
// and current sample.
|
||||||
|
type Samples struct {
|
||||||
|
Previous *Sample
|
||||||
|
Current *Sample
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStats combines two Samples into a Stats.
|
||||||
|
func GetStats(ss Samples) Stats {
|
||||||
|
cur := ss.Current
|
||||||
|
prev := ss.Previous
|
||||||
|
elapsed := cur.when.Sub(prev.when)
|
||||||
|
|
||||||
|
s := Stats{
|
||||||
|
numGoroutine: cur.numGoroutine,
|
||||||
|
allocBytes: cur.memStats.Alloc,
|
||||||
|
heapObjects: cur.memStats.HeapObjects,
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPU Utilization
|
||||||
|
totalCPUSeconds := elapsed.Seconds() * float64(cur.numCPU)
|
||||||
|
if prev.usage.User != 0 && cur.usage.User > prev.usage.User {
|
||||||
|
s.user.used = cur.usage.User - prev.usage.User
|
||||||
|
s.user.fraction = s.user.used.Seconds() / totalCPUSeconds
|
||||||
|
}
|
||||||
|
if prev.usage.System != 0 && cur.usage.System > prev.usage.System {
|
||||||
|
s.system.used = cur.usage.System - prev.usage.System
|
||||||
|
s.system.fraction = s.system.used.Seconds() / totalCPUSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
// GC Pause Fraction
|
||||||
|
deltaPauseTotalNs := cur.memStats.PauseTotalNs - prev.memStats.PauseTotalNs
|
||||||
|
frac := float64(deltaPauseTotalNs) / float64(elapsed.Nanoseconds())
|
||||||
|
s.gcPauseFraction = frac
|
||||||
|
|
||||||
|
// GC Pauses
|
||||||
|
if deltaNumGC := cur.memStats.NumGC - prev.memStats.NumGC; deltaNumGC > 0 {
|
||||||
|
// In case more than 256 pauses have happened between samples
|
||||||
|
// and we are examining a subset of the pauses, we ensure that
|
||||||
|
// the min and max are not on the same side of the average by
|
||||||
|
// using the average as the starting min and max.
|
||||||
|
maxPauseNs := deltaPauseTotalNs / uint64(deltaNumGC)
|
||||||
|
minPauseNs := deltaPauseTotalNs / uint64(deltaNumGC)
|
||||||
|
for i := prev.memStats.NumGC + 1; i <= cur.memStats.NumGC; i++ {
|
||||||
|
pause := cur.memStats.PauseNs[(i+255)%256]
|
||||||
|
if pause > maxPauseNs {
|
||||||
|
maxPauseNs = pause
|
||||||
|
}
|
||||||
|
if pause < minPauseNs {
|
||||||
|
minPauseNs = pause
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.deltaPauseTotal = time.Duration(deltaPauseTotalNs) * time.Nanosecond
|
||||||
|
s.deltaNumGC = deltaNumGC
|
||||||
|
s.minPause = time.Duration(minPauseNs) * time.Nanosecond
|
||||||
|
s.maxPause = time.Duration(maxPauseNs) * time.Nanosecond
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeIntoHarvest implements Harvestable.
|
||||||
|
func (s Stats) MergeIntoHarvest(h *Harvest) {
|
||||||
|
h.Metrics.addValue(heapObjectsAllocated, "", float64(s.heapObjects), forced)
|
||||||
|
h.Metrics.addValue(runGoroutine, "", float64(s.numGoroutine), forced)
|
||||||
|
h.Metrics.addValueExclusive(memoryPhysical, "", bytesToMebibytesFloat(s.allocBytes), 0, forced)
|
||||||
|
h.Metrics.addValueExclusive(cpuUserUtilization, "", s.user.fraction, 0, forced)
|
||||||
|
h.Metrics.addValueExclusive(cpuSystemUtilization, "", s.system.fraction, 0, forced)
|
||||||
|
h.Metrics.addValue(cpuUserTime, "", s.user.used.Seconds(), forced)
|
||||||
|
h.Metrics.addValue(cpuSystemTime, "", s.system.used.Seconds(), forced)
|
||||||
|
h.Metrics.addValueExclusive(gcPauseFraction, "", s.gcPauseFraction, 0, forced)
|
||||||
|
if s.deltaNumGC > 0 {
|
||||||
|
h.Metrics.add(gcPauses, "", metricData{
|
||||||
|
countSatisfied: float64(s.deltaNumGC),
|
||||||
|
totalTolerated: s.deltaPauseTotal.Seconds(),
|
||||||
|
exclusiveFailed: 0,
|
||||||
|
min: s.minPause.Seconds(),
|
||||||
|
max: s.maxPause.Seconds(),
|
||||||
|
sumSquares: s.deltaPauseTotal.Seconds() * s.deltaPauseTotal.Seconds(),
|
||||||
|
}, forced)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
// https://newrelic.atlassian.net/wiki/display/eng/Language+agent+transaction+segment+terms+rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
placeholder = "*"
|
||||||
|
separator = "/"
|
||||||
|
)
|
||||||
|
|
||||||
|
type segmentRule struct {
|
||||||
|
Prefix string `json:"prefix"`
|
||||||
|
Terms []string `json:"terms"`
|
||||||
|
TermsMap map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// segmentRules is keyed by each segmentRule's Prefix field with any trailing
|
||||||
|
// slash removed.
|
||||||
|
type segmentRules map[string]*segmentRule
|
||||||
|
|
||||||
|
func buildTermsMap(terms []string) map[string]struct{} {
|
||||||
|
m := make(map[string]struct{}, len(terms))
|
||||||
|
for _, t := range terms {
|
||||||
|
m[t] = struct{}{}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rules *segmentRules) UnmarshalJSON(b []byte) error {
|
||||||
|
var raw []*segmentRule
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b, &raw); nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rs := make(map[string]*segmentRule)
|
||||||
|
|
||||||
|
for _, rule := range raw {
|
||||||
|
prefix := strings.TrimSuffix(rule.Prefix, "/")
|
||||||
|
if len(strings.Split(prefix, "/")) != 2 {
|
||||||
|
// TODO
|
||||||
|
// Warn("invalid segment term rule prefix",
|
||||||
|
// {"prefix": rule.Prefix})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if nil == rule.Terms {
|
||||||
|
// TODO
|
||||||
|
// Warn("segment term rule has missing terms",
|
||||||
|
// {"prefix": rule.Prefix})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rule.TermsMap = buildTermsMap(rule.Terms)
|
||||||
|
|
||||||
|
rs[prefix] = rule
|
||||||
|
}
|
||||||
|
|
||||||
|
*rules = rs
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rule *segmentRule) apply(name string) string {
|
||||||
|
if !strings.HasPrefix(name, rule.Prefix) {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
s := strings.TrimPrefix(name, rule.Prefix)
|
||||||
|
|
||||||
|
leadingSlash := ""
|
||||||
|
if strings.HasPrefix(s, separator) {
|
||||||
|
leadingSlash = separator
|
||||||
|
s = strings.TrimPrefix(s, separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
if "" != s {
|
||||||
|
segments := strings.Split(s, separator)
|
||||||
|
|
||||||
|
for i, segment := range segments {
|
||||||
|
_, whitelisted := rule.TermsMap[segment]
|
||||||
|
if whitelisted {
|
||||||
|
segments[i] = segment
|
||||||
|
} else {
|
||||||
|
segments[i] = placeholder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
segments = collapsePlaceholders(segments)
|
||||||
|
s = strings.Join(segments, separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rule.Prefix + leadingSlash + s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rules segmentRules) apply(name string) string {
|
||||||
|
if nil == rules {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
rule, ok := rules[firstTwoSegments(name)]
|
||||||
|
if !ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
return rule.apply(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstTwoSegments(name string) string {
|
||||||
|
firstSlashIdx := strings.Index(name, separator)
|
||||||
|
if firstSlashIdx == -1 {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
secondSlashIdx := strings.Index(name[firstSlashIdx+1:], separator)
|
||||||
|
if secondSlashIdx == -1 {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
return name[0 : firstSlashIdx+secondSlashIdx+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func collapsePlaceholders(segments []string) []string {
|
||||||
|
j := 0
|
||||||
|
prevStar := false
|
||||||
|
for i := 0; i < len(segments); i++ {
|
||||||
|
segment := segments[i]
|
||||||
|
if placeholder == segment {
|
||||||
|
if prevStar {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
segments[j] = placeholder
|
||||||
|
j++
|
||||||
|
prevStar = true
|
||||||
|
} else {
|
||||||
|
segments[j] = segment
|
||||||
|
j++
|
||||||
|
prevStar = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return segments[0:j]
|
||||||
|
}
|
|
@ -0,0 +1,254 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"container/heap"
|
||||||
|
"hash/fnv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/newrelic/go-agent/internal/jsonx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type queryParameters map[string]interface{}
|
||||||
|
|
||||||
|
func vetQueryParameters(params map[string]interface{}) queryParameters {
|
||||||
|
if nil == params {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Copying the parameters into a new map is safer than modifying the map
|
||||||
|
// from the customer.
|
||||||
|
vetted := make(map[string]interface{})
|
||||||
|
for key, val := range params {
|
||||||
|
if err := validAttributeKey(key); nil != err {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val = truncateStringValueIfLongInterface(val)
|
||||||
|
if err := valueIsValid(val); nil != err {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
vetted[key] = val
|
||||||
|
}
|
||||||
|
return queryParameters(vetted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q queryParameters) WriteJSON(buf *bytes.Buffer) {
|
||||||
|
buf.WriteByte('{')
|
||||||
|
w := jsonFieldsWriter{buf: buf}
|
||||||
|
for key, val := range q {
|
||||||
|
writeAttributeValueJSON(&w, key, val)
|
||||||
|
}
|
||||||
|
buf.WriteByte('}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://source.datanerd.us/agents/agent-specs/blob/master/Slow-SQLs-LEGACY.md
|
||||||
|
|
||||||
|
// slowQueryInstance represents a single datastore call.
|
||||||
|
type slowQueryInstance struct {
|
||||||
|
// Fields populated right after the datastore segment finishes:
|
||||||
|
|
||||||
|
Duration time.Duration
|
||||||
|
DatastoreMetric string
|
||||||
|
ParameterizedQuery string
|
||||||
|
QueryParameters queryParameters
|
||||||
|
Host string
|
||||||
|
PortPathOrID string
|
||||||
|
DatabaseName string
|
||||||
|
StackTrace *StackTrace
|
||||||
|
|
||||||
|
// Fields populated when merging into the harvest:
|
||||||
|
|
||||||
|
TxnName string
|
||||||
|
TxnURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregation is performed to avoid reporting multiple slow queries with same
|
||||||
|
// query string. Since some datastore segments may be below the slow query
|
||||||
|
// threshold, the aggregation fields Count, Total, and Min should be taken with
|
||||||
|
// a grain of salt.
|
||||||
|
type slowQuery struct {
|
||||||
|
Count int32 // number of times the query has been observed
|
||||||
|
Total time.Duration // cummulative duration
|
||||||
|
Min time.Duration // minimum observed duration
|
||||||
|
|
||||||
|
// When Count > 1, slowQueryInstance contains values from the slowest
|
||||||
|
// observation.
|
||||||
|
slowQueryInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
type slowQueries struct {
|
||||||
|
priorityQueue []*slowQuery
|
||||||
|
// lookup maps query strings to indices in the priorityQueue
|
||||||
|
lookup map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (slows *slowQueries) Len() int {
|
||||||
|
return len(slows.priorityQueue)
|
||||||
|
}
|
||||||
|
func (slows *slowQueries) Less(i, j int) bool {
|
||||||
|
pq := slows.priorityQueue
|
||||||
|
return pq[i].Duration < pq[j].Duration
|
||||||
|
}
|
||||||
|
func (slows *slowQueries) Swap(i, j int) {
|
||||||
|
pq := slows.priorityQueue
|
||||||
|
si := pq[i]
|
||||||
|
sj := pq[j]
|
||||||
|
pq[i], pq[j] = pq[j], pq[i]
|
||||||
|
slows.lookup[si.ParameterizedQuery] = j
|
||||||
|
slows.lookup[sj.ParameterizedQuery] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push and Pop are unused: only heap.Init and heap.Fix are used.
|
||||||
|
func (slows *slowQueries) Push(x interface{}) {}
|
||||||
|
func (slows *slowQueries) Pop() interface{} { return nil }
|
||||||
|
|
||||||
|
func newSlowQueries(max int) *slowQueries {
|
||||||
|
return &slowQueries{
|
||||||
|
lookup: make(map[string]int, max),
|
||||||
|
priorityQueue: make([]*slowQuery, 0, max),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge is used to merge slow queries from the transaction into the harvest.
|
||||||
|
func (slows *slowQueries) Merge(other *slowQueries, txnName, txnURL string) {
|
||||||
|
for _, s := range other.priorityQueue {
|
||||||
|
cp := *s
|
||||||
|
cp.TxnName = txnName
|
||||||
|
cp.TxnURL = txnURL
|
||||||
|
slows.observe(cp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge aggregates the observations from two slow queries with the same Query.
|
||||||
|
func (slow *slowQuery) merge(other slowQuery) {
|
||||||
|
slow.Count += other.Count
|
||||||
|
slow.Total += other.Total
|
||||||
|
|
||||||
|
if other.Min < slow.Min {
|
||||||
|
slow.Min = other.Min
|
||||||
|
}
|
||||||
|
if other.Duration > slow.Duration {
|
||||||
|
slow.slowQueryInstance = other.slowQueryInstance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (slows *slowQueries) observeInstance(slow slowQueryInstance) {
|
||||||
|
slows.observe(slowQuery{
|
||||||
|
Count: 1,
|
||||||
|
Total: slow.Duration,
|
||||||
|
Min: slow.Duration,
|
||||||
|
slowQueryInstance: slow,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (slows *slowQueries) insertAtIndex(slow slowQuery, idx int) {
|
||||||
|
cpy := new(slowQuery)
|
||||||
|
*cpy = slow
|
||||||
|
slows.priorityQueue[idx] = cpy
|
||||||
|
slows.lookup[slow.ParameterizedQuery] = idx
|
||||||
|
heap.Fix(slows, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (slows *slowQueries) observe(slow slowQuery) {
|
||||||
|
// Has the query has previously been observed?
|
||||||
|
if idx, ok := slows.lookup[slow.ParameterizedQuery]; ok {
|
||||||
|
slows.priorityQueue[idx].merge(slow)
|
||||||
|
heap.Fix(slows, idx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Has the collection reached max capacity?
|
||||||
|
if len(slows.priorityQueue) < cap(slows.priorityQueue) {
|
||||||
|
idx := len(slows.priorityQueue)
|
||||||
|
slows.priorityQueue = slows.priorityQueue[0 : idx+1]
|
||||||
|
slows.insertAtIndex(slow, idx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Is this query slower than the existing fastest?
|
||||||
|
fastest := slows.priorityQueue[0]
|
||||||
|
if slow.Duration > fastest.Duration {
|
||||||
|
delete(slows.lookup, fastest.ParameterizedQuery)
|
||||||
|
slows.insertAtIndex(slow, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The third element of the slow query JSON should be a hash of the query
|
||||||
|
// string. This hash may be used by backend services to aggregate queries which
|
||||||
|
// have the have the same query string. It is unknown if this actually used.
|
||||||
|
func makeSlowQueryID(query string) uint32 {
|
||||||
|
h := fnv.New32a()
|
||||||
|
h.Write([]byte(query))
|
||||||
|
return h.Sum32()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (slow *slowQuery) WriteJSON(buf *bytes.Buffer) {
|
||||||
|
buf.WriteByte('[')
|
||||||
|
jsonx.AppendString(buf, slow.TxnName)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
jsonx.AppendString(buf, slow.TxnURL)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
jsonx.AppendInt(buf, int64(makeSlowQueryID(slow.ParameterizedQuery)))
|
||||||
|
buf.WriteByte(',')
|
||||||
|
jsonx.AppendString(buf, slow.ParameterizedQuery)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
jsonx.AppendString(buf, slow.DatastoreMetric)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
jsonx.AppendInt(buf, int64(slow.Count))
|
||||||
|
buf.WriteByte(',')
|
||||||
|
jsonx.AppendFloat(buf, slow.Total.Seconds()*1000.0)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
jsonx.AppendFloat(buf, slow.Min.Seconds()*1000.0)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
jsonx.AppendFloat(buf, slow.Duration.Seconds()*1000.0)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
w := jsonFieldsWriter{buf: buf}
|
||||||
|
buf.WriteByte('{')
|
||||||
|
if "" != slow.Host {
|
||||||
|
w.stringField("host", slow.Host)
|
||||||
|
}
|
||||||
|
if "" != slow.PortPathOrID {
|
||||||
|
w.stringField("port_path_or_id", slow.PortPathOrID)
|
||||||
|
}
|
||||||
|
if "" != slow.DatabaseName {
|
||||||
|
w.stringField("database_name", slow.DatabaseName)
|
||||||
|
}
|
||||||
|
if nil != slow.StackTrace {
|
||||||
|
w.writerField("backtrace", slow.StackTrace)
|
||||||
|
}
|
||||||
|
if nil != slow.QueryParameters {
|
||||||
|
w.writerField("query_parameters", slow.QueryParameters)
|
||||||
|
}
|
||||||
|
buf.WriteByte('}')
|
||||||
|
buf.WriteByte(']')
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteJSON marshals the collection of slow queries into JSON according to the
|
||||||
|
// schema expected by the collector.
|
||||||
|
//
|
||||||
|
// Note: This JSON does not contain the agentRunID. This is for unknown
|
||||||
|
// historical reasons. Since the agentRunID is included in the url,
|
||||||
|
// its use in the other commands' JSON is redundant (although required).
|
||||||
|
func (slows *slowQueries) WriteJSON(buf *bytes.Buffer) {
|
||||||
|
buf.WriteByte('[')
|
||||||
|
buf.WriteByte('[')
|
||||||
|
for idx, s := range slows.priorityQueue {
|
||||||
|
if idx > 0 {
|
||||||
|
buf.WriteByte(',')
|
||||||
|
}
|
||||||
|
s.WriteJSON(buf)
|
||||||
|
}
|
||||||
|
buf.WriteByte(']')
|
||||||
|
buf.WriteByte(']')
|
||||||
|
}
|
||||||
|
|
||||||
|
func (slows *slowQueries) Data(agentRunID string, harvestStart time.Time) ([]byte, error) {
|
||||||
|
if 0 == len(slows.priorityQueue) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
estimate := 1024 * len(slows.priorityQueue)
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, estimate))
|
||||||
|
slows.WriteJSON(buf)
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (slows *slowQueries) MergeIntoHarvest(newHarvest *Harvest) {
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StackTrace is a stack trace.
|
||||||
|
type StackTrace struct {
|
||||||
|
callers []uintptr
|
||||||
|
written int
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStackTrace returns a new StackTrace.
|
||||||
|
func GetStackTrace(skipFrames int) *StackTrace {
|
||||||
|
st := &StackTrace{}
|
||||||
|
|
||||||
|
skip := 2 // skips runtime.Callers and this function
|
||||||
|
skip += skipFrames
|
||||||
|
|
||||||
|
st.callers = make([]uintptr, maxStackTraceFrames)
|
||||||
|
st.written = runtime.Callers(skip, st.callers)
|
||||||
|
st.callers = st.callers[0:st.written]
|
||||||
|
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
|
||||||
|
func pcToFunc(pc uintptr) (*runtime.Func, uintptr) {
|
||||||
|
// The Golang runtime package documentation says "To look up the file
|
||||||
|
// and line number of the call itself, use pc[i]-1. As an exception to
|
||||||
|
// this rule, if pc[i-1] corresponds to the function runtime.sigpanic,
|
||||||
|
// then pc[i] is the program counter of a faulting instruction and
|
||||||
|
// should be used without any subtraction."
|
||||||
|
//
|
||||||
|
// TODO: Fully understand when this subtraction is necessary.
|
||||||
|
place := pc - 1
|
||||||
|
return runtime.FuncForPC(place), place
|
||||||
|
}
|
||||||
|
|
||||||
|
func topCallerNameBase(st *StackTrace) string {
|
||||||
|
f, _ := pcToFunc(st.callers[0])
|
||||||
|
if nil == f {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return path.Base(f.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteJSON adds the stack trace to the buffer in the JSON form expected by the
|
||||||
|
// collector.
|
||||||
|
func (st *StackTrace) WriteJSON(buf *bytes.Buffer) {
|
||||||
|
buf.WriteByte('[')
|
||||||
|
for i, pc := range st.callers {
|
||||||
|
if i > 0 {
|
||||||
|
buf.WriteByte(',')
|
||||||
|
}
|
||||||
|
// Implements the format documented here:
|
||||||
|
// https://source.datanerd.us/agents/agent-specs/blob/master/Stack-Traces.md
|
||||||
|
buf.WriteByte('{')
|
||||||
|
if f, place := pcToFunc(pc); nil != f {
|
||||||
|
name := path.Base(f.Name())
|
||||||
|
file, line := f.FileLine(place)
|
||||||
|
|
||||||
|
w := jsonFieldsWriter{buf: buf}
|
||||||
|
w.stringField("filepath", file)
|
||||||
|
w.stringField("name", name)
|
||||||
|
w.intField("line", int64(line))
|
||||||
|
}
|
||||||
|
buf.WriteByte('}')
|
||||||
|
}
|
||||||
|
buf.WriteByte(']')
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON prepares JSON in the format expected by the collector.
|
||||||
|
func (st *StackTrace) MarshalJSON() ([]byte, error) {
|
||||||
|
estimate := 256 * len(st.callers)
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, estimate))
|
||||||
|
|
||||||
|
st.WriteJSON(buf)
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrDockerUnsupported is returned if Docker is not supported on the
|
||||||
|
// platform.
|
||||||
|
ErrDockerUnsupported = errors.New("Docker unsupported on this platform")
|
||||||
|
// ErrDockerNotFound is returned if a Docker ID is not found in
|
||||||
|
// /proc/self/cgroup
|
||||||
|
ErrDockerNotFound = errors.New("Docker ID not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// DockerID attempts to detect Docker.
|
||||||
|
func DockerID() (string, error) {
|
||||||
|
if "linux" != runtime.GOOS {
|
||||||
|
return "", ErrDockerUnsupported
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open("/proc/self/cgroup")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
return parseDockerID(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
dockerIDLength = 64
|
||||||
|
dockerIDRegexRaw = fmt.Sprintf("^[0-9a-f]{%d}$", dockerIDLength)
|
||||||
|
dockerIDRegex = regexp.MustCompile(dockerIDRegexRaw)
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseDockerID(r io.Reader) (string, error) {
|
||||||
|
// Each line in the cgroup file consists of three colon delimited fields.
|
||||||
|
// 1. hierarchy ID - we don't care about this
|
||||||
|
// 2. subsystems - comma separated list of cgroup subsystem names
|
||||||
|
// 3. control group - control group to which the process belongs
|
||||||
|
//
|
||||||
|
// Example
|
||||||
|
// 5:cpuacct,cpu,cpuset:/daemons
|
||||||
|
|
||||||
|
for scanner := bufio.NewScanner(r); scanner.Scan(); {
|
||||||
|
line := scanner.Bytes()
|
||||||
|
cols := bytes.SplitN(line, []byte(":"), 3)
|
||||||
|
|
||||||
|
if len(cols) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're only interested in the cpu subsystem.
|
||||||
|
if !isCPUCol(cols[1]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're only interested in Docker generated cgroups.
|
||||||
|
// Reference Implementation:
|
||||||
|
// case cpu_cgroup
|
||||||
|
// # docker native driver w/out systemd (fs)
|
||||||
|
// when %r{^/docker/([0-9a-f]+)$} then $1
|
||||||
|
// # docker native driver with systemd
|
||||||
|
// when %r{^/system\.slice/docker-([0-9a-f]+)\.scope$} then $1
|
||||||
|
// # docker lxc driver
|
||||||
|
// when %r{^/lxc/([0-9a-f]+)$} then $1
|
||||||
|
//
|
||||||
|
var id string
|
||||||
|
if bytes.HasPrefix(cols[2], []byte("/docker/")) {
|
||||||
|
id = string(cols[2][len("/docker/"):])
|
||||||
|
} else if bytes.HasPrefix(cols[2], []byte("/lxc/")) {
|
||||||
|
id = string(cols[2][len("/lxc/"):])
|
||||||
|
} else if bytes.HasPrefix(cols[2], []byte("/system.slice/docker-")) &&
|
||||||
|
bytes.HasSuffix(cols[2], []byte(".scope")) {
|
||||||
|
id = string(cols[2][len("/system.slice/docker-") : len(cols[2])-len(".scope")])
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateDockerID(id); err != nil {
|
||||||
|
// We can stop searching at this point, the CPU
|
||||||
|
// subsystem should only occur once, and its cgroup is
|
||||||
|
// not docker or not a format we accept.
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ErrDockerNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCPUCol(col []byte) bool {
|
||||||
|
// Sometimes we have multiple subsystems in one line, as in this example
|
||||||
|
// from:
|
||||||
|
// https://source.datanerd.us/newrelic/cross_agent_tests/blob/master/docker_container_id/docker-1.1.2-native-driver-systemd.txt
|
||||||
|
//
|
||||||
|
// 3:cpuacct,cpu:/system.slice/docker-67f98c9e6188f9c1818672a15dbe46237b6ee7e77f834d40d41c5fb3c2f84a2f.scope
|
||||||
|
splitCSV := func(r rune) bool { return r == ',' }
|
||||||
|
subsysCPU := []byte("cpu")
|
||||||
|
|
||||||
|
for _, subsys := range bytes.FieldsFunc(col, splitCSV) {
|
||||||
|
if bytes.Equal(subsysCPU, subsys) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateDockerID(id string) error {
|
||||||
|
if !dockerIDRegex.MatchString(id) {
|
||||||
|
return fmt.Errorf("%s does not match %s",
|
||||||
|
id, dockerIDRegexRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
10
vendor/github.com/newrelic/go-agent/internal/sysinfo/hostname_generic.go
generated
vendored
Normal file
10
vendor/github.com/newrelic/go-agent/internal/sysinfo/hostname_generic.go
generated
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// +build !linux
|
||||||
|
|
||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
// Hostname returns the host name.
|
||||||
|
func Hostname() (string, error) {
|
||||||
|
return os.Hostname()
|
||||||
|
}
|
50
vendor/github.com/newrelic/go-agent/internal/sysinfo/hostname_linux.go
generated
vendored
Normal file
50
vendor/github.com/newrelic/go-agent/internal/sysinfo/hostname_linux.go
generated
vendored
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hostname returns the host name.
|
||||||
|
func Hostname() (string, error) {
|
||||||
|
// Try the builtin API first, which is designed to match the output of
|
||||||
|
// /bin/hostname, and fallback to uname(2) if that fails to match the
|
||||||
|
// behavior of gethostname(2) as implemented by glibc. On Linux, all
|
||||||
|
// these method should result in the same value because sethostname(2)
|
||||||
|
// limits the hostname to 64 bytes, the same size of the nodename field
|
||||||
|
// returned by uname(2). Note that is correspondence is not true on
|
||||||
|
// other platforms.
|
||||||
|
//
|
||||||
|
// os.Hostname failures should be exceedingly rare, however some systems
|
||||||
|
// configure SELinux to deny read access to /proc/sys/kernel/hostname.
|
||||||
|
// Redhat's OpenShift platform for example. os.Hostname can also fail if
|
||||||
|
// some or all of /proc has been hidden via chroot(2) or manipulation of
|
||||||
|
// the current processes' filesystem namespace via the cgroups APIs.
|
||||||
|
// Docker is an example of a tool that can configure such an
|
||||||
|
// environment.
|
||||||
|
name, err := os.Hostname()
|
||||||
|
if err == nil {
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var uts syscall.Utsname
|
||||||
|
if err2 := syscall.Uname(&uts); err2 != nil {
|
||||||
|
// The man page documents only one possible error for uname(2),
|
||||||
|
// suggesting that as long as the buffer given is valid, the
|
||||||
|
// call will never fail. Return the original error in the hope
|
||||||
|
// it provides more relevant information about why the hostname
|
||||||
|
// can't be retrieved.
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Nodename to a Go string.
|
||||||
|
buf := make([]byte, 0, len(uts.Nodename))
|
||||||
|
for _, c := range uts.Nodename {
|
||||||
|
if c == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buf = append(buf, byte(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(buf), nil
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BytesToMebibytes converts bytes into mebibytes.
|
||||||
|
func BytesToMebibytes(bts uint64) uint64 {
|
||||||
|
return bts / ((uint64)(1024 * 1024))
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
meminfoRe = regexp.MustCompile(`^MemTotal:\s+([0-9]+)\s+[kK]B$`)
|
||||||
|
errMemTotalNotFound = errors.New("supported MemTotal not found in /proc/meminfo")
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseProcMeminfo is used to parse Linux's "/proc/meminfo". It is located
|
||||||
|
// here so that the relevant cross agent tests will be run on all platforms.
|
||||||
|
func parseProcMeminfo(f io.Reader) (uint64, error) {
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
if m := meminfoRe.FindSubmatch(scanner.Bytes()); m != nil {
|
||||||
|
kb, err := strconv.ParseUint(string(m[1]), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return kb * 1024, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := scanner.Err()
|
||||||
|
if err == nil {
|
||||||
|
err = errMemTotalNotFound
|
||||||
|
}
|
||||||
|
return 0, err
|
||||||
|
}
|
29
vendor/github.com/newrelic/go-agent/internal/sysinfo/memtotal_darwin.go
generated
vendored
Normal file
29
vendor/github.com/newrelic/go-agent/internal/sysinfo/memtotal_darwin.go
generated
vendored
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PhysicalMemoryBytes returns the total amount of host memory.
|
||||||
|
func PhysicalMemoryBytes() (uint64, error) {
|
||||||
|
mib := []int32{6 /* CTL_HW */, 24 /* HW_MEMSIZE */}
|
||||||
|
|
||||||
|
buf := make([]byte, 8)
|
||||||
|
bufLen := uintptr(8)
|
||||||
|
|
||||||
|
_, _, e1 := syscall.Syscall6(syscall.SYS___SYSCTL,
|
||||||
|
uintptr(unsafe.Pointer(&mib[0])), uintptr(len(mib)),
|
||||||
|
uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&bufLen)),
|
||||||
|
uintptr(0), uintptr(0))
|
||||||
|
|
||||||
|
if e1 != 0 {
|
||||||
|
return 0, e1
|
||||||
|
}
|
||||||
|
|
||||||
|
if bufLen != 8 {
|
||||||
|
return 0, syscall.EIO
|
||||||
|
}
|
||||||
|
|
||||||
|
return *(*uint64)(unsafe.Pointer(&buf[0])), nil
|
||||||
|
}
|
32
vendor/github.com/newrelic/go-agent/internal/sysinfo/memtotal_freebsd.go
generated
vendored
Normal file
32
vendor/github.com/newrelic/go-agent/internal/sysinfo/memtotal_freebsd.go
generated
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PhysicalMemoryBytes returns the total amount of host memory.
|
||||||
|
func PhysicalMemoryBytes() (uint64, error) {
|
||||||
|
mib := []int32{6 /* CTL_HW */, 5 /* HW_PHYSMEM */}
|
||||||
|
|
||||||
|
buf := make([]byte, 8)
|
||||||
|
bufLen := uintptr(8)
|
||||||
|
|
||||||
|
_, _, e1 := syscall.Syscall6(syscall.SYS___SYSCTL,
|
||||||
|
uintptr(unsafe.Pointer(&mib[0])), uintptr(len(mib)),
|
||||||
|
uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&bufLen)),
|
||||||
|
uintptr(0), uintptr(0))
|
||||||
|
|
||||||
|
if e1 != 0 {
|
||||||
|
return 0, e1
|
||||||
|
}
|
||||||
|
|
||||||
|
switch bufLen {
|
||||||
|
case 4:
|
||||||
|
return uint64(*(*uint32)(unsafe.Pointer(&buf[0]))), nil
|
||||||
|
case 8:
|
||||||
|
return *(*uint64)(unsafe.Pointer(&buf[0])), nil
|
||||||
|
default:
|
||||||
|
return 0, syscall.EIO
|
||||||
|
}
|
||||||
|
}
|
14
vendor/github.com/newrelic/go-agent/internal/sysinfo/memtotal_linux.go
generated
vendored
Normal file
14
vendor/github.com/newrelic/go-agent/internal/sysinfo/memtotal_linux.go
generated
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
// PhysicalMemoryBytes returns the total amount of host memory.
|
||||||
|
func PhysicalMemoryBytes() (uint64, error) {
|
||||||
|
f, err := os.Open("/proc/meminfo")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
return parseProcMeminfo(f)
|
||||||
|
}
|
26
vendor/github.com/newrelic/go-agent/internal/sysinfo/memtotal_solaris.go
generated
vendored
Normal file
26
vendor/github.com/newrelic/go-agent/internal/sysinfo/memtotal_solaris.go
generated
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package sysinfo
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include <unistd.h>
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
// PhysicalMemoryBytes returns the total amount of host memory.
|
||||||
|
func PhysicalMemoryBytes() (uint64, error) {
|
||||||
|
// The function we're calling on Solaris is
|
||||||
|
// long sysconf(int name);
|
||||||
|
var pages C.long
|
||||||
|
var pagesizeBytes C.long
|
||||||
|
var err error
|
||||||
|
|
||||||
|
pagesizeBytes, err = C.sysconf(C._SC_PAGE_SIZE)
|
||||||
|
if pagesizeBytes < 1 {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
pages, err = C.sysconf(C._SC_PHYS_PAGES)
|
||||||
|
if pages < 1 {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint64(pages) * uint64(pagesizeBytes), nil
|
||||||
|
}
|
23
vendor/github.com/newrelic/go-agent/internal/sysinfo/memtotal_windows.go
generated
vendored
Normal file
23
vendor/github.com/newrelic/go-agent/internal/sysinfo/memtotal_windows.go
generated
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PhysicalMemoryBytes returns the total amount of host memory.
|
||||||
|
func PhysicalMemoryBytes() (uint64, error) {
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/cc300158(v=vs.85).aspx
|
||||||
|
// http://stackoverflow.com/questions/30743070/query-total-physical-memory-in-windows-with-golang
|
||||||
|
mod := syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
proc := mod.NewProc("GetPhysicallyInstalledSystemMemory")
|
||||||
|
var memkb uint64
|
||||||
|
|
||||||
|
ret, _, err := proc.Call(uintptr(unsafe.Pointer(&memkb)))
|
||||||
|
// return value TRUE(1) succeeds, FAILED(0) fails
|
||||||
|
if ret != 1 {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return memkb * 1024, nil
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Usage contains process process times.
|
||||||
|
type Usage struct {
|
||||||
|
System time.Duration
|
||||||
|
User time.Duration
|
||||||
|
}
|
26
vendor/github.com/newrelic/go-agent/internal/sysinfo/usage_posix.go
generated
vendored
Normal file
26
vendor/github.com/newrelic/go-agent/internal/sysinfo/usage_posix.go
generated
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func timevalToDuration(tv syscall.Timeval) time.Duration {
|
||||||
|
return time.Duration(tv.Nano()) * time.Nanosecond
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsage gathers process times.
|
||||||
|
func GetUsage() (Usage, error) {
|
||||||
|
ru := syscall.Rusage{}
|
||||||
|
err := syscall.Getrusage(syscall.RUSAGE_SELF, &ru)
|
||||||
|
if err != nil {
|
||||||
|
return Usage{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Usage{
|
||||||
|
System: timevalToDuration(ru.Stime),
|
||||||
|
User: timevalToDuration(ru.Utime),
|
||||||
|
}, nil
|
||||||
|
}
|
34
vendor/github.com/newrelic/go-agent/internal/sysinfo/usage_windows.go
generated
vendored
Normal file
34
vendor/github.com/newrelic/go-agent/internal/sysinfo/usage_windows.go
generated
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package sysinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func filetimeToDuration(ft *syscall.Filetime) time.Duration {
|
||||||
|
ns := ft.Nanoseconds()
|
||||||
|
return time.Duration(ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsage gathers process times.
|
||||||
|
func GetUsage() (Usage, error) {
|
||||||
|
var creationTime syscall.Filetime
|
||||||
|
var exitTime syscall.Filetime
|
||||||
|
var kernelTime syscall.Filetime
|
||||||
|
var userTime syscall.Filetime
|
||||||
|
|
||||||
|
handle, err := syscall.GetCurrentProcess()
|
||||||
|
if err != nil {
|
||||||
|
return Usage{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = syscall.GetProcessTimes(handle, &creationTime, &exitTime, &kernelTime, &userTime)
|
||||||
|
if err != nil {
|
||||||
|
return Usage{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Usage{
|
||||||
|
System: filetimeToDuration(&kernelTime),
|
||||||
|
User: filetimeToDuration(&userTime),
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,413 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/newrelic/go-agent/internal/sysinfo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type segmentStamp uint64
|
||||||
|
|
||||||
|
type segmentTime struct {
|
||||||
|
Stamp segmentStamp
|
||||||
|
Time time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// SegmentStartTime is embedded into the top level segments (rather than
|
||||||
|
// segmentTime) to minimize the structure sizes to minimize allocations.
|
||||||
|
type SegmentStartTime struct {
|
||||||
|
Stamp segmentStamp
|
||||||
|
Depth int
|
||||||
|
}
|
||||||
|
|
||||||
|
type segmentFrame struct {
|
||||||
|
segmentTime
|
||||||
|
children time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type segmentEnd struct {
|
||||||
|
valid bool
|
||||||
|
start segmentTime
|
||||||
|
stop segmentTime
|
||||||
|
duration time.Duration
|
||||||
|
exclusive time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracer tracks segments.
|
||||||
|
type Tracer struct {
|
||||||
|
finishedChildren time.Duration
|
||||||
|
stamp segmentStamp
|
||||||
|
currentDepth int
|
||||||
|
stack []segmentFrame
|
||||||
|
|
||||||
|
customSegments map[string]*metricData
|
||||||
|
datastoreSegments map[DatastoreMetricKey]*metricData
|
||||||
|
externalSegments map[externalMetricKey]*metricData
|
||||||
|
|
||||||
|
DatastoreExternalTotals
|
||||||
|
|
||||||
|
TxnTrace
|
||||||
|
|
||||||
|
SlowQueriesEnabled bool
|
||||||
|
SlowQueryThreshold time.Duration
|
||||||
|
SlowQueries *slowQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
startingStackDepthAlloc = 128
|
||||||
|
datastoreProductUnknown = "Unknown"
|
||||||
|
datastoreOperationUnknown = "other"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *Tracer) time(now time.Time) segmentTime {
|
||||||
|
// Update the stamp before using it so that a 0 stamp can be special.
|
||||||
|
t.stamp++
|
||||||
|
return segmentTime{
|
||||||
|
Time: now,
|
||||||
|
Stamp: t.stamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TracerRootChildren is used to calculate a transaction's exclusive duration.
|
||||||
|
func TracerRootChildren(t *Tracer) time.Duration {
|
||||||
|
var lostChildren time.Duration
|
||||||
|
for i := 0; i < t.currentDepth; i++ {
|
||||||
|
lostChildren += t.stack[i].children
|
||||||
|
}
|
||||||
|
return t.finishedChildren + lostChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartSegment begins a segment.
|
||||||
|
func StartSegment(t *Tracer, now time.Time) SegmentStartTime {
|
||||||
|
if nil == t.stack {
|
||||||
|
t.stack = make([]segmentFrame, startingStackDepthAlloc)
|
||||||
|
}
|
||||||
|
if cap(t.stack) == t.currentDepth {
|
||||||
|
newLimit := 2 * t.currentDepth
|
||||||
|
newStack := make([]segmentFrame, newLimit)
|
||||||
|
copy(newStack, t.stack)
|
||||||
|
t.stack = newStack
|
||||||
|
}
|
||||||
|
|
||||||
|
tm := t.time(now)
|
||||||
|
|
||||||
|
depth := t.currentDepth
|
||||||
|
t.currentDepth++
|
||||||
|
t.stack[depth].children = 0
|
||||||
|
t.stack[depth].segmentTime = tm
|
||||||
|
|
||||||
|
return SegmentStartTime{
|
||||||
|
Stamp: tm.Stamp,
|
||||||
|
Depth: depth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func endSegment(t *Tracer, start SegmentStartTime, now time.Time) segmentEnd {
|
||||||
|
var s segmentEnd
|
||||||
|
if 0 == start.Stamp {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if start.Depth >= t.currentDepth {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if start.Depth < 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if start.Stamp != t.stack[start.Depth].Stamp {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
var children time.Duration
|
||||||
|
for i := start.Depth; i < t.currentDepth; i++ {
|
||||||
|
children += t.stack[i].children
|
||||||
|
}
|
||||||
|
s.valid = true
|
||||||
|
s.stop = t.time(now)
|
||||||
|
s.start = t.stack[start.Depth].segmentTime
|
||||||
|
if s.stop.Time.After(s.start.Time) {
|
||||||
|
s.duration = s.stop.Time.Sub(s.start.Time)
|
||||||
|
}
|
||||||
|
if s.duration > children {
|
||||||
|
s.exclusive = s.duration - children
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that we expect (depth == (t.currentDepth - 1)). However, if
|
||||||
|
// (depth < (t.currentDepth - 1)), that's ok: could be a panic popped
|
||||||
|
// some stack frames (and the consumer was not using defer).
|
||||||
|
t.currentDepth = start.Depth
|
||||||
|
|
||||||
|
if 0 == t.currentDepth {
|
||||||
|
t.finishedChildren += s.duration
|
||||||
|
} else {
|
||||||
|
t.stack[t.currentDepth-1].children += s.duration
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndBasicSegment ends a basic segment.
|
||||||
|
func EndBasicSegment(t *Tracer, start SegmentStartTime, now time.Time, name string) {
|
||||||
|
end := endSegment(t, start, now)
|
||||||
|
if !end.valid {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if nil == t.customSegments {
|
||||||
|
t.customSegments = make(map[string]*metricData)
|
||||||
|
}
|
||||||
|
m := metricDataFromDuration(end.duration, end.exclusive)
|
||||||
|
if data, ok := t.customSegments[name]; ok {
|
||||||
|
data.aggregate(m)
|
||||||
|
} else {
|
||||||
|
// Use `new` in place of &m so that m is not
|
||||||
|
// automatically moved to the heap.
|
||||||
|
cpy := new(metricData)
|
||||||
|
*cpy = m
|
||||||
|
t.customSegments[name] = cpy
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.TxnTrace.considerNode(end) {
|
||||||
|
t.TxnTrace.witnessNode(end, customSegmentMetric(name), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndExternalSegment ends an external segment.
|
||||||
|
func EndExternalSegment(t *Tracer, start SegmentStartTime, now time.Time, u *url.URL) {
|
||||||
|
end := endSegment(t, start, now)
|
||||||
|
if !end.valid {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
host := HostFromURL(u)
|
||||||
|
if "" == host {
|
||||||
|
host = "unknown"
|
||||||
|
}
|
||||||
|
key := externalMetricKey{
|
||||||
|
Host: host,
|
||||||
|
ExternalCrossProcessID: "",
|
||||||
|
ExternalTransactionName: "",
|
||||||
|
}
|
||||||
|
if nil == t.externalSegments {
|
||||||
|
t.externalSegments = make(map[externalMetricKey]*metricData)
|
||||||
|
}
|
||||||
|
t.externalCallCount++
|
||||||
|
t.externalDuration += end.duration
|
||||||
|
m := metricDataFromDuration(end.duration, end.exclusive)
|
||||||
|
if data, ok := t.externalSegments[key]; ok {
|
||||||
|
data.aggregate(m)
|
||||||
|
} else {
|
||||||
|
// Use `new` in place of &m so that m is not
|
||||||
|
// automatically moved to the heap.
|
||||||
|
cpy := new(metricData)
|
||||||
|
*cpy = m
|
||||||
|
t.externalSegments[key] = cpy
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.TxnTrace.considerNode(end) {
|
||||||
|
t.TxnTrace.witnessNode(end, externalHostMetric(key), &traceNodeParams{
|
||||||
|
CleanURL: SafeURL(u),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndDatastoreParams contains the parameters for EndDatastoreSegment.
|
||||||
|
type EndDatastoreParams struct {
|
||||||
|
Tracer *Tracer
|
||||||
|
Start SegmentStartTime
|
||||||
|
Now time.Time
|
||||||
|
Product string
|
||||||
|
Collection string
|
||||||
|
Operation string
|
||||||
|
ParameterizedQuery string
|
||||||
|
QueryParameters map[string]interface{}
|
||||||
|
Host string
|
||||||
|
PortPathOrID string
|
||||||
|
Database string
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
unknownDatastoreHost = "unknown"
|
||||||
|
unknownDatastorePortPathOrID = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ThisHost is the system hostname.
|
||||||
|
ThisHost = func() string {
|
||||||
|
if h, err := sysinfo.Hostname(); nil == err {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
return unknownDatastoreHost
|
||||||
|
}()
|
||||||
|
hostsToReplace = map[string]struct{}{
|
||||||
|
"localhost": struct{}{},
|
||||||
|
"127.0.0.1": struct{}{},
|
||||||
|
"0.0.0.0": struct{}{},
|
||||||
|
"0:0:0:0:0:0:0:1": struct{}{},
|
||||||
|
"::1": struct{}{},
|
||||||
|
"0:0:0:0:0:0:0:0": struct{}{},
|
||||||
|
"::": struct{}{},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t Tracer) slowQueryWorthy(d time.Duration) bool {
|
||||||
|
return t.SlowQueriesEnabled && (d >= t.SlowQueryThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndDatastoreSegment ends a datastore segment.
|
||||||
|
func EndDatastoreSegment(p EndDatastoreParams) {
|
||||||
|
end := endSegment(p.Tracer, p.Start, p.Now)
|
||||||
|
if !end.valid {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if p.Operation == "" {
|
||||||
|
p.Operation = datastoreOperationUnknown
|
||||||
|
}
|
||||||
|
if p.Product == "" {
|
||||||
|
p.Product = datastoreProductUnknown
|
||||||
|
}
|
||||||
|
if p.Host == "" && p.PortPathOrID != "" {
|
||||||
|
p.Host = unknownDatastoreHost
|
||||||
|
}
|
||||||
|
if p.PortPathOrID == "" && p.Host != "" {
|
||||||
|
p.PortPathOrID = unknownDatastorePortPathOrID
|
||||||
|
}
|
||||||
|
if _, ok := hostsToReplace[p.Host]; ok {
|
||||||
|
p.Host = ThisHost
|
||||||
|
}
|
||||||
|
|
||||||
|
// We still want to create a slowQuery if the consumer has not provided
|
||||||
|
// a Query string since the stack trace has value.
|
||||||
|
if p.ParameterizedQuery == "" {
|
||||||
|
collection := p.Collection
|
||||||
|
if "" == collection {
|
||||||
|
collection = "unknown"
|
||||||
|
}
|
||||||
|
p.ParameterizedQuery = fmt.Sprintf(`'%s' on '%s' using '%s'`,
|
||||||
|
p.Operation, collection, p.Product)
|
||||||
|
}
|
||||||
|
|
||||||
|
key := DatastoreMetricKey{
|
||||||
|
Product: p.Product,
|
||||||
|
Collection: p.Collection,
|
||||||
|
Operation: p.Operation,
|
||||||
|
Host: p.Host,
|
||||||
|
PortPathOrID: p.PortPathOrID,
|
||||||
|
}
|
||||||
|
if nil == p.Tracer.datastoreSegments {
|
||||||
|
p.Tracer.datastoreSegments = make(map[DatastoreMetricKey]*metricData)
|
||||||
|
}
|
||||||
|
p.Tracer.datastoreCallCount++
|
||||||
|
p.Tracer.datastoreDuration += end.duration
|
||||||
|
m := metricDataFromDuration(end.duration, end.exclusive)
|
||||||
|
if data, ok := p.Tracer.datastoreSegments[key]; ok {
|
||||||
|
data.aggregate(m)
|
||||||
|
} else {
|
||||||
|
// Use `new` in place of &m so that m is not
|
||||||
|
// automatically moved to the heap.
|
||||||
|
cpy := new(metricData)
|
||||||
|
*cpy = m
|
||||||
|
p.Tracer.datastoreSegments[key] = cpy
|
||||||
|
}
|
||||||
|
|
||||||
|
scopedMetric := datastoreScopedMetric(key)
|
||||||
|
queryParams := vetQueryParameters(p.QueryParameters)
|
||||||
|
|
||||||
|
if p.Tracer.TxnTrace.considerNode(end) {
|
||||||
|
p.Tracer.TxnTrace.witnessNode(end, scopedMetric, &traceNodeParams{
|
||||||
|
Host: p.Host,
|
||||||
|
PortPathOrID: p.PortPathOrID,
|
||||||
|
Database: p.Database,
|
||||||
|
Query: p.ParameterizedQuery,
|
||||||
|
queryParameters: queryParams,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Tracer.slowQueryWorthy(end.duration) {
|
||||||
|
if nil == p.Tracer.SlowQueries {
|
||||||
|
p.Tracer.SlowQueries = newSlowQueries(maxTxnSlowQueries)
|
||||||
|
}
|
||||||
|
// Frames to skip:
|
||||||
|
// this function
|
||||||
|
// endDatastore
|
||||||
|
// DatastoreSegment.End
|
||||||
|
skipFrames := 3
|
||||||
|
p.Tracer.SlowQueries.observeInstance(slowQueryInstance{
|
||||||
|
Duration: end.duration,
|
||||||
|
DatastoreMetric: scopedMetric,
|
||||||
|
ParameterizedQuery: p.ParameterizedQuery,
|
||||||
|
QueryParameters: queryParams,
|
||||||
|
Host: p.Host,
|
||||||
|
PortPathOrID: p.PortPathOrID,
|
||||||
|
DatabaseName: p.Database,
|
||||||
|
StackTrace: GetStackTrace(skipFrames),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeBreakdownMetrics creates segment metrics.
|
||||||
|
func MergeBreakdownMetrics(t *Tracer, metrics *metricTable, scope string, isWeb bool) {
|
||||||
|
// Custom Segment Metrics
|
||||||
|
for key, data := range t.customSegments {
|
||||||
|
name := customSegmentMetric(key)
|
||||||
|
// Unscoped
|
||||||
|
metrics.add(name, "", *data, unforced)
|
||||||
|
// Scoped
|
||||||
|
metrics.add(name, scope, *data, unforced)
|
||||||
|
}
|
||||||
|
|
||||||
|
// External Segment Metrics
|
||||||
|
for key, data := range t.externalSegments {
|
||||||
|
metrics.add(externalAll, "", *data, forced)
|
||||||
|
if isWeb {
|
||||||
|
metrics.add(externalWeb, "", *data, forced)
|
||||||
|
} else {
|
||||||
|
metrics.add(externalOther, "", *data, forced)
|
||||||
|
}
|
||||||
|
hostMetric := externalHostMetric(key)
|
||||||
|
metrics.add(hostMetric, "", *data, unforced)
|
||||||
|
if "" != key.ExternalCrossProcessID && "" != key.ExternalTransactionName {
|
||||||
|
txnMetric := externalTransactionMetric(key)
|
||||||
|
|
||||||
|
// Unscoped CAT metrics
|
||||||
|
metrics.add(externalAppMetric(key), "", *data, unforced)
|
||||||
|
metrics.add(txnMetric, "", *data, unforced)
|
||||||
|
|
||||||
|
// Scoped External Metric
|
||||||
|
metrics.add(txnMetric, scope, *data, unforced)
|
||||||
|
} else {
|
||||||
|
// Scoped External Metric
|
||||||
|
metrics.add(hostMetric, scope, *data, unforced)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datastore Segment Metrics
|
||||||
|
for key, data := range t.datastoreSegments {
|
||||||
|
metrics.add(datastoreAll, "", *data, forced)
|
||||||
|
|
||||||
|
product := datastoreProductMetric(key)
|
||||||
|
metrics.add(product.All, "", *data, forced)
|
||||||
|
if isWeb {
|
||||||
|
metrics.add(datastoreWeb, "", *data, forced)
|
||||||
|
metrics.add(product.Web, "", *data, forced)
|
||||||
|
} else {
|
||||||
|
metrics.add(datastoreOther, "", *data, forced)
|
||||||
|
metrics.add(product.Other, "", *data, forced)
|
||||||
|
}
|
||||||
|
|
||||||
|
if key.Host != "" && key.PortPathOrID != "" {
|
||||||
|
instance := datastoreInstanceMetric(key)
|
||||||
|
metrics.add(instance, "", *data, unforced)
|
||||||
|
}
|
||||||
|
|
||||||
|
operation := datastoreOperationMetric(key)
|
||||||
|
metrics.add(operation, "", *data, unforced)
|
||||||
|
|
||||||
|
if "" != key.Collection {
|
||||||
|
statement := datastoreStatementMetric(key)
|
||||||
|
|
||||||
|
metrics.add(statement, "", *data, unforced)
|
||||||
|
metrics.add(statement, scope, *data, unforced)
|
||||||
|
} else {
|
||||||
|
metrics.add(operation, scope, *data, unforced)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DatastoreExternalTotals contains overview of external and datastore calls
|
||||||
|
// made during a transaction.
|
||||||
|
type DatastoreExternalTotals struct {
|
||||||
|
externalCallCount uint64
|
||||||
|
externalDuration time.Duration
|
||||||
|
datastoreCallCount uint64
|
||||||
|
datastoreDuration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// TxnEvent represents a transaction.
|
||||||
|
// https://source.datanerd.us/agents/agent-specs/blob/master/Transaction-Events-PORTED.md
|
||||||
|
// https://newrelic.atlassian.net/wiki/display/eng/Agent+Support+for+Synthetics%3A+Forced+Transaction+Traces+and+Analytic+Events
|
||||||
|
type TxnEvent struct {
|
||||||
|
Name string
|
||||||
|
Timestamp time.Time
|
||||||
|
Duration time.Duration
|
||||||
|
Queuing time.Duration
|
||||||
|
Zone ApdexZone
|
||||||
|
Attrs *Attributes
|
||||||
|
DatastoreExternalTotals
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteJSON prepares JSON in the format expected by the collector.
|
||||||
|
func (e *TxnEvent) WriteJSON(buf *bytes.Buffer) {
|
||||||
|
w := jsonFieldsWriter{buf: buf}
|
||||||
|
buf.WriteByte('[')
|
||||||
|
buf.WriteByte('{')
|
||||||
|
w.stringField("type", "Transaction")
|
||||||
|
w.stringField("name", e.Name)
|
||||||
|
w.floatField("timestamp", timeToFloatSeconds(e.Timestamp))
|
||||||
|
w.floatField("duration", e.Duration.Seconds())
|
||||||
|
if ApdexNone != e.Zone {
|
||||||
|
w.stringField("nr.apdexPerfZone", e.Zone.label())
|
||||||
|
}
|
||||||
|
if e.Queuing > 0 {
|
||||||
|
w.floatField("queueDuration", e.Queuing.Seconds())
|
||||||
|
}
|
||||||
|
if e.externalCallCount > 0 {
|
||||||
|
w.intField("externalCallCount", int64(e.externalCallCount))
|
||||||
|
w.floatField("externalDuration", e.externalDuration.Seconds())
|
||||||
|
}
|
||||||
|
if e.datastoreCallCount > 0 {
|
||||||
|
// Note that "database" is used for the keys here instead of
|
||||||
|
// "datastore" for historical reasons.
|
||||||
|
w.intField("databaseCallCount", int64(e.datastoreCallCount))
|
||||||
|
w.floatField("databaseDuration", e.datastoreDuration.Seconds())
|
||||||
|
}
|
||||||
|
buf.WriteByte('}')
|
||||||
|
buf.WriteByte(',')
|
||||||
|
userAttributesJSON(e.Attrs, buf, destTxnEvent)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
agentAttributesJSON(e.Attrs, buf, destTxnEvent)
|
||||||
|
buf.WriteByte(']')
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON is used for testing.
|
||||||
|
func (e *TxnEvent) MarshalJSON() ([]byte, error) {
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, 256))
|
||||||
|
|
||||||
|
e.WriteJSON(buf)
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type txnEvents struct {
|
||||||
|
events *analyticsEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTxnEvents(max int) *txnEvents {
|
||||||
|
return &txnEvents{
|
||||||
|
events: newAnalyticsEvents(max),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (events *txnEvents) AddTxnEvent(e *TxnEvent) {
|
||||||
|
stamp := eventStamp(rand.Float32())
|
||||||
|
events.events.addEvent(analyticsEvent{stamp, e})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (events *txnEvents) MergeIntoHarvest(h *Harvest) {
|
||||||
|
h.TxnEvents.events.mergeFailed(events.events)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (events *txnEvents) Data(agentRunID string, harvestStart time.Time) ([]byte, error) {
|
||||||
|
return events.events.CollectorJSON(agentRunID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (events *txnEvents) numSeen() float64 { return events.events.NumSeen() }
|
||||||
|
func (events *txnEvents) numSaved() float64 { return events.events.NumSaved() }
|
|
@ -0,0 +1,315 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"container/heap"
|
||||||
|
"encoding/json"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/newrelic/go-agent/internal/jsonx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// See https://source.datanerd.us/agents/agent-specs/blob/master/Transaction-Trace-LEGACY.md
|
||||||
|
|
||||||
|
type traceNodeHeap []traceNode
|
||||||
|
|
||||||
|
// traceNodeParams is used for trace node parameters. A struct is used in place
|
||||||
|
// of a map[string]interface{} to facilitate testing and reduce JSON Marshal
|
||||||
|
// overhead. If too many fields get added here, it probably makes sense to
|
||||||
|
// start using a map. This struct is not embedded into traceNode to minimize
|
||||||
|
// the size of traceNode: Not all nodes will have parameters.
|
||||||
|
type traceNodeParams struct {
|
||||||
|
StackTrace *StackTrace
|
||||||
|
CleanURL string
|
||||||
|
Database string
|
||||||
|
Host string
|
||||||
|
PortPathOrID string
|
||||||
|
Query string
|
||||||
|
queryParameters queryParameters
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *traceNodeParams) WriteJSON(buf *bytes.Buffer) {
|
||||||
|
w := jsonFieldsWriter{buf: buf}
|
||||||
|
buf.WriteByte('{')
|
||||||
|
if nil != p.StackTrace {
|
||||||
|
w.writerField("backtrace", p.StackTrace)
|
||||||
|
}
|
||||||
|
if "" != p.CleanURL {
|
||||||
|
w.stringField("uri", p.CleanURL)
|
||||||
|
}
|
||||||
|
if "" != p.Database {
|
||||||
|
w.stringField("database_name", p.Database)
|
||||||
|
}
|
||||||
|
if "" != p.Host {
|
||||||
|
w.stringField("host", p.Host)
|
||||||
|
}
|
||||||
|
if "" != p.PortPathOrID {
|
||||||
|
w.stringField("port_path_or_id", p.PortPathOrID)
|
||||||
|
}
|
||||||
|
if "" != p.Query {
|
||||||
|
w.stringField("query", p.Query)
|
||||||
|
}
|
||||||
|
if nil != p.queryParameters {
|
||||||
|
w.writerField("query_parameters", p.queryParameters)
|
||||||
|
}
|
||||||
|
buf.WriteByte('}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON is used for testing.
|
||||||
|
func (p *traceNodeParams) MarshalJSON() ([]byte, error) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
p.WriteJSON(buf)
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type traceNode struct {
|
||||||
|
start segmentTime
|
||||||
|
stop segmentTime
|
||||||
|
duration time.Duration
|
||||||
|
params *traceNodeParams
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h traceNodeHeap) Len() int { return len(h) }
|
||||||
|
func (h traceNodeHeap) Less(i, j int) bool { return h[i].duration < h[j].duration }
|
||||||
|
func (h traceNodeHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
||||||
|
|
||||||
|
// Push and Pop are unused: only heap.Init and heap.Fix are used.
|
||||||
|
func (h traceNodeHeap) Push(x interface{}) {}
|
||||||
|
func (h traceNodeHeap) Pop() interface{} { return nil }
|
||||||
|
|
||||||
|
// TxnTrace contains the work in progress transaction trace.
|
||||||
|
type TxnTrace struct {
|
||||||
|
Enabled bool
|
||||||
|
SegmentThreshold time.Duration
|
||||||
|
StackTraceThreshold time.Duration
|
||||||
|
nodes traceNodeHeap
|
||||||
|
maxNodes int
|
||||||
|
}
|
||||||
|
|
||||||
|
// considerNode exists to prevent unnecessary calls to witnessNode: constructing
|
||||||
|
// the metric name and params map requires allocations.
|
||||||
|
func (trace *TxnTrace) considerNode(end segmentEnd) bool {
|
||||||
|
return trace.Enabled && (end.duration >= trace.SegmentThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (trace *TxnTrace) witnessNode(end segmentEnd, name string, params *traceNodeParams) {
|
||||||
|
node := traceNode{
|
||||||
|
start: end.start,
|
||||||
|
stop: end.stop,
|
||||||
|
duration: end.duration,
|
||||||
|
name: name,
|
||||||
|
params: params,
|
||||||
|
}
|
||||||
|
if !trace.considerNode(end) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if trace.nodes == nil {
|
||||||
|
max := trace.maxNodes
|
||||||
|
if 0 == max {
|
||||||
|
max = maxTxnTraceNodes
|
||||||
|
}
|
||||||
|
trace.nodes = make(traceNodeHeap, 0, max)
|
||||||
|
}
|
||||||
|
if end.exclusive >= trace.StackTraceThreshold {
|
||||||
|
if node.params == nil {
|
||||||
|
p := new(traceNodeParams)
|
||||||
|
node.params = p
|
||||||
|
}
|
||||||
|
// skip the following stack frames:
|
||||||
|
// this method
|
||||||
|
// function in tracing.go (EndBasicSegment, EndExternalSegment, EndDatastoreSegment)
|
||||||
|
// function in internal_txn.go (endSegment, endExternal, endDatastore)
|
||||||
|
// segment end method
|
||||||
|
skip := 4
|
||||||
|
node.params.StackTrace = GetStackTrace(skip)
|
||||||
|
}
|
||||||
|
if len(trace.nodes) < cap(trace.nodes) {
|
||||||
|
trace.nodes = append(trace.nodes, node)
|
||||||
|
if len(trace.nodes) == cap(trace.nodes) {
|
||||||
|
heap.Init(trace.nodes)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if node.duration <= trace.nodes[0].duration {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trace.nodes[0] = node
|
||||||
|
heap.Fix(trace.nodes, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HarvestTrace contains a finished transaction trace ready for serialization to
|
||||||
|
// the collector.
|
||||||
|
type HarvestTrace struct {
|
||||||
|
Start time.Time
|
||||||
|
Duration time.Duration
|
||||||
|
MetricName string
|
||||||
|
CleanURL string
|
||||||
|
Trace TxnTrace
|
||||||
|
ForcePersist bool
|
||||||
|
GUID string
|
||||||
|
SyntheticsResourceID string
|
||||||
|
Attrs *Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
type nodeDetails struct {
|
||||||
|
name string
|
||||||
|
relativeStart time.Duration
|
||||||
|
relativeStop time.Duration
|
||||||
|
params *traceNodeParams
|
||||||
|
}
|
||||||
|
|
||||||
|
func printNodeStart(buf *bytes.Buffer, n nodeDetails) {
|
||||||
|
// time.Seconds() is intentionally not used here. Millisecond
|
||||||
|
// precision is enough.
|
||||||
|
relativeStartMillis := n.relativeStart.Nanoseconds() / (1000 * 1000)
|
||||||
|
relativeStopMillis := n.relativeStop.Nanoseconds() / (1000 * 1000)
|
||||||
|
|
||||||
|
buf.WriteByte('[')
|
||||||
|
jsonx.AppendInt(buf, relativeStartMillis)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
jsonx.AppendInt(buf, relativeStopMillis)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
jsonx.AppendString(buf, n.name)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
if nil == n.params {
|
||||||
|
buf.WriteString("{}")
|
||||||
|
} else {
|
||||||
|
n.params.WriteJSON(buf)
|
||||||
|
}
|
||||||
|
buf.WriteByte(',')
|
||||||
|
buf.WriteByte('[')
|
||||||
|
}
|
||||||
|
|
||||||
|
func printChildren(buf *bytes.Buffer, traceStart time.Time, nodes sortedTraceNodes, next int, stop segmentStamp) int {
|
||||||
|
firstChild := true
|
||||||
|
for next < len(nodes) && nodes[next].start.Stamp < stop {
|
||||||
|
if firstChild {
|
||||||
|
firstChild = false
|
||||||
|
} else {
|
||||||
|
buf.WriteByte(',')
|
||||||
|
}
|
||||||
|
printNodeStart(buf, nodeDetails{
|
||||||
|
name: nodes[next].name,
|
||||||
|
relativeStart: nodes[next].start.Time.Sub(traceStart),
|
||||||
|
relativeStop: nodes[next].stop.Time.Sub(traceStart),
|
||||||
|
params: nodes[next].params,
|
||||||
|
})
|
||||||
|
next = printChildren(buf, traceStart, nodes, next+1, nodes[next].stop.Stamp)
|
||||||
|
buf.WriteString("]]")
|
||||||
|
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
type sortedTraceNodes []*traceNode
|
||||||
|
|
||||||
|
func (s sortedTraceNodes) Len() int { return len(s) }
|
||||||
|
func (s sortedTraceNodes) Less(i, j int) bool { return s[i].start.Stamp < s[j].start.Stamp }
|
||||||
|
func (s sortedTraceNodes) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||||
|
|
||||||
|
func traceDataJSON(trace *HarvestTrace) []byte {
|
||||||
|
estimate := 100 * len(trace.Trace.nodes)
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, estimate))
|
||||||
|
|
||||||
|
nodes := make(sortedTraceNodes, len(trace.Trace.nodes))
|
||||||
|
for i := 0; i < len(nodes); i++ {
|
||||||
|
nodes[i] = &trace.Trace.nodes[i]
|
||||||
|
}
|
||||||
|
sort.Sort(nodes)
|
||||||
|
|
||||||
|
buf.WriteByte('[') // begin trace data
|
||||||
|
|
||||||
|
// If the trace string pool is used, insert another array here.
|
||||||
|
|
||||||
|
jsonx.AppendFloat(buf, 0.0) // unused timestamp
|
||||||
|
buf.WriteByte(',') //
|
||||||
|
buf.WriteString("{}") // unused: formerly request parameters
|
||||||
|
buf.WriteByte(',') //
|
||||||
|
buf.WriteString("{}") // unused: formerly custom parameters
|
||||||
|
buf.WriteByte(',') //
|
||||||
|
|
||||||
|
printNodeStart(buf, nodeDetails{ // begin outer root
|
||||||
|
name: "ROOT",
|
||||||
|
relativeStart: 0,
|
||||||
|
relativeStop: trace.Duration,
|
||||||
|
})
|
||||||
|
|
||||||
|
printNodeStart(buf, nodeDetails{ // begin inner root
|
||||||
|
name: trace.MetricName,
|
||||||
|
relativeStart: 0,
|
||||||
|
relativeStop: trace.Duration,
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(nodes) > 0 {
|
||||||
|
lastStopStamp := nodes[len(nodes)-1].stop.Stamp + 1
|
||||||
|
printChildren(buf, trace.Start, nodes, 0, lastStopStamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteString("]]") // end outer root
|
||||||
|
buf.WriteString("]]") // end inner root
|
||||||
|
|
||||||
|
buf.WriteByte(',')
|
||||||
|
buf.WriteByte('{')
|
||||||
|
buf.WriteString(`"agentAttributes":`)
|
||||||
|
agentAttributesJSON(trace.Attrs, buf, destTxnTrace)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
buf.WriteString(`"userAttributes":`)
|
||||||
|
userAttributesJSON(trace.Attrs, buf, destTxnTrace)
|
||||||
|
buf.WriteByte(',')
|
||||||
|
buf.WriteString(`"intrinsics":{}`) // TODO intrinsics
|
||||||
|
buf.WriteByte('}')
|
||||||
|
|
||||||
|
// If the trace string pool is used, end another array here.
|
||||||
|
|
||||||
|
buf.WriteByte(']') // end trace data
|
||||||
|
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON prepares the trace in the JSON expected by the collector.
|
||||||
|
func (trace *HarvestTrace) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal([]interface{}{
|
||||||
|
trace.Start.UnixNano() / 1000,
|
||||||
|
trace.Duration.Seconds() * 1000.0,
|
||||||
|
trace.MetricName,
|
||||||
|
trace.CleanURL,
|
||||||
|
JSONString(traceDataJSON(trace)),
|
||||||
|
trace.GUID,
|
||||||
|
nil, // reserved for future use
|
||||||
|
trace.ForcePersist,
|
||||||
|
nil, // X-Ray sessions not supported
|
||||||
|
trace.SyntheticsResourceID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type harvestTraces struct {
|
||||||
|
trace *HarvestTrace
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHarvestTraces() *harvestTraces {
|
||||||
|
return &harvestTraces{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (traces *harvestTraces) Witness(trace HarvestTrace) {
|
||||||
|
if nil == traces.trace || traces.trace.Duration < trace.Duration {
|
||||||
|
cpy := new(HarvestTrace)
|
||||||
|
*cpy = trace
|
||||||
|
traces.trace = cpy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (traces *harvestTraces) Data(agentRunID string, harvestStart time.Time) ([]byte, error) {
|
||||||
|
if nil == traces.trace {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return json.Marshal([]interface{}{
|
||||||
|
agentRunID,
|
||||||
|
[]interface{}{
|
||||||
|
traces.trace,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (traces *harvestTraces) MergeIntoHarvest(h *Harvest) {}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import "net/url"
|
||||||
|
|
||||||
|
// SafeURL removes sensitive information from a URL.
|
||||||
|
func SafeURL(u *url.URL) string {
|
||||||
|
if nil == u {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if "" != u.Opaque {
|
||||||
|
// If the URL is opaque, we cannot be sure if it contains
|
||||||
|
// sensitive information.
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Omit user, query, and fragment information for security.
|
||||||
|
ur := url.URL{
|
||||||
|
Scheme: u.Scheme,
|
||||||
|
Host: u.Host,
|
||||||
|
Path: u.Path,
|
||||||
|
}
|
||||||
|
return ur.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SafeURLFromString removes sensitive information from a URL.
|
||||||
|
func SafeURLFromString(rawurl string) string {
|
||||||
|
u, err := url.Parse(rawurl)
|
||||||
|
if nil != err {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return SafeURL(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HostFromURL returns the URL's host.
|
||||||
|
func HostFromURL(u *url.URL) string {
|
||||||
|
if nil == u {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if "" != u.Opaque {
|
||||||
|
return "opaque"
|
||||||
|
}
|
||||||
|
return u.Host
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSONString assists in logging JSON: Based on the formatter used to log
|
||||||
|
// Context contents, the contents could be marshalled as JSON or just printed
|
||||||
|
// directly.
|
||||||
|
type JSONString string
|
||||||
|
|
||||||
|
// MarshalJSON returns the JSONString unmodified without any escaping.
|
||||||
|
func (js JSONString) MarshalJSON() ([]byte, error) {
|
||||||
|
if "" == js {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return []byte(js), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeFirstSegment(name string) string {
|
||||||
|
idx := strings.Index(name, "/")
|
||||||
|
if -1 == idx {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return name[idx+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeToFloatSeconds(t time.Time) float64 {
|
||||||
|
return float64(t.UnixNano()) / float64(1000*1000*1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeToFloatMilliseconds(t time.Time) float64 {
|
||||||
|
return float64(t.UnixNano()) / float64(1000*1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
func floatSecondsToDuration(seconds float64) time.Duration {
|
||||||
|
nanos := seconds * 1000 * 1000 * 1000
|
||||||
|
return time.Duration(nanos) * time.Nanosecond
|
||||||
|
}
|
||||||
|
|
||||||
|
func absTimeDiff(t1, t2 time.Time) time.Duration {
|
||||||
|
if t1.After(t2) {
|
||||||
|
return t1.Sub(t2)
|
||||||
|
}
|
||||||
|
return t2.Sub(t1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func compactJSON(js []byte) []byte {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
if err := json.Compact(buf, js); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompactJSONString removes the whitespace from a JSON string.
|
||||||
|
func CompactJSONString(js string) string {
|
||||||
|
out := compactJSON([]byte(js))
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringLengthByteLimit truncates strings using a byte-limit boundary and
|
||||||
|
// avoids terminating in the middle of a multibyte character.
|
||||||
|
func StringLengthByteLimit(str string, byteLimit int) string {
|
||||||
|
if len(str) <= byteLimit {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
limitIndex := 0
|
||||||
|
for pos := range str {
|
||||||
|
if pos > byteLimit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
limitIndex = pos
|
||||||
|
}
|
||||||
|
return str[0:limitIndex]
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
package utilization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxResponseLengthBytes = 255
|
||||||
|
|
||||||
|
// AWS data gathering requires making three web requests, therefore this
|
||||||
|
// timeout is in keeping with the spec's total timeout of 1 second.
|
||||||
|
individualConnectionTimeout = 300 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
awsHost = "169.254.169.254"
|
||||||
|
|
||||||
|
typeEndpointPath = "/2008-02-01/meta-data/instance-type"
|
||||||
|
idEndpointPath = "/2008-02-01/meta-data/instance-id"
|
||||||
|
zoneEndpointPath = "/2008-02-01/meta-data/placement/availability-zone"
|
||||||
|
|
||||||
|
typeEndpoint = "http://" + awsHost + typeEndpointPath
|
||||||
|
idEndpoint = "http://" + awsHost + idEndpointPath
|
||||||
|
zoneEndpoint = "http://" + awsHost + zoneEndpointPath
|
||||||
|
)
|
||||||
|
|
||||||
|
// awsValidationError represents a response from an AWS endpoint that doesn't
|
||||||
|
// match the format expectations.
|
||||||
|
type awsValidationError struct {
|
||||||
|
e error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a awsValidationError) Error() string {
|
||||||
|
return a.e.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAWSValidationError(e error) bool {
|
||||||
|
_, is := e.(awsValidationError)
|
||||||
|
return is
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAWS() (*vendor, error) {
|
||||||
|
return getEndpoints(&http.Client{
|
||||||
|
Timeout: individualConnectionTimeout,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEndpoints(client *http.Client) (*vendor, error) {
|
||||||
|
v := &vendor{}
|
||||||
|
var err error
|
||||||
|
|
||||||
|
v.ID, err = getAndValidate(client, idEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
v.Type, err = getAndValidate(client, typeEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
v.Zone, err = getAndValidate(client, zoneEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAndValidate(client *http.Client, endpoint string) (string, error) {
|
||||||
|
response, err := client.Get(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
if response.StatusCode != 200 {
|
||||||
|
return "", fmt.Errorf("unexpected response code %d", response.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := make([]byte, maxResponseLengthBytes+1)
|
||||||
|
num, err := response.Body.Read(b)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if num > maxResponseLengthBytes {
|
||||||
|
return "", awsValidationError{
|
||||||
|
fmt.Errorf("maximum length %d exceeded", maxResponseLengthBytes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseText := string(b[:num])
|
||||||
|
|
||||||
|
for _, r := range responseText {
|
||||||
|
if !isAcceptableRune(r) {
|
||||||
|
return "", awsValidationError{
|
||||||
|
fmt.Errorf("invalid character %x", r),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseText, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// See:
|
||||||
|
// https://source.datanerd.us/agents/agent-specs/blob/master/Utilization.md#normalizing-aws-data
|
||||||
|
func isAcceptableRune(r rune) bool {
|
||||||
|
switch r {
|
||||||
|
case 0xFFFD:
|
||||||
|
return false
|
||||||
|
case '_', ' ', '/', '.', '-':
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return r > 0x7f ||
|
||||||
|
('0' <= r && r <= '9') ||
|
||||||
|
('a' <= r && r <= 'z') ||
|
||||||
|
('A' <= r && r <= 'Z')
|
||||||
|
}
|
||||||
|
}
|
140
vendor/github.com/newrelic/go-agent/internal/utilization/utilization.go
generated
vendored
Normal file
140
vendor/github.com/newrelic/go-agent/internal/utilization/utilization.go
generated
vendored
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
// Package utilization implements the Utilization spec, available at
|
||||||
|
// https://source.datanerd.us/agents/agent-specs/blob/master/Utilization.md
|
||||||
|
package utilization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/newrelic/go-agent/internal/logger"
|
||||||
|
"github.com/newrelic/go-agent/internal/sysinfo"
|
||||||
|
)
|
||||||
|
|
||||||
|
const metadataVersion = 2
|
||||||
|
|
||||||
|
// Config controls the behavior of utilization information capture.
|
||||||
|
type Config struct {
|
||||||
|
DetectAWS bool
|
||||||
|
DetectDocker bool
|
||||||
|
LogicalProcessors int
|
||||||
|
TotalRAMMIB int
|
||||||
|
BillingHostname string
|
||||||
|
}
|
||||||
|
|
||||||
|
type override struct {
|
||||||
|
LogicalProcessors *int `json:"logical_processors,omitempty"`
|
||||||
|
TotalRAMMIB *int `json:"total_ram_mib,omitempty"`
|
||||||
|
BillingHostname string `json:"hostname,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data contains utilization system information.
|
||||||
|
type Data struct {
|
||||||
|
MetadataVersion int `json:"metadata_version"`
|
||||||
|
LogicalProcessors int `json:"logical_processors"`
|
||||||
|
RAMMib *uint64 `json:"total_ram_mib"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
Vendors *vendors `json:"vendors,omitempty"`
|
||||||
|
Config *override `json:"config,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
sampleRAMMib = uint64(1024)
|
||||||
|
// SampleData contains sample utilization data useful for testing.
|
||||||
|
SampleData = Data{
|
||||||
|
MetadataVersion: metadataVersion,
|
||||||
|
LogicalProcessors: 16,
|
||||||
|
RAMMib: &sampleRAMMib,
|
||||||
|
Hostname: "my-hostname",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type vendor struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Zone string `json:"zone,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type vendors struct {
|
||||||
|
AWS *vendor `json:"aws,omitempty"`
|
||||||
|
Docker *vendor `json:"docker,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func overrideFromConfig(config Config) *override {
|
||||||
|
ov := &override{}
|
||||||
|
|
||||||
|
if 0 != config.LogicalProcessors {
|
||||||
|
x := config.LogicalProcessors
|
||||||
|
ov.LogicalProcessors = &x
|
||||||
|
}
|
||||||
|
if 0 != config.TotalRAMMIB {
|
||||||
|
x := config.TotalRAMMIB
|
||||||
|
ov.TotalRAMMIB = &x
|
||||||
|
}
|
||||||
|
ov.BillingHostname = config.BillingHostname
|
||||||
|
|
||||||
|
if "" == ov.BillingHostname &&
|
||||||
|
nil == ov.LogicalProcessors &&
|
||||||
|
nil == ov.TotalRAMMIB {
|
||||||
|
ov = nil
|
||||||
|
}
|
||||||
|
return ov
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gather gathers system utilization data.
|
||||||
|
func Gather(config Config, lg logger.Logger) *Data {
|
||||||
|
uDat := Data{
|
||||||
|
MetadataVersion: metadataVersion,
|
||||||
|
Vendors: &vendors{},
|
||||||
|
LogicalProcessors: runtime.NumCPU(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DetectDocker {
|
||||||
|
id, err := sysinfo.DockerID()
|
||||||
|
if err != nil &&
|
||||||
|
err != sysinfo.ErrDockerUnsupported &&
|
||||||
|
err != sysinfo.ErrDockerNotFound {
|
||||||
|
lg.Warn("error gathering Docker information", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
} else if id != "" {
|
||||||
|
uDat.Vendors.Docker = &vendor{ID: id}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DetectAWS {
|
||||||
|
aws, err := getAWS()
|
||||||
|
if nil == err {
|
||||||
|
uDat.Vendors.AWS = aws
|
||||||
|
} else if isAWSValidationError(err) {
|
||||||
|
lg.Warn("AWS validation error", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if uDat.Vendors.AWS == nil && uDat.Vendors.Docker == nil {
|
||||||
|
uDat.Vendors = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
host, err := sysinfo.Hostname()
|
||||||
|
if nil == err {
|
||||||
|
uDat.Hostname = host
|
||||||
|
} else {
|
||||||
|
lg.Warn("error getting hostname", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
bts, err := sysinfo.PhysicalMemoryBytes()
|
||||||
|
if nil == err {
|
||||||
|
mib := sysinfo.BytesToMebibytes(bts)
|
||||||
|
uDat.RAMMib = &mib
|
||||||
|
} else {
|
||||||
|
lg.Warn("error getting memory", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
uDat.Config = overrideFromConfig(config)
|
||||||
|
|
||||||
|
return &uDat
|
||||||
|
}
|
|
@ -0,0 +1,566 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/newrelic/go-agent/internal"
|
||||||
|
"github.com/newrelic/go-agent/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
debugLogging = os.Getenv("NEW_RELIC_DEBUG_LOGGING")
|
||||||
|
redirectHost = func() string {
|
||||||
|
if s := os.Getenv("NEW_RELIC_HOST"); "" != s {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "collector.newrelic.com"
|
||||||
|
}()
|
||||||
|
)
|
||||||
|
|
||||||
|
type dataConsumer interface {
|
||||||
|
Consume(internal.AgentRunID, internal.Harvestable)
|
||||||
|
}
|
||||||
|
|
||||||
|
type appData struct {
|
||||||
|
id internal.AgentRunID
|
||||||
|
data internal.Harvestable
|
||||||
|
}
|
||||||
|
|
||||||
|
type app struct {
|
||||||
|
config Config
|
||||||
|
attrConfig *internal.AttributeConfig
|
||||||
|
rpmControls internal.RpmControls
|
||||||
|
testHarvest *internal.Harvest
|
||||||
|
|
||||||
|
// initiateShutdown is used to tell the processor to shutdown.
|
||||||
|
initiateShutdown chan struct{}
|
||||||
|
|
||||||
|
// shutdownStarted and shutdownComplete are closed by the processor
|
||||||
|
// goroutine to indicate the shutdown status. Two channels are used so
|
||||||
|
// that the call of app.Shutdown() can block until shutdown has
|
||||||
|
// completed but other goroutines can exit when shutdown has started.
|
||||||
|
// This is not just an optimization: This prevents a deadlock if
|
||||||
|
// harvesting data during the shutdown fails and an attempt is made to
|
||||||
|
// merge the data into the next harvest.
|
||||||
|
shutdownStarted chan struct{}
|
||||||
|
shutdownComplete chan struct{}
|
||||||
|
|
||||||
|
// Sends to these channels should not occur without a <-shutdownStarted
|
||||||
|
// select option to prevent deadlock.
|
||||||
|
dataChan chan appData
|
||||||
|
collectorErrorChan chan error
|
||||||
|
connectChan chan *internal.AppRun
|
||||||
|
|
||||||
|
harvestTicker *time.Ticker
|
||||||
|
|
||||||
|
// This mutex protects both `run` and `err`, both of which should only
|
||||||
|
// be accessed using getState and setState.
|
||||||
|
sync.RWMutex
|
||||||
|
// run is non-nil when the app is successfully connected. It is
|
||||||
|
// immutable.
|
||||||
|
run *internal.AppRun
|
||||||
|
// err is non-nil if the application will never be connected again
|
||||||
|
// (disconnect, license exception, shutdown).
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
placeholderRun = &internal.AppRun{
|
||||||
|
ConnectReply: internal.ConnectReplyDefaults(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func isFatalHarvestError(e error) bool {
|
||||||
|
return internal.IsDisconnect(e) ||
|
||||||
|
internal.IsLicenseException(e) ||
|
||||||
|
internal.IsRestartException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldSaveFailedHarvest(e error) bool {
|
||||||
|
if e == internal.ErrPayloadTooLarge || e == internal.ErrUnsupportedMedia {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *app) doHarvest(h *internal.Harvest, harvestStart time.Time, run *internal.AppRun) {
|
||||||
|
h.CreateFinalMetrics()
|
||||||
|
h.Metrics = h.Metrics.ApplyRules(run.MetricRules)
|
||||||
|
|
||||||
|
payloads := h.Payloads()
|
||||||
|
for cmd, p := range payloads {
|
||||||
|
|
||||||
|
data, err := p.Data(run.RunID.String(), harvestStart)
|
||||||
|
|
||||||
|
if nil == data && nil == err {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if nil == err {
|
||||||
|
call := internal.RpmCmd{
|
||||||
|
Collector: run.Collector,
|
||||||
|
RunID: run.RunID.String(),
|
||||||
|
Name: cmd,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The reply from harvest calls is always unused.
|
||||||
|
_, err = internal.CollectorRequest(call, app.rpmControls)
|
||||||
|
}
|
||||||
|
|
||||||
|
if nil == err {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFatalHarvestError(err) {
|
||||||
|
select {
|
||||||
|
case app.collectorErrorChan <- err:
|
||||||
|
case <-app.shutdownStarted:
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.config.Logger.Warn("harvest failure", map[string]interface{}{
|
||||||
|
"cmd": cmd,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if shouldSaveFailedHarvest(err) {
|
||||||
|
app.Consume(run.RunID, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectAttempt(app *app) (*internal.AppRun, error) {
|
||||||
|
js, e := configConnectJSON(app.config)
|
||||||
|
if nil != e {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
return internal.ConnectAttempt(js, redirectHost, app.rpmControls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *app) connectRoutine() {
|
||||||
|
for {
|
||||||
|
run, err := connectAttempt(app)
|
||||||
|
if nil == err {
|
||||||
|
select {
|
||||||
|
case app.connectChan <- run:
|
||||||
|
case <-app.shutdownStarted:
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if internal.IsDisconnect(err) || internal.IsLicenseException(err) {
|
||||||
|
select {
|
||||||
|
case app.collectorErrorChan <- err:
|
||||||
|
case <-app.shutdownStarted:
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.config.Logger.Warn("application connect failure", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
|
||||||
|
time.Sleep(internal.ConnectBackoff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func debug(data internal.Harvestable, lg Logger) {
|
||||||
|
now := time.Now()
|
||||||
|
h := internal.NewHarvest(now)
|
||||||
|
data.MergeIntoHarvest(h)
|
||||||
|
ps := h.Payloads()
|
||||||
|
for cmd, p := range ps {
|
||||||
|
d, err := p.Data("agent run id", now)
|
||||||
|
if nil == d && nil == err {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if nil != err {
|
||||||
|
lg.Debug("integration", map[string]interface{}{
|
||||||
|
"cmd": cmd,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lg.Debug("integration", map[string]interface{}{
|
||||||
|
"cmd": cmd,
|
||||||
|
"data": internal.JSONString(d),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processConnectMessages(run *internal.AppRun, lg Logger) {
|
||||||
|
for _, msg := range run.Messages {
|
||||||
|
event := "collector message"
|
||||||
|
cn := map[string]interface{}{"msg": msg.Message}
|
||||||
|
|
||||||
|
switch strings.ToLower(msg.Level) {
|
||||||
|
case "error":
|
||||||
|
lg.Error(event, cn)
|
||||||
|
case "warn":
|
||||||
|
lg.Warn(event, cn)
|
||||||
|
case "info":
|
||||||
|
lg.Info(event, cn)
|
||||||
|
case "debug", "verbose":
|
||||||
|
lg.Debug(event, cn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *app) process() {
|
||||||
|
// Both the harvest and the run are non-nil when the app is connected,
|
||||||
|
// and nil otherwise.
|
||||||
|
var h *internal.Harvest
|
||||||
|
var run *internal.AppRun
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-app.harvestTicker.C:
|
||||||
|
if nil != run {
|
||||||
|
now := time.Now()
|
||||||
|
go app.doHarvest(h, now, run)
|
||||||
|
h = internal.NewHarvest(now)
|
||||||
|
}
|
||||||
|
case d := <-app.dataChan:
|
||||||
|
if nil != run && run.RunID == d.id {
|
||||||
|
d.data.MergeIntoHarvest(h)
|
||||||
|
}
|
||||||
|
case <-app.initiateShutdown:
|
||||||
|
close(app.shutdownStarted)
|
||||||
|
|
||||||
|
// Remove the run before merging any final data to
|
||||||
|
// ensure a bounded number of receives from dataChan.
|
||||||
|
app.setState(nil, errors.New("application shut down"))
|
||||||
|
app.harvestTicker.Stop()
|
||||||
|
|
||||||
|
if nil != run {
|
||||||
|
for done := false; !done; {
|
||||||
|
select {
|
||||||
|
case d := <-app.dataChan:
|
||||||
|
if run.RunID == d.id {
|
||||||
|
d.data.MergeIntoHarvest(h)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.doHarvest(h, time.Now(), run)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(app.shutdownComplete)
|
||||||
|
return
|
||||||
|
case err := <-app.collectorErrorChan:
|
||||||
|
run = nil
|
||||||
|
h = nil
|
||||||
|
app.setState(nil, nil)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case internal.IsDisconnect(err):
|
||||||
|
app.setState(nil, err)
|
||||||
|
app.config.Logger.Error("application disconnected by New Relic", map[string]interface{}{
|
||||||
|
"app": app.config.AppName,
|
||||||
|
})
|
||||||
|
case internal.IsLicenseException(err):
|
||||||
|
app.setState(nil, err)
|
||||||
|
app.config.Logger.Error("invalid license", map[string]interface{}{
|
||||||
|
"app": app.config.AppName,
|
||||||
|
"license": app.config.License,
|
||||||
|
})
|
||||||
|
case internal.IsRestartException(err):
|
||||||
|
app.config.Logger.Info("application restarted", map[string]interface{}{
|
||||||
|
"app": app.config.AppName,
|
||||||
|
})
|
||||||
|
go app.connectRoutine()
|
||||||
|
}
|
||||||
|
case run = <-app.connectChan:
|
||||||
|
h = internal.NewHarvest(time.Now())
|
||||||
|
app.setState(run, nil)
|
||||||
|
|
||||||
|
app.config.Logger.Info("application connected", map[string]interface{}{
|
||||||
|
"app": app.config.AppName,
|
||||||
|
"run": run.RunID.String(),
|
||||||
|
})
|
||||||
|
processConnectMessages(run, app.config.Logger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *app) Shutdown(timeout time.Duration) {
|
||||||
|
if !app.config.Enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case app.initiateShutdown <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block until shutdown is done or timeout occurs.
|
||||||
|
t := time.NewTimer(timeout)
|
||||||
|
select {
|
||||||
|
case <-app.shutdownComplete:
|
||||||
|
case <-t.C:
|
||||||
|
}
|
||||||
|
t.Stop()
|
||||||
|
|
||||||
|
app.config.Logger.Info("application shutdown", map[string]interface{}{
|
||||||
|
"app": app.config.AppName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAttributeDestinationConfig(c AttributeDestinationConfig) internal.AttributeDestinationConfig {
|
||||||
|
return internal.AttributeDestinationConfig{
|
||||||
|
Enabled: c.Enabled,
|
||||||
|
Include: c.Include,
|
||||||
|
Exclude: c.Exclude,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSampler(app *app, period time.Duration) {
|
||||||
|
previous := internal.GetSample(time.Now(), app.config.Logger)
|
||||||
|
t := time.NewTicker(period)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case now := <-t.C:
|
||||||
|
current := internal.GetSample(now, app.config.Logger)
|
||||||
|
run, _ := app.getState()
|
||||||
|
app.Consume(run.RunID, internal.GetStats(internal.Samples{
|
||||||
|
Previous: previous,
|
||||||
|
Current: current,
|
||||||
|
}))
|
||||||
|
previous = current
|
||||||
|
case <-app.shutdownStarted:
|
||||||
|
t.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *app) WaitForConnection(timeout time.Duration) error {
|
||||||
|
if !app.config.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
pollPeriod := 50 * time.Millisecond
|
||||||
|
|
||||||
|
for {
|
||||||
|
run, err := app.getState()
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if run.RunID != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
return fmt.Errorf("timeout out after %s", timeout.String())
|
||||||
|
}
|
||||||
|
time.Sleep(pollPeriod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newApp(c Config) (Application, error) {
|
||||||
|
c = copyConfigReferenceFields(c)
|
||||||
|
if err := c.Validate(); nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if nil == c.Logger {
|
||||||
|
c.Logger = logger.ShimLogger{}
|
||||||
|
}
|
||||||
|
app := &app{
|
||||||
|
config: c,
|
||||||
|
attrConfig: internal.CreateAttributeConfig(internal.AttributeConfigInput{
|
||||||
|
Attributes: convertAttributeDestinationConfig(c.Attributes),
|
||||||
|
ErrorCollector: convertAttributeDestinationConfig(c.ErrorCollector.Attributes),
|
||||||
|
TransactionEvents: convertAttributeDestinationConfig(c.TransactionEvents.Attributes),
|
||||||
|
TransactionTracer: convertAttributeDestinationConfig(c.TransactionTracer.Attributes),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// This channel must be buffered since Shutdown makes a
|
||||||
|
// non-blocking send attempt.
|
||||||
|
initiateShutdown: make(chan struct{}, 1),
|
||||||
|
|
||||||
|
shutdownStarted: make(chan struct{}),
|
||||||
|
shutdownComplete: make(chan struct{}),
|
||||||
|
connectChan: make(chan *internal.AppRun, 1),
|
||||||
|
collectorErrorChan: make(chan error, 1),
|
||||||
|
dataChan: make(chan appData, internal.AppDataChanSize),
|
||||||
|
rpmControls: internal.RpmControls{
|
||||||
|
UseTLS: c.UseTLS,
|
||||||
|
License: c.License,
|
||||||
|
Client: &http.Client{
|
||||||
|
Transport: c.Transport,
|
||||||
|
Timeout: internal.CollectorTimeout,
|
||||||
|
},
|
||||||
|
Logger: c.Logger,
|
||||||
|
AgentVersion: Version,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.config.Logger.Info("application created", map[string]interface{}{
|
||||||
|
"app": app.config.AppName,
|
||||||
|
"version": Version,
|
||||||
|
"enabled": app.config.Enabled,
|
||||||
|
})
|
||||||
|
|
||||||
|
if !app.config.Enabled {
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
app.harvestTicker = time.NewTicker(internal.HarvestPeriod)
|
||||||
|
|
||||||
|
go app.process()
|
||||||
|
go app.connectRoutine()
|
||||||
|
|
||||||
|
if app.config.RuntimeSampler.Enabled {
|
||||||
|
go runSampler(app, internal.RuntimeSamplerPeriod)
|
||||||
|
}
|
||||||
|
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type expectApp interface {
|
||||||
|
internal.Expect
|
||||||
|
Application
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestApp(replyfn func(*internal.ConnectReply), cfg Config) (expectApp, error) {
|
||||||
|
cfg.Enabled = false
|
||||||
|
application, err := newApp(cfg)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
app := application.(*app)
|
||||||
|
if nil != replyfn {
|
||||||
|
reply := internal.ConnectReplyDefaults()
|
||||||
|
replyfn(reply)
|
||||||
|
app.setState(&internal.AppRun{ConnectReply: reply}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.testHarvest = internal.NewHarvest(time.Now())
|
||||||
|
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *app) getState() (*internal.AppRun, error) {
|
||||||
|
app.RLock()
|
||||||
|
defer app.RUnlock()
|
||||||
|
|
||||||
|
run := app.run
|
||||||
|
if nil == run {
|
||||||
|
run = placeholderRun
|
||||||
|
}
|
||||||
|
return run, app.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *app) setState(run *internal.AppRun, err error) {
|
||||||
|
app.Lock()
|
||||||
|
defer app.Unlock()
|
||||||
|
|
||||||
|
app.run = run
|
||||||
|
app.err = err
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartTransaction implements newrelic.Application's StartTransaction.
|
||||||
|
func (app *app) StartTransaction(name string, w http.ResponseWriter, r *http.Request) Transaction {
|
||||||
|
run, _ := app.getState()
|
||||||
|
return upgradeTxn(newTxn(txnInput{
|
||||||
|
Config: app.config,
|
||||||
|
Reply: run.ConnectReply,
|
||||||
|
Request: r,
|
||||||
|
W: w,
|
||||||
|
Consumer: app,
|
||||||
|
attrConfig: app.attrConfig,
|
||||||
|
}, name))
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errHighSecurityEnabled = errors.New("high security enabled")
|
||||||
|
errCustomEventsDisabled = errors.New("custom events disabled")
|
||||||
|
errCustomEventsRemoteDisabled = errors.New("custom events disabled by server")
|
||||||
|
)
|
||||||
|
|
||||||
|
// RecordCustomEvent implements newrelic.Application's RecordCustomEvent.
|
||||||
|
func (app *app) RecordCustomEvent(eventType string, params map[string]interface{}) error {
|
||||||
|
if app.config.HighSecurity {
|
||||||
|
return errHighSecurityEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if !app.config.CustomInsightsEvents.Enabled {
|
||||||
|
return errCustomEventsDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
event, e := internal.CreateCustomEvent(eventType, params, time.Now())
|
||||||
|
if nil != e {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
run, _ := app.getState()
|
||||||
|
if !run.CollectCustomEvents {
|
||||||
|
return errCustomEventsRemoteDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Consume(run.RunID, event)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *app) Consume(id internal.AgentRunID, data internal.Harvestable) {
|
||||||
|
if "" != debugLogging {
|
||||||
|
debug(data, app.config.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
if nil != app.testHarvest {
|
||||||
|
data.MergeIntoHarvest(app.testHarvest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if "" == id {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case app.dataChan <- appData{id, data}:
|
||||||
|
case <-app.shutdownStarted:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *app) ExpectCustomEvents(t internal.Validator, want []internal.WantCustomEvent) {
|
||||||
|
internal.ExpectCustomEvents(internal.ExtendValidator(t, "custom events"), app.testHarvest.CustomEvents, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *app) ExpectErrors(t internal.Validator, want []internal.WantError) {
|
||||||
|
t = internal.ExtendValidator(t, "traced errors")
|
||||||
|
internal.ExpectErrors(t, app.testHarvest.ErrorTraces, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *app) ExpectErrorEvents(t internal.Validator, want []internal.WantErrorEvent) {
|
||||||
|
t = internal.ExtendValidator(t, "error events")
|
||||||
|
internal.ExpectErrorEvents(t, app.testHarvest.ErrorEvents, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *app) ExpectTxnEvents(t internal.Validator, want []internal.WantTxnEvent) {
|
||||||
|
t = internal.ExtendValidator(t, "txn events")
|
||||||
|
internal.ExpectTxnEvents(t, app.testHarvest.TxnEvents, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *app) ExpectMetrics(t internal.Validator, want []internal.WantMetric) {
|
||||||
|
t = internal.ExtendValidator(t, "metrics")
|
||||||
|
internal.ExpectMetrics(t, app.testHarvest.Metrics, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *app) ExpectTxnTraces(t internal.Validator, want []internal.WantTxnTrace) {
|
||||||
|
t = internal.ExtendValidator(t, "txn traces")
|
||||||
|
internal.ExpectTxnTraces(t, app.testHarvest.TxnTraces, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *app) ExpectSlowQueries(t internal.Validator, want []internal.WantSlowQuery) {
|
||||||
|
t = internal.ExtendValidator(t, "slow queries")
|
||||||
|
internal.ExpectSlowQueries(t, app.testHarvest.SlowSQLs, want)
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/newrelic/go-agent/internal"
|
||||||
|
"github.com/newrelic/go-agent/internal/logger"
|
||||||
|
"github.com/newrelic/go-agent/internal/utilization"
|
||||||
|
)
|
||||||
|
|
||||||
|
func copyDestConfig(c AttributeDestinationConfig) AttributeDestinationConfig {
|
||||||
|
cp := c
|
||||||
|
if nil != c.Include {
|
||||||
|
cp.Include = make([]string, len(c.Include))
|
||||||
|
copy(cp.Include, c.Include)
|
||||||
|
}
|
||||||
|
if nil != c.Exclude {
|
||||||
|
cp.Exclude = make([]string, len(c.Exclude))
|
||||||
|
copy(cp.Exclude, c.Exclude)
|
||||||
|
}
|
||||||
|
return cp
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyConfigReferenceFields(cfg Config) Config {
|
||||||
|
cp := cfg
|
||||||
|
if nil != cfg.Labels {
|
||||||
|
cp.Labels = make(map[string]string, len(cfg.Labels))
|
||||||
|
for key, val := range cfg.Labels {
|
||||||
|
cp.Labels[key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nil != cfg.ErrorCollector.IgnoreStatusCodes {
|
||||||
|
ignored := make([]int, len(cfg.ErrorCollector.IgnoreStatusCodes))
|
||||||
|
copy(ignored, cfg.ErrorCollector.IgnoreStatusCodes)
|
||||||
|
cp.ErrorCollector.IgnoreStatusCodes = ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
cp.Attributes = copyDestConfig(cfg.Attributes)
|
||||||
|
cp.ErrorCollector.Attributes = copyDestConfig(cfg.ErrorCollector.Attributes)
|
||||||
|
cp.TransactionEvents.Attributes = copyDestConfig(cfg.TransactionEvents.Attributes)
|
||||||
|
cp.TransactionTracer.Attributes = copyDestConfig(cfg.TransactionTracer.Attributes)
|
||||||
|
|
||||||
|
return cp
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
agentLanguage = "go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func transportSetting(t http.RoundTripper) interface{} {
|
||||||
|
if nil == t {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%T", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loggerSetting(lg Logger) interface{} {
|
||||||
|
if nil == lg {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, ok := lg.(logger.ShimLogger); ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%T", lg)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// https://source.datanerd.us/agents/agent-specs/blob/master/Custom-Host-Names.md
|
||||||
|
hostByteLimit = 255
|
||||||
|
)
|
||||||
|
|
||||||
|
type settings Config
|
||||||
|
|
||||||
|
func (s settings) MarshalJSON() ([]byte, error) {
|
||||||
|
c := Config(s)
|
||||||
|
transport := c.Transport
|
||||||
|
c.Transport = nil
|
||||||
|
logger := c.Logger
|
||||||
|
c.Logger = nil
|
||||||
|
|
||||||
|
js, err := json.Marshal(c)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fields := make(map[string]interface{})
|
||||||
|
err = json.Unmarshal(js, &fields)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// The License field is not simply ignored by adding the `json:"-"` tag
|
||||||
|
// to it since we want to allow consumers to populate Config from JSON.
|
||||||
|
delete(fields, `License`)
|
||||||
|
fields[`Transport`] = transportSetting(transport)
|
||||||
|
fields[`Logger`] = loggerSetting(logger)
|
||||||
|
return json.Marshal(fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
func configConnectJSONInternal(c Config, pid int, util *utilization.Data, e internal.Environment, version string) ([]byte, error) {
|
||||||
|
return json.Marshal([]interface{}{struct {
|
||||||
|
Pid int `json:"pid"`
|
||||||
|
Language string `json:"language"`
|
||||||
|
Version string `json:"agent_version"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
HostDisplayName string `json:"display_host,omitempty"`
|
||||||
|
Settings interface{} `json:"settings"`
|
||||||
|
AppName []string `json:"app_name"`
|
||||||
|
HighSecurity bool `json:"high_security"`
|
||||||
|
Labels internal.Labels `json:"labels,omitempty"`
|
||||||
|
Environment internal.Environment `json:"environment"`
|
||||||
|
Identifier string `json:"identifier"`
|
||||||
|
Util *utilization.Data `json:"utilization"`
|
||||||
|
}{
|
||||||
|
Pid: pid,
|
||||||
|
Language: agentLanguage,
|
||||||
|
Version: version,
|
||||||
|
Host: internal.StringLengthByteLimit(util.Hostname, hostByteLimit),
|
||||||
|
HostDisplayName: internal.StringLengthByteLimit(c.HostDisplayName, hostByteLimit),
|
||||||
|
Settings: (settings)(c),
|
||||||
|
AppName: strings.Split(c.AppName, ";"),
|
||||||
|
HighSecurity: c.HighSecurity,
|
||||||
|
Labels: internal.Labels(c.Labels),
|
||||||
|
Environment: e,
|
||||||
|
// This identifier field is provided to avoid:
|
||||||
|
// https://newrelic.atlassian.net/browse/DSCORE-778
|
||||||
|
//
|
||||||
|
// This identifier is used by the collector to look up the real
|
||||||
|
// agent. If an identifier isn't provided, the collector will
|
||||||
|
// create its own based on the first appname, which prevents a
|
||||||
|
// single daemon from connecting "a;b" and "a;c" at the same
|
||||||
|
// time.
|
||||||
|
//
|
||||||
|
// Providing the identifier below works around this issue and
|
||||||
|
// allows users more flexibility in using application rollups.
|
||||||
|
Identifier: c.AppName,
|
||||||
|
Util: util,
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
|
||||||
|
func configConnectJSON(c Config) ([]byte, error) {
|
||||||
|
env := internal.NewEnvironment()
|
||||||
|
util := utilization.Gather(utilization.Config{
|
||||||
|
DetectAWS: c.Utilization.DetectAWS,
|
||||||
|
DetectDocker: c.Utilization.DetectDocker,
|
||||||
|
LogicalProcessors: c.Utilization.LogicalProcessors,
|
||||||
|
TotalRAMMIB: c.Utilization.TotalRAMMIB,
|
||||||
|
BillingHostname: c.Utilization.BillingHostname,
|
||||||
|
}, c.Logger)
|
||||||
|
return configConnectJSONInternal(c, os.Getpid(), util, env, Version)
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hasC = 1 << iota // CloseNotifier
|
||||||
|
hasF // Flusher
|
||||||
|
hasH // Hijacker
|
||||||
|
hasR // ReaderFrom
|
||||||
|
)
|
||||||
|
|
||||||
|
type wrap struct{ *txn }
|
||||||
|
type wrapR struct{ *txn }
|
||||||
|
type wrapH struct{ *txn }
|
||||||
|
type wrapHR struct{ *txn }
|
||||||
|
type wrapF struct{ *txn }
|
||||||
|
type wrapFR struct{ *txn }
|
||||||
|
type wrapFH struct{ *txn }
|
||||||
|
type wrapFHR struct{ *txn }
|
||||||
|
type wrapC struct{ *txn }
|
||||||
|
type wrapCR struct{ *txn }
|
||||||
|
type wrapCH struct{ *txn }
|
||||||
|
type wrapCHR struct{ *txn }
|
||||||
|
type wrapCF struct{ *txn }
|
||||||
|
type wrapCFR struct{ *txn }
|
||||||
|
type wrapCFH struct{ *txn }
|
||||||
|
type wrapCFHR struct{ *txn }
|
||||||
|
|
||||||
|
func (x wrapC) CloseNotify() <-chan bool { return x.W.(http.CloseNotifier).CloseNotify() }
|
||||||
|
func (x wrapCR) CloseNotify() <-chan bool { return x.W.(http.CloseNotifier).CloseNotify() }
|
||||||
|
func (x wrapCH) CloseNotify() <-chan bool { return x.W.(http.CloseNotifier).CloseNotify() }
|
||||||
|
func (x wrapCHR) CloseNotify() <-chan bool { return x.W.(http.CloseNotifier).CloseNotify() }
|
||||||
|
func (x wrapCF) CloseNotify() <-chan bool { return x.W.(http.CloseNotifier).CloseNotify() }
|
||||||
|
func (x wrapCFR) CloseNotify() <-chan bool { return x.W.(http.CloseNotifier).CloseNotify() }
|
||||||
|
func (x wrapCFH) CloseNotify() <-chan bool { return x.W.(http.CloseNotifier).CloseNotify() }
|
||||||
|
func (x wrapCFHR) CloseNotify() <-chan bool { return x.W.(http.CloseNotifier).CloseNotify() }
|
||||||
|
|
||||||
|
func (x wrapF) Flush() { x.W.(http.Flusher).Flush() }
|
||||||
|
func (x wrapFR) Flush() { x.W.(http.Flusher).Flush() }
|
||||||
|
func (x wrapFH) Flush() { x.W.(http.Flusher).Flush() }
|
||||||
|
func (x wrapFHR) Flush() { x.W.(http.Flusher).Flush() }
|
||||||
|
func (x wrapCF) Flush() { x.W.(http.Flusher).Flush() }
|
||||||
|
func (x wrapCFR) Flush() { x.W.(http.Flusher).Flush() }
|
||||||
|
func (x wrapCFH) Flush() { x.W.(http.Flusher).Flush() }
|
||||||
|
func (x wrapCFHR) Flush() { x.W.(http.Flusher).Flush() }
|
||||||
|
|
||||||
|
func (x wrapH) Hijack() (net.Conn, *bufio.ReadWriter, error) { return x.W.(http.Hijacker).Hijack() }
|
||||||
|
func (x wrapHR) Hijack() (net.Conn, *bufio.ReadWriter, error) { return x.W.(http.Hijacker).Hijack() }
|
||||||
|
func (x wrapFH) Hijack() (net.Conn, *bufio.ReadWriter, error) { return x.W.(http.Hijacker).Hijack() }
|
||||||
|
func (x wrapFHR) Hijack() (net.Conn, *bufio.ReadWriter, error) { return x.W.(http.Hijacker).Hijack() }
|
||||||
|
func (x wrapCH) Hijack() (net.Conn, *bufio.ReadWriter, error) { return x.W.(http.Hijacker).Hijack() }
|
||||||
|
func (x wrapCHR) Hijack() (net.Conn, *bufio.ReadWriter, error) { return x.W.(http.Hijacker).Hijack() }
|
||||||
|
func (x wrapCFH) Hijack() (net.Conn, *bufio.ReadWriter, error) { return x.W.(http.Hijacker).Hijack() }
|
||||||
|
func (x wrapCFHR) Hijack() (net.Conn, *bufio.ReadWriter, error) { return x.W.(http.Hijacker).Hijack() }
|
||||||
|
|
||||||
|
func (x wrapR) ReadFrom(r io.Reader) (int64, error) { return x.W.(io.ReaderFrom).ReadFrom(r) }
|
||||||
|
func (x wrapHR) ReadFrom(r io.Reader) (int64, error) { return x.W.(io.ReaderFrom).ReadFrom(r) }
|
||||||
|
func (x wrapFR) ReadFrom(r io.Reader) (int64, error) { return x.W.(io.ReaderFrom).ReadFrom(r) }
|
||||||
|
func (x wrapFHR) ReadFrom(r io.Reader) (int64, error) { return x.W.(io.ReaderFrom).ReadFrom(r) }
|
||||||
|
func (x wrapCR) ReadFrom(r io.Reader) (int64, error) { return x.W.(io.ReaderFrom).ReadFrom(r) }
|
||||||
|
func (x wrapCHR) ReadFrom(r io.Reader) (int64, error) { return x.W.(io.ReaderFrom).ReadFrom(r) }
|
||||||
|
func (x wrapCFR) ReadFrom(r io.Reader) (int64, error) { return x.W.(io.ReaderFrom).ReadFrom(r) }
|
||||||
|
func (x wrapCFHR) ReadFrom(r io.Reader) (int64, error) { return x.W.(io.ReaderFrom).ReadFrom(r) }
|
||||||
|
|
||||||
|
func upgradeTxn(txn *txn) Transaction {
|
||||||
|
x := 0
|
||||||
|
if _, ok := txn.W.(http.CloseNotifier); ok {
|
||||||
|
x |= hasC
|
||||||
|
}
|
||||||
|
if _, ok := txn.W.(http.Flusher); ok {
|
||||||
|
x |= hasF
|
||||||
|
}
|
||||||
|
if _, ok := txn.W.(http.Hijacker); ok {
|
||||||
|
x |= hasH
|
||||||
|
}
|
||||||
|
if _, ok := txn.W.(io.ReaderFrom); ok {
|
||||||
|
x |= hasR
|
||||||
|
}
|
||||||
|
|
||||||
|
switch x {
|
||||||
|
default:
|
||||||
|
// Wrap the transaction even when there are no methods needed to
|
||||||
|
// ensure consistent error stack trace depth.
|
||||||
|
return wrap{txn}
|
||||||
|
case hasR:
|
||||||
|
return wrapR{txn}
|
||||||
|
case hasH:
|
||||||
|
return wrapH{txn}
|
||||||
|
case hasH | hasR:
|
||||||
|
return wrapHR{txn}
|
||||||
|
case hasF:
|
||||||
|
return wrapF{txn}
|
||||||
|
case hasF | hasR:
|
||||||
|
return wrapFR{txn}
|
||||||
|
case hasF | hasH:
|
||||||
|
return wrapFH{txn}
|
||||||
|
case hasF | hasH | hasR:
|
||||||
|
return wrapFHR{txn}
|
||||||
|
case hasC:
|
||||||
|
return wrapC{txn}
|
||||||
|
case hasC | hasR:
|
||||||
|
return wrapCR{txn}
|
||||||
|
case hasC | hasH:
|
||||||
|
return wrapCH{txn}
|
||||||
|
case hasC | hasH | hasR:
|
||||||
|
return wrapCHR{txn}
|
||||||
|
case hasC | hasF:
|
||||||
|
return wrapCF{txn}
|
||||||
|
case hasC | hasF | hasR:
|
||||||
|
return wrapCFR{txn}
|
||||||
|
case hasC | hasF | hasH:
|
||||||
|
return wrapCFH{txn}
|
||||||
|
case hasC | hasF | hasH | hasR:
|
||||||
|
return wrapCFHR{txn}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,492 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/newrelic/go-agent/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type txnInput struct {
|
||||||
|
W http.ResponseWriter
|
||||||
|
Request *http.Request
|
||||||
|
Config Config
|
||||||
|
Reply *internal.ConnectReply
|
||||||
|
Consumer dataConsumer
|
||||||
|
attrConfig *internal.AttributeConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type txn struct {
|
||||||
|
txnInput
|
||||||
|
// This mutex is required since the consumer may call the public API
|
||||||
|
// interface functions from different routines.
|
||||||
|
sync.Mutex
|
||||||
|
// finished indicates whether or not End() has been called. After
|
||||||
|
// finished has been set to true, no recording should occur.
|
||||||
|
finished bool
|
||||||
|
queuing time.Duration
|
||||||
|
start time.Time
|
||||||
|
name string // Work in progress name
|
||||||
|
isWeb bool
|
||||||
|
ignore bool
|
||||||
|
errors internal.TxnErrors // Lazily initialized.
|
||||||
|
attrs *internal.Attributes
|
||||||
|
|
||||||
|
// Fields relating to tracing and breakdown metrics/segments.
|
||||||
|
tracer internal.Tracer
|
||||||
|
|
||||||
|
// wroteHeader prevents capturing multiple response code errors if the
|
||||||
|
// user erroneously calls WriteHeader multiple times.
|
||||||
|
wroteHeader bool
|
||||||
|
|
||||||
|
// Fields assigned at completion
|
||||||
|
stop time.Time
|
||||||
|
duration time.Duration
|
||||||
|
finalName string // Full finalized metric name
|
||||||
|
zone internal.ApdexZone
|
||||||
|
apdexThreshold time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTxn(input txnInput, name string) *txn {
|
||||||
|
txn := &txn{
|
||||||
|
txnInput: input,
|
||||||
|
start: time.Now(),
|
||||||
|
name: name,
|
||||||
|
isWeb: nil != input.Request,
|
||||||
|
attrs: internal.NewAttributes(input.attrConfig),
|
||||||
|
}
|
||||||
|
if nil != txn.Request {
|
||||||
|
txn.queuing = internal.QueueDuration(input.Request.Header, txn.start)
|
||||||
|
internal.RequestAgentAttributes(txn.attrs, input.Request)
|
||||||
|
}
|
||||||
|
txn.attrs.Agent.HostDisplayName = txn.Config.HostDisplayName
|
||||||
|
txn.tracer.Enabled = txn.txnTracesEnabled()
|
||||||
|
txn.tracer.SegmentThreshold = txn.Config.TransactionTracer.SegmentThreshold
|
||||||
|
txn.tracer.StackTraceThreshold = txn.Config.TransactionTracer.StackTraceThreshold
|
||||||
|
txn.tracer.SlowQueriesEnabled = txn.slowQueriesEnabled()
|
||||||
|
txn.tracer.SlowQueryThreshold = txn.Config.DatastoreTracer.SlowQuery.Threshold
|
||||||
|
|
||||||
|
return txn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) slowQueriesEnabled() bool {
|
||||||
|
return txn.Config.DatastoreTracer.SlowQuery.Enabled &&
|
||||||
|
txn.Reply.CollectTraces
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) txnTracesEnabled() bool {
|
||||||
|
return txn.Config.TransactionTracer.Enabled &&
|
||||||
|
txn.Reply.CollectTraces
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) txnEventsEnabled() bool {
|
||||||
|
return txn.Config.TransactionEvents.Enabled &&
|
||||||
|
txn.Reply.CollectAnalyticsEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) errorEventsEnabled() bool {
|
||||||
|
return txn.Config.ErrorCollector.CaptureEvents &&
|
||||||
|
txn.Reply.CollectErrorEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) freezeName() {
|
||||||
|
if txn.ignore || ("" != txn.finalName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
txn.finalName = internal.CreateFullTxnName(txn.name, txn.Reply, txn.isWeb)
|
||||||
|
if "" == txn.finalName {
|
||||||
|
txn.ignore = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) getsApdex() bool {
|
||||||
|
return txn.isWeb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) txnTraceThreshold() time.Duration {
|
||||||
|
if txn.Config.TransactionTracer.Threshold.IsApdexFailing {
|
||||||
|
return internal.ApdexFailingThreshold(txn.apdexThreshold)
|
||||||
|
}
|
||||||
|
return txn.Config.TransactionTracer.Threshold.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) shouldSaveTrace() bool {
|
||||||
|
return txn.txnTracesEnabled() &&
|
||||||
|
(txn.duration >= txn.txnTraceThreshold())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) hasErrors() bool {
|
||||||
|
return len(txn.errors) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) MergeIntoHarvest(h *internal.Harvest) {
|
||||||
|
exclusive := time.Duration(0)
|
||||||
|
children := internal.TracerRootChildren(&txn.tracer)
|
||||||
|
if txn.duration > children {
|
||||||
|
exclusive = txn.duration - children
|
||||||
|
}
|
||||||
|
|
||||||
|
internal.CreateTxnMetrics(internal.CreateTxnMetricsArgs{
|
||||||
|
IsWeb: txn.isWeb,
|
||||||
|
Duration: txn.duration,
|
||||||
|
Exclusive: exclusive,
|
||||||
|
Name: txn.finalName,
|
||||||
|
Zone: txn.zone,
|
||||||
|
ApdexThreshold: txn.apdexThreshold,
|
||||||
|
HasErrors: txn.hasErrors(),
|
||||||
|
Queueing: txn.queuing,
|
||||||
|
}, h.Metrics)
|
||||||
|
|
||||||
|
internal.MergeBreakdownMetrics(&txn.tracer, h.Metrics, txn.finalName, txn.isWeb)
|
||||||
|
|
||||||
|
if txn.txnEventsEnabled() {
|
||||||
|
h.TxnEvents.AddTxnEvent(&internal.TxnEvent{
|
||||||
|
Name: txn.finalName,
|
||||||
|
Timestamp: txn.start,
|
||||||
|
Duration: txn.duration,
|
||||||
|
Queuing: txn.queuing,
|
||||||
|
Zone: txn.zone,
|
||||||
|
Attrs: txn.attrs,
|
||||||
|
DatastoreExternalTotals: txn.tracer.DatastoreExternalTotals,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
requestURI := ""
|
||||||
|
if nil != txn.Request && nil != txn.Request.URL {
|
||||||
|
requestURI = internal.SafeURL(txn.Request.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal.MergeTxnErrors(h.ErrorTraces, txn.errors, txn.finalName, requestURI, txn.attrs)
|
||||||
|
|
||||||
|
if txn.errorEventsEnabled() {
|
||||||
|
for _, e := range txn.errors {
|
||||||
|
h.ErrorEvents.Add(&internal.ErrorEvent{
|
||||||
|
Klass: e.Klass,
|
||||||
|
Msg: e.Msg,
|
||||||
|
When: e.When,
|
||||||
|
TxnName: txn.finalName,
|
||||||
|
Duration: txn.duration,
|
||||||
|
Queuing: txn.queuing,
|
||||||
|
Attrs: txn.attrs,
|
||||||
|
DatastoreExternalTotals: txn.tracer.DatastoreExternalTotals,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if txn.shouldSaveTrace() {
|
||||||
|
h.TxnTraces.Witness(internal.HarvestTrace{
|
||||||
|
Start: txn.start,
|
||||||
|
Duration: txn.duration,
|
||||||
|
MetricName: txn.finalName,
|
||||||
|
CleanURL: requestURI,
|
||||||
|
Trace: txn.tracer.TxnTrace,
|
||||||
|
ForcePersist: false,
|
||||||
|
GUID: "",
|
||||||
|
SyntheticsResourceID: "",
|
||||||
|
Attrs: txn.attrs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if nil != txn.tracer.SlowQueries {
|
||||||
|
h.SlowSQLs.Merge(txn.tracer.SlowQueries, txn.finalName, requestURI)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func responseCodeIsError(cfg *Config, code int) bool {
|
||||||
|
if code < http.StatusBadRequest { // 400
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, ignoreCode := range cfg.ErrorCollector.IgnoreStatusCodes {
|
||||||
|
if code == ignoreCode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func headersJustWritten(txn *txn, code int) {
|
||||||
|
if txn.finished {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if txn.wroteHeader {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
txn.wroteHeader = true
|
||||||
|
|
||||||
|
internal.ResponseHeaderAttributes(txn.attrs, txn.W.Header())
|
||||||
|
internal.ResponseCodeAttribute(txn.attrs, code)
|
||||||
|
|
||||||
|
if responseCodeIsError(&txn.Config, code) {
|
||||||
|
e := internal.TxnErrorFromResponseCode(time.Now(), code)
|
||||||
|
e.Stack = internal.GetStackTrace(1)
|
||||||
|
txn.noticeErrorInternal(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) Header() http.Header { return txn.W.Header() }
|
||||||
|
|
||||||
|
func (txn *txn) Write(b []byte) (int, error) {
|
||||||
|
n, err := txn.W.Write(b)
|
||||||
|
|
||||||
|
txn.Lock()
|
||||||
|
defer txn.Unlock()
|
||||||
|
|
||||||
|
headersJustWritten(txn, http.StatusOK)
|
||||||
|
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) WriteHeader(code int) {
|
||||||
|
txn.W.WriteHeader(code)
|
||||||
|
|
||||||
|
txn.Lock()
|
||||||
|
defer txn.Unlock()
|
||||||
|
|
||||||
|
headersJustWritten(txn, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) End() error {
|
||||||
|
txn.Lock()
|
||||||
|
defer txn.Unlock()
|
||||||
|
|
||||||
|
if txn.finished {
|
||||||
|
return errAlreadyEnded
|
||||||
|
}
|
||||||
|
|
||||||
|
txn.finished = true
|
||||||
|
|
||||||
|
r := recover()
|
||||||
|
if nil != r {
|
||||||
|
e := internal.TxnErrorFromPanic(time.Now(), r)
|
||||||
|
e.Stack = internal.GetStackTrace(0)
|
||||||
|
txn.noticeErrorInternal(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
txn.stop = time.Now()
|
||||||
|
txn.duration = txn.stop.Sub(txn.start)
|
||||||
|
|
||||||
|
txn.freezeName()
|
||||||
|
|
||||||
|
// Assign apdexThreshold regardless of whether or not the transaction
|
||||||
|
// gets apdex since it may be used to calculate the trace threshold.
|
||||||
|
txn.apdexThreshold = internal.CalculateApdexThreshold(txn.Reply, txn.finalName)
|
||||||
|
|
||||||
|
if txn.getsApdex() {
|
||||||
|
if txn.hasErrors() {
|
||||||
|
txn.zone = internal.ApdexFailing
|
||||||
|
} else {
|
||||||
|
txn.zone = internal.CalculateApdexZone(txn.apdexThreshold, txn.duration)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
txn.zone = internal.ApdexNone
|
||||||
|
}
|
||||||
|
|
||||||
|
if txn.Config.Logger.DebugEnabled() {
|
||||||
|
txn.Config.Logger.Debug("transaction ended", map[string]interface{}{
|
||||||
|
"name": txn.finalName,
|
||||||
|
"duration_ms": txn.duration.Seconds() * 1000.0,
|
||||||
|
"ignored": txn.ignore,
|
||||||
|
"run": txn.Reply.RunID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !txn.ignore {
|
||||||
|
txn.Consumer.Consume(txn.Reply.RunID, txn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that if a consumer uses `panic(nil)`, the panic will not
|
||||||
|
// propagate.
|
||||||
|
if nil != r {
|
||||||
|
panic(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) AddAttribute(name string, value interface{}) error {
|
||||||
|
txn.Lock()
|
||||||
|
defer txn.Unlock()
|
||||||
|
|
||||||
|
if txn.finished {
|
||||||
|
return errAlreadyEnded
|
||||||
|
}
|
||||||
|
|
||||||
|
return internal.AddUserAttribute(txn.attrs, name, value, internal.DestAll)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errorsLocallyDisabled = errors.New("errors locally disabled")
|
||||||
|
errorsRemotelyDisabled = errors.New("errors remotely disabled")
|
||||||
|
errNilError = errors.New("nil error")
|
||||||
|
errAlreadyEnded = errors.New("transaction has already ended")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
highSecurityErrorMsg = "message removed by high security setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (txn *txn) noticeErrorInternal(err internal.TxnError) error {
|
||||||
|
if !txn.Config.ErrorCollector.Enabled {
|
||||||
|
return errorsLocallyDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if !txn.Reply.CollectErrors {
|
||||||
|
return errorsRemotelyDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if nil == txn.errors {
|
||||||
|
txn.errors = internal.NewTxnErrors(internal.MaxTxnErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
if txn.Config.HighSecurity {
|
||||||
|
err.Msg = highSecurityErrorMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
txn.errors.Add(err)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) NoticeError(err error) error {
|
||||||
|
txn.Lock()
|
||||||
|
defer txn.Unlock()
|
||||||
|
|
||||||
|
if txn.finished {
|
||||||
|
return errAlreadyEnded
|
||||||
|
}
|
||||||
|
|
||||||
|
if nil == err {
|
||||||
|
return errNilError
|
||||||
|
}
|
||||||
|
|
||||||
|
e := internal.TxnErrorFromError(time.Now(), err)
|
||||||
|
e.Stack = internal.GetStackTrace(2)
|
||||||
|
return txn.noticeErrorInternal(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) SetName(name string) error {
|
||||||
|
txn.Lock()
|
||||||
|
defer txn.Unlock()
|
||||||
|
|
||||||
|
if txn.finished {
|
||||||
|
return errAlreadyEnded
|
||||||
|
}
|
||||||
|
|
||||||
|
txn.name = name
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) Ignore() error {
|
||||||
|
txn.Lock()
|
||||||
|
defer txn.Unlock()
|
||||||
|
|
||||||
|
if txn.finished {
|
||||||
|
return errAlreadyEnded
|
||||||
|
}
|
||||||
|
txn.ignore = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) StartSegmentNow() SegmentStartTime {
|
||||||
|
var s internal.SegmentStartTime
|
||||||
|
txn.Lock()
|
||||||
|
if !txn.finished {
|
||||||
|
s = internal.StartSegment(&txn.tracer, time.Now())
|
||||||
|
}
|
||||||
|
txn.Unlock()
|
||||||
|
return SegmentStartTime{
|
||||||
|
segment: segment{
|
||||||
|
start: s,
|
||||||
|
txn: txn,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type segment struct {
|
||||||
|
start internal.SegmentStartTime
|
||||||
|
txn *txn
|
||||||
|
}
|
||||||
|
|
||||||
|
func endSegment(s Segment) {
|
||||||
|
txn := s.StartTime.txn
|
||||||
|
if nil == txn {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
txn.Lock()
|
||||||
|
if !txn.finished {
|
||||||
|
internal.EndBasicSegment(&txn.tracer, s.StartTime.start, time.Now(), s.Name)
|
||||||
|
}
|
||||||
|
txn.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func endDatastore(s DatastoreSegment) {
|
||||||
|
txn := s.StartTime.txn
|
||||||
|
if nil == txn {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
txn.Lock()
|
||||||
|
defer txn.Unlock()
|
||||||
|
|
||||||
|
if txn.finished {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if txn.Config.HighSecurity {
|
||||||
|
s.QueryParameters = nil
|
||||||
|
}
|
||||||
|
if !txn.Config.DatastoreTracer.QueryParameters.Enabled {
|
||||||
|
s.QueryParameters = nil
|
||||||
|
}
|
||||||
|
if !txn.Config.DatastoreTracer.DatabaseNameReporting.Enabled {
|
||||||
|
s.DatabaseName = ""
|
||||||
|
}
|
||||||
|
if !txn.Config.DatastoreTracer.InstanceReporting.Enabled {
|
||||||
|
s.Host = ""
|
||||||
|
s.PortPathOrID = ""
|
||||||
|
}
|
||||||
|
internal.EndDatastoreSegment(internal.EndDatastoreParams{
|
||||||
|
Tracer: &txn.tracer,
|
||||||
|
Start: s.StartTime.start,
|
||||||
|
Now: time.Now(),
|
||||||
|
Product: string(s.Product),
|
||||||
|
Collection: s.Collection,
|
||||||
|
Operation: s.Operation,
|
||||||
|
ParameterizedQuery: s.ParameterizedQuery,
|
||||||
|
QueryParameters: s.QueryParameters,
|
||||||
|
Host: s.Host,
|
||||||
|
PortPathOrID: s.PortPathOrID,
|
||||||
|
Database: s.DatabaseName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func externalSegmentURL(s ExternalSegment) *url.URL {
|
||||||
|
if "" != s.URL {
|
||||||
|
u, _ := url.Parse(s.URL)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
r := s.Request
|
||||||
|
if nil != s.Response && nil != s.Response.Request {
|
||||||
|
r = s.Response.Request
|
||||||
|
}
|
||||||
|
if r != nil {
|
||||||
|
return r.URL
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func endExternal(s ExternalSegment) {
|
||||||
|
txn := s.StartTime.txn
|
||||||
|
if nil == txn {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
txn.Lock()
|
||||||
|
defer txn.Unlock()
|
||||||
|
|
||||||
|
if txn.finished {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
internal.EndExternalSegment(&txn.tracer, s.StartTime.start, time.Now(), externalSegmentURL(s))
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/newrelic/go-agent/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger is the interface that is used for logging in the go-agent. Assign the
|
||||||
|
// Config.Logger field to the Logger you wish to use. Loggers must be safe for
|
||||||
|
// use in multiple goroutines.
|
||||||
|
//
|
||||||
|
// For an example implementation, see: _integrations/nrlogrus/nrlogrus.go
|
||||||
|
type Logger interface {
|
||||||
|
Error(msg string, context map[string]interface{})
|
||||||
|
Warn(msg string, context map[string]interface{})
|
||||||
|
Info(msg string, context map[string]interface{})
|
||||||
|
Debug(msg string, context map[string]interface{})
|
||||||
|
DebugEnabled() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogger creates a basic Logger at info level.
|
||||||
|
func NewLogger(w io.Writer) Logger {
|
||||||
|
return logger.New(w, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDebugLogger creates a basic Logger at debug level.
|
||||||
|
func NewDebugLogger(w io.Writer) Logger {
|
||||||
|
return logger.New(w, true)
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// SegmentStartTime is created by Transaction.StartSegmentNow and marks the
|
||||||
|
// beginning of a segment. A segment with a zero-valued SegmentStartTime may
|
||||||
|
// safely be ended.
|
||||||
|
type SegmentStartTime struct{ segment }
|
||||||
|
|
||||||
|
// Segment is used to instrument functions, methods, and blocks of code. The
|
||||||
|
// easiest way use Segment is the StartSegment function.
|
||||||
|
type Segment struct {
|
||||||
|
StartTime SegmentStartTime
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DatastoreSegment is used to instrument calls to databases and object stores.
|
||||||
|
// Here is an example:
|
||||||
|
//
|
||||||
|
// defer newrelic.DatastoreSegment{
|
||||||
|
// StartTime: newrelic.StartSegmentNow(txn),
|
||||||
|
// Product: newrelic.DatastoreMySQL,
|
||||||
|
// Collection: "my_table",
|
||||||
|
// Operation: "SELECT",
|
||||||
|
// }.End()
|
||||||
|
//
|
||||||
|
type DatastoreSegment struct {
|
||||||
|
StartTime SegmentStartTime
|
||||||
|
// Product is the datastore type. See the constants in datastore.go.
|
||||||
|
Product DatastoreProduct
|
||||||
|
// Collection is the table or group.
|
||||||
|
Collection string
|
||||||
|
// Operation is the relevant action, e.g. "SELECT" or "GET".
|
||||||
|
Operation string
|
||||||
|
// ParameterizedQuery may be set to the query being performed. It must
|
||||||
|
// not contain any raw parameters, only placeholders.
|
||||||
|
ParameterizedQuery string
|
||||||
|
// QueryParameters may be used to provide query parameters. Care should
|
||||||
|
// be taken to only provide parameters which are not sensitive.
|
||||||
|
// QueryParameters are ignored in high security mode.
|
||||||
|
QueryParameters map[string]interface{}
|
||||||
|
// Host is the name of the server hosting the datastore.
|
||||||
|
Host string
|
||||||
|
// PortPathOrID can represent either the port, path, or id of the
|
||||||
|
// datastore being connected to.
|
||||||
|
PortPathOrID string
|
||||||
|
// DatabaseName is name of database where the current query is being
|
||||||
|
// executed.
|
||||||
|
DatabaseName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExternalSegment is used to instrument external calls. StartExternalSegment
|
||||||
|
// is recommended when you have access to an http.Request.
|
||||||
|
type ExternalSegment struct {
|
||||||
|
StartTime SegmentStartTime
|
||||||
|
Request *http.Request
|
||||||
|
Response *http.Response
|
||||||
|
// If you do not have access to the request, this URL field should be
|
||||||
|
// used to indicate the endpoint.
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// End finishes the segment.
|
||||||
|
func (s Segment) End() { endSegment(s) }
|
||||||
|
|
||||||
|
// End finishes the datastore segment.
|
||||||
|
func (s DatastoreSegment) End() { endDatastore(s) }
|
||||||
|
|
||||||
|
// End finishes the external segment.
|
||||||
|
func (s ExternalSegment) End() { endExternal(s) }
|
||||||
|
|
||||||
|
// StartSegmentNow helps avoid Transaction nil checks.
|
||||||
|
func StartSegmentNow(txn Transaction) SegmentStartTime {
|
||||||
|
if nil != txn {
|
||||||
|
return txn.StartSegmentNow()
|
||||||
|
}
|
||||||
|
return SegmentStartTime{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartSegment makes it easy to instrument segments. To time a function, do
|
||||||
|
// the following:
|
||||||
|
//
|
||||||
|
// func timeMe(txn newrelic.Transaction) {
|
||||||
|
// defer newrelic.StartSegment(txn, "timeMe").End()
|
||||||
|
// // ... function code here ...
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// To time a block of code, do the following:
|
||||||
|
//
|
||||||
|
// segment := StartSegment(txn, "myBlock")
|
||||||
|
// // ... code you want to time here ...
|
||||||
|
// segment.End()
|
||||||
|
//
|
||||||
|
func StartSegment(txn Transaction, name string) Segment {
|
||||||
|
return Segment{
|
||||||
|
StartTime: StartSegmentNow(txn),
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartExternalSegment makes it easier to instrument external calls.
|
||||||
|
//
|
||||||
|
// segment := newrelic.StartExternalSegment(txn, request)
|
||||||
|
// resp, err := client.Do(request)
|
||||||
|
// segment.Response = resp
|
||||||
|
// segment.End()
|
||||||
|
//
|
||||||
|
func StartExternalSegment(txn Transaction, request *http.Request) ExternalSegment {
|
||||||
|
return ExternalSegment{
|
||||||
|
StartTime: StartSegmentNow(txn),
|
||||||
|
Request: request,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// Transaction represents a request or a background task.
|
||||||
|
// Each Transaction should only be used in a single goroutine.
|
||||||
|
type Transaction interface {
|
||||||
|
// If StartTransaction is called with a non-nil http.ResponseWriter then
|
||||||
|
// the Transaction may be used in its place. This allows
|
||||||
|
// instrumentation of the response code and response headers.
|
||||||
|
http.ResponseWriter
|
||||||
|
|
||||||
|
// End finishes the current transaction, stopping all further
|
||||||
|
// instrumentation. Subsequent calls to End will have no effect.
|
||||||
|
End() error
|
||||||
|
|
||||||
|
// Ignore ensures that this transaction's data will not be recorded.
|
||||||
|
Ignore() error
|
||||||
|
|
||||||
|
// SetName names the transaction. Transactions will not be grouped
|
||||||
|
// usefully if too many unique names are used.
|
||||||
|
SetName(name string) error
|
||||||
|
|
||||||
|
// NoticeError records an error. The first five errors per transaction
|
||||||
|
// are recorded (this behavior is subject to potential change in the
|
||||||
|
// future).
|
||||||
|
NoticeError(err error) error
|
||||||
|
|
||||||
|
// AddAttribute adds a key value pair to the current transaction. This
|
||||||
|
// information is attached to errors, transaction events, and error
|
||||||
|
// events. The key must contain fewer than than 255 bytes. The value
|
||||||
|
// must be a number, string, or boolean. Attribute configuration is
|
||||||
|
// applied (see config.go).
|
||||||
|
//
|
||||||
|
// For more information, see:
|
||||||
|
// https://docs.newrelic.com/docs/agents/manage-apm-agents/agent-metrics/collect-custom-attributes
|
||||||
|
AddAttribute(key string, value interface{}) error
|
||||||
|
|
||||||
|
// StartSegmentNow allows the timing of functions, external calls, and
|
||||||
|
// datastore calls. The segments of each transaction MUST be used in a
|
||||||
|
// single goroutine. Consumers are encouraged to use the
|
||||||
|
// `StartSegmentNow` functions which checks if the Transaction is nil.
|
||||||
|
// See segments.go
|
||||||
|
StartSegmentNow() SegmentStartTime
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package newrelic
|
||||||
|
|
||||||
|
const (
|
||||||
|
major = "1"
|
||||||
|
minor = "5"
|
||||||
|
patch = "0"
|
||||||
|
|
||||||
|
// Version is the full string version of this Go Agent.
|
||||||
|
Version = major + "." + minor + "." + patch
|
||||||
|
)
|
|
@ -2125,6 +2125,42 @@
|
||||||
"revision": "2dc6a86cf75ce4b33516d2a13c9c0c378310cf3b",
|
"revision": "2dc6a86cf75ce4b33516d2a13c9c0c378310cf3b",
|
||||||
"revisionTime": "2016-04-25T23:31:34Z"
|
"revisionTime": "2016-04-25T23:31:34Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "WQJBP9v20jr44RiZ1YbfrpGaEqk=",
|
||||||
|
"path": "github.com/newrelic/go-agent",
|
||||||
|
"revision": "7d12ae2201fc160e486197614a6f65afcf3f8170",
|
||||||
|
"revisionTime": "2016-11-16T22:44:47Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "lLXXIL0C/ZzMDqN2BlQRZInhot0=",
|
||||||
|
"path": "github.com/newrelic/go-agent/internal",
|
||||||
|
"revision": "7d12ae2201fc160e486197614a6f65afcf3f8170",
|
||||||
|
"revisionTime": "2016-11-16T22:44:47Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "mkbupMdy+cF7xyo8xW0A6Bq15k4=",
|
||||||
|
"path": "github.com/newrelic/go-agent/internal/jsonx",
|
||||||
|
"revision": "7d12ae2201fc160e486197614a6f65afcf3f8170",
|
||||||
|
"revisionTime": "2016-11-16T22:44:47Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "ywxlVKtGArJ2vDfH1rAqEFwSGds=",
|
||||||
|
"path": "github.com/newrelic/go-agent/internal/logger",
|
||||||
|
"revision": "7d12ae2201fc160e486197614a6f65afcf3f8170",
|
||||||
|
"revisionTime": "2016-11-16T22:44:47Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "S7CiHO7EblgZt9q7wgiXMv/j/ao=",
|
||||||
|
"path": "github.com/newrelic/go-agent/internal/sysinfo",
|
||||||
|
"revision": "7d12ae2201fc160e486197614a6f65afcf3f8170",
|
||||||
|
"revisionTime": "2016-11-16T22:44:47Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "c2JSKesj3tHYgzIF3QL37WfHWG8=",
|
||||||
|
"path": "github.com/newrelic/go-agent/internal/utilization",
|
||||||
|
"revision": "7d12ae2201fc160e486197614a6f65afcf3f8170",
|
||||||
|
"revisionTime": "2016-11-16T22:44:47Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "github.com/nu7hatch/gouuid",
|
"path": "github.com/nu7hatch/gouuid",
|
||||||
"revision": "179d4d0c4d8d407a32af483c2354df1d2c91e6c3"
|
"revision": "179d4d0c4d8d407a32af483c2354df1d2c91e6c3"
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
---
|
||||||
|
layout: "newrelic"
|
||||||
|
page_title: "New Relic: newrelic_application"
|
||||||
|
sidebar_current: "docs-newrelic-datasource-application"
|
||||||
|
description: |-
|
||||||
|
Looks up the information about an application in New Relic.
|
||||||
|
---
|
||||||
|
|
||||||
|
# newrelic\_application
|
||||||
|
|
||||||
|
Use this data source to get information about a specific application in New Relic.
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
data "newrelic_application" "app" {
|
||||||
|
name = "my-app"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "newrelic_alert_policy" "foo" {
|
||||||
|
name = "foo"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "newrelic_alert_condition" "foo" {
|
||||||
|
policy_id = "${newrelic_alert_policy.foo.id}"
|
||||||
|
|
||||||
|
name = "foo"
|
||||||
|
type = "apm_app_metric"
|
||||||
|
entities = ["${data.newrelic_application.app.id}"]
|
||||||
|
metric = "apdex"
|
||||||
|
runbook_url = "https://www.example.com"
|
||||||
|
|
||||||
|
term {
|
||||||
|
duration = 5
|
||||||
|
operator = "below"
|
||||||
|
priority = "critical"
|
||||||
|
threshold = "0.75"
|
||||||
|
time_function = "all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Argument Reference
|
||||||
|
|
||||||
|
The following arguments are supported:
|
||||||
|
|
||||||
|
* `name` - (Required) The name of the application in New Relic.
|
||||||
|
|
||||||
|
## Attributes Reference
|
||||||
|
* `id` - The ID of the application.
|
||||||
|
* `instance_ids` - A list of instance IDs associated with the application.
|
||||||
|
* `host_ids` - A list of host IDs associated with the application.
|
|
@ -0,0 +1,71 @@
|
||||||
|
---
|
||||||
|
layout: "newrelic"
|
||||||
|
page_title: "Provider: New Relic"
|
||||||
|
sidebar_current: "docs-newrelic-index"
|
||||||
|
description: |-
|
||||||
|
New Relic offers a performance management solution enabling developers to
|
||||||
|
diagnose and fix application performance problems in real time.
|
||||||
|
---
|
||||||
|
|
||||||
|
# New Relic Provider
|
||||||
|
|
||||||
|
[New Relic](https://newrelic.com/) offers a performance management solution
|
||||||
|
enabling developers to diagnose and fix application performance problems in real time.
|
||||||
|
|
||||||
|
Use the navigation to the left to read about the available resources.
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
# Configure the New Relic provider
|
||||||
|
provider "newrelic" {
|
||||||
|
api_key = "${var.newrelic_api_key}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create an alert policy
|
||||||
|
resource "newrelic_alert_policy" "alert" {
|
||||||
|
name = "Alert"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add a condition
|
||||||
|
resource "newrelic_alert_condition" "foo" {
|
||||||
|
policy_id = "${newrelic_alert_policy.alert.id}"
|
||||||
|
|
||||||
|
name = "foo"
|
||||||
|
type = "apm_app_metric"
|
||||||
|
entities = ["12345"] # You can look this up in New Relic
|
||||||
|
metric = "apdex"
|
||||||
|
runbook_url = "https://docs.example.com/my-runbook"
|
||||||
|
|
||||||
|
term {
|
||||||
|
duration = 5
|
||||||
|
operator = "below"
|
||||||
|
priority = "critical"
|
||||||
|
threshold = "0.75"
|
||||||
|
time_function = "all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add a notification channel
|
||||||
|
resource "newrelic_alert_channel" "email" {
|
||||||
|
name = "email"
|
||||||
|
type = "email"
|
||||||
|
|
||||||
|
configuration = {
|
||||||
|
recipients = "paul@example.com"
|
||||||
|
include_json_attachment = "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Link the channel to the policy
|
||||||
|
resource "newrelic_alert_policy_channel" "alert_email" {
|
||||||
|
policy_id = "${newrelic_alert_policy.alert.id}"
|
||||||
|
channel_id = "${newrelic_alert_channel.email.id}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Argument Reference
|
||||||
|
|
||||||
|
The following arguments are supported:
|
||||||
|
|
||||||
|
* `api_key` - (Required) Your New Relic API key.
|
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
layout: "newrelic"
|
||||||
|
page_title: "New Relic: newrelic_alert_channel"
|
||||||
|
sidebar_current: "docs-newrelic-resource-alert-channel"
|
||||||
|
description: |-
|
||||||
|
Create and manage a notification channel for alerts in New Relic.
|
||||||
|
---
|
||||||
|
|
||||||
|
# newrelic\_alert\_channel
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
resource "newrelic_alert_channel" "foo" {
|
||||||
|
name = "foo"
|
||||||
|
type = "email"
|
||||||
|
|
||||||
|
configuration = {
|
||||||
|
recipients = "foo@example.com"
|
||||||
|
include_json_attachment = "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Argument Reference
|
||||||
|
|
||||||
|
The following arguments are supported:
|
||||||
|
|
||||||
|
* `name` - (Required) The name of the channel.
|
||||||
|
* `type` - (Required) The type of channel. One of: `campfire`, `email`, `hipchat`, `opsgenie`, `pagerduty`, `slack`, `user`, `victorops`, or `webhook`.
|
||||||
|
* `configuration` - (Required) A map of key / value pairs with channel type specific values.
|
||||||
|
|
||||||
|
## Attributes Reference
|
||||||
|
|
||||||
|
The following attributes are exported:
|
||||||
|
|
||||||
|
* `id` - The ID of the channel.
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
Alert channels can be imported using the `id`, e.g.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ terraform import newrelic_alert_channel.main 12345
|
||||||
|
```
|
|
@ -0,0 +1,77 @@
|
||||||
|
---
|
||||||
|
layout: "newrelic"
|
||||||
|
page_title: "New Relic: newrelic_alert_condition"
|
||||||
|
sidebar_current: "docs-newrelic-resource-alert-condition"
|
||||||
|
description: |-
|
||||||
|
Create and manage an alert condition for a policy in New Relic.
|
||||||
|
---
|
||||||
|
|
||||||
|
# newrelic\_alert\_condition
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
data "newrelic_application" "app" {
|
||||||
|
name = "my-app"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "newrelic_alert_policy" "foo" {
|
||||||
|
name = "foo"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "newrelic_alert_condition" "foo" {
|
||||||
|
policy_id = "${newrelic_alert_policy.foo.id}"
|
||||||
|
|
||||||
|
name = "foo"
|
||||||
|
type = "apm_app_metric"
|
||||||
|
entities = ["${data.newrelic_application.app.id}"]
|
||||||
|
metric = "apdex"
|
||||||
|
runbook_url = "https://www.example.com"
|
||||||
|
|
||||||
|
term {
|
||||||
|
duration = 5
|
||||||
|
operator = "below"
|
||||||
|
priority = "critical"
|
||||||
|
threshold = "0.75"
|
||||||
|
time_function = "all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Argument Reference
|
||||||
|
|
||||||
|
The following arguments are supported:
|
||||||
|
|
||||||
|
* `policy_id` - (Required) The ID of the policy where this condition should be used.
|
||||||
|
* `name` - (Required) The title of the condition
|
||||||
|
* `type` - (Required) The type of condition. One of: `apm_app_metric`, `apm_kt_metric`, `servers_metric`, `browser_metric`, `mobile_metric`
|
||||||
|
* `entities` - (Required) The instance IDS associated with this condition.
|
||||||
|
* `metric` - (Required) The metric field accepts parameters based on the `type` set.
|
||||||
|
* `runbook_url` - (Optional) Runbook URL to display in notifications.
|
||||||
|
* `term` - (Required) A list of terms for this condition. See [Terms](#terms) below for details.
|
||||||
|
* `user_defined_metric` - (Optional) A custom metric to be evaluated.
|
||||||
|
* `user_defined_value_function` - (Optional) One of: `average`, `min`, `max`, `total`, or `sample_size`.
|
||||||
|
|
||||||
|
## Terms
|
||||||
|
|
||||||
|
The `term` mapping supports the following arguments:
|
||||||
|
|
||||||
|
* `duration` - (Required) In minutes, must be: `5`, `10`, `15`, `30`, `60`, or `120`.
|
||||||
|
* `operator` - (Optional) `above`, `below`, or `equal`. Defaults to `equal`.
|
||||||
|
* `priority` - (Optional) `critical` or `warning`. Defaults to `critical`.
|
||||||
|
* `threshold` - (Required) Must be 0 or greater.
|
||||||
|
* `time_function` - (Required) `all` or `any`.
|
||||||
|
|
||||||
|
## Attributes Reference
|
||||||
|
|
||||||
|
The following attributes are exported:
|
||||||
|
|
||||||
|
* `id` - The ID of the alert condition.
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
Alert conditions can be imported using the `id`, e.g.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ terraform import newrelic_alert_condition.main 12345
|
||||||
|
```
|
|
@ -0,0 +1,40 @@
|
||||||
|
---
|
||||||
|
layout: "newrelic"
|
||||||
|
page_title: "New Relic: newrelic_alert_policy"
|
||||||
|
sidebar_current: "docs-newrelic-resource-alert-policy"
|
||||||
|
description: |-
|
||||||
|
Create and manage alert policies in New Relic.
|
||||||
|
---
|
||||||
|
|
||||||
|
# newrelic\_alert\_policy
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
resource "newrelic_alert_policy" "foo" {
|
||||||
|
name = "foo"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Argument Reference
|
||||||
|
|
||||||
|
The following arguments are supported:
|
||||||
|
|
||||||
|
* `name` - (Required) The name of the policy.
|
||||||
|
* `incident_preference` - (Optional) The rollup strategy for the policy. Options include: `PER_POLICY`, `PER_CONDITION`, or `PER_CONDITION_AND_TARGET`. The default is `PER_POLICY`.
|
||||||
|
|
||||||
|
## Attributes Reference
|
||||||
|
|
||||||
|
The following attributes are exported:
|
||||||
|
|
||||||
|
* `id` - The ID of the policy.
|
||||||
|
* `created_at` - The time the policy was created.
|
||||||
|
* `updated_at` - The time the policy was last updated.
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
Alert policies can be imported using the `id`, e.g.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ terraform import newrelic_alert_policy.main 12345
|
||||||
|
```
|
|
@ -0,0 +1,39 @@
|
||||||
|
---
|
||||||
|
layout: "newrelic"
|
||||||
|
page_title: "New Relic: newrelic_alert_policy_channel"
|
||||||
|
sidebar_current: "docs-newrelic-resource-alert-policy-channel"
|
||||||
|
description: |-
|
||||||
|
Map alert policies to alert channels in New Relic.
|
||||||
|
---
|
||||||
|
|
||||||
|
# newrelic\_alert\_policy\_channel
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
resource "newrelic_alert_policy" "foo" {
|
||||||
|
name = "foo"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "newrelic_alert_channel" "foo" {
|
||||||
|
name = "foo"
|
||||||
|
type = "email"
|
||||||
|
|
||||||
|
configuration = {
|
||||||
|
recipients = "foo@example.com"
|
||||||
|
include_json_attachment = "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "newrelic_alert_policy_channel" "foo" {
|
||||||
|
policy_id = "${newrelic_alert_policy.foo.id}"
|
||||||
|
channel_id = "${newrelic_alert_channel.foo.id}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Argument Reference
|
||||||
|
|
||||||
|
The following arguments are supported:
|
||||||
|
|
||||||
|
* `policy_id` - (Required) The ID of the policy.
|
||||||
|
* `channel_id` - (Required) The ID of the channel.
|
|
@ -0,0 +1,44 @@
|
||||||
|
<% wrap_layout :inner do %>
|
||||||
|
<% content_for :sidebar do %>
|
||||||
|
<div class="docs-sidebar hidden-print affix-top" role="complementary">
|
||||||
|
<ul class="nav docs-sidenav">
|
||||||
|
<li<%= sidebar_current("docs-home") %>>
|
||||||
|
<a href="/docs/providers/index.html">« Documentation Home</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-newrelic-index") %>>
|
||||||
|
<a href="/docs/providers/newrelic/index.html">New Relic Provider</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current(/^docs-newrelic-datasource/) %>>
|
||||||
|
<a href="#">Data Sources</a>
|
||||||
|
<ul class="nav nav-visible">
|
||||||
|
<li<%= sidebar_current("docs-newrelic-datasource-application") %>>
|
||||||
|
<a href="/docs/providers/newrelic/d/application.html">newrelic_application</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current(/^docs-newrelic-resource/) %>>
|
||||||
|
<a href="#">Resources</a>
|
||||||
|
<ul class="nav nav-visible">
|
||||||
|
<li<%= sidebar_current("docs-newrelic-resource-alert-channel") %>>
|
||||||
|
<a href="/docs/providers/newrelic/r/alert_channel.html">newrelic_alert_channel</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-newrelic-resource-alert-condition") %>>
|
||||||
|
<a href="/docs/providers/newrelic/r/alert_condition.html">newrelic_alert_condition</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-newrelic-resource-alert-policy") %>>
|
||||||
|
<a href="/docs/providers/newrelic/r/alert_policy.html">newrelic_alert_policy</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-newrelic-resource-alert-policy-channel") %>>
|
||||||
|
<a href="/docs/providers/newrelic/r/alert_policy_channel.html">newrelic_alert_policy_channel</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= yield %>
|
||||||
|
<% end %>
|
Loading…
Reference in New Issue