Setting up APIGEE Instrumentation using JavaScript Policy

## Overview

This guide explains how to set up a JavaScript policy in an Apigee proxy to instrument request and response flows. The JavaScript policy file is available at: GitHub Repository.

Prerequisites

Step 1: Setting up HTTP(S) communication between APIGEE and Astra Traffic Collector

Apigee can send data over both HTTP and HTTPS. However, using HTTPS ensures encryption and security, especially if data is sent over the public internet.

  1. Enable HTTP in config_custom.yaml Open config_custom.yaml in your traffic collector's instance and update the receivers section

receivers:
  otlp:
    protocols:
      http:
        endpoint: "0.0.0.0:4318"  # HTTP receiver on port 4318
processors:
#...

save the file once edited and restart the collector using systemctl restart astra-traffic-collector.service

  1. Enable HTTPS in config_custom.yaml (optional)

Update the same config_custom.yaml to use HTTPS with your cert and key files from the trusted authority:

receivers:
  otlp:
    protocols:
      http:
        endpoint: "0.0.0.0:4318"  # HTTP receiver on port 4318
        tls:
          cert_file: "/etc/otelcol-contrib/" 
          key_file: "/etc/otelcol-contrib/"
processors:
#...

The third field is ca_file, which is used when the certificate is self-signed or from an untrusted CA. If your certificate is issued by a trusted CA (e.g., Let's Encrypt), you don’t need to specify ca_file.

|| Ensure the certificate files are correctly placed in /etc/otelcol-contrib/ and have proper permissions. The private key should be readable only by the owner (for security reasons) & The certificate can be readable by others but should not be writable:

Next, modify your volume section in the docker-compose.yaml by adding the following lines in your Astra Traffic Collector to include volume mounts for the certificates:

volumes:
      - /:/etc/otelcol-contrib/:ro
      - /:/etc/otelcol-contrib/:ro

save the file once edited and restart the collector using systemctl restart astra-traffic-collector.service

Step 2: Create a JavaScript Policy

  1. Go to your Apigee API Proxy.

  2. Select the Develop tab.

  3. Click + Add Step in the ProxyEndpoint PostFlow section at Response time.

  4. Choose to create a new Policy with type as JavaScript under the Extension Policies.

  5. Provide a name and then upload the javascript policy available here.

Before attaching the JavaScript policy, update the following values in the script given below.

var samplingRatio = 1;                   // exampleValue: 0.5
var domainName = "your-domain.com";      // exampleValue: "mydomain.nip.io"
var sensorId = "your-sensor-id";         // exampleValue: "12345678-1234-4abc-9def-987654321000"
var astraTrafficCollectorEndpoint = "";  // exampleValue: "https?://collector-ip-or-domain-endpoint:4318/v1/traces"

//----------------------below is javascript policy. DO NOT CHANGE THIS ----------------------

// Generate a random number between 0 and 1
var randomValue = Math.random();

// Execute only if the random value is = samplingRatio
if (samplingRatio == 1 || randomValue = samplingRatio) {
    // Capture request headers
    var allReqHeaders = {};
    for (var header in request.headers) {
        allReqHeaders[header] = request.headers[header];
    }
    allReqHeaders["host"] = domainName;
    context.setVariable('allReqHeaders', JSON.stringify(allReqHeaders));

    // Capture response headers
    var allRespHeaders = {};
    for (var header in response.headers) {
        allRespHeaders[header] = response.headers[header];
    }
    context.setVariable('allRespHeaders', JSON.stringify(allRespHeaders));

    // Extract path from proxy URL
    var proxyUrl = context.getVariable('proxy.url');
    
    function getPathFromUrl(proxyUrl) {
        var firstDoubleSlash = proxyUrl.indexOf("//");
        if (firstDoubleSlash !== -1) {
            var startIndex = proxyUrl.indexOf("/", firstDoubleSlash + 2);
            return startIndex !== -1 ? proxyUrl.substring(startIndex) : "/";
        }
        return "/";
    }

    var targetPath = getPathFromUrl(proxyUrl);

    // Construct OpenTelemetry payload
    var payload = {
        "resourceSpans": [
            {
                "resource": {
                    "attributes": [
                        { "key": "service.name", "value": { "stringValue": "astra-apigee-js-instrumentation" } },
                        { "key": "sensor.id", "value": { "stringValue": sensorId } },
                        { "key": "sensor.version", "value": { "stringValue": "1.0.0" } },
                    ]
                },
                "scopeSpans": [
                    {
                        "scope": { "name": "instrumentaton" },
                        "spans": [
                            {
                                "traceId": "1234567890abcdef1234567890abcdef",
                                "spanId": "abcdef1234567890",
                                "name": "proxy-endpoint-span",
                                "kind": 1,
                                "startTimeUnixNano": Date.now() * 1e6,
                                "endTimeUnixNano": Date.now() * 1e6,
                                "attributes": [
                                    { "key": "http.method", "value": { "stringValue": context.getVariable('request.verb') } },
                                    { "key": "http.status_code", "value": { "intValue": parseInt(context.getVariable('response.status.code'), 10) } },
                                    { "key": "http.target", "value": { "stringValue": targetPath } },
                                    { "key": "http.host", "value": { "stringValue": domainName } },
                                    { "key": "http.flavor", "value": { "stringValue": context.getVariable('request.version') } },
                                    { "key": "http.scheme", "value": { "stringValue": context.getVariable('client.scheme') } },
                                    { "key": "net.sock.peer.addr", "value": { "stringValue": context.getVariable('client.ip') } },
                                    { "key": "http.request.headers", "value": { "stringValue": context.getVariable('allReqHeaders') } },
                                    { "key": "http.response.headers", "value": { "stringValue": context.getVariable('allRespHeaders') } },
                                    { "key": "http.request.body", "value": { "stringValue": context.getVariable('request.content') } },
                                    { "key": "http.response.body", "value": { "stringValue": context.getVariable('response.content') } }
                                ]
                            }
                        ]
                    }
                ]
            }
        ]
    };

    // Store payload as an Apigee variable
    context.setVariable('OTELPayload', JSON.stringify(payload));

    // Function to handle HTTP response
    function onComplete(response, error) {
        if (response) {
            if (response.status >= 200 && response.status  300) {
                print('Payload sent successfully. Response Status: ' + response.status);
            } else {
                print('Failed to send payload. Status: ' + response.status);
                print('Response Content: ' + response.content);
            }
        } else {
            print('HTTP Request Error: ' + error);
            context.setVariable('jscallout-http-error', 'Error: ' + error);
        }
    }

    // Send payload to OpenTelemetry collector
    var method = 'POST';
    var headers = { 'content-type': 'application/json' };
    var body = context.getVariable('OTELPayload');
    var req = new Request(astraTrafficCollectorEndpoint, method, headers, body);
    httpClient.send(req, onComplete);
}
  1. Click on the policy that just got created and you will be able to see a similar XML that apigee created to attach the JS File to the policy

logReqRespPolicyjsc://instrument.js

||| By default, the timeLimit attribute in policy's xml file is set to 200ms. If your Astra Traffic Collector used https secure communication, increase the timeLimit (500ms) to avoid failure of response from your backend service.

Troubleshooting

For further assistance, refer to the Apigee Documentation.