diff --git a/integration-examples/synthetics-examples/API/graphql-api/README.md b/integration-examples/synthetics-examples/API/graphql-api/README.md new file mode 100644 index 0000000..494cae0 --- /dev/null +++ b/integration-examples/synthetics-examples/API/graphql-api/README.md @@ -0,0 +1,82 @@ +# GraphQL API Check Example +This example API test shows how to query a GraphQL endpoint and validate the returned data using built in matching and JavaScript parsing. +The test and it's configuration are included in this directory: +- [`synthetics_example_graphql_api_check.tf`](./synthetics_example_graphql_api_check.tf) + - Uses the [Splunk Synthetics Terraform provider](https://registry.terraform.io/providers/splunk/synthetics/latest/docs) + +## Synthetic API Test +The API test will call [https://countries.trevorblades.com/graphql](https://countries.trevorblades.com/graphql) which provides data on various countries. We use this data for ease of accessibility purposes. But this approach applies just as well to internal GraphQL endpoints where you want to validate the responses being provided or specific values of data within those responses. + +Our test will query the info on Canada and check that: +- The endpoint returns the expected national languages in the expected JSON format. +- The endpoint returns an array of states within the country. We will use JavaScript and custom variables to validate that there are still exactly 13 "states" (provences) in Canada. + +The expected body we will receive from our request and that we will be validating against looks like so: +```JSON +{ + "data": + { + "country": + { + "name": "Canada", + "native": "Canada", + "capital": "Ottawa", + "emoji": "🇨🇦", + "currency": "CAD", + "languages": + [ + { + "code": "en", + "name": "English" + }, + { + "code": "fr", + "name": "French" + } + ], + "states": + [ + { + "code": "AB" + }, + { + "code": "BC" + }, + { + "code": "MB" + }, + { + "code": "NB" + }, + { + "code": "NL" + }, + { + "code": "NS" + }, + { + "code": "NU" + }, + { + "code": "NT" + }, + { + "code": "ON" + }, + { + "code": "PE" + }, + { + "code": "QC" + }, + { + "code": "SK" + }, + { + "code": "YT" + } + ] + } + } +} +``` diff --git a/integration-examples/synthetics-examples/API/graphql-api/synthetics_example_graphql_api_check.tf b/integration-examples/synthetics-examples/API/graphql-api/synthetics_example_graphql_api_check.tf new file mode 100644 index 0000000..c4c20de --- /dev/null +++ b/integration-examples/synthetics-examples/API/graphql-api/synthetics_example_graphql_api_check.tf @@ -0,0 +1,86 @@ +resource "synthetics_create_api_check_v2" "synthetics_example_graphql_api_check" { + test { + active = true + automatic_retries = 0 + device_id = 34 + frequency = 1440 + location_ids = ["aws-us-east-1", "aws-us-west-1"] + name = "Canada - Languages - Number of States" + scheduling_strategy = "round_robin" + requests { + configuration { + body = jsonencode({ + operationName = "Query" + query = "query Query {\n country(code: \"CA\") {\n name\n native\n capital\n emoji\n currency\n languages {\n code\n name\n }\n states {\n code\n }\n }\n}\n" + variables = {} + }) + headers = { + Content-Type = "application/json" + } + name = "https://countries.trevorblades.com/graphql" + request_method = "POST" + url = "https://countries.trevorblades.com/graphql" + } + validations { + actual = "{{response.code}}" + code = null + comparator = "is_less_than" + expected = "300" + extractor = null + name = "Assert response code is less than 300" + source = null + type = "assert_numeric" + value = null + variable = null + } + validations { + actual = "{{response.body}}" + code = null + comparator = "contains" + expected = "\"languages\":[{\"code\":\"en\",\"name\":\"English\"},{\"code\":\"fr\",\"name\":\"French\"}]" + extractor = null + name = "Assert response body value" + source = null + type = "assert_string" + value = null + variable = null + } + validations { + actual = null + code = null + comparator = null + expected = null + extractor = "$.data.country.states" + name = "Extract from response body" + source = "{{response.body}}" + type = "extract_json" + value = null + variable = "statearray" + } + validations { + actual = null + code = "const apiResponse = custom[\"statearray\"] ? JSON.parse(custom[\"statearray\"]) || [] : [];\n\nfunction countStates(jsonData) { \n const stateCount = jsonData.length;\n \n custom.statcount = stateCount;\n return stateCount;\n}\n\n\ncountStates(apiResponse);" + comparator = null + expected = null + extractor = null + name = "JavaScript run" + source = null + type = "javascript" + value = null + variable = "statecount" + } + validations { + actual = "{{custom.statecount}}" + code = null + comparator = "equals" + expected = "13" + extractor = null + name = "Assert custom variable statecount equals 13" + source = null + type = "assert_numeric" + value = null + variable = null + } + } + } +} diff --git a/integration-examples/synthetics-examples/API/status-page-to-metrics-api/README.md b/integration-examples/synthetics-examples/API/status-page-to-metrics-api/README.md new file mode 100644 index 0000000..d54764c --- /dev/null +++ b/integration-examples/synthetics-examples/API/status-page-to-metrics-api/README.md @@ -0,0 +1,22 @@ +# Third-party Status Page API Check to Metric +This example API test shows how to call multiple APIs, collect data, turn that data into a usable JSON payload, and send it off to another API. +This test creates metrics using a Splunk Synthetics API test. +The test and it's configuration are included in this directory: +- [`synthetics_thirdparty_status_api_check.tf`](./synthetics_thirdparty_status_api_check.tf) + - Uses the [Splunk Synthetics Terraform provider](https://registry.terraform.io/providers/splunk/synthetics/latest/docs) + +For a detailed description of this test and how it functions check out the [Splunk Lantern Article: Constructing an API test JSON payload](https://lantern.splunk.com/Observability/Product_Tips/Synthetic_Monitoring/Constructing_an_API_test_JSON_payload_for_alerting_on_external_dependencies) + +## Synthetic API Test +The synthetic API test will call the CloudFlare and GitHub status pages and report a metric with a value of 1 (status is impacted) or 0 (status is normal) for each: +- `cloudflare.status` +- `github.status` + +These metrics include dimensions for description of any impact to status and an indicator (none, minor, major, or critical). +![alt text](image.png) + +### Required Splunk Synthetic Global Variables +The following [global variables](https://docs.splunk.com/observability/en/synthetics/test-config/global-variables.html) are **REQUIRED** to run the included API test. +- `org_ingest_token`: A provisioned INGEST token +![required synthetic variables](synthetic-variables.png) + diff --git a/integration-examples/synthetics-examples/API/status-page-to-metrics-api/image.png b/integration-examples/synthetics-examples/API/status-page-to-metrics-api/image.png new file mode 100644 index 0000000..abf853e Binary files /dev/null and b/integration-examples/synthetics-examples/API/status-page-to-metrics-api/image.png differ diff --git a/integration-examples/synthetics-examples/API/status-page-to-metrics-api/synthetic-variables.png b/integration-examples/synthetics-examples/API/status-page-to-metrics-api/synthetic-variables.png new file mode 100644 index 0000000..34c7668 Binary files /dev/null and b/integration-examples/synthetics-examples/API/status-page-to-metrics-api/synthetic-variables.png differ diff --git a/integration-examples/synthetics-examples/API/status-page-to-metrics-api/synthetics_thirdparty_status_api_check.tf b/integration-examples/synthetics-examples/API/status-page-to-metrics-api/synthetics_thirdparty_status_api_check.tf new file mode 100644 index 0000000..6a4281f --- /dev/null +++ b/integration-examples/synthetics-examples/API/status-page-to-metrics-api/synthetics_thirdparty_status_api_check.tf @@ -0,0 +1,134 @@ +resource "synthetics_create_api_check_v2" "synthetics_thirdparty_status_api_check" { + test { + active = true + automatic_retries = 0 + device_id = 34 + frequency = 60 + location_ids = ["aws-us-east-1", "aws-us-west-1"] + name = "Cloudflare Status API" + scheduling_strategy = "round_robin" + requests { + configuration { + body = null + headers = {} + name = "https://www.cloudflarestatus.com/api/v2/status.json" + request_method = "GET" + url = "https://www.cloudflarestatus.com/api/v2/status.json" + } + validations { + actual = null + code = null + comparator = null + expected = null + extractor = "*" + name = "Extract from response body" + source = "{{response.body}}" + type = "extract_json" + value = null + variable = "response" + } + validations { + actual = null + code = "var apiResponse = JSON.parse(custom[\"response\"]);\n\nfunction createGaugeEntry(apiResponse) {\n var value = (apiResponse.status.indicator === \"none\") ? \"0\" : \"1\";\n \n var gaugeEntry = {\n \"gauge\": [\n {\n \"metric\": \"cloudflare.status\",\n \"dimensions\": {\n \"description\": apiResponse.status.description,\n \"indicator\": apiResponse.status.indicator\n },\n \"value\": value\n }\n ]\n };\n\n return gaugeEntry;\n}\n\nvar gaugeEntry = createGaugeEntry(apiResponse);\n\ncustom.payload_cloudflare = gaugeEntry;" + comparator = null + expected = null + extractor = null + name = "JavaScript run" + source = null + type = "javascript" + value = null + variable = "payload_cloudflare" + } + validations { + actual = "{{custom.payload_cloudflare}}" + code = null + comparator = "is_not_empty" + expected = null + extractor = null + name = "Assert custom variable payload_cloudflare is not empty" + source = null + type = "assert_string" + value = null + variable = null + } + } + requests { + configuration { + body = null + headers = {} + name = "curl https://www.githubstatus.com/api/v2/status.json" + request_method = "GET" + url = "https://www.githubstatus.com/api/v2/status.json" + } + validations { + actual = null + code = null + comparator = null + expected = null + extractor = "*" + name = "Extract from response body" + source = "{{response.body}}" + type = "extract_json" + value = null + variable = "response" + } + validations { + actual = null + code = "var apiResponse = JSON.parse(custom[\"response\"]);\n\nfunction createGaugeEntry(apiResponse) {\n var value = (apiResponse.status.indicator === \"none\") ? \"0\" : \"1\";\n\n var gaugeEntry = {\n \"gauge\": [\n {\n \"metric\": \"github.status\",\n \"dimensions\": {\n \"description\": apiResponse.status.description,\n \"indicator\": apiResponse.status.indicator\n },\n \"value\": value\n }\n ]\n };\n\n return gaugeEntry;\n}\n\nvar gaugeEntry = createGaugeEntry(apiResponse);\n\ncustom.payload_github = gaugeEntry;" + comparator = null + expected = null + extractor = null + name = "JavaScript run" + source = null + type = "javascript" + value = null + variable = "payload_github" + } + validations { + actual = "{{custom.payload_github}}" + code = null + comparator = "is_not_empty" + expected = null + extractor = null + name = "Assert custom variable payload_github is not empty" + source = null + type = "assert_string" + value = null + variable = null + } + } + requests { + configuration { + body = "{{custom.payload}}" + headers = { + Content-Type = "application/json" + X-SF-Token = "{{env.org_ingest_token}}" + } + name = "https://ingest.us1.signalfx.com/v2/datapoint" + request_method = "POST" + url = "https://ingest.us1.signalfx.com/v2/datapoint" + } + setup { + code = "var gaugePayloads = [\n JSON.parse(custom[\"payload_github\"]),\n JSON.parse(custom[\"payload_cloudflare\"])\n];\n\nfunction combineMultipleGaugeEntries(payloads) {\n var combinedGaugeArray = [];\n \n for (var i = 0; i < payloads.length; i++) {\n combinedGaugeArray = combinedGaugeArray.concat(payloads[i].gauge);\n }\n\n return { \"gauge\": combinedGaugeArray };\n}\n\nvar combinedGaugeEntry = combineMultipleGaugeEntries(gaugePayloads);\n\ncustom.payload = combinedGaugeEntry;" + extractor = null + name = "JavaScript run" + source = null + type = "javascript" + value = null + variable = "payload" + } + validations { + actual = "{{response.code}}" + code = null + comparator = "is_less_than" + expected = "300" + extractor = null + name = "Assert response code is less than 300" + source = null + type = "assert_numeric" + value = null + variable = null + } + } + } +} diff --git a/integration-examples/synthetics-examples/API/status-to-splunk-hec/README.md b/integration-examples/synthetics-examples/API/status-to-splunk-hec/README.md new file mode 100644 index 0000000..e5dc5c8 --- /dev/null +++ b/integration-examples/synthetics-examples/API/status-to-splunk-hec/README.md @@ -0,0 +1,17 @@ +# Third-party Status Page API Check to Metric +This example API test calls the OpenAI status endpoint and collects data on ongoing incidents and updates. +This test creates and sends a log event containing that incident data to a Splunk HEC endpoint. +The test and it's configuration are included in this directory: +- [`synthetics_status_to_splunk_hec_api_check.tf`](./synthetics_status_to_splunk_hec_api_check.tf) + - Uses the [Splunk Synthetics Terraform provider](https://registry.terraform.io/providers/splunk/synthetics/latest/docs) + +## Synthetic API Test +The synthetic API test will call the OpenAI status page and report any current and ongoing incidents to a Splunk HEC endpoint of your choice. This example is mostly to illustrate ingest arbitrary ingest into Splunk. The test serves a double function of providing external monitoring of the HEC endpoint in question in addition to providing ingest of useful incident data. + + +### Required Splunk Synthetic Global Variables +The following [global variables](https://docs.splunk.com/observability/en/synthetics/test-config/global-variables.html) are **REQUIRED** to run the included API test. +- `splunk_hec_url`: The url to your hec raw ingest (E.G. `https://hec-inputs-for-my-service.mysplunkinstance.com:443/services/collector/raw`) + - **Terraform apply will fail if this global variable does not exist in your environment!** +- `hec_token`: A provisioned hec token for basic auth (E.G. `Splunk 123412-3123-1234-abcd-1234123412abc`) + diff --git a/integration-examples/synthetics-examples/API/status-to-splunk-hec/synthetics_status_to_splunk_hec_api_check.tf b/integration-examples/synthetics-examples/API/status-to-splunk-hec/synthetics_status_to_splunk_hec_api_check.tf new file mode 100644 index 0000000..10d98f7 --- /dev/null +++ b/integration-examples/synthetics-examples/API/status-to-splunk-hec/synthetics_status_to_splunk_hec_api_check.tf @@ -0,0 +1,67 @@ +resource "synthetics_create_api_check_v2" "synthetics_status_to_splunk_hec_api_check" { + test { + active = true + automatic_retries = 0 + device_id = 34 + frequency = 60 + location_ids = ["aws-us-east-1", "aws-us-west-1"] + name = "OpenAI Status - To Splunk HEC" + scheduling_strategy = "round_robin" + requests { + configuration { + body = null + headers = {} + name = "Get OpenAI status" + request_method = "GET" + url = "https://status.openai.com/proxy/status.openai.com" + } + validations { + actual = "{{response.code}}" + code = null + comparator = "is_less_than" + expected = "300" + extractor = null + name = "Assert response code is less than 300" + source = null + type = "assert_numeric" + value = null + variable = null + } + validations { + actual = null + code = null + comparator = null + expected = null + extractor = "$.summary.ongoing_incidents[*].updates" + name = "Extract from response body" + source = "{{response.body}}" + type = "extract_json" + value = null + variable = "openai_ongoing_incidents" + } + } + requests { + configuration { + body = "{{custom.openai_ongoing_incidents}}" + headers = { + Authorization = "{{env.hec_token}}" + } + name = "Send to Splunk HEC Ingest" + request_method = "POST" + url = "{{env.splunk_hec_url}}" + } + validations { + actual = "{{response.code}}" + code = null + comparator = "is_less_than" + expected = "300" + extractor = null + name = "Assert response code is less than 300" + source = null + type = "assert_numeric" + value = null + variable = null + } + } + } +} diff --git a/integration-examples/synthetics-examples/API/token-expiration-to-metrics-api/README.md b/integration-examples/synthetics-examples/API/token-expiration-to-metrics-api/README.md new file mode 100644 index 0000000..13184c9 --- /dev/null +++ b/integration-examples/synthetics-examples/API/token-expiration-to-metrics-api/README.md @@ -0,0 +1,27 @@ +# Token Expiration using Splunk Synthetics API check +This Test queries the `/organization` endpoint of a Splunk Observability organization and retrieves the values of any tokens expiring within the next 30 days or next 7 days and sends metrics for that data to Splunk Observability. +- [`synthetics_token_expiration_api_check.tf`](./synthetics_token_expiration_api_check.tf) +This API test includes a detector which relies on metrics created by this test. That test and it's configuration are also included in this directory along with the detector as Terraform `.tf` files. + - Uses the [Splunk Synthetics Terraform provider](https://registry.terraform.io/providers/splunk/synthetics/latest/docs) +- [`detector_token_expiration.tf`](detector_token_expiration.tf) + - Uses the [Signalfx Terraform Provider](https://registry.terraform.io/providers/splunk-terraform/signalfx/latest/docs) + +## Synthetic API Test +The synthetic API test will call the [`/organization` endpoint](https://dev.splunk.com/observability/reference/api/organizations/latest#endpoint-retrieve-organization) for your Splunk Observability organization and collect the list of tokens expiring in the next 7 and 30 days. Those token names will be added as dimension attributes to two new metrics named: +- `tokens.expiring.7days` +- `tokens.expiring.30days` + +These metrics and dimensions will be sent to your organization's ingest endpoint and will power your detector. + +### Required Splunk Synthetic Global Variables +The following [global variables](https://docs.splunk.com/observability/en/synthetics/test-config/global-variables.html) are **REQUIRED** to run the included API test. +- `org_api_token`: A provisioned API token (Read-only is fine) +- `org_ingest_token`: A provisioned INGEST token +![required synthetic variables](synthetic-variables.png) + + +## Token Expiration Metrics and Detection +Both `tokens.expiring.7days` and `tokens.expiring.30days` can be charted as you normally would with any other metric. +![chart of token expiration metrics](token-expire-chart.png) + +The [included alert](./detector_token_expiration.tf) includes custom thresholds for both of the included metrics. If you'd prefer these can easily be split into two alerts of different severities. Simply alert when either of the signals is greater than 0. \ No newline at end of file diff --git a/integration-examples/synthetics-examples/API/token-expiration-to-metrics-api/detector_token_expiration.tf b/integration-examples/synthetics-examples/API/token-expiration-to-metrics-api/detector_token_expiration.tf new file mode 100644 index 0000000..710727b --- /dev/null +++ b/integration-examples/synthetics-examples/API/token-expiration-to-metrics-api/detector_token_expiration.tf @@ -0,0 +1,45 @@ +resource "signalfx_detector" "token-expiration" { + authorized_writer_teams = null + authorized_writer_users = null + description = null + disable_sampling = false + end_time = null + max_delay = 0 + min_delay = 0 + name = "Tokens Expiring 7 & 30 day Detector (jrh)" + program_text = "A = data('tokens.expiring.7days').sum(by=['token_names']).publish(label='A')\nAB = alerts(detector_name='Tokens Expiring 7 & 30 day Detector').publish(label='AB')\nB = data('tokens.expiring.30days').sum(by=['token_names']).publish(label='B')\ndetect((when(A > threshold(0))) or (when(B > threshold(0))), auto_resolve_after='6h').publish('Tokens Expiring 7 & 30 day Detector')" + show_data_markers = true + show_event_lines = false + start_time = null + tags = [] + teams = [] + time_range = 43200 + timezone = null + rule { + description = "The value of tokens.expiring.7days - Sum by token_names is above 0 OR The value of tokens.expiring.30days - Sum by token_names is above 0." + detect_label = "Tokens Expiring 7 & 30 day Detector" + disabled = false + notifications = [] + parameterized_body = "{{#if anomalous}}\n\tRule \"{{{ruleName}}}\" in detector \"{{{detectorName}}}\" triggered at {{dateTimeFormat timestamp format=\"full\"}}.\n{{else}}\n\tRule \"{{{ruleName}}}\" in detector \"{{{detectorName}}}\" cleared at {{dateTimeFormat timestamp format=\"full\"}}.\n{{/if}}\n\n{{#if anomalous}}Signal value for tokens.expiring.7days - Sum by token_names: {{inputs.A.value}}\ntoken_names: {{dimensions.token_names}}\n{{else}}\nCurrent signal value for tokens.expiring.7days - Sum by token_names: {{inputs.A.value}}\n{{/if}}\n\n\n{{#if anomalous}}Signal value for tokens.expiring.30days - Sum by token_names: {{inputs.B.value}}\ntoken_names: {{dimensions.token_names}}\n{{else}}\nCurrent signal value for tokens.expiring.30days - Sum by token_names: {{inputs.B.value}}\n{{/if}}\n\n{{#notEmpty dimensions}}\nSignal details:\n{{{dimensions}}}\n{{/notEmpty}}\n\n{{#if anomalous}}\n{{#if runbookUrl}}Runbook: {{{runbookUrl}}}{{/if}}\n{{#if tip}}Tip: {{{tip}}}{{/if}}\n{{/if}}\n\n{{#if detectorTags}}Tags: {{detectorTags}}{{/if}}\n\n{{#if detectorTeams}}\nTeams:{{#each detectorTeams}} {{name}}{{#unless @last}},{{/unless}}{{/each}}.\n{{/if}}" + parameterized_subject = "({{{detectorName}}}) Alert" + runbook_url = null + severity = "Critical" + tip = null + } + viz_options { + color = "brown" + display_name = "tokens.expiring.7days - Sum by token_names" + label = "A" + value_prefix = null + value_suffix = null + value_unit = null + } + viz_options { + color = "yellow" + display_name = "tokens.expiring.30days - Sum by token_names" + label = "B" + value_prefix = null + value_suffix = null + value_unit = null + } +} diff --git a/integration-examples/synthetics-examples/API/token-expiration-to-metrics-api/synthetic-variables.png b/integration-examples/synthetics-examples/API/token-expiration-to-metrics-api/synthetic-variables.png new file mode 100644 index 0000000..34c7668 Binary files /dev/null and b/integration-examples/synthetics-examples/API/token-expiration-to-metrics-api/synthetic-variables.png differ diff --git a/integration-examples/synthetics-examples/API/token-expiration-to-metrics-api/synthetics_token_expiration_api_check.tf b/integration-examples/synthetics-examples/API/token-expiration-to-metrics-api/synthetics_token_expiration_api_check.tf new file mode 100644 index 0000000..9047ce7 --- /dev/null +++ b/integration-examples/synthetics-examples/API/token-expiration-to-metrics-api/synthetics_token_expiration_api_check.tf @@ -0,0 +1,95 @@ +resource "synthetics_create_api_check_v2" "synthetics_token_expiration_api_check" { + test { + active = true + automatic_retries = 0 + device_id = 1 + frequency = 60 + location_ids = ["aws-us-east-1", "aws-us-west-1"] + name = "org-token-expiration-to-count-metrics" + scheduling_strategy = "round_robin" + requests { + configuration { + body = null + headers = { + Content-Type = "application/json" + X-SF-Token = "{{env.org_api_token}}" + } + name = "https://api.us1.signalfx.com/v2/organization" + request_method = "GET" + url = "https://api.us1.signalfx.com/v2/organization" + } + validations { + actual = "{{response.code}}" + code = null + comparator = "is_less_than" + expected = "300" + extractor = null + name = "Assert response code is less than 300" + source = null + type = "assert_numeric" + value = null + variable = null + } + validations { + actual = null + code = null + comparator = null + expected = null + extractor = "$.tokensExpiringInThirtyDays" + name = "Extract from response body" + source = "{{response.body}}" + type = "extract_json" + value = null + variable = "30daytokenarray" + } + validations { + actual = null + code = null + comparator = null + expected = null + extractor = "$.tokensExpiringInSevenDays" + name = "Extract from response body" + source = "{{response.body}}" + type = "extract_json" + value = null + variable = "7daytokenarray" + } + validations { + actual = null + code = "const sevenDayTokens = {\n metric: \"tokens.expiring.7days\",\n tokens: custom[\"7daytokenarray\"] ? JSON.parse(custom[\"7daytokenarray\"]) || [] : []\n};\n\nconst thirtyDayTokens = {\n metric: \"tokens.expiring.30days\",\n tokens: custom[\"30daytokenarray\"] ? JSON.parse(custom[\"30daytokenarray\"]) || [] : []\n};\n\nfunction generateGaugeJSON(...tokenObjects) {\n const result = { gauge: [] };\n \n tokenObjects.forEach(tokenObject => {\n const { metric, tokens } = tokenObject;\n tokens.forEach(token => {\n result.gauge.push({\n metric: metric,\n dimensions: { token_names: token },\n value: \"1\"\n });\n });\n });\n\n custom.body_json = result;\n return result;\n}\n\ngenerateGaugeJSON(sevenDayTokens, thirtyDayTokens);" + comparator = null + expected = null + extractor = null + name = "JavaScript run" + source = null + type = "javascript" + value = null + variable = "body_json" + } + } + requests { + configuration { + body = "{{custom.body_json}}" + headers = { + Content-Type = "application/json" + X-SF-Token = "{{env.org_ingest_token}}" + } + name = "https://ingest.us1.signalfx.com/v2/datapoint" + request_method = "POST" + url = "https://ingest.us1.signalfx.com/v2/datapoint" + } + validations { + actual = "{{response.code}}" + code = null + comparator = "is_less_than" + expected = "300" + extractor = null + name = "Assert response code is less than 300" + source = null + type = "assert_numeric" + value = null + variable = null + } + } + } +} diff --git a/integration-examples/synthetics-examples/API/token-expiration-to-metrics-api/token-expire-chart.png b/integration-examples/synthetics-examples/API/token-expiration-to-metrics-api/token-expire-chart.png new file mode 100644 index 0000000..73cb93d Binary files /dev/null and b/integration-examples/synthetics-examples/API/token-expiration-to-metrics-api/token-expire-chart.png differ diff --git a/integration-examples/synthetics-examples/Browser/hipstershop-complete-order-test-browser/README.md b/integration-examples/synthetics-examples/Browser/hipstershop-complete-order-test-browser/README.md new file mode 100644 index 0000000..8171046 --- /dev/null +++ b/integration-examples/synthetics-examples/Browser/hipstershop-complete-order-test-browser/README.md @@ -0,0 +1,35 @@ +# Synthetic Browser Check - Purchase Checkout Example +This synthetic browser check provides an example test for complex user flows (in this case checkout) on an e-commerce website ([HipsterShop](https://github.com/signalfx/microservices-demo/)). It simulates the user journey from browsing products to completing an order, ensuring critical functionalities are working correctly. +The test and it's configuration are included in this directory: +- [`synthetics_hipstershop_order_completion_browser_check.tf`](./synthetics_hipstershop_order_completion_browser_check.tf) + - Uses the [Splunk Synthetics Terraform provider](https://registry.terraform.io/providers/splunk/synthetics/latest/docs) + +## Synthetic Browser Test +The configuration leverages Terraform's synthetics browser check resource to automate interactions such as navigating URLs, selecting products, adding them to the cart, and placing orders. This example can be adapted for testing similar flows in your own applications. + +- For for more information on selectors and how to find the correct ones when building off this example check out this [Splunk Lantern article](https://lantern.splunk.com/Observability/UCE/Proactive_response/Improve_User_Experiences/Running_Synthetics_browser_tests/Selectors_for_multi-step_browser_tests)! + +## Required Setup + +1. **Replace the hipstershop URL in the test with your URL**: Modify the placeholder value in this test from `https://my-hipstershop-demo-url-should-go-here.com/` to the URL for your hipstershop instance url + +## Transaction Steps Details: + +**Home Transaction:** +Uses the go_to_url action to navigate to the Hipstershop demo site's URL. + +**Shop Transaction:** +Executes JavaScript to select a random product from a predefined list and open the product's page. + +**Cart Transaction:** +Clicks the "Add to Cart" button using an `xpath` selector to locate the button. + +**Place Order Transaction:** +Step 1: Clicks the "Place order" button using an `xpath` selector. +Step 2: Waits for 20 seconds to allow for the backend to process the order. +Step 3: Asserts that the text "Order Confirmation ID" is present on the page. + +**Keep Browsing Transaction:** +Clicks a button to navigate away from the order confirmation page + + diff --git a/integration-examples/synthetics-examples/Browser/hipstershop-complete-order-test-browser/synthetics_hipstershop_order_completion_browser_check.tf b/integration-examples/synthetics-examples/Browser/hipstershop-complete-order-test-browser/synthetics_hipstershop_order_completion_browser_check.tf new file mode 100644 index 0000000..a886adf --- /dev/null +++ b/integration-examples/synthetics-examples/Browser/hipstershop-complete-order-test-browser/synthetics_hipstershop_order_completion_browser_check.tf @@ -0,0 +1,145 @@ +resource "synthetics_create_browser_check_v2" "synthetics_hipstershop_order_completion_browser_check" { + test { + active = true + automatic_retries = 0 + device_id = 1 + frequency = 1 + location_ids = ["aws-us-east-1", "aws-us-west-1"] + name = "Hipstershop Demo - checkout test" + scheduling_strategy = "round_robin" + start_url = null + url_protocol = null + advanced_settings { + collect_interactive_metrics = true + user_agent = null + verify_certificates = true + } + transactions { + name = "Home" + steps { + action = null + duration = 0 + max_wait_time = 0 + name = "Online Boutique (add your hipstershop demo URL to this step)" + option_selector = null + option_selector_type = null + selector = null + selector_type = null + type = "go_to_url" + url = "https://my-hipstershop-demo-url-should-go-here.com/" + value = null + variable_name = null + wait_for_nav = false + wait_for_nav_timeout = 0 + } + } + transactions { + name = "Shop" + steps { + action = null + duration = 0 + max_wait_time = 0 + name = "Select Random Product" + option_selector = null + option_selector_type = null + selector = null + selector_type = null + type = "run_javascript" + url = null + value = "var products = [\n\"OLJCESPC7Z\",\n\"2ZYFJ3GM2N\",\n\"1YMWWN1N4O\",\n\"LS4PSXUNUM\",\n\"L9ECAV7KIM\",\n\"0PUK6V6EV0\",\n\"9SIQT8TOJO\",\n\"66VCHSJNUP\",\n\"6E92ZMYYFZ\"\n]\n\nvar randomProducts = products[Math.floor(Math.random() * products.length)];\n\nwindow.open(\"{{env.hipstershop_url}}/product/\" + randomProducts, \"_self\");" + variable_name = null + wait_for_nav = true + wait_for_nav_timeout = 2000 + } + } + transactions { + name = "Cart" + steps { + action = null + duration = 0 + max_wait_time = 0 + name = "Add to cart" + option_selector = null + option_selector_type = null + selector = "//button[contains(text(), \"Add to Cart\")]" + selector_type = "xpath" + type = "click_element" + url = null + value = null + variable_name = null + wait_for_nav = true + wait_for_nav_timeout = 2000 + } + } + transactions { + name = "Place Order" + steps { + action = null + duration = 0 + max_wait_time = 0 + name = "Place Order" + option_selector = null + option_selector_type = null + selector = "//button[contains(text(), \"Place order\")]" + selector_type = "xpath" + type = "click_element" + url = null + value = null + variable_name = null + wait_for_nav = true + wait_for_nav_timeout = 2000 + } + steps { + action = null + duration = 20000 + max_wait_time = 0 + name = "Wait 20 seconds" + option_selector = null + option_selector_type = null + selector = null + selector_type = null + type = "wait" + url = null + value = null + variable_name = null + wait_for_nav = false + wait_for_nav_timeout = 0 + } + steps { + action = null + duration = 0 + max_wait_time = 10000 + name = "confirm checkout" + option_selector = null + option_selector_type = null + selector = null + selector_type = null + type = "assert_text_present" + url = null + value = "Order Confirmation ID" + variable_name = null + wait_for_nav = false + wait_for_nav_timeout = 0 + } + } + transactions { + name = "Keep Browsing" + steps { + action = null + duration = 0 + max_wait_time = 0 + name = "Keep Browsing" + option_selector = null + option_selector_type = null + selector = "//a[@role='button']" + selector_type = "xpath" + type = "click_element" + url = null + value = null + variable_name = null + wait_for_nav = true + wait_for_nav_timeout = 2000 + } + } + } +} diff --git a/integration-examples/synthetics-examples/Browser/o11y-login-apm-loading-browser/README.md b/integration-examples/synthetics-examples/Browser/o11y-login-apm-loading-browser/README.md new file mode 100644 index 0000000..365ce93 --- /dev/null +++ b/integration-examples/synthetics-examples/Browser/o11y-login-apm-loading-browser/README.md @@ -0,0 +1,38 @@ +# Synthetic Browser Check - Login, Navigate, Validate Example +This Terraform configuration sets up a synthetic browser test to verify the login process and navigation within an application. It demonstrates how to log in with a username and password, navigate to specific pages, and validate the presence of certain text and elements on the page. + +## Synthetic Browser Test + +The example test is designed to demonstrate how to login with a username and password, navigate using a specific page element, and validate the correct data was displayed on the page afterwards. +Specifically it uses a service account with a username and password to log in to Splunk Observability, navigate to the APM section, and validate the text on the screen which confirms there is APM data in our environment. + +This is a contrived example using Splunk Observability only due to the audience of this repository. + - For for more information on selectors and how to find the correct ones when building off this example check out this [Splunk Lantern article](https://lantern.splunk.com/Observability/UCE/Proactive_response/Improve_User_Experiences/Running_Synthetics_browser_tests/Selectors_for_multi-step_browser_tests)! + +## Required Setup + +1. **Update Environment Variables**: The following [global variables](https://docs.splunk.com/observability/en/synthetics/test-config/global-variables.html) are **REQUIRED** to run the included API test. +- `service_acct_email`: A Splunk Observability service account's e-mail address +- `service_acct_password`: A Splunk Observability service account's password (Read permissions for safety) +2. **Adjust URL for Splunk Observability**: Adjust the url for your domain or realm (E.G. `https://app.us1.signalfx.com`) to this test + +## Transaction Steps Details: + +**Open the Login Page Transaction:** +Uses the `go_to_url` to access Splunk Observability. + +**Enter Credentials Transaction:** +- **Enter Username**: Uses the `enter_value` action with an `id` selector to input the username stored in `service_acct_email`. +- **Enter Password**: Uses the `enter_value` action with an `id` selector to input the password stored in `service_acct_password`. + +**Complete User Login Transaction:** +- **Signin**: Clicks the login button using a `css` selector with the `click_element` action. This step initiates the login process and waits for navigation. +- **View O11y Home Page**: Asserts the presence of the text `Home` on the page to confirm successful login. + +**Load Log Observer Page Transaction:** +- **Click to Open APM Page**: Navigates to the APM section by clicking the appropriate menu item using a `css` selector with the `click_element` action. +- **View APM Page**: Asserts the presence of the text `Search for service` to ensure the page loaded correctly and APM data exists in the environment. This text will not load otherwise. + +**Confirm Data Loaded Transaction:** +- **Verify Graph Visualization Rendered**: Uses the `assert_element_visible` action to check that the graph visualizations are correctly rendered on the page using `css` selectors. + diff --git a/integration-examples/synthetics-examples/Browser/o11y-login-apm-loading-browser/synthetics_o11y_login_apm_loading_browser_check.tf b/integration-examples/synthetics-examples/Browser/o11y-login-apm-loading-browser/synthetics_o11y_login_apm_loading_browser_check.tf new file mode 100644 index 0000000..39f6839 --- /dev/null +++ b/integration-examples/synthetics-examples/Browser/o11y-login-apm-loading-browser/synthetics_o11y_login_apm_loading_browser_check.tf @@ -0,0 +1,174 @@ +resource "synthetics_create_browser_check_v2" "synthetics_o11y_login_apm_loading_browser_check" { + test { + active = true + automatic_retries = 0 + device_id = 1 + frequency = 30 + location_ids = ["aws-us-east-1", "aws-us-west-1"] + name = "APM Sanity Check" + scheduling_strategy = "round_robin" + start_url = null + url_protocol = null + advanced_settings { + collect_interactive_metrics = true + user_agent = null + verify_certificates = true + } + transactions { + name = "Open the login page" + steps { + action = null + duration = 0 + max_wait_time = 0 + name = null + option_selector = null + option_selector_type = null + selector = null + selector_type = null + type = "go_to_url" + url = "https://app.us1.signalfx.com" + value = null + variable_name = null + wait_for_nav = false + wait_for_nav_timeout = 0 + } + steps { + action = null + duration = 0 + max_wait_time = 0 + name = "Enter username" + option_selector = null + option_selector_type = null + selector = "email" + selector_type = "id" + type = "enter_value" + url = null + value = "{{env.service_acct_email}}" + variable_name = null + wait_for_nav = false + wait_for_nav_timeout = 50 + } + steps { + action = null + duration = 0 + max_wait_time = 0 + name = "Enter password" + option_selector = null + option_selector_type = null + selector = "password" + selector_type = "id" + type = "enter_value" + url = null + value = "{{env.service_acct_password}}" + variable_name = null + wait_for_nav = false + wait_for_nav_timeout = 50 + } + } + transactions { + name = "Complete user login" + steps { + action = null + duration = 0 + max_wait_time = 0 + name = "Signin" + option_selector = null + option_selector_type = null + selector = "button.login-button" + selector_type = "css" + type = "click_element" + url = null + value = null + variable_name = null + wait_for_nav = true + wait_for_nav_timeout = 2000 + } + steps { + action = null + duration = 0 + max_wait_time = 10000 + name = "View O11y Home Page" + option_selector = null + option_selector_type = null + selector = null + selector_type = null + type = "assert_text_present" + url = null + value = "Home" + variable_name = null + wait_for_nav = false + wait_for_nav_timeout = 0 + } + } + transactions { + name = "Load APM page" + steps { + action = null + duration = 0 + max_wait_time = 0 + name = "Click to open APM page" + option_selector = null + option_selector_type = null + selector = "[data-test=apm-nav-menu]" + selector_type = "css" + type = "click_element" + url = null + value = null + variable_name = null + wait_for_nav = true + wait_for_nav_timeout = 2000 + } + steps { + action = null + duration = 0 + max_wait_time = 10000 + name = "View APM Page" + option_selector = null + option_selector_type = null + selector = null + selector_type = null + type = "assert_text_present" + url = null + value = "Search for service" + variable_name = null + wait_for_nav = false + wait_for_nav_timeout = 0 + } + } + transactions { + name = "Confirm data loaded" + steps { + action = null + duration = 0 + max_wait_time = 10000 + name = "Wait for data loaded" + option_selector = null + option_selector_type = null + selector = "[data-test=\"apm-automation-legend-p99\"]" + selector_type = "css" + type = "assert_element_visible" + url = null + value = null + variable_name = null + wait_for_nav = false + wait_for_nav_timeout = 0 + } + steps { + action = null + duration = 0 + max_wait_time = 10000 + name = "Verify graph visualization rendered" + option_selector = null + option_selector_type = null + selector = "[class=rv-xy-plot__inner]" + selector_type = "css" + type = "assert_element_visible" + url = null + value = null + variable_name = null + wait_for_nav = false + wait_for_nav_timeout = 0 + } + } + } +}