django-log-formatter-asim 1.1.0a3__py3-none-any.whl → 1.2.0a0__py3-none-any.whl

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.
@@ -1,6 +1,5 @@
1
1
  import json
2
2
  import logging
3
- import os
4
3
  from datetime import datetime
5
4
  from datetime import timezone
6
5
  from importlib.metadata import distribution
@@ -9,6 +8,8 @@ import ddtrace
9
8
  from ddtrace.trace import tracer
10
9
  from django.conf import settings
11
10
 
11
+ from .ecs import _get_container_id
12
+
12
13
 
13
14
  class ASIMRootFormatter:
14
15
  def __init__(self, record):
@@ -29,19 +30,6 @@ class ASIMRootFormatter:
29
30
 
30
31
  return copied_dict
31
32
 
32
- def _get_container_id(self):
33
- """
34
- The dockerId (container Id) is available via the metadata endpoint. However, it looks like it is embedded in the
35
- metadata URL e.g.:
36
- ECS_CONTAINER_METADATA_URI=http://169.254.170.2/v3/709d1c10779d47b2a84db9eef2ebd041-0265927825
37
- See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4-response.html
38
- """
39
-
40
- try:
41
- return os.environ["ECS_CONTAINER_METADATA_URI"].split("/")[-1]
42
- except (KeyError, IndexError):
43
- return ""
44
-
45
33
  def _get_first_64_bits_of(self, trace_id):
46
34
  # See https://docs.datadoghq.com/tracing/other_telemetry/connect_logs_and_traces/python/#no-standard-library-logging
47
35
  return str((1 << 64) - 1 & trace_id)
@@ -65,7 +53,7 @@ class ASIMRootFormatter:
65
53
  event_dict["service"] = ddtrace.config.service or ""
66
54
  event_dict["version"] = ddtrace.config.version or ""
67
55
 
68
- event_dict["container_id"] = self._get_container_id()
56
+ event_dict["container_id"] = _get_container_id()
69
57
 
70
58
  return event_dict
71
59
 
@@ -0,0 +1,16 @@
1
+ import os
2
+
3
+
4
+ def _get_container_id():
5
+ """
6
+ The dockerId (container Id) is available via the metadata endpoint.
7
+
8
+ However, it looks like it is embedded in the metadata URL e.g.:
9
+ ECS_CONTAINER_METADATA_URI=http://169.254.170.2/v3/709d1c10779d47b2a84db9eef2ebd041-0265927825
10
+ See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4-response.html
11
+ """
12
+
13
+ try:
14
+ return os.environ["ECS_CONTAINER_METADATA_URI"].split("/")[-1]
15
+ except (KeyError, IndexError):
16
+ return ""
@@ -1,12 +1,16 @@
1
1
  import datetime
2
2
  import json
3
+ import os
3
4
  from enum import Enum
5
+ from hashlib import sha3_512
4
6
  from typing import Literal
5
7
  from typing import Optional
6
8
  from typing import TypedDict
7
9
 
8
10
  from django.http import HttpRequest
9
11
 
12
+ from django_log_formatter_asim.ecs import _get_container_id
13
+
10
14
  from .common import Client
11
15
  from .common import Result
12
16
  from .common import Server
@@ -27,7 +31,7 @@ class AuthenticationLoginMethod(str, Enum):
27
31
  ExternalIDP = "External IdP"
28
32
 
29
33
 
30
- class AuthenticationUser(TypedDict):
34
+ class AuthenticationUser(TypedDict, total=False):
31
35
  """Dictionary to represent properties of the users session."""
32
36
 
33
37
  """What type of role best describes this Authentication event."""
@@ -51,12 +55,6 @@ class AuthenticationUser(TypedDict):
51
55
  """
52
56
  username: Optional[str]
53
57
  """
54
- Email address for the user if one exists.
55
-
56
- Defaults to the logged in Django User.email if not provided.
57
- """
58
- email: Optional[str]
59
- """
60
58
  A unique identifier for this authentication session if one exists.
61
59
 
62
60
  Defaults to the Django Sessions session key if not provided.
@@ -86,7 +84,7 @@ def log_authentication(
86
84
  - Django Session middlewares Session Key
87
85
  - Client IP address
88
86
  - URL requested by the client
89
- - Server hostname
87
+ - Server domain name
90
88
  :param event: What authentication action was attempted, either "Logon" or "Logoff"
91
89
  :param result: What outcome did the action have, either "Success", "Failure", "Partial", "NA"
92
90
  :param login_method: What authentication mechanism was being used, one of:
@@ -112,17 +110,37 @@ def log_authentication(
112
110
 
113
111
  See also: https://learn.microsoft.com/en-us/azure/sentinel/normalization-schema-authentication
114
112
  """
115
- if user == None:
116
- user = {}
117
- if server == None:
118
- server = {}
119
- if client == None:
120
- client = {}
121
-
122
- event_created = time_generated or datetime.datetime.now(tz=datetime.timezone.utc)
123
113
 
114
+ _log_authentication(
115
+ request,
116
+ event,
117
+ result,
118
+ login_method,
119
+ user={} if user == None else user,
120
+ server={} if server == None else server,
121
+ client={} if client == None else client,
122
+ event_created=time_generated or datetime.datetime.now(tz=datetime.timezone.utc),
123
+ severity=severity,
124
+ result_details=result_details,
125
+ message=message,
126
+ )
127
+
128
+
129
+ def _log_authentication(
130
+ request: HttpRequest,
131
+ event: AuthenticationEvent,
132
+ result: Result,
133
+ login_method: AuthenticationLoginMethod,
134
+ user: AuthenticationUser,
135
+ server: Server,
136
+ client: Client,
137
+ event_created: datetime.datetime,
138
+ severity: Optional[Severity] = None,
139
+ result_details: Optional[str] = None,
140
+ message: Optional[str] = None,
141
+ ):
124
142
  log = {
125
- "EventCreated": event_created.isoformat(), # TODO: Should this really be EventCreated, or TimeGenerated
143
+ "EventStartTime": event_created.isoformat(),
126
144
  "EventSeverity": severity or _default_severity(result),
127
145
  "EventOriginalType": _event_code(event, result),
128
146
  "EventType": event,
@@ -132,10 +150,19 @@ def log_authentication(
132
150
  "EventSchemaVersion": "0.1.4",
133
151
  }
134
152
 
135
- if "hostname" in server:
136
- log["DvcHostname"] = server["hostname"]
153
+ if "domain_name" in server:
154
+ log["HttpHost"] = server["domain_name"]
137
155
  elif "HTTP_HOST" in request.META:
138
- log["DvcHostname"] = request.get_host()
156
+ log["HttpHost"] = request.get_host()
157
+
158
+ if "service_name" in server:
159
+ log["TargetAppName"] = server["service_name"]
160
+ elif os.environ.get("COPILOT_APPLICATION_NAME") and os.environ.get("COPILOT_SERVICE_NAME"):
161
+ app_name = f"{os.environ['COPILOT_APPLICATION_NAME']}-{os.environ['COPILOT_SERVICE_NAME']}"
162
+ log["TargetAppName"] = app_name
163
+
164
+ if container_id := _get_container_id():
165
+ log["TargetContainerId"] = container_id
139
166
 
140
167
  if "ip_address" in client:
141
168
  log["SrcIpAddr"] = client["ip_address"]
@@ -148,23 +175,18 @@ def log_authentication(
148
175
  log["TargetUrl"] = request.scheme + "://" + request.get_host() + request.get_full_path()
149
176
 
150
177
  if "role" in user:
151
- log["ActorUserType"] = user["role"]
178
+ log["TargetUserType"] = user["role"]
152
179
 
153
180
  if "sessionId" in user:
154
- log["ActorSessionId"] = user["sessionId"]
155
- elif request.session.session_key:
156
- log["ActorSessionId"] = request.session.session_key
181
+ log["TargetSessionId"] = _cryptographically_hash(user["sessionId"])
182
+ elif hasattr(request, "session") and request.session.session_key:
183
+ log["TargetSessionId"] = _cryptographically_hash(request.session.session_key)
157
184
 
158
185
  if "username" in user:
159
186
  log["TargetUsername"] = user["username"]
160
187
  elif hasattr(request, "user") and request.user.username:
161
188
  log["TargetUsername"] = request.user.username
162
189
 
163
- if "email" in user:
164
- log["ActorUsername"] = user["email"]
165
- elif hasattr(request, "user") and hasattr(request.user, "email") and request.user.email:
166
- log["ActorUsername"] = request.user.email
167
-
168
190
  if result_details:
169
191
  log["EventResultDetails"] = result_details
170
192
 
@@ -183,6 +205,12 @@ log_authentication.LoginMethod = AuthenticationLoginMethod
183
205
  log_authentication.Severity = Severity
184
206
 
185
207
 
208
+ def _cryptographically_hash(data: Optional[str]) -> Optional[str]:
209
+ if data is None:
210
+ return None
211
+ return sha3_512(data.encode("UTF-8")).hexdigest()
212
+
213
+
186
214
  def _event_code(event: AuthenticationEvent, result: Result) -> str:
187
215
  if event == AuthenticationEvent.Logon:
188
216
  if result == Result.Success:
@@ -19,7 +19,7 @@ class Severity(str, Enum):
19
19
  High = "High"
20
20
 
21
21
 
22
- class Client(TypedDict):
22
+ class Client(TypedDict, total=False):
23
23
  """Dictionary to represent properties of the HTTP Client."""
24
24
 
25
25
  """Internet Protocol Address of the client making the Authentication
@@ -29,17 +29,26 @@ class Client(TypedDict):
29
29
  requested_url: Optional[str]
30
30
 
31
31
 
32
- class Server(TypedDict):
32
+ class Server(TypedDict, total=False):
33
33
  """Dictionary to represent properties of the HTTP Server."""
34
34
 
35
35
  """
36
- A unique identifier for the server which serviced the Authentication event.
36
+ The FQDN that this server is listening to HTTP requests on. For example:
37
+ web.trade.gov.uk
37
38
 
38
39
  Defaults to the WSGI HTTP_HOST field if not provided.
39
40
  """
40
- hostname: Optional[str]
41
+ domain_name: Optional[str]
41
42
  """Internet Protocol Address of the server serving this request."""
42
43
  ip_address: Optional[str]
44
+ """
45
+ A unique (within DBT) identifier for the software running on the server.
46
+ For example: berry-auctions-frontend
47
+
48
+ Defaults to combining the environment variables COPILOT_APPLICATION_NAME and
49
+ COPILOT_SERVICE_NAME separated by a '-'.
50
+ """
51
+ service_name: Optional[str]
43
52
 
44
53
 
45
54
  def _default_severity(result: Result) -> Severity:
@@ -7,6 +7,8 @@ from typing import TypedDict
7
7
 
8
8
  from django.http import HttpRequest
9
9
 
10
+ from django_log_formatter_asim.ecs import _get_container_id
11
+
10
12
  from .common import Client
11
13
  from .common import Result
12
14
  from .common import Server
@@ -29,24 +31,30 @@ class FileActivityEvent(str, Enum):
29
31
  FolderModified = "FolderModified"
30
32
 
31
33
 
32
- class FileActivityFile(TypedDict):
33
- """Dictionary to represent properties of the target file."""
34
+ class FileActivityFileBase(TypedDict):
35
+ """Mandatory field definitions of FileActivityFile."""
34
36
 
35
37
  """
36
- The full, normalized path of the target file, including the folder or location,
37
- the file name, and the extension.
38
+ The full, normalized path of the target file, including the folder or
39
+ location, the file name, and the extension.
38
40
  """
39
41
  path: str
42
+
43
+
44
+ class FileActivityFile(FileActivityFileBase, total=False):
45
+ """Dictionary to represent properties of either the target or source
46
+ file."""
47
+
40
48
  """
41
49
  The name of the target file, without a path or a location, but with an
42
50
  extension if available. This field should be similar to the final element in
43
- the TargetFilePath field.
51
+ the *FilePath field.
44
52
 
45
53
  Defaults to extracting the name based off the path if not provided.
46
54
  """
47
55
  name: Optional[str]
48
56
  """
49
- The target file extension.
57
+ The file extension.
50
58
 
51
59
  Defaults to extracting the extension based off the path if not provided.
52
60
  """
@@ -57,13 +65,13 @@ class FileActivityFile(TypedDict):
57
65
  Allowed values are listed in the IANA Media Types repository.
58
66
  """
59
67
  content_type: Optional[str]
60
- """The SHA256 value of the target file."""
68
+ """The SHA256 value of the file."""
61
69
  sha256: Optional[str]
62
- """The size of the target file in bytes."""
70
+ """The size of the file in bytes."""
63
71
  size: Optional[int]
64
72
 
65
73
 
66
- class FileActivityUser(TypedDict):
74
+ class FileActivityUser(TypedDict, total=False):
67
75
  """
68
76
  A unique identifier for the user.
69
77
 
@@ -78,6 +86,7 @@ def log_file_activity(
78
86
  event: FileActivityEvent,
79
87
  result: Result,
80
88
  file: FileActivityFile,
89
+ source_file: Optional[FileActivityFile] = None,
81
90
  user: Optional[FileActivityUser] = None,
82
91
  server: Optional[Server] = None,
83
92
  client: Optional[Client] = None,
@@ -94,7 +103,7 @@ def log_file_activity(
94
103
  - Django Authentication systems current username
95
104
  - Client IP address
96
105
  - URL requested by the client
97
- - Server hostname
106
+ - Server domain name
98
107
  :param event: What File Event action was attempted, one of:
99
108
  - FileAccessed
100
109
  - FileCreated
@@ -109,7 +118,10 @@ def log_file_activity(
109
118
  - FolderModified
110
119
  :param result: What outcome did the action have, either "Success", "Failure", "Partial", "NA"
111
120
  :param file: Dictionary containing information on the target of this File event see
112
- FileActivityFile for more details.
121
+ FileActivityFile for more details.
122
+ :param source_file: Dictionary containing information on the source of this File event,
123
+ this MUST be used for a FileRenamed, FileMoved, FileCopied, FolderMoved
124
+ operation. See FileActivityFile for more details.
113
125
  :param user: Dictionary containing information on the logged in users username.
114
126
  :param server: Dictionary containing information on the server servicing this File event
115
127
  see Server class for more details.
@@ -127,50 +139,63 @@ def log_file_activity(
127
139
 
128
140
  See also: https://learn.microsoft.com/en-us/azure/sentinel/normalization-schema-file-event
129
141
  """
130
- if user == None:
131
- user = {}
132
- if server == None:
133
- server = {}
134
- if client == None:
135
- client = {}
136
-
137
- event_created = time_generated or datetime.datetime.now(tz=datetime.timezone.utc)
138
142
 
143
+ _log_file_activity(
144
+ request,
145
+ event,
146
+ result,
147
+ file,
148
+ source_file,
149
+ user={} if user == None else user,
150
+ server={} if server == None else server,
151
+ client={} if client == None else client,
152
+ event_created=time_generated or datetime.datetime.now(tz=datetime.timezone.utc),
153
+ severity=severity,
154
+ result_details=result_details,
155
+ message=message,
156
+ )
157
+
158
+
159
+ def _log_file_activity(
160
+ request: HttpRequest,
161
+ event: FileActivityEvent,
162
+ result: Result,
163
+ file: FileActivityFile,
164
+ source_file: Optional[FileActivityFile],
165
+ user: FileActivityUser,
166
+ server: Server,
167
+ client: Client,
168
+ event_created: datetime.datetime,
169
+ severity: Optional[Severity] = None,
170
+ result_details: Optional[str] = None,
171
+ message: Optional[str] = None,
172
+ ):
139
173
  log = {
140
174
  "EventSchema": "FileEvent",
141
175
  "EventSchemaVersion": "0.2.1",
142
176
  "EventType": event,
143
177
  "EventResult": result,
144
- "EventCreated": event_created.isoformat(), # TODO: Should this really be EventCreated, or TimeGenerated
178
+ "EventStartTime": event_created.isoformat(),
145
179
  "EventSeverity": severity or _default_severity(result),
146
- "TargetFilePath": file["path"],
147
180
  }
148
181
 
149
- if "name" in file:
150
- log["TargetFileName"] = file["name"]
151
- else:
152
- log["TargetFileName"] = os.path.basename(file["path"])
153
-
154
- if "extension" in file:
155
- log["TargetFileExtension"] = file["extension"]
156
- else:
157
- file_name_parts = list(filter(None, log["TargetFileName"].split(".", 1)))
158
- if len(file_name_parts) > 1:
159
- log["TargetFileExtension"] = file_name_parts[1]
160
-
161
- if "content_type" in file:
162
- log["TargetFileMimeType"] = file["content_type"]
182
+ log.update(_generate_file_attributes(file, "Target"))
183
+ if source_file:
184
+ log.update(_generate_file_attributes(source_file, "Src"))
163
185
 
164
- if "sha256" in file:
165
- log["TargetFileSHA256"] = file["sha256"]
186
+ if "domain_name" in server:
187
+ log["HttpHost"] = server["domain_name"]
188
+ elif "HTTP_HOST" in request.META:
189
+ log["HttpHost"] = request.get_host()
166
190
 
167
- if "size" in file:
168
- log["TargetFileSize"] = file["size"]
191
+ if "service_name" in server:
192
+ log["TargetAppName"] = server["service_name"]
193
+ elif os.environ.get("COPILOT_APPLICATION_NAME") and os.environ.get("COPILOT_SERVICE_NAME"):
194
+ app_name = f"{os.environ['COPILOT_APPLICATION_NAME']}-{os.environ['COPILOT_SERVICE_NAME']}"
195
+ log["TargetAppName"] = app_name
169
196
 
170
- if "hostname" in server:
171
- log["DvcHostname"] = server["hostname"]
172
- elif "HTTP_HOST" in request.META:
173
- log["DvcHostname"] = request.get_host()
197
+ if container_id := _get_container_id():
198
+ log["TargetContainerId"] = container_id
174
199
 
175
200
  if "ip_address" in client:
176
201
  log["SrcIpAddr"] = client["ip_address"]
@@ -184,7 +209,7 @@ def log_file_activity(
184
209
 
185
210
  if "username" in user:
186
211
  log["TargetUsername"] = user["username"]
187
- elif request.user.username:
212
+ elif hasattr(request, "user") and request.user.username:
188
213
  log["TargetUsername"] = request.user.username
189
214
 
190
215
  if result_details:
@@ -199,6 +224,33 @@ def log_file_activity(
199
224
  print(json.dumps(log), flush=True)
200
225
 
201
226
 
227
+ def _generate_file_attributes(file: FileActivityFile, prefix: str) -> dict:
228
+ log = {prefix + "FilePath": file["path"]}
229
+
230
+ if "name" in file:
231
+ log[prefix + "FileName"] = file["name"]
232
+ else:
233
+ log[prefix + "FileName"] = os.path.basename(file["path"])
234
+
235
+ if "extension" in file:
236
+ log[prefix + "FileExtension"] = file["extension"]
237
+ else:
238
+ file_name_parts = list(filter(None, log[prefix + "FileName"].split(".", 1)))
239
+ if len(file_name_parts) > 1:
240
+ log[prefix + "FileExtension"] = file_name_parts[1]
241
+
242
+ if "content_type" in file:
243
+ log[prefix + "FileMimeType"] = file["content_type"]
244
+
245
+ if "sha256" in file:
246
+ log[prefix + "FileSHA256"] = file["sha256"]
247
+
248
+ if "size" in file:
249
+ log[prefix + "FileSize"] = file["size"]
250
+
251
+ return log
252
+
253
+
202
254
  log_file_activity.Event = FileActivityEvent
203
255
  log_file_activity.Result = Result
204
256
  log_file_activity.Severity = Severity
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-log-formatter-asim
3
- Version: 1.1.0a3
3
+ Version: 1.2.0a0
4
4
  Summary: Formats Django logs in ASIM format.
5
5
  License: MIT
6
6
  Author: Department for Business and Trade Platform Team
@@ -25,8 +25,6 @@ The library formats Django logs in [ASIM format](https://learn.microsoft.com/en-
25
25
 
26
26
  Mapping to the format may not be complete, but best effort has been made to create logical field mappings.
27
27
 
28
- If you need to amend the mapping, you can implement a custom formatter.
29
-
30
28
  ## Installation
31
29
 
32
30
  ``` shell
@@ -35,7 +33,16 @@ pip install django-log-formatter-asim
35
33
 
36
34
  ## Usage
37
35
 
38
- Using in a Django logging configuration:
36
+ This package provides the following ASIM functionality:
37
+
38
+ - A Python [logging.Formatter] implementation.
39
+ - A module of functions `django_log_formatter_asim.events` which generate ASIM event log entries.
40
+
41
+ [logging.Formatter]: https://docs.python.org/3/library/logging.html#formatter-objects
42
+
43
+ ### `logging.Formatter` setup
44
+
45
+ Using the formatter in a Django logging configuration:
39
46
 
40
47
  ``` python
41
48
  from django_log_formatter_asim import ASIMFormatter
@@ -66,15 +73,61 @@ LOGGING = {
66
73
  },
67
74
  }
68
75
  ```
69
- In this example we assign the ASIM formatter to a `handler` and ensure both `root` and `django` loggers use this `handler`. We then set `propagate` to `False` on the `django` logger, to avoid duplicating logs at the root level.
70
76
 
71
- ## Dependencies
77
+ In this example we assign the ASIM formatter to a `handler` and ensure both `root` and `django` loggers use this `handler`.
78
+ We then set `propagate` to `False` on the `django` logger, to avoid duplicating logs at the root level.
72
79
 
73
- This package uses [Django IPware](https://github.com/un33k/django-ipware) for IP address capture.
80
+ ### ASIM Events
74
81
 
75
- This package is compatible with [Django User Agents](https://pypi.org/project/django-user-agents) which, when used, will enhance logged user agent information.
82
+ The events mostly follow the Microsoft schema but have been tailored to Department of Business and Trade needs.
83
+
84
+ Events are designed for simple integrate into your Django app.
85
+ Each will take additional information from the [Django HttpRequest object][django-request].
86
+
87
+ [django-request]: https://docs.djangoproject.com/en/5.2/ref/request-response/#httprequest-objects
88
+
89
+ #### Authentication event
76
90
 
77
- ## Settings
91
+ Following the [ASIM Authentication Schema](https://learn.microsoft.com/en-us/azure/sentinel/normalization-schema-authentication).
92
+
93
+ ```python
94
+ # Example usage
95
+ from django_log_formatter_asim.events import log_authentication
96
+
97
+ log_authentication(
98
+ request,
99
+ event=log_authentication.Event.Logoff,
100
+ result=log_authentication.Result.Success,
101
+ login_method=log_authentication.LoginMethod.UsernamePassword,
102
+ )
103
+
104
+ # Example JSON printed to standard output
105
+ {
106
+ # Values provided as arguments
107
+ "EventType": "Logoff",
108
+ "EventResult": "Success",
109
+ "LogonMethod": "Username & Password",
110
+
111
+ # Calculated / Hard coded fields
112
+ "EventStartTime": "2025-07-02T08:15:20+00:00",
113
+ "EventSeverity": "Informational",
114
+ "EventOriginalType": "001c",
115
+ "EventSchema": "Authentication",
116
+ "EventSchemaVersion": "0.1.4",
117
+
118
+ # Taken from Django HttpRequest object
119
+ "HttpHost": "WebServer.local",
120
+ "SrcIpAddr": "192.168.1.101",
121
+ "TargetUrl": "https://WebServer.local/steel",
122
+ "TargetSessionId": "def456",
123
+ "TargetUsername": "Adrian"
124
+
125
+ # Taken from DBT Platform environment variables
126
+ "TargetAppName": "export-analytics-frontend",
127
+ }
128
+ ```
129
+
130
+ ### Settings
78
131
 
79
132
  `DLFA_LOG_PERSONALLY_IDENTIFIABLE_INFORMATION` - the formatter checks this setting to see if personally identifiable information should be logged. If this is not set to true, only the user's id is logged.
80
133
 
@@ -89,7 +142,7 @@ if is_copilot():
89
142
  DLFA_TRACE_HEADERS = ("X-B3-TraceId", "X-B3-SpanId")
90
143
  ```
91
144
 
92
- ## Formatter classes
145
+ ### Formatter classes
93
146
 
94
147
  ``` python
95
148
  ASIM_FORMATTERS = {
@@ -104,7 +157,7 @@ The default class for other loggers is:
104
157
  ASIMSystemFormatter
105
158
  ```
106
159
 
107
- ## Creating a custom formatter
160
+ ### Creating a custom `logging.Formatter`
108
161
 
109
162
  If you wish to create your own ASIM formatter, you can inherit from ASIMSystemFormatter and call _get_event_base to get the base level logging data for use in augmentation:
110
163
 
@@ -118,6 +171,12 @@ If you wish to create your own ASIM formatter, you can inherit from ASIMSystemFo
118
171
  return logger_event
119
172
  ```
120
173
 
174
+ ## Dependencies
175
+
176
+ This package uses [Django IPware](https://github.com/un33k/django-ipware) for IP address capture.
177
+
178
+ This package is compatible with [Django User Agents](https://pypi.org/project/django-user-agents) which, when used, will enhance logged user agent information.
179
+
121
180
  ## Contributing to the `django-log-formatter-asim` package
122
181
 
123
182
  ### Getting started
@@ -0,0 +1,10 @@
1
+ django_log_formatter_asim/__init__.py,sha256=ftbSWdejSnWK_MpT3RSfTgcs3E-661FM-e3ugnoiBqA,7133
2
+ django_log_formatter_asim/ecs.py,sha256=SSg3A5pfS5E-Hm7AXQnN1RrtXclgq07oSBxPCRn5gDg,536
3
+ django_log_formatter_asim/events/__init__.py,sha256=th8AEFNM-J5lNlO-d8Lk465jXqplE3IoTwj4DlscwYo,92
4
+ django_log_formatter_asim/events/authentication.py,sha256=WUHSllqTKTLnEdRlLn2SqCn3YR1_AKPA3y8Gm22CyIY,7808
5
+ django_log_formatter_asim/events/common.py,sha256=-QjR9QPkMDqD3nXPiukAJvm8qjUt2LwzhL_zN5ae_t0,1702
6
+ django_log_formatter_asim/events/file_activity.py,sha256=h8xlY0ABDCejAVnRMJ0aeWdNJZpu_54et1squMEfSM8,8755
7
+ django_log_formatter_asim-1.2.0a0.dist-info/LICENSE,sha256=dP79lN73--7LMApnankTGLqDbImXg8iYFqWgnExGkGk,1090
8
+ django_log_formatter_asim-1.2.0a0.dist-info/METADATA,sha256=ofAaVdlWggllH93apSQg16S0ZHaPmF1xd27APovOirc,7428
9
+ django_log_formatter_asim-1.2.0a0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
10
+ django_log_formatter_asim-1.2.0a0.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- django_log_formatter_asim/__init__.py,sha256=LDQQch2GKY6LRj-KOME393FCiwjisQ49ATWBEqAP4ZA,7684
2
- django_log_formatter_asim/events/__init__.py,sha256=th8AEFNM-J5lNlO-d8Lk465jXqplE3IoTwj4DlscwYo,92
3
- django_log_formatter_asim/events/authentication.py,sha256=OxWztyvgaIXRZm-ake_OrxVXoH4NVMW1PaWZQi67UN8,6863
4
- django_log_formatter_asim/events/common.py,sha256=YJ_t6nXShoMwgqgezUzi1f5qA-8J5DcBJWTqwbdZehs,1358
5
- django_log_formatter_asim/events/file_activity.py,sha256=Re51ZOrcy1k62bORruPwCTdhuQqExbN_OtZ9UuCgDBE,6967
6
- django_log_formatter_asim-1.1.0a3.dist-info/LICENSE,sha256=dP79lN73--7LMApnankTGLqDbImXg8iYFqWgnExGkGk,1090
7
- django_log_formatter_asim-1.1.0a3.dist-info/METADATA,sha256=3ivJhQmrP7al1vvMKK-zQX2iBiLX56nek4sLCUR9E1A,5581
8
- django_log_formatter_asim-1.1.0a3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
9
- django_log_formatter_asim-1.1.0a3.dist-info/RECORD,,