tinybird 4.1.1.dev0__tar.gz → 4.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/PKG-INFO +17 -1
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/__cli__.py +2 -2
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/cli.py +1 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/create.py +3 -2
- tinybird-4.2.1/tinybird/tb/modules/logs.py +676 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird.egg-info/PKG-INFO +17 -1
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird.egg-info/SOURCES.txt +1 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/setup.cfg +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/__cli__.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/ch_utils/constants.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/ch_utils/engine.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/check_pypi.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/client.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/config.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/context.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/datafile/common.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/datafile/exceptions.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/datafile/parse_connection.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/datafile/parse_datasource.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/datafile/parse_pipe.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/datatypes.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/feedback_manager.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/git_settings.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/prompts.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/service_datasources.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/sql.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/sql_template.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/sql_template_fmt.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/sql_toolset.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/syncasync.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/check_pypi.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/client.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/config.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/branch.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/build.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/build_common.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/cicd.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/cli.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/common.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/config.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/connection.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/connection_kafka.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/connection_s3.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/copy.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/build.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/build_common.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/build_pipe.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/diff.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/fixture.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/format_common.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/format_connection.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/pipe_checker.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/playground.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datafile/pull.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/datasource.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/deployment.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/deployment_common.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/deprecations.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/endpoint.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/exceptions.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/feedback_manager.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/fmt.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/info.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/infra.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/job.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/job_common.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/llm.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/llm_utils.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/local.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/local_common.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/local_logs.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/login.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/login_common.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/logout.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/materialization.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/open.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/pipe.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/preview.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/project.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/project_commands.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/py_project.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/query_output.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/regions.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/secret.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/secret_common.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/sink.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/table.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/telemetry.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/test.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/test_common.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/token.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/ts_project.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/watch.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/workspace.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb/modules/workspace_members.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/auth.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/branch.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/cicd.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/cli.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/common.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/config.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/connection.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/datasource.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/exceptions.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/fmt.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/job.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/pipe.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/regions.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/tag.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/telemetry.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/test.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/workspace.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tb_cli_modules/workspace_members.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird/tornado_template.py +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird.egg-info/dependency_links.txt +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird.egg-info/entry_points.txt +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird.egg-info/requires.txt +0 -0
- {tinybird-4.1.1.dev0 → tinybird-4.2.1}/tinybird.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: tinybird
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.1
|
|
4
4
|
Summary: Tinybird Command Line Tool
|
|
5
5
|
Home-page: https://www.tinybird.co/docs/forward/commands
|
|
6
6
|
Author: Tinybird
|
|
@@ -52,6 +52,12 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
|
|
|
52
52
|
Changelog
|
|
53
53
|
----------
|
|
54
54
|
|
|
55
|
+
4.1.1
|
|
56
|
+
*******
|
|
57
|
+
|
|
58
|
+
- `Changed` `tb init` now installs `tinybird-python-sdk-guidelines` when selecting the Python SDK project type.
|
|
59
|
+
- `Changed` `tb init` now defaults `dev_mode` to `branch` and persists that value in `tinybird.config.json`.
|
|
60
|
+
|
|
55
61
|
4.1.0
|
|
56
62
|
*******
|
|
57
63
|
|
|
@@ -95,6 +101,16 @@ Changelog
|
|
|
95
101
|
|
|
96
102
|
- `Changed` Prompt-based AI flows now print a deprecation warning (`tb --prompt`, `tb create --prompt`, `tb datasource create --prompt`, `tb test create`, `tb mock`) ahead of their removal in a future release.
|
|
97
103
|
|
|
104
|
+
3.3.2
|
|
105
|
+
*******
|
|
106
|
+
|
|
107
|
+
- `Added` `tb logs` command to query service observability data sources from the CLI.
|
|
108
|
+
- `Added` support for `--start`, `--end`, `--source`, `--limit`, and `--expand` in `tb logs`.
|
|
109
|
+
- `Added` support for `--source '*'` in `tb logs` to query all supported sources.
|
|
110
|
+
- `Changed` default `tb logs` sources to `tinybird.datasources_ops_log`, `tinybird.pipe_stats_rt`, and `tinybird.jobs_log`.
|
|
111
|
+
- `Changed` `tb logs` table output to show full datetime (`YYYY-MM-DD HH:MM:SS`) in `TIME` and to hide temporal fields from `DETAILS`.
|
|
112
|
+
- `Fixed` `tb logs` to correctly parse SQL responses returned as JSON strings.
|
|
113
|
+
|
|
98
114
|
3.3.1
|
|
99
115
|
*******
|
|
100
116
|
|
|
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
|
|
|
4
4
|
__url__ = 'https://www.tinybird.co/docs/forward/commands'
|
|
5
5
|
__author__ = 'Tinybird'
|
|
6
6
|
__author_email__ = 'support@tinybird.co'
|
|
7
|
-
__version__ = '4.
|
|
8
|
-
__revision__ = '
|
|
7
|
+
__version__ = '4.2.1'
|
|
8
|
+
__revision__ = '7e04532'
|
|
@@ -16,6 +16,7 @@ import tinybird.tb.modules.job
|
|
|
16
16
|
import tinybird.tb.modules.local
|
|
17
17
|
import tinybird.tb.modules.login
|
|
18
18
|
import tinybird.tb.modules.logout
|
|
19
|
+
import tinybird.tb.modules.logs
|
|
19
20
|
import tinybird.tb.modules.materialization
|
|
20
21
|
import tinybird.tb.modules.open
|
|
21
22
|
import tinybird.tb.modules.pipe
|
|
@@ -18,7 +18,7 @@ from tinybird.tb.modules.project import Project
|
|
|
18
18
|
|
|
19
19
|
DEFAULT_FOLDER = "tinybird"
|
|
20
20
|
DEFAULT_SDK = "cli"
|
|
21
|
-
DEFAULT_MODE = "
|
|
21
|
+
DEFAULT_MODE = "branch"
|
|
22
22
|
DEFAULT_CICD = "skip"
|
|
23
23
|
SDK_CHOICES = ("typescript", "python", "cli")
|
|
24
24
|
MODE_CHOICES = ("branch", "local", "manual")
|
|
@@ -32,6 +32,7 @@ SKILLS_INSTALL_BASE_ARGS = [
|
|
|
32
32
|
GLOBAL_AGENT_SKILL = "tinybird"
|
|
33
33
|
PROJECT_TYPE_AGENT_SKILLS = {
|
|
34
34
|
"cli": "tinybird-cli-guidelines",
|
|
35
|
+
"python": "tinybird-python-sdk-guidelines",
|
|
35
36
|
"typescript": "tinybird-typescript-sdk-guidelines",
|
|
36
37
|
}
|
|
37
38
|
SKILLS_INSTALL_TIMEOUT_SECONDS = 120
|
|
@@ -315,7 +316,7 @@ def _prompt_mode(mode: Optional[str]) -> str:
|
|
|
315
316
|
click.echo(" [1] branch - Cloud branches mapped to your git feature branch")
|
|
316
317
|
click.echo(" [2] local - Run build/test against Tinybird Local")
|
|
317
318
|
click.echo(" [3] manual - Choose environment manually with flags")
|
|
318
|
-
choice = click.prompt("\nSelect option", default=
|
|
319
|
+
choice = click.prompt("\nSelect option", default=1, type=int)
|
|
319
320
|
if choice == 1:
|
|
320
321
|
return "branch"
|
|
321
322
|
if choice == 2:
|
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
# This is a command file for our CLI. Please keep it clean.
|
|
2
|
+
#
|
|
3
|
+
# - If it makes sense and only when strictly necessary, you can create utility functions in this file.
|
|
4
|
+
# - But please, **do not** interleave utility functions and command definitions.
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
from contextlib import suppress
|
|
9
|
+
from datetime import datetime, timedelta, timezone
|
|
10
|
+
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
import humanfriendly.tables
|
|
14
|
+
from click import Context
|
|
15
|
+
|
|
16
|
+
from tinybird.tb.client import TinyB
|
|
17
|
+
from tinybird.tb.modules.cli import cli
|
|
18
|
+
from tinybird.tb.modules.common import echo_json, force_echo
|
|
19
|
+
from tinybird.tb.modules.exceptions import CLIException
|
|
20
|
+
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
21
|
+
|
|
22
|
+
LOG_SOURCES: Tuple[str, ...] = (
|
|
23
|
+
"tinybird.pipe_stats_rt",
|
|
24
|
+
"tinybird.bi_stats_rt",
|
|
25
|
+
"tinybird.block_log",
|
|
26
|
+
"tinybird.datasources_ops_log",
|
|
27
|
+
"tinybird.endpoint_errors",
|
|
28
|
+
"tinybird.kafka_ops_log",
|
|
29
|
+
"tinybird.sinks_ops_log",
|
|
30
|
+
"tinybird.jobs_log",
|
|
31
|
+
"tinybird.llm_usage",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
DEFAULT_LOG_SOURCES: Tuple[str, ...] = (
|
|
35
|
+
"tinybird.datasources_ops_log",
|
|
36
|
+
"tinybird.pipe_stats_rt",
|
|
37
|
+
"tinybird.jobs_log",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
TIMESTAMP_COLUMNS: Dict[str, str] = {
|
|
41
|
+
"tinybird.pipe_stats_rt": "start_datetime",
|
|
42
|
+
"tinybird.bi_stats_rt": "start_datetime",
|
|
43
|
+
"tinybird.block_log": "timestamp",
|
|
44
|
+
"tinybird.datasources_ops_log": "timestamp",
|
|
45
|
+
"tinybird.endpoint_errors": "start_datetime",
|
|
46
|
+
"tinybird.kafka_ops_log": "timestamp",
|
|
47
|
+
"tinybird.sinks_ops_log": "timestamp",
|
|
48
|
+
"tinybird.jobs_log": "created_at",
|
|
49
|
+
"tinybird.llm_usage": "start_time",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
RELEVANT_DETAIL_FIELDS: Dict[str, Tuple[str, ...]] = {
|
|
53
|
+
"tinybird.pipe_stats_rt": (
|
|
54
|
+
"pipe_name",
|
|
55
|
+
"method",
|
|
56
|
+
"status_code",
|
|
57
|
+
"Error",
|
|
58
|
+
"error",
|
|
59
|
+
"duration",
|
|
60
|
+
"read_rows",
|
|
61
|
+
"read_bytes",
|
|
62
|
+
"result_rows",
|
|
63
|
+
),
|
|
64
|
+
"tinybird.bi_stats_rt": (
|
|
65
|
+
"query_normalized",
|
|
66
|
+
"error_code",
|
|
67
|
+
"error",
|
|
68
|
+
"duration",
|
|
69
|
+
"read_rows",
|
|
70
|
+
"read_bytes",
|
|
71
|
+
"result_rows",
|
|
72
|
+
"result_bytes",
|
|
73
|
+
),
|
|
74
|
+
"tinybird.block_log": (
|
|
75
|
+
"datasource_name",
|
|
76
|
+
"status",
|
|
77
|
+
"source",
|
|
78
|
+
"rows",
|
|
79
|
+
"bytes",
|
|
80
|
+
"processing_time",
|
|
81
|
+
"processing_error",
|
|
82
|
+
"quarantine_lines",
|
|
83
|
+
),
|
|
84
|
+
"tinybird.datasources_ops_log": (
|
|
85
|
+
"event_type",
|
|
86
|
+
"datasource_name",
|
|
87
|
+
"result",
|
|
88
|
+
"elapsed_time",
|
|
89
|
+
"rows",
|
|
90
|
+
"error",
|
|
91
|
+
"pipe_name",
|
|
92
|
+
),
|
|
93
|
+
"tinybird.endpoint_errors": (
|
|
94
|
+
"pipe_name",
|
|
95
|
+
"status_code",
|
|
96
|
+
"error",
|
|
97
|
+
"url",
|
|
98
|
+
"params",
|
|
99
|
+
),
|
|
100
|
+
"tinybird.kafka_ops_log": (
|
|
101
|
+
"topic",
|
|
102
|
+
"partition",
|
|
103
|
+
"msg_type",
|
|
104
|
+
"lag",
|
|
105
|
+
"processed_messages",
|
|
106
|
+
"committed_messages",
|
|
107
|
+
"msg",
|
|
108
|
+
),
|
|
109
|
+
"tinybird.sinks_ops_log": (
|
|
110
|
+
"service",
|
|
111
|
+
"pipe_name",
|
|
112
|
+
"result",
|
|
113
|
+
"error",
|
|
114
|
+
"elapsed_time",
|
|
115
|
+
"read_rows",
|
|
116
|
+
"written_rows",
|
|
117
|
+
),
|
|
118
|
+
"tinybird.jobs_log": (
|
|
119
|
+
"job_id",
|
|
120
|
+
"job_type",
|
|
121
|
+
"status",
|
|
122
|
+
"pipe_name",
|
|
123
|
+
"error",
|
|
124
|
+
),
|
|
125
|
+
"tinybird.llm_usage": (
|
|
126
|
+
"feature",
|
|
127
|
+
"origin",
|
|
128
|
+
"user_email",
|
|
129
|
+
"prompt_tokens",
|
|
130
|
+
"completion_tokens",
|
|
131
|
+
"total_tokens",
|
|
132
|
+
"duration",
|
|
133
|
+
"cost",
|
|
134
|
+
),
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
LOG_TABLE_COLUMNS: Tuple[str, ...] = ("Timestamp", "Source", "Type", "Resource", "Result", "Duration", "Payload")
|
|
138
|
+
|
|
139
|
+
PRIMARY_LOG_FIELDS: Dict[str, Dict[str, Tuple[str, ...]]] = {
|
|
140
|
+
"tinybird.pipe_stats_rt": {
|
|
141
|
+
"type": ("method",),
|
|
142
|
+
"resource": ("pipe_name",),
|
|
143
|
+
"result": ("status_code",),
|
|
144
|
+
"duration": ("duration",),
|
|
145
|
+
},
|
|
146
|
+
"tinybird.datasources_ops_log": {
|
|
147
|
+
"type": ("event_type",),
|
|
148
|
+
"resource": ("datasource_name",),
|
|
149
|
+
"result": ("result",),
|
|
150
|
+
"duration": ("elapsed_time",),
|
|
151
|
+
},
|
|
152
|
+
"tinybird.kafka_ops_log": {
|
|
153
|
+
"type": ("msg_type",),
|
|
154
|
+
"resource": ("topic",),
|
|
155
|
+
"result": (),
|
|
156
|
+
"duration": ("time_write",),
|
|
157
|
+
},
|
|
158
|
+
"tinybird.sinks_ops_log": {
|
|
159
|
+
"type": ("service",),
|
|
160
|
+
"resource": ("pipe_name",),
|
|
161
|
+
"result": ("result",),
|
|
162
|
+
"duration": ("elapsed_time",),
|
|
163
|
+
},
|
|
164
|
+
"tinybird.jobs_log": {
|
|
165
|
+
"type": ("job_type",),
|
|
166
|
+
"resource": ("pipe_name",),
|
|
167
|
+
"result": ("status",),
|
|
168
|
+
"duration": (),
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
FALLBACK_PRIMARY_LOG_FIELDS: Dict[str, Tuple[str, ...]] = {
|
|
173
|
+
"type": ("event_type", "msg_type", "method", "job_type", "type"),
|
|
174
|
+
"resource": ("pipe_name", "datasource_name", "topic", "service", "resource"),
|
|
175
|
+
"result": ("result", "status", "status_code", "error"),
|
|
176
|
+
"duration": ("elapsed_time", "duration", "processing_time", "time_write"),
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
_RELATIVE_TIME_RE = re.compile(r"^-?(\d+)([mhdw])$")
|
|
180
|
+
_DETAILS_EXCLUDED_FIELDS = {
|
|
181
|
+
"start_datetime",
|
|
182
|
+
"timestamp",
|
|
183
|
+
"created_at",
|
|
184
|
+
"start_time",
|
|
185
|
+
"end_datetime",
|
|
186
|
+
"end_time",
|
|
187
|
+
"date",
|
|
188
|
+
"source",
|
|
189
|
+
}
|
|
190
|
+
_TEMPORAL_DETAIL_FIELDS = {
|
|
191
|
+
"date",
|
|
192
|
+
"datetime",
|
|
193
|
+
"timestamp",
|
|
194
|
+
"start_datetime",
|
|
195
|
+
"end_datetime",
|
|
196
|
+
"created_at",
|
|
197
|
+
"updated_at",
|
|
198
|
+
"started_at",
|
|
199
|
+
"ended_at",
|
|
200
|
+
"finished_at",
|
|
201
|
+
"start_time",
|
|
202
|
+
"end_time",
|
|
203
|
+
"event_date",
|
|
204
|
+
"query_last_execution",
|
|
205
|
+
"run_validation",
|
|
206
|
+
}
|
|
207
|
+
_DURATION_DETAIL_FIELDS = {"duration", "elapsed_time", "processing_time"}
|
|
208
|
+
_ROW_COUNT_DETAIL_FIELDS = {"rows", "read_rows", "written_rows", "result_rows", "quarantine_lines"}
|
|
209
|
+
_BYTE_COUNT_DETAIL_FIELDS = {"bytes", "read_bytes", "result_bytes", "written_bytes"}
|
|
210
|
+
_MILLISECOND_DURATION_SOURCES = {"tinybird.bi_stats_rt"}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _to_iso_utc(dt: datetime) -> str:
|
|
214
|
+
return dt.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _parse_relative_time(value: str, now: Optional[datetime] = None) -> str:
|
|
218
|
+
now = now or datetime.now(timezone.utc)
|
|
219
|
+
cleaned_value = value.strip()
|
|
220
|
+
match = _RELATIVE_TIME_RE.match(cleaned_value)
|
|
221
|
+
if not match:
|
|
222
|
+
return cleaned_value
|
|
223
|
+
|
|
224
|
+
amount = int(match.group(1))
|
|
225
|
+
unit = match.group(2)
|
|
226
|
+
delta_by_unit = {
|
|
227
|
+
"m": timedelta(minutes=amount),
|
|
228
|
+
"h": timedelta(hours=amount),
|
|
229
|
+
"d": timedelta(days=amount),
|
|
230
|
+
"w": timedelta(weeks=amount),
|
|
231
|
+
}
|
|
232
|
+
return _to_iso_utc(now - delta_by_unit[unit])
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _escape_sql_literal(value: str) -> str:
|
|
236
|
+
return value.replace("\\", "\\\\").replace("'", "\\'")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _build_source_query(source: str, start_time: str, end_time: str) -> str:
|
|
240
|
+
timestamp_column = TIMESTAMP_COLUMNS[source]
|
|
241
|
+
escaped_start = _escape_sql_literal(start_time)
|
|
242
|
+
escaped_end = _escape_sql_literal(end_time)
|
|
243
|
+
return f"""
|
|
244
|
+
SELECT
|
|
245
|
+
'{source}' AS source,
|
|
246
|
+
{timestamp_column} AS timestamp,
|
|
247
|
+
formatRowNoNewline('JSONEachRow', *) AS data
|
|
248
|
+
FROM {source}
|
|
249
|
+
WHERE {timestamp_column} >= parseDateTimeBestEffort('{escaped_start}')
|
|
250
|
+
AND {timestamp_column} < parseDateTimeBestEffort('{escaped_end}')"""
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _build_query(sources: Sequence[str], start_time: str, end_time: str, limit: int) -> str:
|
|
254
|
+
query_parts = [_build_source_query(source, start_time, end_time) for source in sources]
|
|
255
|
+
source_union = "\n UNION ALL\n".join(query_parts)
|
|
256
|
+
return f"""
|
|
257
|
+
SELECT *
|
|
258
|
+
FROM (
|
|
259
|
+
{source_union}
|
|
260
|
+
)
|
|
261
|
+
ORDER BY timestamp DESC
|
|
262
|
+
LIMIT {limit}
|
|
263
|
+
FORMAT JSON"""
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _parse_sources(raw_sources: Sequence[str]) -> List[str]:
|
|
267
|
+
if not raw_sources:
|
|
268
|
+
return list(DEFAULT_LOG_SOURCES)
|
|
269
|
+
|
|
270
|
+
selected_sources: List[str] = []
|
|
271
|
+
for raw_source in raw_sources:
|
|
272
|
+
for source in raw_source.split(","):
|
|
273
|
+
cleaned_source = source.strip()
|
|
274
|
+
if not cleaned_source:
|
|
275
|
+
continue
|
|
276
|
+
if cleaned_source == "*":
|
|
277
|
+
return list(LOG_SOURCES)
|
|
278
|
+
if cleaned_source not in LOG_SOURCES:
|
|
279
|
+
raise CLIException(
|
|
280
|
+
FeedbackManager.error(
|
|
281
|
+
message=f"Unknown source '{cleaned_source}'. Valid sources: {', '.join(LOG_SOURCES)}"
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
if cleaned_source not in selected_sources:
|
|
285
|
+
selected_sources.append(cleaned_source)
|
|
286
|
+
|
|
287
|
+
if not selected_sources:
|
|
288
|
+
raise CLIException(FeedbackManager.error(message="At least one source is required"))
|
|
289
|
+
|
|
290
|
+
return selected_sources
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _format_time(timestamp_value: Any) -> str:
|
|
294
|
+
timestamp = str(timestamp_value)
|
|
295
|
+
try:
|
|
296
|
+
parsed_dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
|
|
297
|
+
return parsed_dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
298
|
+
except ValueError:
|
|
299
|
+
return timestamp
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _normalize_json_value(value: Any) -> Any:
|
|
303
|
+
if isinstance(value, dict):
|
|
304
|
+
return {key: _normalize_json_value(val) for key, val in value.items()}
|
|
305
|
+
if isinstance(value, list):
|
|
306
|
+
return [_normalize_json_value(item) for item in value]
|
|
307
|
+
if isinstance(value, str):
|
|
308
|
+
stripped = value.strip()
|
|
309
|
+
if stripped and stripped[0] in {"{", "["}:
|
|
310
|
+
with suppress(json.JSONDecodeError):
|
|
311
|
+
return _normalize_json_value(json.loads(stripped))
|
|
312
|
+
return value
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _normalize_json_row(row: Dict[str, Any]) -> Dict[str, Any]:
|
|
316
|
+
normalized_row = dict(row)
|
|
317
|
+
normalized_row["data"] = _normalize_json_value(row.get("data"))
|
|
318
|
+
return normalized_row
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _truncate(value: str, width: int) -> str:
|
|
322
|
+
if width <= 3:
|
|
323
|
+
return value[:width]
|
|
324
|
+
if len(value) <= width:
|
|
325
|
+
return value
|
|
326
|
+
return f"{value[: width - 3]}..."
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _truncate_details(value: str, width: int) -> str:
|
|
330
|
+
if len(value) <= width:
|
|
331
|
+
return value
|
|
332
|
+
|
|
333
|
+
items = value.split(", ")
|
|
334
|
+
if not items:
|
|
335
|
+
return value[:width]
|
|
336
|
+
|
|
337
|
+
# Keep only whole values that fit in the available width.
|
|
338
|
+
kept: List[str] = []
|
|
339
|
+
for item in items:
|
|
340
|
+
candidate = item if not kept else f"{', '.join(kept)}, {item}"
|
|
341
|
+
if len(candidate) <= width:
|
|
342
|
+
kept.append(item)
|
|
343
|
+
continue
|
|
344
|
+
break
|
|
345
|
+
|
|
346
|
+
if not kept:
|
|
347
|
+
return value[:width]
|
|
348
|
+
|
|
349
|
+
return ", ".join(kept)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _format_detail_value(value: Any) -> str:
|
|
353
|
+
if isinstance(value, bool):
|
|
354
|
+
return str(value).lower()
|
|
355
|
+
if isinstance(value, (int, float)):
|
|
356
|
+
return str(value)
|
|
357
|
+
if isinstance(value, (list, dict)):
|
|
358
|
+
return json.dumps(value, separators=(",", ":"))
|
|
359
|
+
return str(value).strip()
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _format_numeric_value(value: Any) -> str:
|
|
363
|
+
if isinstance(value, bool):
|
|
364
|
+
return str(value).lower()
|
|
365
|
+
if isinstance(value, int):
|
|
366
|
+
return f"{value:,}"
|
|
367
|
+
if isinstance(value, float):
|
|
368
|
+
normalized = int(value) if value.is_integer() else value
|
|
369
|
+
if isinstance(normalized, int):
|
|
370
|
+
return f"{normalized:,}"
|
|
371
|
+
return f"{normalized:,.3f}".rstrip("0").rstrip(".")
|
|
372
|
+
return _format_detail_value(value)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _as_float(value: Any) -> Optional[float]:
|
|
376
|
+
if isinstance(value, bool):
|
|
377
|
+
return None
|
|
378
|
+
if isinstance(value, (int, float)):
|
|
379
|
+
return float(value)
|
|
380
|
+
if isinstance(value, str):
|
|
381
|
+
stripped = value.strip()
|
|
382
|
+
if not stripped:
|
|
383
|
+
return None
|
|
384
|
+
try:
|
|
385
|
+
return float(stripped)
|
|
386
|
+
except ValueError:
|
|
387
|
+
return None
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _format_pretty_detail_value(key: str, value: Any, source: str) -> str:
|
|
392
|
+
base_key = key.strip().lower().split(".")[-1]
|
|
393
|
+
numeric_value = _as_float(value)
|
|
394
|
+
|
|
395
|
+
if base_key in _DURATION_DETAIL_FIELDS and numeric_value is not None:
|
|
396
|
+
if base_key == "duration" and source in _MILLISECOND_DURATION_SOURCES:
|
|
397
|
+
milliseconds = numeric_value
|
|
398
|
+
else:
|
|
399
|
+
milliseconds = numeric_value * 1000
|
|
400
|
+
return f"{_format_numeric_value(milliseconds)} ms"
|
|
401
|
+
|
|
402
|
+
if base_key in _ROW_COUNT_DETAIL_FIELDS and numeric_value is not None:
|
|
403
|
+
row_count = int(numeric_value) if float(numeric_value).is_integer() else numeric_value
|
|
404
|
+
return f"{_format_numeric_value(row_count)} rows"
|
|
405
|
+
|
|
406
|
+
if base_key in _BYTE_COUNT_DETAIL_FIELDS and numeric_value is not None:
|
|
407
|
+
byte_count = int(numeric_value) if float(numeric_value).is_integer() else numeric_value
|
|
408
|
+
return f"{_format_numeric_value(byte_count)} bytes"
|
|
409
|
+
|
|
410
|
+
return _format_detail_value(value)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _select_detail_keys(parsed_data: Dict[str, Any], source: str, verbose: bool) -> List[str]:
|
|
414
|
+
visible_keys: List[str] = []
|
|
415
|
+
for key, value in parsed_data.items():
|
|
416
|
+
if value is None or value == "":
|
|
417
|
+
continue
|
|
418
|
+
if not verbose and _is_temporal_detail_field(key):
|
|
419
|
+
continue
|
|
420
|
+
visible_keys.append(key)
|
|
421
|
+
|
|
422
|
+
if verbose:
|
|
423
|
+
return visible_keys
|
|
424
|
+
|
|
425
|
+
preferred_keys = RELEVANT_DETAIL_FIELDS.get(source, ())
|
|
426
|
+
selected_keys: List[str] = [
|
|
427
|
+
key for key in preferred_keys if key in parsed_data and parsed_data.get(key) not in ("", None)
|
|
428
|
+
]
|
|
429
|
+
return selected_keys or visible_keys
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _parse_data_object(data: Any) -> Optional[Dict[str, Any]]:
|
|
433
|
+
if isinstance(data, dict):
|
|
434
|
+
return data
|
|
435
|
+
|
|
436
|
+
text = str(data)
|
|
437
|
+
try:
|
|
438
|
+
parsed_data = json.loads(text)
|
|
439
|
+
except (json.JSONDecodeError, TypeError):
|
|
440
|
+
return None
|
|
441
|
+
|
|
442
|
+
return parsed_data if isinstance(parsed_data, dict) else None
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _find_matching_key(parsed_data: Dict[str, Any], key: str) -> Optional[str]:
|
|
446
|
+
if key in parsed_data:
|
|
447
|
+
return key
|
|
448
|
+
|
|
449
|
+
key_lower = key.lower()
|
|
450
|
+
for parsed_key in parsed_data:
|
|
451
|
+
if parsed_key.lower() == key_lower:
|
|
452
|
+
return parsed_key
|
|
453
|
+
return None
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _extract_primary_column_value(parsed_data: Dict[str, Any], source: str, column: str, used_keys: Set[str]) -> str:
|
|
457
|
+
source_fields = PRIMARY_LOG_FIELDS.get(source, {})
|
|
458
|
+
candidate_keys = source_fields[column] if column in source_fields else FALLBACK_PRIMARY_LOG_FIELDS.get(column, ())
|
|
459
|
+
|
|
460
|
+
for key in candidate_keys:
|
|
461
|
+
matching_key = _find_matching_key(parsed_data, key)
|
|
462
|
+
if matching_key is None:
|
|
463
|
+
continue
|
|
464
|
+
|
|
465
|
+
value = parsed_data.get(matching_key)
|
|
466
|
+
if value in ("", None):
|
|
467
|
+
continue
|
|
468
|
+
|
|
469
|
+
used_keys.add(matching_key)
|
|
470
|
+
if column == "duration":
|
|
471
|
+
return _format_pretty_detail_value(matching_key, value, source=source)
|
|
472
|
+
return _format_detail_value(value)
|
|
473
|
+
|
|
474
|
+
return "-"
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _summarize_payload(
|
|
478
|
+
parsed_data: Dict[str, Any], source: str, used_keys: Set[str], expand: bool, verbose: bool
|
|
479
|
+
) -> str:
|
|
480
|
+
selected_keys = _select_detail_keys(parsed_data, source=source, verbose=verbose)
|
|
481
|
+
used_lower = {key.lower() for key in used_keys}
|
|
482
|
+
payload_keys = [
|
|
483
|
+
key for key in selected_keys if key.lower() not in used_lower and parsed_data.get(key) not in ("", None)
|
|
484
|
+
]
|
|
485
|
+
if not payload_keys:
|
|
486
|
+
return "-"
|
|
487
|
+
|
|
488
|
+
values = [f"{key}: {_format_pretty_detail_value(key, parsed_data.get(key), source=source)}" for key in payload_keys]
|
|
489
|
+
summary = ", ".join(values)
|
|
490
|
+
return summary if expand else _truncate_details(summary, 120)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _summarize_details(data: Any, source: str, expand: bool, verbose: bool) -> str:
|
|
494
|
+
text = str(data)
|
|
495
|
+
try:
|
|
496
|
+
parsed_data = json.loads(text)
|
|
497
|
+
except (json.JSONDecodeError, TypeError):
|
|
498
|
+
return text if expand else _truncate(text, 120)
|
|
499
|
+
|
|
500
|
+
if not isinstance(parsed_data, dict):
|
|
501
|
+
normalized = str(parsed_data)
|
|
502
|
+
return normalized if expand else _truncate(normalized, 120)
|
|
503
|
+
|
|
504
|
+
values: List[str] = []
|
|
505
|
+
for key in _select_detail_keys(parsed_data, source=source, verbose=verbose):
|
|
506
|
+
value = parsed_data.get(key)
|
|
507
|
+
formatted_value = _format_pretty_detail_value(key, value, source=source)
|
|
508
|
+
values.append(f"{key}: {formatted_value}" if verbose else formatted_value)
|
|
509
|
+
|
|
510
|
+
summary = ", ".join(values) if values else text
|
|
511
|
+
return summary
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _render_logs_table(rows: Sequence[Dict[str, Any]], expand: bool, verbose: bool) -> str:
|
|
515
|
+
rendered_rows: List[Tuple[str, str, str, str, str, str, str]] = []
|
|
516
|
+
for row in rows:
|
|
517
|
+
source = str(row.get("source", ""))
|
|
518
|
+
timestamp = _format_time(row.get("timestamp", ""))
|
|
519
|
+
parsed_data = _parse_data_object(row.get("data", ""))
|
|
520
|
+
|
|
521
|
+
if parsed_data is None:
|
|
522
|
+
payload = _format_detail_value(row.get("data", ""))
|
|
523
|
+
payload = payload if expand else _truncate(payload, 120)
|
|
524
|
+
rendered_rows.append((timestamp, source, "-", "-", "-", "-", payload))
|
|
525
|
+
continue
|
|
526
|
+
|
|
527
|
+
used_keys: Set[str] = set()
|
|
528
|
+
row_type = _extract_primary_column_value(parsed_data, source, "type", used_keys)
|
|
529
|
+
resource = _extract_primary_column_value(parsed_data, source, "resource", used_keys)
|
|
530
|
+
result = _extract_primary_column_value(parsed_data, source, "result", used_keys)
|
|
531
|
+
duration = _extract_primary_column_value(parsed_data, source, "duration", used_keys)
|
|
532
|
+
payload = _summarize_payload(parsed_data, source, used_keys, expand=expand, verbose=verbose)
|
|
533
|
+
|
|
534
|
+
rendered_rows.append((timestamp, source, row_type, resource, result, duration, payload))
|
|
535
|
+
|
|
536
|
+
return humanfriendly.tables.format_pretty_table(rendered_rows, column_names=list(LOG_TABLE_COLUMNS))
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _resolve_environment_label(ctx: Context) -> str:
|
|
540
|
+
env = ctx.ensure_object(dict).get("env", "local")
|
|
541
|
+
branch = ctx.ensure_object(dict).get("branch")
|
|
542
|
+
if env == "cloud" and branch:
|
|
543
|
+
return f"branch:{branch}"
|
|
544
|
+
return str(env)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _is_temporal_detail_field(key: str) -> bool:
|
|
548
|
+
normalized_key = key.strip().lower()
|
|
549
|
+
base_key = normalized_key.split(".")[-1]
|
|
550
|
+
|
|
551
|
+
if base_key in _DETAILS_EXCLUDED_FIELDS:
|
|
552
|
+
return True
|
|
553
|
+
if base_key in _TEMPORAL_DETAIL_FIELDS:
|
|
554
|
+
return True
|
|
555
|
+
if base_key.endswith("_at"):
|
|
556
|
+
return True
|
|
557
|
+
if base_key.endswith("_timestamp"):
|
|
558
|
+
return True
|
|
559
|
+
return bool(base_key.endswith("_date"))
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
@cli.command(name="logs")
|
|
563
|
+
@click.option(
|
|
564
|
+
"-s",
|
|
565
|
+
"--start",
|
|
566
|
+
"start_time",
|
|
567
|
+
default="-1h",
|
|
568
|
+
show_default=True,
|
|
569
|
+
help="Start time (relative: -1h, -30m, -1d, -7d or ISO 8601).",
|
|
570
|
+
)
|
|
571
|
+
@click.option(
|
|
572
|
+
"-e",
|
|
573
|
+
"--end",
|
|
574
|
+
"end_time",
|
|
575
|
+
default=None,
|
|
576
|
+
help="End time (relative or ISO 8601).",
|
|
577
|
+
)
|
|
578
|
+
@click.option(
|
|
579
|
+
"--source",
|
|
580
|
+
"sources",
|
|
581
|
+
multiple=True,
|
|
582
|
+
help=f"Comma-separated or repeated list of sources. Use '*' for all. Available: {', '.join(LOG_SOURCES)}",
|
|
583
|
+
)
|
|
584
|
+
@click.option(
|
|
585
|
+
"-n",
|
|
586
|
+
"--limit",
|
|
587
|
+
default=100,
|
|
588
|
+
show_default=True,
|
|
589
|
+
type=click.IntRange(1, 1000),
|
|
590
|
+
help="Maximum rows to return.",
|
|
591
|
+
)
|
|
592
|
+
@click.option("-x", "--expand", is_flag=True, default=False, help="Show full details without truncation.")
|
|
593
|
+
@click.option("-v", "--verbose", is_flag=True, default=False, help="Show all fields in details with property names.")
|
|
594
|
+
@click.pass_context
|
|
595
|
+
def logs(
|
|
596
|
+
ctx: Context,
|
|
597
|
+
start_time: str,
|
|
598
|
+
end_time: Optional[str],
|
|
599
|
+
sources: Tuple[str, ...],
|
|
600
|
+
limit: int,
|
|
601
|
+
expand: bool,
|
|
602
|
+
verbose: bool,
|
|
603
|
+
) -> None:
|
|
604
|
+
"""Query Tinybird real-time service logs."""
|
|
605
|
+
output = ctx.ensure_object(dict)["output"]
|
|
606
|
+
if output not in {"human", "json"}:
|
|
607
|
+
force_echo(FeedbackManager.error_invalid_output_format(formats=", ".join(["human", "json"])))
|
|
608
|
+
return
|
|
609
|
+
|
|
610
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
611
|
+
now = datetime.now(timezone.utc)
|
|
612
|
+
|
|
613
|
+
resolved_start = _parse_relative_time(start_time, now)
|
|
614
|
+
resolved_end = _parse_relative_time(end_time, now) if end_time else _to_iso_utc(now)
|
|
615
|
+
resolved_sources = _parse_sources(sources)
|
|
616
|
+
environment = _resolve_environment_label(ctx)
|
|
617
|
+
query = _build_query(resolved_sources, resolved_start, resolved_end, limit)
|
|
618
|
+
|
|
619
|
+
started = time.monotonic()
|
|
620
|
+
try:
|
|
621
|
+
result = client.query(query)
|
|
622
|
+
except Exception as e:
|
|
623
|
+
raise CLIException(FeedbackManager.error_exception(error=str(e)))
|
|
624
|
+
|
|
625
|
+
if isinstance(result, str):
|
|
626
|
+
try:
|
|
627
|
+
result = json.loads(result)
|
|
628
|
+
except json.JSONDecodeError:
|
|
629
|
+
raise CLIException(FeedbackManager.error_exception(error=result))
|
|
630
|
+
|
|
631
|
+
if not isinstance(result, dict):
|
|
632
|
+
raise CLIException(FeedbackManager.error_exception(error="Unexpected response format while querying logs"))
|
|
633
|
+
if "error" in result:
|
|
634
|
+
raise CLIException(FeedbackManager.error_exception(error=str(result["error"])))
|
|
635
|
+
|
|
636
|
+
rows = [row for row in result.get("data", []) if isinstance(row, dict)]
|
|
637
|
+
reported_rows = result.get("rows")
|
|
638
|
+
rows_count = len(rows)
|
|
639
|
+
if isinstance(reported_rows, int) and reported_rows >= 0:
|
|
640
|
+
rows_count = reported_rows
|
|
641
|
+
elif isinstance(reported_rows, str) and reported_rows.isdigit():
|
|
642
|
+
rows_count = int(reported_rows)
|
|
643
|
+
if not rows:
|
|
644
|
+
rows_count = 0
|
|
645
|
+
statistics = result.get("statistics", {})
|
|
646
|
+
elapsed_seconds = statistics.get("elapsed") if isinstance(statistics, dict) else None
|
|
647
|
+
if not isinstance(elapsed_seconds, (float, int)):
|
|
648
|
+
elapsed_seconds = time.monotonic() - started
|
|
649
|
+
|
|
650
|
+
payload = {
|
|
651
|
+
"environment": environment,
|
|
652
|
+
"query": {
|
|
653
|
+
"start": resolved_start,
|
|
654
|
+
"end": resolved_end,
|
|
655
|
+
"sources": resolved_sources,
|
|
656
|
+
"limit": limit,
|
|
657
|
+
"verbose": verbose,
|
|
658
|
+
},
|
|
659
|
+
"statistics": statistics if isinstance(statistics, dict) else {},
|
|
660
|
+
"rows": rows_count,
|
|
661
|
+
"data": [_normalize_json_row(row) for row in rows],
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if output == "json":
|
|
665
|
+
echo_json(payload, indent=8)
|
|
666
|
+
return
|
|
667
|
+
|
|
668
|
+
if not rows:
|
|
669
|
+
click.echo("No logs found for the specified time range.")
|
|
670
|
+
click.echo(f"\nQueried {len(resolved_sources)} source(s) from {environment} in {elapsed_seconds:.1f}s")
|
|
671
|
+
return
|
|
672
|
+
|
|
673
|
+
click.echo(_render_logs_table(rows, expand=expand, verbose=verbose))
|
|
674
|
+
click.echo(
|
|
675
|
+
f"\nFetched {rows_count} logs from {len(resolved_sources)} source(s) in {elapsed_seconds:.1f}s ({environment})"
|
|
676
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: tinybird
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.1
|
|
4
4
|
Summary: Tinybird Command Line Tool
|
|
5
5
|
Home-page: https://www.tinybird.co/docs/forward/commands
|
|
6
6
|
Author: Tinybird
|
|
@@ -52,6 +52,12 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
|
|
|
52
52
|
Changelog
|
|
53
53
|
----------
|
|
54
54
|
|
|
55
|
+
4.1.1
|
|
56
|
+
*******
|
|
57
|
+
|
|
58
|
+
- `Changed` `tb init` now installs `tinybird-python-sdk-guidelines` when selecting the Python SDK project type.
|
|
59
|
+
- `Changed` `tb init` now defaults `dev_mode` to `branch` and persists that value in `tinybird.config.json`.
|
|
60
|
+
|
|
55
61
|
4.1.0
|
|
56
62
|
*******
|
|
57
63
|
|
|
@@ -95,6 +101,16 @@ Changelog
|
|
|
95
101
|
|
|
96
102
|
- `Changed` Prompt-based AI flows now print a deprecation warning (`tb --prompt`, `tb create --prompt`, `tb datasource create --prompt`, `tb test create`, `tb mock`) ahead of their removal in a future release.
|
|
97
103
|
|
|
104
|
+
3.3.2
|
|
105
|
+
*******
|
|
106
|
+
|
|
107
|
+
- `Added` `tb logs` command to query service observability data sources from the CLI.
|
|
108
|
+
- `Added` support for `--start`, `--end`, `--source`, `--limit`, and `--expand` in `tb logs`.
|
|
109
|
+
- `Added` support for `--source '*'` in `tb logs` to query all supported sources.
|
|
110
|
+
- `Changed` default `tb logs` sources to `tinybird.datasources_ops_log`, `tinybird.pipe_stats_rt`, and `tinybird.jobs_log`.
|
|
111
|
+
- `Changed` `tb logs` table output to show full datetime (`YYYY-MM-DD HH:MM:SS`) in `TIME` and to hide temporal fields from `DETAILS`.
|
|
112
|
+
- `Fixed` `tb logs` to correctly parse SQL responses returned as JSON strings.
|
|
113
|
+
|
|
98
114
|
3.3.1
|
|
99
115
|
*******
|
|
100
116
|
|
|
@@ -65,6 +65,7 @@ tinybird/tb/modules/local_logs.py
|
|
|
65
65
|
tinybird/tb/modules/login.py
|
|
66
66
|
tinybird/tb/modules/login_common.py
|
|
67
67
|
tinybird/tb/modules/logout.py
|
|
68
|
+
tinybird/tb/modules/logs.py
|
|
68
69
|
tinybird/tb/modules/materialization.py
|
|
69
70
|
tinybird/tb/modules/open.py
|
|
70
71
|
tinybird/tb/modules/pipe.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|