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.logfiles are found with a directory, they will be read in descending order (for instance, firstrestapi.log.2, thenrestapi.log.1, thenrestapi.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
INFOstatus.
> 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
-coption ofjqkeeps 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
HTTPverb
> 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 areSecurity,Administrator,OperatorandUser.
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 (
// emptyavoids showingnullresults)
> 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.