rda-python-metrics 3.0.6__tar.gz → 3.0.8__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.
Files changed (87) hide show
  1. {rda_python_metrics-3.0.6/src/rda_python_metrics.egg-info → rda_python_metrics-3.0.8}/PKG-INFO +1 -1
  2. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/pyproject.toml +1 -1
  3. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillawsusage.py +5 -2
  4. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillcdgusage.py +4 -1
  5. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillcodusage.py +6 -3
  6. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillgdexusage.py +4 -1
  7. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillglobususage.py +5 -2
  8. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillipinfo.py +4 -0
  9. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillosdfusage.py +26 -8
  10. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/filltdsusage.py +5 -2
  11. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillzenodousage.py +4 -1
  12. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/pg_ipinfo.py +135 -12
  13. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8/src/rda_python_metrics.egg-info}/PKG-INFO +1 -1
  14. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/LICENSE +0 -0
  15. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/MANIFEST.in +0 -0
  16. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/README.md +0 -0
  17. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/setup.cfg +0 -0
  18. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/PgIPInfo.py +0 -0
  19. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/PgView.py +0 -0
  20. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/__init__.py +0 -0
  21. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fill_awsusage.py +0 -0
  22. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fill_cdgusage.py +0 -0
  23. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fill_codusage.py +0 -0
  24. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fill_country.py +0 -0
  25. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fill_endtime.py +0 -0
  26. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fill_globususage.py +0 -0
  27. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fill_ipinfo.py +0 -0
  28. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fill_oneorder.py +0 -0
  29. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fill_osdfusage.py +0 -0
  30. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fill_rdadb.py +0 -0
  31. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fill_tdsusage.py +0 -0
  32. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillawsusage.usg +0 -0
  33. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillcdgusage.usg +0 -0
  34. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillcodusage.usg +0 -0
  35. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillcountry.py +0 -0
  36. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillendtime.py +0 -0
  37. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillgdexusage.usg +0 -0
  38. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillglobususage.usg +0 -0
  39. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillipinfo.usg +0 -0
  40. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/filloneorder.py +0 -0
  41. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/filloneorder.usg +0 -0
  42. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillosdfusage.usg +0 -0
  43. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillrdadb.py +0 -0
  44. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillrdadb.usg +0 -0
  45. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/filltdsusage.usg +0 -0
  46. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/fillzenodousage.usg +0 -0
  47. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/log_arch.py +0 -0
  48. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/logarch.py +0 -0
  49. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/logarch.usg +0 -0
  50. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/pg_view.py +0 -0
  51. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/pgsyspath.py +0 -0
  52. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/view_allusage.py +0 -0
  53. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/view_awsusage.py +0 -0
  54. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/view_checkusage.py +0 -0
  55. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/view_codusage.py +0 -0
  56. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/view_ordusage.py +0 -0
  57. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/view_osdfusage.py +0 -0
  58. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/view_rqstusage.py +0 -0
  59. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/view_tdsusage.py +0 -0
  60. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/view_webfile.py +0 -0
  61. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/view_webusage.py +0 -0
  62. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewallusage.py +0 -0
  63. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewallusage.usg +0 -0
  64. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewawsusage.py +0 -0
  65. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewawsusage.usg +0 -0
  66. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewcheckusage.py +0 -0
  67. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewcheckusage.usg +0 -0
  68. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewcodusage.py +0 -0
  69. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewcodusage.usg +0 -0
  70. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewordusage.py +0 -0
  71. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewordusage.usg +0 -0
  72. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewosdfusage.py +0 -0
  73. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewosdfusage.usg +0 -0
  74. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewrqstusage.py +0 -0
  75. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewrqstusage.usg +0 -0
  76. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewtdsusage.py +0 -0
  77. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewtdsusage.usg +0 -0
  78. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewwebfile.py +0 -0
  79. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewwebfile.usg +0 -0
  80. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewwebusage.py +0 -0
  81. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics/viewwebusage.usg +0 -0
  82. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics.egg-info/SOURCES.txt +0 -0
  83. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics.egg-info/dependency_links.txt +0 -0
  84. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics.egg-info/entry_points.txt +0 -0
  85. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics.egg-info/requires.txt +0 -0
  86. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/src/rda_python_metrics.egg-info/top_level.txt +0 -0
  87. {rda_python_metrics-3.0.6 → rda_python_metrics-3.0.8}/tests/test_metrics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_metrics
3
- Version: 3.0.6
3
+ Version: 3.0.8
4
4
  Summary: RDA Python Package to gather and view data usage metrics
5
5
  Author-email: Zaihua Ji <zji@ucar.edu>
6
6
  Project-URL: Homepage, https://github.com/NCAR/rda-python-metrics
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "rda_python_metrics"
9
- version = "3.0.6"
9
+ version = "3.0.8"
10
10
  authors = [
11
11
  { name="Zaihua Ji", email="zji@ucar.edu" },
12
12
  ]
@@ -107,6 +107,7 @@ class FillAWSUsage(PgIPInfo, PgFile):
107
107
  for logfile in fnames:
108
108
  aws = self.open_local_file(logfile)
109
109
  if not aws: continue
110
+ self.prescan_ips(aws, r'^\w+ [\w-]+ \[[^\]]*\] ([\da-fA-F.:]+) ')
110
111
  while True:
111
112
  line = aws.readline()
112
113
  if not line: break
@@ -114,13 +115,15 @@ class FillAWSUsage(PgIPInfo, PgFile):
114
115
  if entcnt%20000 == 0:
115
116
  dcnt = len(records)
116
117
  self.pglog("{}: {}/{} AWS log entries processed/records to add".format(pdate, entcnt, dcnt), self.WARNLG)
117
- ms = re.match(r'^\w+ ([\w-]+) \[(\S+).*\] ([\d\.]+) .+ REST\.GET\.OBJECT \S+ "GET.+" \d+ - (\d+) \d+ .* ".+" "(.+)" ', line)
118
+ # the ip field holds an IPv6 address as well as an IPv4 one
119
+ ms = re.match(r'^\w+ ([\w-]+) \[(\S+).*\] ([\da-fA-F.:]+) .+ REST\.GET\.OBJECT \S+ "GET.+" \d+ - (\d+) \d+ .* ".+" "(.+)" ', line)
118
120
  if not ms: continue
119
121
  values = list(ms.groups())
120
122
  if values[0] not in self.DSIDS: continue
121
123
  dsid = self.DSIDS[values[0]]
122
124
  size = int(values[3])
123
- ip = values[2]
125
+ ip = self.normalize_ip(values[2])
126
+ if not ip: continue
124
127
  engine = values[4]
125
128
  moff = engine.find('/')
126
129
  if moff > 0:
@@ -204,6 +204,8 @@ class FillCDGUsage(PgSplit, PgIPInfo):
204
204
  self.pglog("{}: Query for {} CDG dsid/subdsids between {} and {} at {}".format(strids, dscnt, dates[0], dates[1], self.current_datetime()), self.LOGWRN)
205
205
  pgrecs = self.pgmget(tbname, fields, cond)
206
206
  self.dssdb_dbname()
207
+ # back on dssdb, where table ipinfo is, before presolving the host names
208
+ if pgrecs: self.preset_ip_hostnames(pgrecs['remote_address'])
207
209
  return pgrecs
208
210
 
209
211
  def fill_cdg_usages(self, dsids, dranges):
@@ -243,7 +245,8 @@ class FillCDGUsage(PgSplit, PgIPInfo):
243
245
  (year, quarter, date, time) = self.get_record_date_time(pgrec['date_completed'])
244
246
  url = pgrec['dataset_file_file_access_point_uri']
245
247
  if not url: url = pgrec['file_access_point_uri']
246
- ip = pgrec['remote_address']
248
+ ip = self.normalize_ip(pgrec['remote_address'])
249
+ if not ip: continue
247
250
  engine = pgrec['user_agent_name']
248
251
  ms = re.search(r'^https*://tds.ucar.edu/thredds/(\w+)/', url)
249
252
  if ms:
@@ -116,9 +116,11 @@ class FillCODUsage(PgIPInfo):
116
116
  size = int(ms.group(2))
117
117
  engine = ms.group(3)
118
118
  if not (aid in self.USERS or self.cache_users(aid)): continue
119
- ms = re.match(r'^([\d\.]+).+\[(\d+)/(\w+)/(\d+):([\d:]+)', line)
119
+ # the ip field holds an IPv6 address as well as an IPv4 one
120
+ ms = re.match(r'^([\da-fA-F.:]+).+\[(\d+)/(\w+)/(\d+):([\d:]+)', line)
120
121
  if not ms: continue
121
- ip = ms.group(1)
122
+ ip = self.normalize_ip(ms.group(1))
123
+ if not ip: continue
122
124
  ctime = ms.group(5)
123
125
  cdate = "{}-{:02}-{:02}".format(ms.group(4), self.get_month(ms.group(3)), int(ms.group(2)))
124
126
  if pdate != cdate:
@@ -153,6 +155,7 @@ class FillCODUsage(PgIPInfo):
153
155
  year = ms.group(1)
154
156
  quarter = 1 + int((int(ms.group(2)) - 1) / 3)
155
157
  cnt = 0
158
+ self.preset_ip_hostnames([records[aid]['ip'] for aid in records])
156
159
  for aid in records:
157
160
  if self.pgget(self.USAGE['PGTBL'], '', "aid = '{}' AND date = '{}'".format(aid, date), self.LGEREX): continue
158
161
  record = records[aid]
@@ -162,7 +165,7 @@ class FillCODUsage(PgIPInfo):
162
165
  record['org_type'] = wurec['org_type']
163
166
  record['country'] = wurec['country']
164
167
  record['region'] = wurec['region']
165
- record['email'] = 'unknown@' + wurec['hostname']
168
+ record['email'] = wurec['email'] # already 'unknown@<hostname>' for a generic ip
166
169
  else:
167
170
  wuid = self.check_wuser_wuid(record['email'], date)
168
171
  if not wuid: continue
@@ -857,6 +857,8 @@ class FillGDEXUsage(PgSplit, PgIPInfo):
857
857
  self.pglog("{}: Query for {} GDEX dsid/subdsids between {} and {} at {}".format(strids, dscnt, dates[0], dates[1], self.current_datetime()), self.LOGWRN)
858
858
  pgrecs = self.pgmget(tbname, fields, cond)
859
859
  self.dssdb_dbname()
860
+ # back on dssdb, where table ipinfo is, before presolving the host names
861
+ if pgrecs: self.preset_ip_hostnames(pgrecs['remote_address'])
860
862
 
861
863
  return pgrecs
862
864
 
@@ -892,7 +894,8 @@ class FillGDEXUsage(PgSplit, PgIPInfo):
892
894
  dsize = pgrec['bytes_sent']
893
895
  if not dsize: continue
894
896
  (year, quarter, date, time) = self.get_record_date_time(pgrec['date_completed'])
895
- ip = pgrec['remote_address']
897
+ ip = self.normalize_ip(pgrec['remote_address'])
898
+ if not ip: continue
896
899
  engine = pgrec['user_agent_name']
897
900
  wfile = pgrec['dataset_file_name']
898
901
  if not wfile: wfile = pgrec['logical_file_name']
@@ -106,6 +106,7 @@ class FillGlobusUsage(PgIPInfo, PgSplit, PgFile):
106
106
  self.pglog("Gathering usage info from {} at {}".format(logfile, self.current_datetime()), self.LOGWRN)
107
107
  globus = self.open_local_file(logfile)
108
108
  if not globus: continue
109
+ self.prescan_ips(globus, r'^([\da-fA-F.:]+)\s')
109
110
  ptime = ''
110
111
  record = {}
111
112
  cntadd = entcnt = 0
@@ -116,11 +117,13 @@ class FillGlobusUsage(PgIPInfo, PgSplit, PgFile):
116
117
  entcnt += 1
117
118
  if entcnt%10000 == 0:
118
119
  self.pglog("{}: {}/{} Globus log entries processed/records added".format(logfile, entcnt, cntadd), self.WARNLG)
119
- ms = re.match(r'^([\d\.]+)\s.*\s+\[(\S+).*"GET\s+/(ds\d\d\d\.\d|[a-z]\d{6})/(\S+)\s.*\s(200|206)\s+(\d+)\s+"(\S+)"\s+"(.+)"$', line)
120
+ # the ip field holds an IPv6 address as well as an IPv4 one
121
+ ms = re.match(r'^([\da-fA-F.:]+)\s.*\s+\[(\S+).*"GET\s+/(ds\d\d\d\.\d|[a-z]\d{6})/(\S+)\s.*\s(200|206)\s+(\d+)\s+"(\S+)"\s+"(.+)"$', line)
120
122
  if not ms: continue
121
123
  size = int(ms.group(6))
122
124
  if size < 100: continue # ignore small files
123
- ip = ms.group(1)
125
+ ip = self.normalize_ip(ms.group(1))
126
+ if not ip: continue
124
127
  dsid = self.format_dataset_id(ms.group(3))
125
128
  wfile = ms.group(4)
126
129
  stat = ms.group(5)
@@ -117,6 +117,7 @@ class FillIPInfo(PgIPInfo):
117
117
  if not pgrecs: return 0
118
118
  cnt = len(pgrecs['ip']) if pgrecs else 0
119
119
  mcnt = 0
120
+ self.preset_ip_hostnames(pgrecs['ip'])
120
121
  for i in range(cnt):
121
122
  record = self.get_missing_ipinfo(pgrecs['ip'][i], pgrecs['email'][i])
122
123
  if record: mcnt += self.pgupdt(table, record, "aidx = '{}'".format(pgrecs['aidx'][i]))
@@ -132,6 +133,7 @@ class FillIPInfo(PgIPInfo):
132
133
  if not pgrecs: return 0
133
134
  cnt = len(pgrecs['ip']) if pgrecs else 0
134
135
  mcnt = 0
136
+ self.preset_ip_hostnames(pgrecs['ip'])
135
137
  for i in range(cnt):
136
138
  ip = pgrecs['ip'][i]
137
139
  record = self.get_missing_ipinfo(ip, pgrecs['email'][i])
@@ -150,6 +152,7 @@ class FillIPInfo(PgIPInfo):
150
152
  if not pgrecs: return 0
151
153
  cnt = len(pgrecs['ip']) if pgrecs else 0
152
154
  mcnt = 0
155
+ self.preset_ip_hostnames(pgrecs['ip'])
153
156
  for i in range(cnt):
154
157
  record = self.get_missing_ipinfo(pgrecs['ip'][i], pgrecs['email'][i])
155
158
  if record:
@@ -183,6 +186,7 @@ class FillIPInfo(PgIPInfo):
183
186
  if not pgrecs: return 0
184
187
  cnt = len(pgrecs['ip']) if pgrecs else 0
185
188
  mcnt = 0
189
+ self.preset_ip_hostnames(pgrecs['ip'])
186
190
  for i in range(cnt):
187
191
  if self.set_ipinfo(pgrecs['ip'][i]): mcnt +=1
188
192
  s = 's' if cnt > 1 else ''
@@ -105,6 +105,7 @@ class FillOSDFUsage(PgIPInfo, PgFile, PgSplit):
105
105
  osdf = self.open_local_file(logfile)
106
106
  if not osdf: continue
107
107
  records = {}
108
+ ips = {}
108
109
  cntadd = entcnt = 0
109
110
  while True:
110
111
  line = osdf.readline()
@@ -119,8 +120,10 @@ class FillOSDFUsage(PgIPInfo, PgFile, PgSplit):
119
120
  dt = ms.group(1)
120
121
  dsid = ms.group(2)
121
122
  site = ms.group(3)
122
- ip = ms.group(4)
123
- if ip == 'N/A': ip = self.GIP
123
+ # normalize before keying on it, so one address spelled two ways in the
124
+ # log aggregates into one usage record rather than two
125
+ ip = self.GIP if ms.group(4) == 'N/A' else self.normalize_ip(ms.group(4))
126
+ if not ip: continue
124
127
  engine = ms.group(5)
125
128
  size = int(ms.group(6))
126
129
  if re.match(r'^N/A', engine, re.I):
@@ -138,13 +141,13 @@ class FillOSDFUsage(PgIPInfo, PgFile, PgSplit):
138
141
  records[key]['fcount'] += 1
139
142
  else:
140
143
  (year, quarter, date, time) = self.get_record_date_time(dt)
141
- iprec = self.get_missing_ipinfo(ip)
142
- if not iprec: continue
143
- records[key] = {'ip' : iprec['ip'], 'dsid' : dsid, 'date' : date, 'time' : time, 'quarter' : quarter,
144
- 'size' : size, 'fcount' : 1, 'method' : method, 'engine' : engine,
145
- 'org_type' : iprec['org_type'], 'country' : iprec['country'],
146
- 'region' : iprec['region'], 'email' : iprec['email'], 'site' : site}
144
+ ips[ip] = 1
145
+ records[key] = {'ip' : ip, 'dsid' : dsid, 'date' : date, 'time' : time, 'quarter' : quarter,
146
+ 'size' : size, 'fcount' : 1, 'method' : method, 'engine' : engine, 'site' : site}
147
147
  osdf.close()
148
+ if records:
149
+ self.preset_ip_hostnames(list(ips))
150
+ records = self.fill_ipinfo_records(records)
148
151
  if records: cntadd = self.add_usage_records(records, year)
149
152
  self.pglog("{}: {} OSDF usage records added for {} entries at {}".format(logfile, cntadd, entcnt, self.current_datetime()), self.LOGWRN)
150
153
  cntall += entcnt
@@ -166,6 +169,21 @@ class FillOSDFUsage(PgIPInfo, PgFile, PgSplit):
166
169
  else:
167
170
  self.pglog(ctime + ": Invalid date/time format", self.LGEREX)
168
171
 
172
+ def fill_ipinfo_records(self, records):
173
+ """Add the ip info fields to the usage records; drop the ones without ip info."""
174
+ for key in list(records):
175
+ record = records[key]
176
+ iprec = self.get_missing_ipinfo(record['ip'])
177
+ if not iprec:
178
+ del records[key]
179
+ continue
180
+ record['ip'] = iprec['ip']
181
+ record['org_type'] = iprec['org_type']
182
+ record['country'] = iprec['country']
183
+ record['region'] = iprec['region']
184
+ record['email'] = iprec['email']
185
+ return records
186
+
169
187
  def add_usage_records(self, records, year):
170
188
  """Add usage to table osdfusage."""
171
189
  cnt = 0
@@ -115,6 +115,7 @@ class FillTDSUsage(PgIPInfo, PgFile):
115
115
  self.pglog("{}: Gathering TDS usage at {}".format(logfile, self.current_datetime()), self.LOGWRN)
116
116
  tds = self.open_local_file(logfile)
117
117
  if not tds: continue
118
+ self.prescan_ips(tds, r'^([\da-fA-F.:]+)\s')
118
119
  records = {}
119
120
  cntadd = entcnt = 0
120
121
  while True:
@@ -128,7 +129,8 @@ class FillTDSUsage(PgIPInfo, PgFile):
128
129
  if ms: continue
129
130
  ms = re.search(r'/thredds/\S+\.(png|jpg|gif|css|htm)', line)
130
131
  if ms: continue
131
- ms = re.match(r'^([\d\.]+)\s.*\s(-|\S+@\S+)\s+\[(\S+).*/thredds/(\w+)(/|/grid/)(aggregations|files).*/(ds\d\d\d.\d|[a-z]\d{6})/.*\s200\s+(\d+)(.*)$', line)
132
+ # the ip field holds an IPv6 address as well as an IPv4 one
133
+ ms = re.match(r'^([\da-fA-F.:]+)\s.*\s(-|\S+@\S+)\s+\[(\S+).*/thredds/(\w+)(/|/grid/)(aggregations|files).*/(ds\d\d\d.\d|[a-z]\d{6})/.*\s200\s+(\d+)(.*)$', line)
132
134
  if not ms: continue
133
135
  ip = ms.group(1)
134
136
  email = ms.group(2)
@@ -178,6 +180,7 @@ class FillTDSUsage(PgIPInfo, PgFile):
178
180
  if ms:
179
181
  year = ms.group(1)
180
182
  quarter = 1 + int((int(ms.group(2)) - 1)/3)
183
+ self.preset_ip_hostnames([records[key]['ip'] for key in records])
181
184
  for key in records:
182
185
  record = records[key]
183
186
  cond = "date = '{}' AND time = '{}' AND ip = '{}' AND dsid = '{}'".format(date, record['time'], record['ip'], record['dsid'])
@@ -189,7 +192,7 @@ class FillTDSUsage(PgIPInfo, PgFile):
189
192
  record['org_type'] = wurec['org_type']
190
193
  record['country'] = wurec['country']
191
194
  record['region'] = wurec['region']
192
- record['email'] = 'unknown@' + wurec['hostname']
195
+ record['email'] = wurec['email'] # already 'unknown@<hostname>' for a generic ip
193
196
  record['quarter'] = quarter
194
197
  record['date'] = date
195
198
  if self.add_to_allusage(year, record):
@@ -284,6 +284,8 @@ class FillZenodoUsage(PgSplit, PgIPInfo):
284
284
  self.pglog("{}: Query for {} GDEX dsid/subdsids between {} and {} at {}".format(strids, dscnt, dates[0], dates[1], self.current_datetime()), self.LOGWRN)
285
285
  pgrecs = self.pgmget(tbname, fields, cond)
286
286
  self.dssdb_dbname()
287
+ # back on dssdb, where table ipinfo is, before presolving the host names
288
+ if pgrecs: self.preset_ip_hostnames(pgrecs['remote_address'])
287
289
 
288
290
  return pgrecs
289
291
 
@@ -322,7 +324,8 @@ class FillZenodoUsage(PgSplit, PgIPInfo):
322
324
  (year, quarter, date, time) = self.get_record_date_time(pgrec['date_completed'])
323
325
  url = pgrec['dataset_file_file_access_point_uri']
324
326
  if not url: url = pgrec['file_access_point_uri']
325
- ip = pgrec['remote_address']
327
+ ip = self.normalize_ip(pgrec['remote_address'])
328
+ if not ip: continue
326
329
  engine = pgrec['user_agent_name']
327
330
  ms = re.search(r'^https*://tds.ucar.edu/thredds/(\w+)/', url)
328
331
  if ms:
@@ -12,9 +12,12 @@
12
12
  import re
13
13
  import geoip2.database as geodb
14
14
  import ipinfo
15
- import socket
15
+ from ipinfo.exceptions import RequestQuotaExceededError
16
16
  import dns.resolver
17
+ import dns.reversename
17
18
  import json
19
+ import ipaddress
20
+ from concurrent.futures import ThreadPoolExecutor
18
21
  from rda_python_common.pg_dbi import PgDBI
19
22
  from rda_python_common.pg_util import PgUtil
20
23
 
@@ -30,7 +33,11 @@ class PgIPInfo(PgUtil, PgDBI):
30
33
  'DBFILE' : self.PGLOG['DSSHOME'] + '/dssdb/GeoLite2-City.mmdb',
31
34
  'CDATE' : self.curdate(),
32
35
  'IPUPDT' : 0,
33
- 'IPADD' : 0
36
+ 'IPADD' : 0,
37
+ 'QUOTA' : 1, # unset when the ipinfo monthly request quota is used up
38
+ 'DNSTMO' : 1, # seconds to wait on a single name server
39
+ 'DNSLTM' : 2, # seconds to spend on one name resolution in total
40
+ 'DNSTHD' : 8 # number of threads used to presolve a list of ip addresses
34
41
  }
35
42
  self.GIP = '0.0.0.0'
36
43
  self.IPDNS = None
@@ -38,11 +45,35 @@ class PgIPInfo(PgUtil, PgDBI):
38
45
  self.G2DB = None
39
46
  self.IPRECS = {}
40
47
  self.DMRECS = {}
48
+ self.IPDMS = {}
49
+ self.IPNRM = {}
50
+ self.IPSKP = {}
41
51
  self.COUNTRIES = {}
42
52
 
53
+ def normalize_ip(self, ip):
54
+ """Return the canonical form of an ip address; empty string if it is not valid.
55
+
56
+ One IPv6 address can be written several ways, upper or lower case and with the
57
+ zero groups spelled out or collapsed, and the IPv4-embedded form is longer than
58
+ the 39 characters ipinfo.ip holds. Canonicalizing on the way in keeps one row
59
+ and one set of usage records per address whichever way a log spells it.
60
+ """
61
+ if ip in self.IPNRM: return self.IPNRM[ip]
62
+ try:
63
+ nip = ipaddress.ip_address(ip).compressed
64
+ except ValueError:
65
+ self.pglog("{}: invalid ip address".format(ip), self.LOGWRN)
66
+ nip = ''
67
+ self.IPNRM[ip] = nip
68
+ return nip
69
+
43
70
  def get_dns_resolver(self, forceget = False):
44
71
  """Get save a global dns.resolver.Resolver object."""
45
- if forceget or not self.IPDNS: self.IPDNS = dns.resolver.Resolver()
72
+ if forceget or not self.IPDNS:
73
+ self.IPDNS = dns.resolver.Resolver()
74
+ self.IPDNS.timeout = self.IPINFO['DNSTMO']
75
+ self.IPDNS.lifetime = self.IPINFO['DNSLTM']
76
+ self.IPDNS.cache = dns.resolver.Cache()
46
77
  return self.IPDNS
47
78
 
48
79
  def dns_to_ip(self, dmname, type = 'A'):
@@ -89,32 +120,121 @@ class PgIPInfo(PgUtil, PgDBI):
89
120
  if ips: return self.set_ipinfo(ips[0])
90
121
  return None
91
122
 
123
+ def reverse_resolve_ip(self, ip):
124
+ """Reverse resolve one ip address without logging; called from worker threads too.
125
+
126
+ Returns tuple (ip, hostname, errmsg); hostname is empty if the ip has no PTR record.
127
+ """
128
+ try:
129
+ answers = self.IPDNS.resolve(dns.reversename.from_address(ip), 'PTR')
130
+ # an ip may have several PTR records, answered in varying order; always pick
131
+ # the same one so the host name on file does not flip between runs
132
+ if answers: return (ip, min([str(a).rstrip('.') for a in answers]), None)
133
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
134
+ pass # no PTR record on file for the ip, too common to be worth logging
135
+ except dns.exception.DNSException as e:
136
+ return (ip, '', "{}: error reverse domain name request: {}".format(ip, e))
137
+ return (ip, '', None)
138
+
139
+ def ip_to_dns(self, ip):
140
+ """Reverse resolve an ip address to a host name; empty string if none."""
141
+ if ip in self.IPDMS: return self.IPDMS[ip]
142
+ self.get_dns_resolver()
143
+ (ip, dm, errmsg) = self.reverse_resolve_ip(ip)
144
+ if errmsg: self.pglog(errmsg, self.LOGWRN)
145
+ self.IPDMS[ip] = dm
146
+ return dm
147
+
148
+ def prescan_ips(self, fd, pattern, group = 1):
149
+ """Scan an open log file for ip addresses, presolve them, and rewind the file.
150
+
151
+ A fill program reads its log line by line and looks each new ip up as it goes,
152
+ which serializes one slow reverse DNS lookup per address. Picking the addresses
153
+ out in a first cheap pass lets preset_ip_hostnames() resolve them several at a
154
+ time, so the main loop finds the host names already cached.
155
+ """
156
+ ips = {}
157
+ for line in fd:
158
+ ms = re.match(pattern, line)
159
+ if ms: ips[ms.group(group)] = 1
160
+ fd.seek(0)
161
+ return self.preset_ip_hostnames(list(ips))
162
+
163
+ def get_recorded_ips(self, ips):
164
+ """Return a dict of the given ip addresses that have an ipinfo record on file."""
165
+ recorded = {}
166
+ for i in range(0, len(ips), 1000):
167
+ vals = ', '.join(["'{}'".format(ip) for ip in ips[i:i+1000]])
168
+ pgrecs = self.pgmget('ipinfo', 'ip', "ip IN ({})".format(vals), self.LOGWRN)
169
+ if pgrecs:
170
+ for ip in pgrecs['ip']: recorded[ip] = 1
171
+ return recorded
172
+
173
+ def preset_ip_hostnames(self, ips):
174
+ """Reverse resolve a list of ip addresses in parallel and cache the host names.
175
+
176
+ Resolving one ip at a time is the slowest part of gathering usage from a large
177
+ log file. Doing the new ones up front, several at a time, leaves the host names
178
+ in self.IPDMS for get_ip_hostname() to pick up without waiting again.
179
+ """
180
+ ips = [ip for ip in {self.normalize_ip(ip) for ip in ips}
181
+ if ip and ip not in self.IPDMS and ip not in self.IPSKP]
182
+ if not ips: return 0
183
+ # ips on file are either reused as is or refreshed by ipinfo, which carries its
184
+ # own host name, so only the ones not on file are worth resolving ahead of time.
185
+ # remember which ones those were, or every later log file queries them again
186
+ recorded = self.get_recorded_ips(ips)
187
+ self.IPSKP.update(recorded)
188
+ newips = [ip for ip in ips if ip not in recorded]
189
+ if not newips: return 0
190
+ self.get_dns_resolver() # create the shared resolver before starting the threads
191
+ cnt = 0
192
+ with ThreadPoolExecutor(max_workers = self.IPINFO['DNSTHD']) as executor:
193
+ for (ip, dm, errmsg) in executor.map(self.reverse_resolve_ip, newips):
194
+ if errmsg: self.pglog(errmsg, self.LOGWRN)
195
+ self.IPDMS[ip] = dm
196
+ if dm: cnt += 1
197
+ self.pglog("{}/{} host names presolved for {} IP addresses".format(cnt, len(newips), len(ips)), self.LOGWRN)
198
+ return cnt
199
+
92
200
  def get_ip_hostname(self, ip, iprec, record):
93
- """Try to get hostname via socket for given ip address."""
201
+ """Try to get hostname via reverse DNS for given ip address."""
94
202
  record['hostname'] = ip
95
203
  if iprec:
96
204
  if 'hostname' in iprec and iprec['hostname']:
97
- record['hostname'] = iprec['hostname']
205
+ record['hostname'] = iprec['hostname']
98
206
  record['org_type'] = self.get_org_type(None, record['hostname'])
99
207
  return
100
208
  if 'asn' in iprec and iprec['asn'] and 'domain' in iprec['asn'] and iprec['asn']['domain']:
101
209
  record['hostname'] += '.' + iprec['asn']['domain']
102
210
  record['org_type'] = self.get_org_type(None, record['hostname'])
103
211
  return
104
- try:
105
- hostrec = socket.gethostbyaddr(ip)
106
- record['hostname'] = hostrec[1][0] if hostrec[1] else hostrec[0]
107
- record['org_type'] = self.get_org_type(None, record['hostname'])
108
- except Exception as e:
109
- self.pglog("socket: {} - {}".format(ip, str(e)), self.LOGWRN)
212
+ dm = self.ip_to_dns(ip)
213
+ if dm:
214
+ record['hostname'] = dm
215
+ record['org_type'] = self.get_org_type(None, dm)
216
+
217
+ def unset_ipinfo_quota(self):
218
+ """Stop calling ipinfo for the rest of the run once its quota is used up."""
219
+ if self.IPINFO['QUOTA']:
220
+ self.IPINFO['QUOTA'] = 0
221
+ self.pglog("ipinfo: monthly request quota used up, use geoip2 for the rest of the run", self.LOGWRN)
110
222
 
111
223
  def get_ipinfo_record(self, ip):
112
224
  """Get a ipinfo record for given ip address."""
225
+ if not self.IPINFO['QUOTA']: return None
113
226
  if not self.IPDB: self.set_ipinfo_database()
114
227
  try:
115
228
  iprec = self.IPDB.getDetails(ip).all
229
+ except RequestQuotaExceededError:
230
+ self.unset_ipinfo_quota()
231
+ return None
116
232
  except Exception as e:
117
- self.pglog("ipinfo: {} - {}".format(ip, str(e)), self.LOGWRN)
233
+ estr = str(e)
234
+ if re.search(r'(\b429\b|quota)', estr, re.I):
235
+ self.unset_ipinfo_quota()
236
+ else:
237
+ self.pglog("ipinfo: {} - {}".format(ip, estr), self.LOGWRN)
118
238
  return None
119
239
  if 'bogon' in iprec and iprec['bogon']:
120
240
  self.pglog(f"ipinfo: {ip} - bogon, use {self.GIP}", self.LOGWRN)
@@ -195,7 +315,10 @@ class PgIPInfo(PgUtil, PgDBI):
195
315
 
196
316
  def set_ipinfo(self, ip, ipopt = True):
197
317
  """If ipopt is True; otherwise, use module geoip2."""
318
+ ip = self.normalize_ip(ip)
319
+ if not ip: return None
198
320
  if ip in self.IPRECS: return self.IPRECS[ip]
321
+ if ipopt and not self.IPINFO['QUOTA']: ipopt = False # quota used up, geoip2 only
199
322
  pgrec = self.pgget('ipinfo', '*', "ip = '{}'".format(ip))
200
323
  if not pgrec or ipopt and pgrec['stat_flag'] == 'M':
201
324
  record = self.get_ipinfo_record(ip) if ipopt else None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_metrics
3
- Version: 3.0.6
3
+ Version: 3.0.8
4
4
  Summary: RDA Python Package to gather and view data usage metrics
5
5
  Author-email: Zaihua Ji <zji@ucar.edu>
6
6
  Project-URL: Homepage, https://github.com/NCAR/rda-python-metrics