Skip to content

Add opt-in API call logging for LANforge JSON requests - #320

Open
Sidartha-CT wants to merge 18 commits into
greearb:masterfrom
goyalsaurabh06:api_logs_rex
Open

Add opt-in API call logging for LANforge JSON requests#320
Sidartha-CT wants to merge 18 commits into
greearb:masterfrom
goyalsaurabh06:api_logs_rex

Conversation

@Sidartha-CT

Copy link
Copy Markdown
Contributor

Add opt-in API call logging for LANforge JSON requests

Summary

Adds a lightweight, opt-in CSV logger for every json_get / json_post / json_put / json_delete call made through either of LANforge's two client paths:

  • py-json/LANforge/lfcli_base.pyRealm
  • lanforge_client/lanforge_api.pyLFJsonCommand / LFJsonQuery

When enabled via:

lf_webpage.py --save_api

each call's method, URL, payload, HTTP status, and error/diagnostics get appended to a CSV (default ~/lf_api_calls.csv), which is copied into the run's report folder.

This is meant for debugging a specific test run, not for permanent instrumentation.


Why a new lanforge_client/api_logger.py module

We want to record specific fields per call (method, url, payload, status, error) and post-process them later as CSV, so the recording logic needed to live inside json_get() / json_post() / json_put() / json_delete() themselves rather than being bolted on from outside.

There are two independent call paths that implement those four methods:

  • LFCliBase (py-json, used by Realm and most scripts)
  • BaseLFJsonRequest (lanforge_client/lanforge_api.py, not currently used by interop)

Rather than duplicating logging logic in both, the common pieces (CSV schema, enable/disable state, the actual record() call) live in one new module in lanforge_client/, and each call path just calls into it.

lanforge_client is the natural home since it's already a shared dependency of both.


Why global (module-level) state instead of a constructor parameter

The enable flag is deliberately process-wide global state in api_logger.py, not a parameter threaded through Realm / LFCliBase / profile constructors.

Reason:

Realm gets instantiated directly all over the py-scripts tree, and profile classes build their own nested instances.

For example:

  • lf_webpage.py holds a Realm
  • It also imports l4_cxprofile, which itself instantiates its own Realm

To pass an "enable logging" flag through the constructor, it would need to be threaded through every one of those call sites, and there's no guarantee the import/instantiation chain doesn't go a level or two deeper in other scripts.

A module-level configure() called once near the top of a script's main() avoids that — every LFCliBase / Realm or LFJsonCommand / LFJsonQuery created afterward in the same process just picks it up.


What's covered / not covered

Covered

  • LFRequest.py now captures last_response_code and a one-line last_diagnostics summary (reason + X-Error-* headers) on every request, win or fail, so callers have something worth logging.
  • lfcli_base.py (LFCliBase, used by Realm) and lanforge_api.py (BaseLFJsonRequest) both call api_logger.record(...) after every GET / POST / PUT / DELETE, using those diagnostics.

Not covered

  • A small number of scripts don't go through json_get() / etc. at all — those aren't covered yet and will be migrated separately.

Pause / resume

monitor_for_runtime_csv() in lf_webpage.py polls in a tight loop and would otherwise flood the CSV with repetitive, rarely-useful entries.

Added:

  • api_logger.pause()
  • api_logger.resume()

so state (enabled + filename) is preserved but recording is suppressed during that loop by default.

--log_monitor_api_calls opts back in if someone actually wants those calls logged too.


Usage

lf_webpage.py ... \
    --save_api \
    [--api_log_file_name /path/to/log.csv] \
    [--log_monitor_api_calls] (this is somthing change wrt script ) 

Commits

  • LFRequest.py — capture response code + diagnostics summary per request
  • api_logger.py — new process-wide CSV logger (configure / record / is_enabled)
  • lfcli_base.py — wire _log_api_call into json_get / json_post / json_put / json_delete
  • lanforge_api.py — same, for the lanforge_client request path
  • lf_webpage.py — add --save_api / --api_log_file_name flags, configure logger, copy CSV into report dir
  • api_logger.py — add pause() / resume()
  • lf_webpage.py — pause logging during the runtime-CSV monitor loop by default, add --log_monitor_api_calls override


Future Scope

The current changes improve the existing API logging by capturing additional request information and storing it in a structured format for easier debugging. Potential future enhancements include:

  • Extend API call capture to scripts and code paths that do not currently use the common json_get(), json_post(), json_put(), and json_delete() interfaces.

  • Record REST request latency alongside the existing response code and diagnostics to help identify slow responses.

  • Capture additional system state when communication issues are detected, such as:

    • Device presence in /ports/list
    • Endpoint update timestamps
    • /resources/ response status
    • TX/RX byte counters and other activity metrics
  • Add configurable troubleshooting indicators that detect missed report intervals and automatically record additional diagnostic information.

  • Include high-level runtime summaries, such as:

    • Running, waiting, and stopped connection counts
    • Station state summary (associated, unassociated, with IP, down, phantom)
    • Distribution of endpoint update intervals
    • Average REST response time during the test run
  • Improve the generated API logs with optional summaries or filtering to simplify correlating API activity with existing LANforge logs and test failures.

@Sidartha-CT

Copy link
Copy Markdown
Contributor Author

Here is the log csv that generated out of this when we ran lf_webpage.py :
lf_api_calls.csv

Comment thread lanforge_client/api_logger.py Outdated
_logging_paused = False
if not _logging_enabled:
return
_log_filename = log_filename or os.path.join(os.path.expanduser('~'), 'lf_api_calls.csv')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might we consider:
from pathlib import Path
home_dir = Path.home()

Returns:
None.
"""
global _logging_enabled, _log_filename, _logging_paused

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would appreciate an alternative to using globals.

CSV_FIELDS = ['api_timestamp', 'api_method', 'api_url', 'api_status', 'api_response_code',
'api_payload', 'api_error', 'api_diagnostics']

_logging_enabled = False

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would appreciate an alternative to using globals. Can we turn these into class members?

_log_filename = None


def configure_api_call_logging(enabled: bool, log_filename: Optional[str] = None) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason we cannot design these methods into an ApiLogger class?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like all of these changes will be lost on the next API generation. These changes need to live in btbits/client/candela/lanforge/json_api.py

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing that out. Is this something that needs to be updated on the btbits side first? I don't have access to that repository, so I'm not very familiar with how the API generation flow works. If these changes need to be made there before they're generated into this repo, please let me know.

Comment thread py-scripts/lf_webpage.py Outdated
optional.add_argument("--test_priority", default="", help="dut model for kpi.csv, test-priority is arbitrary number")
optional.add_argument("--test_id", default="lf_webpage", help="test-id for kpi.csv, script or test name")
optional.add_argument('--csv_outfile', help="--csv_outfile <Output file for csv data>", default="")
optional.add_argument('--save_api', help="save json_get/json_post/json_put/json_delete calls to a lightweight csv log file",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"save_api" is a misleading term, please consider "enable_api_logging"

status = "UNKNOWN"
try:
with open(_log_filename, 'a', newline='') as csv_file:
csv.writer(csv_file).writerow([

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the time the message is sent, it refers to the time that the csv is recording this data

… request

Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
…call logging

Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
…default

Verified CLI :  python3 lf_webpage.py --ap_name "Cisco" --mgr 192.168.207.75 --ssid Cisco-5g --security wpa2 --passwd sharedsecret --upstream_port eth1 --duration 20s --bands 5G --client_type Real --file_size 2MB --save_api

Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
…stamps

record_api_call() previously stamped datetime.now() at CSV-write time,
which is after the request/response cycle completes. Accept an optional
sent_at param and use it instead when the caller provides it.

Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
json_post() and get() now record sent_at right before the request is
issued and pass it through to api_logger.record_api_call() on every
success/error path, instead of letting it default to log-write time.

Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
Adds a last_sent_at attribute, set immediately before urlopen() in
json_post() and get(), so callers can log the actual send time rather
than the time the call happened to finish.

Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
_log_api_call() now accepts sent_at and forwards it to
api_logger.record_api_call(); json_post/json_put/json_get/json_delete
pass lf_r.last_sent_at so the CSV timestamp reflects send time.

Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
record_api_call() now accepts an optional elapsed_ms param, measured
by the caller with time.perf_counter() around just the request itself
(excludes diagnostics/logging overhead), and writes it to a new
api_elapsed_ms CSV column so slow calls can be spotted at a glance.

Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
json_post() and get_as_json() now wrap the urlopen()/read() call with
time.perf_counter() and pass the resulting elapsed_ms through to
api_logger.record_api_call() on every success/error path, so the CSV
log reflects the actual network call time rather than a timestamp
delta that includes diagnostics overhead.

Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
Match the terser placeholder style rather than the verbose
"No payload" string in the api_payload column.

Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
…ration

Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
…logger

Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
@Sidartha-CT

Sidartha-CT commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@memnochproxy ,

Why global (module-level) state instead of a constructor parameter?

The logging enable flag is intentionally kept as process-wide module state in api_logger.py instead of being passed through constructors.

Realm is instantiated in many places across the py-scripts tree, and profile classes often create their own nested Realm instances. Passing a logging flag through constructors would require updating every instantiation path, including nested ones, making the change invasive and harder to maintain.

By calling configure() once near the beginning of the script, every LFCliBase/Realm and LFJsonCommand/LFJsonQuery created afterward automatically uses the same configuration within the process.

For now, this approach achieves the required functionality with minimal changes. Once this PR is approved, I'll revisit the implementation and, if feasible, migrate it to a class-based design while preserving the same behavior.
++ @smileyrekiere

Signed-off-by: Sidartha-CT <neelapu.sidartha@candelatech.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants