Skip to content

Commit 4ea33b7

Browse files
authored
feat: log most recent API calls as recent-bigframes-api-xx labels on BigQuery jobs (#145)
1 parent 3a2761f commit 4ea33b7

File tree

14 files changed

+276
-11
lines changed

14 files changed

+276
-11
lines changed

bigframes/core/groupby/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import pandas as pd
2020

2121
import bigframes.constants as constants
22+
from bigframes.core import log_adapter
2223
import bigframes.core as core
2324
import bigframes.core.block_transforms as block_ops
2425
import bigframes.core.blocks as blocks
@@ -33,6 +34,7 @@
3334
import third_party.bigframes_vendored.pandas.core.groupby as vendored_pandas_groupby
3435

3536

37+
@log_adapter.class_logger
3638
class DataFrameGroupBy(vendored_pandas_groupby.DataFrameGroupBy):
3739
__doc__ = vendored_pandas_groupby.GroupBy.__doc__
3840

@@ -406,6 +408,7 @@ def _resolve_label(self, label: blocks.Label) -> str:
406408
return col_ids[0]
407409

408410

411+
@log_adapter.class_logger
409412
class SeriesGroupBy(vendored_pandas_groupby.SeriesGroupBy):
410413
__doc__ = vendored_pandas_groupby.GroupBy.__doc__
411414

bigframes/core/log_adapter.py

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import functools
16+
import threading
17+
from typing import List
18+
19+
_lock = threading.Lock()
20+
MAX_LABELS_COUNT = 64
21+
_api_methods: List = []
22+
23+
24+
def class_logger(decorated_cls):
25+
"""Decorator that adds logging functionality to each method of the class."""
26+
for attr_name, attr_value in decorated_cls.__dict__.items():
27+
if callable(attr_value):
28+
setattr(decorated_cls, attr_name, method_logger(attr_value))
29+
return decorated_cls
30+
31+
32+
def method_logger(method):
33+
"""Decorator that adds logging functionality to a method."""
34+
35+
@functools.wraps(method)
36+
def wrapper(*args, **kwargs):
37+
api_method_name = str(method.__name__)
38+
# Track regular and "dunder" methods
39+
if api_method_name.startswith("__") or not api_method_name.startswith("_"):
40+
add_api_method(api_method_name)
41+
return method(*args, **kwargs)
42+
43+
return wrapper
44+
45+
46+
def add_api_method(api_method_name):
47+
global _lock
48+
global _api_methods
49+
with _lock:
50+
# Push the method to the front of the _api_methods list
51+
_api_methods.insert(0, api_method_name)
52+
# Keep the list length within the maximum limit (adjust MAX_LABELS_COUNT as needed)
53+
_api_methods = _api_methods[:MAX_LABELS_COUNT]
54+
55+
56+
def get_and_reset_api_methods():
57+
global _lock
58+
with _lock:
59+
previous_api_methods = list(_api_methods)
60+
_api_methods.clear()
61+
return previous_api_methods

bigframes/core/nodes.py

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
1415
from __future__ import annotations
1516

1617
from dataclasses import dataclass, field

bigframes/core/window/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616

1717
import typing
1818

19+
from bigframes.core import log_adapter
1920
import bigframes.core as core
2021
import bigframes.core.blocks as blocks
2122
import bigframes.operations.aggregations as agg_ops
2223
import third_party.bigframes_vendored.pandas.core.window.rolling as vendored_pandas_rolling
2324

2425

26+
@log_adapter.class_logger
2527
class Window(vendored_pandas_rolling.Window):
2628
__doc__ = vendored_pandas_rolling.Window.__doc__
2729

bigframes/dataframe.py

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import bigframes._config.display_options as display_options
4242
import bigframes.constants as constants
4343
import bigframes.core
44+
from bigframes.core import log_adapter
4445
import bigframes.core.block_transforms as block_ops
4546
import bigframes.core.blocks as blocks
4647
import bigframes.core.groupby as groupby
@@ -81,6 +82,7 @@
8182

8283

8384
# Inherits from pandas DataFrame so that we can use the same docstrings.
85+
@log_adapter.class_logger
8486
class DataFrame(vendored_pandas_frame.DataFrame):
8587
__doc__ = vendored_pandas_frame.DataFrame.__doc__
8688

bigframes/operations/datetimes.py

+2
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414

1515
from __future__ import annotations
1616

17+
from bigframes.core import log_adapter
1718
import bigframes.operations as ops
1819
import bigframes.operations.base
1920
import bigframes.series as series
2021
import third_party.bigframes_vendored.pandas.core.indexes.accessor as vendordt
2122

2223

24+
@log_adapter.class_logger
2325
class DatetimeMethods(
2426
bigframes.operations.base.SeriesMethods, vendordt.DatetimeProperties
2527
):

bigframes/operations/strings.py

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from typing import cast, Literal, Optional, Union
1919

2020
import bigframes.constants as constants
21+
from bigframes.core import log_adapter
2122
import bigframes.dataframe as df
2223
import bigframes.operations as ops
2324
import bigframes.operations.base
@@ -32,6 +33,7 @@
3233
}
3334

3435

36+
@log_adapter.class_logger
3537
class StringMethods(bigframes.operations.base.SeriesMethods, vendorstr.StringMethods):
3638
__doc__ = vendorstr.StringMethods.__doc__
3739

bigframes/operations/structs.py

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import ibis.expr.types as ibis_types
2020

21+
from bigframes.core import log_adapter
2122
import bigframes.dataframe
2223
import bigframes.operations
2324
import bigframes.operations.base
@@ -38,6 +39,7 @@ def _as_ibis(self, x: ibis_types.Value):
3839
return struct_value[name].name(name)
3940

4041

42+
@log_adapter.class_logger
4143
class StructAccessor(
4244
bigframes.operations.base.SeriesMethods, vendoracessors.StructAccessor
4345
):

bigframes/series.py

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
import bigframes.constants as constants
3232
import bigframes.core
33+
from bigframes.core import log_adapter
3334
import bigframes.core.block_transforms as block_ops
3435
import bigframes.core.blocks as blocks
3536
import bigframes.core.groupby as groupby
@@ -55,6 +56,7 @@
5556
LevelsType = typing.Union[LevelType, typing.Sequence[LevelType]]
5657

5758

59+
@log_adapter.class_logger
5860
class Series(bigframes.operations.base.SeriesMethods, vendored_pandas_series.Series):
5961
def __init__(self, *args, **kwargs):
6062
self._query_job: Optional[bigquery.QueryJob] = None

bigframes/session/__init__.py

+7
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464

6565
import bigframes._config.bigquery_options as bigquery_options
6666
import bigframes.constants as constants
67+
from bigframes.core import log_adapter
6768
import bigframes.core as core
6869
import bigframes.core.blocks as blocks
6970
import bigframes.core.guid as guid
@@ -1347,6 +1348,10 @@ def _start_query(
13471348
Starts query job and waits for results.
13481349
"""
13491350
job_config = self._prepare_job_config(job_config)
1351+
api_methods = log_adapter.get_and_reset_api_methods()
1352+
job_config.labels = bigframes_io.create_job_configs_labels(
1353+
job_configs_labels=job_config.labels, api_methods=api_methods
1354+
)
13501355
query_job = self.bqclient.query(sql, job_config=job_config)
13511356

13521357
opts = bigframes.options.display
@@ -1381,6 +1386,8 @@ def _prepare_job_config(
13811386
) -> bigquery.QueryJobConfig:
13821387
if job_config is None:
13831388
job_config = self.bqclient.default_query_job_config
1389+
if job_config is None:
1390+
job_config = bigquery.QueryJobConfig()
13841391
if bigframes.options.compute.maximum_bytes_billed is not None:
13851392
job_config.maximum_bytes_billed = (
13861393
bigframes.options.compute.maximum_bytes_billed

bigframes/session/_io/bigquery.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,36 @@
1717
from __future__ import annotations
1818

1919
import datetime
20+
import itertools
2021
import textwrap
2122
import types
22-
from typing import Dict, Iterable, Optional, Union
23+
from typing import Dict, Iterable, Optional, Sequence, Union
2324
import uuid
2425

2526
import google.cloud.bigquery as bigquery
2627

2728
IO_ORDERING_ID = "bqdf_row_nums"
29+
MAX_LABELS_COUNT = 64
2830
TEMP_TABLE_PREFIX = "bqdf{date}_{random_id}"
2931

3032

33+
def create_job_configs_labels(
34+
job_configs_labels: Optional[Dict[str, str]],
35+
api_methods: Sequence[str],
36+
) -> Dict[str, str]:
37+
if job_configs_labels is None:
38+
job_configs_labels = {}
39+
40+
labels = list(
41+
itertools.chain(
42+
job_configs_labels.keys(),
43+
(f"recent-bigframes-api-{i}" for i in range(len(api_methods))),
44+
)
45+
)
46+
values = list(itertools.chain(job_configs_labels.values(), api_methods))
47+
return dict(zip(labels[:MAX_LABELS_COUNT], values[:MAX_LABELS_COUNT]))
48+
49+
3150
def create_export_csv_statement(
3251
table_id: str, uri: str, field_delimiter: str, header: bool
3352
) -> str:

tests/unit/core/test_log_adapter.py

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import pytest
16+
17+
from bigframes.core import log_adapter
18+
19+
MAX_LABELS_COUNT = 64
20+
21+
22+
@pytest.fixture
23+
def test_instance():
24+
# Create a simple class for testing
25+
@log_adapter.class_logger
26+
class TestClass:
27+
def method1(self):
28+
pass
29+
30+
def method2(self):
31+
pass
32+
33+
return TestClass()
34+
35+
36+
def test_method_logging(test_instance):
37+
test_instance.method1()
38+
test_instance.method2()
39+
40+
# Check if the methods were added to the _api_methods list
41+
api_methods = log_adapter.get_and_reset_api_methods()
42+
assert api_methods is not None
43+
assert "method1" in api_methods
44+
assert "method2" in api_methods
45+
46+
47+
def test_add_api_method_limit(test_instance):
48+
# Ensure that add_api_method correctly adds a method to _api_methods
49+
for i in range(70):
50+
test_instance.method2()
51+
assert len(log_adapter._api_methods) == MAX_LABELS_COUNT
52+
53+
54+
def test_get_and_reset_api_methods(test_instance):
55+
# Ensure that get_and_reset_api_methods returns a copy and resets the list
56+
test_instance.method1()
57+
test_instance.method2()
58+
previous_methods = log_adapter.get_and_reset_api_methods()
59+
assert previous_methods is not None
60+
assert log_adapter._api_methods == []

0 commit comments

Comments
 (0)