Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Name | Brief Description
[**SSG DB Connection Monitor**](./SSG-DB-Connection-Monitor) | *Monitor the count of MySQL database connections and generate an alert when it is not 'normal'*
[**Custom Assertion Plugin**](./custom-assertion-plugin) | *The Layer7 API Management Custom Assertion Plugin makes it very quick and easy to build new custom assertions for the Layer7 API Gateway using the Eclipse IDE.*
[**FindAssertions-GraphmanUtility**](./FindAssertions-GraphmanUtility) | *This directory contains scripts for searching and exporting services based on assertion types in Layer7 API Gateway policies.*
[**Unused APIs**](./Unused-APIs) | *This directory contains scripts for searching which APIs exposed by a gateway are not being used*



## Using the Utilities
Expand Down
39 changes: 39 additions & 0 deletions Unused-APIs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

# Purpose #
The purpose of this script is to cross correlate information coming from the gateway (Graphman) with run time traces obtained over a perior of time, in order to find which services exposed by the gateway have not been used during that period of time.

# How to #
1/ The list of APIs/services defined on the gateway is obtained from graphman as follow:

graphman export --gateway <gateway> --using services:summary --output services.json

The result of the output is in a file "service.json"

2/ This example assumes a global message received policy such as the "tracing_MessageReceived_policy.json" (Graphman bundle), which writes a message like "Service called: ${request.http.uri}" in the gateway audit/logs, is defined in the gateway. The result of the output is in a file "service_traces.logs". Whenever a service is called, a trace is generated. This trace includes the service URI.

3/ To find the list of used and unused services based on these two files, the python script "find_unused_services.py" is used as follows:

python3 find_unused_services.py --services services.json --traces service_traces.logs

The output looks is something like:

#### Unused services ####
| Service Name | Resolution Path |
|--------------|-----------------|
| ACME | /ACMEWarehouse* |
| AI Basic | /ai/basic |
| Analyse a Certificate | /analyse/cert |
| Authenticate User | /auth/user |
| Bank Service | /bank |
...

#### Used services ####
| Service Name | Resolution Path |
|--------------|-----------------|
| echo | /echotest |
| test1 | /test1 |

# Modifications #
In case you use a different trace, you need to modify the Python script line 20 in order to adjust the regex:

pattern = re.compile(r'"message":"Service called: (/.*?)"')
88 changes: 88 additions & 0 deletions Unused-APIs/find_unused_services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import json
import re
import argparse
import os

def find_unused_services(services_json_content, log_content):
# Parse services.json
services_data = json.loads(services_json_content)
all_services = []
for service in services_data["services"]:
all_services.append({
"name": service["name"],
"resolutionPath": service["resolutionPath"]
})

# Extract called resolution paths from logs based on the new pattern
called_paths = set()
# Regex to find "message":"Service called: <uri>" and extract <uri>
# The URI starts with '/' and can contain any characters until the next double quote
pattern = re.compile(r'"message":"Service called: (/.*?)"')

for line in log_content.splitlines():
match = pattern.search(line)
if match:
uri = match.group(1)
called_paths.add(uri)

# Identify uncalled and used services
uncalled_services = []
used_services = []
for service in all_services:
service_path = service["resolutionPath"]
is_called = False

if service_path.endswith("*"):
base_path = service_path[:-1]
for called_path in called_paths:
if called_path.startswith(base_path):
is_called = True
break
else:
if service_path in called_paths:
is_called = True

if not is_called:
uncalled_services.append(service)
else:
used_services.append(service)

return uncalled_services, used_services

def format_services_table(heading, services_list):
output = f"*** {heading} ***\n"
if services_list:
output += "| Service Name | Resolution Path |\n"
output += "|--------------|-----------------|\n"
for s in services_list:
output += f"| {s['name']} | {s['resolutionPath']} |\n"
else:
output += f"No {heading.lower()} found.\n"
return output

if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Find unused services by comparing a services JSON file with a log file.")
parser.add_argument("--services", required=True, help="Path to the services JSON file (e.g., services.json)")
parser.add_argument("--traces", required=True, help="Path to the service traces log file (e.g., service_traces.logs)")

args = parser.parse_args()

# Read the services JSON file
if not os.path.exists(args.services):
print(f"Error: Services file not found at {args.services}")
exit(1)
with open(args.services, 'r') as f:
services_json_content = f.read()

# Read the log file
if not os.path.exists(args.traces):
print(f"Error: Traces file not found at {args.traces}")
exit(1)
with open(args.traces, 'r') as f:
log_content = f.read()

uncalled, used = find_unused_services(services_json_content, log_content)

print(format_services_table("Unused services", uncalled))
print("\n") # Add a newline for separation
print(format_services_table("Used services", used))
4 changes: 4 additions & 0 deletions Unused-APIs/service_traces.logs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"package":"com.l7tech.server.policy.assertion.ServerAuditDetailAssertion","level":"INFO","log":{"detail-id":-4.0,"message":"Service called: /test1","listen-port":"Default HTTPS (8443)","client-ip":"10.244.0.50","request-id":"627f8aeeb0c5d911-f1d889e1c356579b"},"time":"2026-05-26T11:21:32.873+0000","otelId":"bc77ccae43f69f9c7853eb112747853f-aacdd4e0149fc379"}
{"package":"com.l7tech.server.policy.assertion.ServerAuditDetailAssertion","level":"INFO","log":{"detail-id":-4.0,"message":"Service called: /test1","listen-port":"Default HTTPS (8443)","client-ip":"10.244.0.50","request-id":"627f8aeeb0c5d911-f1d889e1c356579b"},"time":"2026-05-26T11:21:32.873+0000","otelId":"bc77ccae43f69f9c7853eb112747853f-aacdd4e0149fc379"}
{"package":"com.l7tech.server.policy.assertion.ServerAuditDetailAssertion","level":"INFO","log":{"detail-id":-4.0,"message":"Service called: /echotest","listen-port":"Default HTTPS (8443)","client-ip":"127.0.0.1","request-id":"627f8aeeb0c5d911-f1d889e1c356579c"},"time":"2026-05-26T11:21:32.883+0000","otelId":"bc77ccae43f69f9c7853eb112747853f-314bbe8740ddad6f"}
{"package":"com.l7tech.server.policy.assertion.ServerAuditDetailAssertion","level":"INFO","log":{"detail-id":-4.0,"message":"Service called: /echotest","listen-port":"Default HTTPS (8443)","client-ip":"127.0.0.1","request-id":"627f8aeeb0c5d911-f1d889e1c356579c"},"time":"2026-05-26T11:21:32.883+0000","otelId":"bc77ccae43f69f9c7853eb112747853f-314bbe8740ddad6f"}
Loading