rda-python-metrics 3.0.5__tar.gz → 3.0.7__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.5/src/rda_python_metrics.egg-info → rda_python_metrics-3.0.7}/PKG-INFO +1 -1
  2. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/pyproject.toml +1 -1
  3. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillawsusage.py +5 -2
  4. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillcdgusage.py +4 -1
  5. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillcodusage.py +6 -3
  6. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillgdexusage.py +4 -1
  7. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillglobususage.py +5 -2
  8. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillipinfo.py +4 -0
  9. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillosdfusage.py +28 -9
  10. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/filltdsusage.py +5 -2
  11. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillzenodousage.py +4 -1
  12. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/pg_ipinfo.py +131 -12
  13. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7/src/rda_python_metrics.egg-info}/PKG-INFO +1 -1
  14. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/LICENSE +0 -0
  15. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/MANIFEST.in +0 -0
  16. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/README.md +0 -0
  17. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/setup.cfg +0 -0
  18. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/PgIPInfo.py +0 -0
  19. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/PgView.py +0 -0
  20. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/__init__.py +0 -0
  21. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fill_awsusage.py +0 -0
  22. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fill_cdgusage.py +0 -0
  23. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fill_codusage.py +0 -0
  24. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fill_country.py +0 -0
  25. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fill_endtime.py +0 -0
  26. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fill_globususage.py +0 -0
  27. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fill_ipinfo.py +0 -0
  28. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fill_oneorder.py +0 -0
  29. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fill_osdfusage.py +0 -0
  30. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fill_rdadb.py +0 -0
  31. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fill_tdsusage.py +0 -0
  32. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillawsusage.usg +0 -0
  33. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillcdgusage.usg +0 -0
  34. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillcodusage.usg +0 -0
  35. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillcountry.py +0 -0
  36. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillendtime.py +0 -0
  37. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillgdexusage.usg +0 -0
  38. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillglobususage.usg +0 -0
  39. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillipinfo.usg +0 -0
  40. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/filloneorder.py +0 -0
  41. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/filloneorder.usg +0 -0
  42. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillosdfusage.usg +0 -0
  43. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillrdadb.py +0 -0
  44. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillrdadb.usg +0 -0
  45. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/filltdsusage.usg +0 -0
  46. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/fillzenodousage.usg +0 -0
  47. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/log_arch.py +0 -0
  48. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/logarch.py +0 -0
  49. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/logarch.usg +0 -0
  50. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/pg_view.py +0 -0
  51. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/pgsyspath.py +0 -0
  52. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/view_allusage.py +0 -0
  53. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/view_awsusage.py +0 -0
  54. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/view_checkusage.py +0 -0
  55. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/view_codusage.py +0 -0
  56. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/view_ordusage.py +0 -0
  57. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/view_osdfusage.py +0 -0
  58. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/view_rqstusage.py +0 -0
  59. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/view_tdsusage.py +0 -0
  60. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/view_webfile.py +0 -0
  61. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/view_webusage.py +0 -0
  62. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewallusage.py +0 -0
  63. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewallusage.usg +0 -0
  64. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewawsusage.py +0 -0
  65. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewawsusage.usg +0 -0
  66. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewcheckusage.py +0 -0
  67. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewcheckusage.usg +0 -0
  68. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewcodusage.py +0 -0
  69. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewcodusage.usg +0 -0
  70. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewordusage.py +0 -0
  71. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewordusage.usg +0 -0
  72. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewosdfusage.py +0 -0
  73. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewosdfusage.usg +0 -0
  74. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewrqstusage.py +0 -0
  75. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewrqstusage.usg +0 -0
  76. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewtdsusage.py +0 -0
  77. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewtdsusage.usg +0 -0
  78. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewwebfile.py +0 -0
  79. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewwebfile.usg +0 -0
  80. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewwebusage.py +0 -0
  81. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics/viewwebusage.usg +0 -0
  82. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics.egg-info/SOURCES.txt +0 -0
  83. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics.egg-info/dependency_links.txt +0 -0
  84. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics.egg-info/entry_points.txt +0 -0
  85. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics.egg-info/requires.txt +0 -0
  86. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/src/rda_python_metrics.egg-info/top_level.txt +0 -0
  87. {rda_python_metrics-3.0.5 → rda_python_metrics-3.0.7}/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.5
3
+ Version: 3.0.7
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.5"
9
+ version = "3.0.7"
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()
@@ -113,13 +114,16 @@ class FillOSDFUsage(PgIPInfo, PgFile, PgSplit):
113
114
  if entcnt%20000 == 0:
114
115
  dcnt = len(records)
115
116
  self.pglog("{}: {}/{} OSDF log entries processed/records added".format(logfile, entcnt, dcnt), self.WARNLG)
116
- ms = re.match(r'^\[(\S+)\] \[Objectname:\/ncar\/rda\/([a-z]\d{6})\/\S+\].* \[Site:(\S+)\].* \[Host:(\S+)\].* \[AppInfo:(\S*)\].* \[Read:(\d+)\]', line)
117
+ # the OSDF namespace was renamed from /ncar/rda to /ncar/gdex
118
+ ms = re.match(r'^\[(\S+)\] \[Objectname:\/ncar\/(?:rda|gdex)\/([a-z]\d{6})\/\S+\].* \[Site:(\S+)\].* \[Host:(\S+)\].* \[AppInfo:(\S*)\].* \[Read:(\d+)\]', line)
117
119
  if not ms: continue
118
120
  dt = ms.group(1)
119
121
  dsid = ms.group(2)
120
122
  site = ms.group(3)
121
- ip = ms.group(4)
122
- 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
123
127
  engine = ms.group(5)
124
128
  size = int(ms.group(6))
125
129
  if re.match(r'^N/A', engine, re.I):
@@ -137,13 +141,13 @@ class FillOSDFUsage(PgIPInfo, PgFile, PgSplit):
137
141
  records[key]['fcount'] += 1
138
142
  else:
139
143
  (year, quarter, date, time) = self.get_record_date_time(dt)
140
- iprec = self.get_missing_ipinfo(ip)
141
- if not iprec: continue
142
- records[key] = {'ip' : iprec['ip'], 'dsid' : dsid, 'date' : date, 'time' : time, 'quarter' : quarter,
143
- 'size' : size, 'fcount' : 1, 'method' : method, 'engine' : engine,
144
- 'org_type' : iprec['org_type'], 'country' : iprec['country'],
145
- '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}
146
147
  osdf.close()
148
+ if records:
149
+ self.preset_ip_hostnames(list(ips))
150
+ records = self.fill_ipinfo_records(records)
147
151
  if records: cntadd = self.add_usage_records(records, year)
148
152
  self.pglog("{}: {} OSDF usage records added for {} entries at {}".format(logfile, cntadd, entcnt, self.current_datetime()), self.LOGWRN)
149
153
  cntall += entcnt
@@ -165,6 +169,21 @@ class FillOSDFUsage(PgIPInfo, PgFile, PgSplit):
165
169
  else:
166
170
  self.pglog(ctime + ": Invalid date/time format", self.LGEREX)
167
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
+
168
187
  def add_usage_records(self, records, year):
169
188
  """Add usage to table osdfusage."""
170
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,34 @@ class PgIPInfo(PgUtil, PgDBI):
38
45
  self.G2DB = None
39
46
  self.IPRECS = {}
40
47
  self.DMRECS = {}
48
+ self.IPDMS = {}
49
+ self.IPNRM = {}
41
50
  self.COUNTRIES = {}
42
51
 
52
+ def normalize_ip(self, ip):
53
+ """Return the canonical form of an ip address; empty string if it is not valid.
54
+
55
+ One IPv6 address can be written several ways, upper or lower case and with the
56
+ zero groups spelled out or collapsed, and the IPv4-embedded form is longer than
57
+ the 39 characters ipinfo.ip holds. Canonicalizing on the way in keeps one row
58
+ and one set of usage records per address whichever way a log spells it.
59
+ """
60
+ if ip in self.IPNRM: return self.IPNRM[ip]
61
+ try:
62
+ nip = ipaddress.ip_address(ip).compressed
63
+ except ValueError:
64
+ self.pglog("{}: invalid ip address".format(ip), self.LOGWRN)
65
+ nip = ''
66
+ self.IPNRM[ip] = nip
67
+ return nip
68
+
43
69
  def get_dns_resolver(self, forceget = False):
44
70
  """Get save a global dns.resolver.Resolver object."""
45
- if forceget or not self.IPDNS: self.IPDNS = dns.resolver.Resolver()
71
+ if forceget or not self.IPDNS:
72
+ self.IPDNS = dns.resolver.Resolver()
73
+ self.IPDNS.timeout = self.IPINFO['DNSTMO']
74
+ self.IPDNS.lifetime = self.IPINFO['DNSLTM']
75
+ self.IPDNS.cache = dns.resolver.Cache()
46
76
  return self.IPDNS
47
77
 
48
78
  def dns_to_ip(self, dmname, type = 'A'):
@@ -89,32 +119,118 @@ class PgIPInfo(PgUtil, PgDBI):
89
119
  if ips: return self.set_ipinfo(ips[0])
90
120
  return None
91
121
 
122
+ def reverse_resolve_ip(self, ip):
123
+ """Reverse resolve one ip address without logging; called from worker threads too.
124
+
125
+ Returns tuple (ip, hostname, errmsg); hostname is empty if the ip has no PTR record.
126
+ """
127
+ try:
128
+ answers = self.IPDNS.resolve(dns.reversename.from_address(ip), 'PTR')
129
+ # an ip may have several PTR records, answered in varying order; always pick
130
+ # the same one so the host name on file does not flip between runs
131
+ if answers: return (ip, min([str(a).rstrip('.') for a in answers]), None)
132
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
133
+ pass # no PTR record on file for the ip, too common to be worth logging
134
+ except dns.exception.DNSException as e:
135
+ return (ip, '', "{}: error reverse domain name request: {}".format(ip, e))
136
+ return (ip, '', None)
137
+
138
+ def ip_to_dns(self, ip):
139
+ """Reverse resolve an ip address to a host name; empty string if none."""
140
+ if ip in self.IPDMS: return self.IPDMS[ip]
141
+ self.get_dns_resolver()
142
+ (ip, dm, errmsg) = self.reverse_resolve_ip(ip)
143
+ if errmsg: self.pglog(errmsg, self.LOGWRN)
144
+ self.IPDMS[ip] = dm
145
+ return dm
146
+
147
+ def prescan_ips(self, fd, pattern, group = 1):
148
+ """Scan an open log file for ip addresses, presolve them, and rewind the file.
149
+
150
+ A fill program reads its log line by line and looks each new ip up as it goes,
151
+ which serializes one slow reverse DNS lookup per address. Picking the addresses
152
+ out in a first cheap pass lets preset_ip_hostnames() resolve them several at a
153
+ time, so the main loop finds the host names already cached.
154
+ """
155
+ ips = {}
156
+ for line in fd:
157
+ ms = re.match(pattern, line)
158
+ if ms: ips[ms.group(group)] = 1
159
+ fd.seek(0)
160
+ return self.preset_ip_hostnames(list(ips))
161
+
162
+ def get_recorded_ips(self, ips):
163
+ """Return a dict of the given ip addresses that have an ipinfo record on file."""
164
+ recorded = {}
165
+ for i in range(0, len(ips), 1000):
166
+ vals = ', '.join(["'{}'".format(ip) for ip in ips[i:i+1000]])
167
+ pgrecs = self.pgmget('ipinfo', 'ip', "ip IN ({})".format(vals), self.LOGWRN)
168
+ if pgrecs:
169
+ for ip in pgrecs['ip']: recorded[ip] = 1
170
+ return recorded
171
+
172
+ def preset_ip_hostnames(self, ips):
173
+ """Reverse resolve a list of ip addresses in parallel and cache the host names.
174
+
175
+ Resolving one ip at a time is the slowest part of gathering usage from a large
176
+ log file. Doing the new ones up front, several at a time, leaves the host names
177
+ in self.IPDMS for get_ip_hostname() to pick up without waiting again.
178
+ """
179
+ ips = [ip for ip in {self.normalize_ip(ip) for ip in ips} if ip and ip not in self.IPDMS]
180
+ if not ips: return 0
181
+ # ips on file are either reused as is or refreshed by ipinfo, which carries its
182
+ # own host name, so only the ones not on file are worth resolving ahead of time
183
+ recorded = self.get_recorded_ips(ips)
184
+ newips = [ip for ip in ips if ip not in recorded]
185
+ if not newips: return 0
186
+ self.get_dns_resolver() # create the shared resolver before starting the threads
187
+ cnt = 0
188
+ with ThreadPoolExecutor(max_workers = self.IPINFO['DNSTHD']) as executor:
189
+ for (ip, dm, errmsg) in executor.map(self.reverse_resolve_ip, newips):
190
+ if errmsg: self.pglog(errmsg, self.LOGWRN)
191
+ self.IPDMS[ip] = dm
192
+ if dm: cnt += 1
193
+ self.pglog("{}/{} host names presolved for {} IP addresses".format(cnt, len(newips), len(ips)), self.LOGWRN)
194
+ return cnt
195
+
92
196
  def get_ip_hostname(self, ip, iprec, record):
93
- """Try to get hostname via socket for given ip address."""
197
+ """Try to get hostname via reverse DNS for given ip address."""
94
198
  record['hostname'] = ip
95
199
  if iprec:
96
200
  if 'hostname' in iprec and iprec['hostname']:
97
- record['hostname'] = iprec['hostname']
201
+ record['hostname'] = iprec['hostname']
98
202
  record['org_type'] = self.get_org_type(None, record['hostname'])
99
203
  return
100
204
  if 'asn' in iprec and iprec['asn'] and 'domain' in iprec['asn'] and iprec['asn']['domain']:
101
205
  record['hostname'] += '.' + iprec['asn']['domain']
102
206
  record['org_type'] = self.get_org_type(None, record['hostname'])
103
207
  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)
208
+ dm = self.ip_to_dns(ip)
209
+ if dm:
210
+ record['hostname'] = dm
211
+ record['org_type'] = self.get_org_type(None, dm)
212
+
213
+ def unset_ipinfo_quota(self):
214
+ """Stop calling ipinfo for the rest of the run once its quota is used up."""
215
+ if self.IPINFO['QUOTA']:
216
+ self.IPINFO['QUOTA'] = 0
217
+ self.pglog("ipinfo: monthly request quota used up, use geoip2 for the rest of the run", self.LOGWRN)
110
218
 
111
219
  def get_ipinfo_record(self, ip):
112
220
  """Get a ipinfo record for given ip address."""
221
+ if not self.IPINFO['QUOTA']: return None
113
222
  if not self.IPDB: self.set_ipinfo_database()
114
223
  try:
115
224
  iprec = self.IPDB.getDetails(ip).all
225
+ except RequestQuotaExceededError:
226
+ self.unset_ipinfo_quota()
227
+ return None
116
228
  except Exception as e:
117
- self.pglog("ipinfo: {} - {}".format(ip, str(e)), self.LOGWRN)
229
+ estr = str(e)
230
+ if re.search(r'(\b429\b|quota)', estr, re.I):
231
+ self.unset_ipinfo_quota()
232
+ else:
233
+ self.pglog("ipinfo: {} - {}".format(ip, estr), self.LOGWRN)
118
234
  return None
119
235
  if 'bogon' in iprec and iprec['bogon']:
120
236
  self.pglog(f"ipinfo: {ip} - bogon, use {self.GIP}", self.LOGWRN)
@@ -195,7 +311,10 @@ class PgIPInfo(PgUtil, PgDBI):
195
311
 
196
312
  def set_ipinfo(self, ip, ipopt = True):
197
313
  """If ipopt is True; otherwise, use module geoip2."""
314
+ ip = self.normalize_ip(ip)
315
+ if not ip: return None
198
316
  if ip in self.IPRECS: return self.IPRECS[ip]
317
+ if ipopt and not self.IPINFO['QUOTA']: ipopt = False # quota used up, geoip2 only
199
318
  pgrec = self.pgget('ipinfo', '*', "ip = '{}'".format(ip))
200
319
  if not pgrec or ipopt and pgrec['stat_flag'] == 'M':
201
320
  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.5
3
+ Version: 3.0.7
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