In this blog post, I will guide you on how to query the restapi.log file of your GoldenGate deployments more efficiently. These logs are a valuable source of information for GoldenGate administrators but are unfortunately not easy to analyze. In fact, their format is not really suited for a complex analysis. With what I present here, you could improve the debugging of GoldenGate issues, the automation of your deployments, or compare the web UI built-in calls with what you want to achieve when calling the REST API.

Where can I find the restapi.log files ?

The restapi.log files are located in the var/log folder of your deployment. This is valid for:

  • You service manager, for instance /u01/app/oracle/product/oggsm/var/log/restapi.log.
  • Any of your deployments, for instance /u01/app/oracle/product/ogg_test_01/var/log/restapi.log.

No matter your installation, you will always have at least two active restapi.log files (one for the service manager and one per deployment). Depending on what you want to analyze, you will have to analyze one file or the other.

What does a restapi.log entry look like ?

A typical restapi.log entry looks like this :

2026-02-01 07:59:28.566+0000 INFO |RestAPI.adminsrvr | Request #23315: {
    "context": {
        "verb": "DELETE",
        "uri": "/services/v2/currentuser",
        "protocol": "http",
        "host": "vmogg",
        "securityEnabled": false,
        ...
    }
    Response: {
        ...
    }

While these logs are very informative, their structure makes it nearly impossible to analyze efficiently. These logs have no clear structure, even though one entry can be identified by the first line in the form YYYY-MM-DD HH:MM:SS.xxx+0000 INFO |RestAPI.service | Request #req_no: {. And to make it even harder, what follows is not a valid JSON object. There are in fact two JSON objects:

  • Request #req_no, containing the request that was made.
  • Response, containing the response from the API.

Making restapi.log entries more readable

Below is a Python script that you can use to read and transform your restapi.log entries. It takes three arguments:

  • The path to a specific log file or a log directory. If multiple restapi.log files are found with a directory, they will be read in descending order (for instance, first restapi.log.2, then restapi.log.1, then restapi.log)
  • The path to the new log file which will be generated. This log file which will contain one log entry per line, in a JSON format.
  • [Optional] The path to a log file containing all invalid records.
#!/usr/bin/env python3
import re
import sys
import json
import logging
from pathlib import Path
from datetime import datetime

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
)

REQUEST_START_RE = re.compile(
    r"^\d{4}-\d{2}-\d{2} "
    r"\d{2}:\d{2}:\d{2}\.\d{3}\+\d{4}\s+"
    r"\w+\s*\|\s*RestAPI\..*?\|\s*Request #\d+:"
)


def find_restapi_logs(directory: Path):
    """
    Return restapi logs ordered OLDEST -> NEWEST
    so records split across rotations are reconstructed.
    """
    def sort_key(p: Path):
        if p.name == "restapi.log":
            return (1, 0)
        m = re.search(r"\.(\d+)$", p.name)
        return (0, int(m.group(1)) if m else 0)

    files = [f for f in directory.glob("restapi*") if f.is_file()]
    return sorted(files, key=sort_key)


def parse_json_block(text: str):
    try:
        text_clean = re.sub(r",\s*([}\]])", r"\1", text)
        return json.loads(text_clean)
    except json.JSONDecodeError as e:
        raise ValueError(f"JSON parse error: {e}") from e


def extract_first_json_object(s: str) -> str:
    depth = 0
    start = None
    in_string = False
    escape = False

    for i, ch in enumerate(s):
        if in_string:
            if escape:
                escape = False
            elif ch == "\\":
                escape = True
            elif ch == '"':
                in_string = False
            continue

        if ch == '"':
            in_string = True
        elif ch == "{":
            if depth == 0:
                start = i
            depth += 1
        elif ch == "}":
            depth -= 1
            if depth == 0 and start is not None:
                return s[start:i + 1]

    raise ValueError("No complete JSON object found")


def extract_request_response(text_block: str):
    # Match log header (timestamp, status, service)
    log_header_match = re.search(
        r"^(\d{4}-\d{2}-\d{2} "
        r"\d{2}:\d{2}:\d{2}\.\d{3}\+\d{4})\s+"
        r"(\w+)\s*\|\s*RestAPI\.(.*?)\|",
        text_block,
        re.MULTILINE
    )
    restapi_datetime = log_header_match.group(1) if log_header_match else None
    restapi_status = log_header_match.group(2) if log_header_match else None
    restapi_service = log_header_match.group(3).strip() if log_header_match else None

    # Convert datetime to epoch
    restapi_epoch = None
    if restapi_datetime:
        try:
            dt = datetime.strptime(restapi_datetime, "%Y-%m-%d %H:%M:%S.%f%z")
            restapi_epoch = int(dt.timestamp())
        except Exception as e:
            logging.warning(f"Failed to parse restapi_datetime '{restapi_datetime}': {e}")

    # Extract request number
    reqno_match = re.search(r"Request #(\d+)", text_block)
    restapi_reqno = int(reqno_match.group(1)) if reqno_match else None

    parts = text_block.split("Response:", 1)

    request_raw = extract_first_json_object(parts[0])
    request = parse_json_block(request_raw)

    response = None
    if len(parts) > 1:
        try:
            response_raw = extract_first_json_object(parts[1])
            response = parse_json_block(response_raw)
        except Exception as e:
            logging.warning(
                f"Response parse error for reqno {restapi_reqno}: {e}, storing raw"
            )
            response = {"raw": parts[1].strip()}

    return {
        "request": request,
        "response": response,
        "restapi_datetime": restapi_datetime,
        "restapi_epoch": restapi_epoch,
        "restapi_status": restapi_status,
        "restapi_service": restapi_service,
        "restapi_reqno": restapi_reqno,
    }


def parse_logs(log_files, output_file: Path, invalid_file: Path = None):
    total_found = total_written = total_invalid = 0
    invalid_out = invalid_file.open("w", encoding="utf-8") if invalid_file else None

    buffer = []
    in_record = False

    with output_file.open("w", encoding="utf-8") as out:
        for log_file in log_files:
            file_found = file_written = file_invalid = 0
            logging.info(f"Processing {log_file}")

            with log_file.open("r", encoding="utf-8", errors="ignore") as f:
                for line in f:
                    if REQUEST_START_RE.match(line):
                        if buffer:
                            total_found += 1
                            file_found += 1
                            record_text = "".join(buffer)
                            try:
                                record = extract_request_response(record_text)
                                out.write(json.dumps(record, ensure_ascii=False) + "\n")
                                total_written += 1
                                file_written += 1
                            except Exception:
                                total_invalid += 1
                                file_invalid += 1
                                if invalid_out:
                                    invalid_out.write(record_text + "\n")
                        buffer = [line]
                        in_record = True
                    elif in_record:
                        buffer.append(line)

            logging.info(
                f"{log_file.name}: {file_found} found, {file_written} written, {file_invalid} invalid"
            )

        # Final flush
        if buffer:
            total_found += 1
            try:
                record = extract_request_response("".join(buffer))
                out.write(json.dumps(record, ensure_ascii=False) + "\n")
                total_written += 1
            except Exception:
                total_invalid += 1
                if invalid_out:
                    invalid_out.write("".join(buffer) + "\n")

    if invalid_out:
        invalid_out.close()

    logging.info(
        f"DONE: {total_found} records found total, "
        f"{total_written} written, {total_invalid} invalid"
    )


def main():
    if len(sys.argv) < 3:
        print(f"Usage: {sys.argv[0]} <log_directory> <output_ndjson> [invalid_file]")
        sys.exit(1)

    log_dir = Path(sys.argv[1])
    output_file = Path(sys.argv[2])
    invalid_file = Path(sys.argv[3]) if len(sys.argv) > 3 else None

    log_files = find_restapi_logs(log_dir)

    logging.info(f"Input is a directory: {log_dir}")
    logging.info(
        "Using log files (from oldest to newest): "
        + ", ".join(f.name for f in log_files)
    )

    parse_logs(log_files, output_file, invalid_file)


if __name__ == "__main__":
    main()

You can either analyze all the log files, or just a single file.

# Single file analysis
python3 restapi_to_ndjson.py /path/to/var/log/restapi.log restapi.ndjson

# Complete analysis
python3 restapi_to_ndjson.py /path/to/var/log restapi.ndjson

Here is a usage example of the script given above. It analyzes all restapi.log files inside a ogg_test_01 GoldenGate deployment.

> python3 restapi_to_ndjson.py /u01/app/oracle/product/ogg_test_01/var/log /home/oracle/restapi_test_01.ndjson
oracle@vmogg:~/ [OGG] python3 restapi_to_ndjson.py /u01/app/oracle/product/ogg_test_01/var/log /home/oracle/restapi_test_01.ndjson
2026-02-14 07:34:14,934 INFO Input is a directory: /u01/app/oracle/product/ogg_test_01/var/log
2026-02-14 07:34:14,934 INFO Using log files (from oldest to newest): restapi.log.1, restapi.log.2, restapi.log.3, restapi.log
2026-02-14 07:34:14,935 INFO Processing /u01/app/oracle/product/ogg_test_01/var/log/restapi.log.1
2026-02-14 07:34:15,807 INFO restapi.log.1: 614 found, 614 written, 0 invalid
2026-02-14 07:34:15,807 INFO Processing /u01/app/oracle/product/ogg_test_01/var/log/restapi.log.2
2026-02-14 07:34:16,679 INFO restapi.log.2: 740 found, 740 written, 0 invalid
2026-02-14 07:34:16,679 INFO Processing /u01/app/oracle/product/ogg_test_01/var/log/restapi.log.3
2026-02-14 07:34:17,567 INFO restapi.log.3: 938 found, 938 written, 0 invalid
2026-02-14 07:34:17,567 INFO Processing /u01/app/oracle/product/ogg_test_01/var/log/restapi.log
2026-02-14 07:34:17,670 INFO restapi.log: 70 found, 70 written, 0 invalid
2026-02-14 07:34:17,673 INFO DONE: 2363 records found total, 2363 written, 0 invalid

This generates a restapi_test_01.ndjson file. Let’s see in the next chapter what one entry looks like.

Log entry example

Here is an example of the transformed log entry. It is still not quite readable given the amount of information, but we can at least filter the logs more efficiently now because of the ndjson structure (each line is a valid JSON document).

> tail -1 restapi_test_01.ndjson
{"request": {"context": {"httpContextKey": 140656324316240, "verbId": 2, "verb": "GET", "originalVerb": "GET", "uri": "/services/v2/extracts", "protocol": "http", "headers": {"Host": "vmogg:7810", "User-Agent": "python-requests/2.32.3", "Accept-Encoding": "gzip, deflate", "Accept": "application/json", "Connection": "keep-alive", "Content-Type": "application/json", "Authorization": "** Masked **", "X-OGG-Requestor-Id": "", "X-OGG-Feature-List": ""}, "host": "vmogg:7810", "securityEnabled": false, "authorization": {"authUserName": "ogg", "authPassword": "** Masked **", "authMode": "Basic", "authUserRole": "Security"}, "requestId": 2208, "uriTemplate": "/services/{version}/extracts", "catalogUriTemplate": "/services/{version}/metadata-catalog/extracts"}, "isScaRequest": true, "content": null, "parameters": {"uri": {"version": "v2"}}}, "response": {"context": {"httpContextKey": 140656324316240, "requestId": 2208, "code": "200 OK", "headers": {"Content-Type": "application/json", "Set-Cookie": "** Masked **"}, "Content-Type": "application/json", "contentType": "application/json"}, "isScaResponse": true, "content": {"$schema": "api:standardResponse", "links": [{"rel": "canonical", "href": "http://vmogg:7810/services/v2/extracts", "mediaType": "application/json"}, {"rel": "self", "href": "http://vmogg:7810/services/v2/extracts", "mediaType": "application/json"}, {"rel": "describedby", "href": "http://vmogg:7810/services/v2/metadata-catalog/extracts", "mediaType": "application/schema+json"}], "messages": [], "response": {"$schema": "ogg:collection", "items": [{"links": [{"rel": "parent", "href": "http://vmogg:7810/services/v2/extracts", "mediaType": "application/json"}, {"rel": "canonical", "href": "http://vmogg:7810/services/v2/extracts/TEST", "mediaType": "application/json"}], "$schema": "ogg:collectionItem", "name": "TEST", "status": "running"}]}}}, "restapi_datetime": "2026-02-09 15:49:26.581+0000", "restapi_epoch": 1770652166, "restapi_status": "INFO", "restapi_service": "adminsrvr", "restapi_reqno": 294698}

Or in a better format, displayed with jq:

oracle@vmogg:~/ [OGG] tail -1 restapi_test_01.ndjson | jq
{
  "request": {
    "context": {
      "httpContextKey": 140656324316240,
      "verbId": 2,
      "verb": "GET",
      ...
    }
  },
  "response": {
    "context": {
      "httpContextKey": 140656324316240,
      "requestId": 2208,
      "code": "200 OK",
      ...
    },
    "isScaResponse": true,
    "content": {
      "$schema": "api:standardResponse",
      ...
      "messages": [],
      "response": {
        "items": [
          {
            ...
            "$schema": "ogg:collectionItem",
            "name": "TEST",
            "status": "running"
          }
        ]
      }
    }
  },
  "restapi_datetime": "2026-02-09 15:49:26.581+0000",
  "restapi_epoch": 1770652166,
  "restapi_status": "INFO",
  "restapi_service": "adminsrvr",
  "restapi_reqno": 294698
}

Here are the keys available:

  • request: original Request #reqno element of the log.
  • response: original response element of the log.
  • restapi_datetime: date string of the log entry.
  • restapi_epoch: jq-readable timestamp, useful for filtering.
  • restapi_status: status displayed in the log header (INFO, ERROR, etc.)
  • restapi_service: GoldenGate service displayed in the log header (adminsrvr, distsrvr, etc.).
  • restapi_reqno: request number.

To analyze the results of a call, the interesting part usually lies in the response.content.response.items section. But let me present a few of the calls you could make with this.

Examples of jq queries on restapi.ndjson

There are a ton of very useful call examples. Here are a few interesting ones to use on your GoldenGate deployments. Of course, feel free to build your own !

  • Get the number of each event type.
> jq -r '.restapi_status' restapi.ndjson | sort | uniq -c
     74 ERROR
   2283 INFO

Or if you want a JSON output for monitoring.

jq -s '
  group_by(.restapi_status)
  | map({status: .[0].restapi_status, count: length})
' restapi.ndjson
[
  {
    "status": "ERROR",
    "count": 74
  },
  {
    "status": "INFO",
    "count": 2283
  }
]
  • Search all logs that do not have the INFO status.
> jq 'select(.restapi_status != "INFO")' restapi.ndjson
{
  "context": {
    "httpContextKey": 139991246113872,
    "verbId": 2,
    "verb": "GET",
...
    "restapi_status": "ERROR",
    "restapi_service": "adminsrvr",
    "restapi_reqno": 15
}
  • As a reminder, the -c option of jq keeps the results with a single JSON document per line, which is perfect for our use case.
> jq -c 'select(.restapi_status != "INFO")' restapi.ndjson
{"context":{"httpContextKey":139991246113872,"verbId":2,"verb":"GET",...,"restapi_status":"ERROR","restapi_service":"adminsrvr","restapi_reqno":15}
  • Retrieve the list of distinct services.
> jq -r '.restapi_service' restapi.ndjson | sort | uniq
adminsrvr
distsrvr
pmsrvr
recvsrvr
  • Retrieve a log count per service.
> jq -r '.restapi_service' restapi.ndjson | sort | uniq -c
    623 adminsrvr
   1413 distsrvr
    154 pmsrvr
    167 recvsrvr
  • Retrieve all logs for a specific service.
jq -c 'select(.restapi_service == "adminsrvr")' restapi.ndjson
  • Count occurrences of each HTTP verb
> jq -r '.request.context.verb' restapi.ndjson | sort | uniq -c
      6 DELETE
   1773 GET
      8 PATCH
    574 POST
      2 PUT
  • Request all logs for a specific HTTP verb.
jq -c 'select(.request.context.verb == "PUT")' restapi.ndjson
  • Count logs per authUserRole
> jq -r '.request.context.authorization.authUserRole' restapi.ndjson | sort | uniq -c
      2 any
     42 null
   2319 Security
  • Retrieve a specific authUserRole. As a reminder, the available roles are Security, Administrator, Operator and User.
jq -c 'select(.request.context.authorization.authUserRole == "Operator")' restapi.ndjson
  • Request by authUserName (GoldenGate user used when calling the API).
jq -r '.request.context.authorization.authUserName' restapi.ndjson | sort | uniq -c
  • Specific authUserName.
jq -c 'select(.request.context.authorization.authUserName == "ogg")' restapi.ndjson
  • Find logs where the response code is not 200 OK.
jq -c 'select(.response.context.code != "200 OK")' restapi.ndjson
  • Retrieve logs from the last hour.
jq -c --argjson now $(date +%s) '. | select(.restapi_epoch >= ($now - 3600))' restapi.ndjson
  • Retrieve logs from the last 24 hours.
jq -c --argjson now $(date +%s) '. | select(.restapi_epoch >= ($now - 86400))' restapi.ndjson
  • Retrieve a specific request number (reqno).
jq 'select(.restapi_reqno == 689)' restapi.ndjson
  • Retrieve logs between two specific dates.
START_EPOCH=$(date -d "2026-02-14 07:26:01 UTC" +%s)
END_EPOCH=$(date -d "2026-02-14 07:26:02 UTC" +%s)

jq --argjson s "$START_EPOCH" --argjson e "$END_EPOCH" \
   'select(.restapi_epoch >= $s and .restapi_epoch <= $e)' \
   restapi.ndjson
  • Get a timeline view of your logs, with only the date of the request, the status, the HTTP verb and the URI.
jq -r '"\(.restapi_datetime) \(.restapi_status) \(.request.context.verb) \(.request.context.uri)"' restapi.ndjson | head
2026-01-30 17:02:54.382+0000 INFO POST /services/v2/authorizations/security/ogg
2026-01-30 17:02:58.618+0000 INFO POST /services/v2/config/files/GLOBALS
2026-01-30 17:44:20.879+0000 ERROR GET /services/v2/currentuser
2026-01-30 17:44:20.898+0000 ERROR GET /services/v2/currentuser
2026-01-30 17:44:23.309+0000 ERROR GET /services/v2/config/health
2026-01-30 17:44:23.309+0000 ERROR GET /services/v2/currentuser
2026-01-30 17:44:23.382+0000 INFO GET /services/v2/config/health
2026-01-30 17:44:23.406+0000 INFO GET /services/v2/config/health
2026-01-30 17:44:23.648+0000 INFO GET /services/v2/config/summary
2026-01-30 17:44:23.656+0000 INFO GET /services/v2/installation/services
  • Get a count of the most used URI.
> jq -r '.request.context.uri' restapi.ndjson | sort | uniq -c | sort -nr | head -10
    529 /services/v2/commands/execute
    454 /services/v2/sources
    243 /services/v2/config/summary
    148 /services/v2/currentuser
    106 /services/v2/extracts
     62 /services/v2/stream
     57 /services/v2/installation/services
     52 /services/v2/replicats
     43 /services/v2/config/types/ogg:encryptionProfile/values/ogg:encryptionProfile:LocalWallet
     33 /services/v2/logs/default
  • Get a count of the most used URI templates.
> jq -r '.request.context.uriTemplate' restapi.ndjson | sort | uniq -c | sort -nr | head -10
    529 /services/{version}/commands/execute
    454 /services/{version}/sources
    243 /services/{version}/config/summary
    148 /services/{version}/currentuser
    110 /services/{version}/extracts
     87 /services/{version}/sources/{distpath}
     67 /services/{version}/credentials/{domain}/{alias}
     62 /services/{version}/stream
     57 /services/{version}/installation/services
     53 /services/{version}/config/types/{type}/values/{value}
  • Search for a specific URI
jq -c 'select(.request.context.uri == "/services/v2/sources")' restapi.ndjson
  • Search for a specific URI template
jq -c 'select(.request.context.uriTemplate == "/services/{version}/sources")' restapi.ndjson
  • Number of request per certificate (// empty avoids showing null results)
> jq -r '.request.parameters.uri.certificate // empty' restapi.ndjson \
| sort | uniq -c
     11 ogg_23
     22 ogg_target
     19 ogg_target2
  • Top callers
> jq -r '.request.context.headers."X-Real-IP"' restapi.ndjson \
| sort | uniq -c | sort -nr
   1385 null
    874 10.0.0.1
     96 vmogg
      2 10.0.0.2
  • Search for the request coming from a specific IP
jq -c 'select(.request.context.headers["X-Real-IP"] == "10.0.0.2")' restapi.ndjson

Of course, feel free to design your own jq calls to suit your restapi.log analysis or monitoring needs. And for more information on the API, check out my other blogs on the subject, as well as the GoldenGate documentation.