Skip to content

Commit 034149d

Browse files
committed
Adds fastly_hourly_stats.pl arxivce-865
1 parent 083a11f commit 034149d

File tree

5 files changed

+335
-6
lines changed

5 files changed

+335
-6
lines changed

arxiv/ops/fastly_hourly_stats.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""Script to add Fastly requests to the arXiv_stats_hourly table.
2+
3+
It reads these from a metric at GCP.
4+
5+
To use this:
6+
7+
cd arxiv-base
8+
poetry install --extras=mysql
9+
poetry run python arxiv/ops/fastly_hourly_stats.py config_file_example > fastly_hourly_stats.ini
10+
vim fastly_hourly_stats.ini # setup configs
11+
poetry python arxiv/ops/fastly_hourly_stats.py --config-file fastly_hourly_stats.ini
12+
13+
This reads from a GCP metric named "arxiv-org-fastly-requests" and uses the fitler:
14+
15+
resource.type="generic_node"
16+
logName="projects/arxiv-production/logs/fastly_log_ingest"
17+
( (jsonPayload.backend =~ "_web\d+$" (jsonPayload.state = "HIT" jsonPayload.state = "HIT-CLUSTER"
18+
jsonPayload.state = "HIT-WAIT" jsonPayload.state = "HIT-STALE"))
19+
OR (jsonPayload.backend !~ "_web\d+$") )
20+
21+
This filter gets any fastly log line that is either
22+
1. from a web node backend but was a cache hit
23+
2. not from a web node backend (i.e. at GCP)
24+
25+
The logs are added to GCP using `arxiv/ops/fastly_log_ingest`
26+
"""
27+
28+
import datetime
29+
import sys
30+
from typing import Tuple, MutableMapping
31+
import click
32+
import configparser
33+
34+
from google.cloud.monitoring_v3 import TimeInterval, Aggregation, MetricServiceClient, ListTimeSeriesRequest
35+
36+
from sqlalchemy import create_engine
37+
from sqlalchemy.sql import text
38+
from sqlalchemy.dialects import mysql
39+
40+
41+
@click.group()
42+
def cli():
43+
pass
44+
45+
46+
@cli.command()
47+
def config_file_example():
48+
"""Print ex config file and exit."""
49+
print(_config_file_example)
50+
sys.exit(1)
51+
52+
53+
@cli.command()
54+
@click.option('--dry-run', default=False, is_flag=True)
55+
@click.option('--verbose', default=False, is_flag=True)
56+
@click.option("--config-file", required=True)
57+
def last_hour(dry_run: bool, config_file: str, verbose: bool):
58+
"""Adds request count for last clock hour.
59+
60+
Ex time is 2023-11-27T19:05:00Z,
61+
the start of the interval is 2023-11-27T18:00:00.000000000Z
62+
and the end time will be 2023-11-27T18:59:59.999999999Z
63+
"""
64+
config = configparser.ConfigParser()
65+
config.read(config_file)
66+
config = config["DEFAULT"]
67+
now, start, end = _get_default_time()
68+
if verbose:
69+
print(f"Getting all requests between start time {start} and {end}")
70+
count = _get_count_from_gcp_v2(config, start, end, verbose)
71+
_load_count_to_db(config, count, now, dry_run, verbose)
72+
73+
74+
def _get_default_time() -> Tuple[datetime.datetime, str, str]:
75+
# Since this uses only UTC, there should be no problems with DST transitions
76+
now = (datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta(hours=1))
77+
return now, now.strftime("%Y-%m-%dT%H:00:00.000000000Z"), now.strftime("%Y-%m-%dT%H:59:59.999999999Z")
78+
79+
80+
def _get_count_from_gcp_v2(config: MutableMapping, start: str, end: str, verbose: bool):
81+
"""Gets metric for interval.
82+
83+
`start` and `end` formats are like: 2023-11-27T18:00:00.000000000Z
84+
"""
85+
client = MetricServiceClient()
86+
# noinspection PyTypeChecker
87+
request = ListTimeSeriesRequest(
88+
name="projects/" + config["gcp_project"],
89+
filter=F"metric.type = \"logging.googleapis.com/user/{config['gcp_metric']}\"",
90+
interval=TimeInterval(start_time=start, end_time=end),
91+
aggregation=Aggregation(
92+
alignment_period="3600s", # 1 hour
93+
per_series_aligner="ALIGN_SUM"
94+
)
95+
)
96+
97+
page_result = client.list_time_series(request)
98+
if verbose:
99+
print("Results from GCP metric:")
100+
for response in page_result:
101+
print(response)
102+
print("End of results from gcp metric.")
103+
104+
points = []
105+
for response in page_result:
106+
points.extend(response.points)
107+
108+
if len(points) == 0:
109+
print(f"No points in fastly request metric between {start} and {end}. Expected 1. May be due to no traffic to fastly.")
110+
sys.exit(1)
111+
112+
return sum([point.value.int64_value for point in points])
113+
114+
115+
def _load_count_to_db(config: MutableMapping, count: int, now: datetime.datetime, dry_run: bool = True, verbose: bool = False) -> None:
116+
insert = text("INSERT "
117+
"INTO arXiv_stats_hourly "
118+
"(ymd, hour, node_num, access_type, connections) "
119+
"VALUES "
120+
"(:ymd, :hour, :node_num, 'A', :connections)")
121+
insert = insert.bindparams(ymd=now.date().isoformat(),
122+
hour=now.hour,
123+
node_num=config['row_node_num'],
124+
connections=count)
125+
if dry_run or verbose:
126+
print(insert.compile(dialect=mysql.dialect(), compile_kwargs={"literal_binds": True}))
127+
128+
if dry_run:
129+
print("SQL not executed due to dry_run.")
130+
sys.exit(1)
131+
132+
engine = create_engine(config['sqlalchemy_database_uri'])
133+
with engine.begin() as conn:
134+
conn.execute(insert)
135+
136+
137+
_config_file_example = """
138+
[DEFAULT]
139+
# DB to write to, needs rw access to arXiv_stats_hourly
140+
sqlalchemy_database_uri=mysql://rw_user:[email protected]:1234/arXiv
141+
142+
# value to put in as web node ID, using zero to indicate fastly
143+
row_node_num=0
144+
145+
146+
# Name of gcp project to read from, do not preface with "projects/"
147+
gcp_project=arxiv-production
148+
149+
# Name of metric to use, do not preface with "logging.googleapis.com/user/"
150+
gcp_metric=arxiv-org-fastly-requests
151+
"""
152+
153+
if __name__ == "__main__":
154+
cli()

arxiv/ops/fastly_log_ingest/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
VERBOSE = os.environ.get("VERBOSE", "verbose_off_by_default") == "1"
2121
THREADS = int(os.environ.get("THREADS", 1)) # threads to send logs
2222
SEND_PERIOD = int(os.environ.get("SEND_PERIOD", 8.0)) # seconds to wait for messages to accumulate
23-
INFO_PERIOD = int(os.environ.get("INFO_PERIOD", 80.0)) # seconds between info logging
23+
INFO_PERIOD = int(os.environ.get("INFO_PERIOD", 120.0)) # seconds between info logging
2424

2525
"""Number of log records in a batch.
2626
GCP Logging limits (https://cloud.google.com/logging/quotas#api-limits) say 10MB as of 2023-11.

monitor_explore.py

Whitespace-only changes.

0 commit comments

Comments
 (0)