Add Logs to a Run

To use this guide, install the Nominal Python library with pip3 install nominal.

See Quickstart for more details.

Please contact us if you’re not sure whether your organization has access to Nominal.

Log files are a standard output from aircraft, land vehicles, manufacturing equipment, and practically any present-day machine with an on-board computer.

Nominal makes it simple to upload log files to a Nominal Run for collaborative inspection, root-cause analysis, and automated alerting.

In Nominal, Runs are time bounded views onto multimodal test data - including Datasets, Videos, Logs, and database connections.

To see your organization’s latest Runs, head over to the Runs page

Connect to Nominal

Concepts
  • Base URL: The URL through which the Nominal API is accessed (typically https://api.gov.nominal.io/api; shown under Settings → API keys).
  • Workspace: A mechanism by which to isolate datasets; each user has one or more workspace, and data in one cannot be seen from another. Note that a token / API key is attached to a user, and may access multiple workspaces.
  • Profile: A combination of base URL, API key, and workspace.

There are two primary ways of authenticating the Nominal Client. The first is to use a profile stored on disk, and the second is to use a token directly.

Run the following in a terminal and follow on-screen prompts to set up a connection profile:

$$ nom config profile add default
>
># Alternatively, if `nom` is missing from the path:
>$ python -m nominal config profile add default

Here, “default” can be any name chosen to represent this profile (reminder: a profile represents a base URL, API key, and workspace).

The profile will be stored in ~/.config/nominal/config.yml, and can then be used to create a client:

1from nominal.core import NominalClient
2
3client = NominalClient.from_profile("default")
4
5# Get details about the currently logged-in user to validate authentication
6# Will display an object like: `User(display_name='your_email@your_company.com', ...)`
7print(client.get_user())

If you have previously used nom to store credentials, prior to the availability of profiles, you will need to migrate your old configuration file (~/.nominal.yml) to the new format (~/.config/nominal/config.yml).

You can do this with the following command:

$nom config migrate
>
># Or, if `nom` is missing from your path:
>python -m nominal config migrate
1from nominal.core import NominalClient
2
3# Get an instance of the client using provided credentials
4client = NominalClient.from_token("<insert api key>")
5
6# Get details about the currently logged-in user to validate authentication
7# Will display an object like: `User(display_name='your_email@your_company.com', ...)`
8print(client.get_user())

NOTE: you should never share your Nominal API key with anyone. We therefore recommend that you not save it in your code and/or scripts.

  • If you trust the computer you are on, use nom to store the credential to disk.
  • Otherwise, use a password manager such as 1password or bitwarden to keep your token safe.
If you’re not sure whether your company has a Nominal tenant, please reach out to us.

Create a Run

We are considering the following dataset:

1import polars as pl
2
3df = pl.read_csv('hf://datasets/nominal-io/frosty-flight/frosty_flight_1k_rows.csv')
4df.write_csv('frosty_flight_1k_rows.csv')
5print(df.head())

Next, we upload that data to Nominal:

from nominal.core import NominalClient
client = NominalClient.from_profile("default")
dataset = client.create_dataset(
name='Frosty Flight measurements',
)
dataset.add_tabular_data(
'frosty_flight_1k_rows.csv',
timestamp_column = 'source_time',
timestamp_type = 'iso_8601'
)

This dataset is part of an experimental Run. We create the Run, and attach the dataset:

flight_simulator_run = client.create_run(
name='Frosty Flight',
start=df['source_time'].min(),
end=df['source_time'].max()
)
flight_simulator_run.add_dataset(
'measurements',
dataset
)

If you navigate to your organization’s Runs page, you’ll see a Run at the top called Frosty Flight.

Generate a log file

Next, we’ll generate a demo log file, and add it to our Run.

To make the demo log file visually interesting and distinct, we’ll add a random sparkline chart to each log file line:

1import random
2
3def generate_sparkline(length=10):
4 """Generate a random sparkline in ASCII."""
5 data = [random.randint(1, 10) for _ in range(length)]
6 chars = "▁▂▃▄▅▆▇█"
7 max_value = max(data)
8 min_value = min(data)
9
10 def normalize(value):
11 if max_value == min_value:
12 return 0
13 return int((value - min_value) / (max_value - min_value) * (len(chars) - 1))
14
15 sparkline = ''.join(chars[normalize(value)] for value in data)
16 return sparkline

Nominal log files are defined in Python as a list of tuples:

1logs = [
2 (df['source_time'][i], f"Log message {i} {generate_sparkline(random.randint(15, 40))}")
3 for i in range(len(df['source_time']))
4]
5
6for row in logs[:5]:
7 print(row)
('2024-06-08T05:58:42.000Z', 'Log message 0 ▅▅▅▂▅█▆▆▁▅▆▅▇███▆▅▄█')
('2024-06-08T05:58:51.000Z', 'Log message 1 ▆▃▃▃▅▃▄▃▅▁▁▂▁▄▄█▄▇▂▇▆▁▇█▃▁▅▆▃▁█▆')
('2024-06-08T05:58:52.000Z', 'Log message 2 ▁▂▄▃▄▄▂▄▃▆▁▇▁▄▅▄▄▄▄█▁▄▇▂▂▁')
('2024-06-08T05:58:52.000Z', 'Log message 3 ▁█▅▅▆▃▇▂█▂▇▃▄▂▇▆▁▁▄▃▄▁█▄█▂')
('2024-06-08T05:58:52.000Z', 'Log message 4 ▄██▁▄▆▃▁█▄▃▂▄▁▇▄')

Finally, add the log file to flight_simulator_run:

1log_set = client.create_log_set(
2 name="Sparkline Logs",
3 logs=logs,
4 description="description"
5)
6
7ref_name = "fun_with_logs"
8
9flight_simulator_run.add_log_set(ref_name, log_set)

If you visit the Datasets tab of the ‘Frosty Flight’ Runs page, you’ll see “Sparkline Logs” in the “Data scopes” tab.

To inspect “Sparkline Logs”, open the Run in an empty Workbook and click on the “Logs” pane at the bottom of the window.

Retrieve log files

To retrieve a log file, use client.get_log_set() and LogSet.stream_logs().

You can find the resource ID (“RID”) for log files under the “Data sources” tab of any Run that has an associated log file.

1log = client.get_log_set("ri.data-source.cerulean-staging.log-set.8bc1f396-a42e-40db-9680-e686e288e45d")
2
3counter = 1
4for line in log.stream_logs():
5 counter += 1
6 print(line)
7 if counter > 5:
8 break
Log(timestamp=1728058190751960000, body='Log message 0 ▅▅▅▂▅█▆▆▁▅▆▅▇███▆▅▄█')
Log(timestamp=1728058191751960000, body='Log message 1 ▆▃▃▃▅▃▄▃▅▁▁▂▁▄▄█▄▇▂▇▆▁▇█▃▁▅▆▃▁█▆')
Log(timestamp=1728058192751960000, body='Log message 2 ▁▂▄▃▄▄▂▄▃▆▁▇▁▄▅▄▄▄▄█▁▄▇▂▂▁')
Log(timestamp=1728058193751960000, body='Log message 3 ▁█▅▅▆▃▇▂█▂▇▃▄▂▇▆▁▁▄▃▄▁█▄█▂')
Log(timestamp=1728058194751960000, body='Log message 4 ▄██▁▄▆▃▁█▄▃▂▄▁▇▄')