# Report usage data to F5 (disconnected) Type of document: How-to guide Product: NGINX Instance Manager > Prepare and submit usage reports from F5 NGINX Instance Manager to F5 in a disconnected (offline) environment using the provided bash script or REST API. --- ## Overview In a disconnected environment, NGINX Plus sends usage data to F5 NGINX Instance Manager. Download the usage report from NGINX Instance Manager and submit it to F5 from a system with internet access. After F5 verifies the report, download the acknowledgement and upload it back to NGINX Instance Manager. --- ## Before you begin Before submitting usage data to F5, configure NGINX Plus to report to NGINX Instance Manager. ### Configure NGINX Plus to report usage to NGINX Instance Manager To configure NGINX Plus (R33 and later) to report usage data to NGINX Instance Manager: 1. Allow NGINX Plus instances to connect to NGINX Instance Manager over HTTPS (TCP `443`). 1. On each NGINX Plus instance, set the [`usage_report`](https://nginx.org/en/docs/ngx_mgmt_module.html#usage_report) directive in the [`mgmt`](https://nginx.org/en/docs/ngx_mgmt_module.html) block of `/etc/nginx/nginx.conf` to point to your NGINX Instance Manager host: ```nginx mgmt { usage_report endpoint=; } ``` 1. Reload NGINX: ``` bash systemctl reload nginx ``` **Note:** If you’re using self-signed certificates with NGINX Instance Manager, see [Configure SSL verification for self-signed certificates](nim/system-configuration/secure-traffic.md#configure-ssl-verify) for additional steps. --- ## Submit usage report to F5 with NGINX Instance Manager 2.22 and later {#submit-usage-report} **Note:** Starting with NGINX Instance Manager 2.22, it's not possible to report usage with the REST API option. #### bash script (recommended) To submit the usage report in a disconnected environment, use the `offline_usage.sh` script. Run it on a system that can reach NGINX Instance Manager and connect to `https://product.connect.nginx.com/api/nginx-usage/batch` on port `443`. Replace each placeholder with your values. Download the [icon: download][offline_usage.sh](/scripts/offline_usage.sh) script and make it executable: ```shell chmod +x /offline_usage.sh ``` ### Download usage report The download option takes these arguments: - ``: The admin username for NGINX Instance Manager authentication. - ``: The admin password for NGINX Instance Manager authentication. - ``: The IP address of the NGINX Instance Manager instance. And uses this environment variable: | Variable | Description | Default value | | --- | --- | --- | | CURL_TIMEOUT | 30 | Connection timeout for curl requests in seconds | To download the usage report from NGINX Instance Manager: ```shell ./offline_usage.sh download ``` 1. The script verifies connectivity to NGINX Instance Manager over HTTPS. 1. Checks that the device is in DISCONNECTED mode (exits with an error if mode is CONNECTED). 1. Downloads the usage report as a ZIP file to `/tmp/response.zip`. ### Upload usage report To upload the usage acknowledgment to NGINX One Console: ```shell ./offline_usage.sh upload --result-dir [--endpoint-url ] ``` Where: | Argument | Description | Required | | --- | --- | --- | | `` | The path to the usage acknowledgment ZIP file downloaded using the `download` operation. | Yes | | --result-dir, -r | Directory used to track uploaded files and store unzipped contents. | Yes | | --endpoint-url, -e | Upload endpoint URL. Default: https://product.connect.nginx.com/api/nginx-usage/batch | No | The script provides the following output: | File | Description | | --- | --- | | `/uploaded_files.txt` | A text file containing the names of the successfully uploaded files. | | `/unzip/` | Extracted contents of the usage report | | `/upload_usage.log` | Detailed log of all upload attempts (in CWD) | And returns one of the following exit codes: | Code | Description | | --- | --- | | 0 | Operation completed successfully | | 1 | Error — missing arguments, connectivity failure, invalid file, wrong device mode, or upload failure | #### Web interface Download the usage report from `https:///ui/nginx-plus`. Replace `` with your NGINX Instance Manager's fully qualified domain name. Move the file to a system with internet access and run the script with the upload option. **Note:** In NGINX Instance Manager 2.22 and later, you don't need to re-upload the usage acknowledgement file to NGINX Instance Manager. **Note:** NGINX Instance Manager 2.22 and later report raw data instead of aggregated usage data. File sizes will be larger than before. --- ## Submit usage report to F5 with NGINX Instance Manager 2.21 and earlier {#submit-usage-report-2.21} **Note:** Use tools such as `curl` or [Postman](https://www.postman.com) to send requests to the NGINX Instance Manager REST API. The API base URL is `https:///api/[nim|platform]/`. All requests require authentication. For details on authentication methods, see the [API overview](/nim/fundamentals/api-overview.md).
#### bash script (recommended) ### Submit usage report with a bash script To submit a usage report in a disconnected environment, use the `license_usage_offline.sh` script. Run it on a system that can reach NGINX Instance Manager and connect to `https://product.apis.f5.com/` on port `443`. Replace each placeholder with your values.
1. [icon: download][Download license_usage_offline.sh](/scripts/license_usage_offline.sh). 1. Run the following command to allow the script to run: ```shell chmod +x /license_usage_offline.sh ``` 1. Run the script. Replace each placeholder with your specific values: ```shell ./license_usage_offline.sh \ -j .jwt \ -i \ -u admin \ -p \ -s telemetry ``` This command downloads the usage report (`report.zip`), submits it to F5 for acknowledgment, and uploads the acknowledgment back to NGINX Instance Manager. #### Full license_usage_offline.sh script ```shell #!/bin/bash # Enable strict mode for better error handling set -euo pipefail IFS=$'\n\t' # Debug mode if [[ "${DEBUG:-false}" == "true" ]]; then set -x # Enable command tracing echo "Debug mode enabled" echo "Running in directory: $(pwd)" echo "Script arguments: $*" env | grep -E 'JWT_FILE|NIM_IP|USERNAME|PASSWORD|USE_CASE' fi # Set timeouts for operations CURL_TIMEOUT=${CURL_TIMEOUT:-30} API_POLL_TIMEOUT=${API_POLL_TIMEOUT:-60} # Function to log with timestamp log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" } log "Script started" # Function to display usage usage() { echo "Usage: $0 -j -i -u -p -s " echo echo "Options:" echo " -j Path to the JWT (JSON Web Token) file used for authentication." echo " -i IP address of the NIM (NGINX Instance Manager) to connect to." echo " -u Username for login/authentication to NIM (NGINX Instance Manager)." echo " -p Password corresponding to the username for NIM (NGINX Instance Manager) authentication." echo " -s Script execution mode. One of the following:" echo " initial - Perform Initial License Activation" echo " telemetry - Perform telemetry submission: download usage report from NGINX Instance Manager and submit to F5." exit 1 } # Parse command-line arguments while getopts ":j:i:u:p:s:" opt; do case $opt in j) JWT_FILE="$OPTARG" ;; i) NIM_IP="$OPTARG" ;; u) USERNAME="$OPTARG" ;; p) PASSWORD="$OPTARG" ;; s) USE_CASE="$OPTARG" ;; *) usage ;; esac done # Check if all required arguments are provided if [ -z "${JWT_FILE:-}" ] || [ -z "${NIM_IP:-}" ] || [ -z "${USERNAME:-}" ] || [ -z "${PASSWORD:-}" ] || [ -z "${USE_CASE:-}" ]; then usage fi echo "Running $USE_CASE report" # Ensure /tmp directory exists or else create it and proceed if [ ! -d "/tmp" ]; then echo "/tmp directory does not exist. Creating it now..." mkdir -p /tmp || { echo "Failed to create /tmp directory. Exiting."; exit 1; } fi # Read JWT contents if [ ! -f "$JWT_FILE" ]; then echo -e "JWT file '$JWT_FILE' not found.$" >&2 exit 1 fi JWT_CONTENT=$(<"$JWT_FILE") # Encode credentials AUTH_HEADER=$(echo -n "$USERNAME:$PASSWORD" | base64) # Check connectivity to NGINX Instance Manager IP and the F5 licensing server (product.apis.f5.com) echo -e "Checking connectivity to NGINX Instance Manager and F5 licensing server..." # Function to test ping check_ping() { local host=$1 echo "Pinging $host... " if ! ping -c 2 -W 2 "$host" > /dev/null 2>&1; then echo -e "Cannot reach $host. Please check your network, DNS or check if any proxy is set.$" >&2 exit 1 fi echo -e "$host is reachable." } # Call the function for each host is_ipv4() { local ip=$1 if [[ $ip =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then IFS='.' read -r -a octets <<< "$ip" for octet in "${octets[@]}"; do if ((octet < 0 || octet > 255)); then return 1 fi done return 0 else return 1 fi } echo "Checking connectivity to NGINX Instance Manager using Curl ..." if ! curl -sk --output /dev/null --silent --fail --max-time $CURL_TIMEOUT "https://$NIM_IP"; then echo -e "The NGINX Instance Manager UI is not reachable on $NIM_IP" exit 1 fi echo "Checking connectivity to F5 licensing server..." SERVER_RESPONSE=$(curl -v --max-time $CURL_TIMEOUT https://product.apis.f5.com 2>&1) # Check if the server is reachable by verifying connection was established if echo "$SERVER_RESPONSE" | grep -q "Connected to product.apis.f5.com" && echo "$SERVER_RESPONSE" | grep -q "server accepted"; then echo -e "The licensing server is reachable on product.apis.f5.com" else echo -e "The licensing server is not reachable on product.apis.f5.com" echo -e "Connection details: $SERVER_RESPONSE" exit 1 fi # NGINX Instance Manager Version check VERSION_JSON=$(curl -sk -X GET "https://$NIM_IP/api/platform/v1/modules/versions" \ --header "Content-Type: application/json" \ --header "Authorization: Basic $AUTH_HEADER") NIM_VER=$(echo "$VERSION_JSON" | sed -E 's/.*"nim"[ \t]*:[ \t]*"([0-9]+\.[0-9]+)(\.[0-9]+)?".*/\1/') echo "Current version of NGINX Instance Manager is $NIM_VER" # Construct JSON payload JSON_PAYLOAD=$(cat <&2 if echo "$HTTP_BODY" | jq -r '.message' | grep -q "failed to register token. already registered"; then echo -e "NGINX Instance Manager already registered and licensed.\nIf needed, terminate the current license manually in the NGINX Instance Manager UI and re-run the script with the correct license.\nhttps://docs.nginx.com/nginx-instance-manager/disconnected/add-license-disconnected-deployment/" fi exit 1 fi fi if [[ "$NIM_VER" < "2.18" ]]; then echo "NGINX Instance Manager version $NIM_VER is not supported by this script. Please use NGINX Instance Manager 2.18 or later" exit 1 elif [[ "$NIM_VER" == "2.18" ]] || [[ "$NIM_VER" == "2.19" ]]; then echo "NGINX Instance Manager version $NIM_VER detected." # Send the PUT request and separate body and status code PUT_RESPONSE_CODE=$(curl -k -s -w "%{http_code}" -o /tmp/put_response.json --location --request PUT "https://$NIM_IP/api/platform/v1/license?telemetry=true" \ --header "Content-Type: application/json" \ --header "Authorization: Basic $AUTH_HEADER" \ --data '{ "desiredState": { "content": "'"$JWT_CONTENT"'", "type": "JWT", "features": [ {"limit": 0, "name": "NGINX_NAP_DOS", "valueType": ""}, {"limit": 0, "name": "IM_INSTANCES", "valueType": ""}, {"limit": 0, "name": "TM_INSTANCES", "valueType": ""}, {"limit": 0, "name": "DATA_PER_HOUR_GB", "valueType": ""}, {"limit": 0, "name": "NGINX_INSTANCES", "valueType": ""}, {"limit": 0, "name": "NGINX_NAP", "valueType": ""}, {"limit": 0, "name": "SUCCESSFUL_API_CALLS_MILLIONS", "valueType": ""}, {"limit": 0, "name": "IC_PODS", "valueType": ""}, {"limit": 0, "name": "IC_K8S_NODES", "valueType": ""} ] }, "metadata": { "name": "license" } }') echo "Response status code: $PUT_RESPONSE_CODE" if [[ "$PUT_RESPONSE_CODE" == "200" ]]; then echo -e "(legacy): License applied successfully in DISCONNECTED mode for version $NIM_VER." else echo -e "(legacy): License PUT request failed. Status code: $PUT_RESPONSE_CODE$" echo "clear the license database and re-trigger the script again" exit 1 fi fi if [[ "$USE_CASE" != "telemetry" ]]; then RESPONSE=$(curl -sS -k --max-time 10 -w "\n%{http_code}" -X POST "https://$NIM_IP/api/platform/v1/license?telemetry=true" \ -H "Content-Type: application/json" \ -H "Authorization: Basic $AUTH_HEADER" \ -d "$JSON_PAYLOAD") HTTP_BODY=$(echo "$RESPONSE" | sed '$d') HTTP_STATUS=$(echo "$RESPONSE" | tail -n1) echo -e "License applied successfully in DISCONNECTED mode." fi sleep 5 echo "Executing telemetry tasks" if [[ "$NIM_VER" == "2.18" ]] || [[ "$NIM_VER" == "2.19" ]]; then if [[ "$USE_CASE" == "initial" ]]; then HTTP_RESPONSE=$(curl -k -sS -w "\n%{http_code}" --location "https://$NIM_IP/api/platform/v1/report/download?format=zip&reportType=initial" \ --header "accept: application/json" \ --header "authorization: Basic $AUTH_HEADER" \ --header "content-type: application/json" \ --output /tmp/response.zip) else prepare_usage_command="curl --insecure --location 'https://$NIM_IP/api/platform/v1/report/download?format=zip&reportType=telemetry&telemetryAction=prepare' \ --header 'accept: application/json' \ --header 'authorization: Basic $AUTH_HEADER' report_save_path="${output_file:-/tmp/response.zip}" download_usage_command="curl --insecure --location 'https://$NIM_IP/api/platform/v1/report/download?format=zip&reportType=telemetry&telemetryAction=download' \ --header 'accept: */*' \ --header 'authorization: Basic $AUTH_HEADER' \ --output \"$report_save_path\"" if [ "$USE_CASE" == "telemetry" ]; then echo "Running telemetry stage: " response=$(eval $prepare_usage_command) sleep 2 if echo "$response" | grep -q '"telemetry":"Report generation in progress"'; then echo -e "Success: Report generation is in progress." else echo -e "Failure: Report generation not in progress or unexpected response." exit 1 fi echo "Running command: $download_usage_command" eval $download_usage_command else echo "Running command: $download_usage_command" eval $download_usage_command fi fi else HTTP_RESPONSE=$(curl -k -sS -w "\n%{http_code}" --location "https://$NIM_IP/api/platform/v1/report/download?format=zip" \ --header "accept: application/json" \ --header "authorization: Basic $AUTH_HEADER" \ --header "content-type: application/json" \ --output /tmp/response.zip) HTTP_STATUS=$(echo "$HTTP_RESPONSE" | tail -n1) if [ "$HTTP_STATUS" -ne 200 ]; then echo -e "Failed to download usage report from NGINX Instance Manager. HTTP Status Code: $HTTP_STATUS" >&2 echo "Please verify that NGINX Instance Manager is reachable and the credentials are correct." >&2 echo "(or) Verify that NGINX Instance Manager is licensed before using the 'telemetry' flag (run it with 'initial' first)." rm -f /tmp/response.zip exit 1 fi fi echo -e "Usage report downloaded successfully as '/tmp/response.zip'." echo "Uploading the usage report to F5 Licensing server" TEEM_UPLOAD_URL="https://product.apis.f5.com/ee/v1/entitlements/telemetry/bulk" UPLOAD_RESULT=$(curl -sS -w "\n%{http_code}" --location "$TEEM_UPLOAD_URL" \ --header "Authorization: Bearer $JWT_CONTENT" \ --form "file=@/tmp/response.zip") UPLOAD_STATUS=$(echo "$UPLOAD_RESULT" | tail -n1) UPLOAD_BODY=$(echo "$UPLOAD_RESULT" | sed '$d') if [ "$UPLOAD_STATUS" -ne 202 ]; then echo -e "Usage report upload failed. HTTP Status: $UPLOAD_STATUS$" >&2 echo "Response Body: $UPLOAD_BODY" >&2 exit 1 fi if ! echo "$UPLOAD_BODY" | jq empty >/dev/null 2>&1; then echo -e "Upload response is not valid JSON. Response: $UPLOAD_BODY$" >&2 exit 1 fi STATUS_LINK=$(echo "$UPLOAD_BODY" | jq -r '.statusLink // empty') if [ -z "$STATUS_LINK" ]; then echo -e "Failed to extract statusLink from the upload response. Response: $UPLOAD_BODY$" >&2 exit 1 fi echo "StatusLink extracted: $STATUS_LINK" STATUS_ID=$(echo "$STATUS_LINK" | sed 's|/ee/v1/entitlements/telemetry/bulk/status/||') echo "Validating the report status" echo "Validating report status using status ID: $STATUS_LINK" STATUS_URL="https://product.apis.f5.com/ee/v1/entitlements/telemetry/bulk/status/$STATUS_ID" sleep 5 STATUS_RESPONSE=$(curl -k -sS -w "\n%{http_code}" --location "$STATUS_URL" \ --header "Authorization: Bearer $JWT_CONTENT") STATUS_BODY=$(echo "$STATUS_RESPONSE" | sed '$d') STATUS_CODE=$(echo "$STATUS_RESPONSE" | tail -n1) if [ "$STATUS_CODE" -ne 200 ]; then echo -e "Status check failed. HTTP Status: $STATUS_CODE$" >&2 echo "Response Body: $STATUS_BODY" >&2 exit 1 fi if ! echo "$STATUS_BODY" | jq empty >/dev/null 2>&1; then echo -e "Invalid JSON in status body: $STATUS_BODY$" >&2 exit 1 fi PERCENTAGE_COMPLETE=$(echo "$STATUS_BODY" | jq -r '.percentageComplete') PERCENTAGE_SUCCESSFUL=$(echo "$STATUS_BODY" | jq -r '.percentageSuccessful') READY_FOR_DOWNLOAD=$(echo "$STATUS_BODY" | jq -r '.readyForDownload') TIME_LIMIT=30 START_TIME=$(date +%s) elapsed_time() { echo $(($(date +%s) - $START_TIME)) } while true; do if [ "$PERCENTAGE_COMPLETE" -eq "100" ] && [ "$READY_FOR_DOWNLOAD" == "true" ]; then echo -e "Validating Report." break fi if [ $(elapsed_time) -ge "$TIME_LIMIT" ]; then echo -e "Time limit exceeded. Report validation failed." echo " percentageComplete: $PERCENTAGE_COMPLETE" echo " percentageSuccessful: $PERCENTAGE_SUCCESSFUL" echo " readyForDownload: $READY_FOR_DOWNLOAD" echo " F5 upload issue failed even after 30 seconds" echo " re-run the script" exit 1 fi echo -e "Report validation failed. Waiting for conditions to be met...$" echo " percentageComplete: $PERCENTAGE_COMPLETE" echo " percentageSuccessful: $PERCENTAGE_SUCCESSFUL" echo " readyForDownload: $READY_FOR_DOWNLOAD" sleep 5 done echo -e "Report validated successfully. All conditions met." echo "Downloading report from F5 License server..." DOWNLOAD_URL="https://product.apis.f5.com/ee/v1/entitlements/telemetry/bulk/download/$STATUS_ID" DOWNLOAD_RESPONSE=$(curl -sS -w "%{http_code}" --location "$DOWNLOAD_URL" \ --header "Authorization: Bearer $JWT_CONTENT" \ --output /tmp/response_teem.zip) HTTP_STATUS=$(echo "$DOWNLOAD_RESPONSE" | tail -n1) if [ "$HTTP_STATUS" -ne 200 ]; then echo -e "Failed to download the report from F5. HTTP Status Code: $HTTP_STATUS$" >&2 exit 1 fi echo -e "Report downloaded successfully from F5 as '/tmp/response_teem.zip'." echo "Uploading the license acknowledgement to NGINX Instance Manager..." UPLOAD_URL="https://$NIM_IP/api/platform/v1/report/upload" UPLOAD_RESPONSE=$(curl -k -sS --location "$UPLOAD_URL" \ --header "Authorization: Basic $AUTH_HEADER" \ --form "file=@/tmp/response_teem.zip" \ -w "%{http_code}" -o /tmp/temp_response.json) HTTP_STATUS=$(echo "$UPLOAD_RESPONSE" | tail -n1) UPLOAD_MESSAGE=$(cat /tmp/temp_response.json | jq -r '.message') if ! [[ "$HTTP_STATUS" =~ ^[0-9]+$ ]]; then echo -e "Invalid HTTP status code. Response: $UPLOAD_RESPONSE$" >&2 exit 1 fi if [ "$UPLOAD_MESSAGE" != "Report uploaded successfully." ] || [ "$HTTP_STATUS" -ne 200 ]; then echo -e "Upload failed. Response: $UPLOAD_RESPONSE$" >&2 exit 1 fi echo -e "Acknowledgement uploaded successfully to NGINX Instance Manager." ``` #### REST ### Submit usage report with curl To submit a usage report using `curl`, complete each step in order. Run these commands on a system that can reach NGINX Instance Manager and connect to `https://product.apis.f5.com/` on port `443`. Replace each placeholder with your values. **Note:** The `-k` flag skips SSL certificate validation. Use this only if your NGINX Instance Manager is using a self-signed certificate or if the certificate is not trusted by your system. 1. **Prepare the usage report**: ```shell curl -k --location 'https:///api/platform/v1/report/download?format=zip&reportType=telemetry&telemetryAction=prepare' \ --header 'accept: application/json' \ --header 'authorization: Basic ' \ --header 'referer: https:///ui/settings/license' ``` 1. **Download the usage report from NGINX Instance Manager**: ```shell curl -k --location 'https:///api/platform/v1/report/download?format=zip&reportType=telemetry&telemetryAction=download' \ --header 'accept: */*' \ --header 'authorization: Basic ' \ --output report.zip ``` 1. **Submit the usage report to F5 for verification**: ```shell curl --location 'https://product.apis.f5.com/ee/v1/entitlements/telemetry/bulk' \ --header "Authorization: Bearer $(cat /path/to/jwt-file)" \ --form 'file=@".zip"' ``` After running this command, find the `statusLink` in the response. The `report-id` is the UUID at the end of the `statusLink` value. For example: ```json {"statusLink":"/status/2214e480-3401-43a3-a54c-9dc501a01f83"} ``` In this example, the `report-id` is `2214e480-3401-43a3-a54c-9dc501a01f83`. Use your `report-id` in the following steps. 1. **Check the status of the usage acknowledgement**: Replace `` with your specific ID from the previous response. ```shell curl --location 'https://product.apis.f5.com/ee/v1/entitlements/telemetry/bulk/status/{report_id}' \ --header "Authorization: Bearer $(cat /path/to/jwt-file)" ``` 1. **Download the usage acknowledgement from F5**: ```shell curl --location 'https://product.apis.f5.com/ee/v1/entitlements/telemetry/bulk/download/{report_id}' \ --header "Authorization: Bearer $(cat /path/to/jwt-file)" \ --output .zip ``` 1. **Upload the usage acknowledgement to NGINX Instance Manager**: ```shell curl -k --location 'https:///api/platform/v1/report/upload' \ --header 'Authorization: Basic ' \ --form 'file=@".zip"' ``` #### Web interface ### Submit usage report with the web interface #### Download usage report On the **License > Overview** page, select **Download License Report**. #### Submit usage report to F5 To submit the report and download the acknowledgment, follow steps 3–5 in the [**REST**](#add-license-submit-initial-usage-report) tab. #### Upload the usage acknowledgement 1. On the **License > Overview** page, select **Upload Usage Acknowledgement**. 1. Select **Browse** or drag the file into the form. 1. Select **Add**. --- ## What’s reported {#telemetry} By default, NGINX Plus sends usage data to F5 every hour in a `POST` request. The report includes information such as traffic volume, runtime, and instance activity. Here’s an example of a usage report: ```json { "version": "", "uuid": "", "nap": "", // F5 WAF for NGINX status "http": { "client": { "received": 0, // bytes received "sent": 0, // bytes sent "requests": 0 // HTTP requests processed }, "upstream": { "received": 0, // bytes received "sent": 0 // bytes sent } }, "stream": { "client": { "received": 0, // bytes received "sent": 0 // bytes sent }, "upstream": { "received": 0, // bytes received "sent": 0 // bytes sent } }, "workers": 0, // number of worker processes running "uptime": 0, // seconds the instance has been running "reloads": 0, // number of reloads "start_time": "epoch", // start of data collection "end_time": "epoch" // end of data collection } --- ## Error log location and monitoring {#log-monitoring} All usage reporting logs are written by the `nms-integrations` process. Where you find them depends on your deployment: | Deployment | Log location | |------------|--------------| | Linux (systemd) | `journalctl -u nms-integrations` or `/var/log/nms/nms.log` | | Container | `docker logs ` or `kubectl logs -c integrations` | Monitor the following log prefixes to identify issues generating and exporting offline usage reports. **`[OfflineAggregator]`** — offline usage report packaging: ```text [error] [OfflineAggregator] PullSubscribe failed consumer=: [error] [OfflineAggregator] error fetching messages: [error] [OfflineAggregator] error processing message msg_index=: (skipping) [warn] [OfflineAggregator] proto.Unmarshal failed: (data_len=) [warn] [OfflineAggregator] message silently dropped (likely proto unmarshal failure) msg_index= [error] [OfflineAggregator] failed to delete stale consumer=: [info] [OfflineAggregator] aggregation complete: total_directories= total_processed= ... [info] [OfflineAggregator] empty fetch attempt / (total_processed_so_far=) [info] [OfflineAggregator] consecutive empty fetches, stream exhausted ``` Monitor the [NGINX error log](https://nginx.org/en/docs/ngx_core_module.html#error_log), usually at `/var/log/nginx/error.log`, to identify subscription issues early. The log records problems such as failed usage reports or licenses that are about to expire. Check it regularly to avoid downtime and stay compliant. You can also use the [license API endpoint](https://demo.nginx.com/api/9/license) to check license status programmatically. For details, see the [ngx_http_api_module docs](https://nginx.org/en/docs/http/ngx_http_api_module.html#def_nginx_license_object). Examples of log entries: - **Failed usage reports:** ``` text [error] 36387#36387: server returned 500 for : during usage report [error] 36528#36528: : could not be resolved (host not found) during usage report [error] 36619#36619: connect() failed (111: Connection refused) for : during usage report [error] 38888#88: server returned 401 for :443 during usage report ``` - **License nearing expiration:** ``` text [warn] license will expire in 14 days ``` - **License expired:** ``` text [alert] license expiry; grace period will end in 89 days [emerg] license expired ``` **Note:** NGINX Plus stops processing traffic if the license has been expired for more than 90 days.