[AIRFLOW-1385] Create abstraction for Airflow task logging - #2422
[AIRFLOW-1385] Create abstraction for Airflow task logging#2422allisonwang wants to merge 2 commits into
Conversation
1e5c5d9 to
a4a756a
Compare
Codecov Report
@@ Coverage Diff @@
## master #2422 +/- ##
==========================================
- Coverage 69.29% 69.29% -0.01%
==========================================
Files 146 148 +2
Lines 11240 11277 +37
==========================================
+ Hits 7789 7814 +25
- Misses 3451 3463 +12
Continue to review full report at Codecov.
|
1c66ec2 to
1e7bc43
Compare
There was a problem hiding this comment.
maybe a better name is airflow.utils.logging
There was a problem hiding this comment.
I was about to use logging but there is another file under airflow.utils that's named logging.py. Should we change the name of that file?
There was a problem hiding this comment.
Other projects (django) use airflow.utils.log
There was a problem hiding this comment.
We can't assume the logger will log into a file anymore so this line is removed.
There was a problem hiding this comment.
If the default Airflow FileHandler has some kind of init call that is run before the other log lines we could put that there.
1e7bc43 to
48e5078
Compare
e0bae17 to
131171d
Compare
There was a problem hiding this comment.
Is BaseAirflowTaskLogging better?
There was a problem hiding this comment.
I like BaseAirflowTaskLogging more, it's more explicit.
There was a problem hiding this comment.
s/custom/User-defined also AIRFLOW_HOME should be PYTHONPATH . Let's also update the README (airflow/docs/concepts.rst) with this, similar to how "Cluster Policy" is written.
There was a problem hiding this comment.
Any reason we can't reuse the existing "airflow_local_settings.py" logic in settings.py? We could basically copy what is done for the policy function in that file. That way we can centralize the logic for loading custom modules.
There was a problem hiding this comment.
Nit: grammar
"and optionally uploads to S3/GCS on task completion."
There was a problem hiding this comment.
I like BaseAirflowTaskLogging more, it's more explicit.
There was a problem hiding this comment.
Same comment as above, i.e. why are we doing this differently from the way airflow_local_settings works for the policy file. Also AIRFLOW_HOME should be PYTHONPATH (in general the comment should be similar to the one in settings.py for "def policy")
131171d to
f6ec201
Compare
There was a problem hiding this comment.
Why do you set this up in cli.py? and not in airflow.logging / airflow.utils.logging?
There was a problem hiding this comment.
The comment above indicates it needs to be output to STDOUT for parent to read the log. Does it make sense to pass this logic to other places?
|
Hi guys, Im not sure if I follow the approach. It seems very complex to me and a bit java-esque (ohhh the horror :P ). Should we do a vid conf on this? |
|
@bolkedebruin are you free now? I just messaged you on your gmail. |
There was a problem hiding this comment.
Let's log debug here whether or not the user defined airflow task logging was loaded.
|
@aoen yes I am, no message received yet it seems? |
|
@bolkedebruin Looking forward to the discussion! Here are some of my thoughts when I was designing the abstraction:
Let's take the current Airflow logging as an example for each of the above steps:
What's the best way to use conf to represent all those situations without making user implementing the interface? I considered multiple configuration style abstractions but none of them seems to even be able to abstract the current implementations. |
|
Hi @allisonwang So from my point of view I would take a leaf out of Django's book: So the configure logging I would expect something like Here In the TaskHandler you can then use something like: Doing it this way allows us to continue to use To get the location of the log (and to display it in the UI) several options exist, but I could imagine that you would extend the TaskHandler with a This approach ensures that you only have to create two new classes in your case (I guess you are using S3) airflow.utils.log.TaskHandler and airflow.utils.log.S3TaskHandler. And some adjustments to configure_logging and to TaskInstance in models.py to obtain the right logger. Maybe also to the some of the operators if they are miss behaving (ie. not using the default handler), but I just checked I do not think works needs to be done there. |
|
@bolkedebruin
I am still confused about how to address questions that's in current Airflow logging:
From my perspective, TaskHandler with configuration is a lot more complex and less extensible than having one interface.
It's definitely good to use and extend existing Python logging module if we can solve the above mention problems. |
So no I don't think your approach is more flexible, but the contrary:
|
|
@bolkedebruin Thanks for the discussing the problem with us. As I mentioned, this solution has difficulties in actual implementation. So I spent some time writing the logic out. I have my working code pasted below and some pros and cons for this approach. Pros:
Cons:
Codetask_handler.py import logging
class TaskHandler(logging.FileHandler):
def __init__(self, dag_id, task_id, execution_date, try_number):
# Do directory creation here, for simplicity let's just use the filename
self.filename = "{}-{}-{}-{}.log".format(dag_id, task_id, execution_date, try_number)
logging.FileHandler.__init__(self, self.filename)
def close(self):
# Upload to S3/GCS
super(logging.FileHandler, self).close()
def read(self, dag_id, task_id, execution_date, try_number):
# Parse the airflow.cfg and read from logging backend and worker machine.
return "log"cli.py import logging
import logging.config
CONFIG = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'airflow.task': {
# '()': 'airflow.utils.log.TaskFormatter',
'format': '[%(asctime)s] %(message)s',
}
},
'handlers': {
'console': {
'level': 'INFO',
'class': 'logging.StreamHandler',
},
'airflow.task': {
'level': 'INFO',
'class': 'task_handler.TaskHandler',
'formatter': 'airflow.task',
'dag_id': '{}',
'task_id': '{}',
'execution_date': '{}',
'try_number': '{}',
},
},
'loggers': {
'airflow.task': {
'handlers': ['airflow.task'],
'level': 'INFO',
'propagate': False,
},
}
}
TASK_LOGGER = 'airflow.task'
if __name__ == "__main__":
dag_id = "dag_id"
task_id = "task_id"
execution_date = "execution_date"
try_number = 1
### We need to include those in CLI run method:
# One of the two ways to dynamically construct the config based on
# http://codeinthehole.com/tips/a-deferred-logging-file-handler-for-django/
for handler in CONFIG.get('handlers').values():
if handler.get('class') == 'task_handler.TaskHandler':
handler['dag_id'] = handler['dag_id'].format(dag_id)
handler['task_id'] = handler['task_id'].format(task_id)
handler['execution_date'] = handler['execution_date'].format(execution_date)
handler['try_number'] = handler['try_number'].format(try_number)
print CONFIG
logging.config.dictConfig(CONFIG)
logger = logging.getLogger(TASK_LOGGER)
logger.info("testing")
# close all handlers. Maybe we should flush all handlers first?
for handler in logger.handlers:
handler.close()
##################
# webserver side #
##################
logger = logging.getLogger(TASK_LOGGER)
# What if there are more than one handlers?
print logger.handlers[0].read(dag_id, task_id, execution_date, try_number) |
I would use the flush method for uploading (https://github.com/python/cpython/blob/2.7/Lib/logging/__init__.py#L770), but that's arguable. I think you don't you need to close the handlers by hand, I presume these are closed on interpreter shutdown. But that can be tested of course. |
Max and I discussed using the configuration style today. This configuration is ultimately static and doesn't provide enough context when instantiating the handler (unless the second point is addressed). Ideally, we can use a log handler generator that returns a callable handler. It can then be instantiated during run time with task instance and other contexts as arguments. Then config dictionary would not be used in this design but might be used in each handler's implementation. I will sketch this new design out soon. |
What I would do is something like (high level pseudo code): You probably want to add a check if you are a still logging for the same dag_id,execution_date, task_id, but I think you get the point.
|
|
@bolkedebruin |
|
@allisonwang Some further thoughts on your No 1 (redirecting other logs), if logging is properly configured in the operators/task_runners etc (e.g. logging.getLogger(name) ), it is just a matter of correctly configuring the handler in the logging config to point to the TaskHandler. This will work as it will get the reference to the same logger that does have set_context, by python logging design. |
|
@bolkedebruin Make sense to me. Both jobs and task runner uses LoggingMixin which returns logger with format |
|
@allisonwang completely agree with not wanting to show models and jobs logging. This is also something from a risk perspective (separation of concerns) we like. |
872248f to
32bd58b
Compare
|
@bolkedebruin I refactored the original handler into |
30183d0 to
d6f3c37
Compare
d6f3c37 to
709b6b0
Compare
|
@allisonwang Nice! I don't have a very very strong opinion on using another handler in a handler, but it does feel somewhat unclean. I would prefer one that doesn't do this. As to your struggle on what the FileHandler does, you can just override the init method of course I guess? Overall I like it much better now, it starts making sense. |
| unit_test_mode = False | ||
|
|
||
| # Logging configuration path | ||
| logging_config_path = airflow.logging.airflow_logging_config.AIRFLOW_LOGGING_CONFIG |
There was a problem hiding this comment.
Don't forget to correct this (DEFAULT_LOGGING)
| filename = "{}.log".format(ti.execution_date.isoformat()) | ||
| return os.path.join(directory, filename) | ||
|
|
||
| def get_local_loc(self, ti): |
| """ | ||
| return "{}/{}".format(ti.dag_id, ti.task_id) | ||
|
|
||
| def get_log_relative_path(self, ti): |
| **locals()) | ||
| return log | ||
|
|
||
| def get_log_relative_dir(self, ti): |
| self.handler.setLevel(self.level) | ||
|
|
||
| def emit(self, record): | ||
| if self.handler: |
There was a problem hiding this comment.
I don't think we should lose log entries if a handler isn't present. Maybe throw an exception when context isn't present?
This PR splits logs based on try number and add tabs to display different task instance tries. **Note this PR is a temporary change for separating task attempts. The code in this PR will be refactored in the future. Please refer to #2422 for Airflow logging abstractions redesign.** Testing: 1. Added unit tests. 2. Tested on localhost. 3. Tested on production environment with S3 remote storage, MySQL database, Redis, one Airflow scheduler and two airflow workers. Closes #2383 from AllisonWang/allison--add-task- attempt
|
This PR is closed by accident. I will create a new PR for this change. |
|
We can just revert (and should) |
|
Dan should revert it already but PR doesn't seem to be re-openable. |
|
Just open a new one and link this one from there. This thread is getting pretty long anyways. |
This PR adds configurable task logging to Airflow. Please refer to #2422 for previous discussions. This is the first step of making entire Airflow logging configurable ([AIRFLOW-1454](https://issue s.apache.org/jira/browse/AIRFLOW-1454)). Closes #2464 from AllisonWang/allison--log- abstraction
Dear Airflow maintainers,
Please accept this PR. I understand that it will not be reviewed until I have checked off all the steps below!
JIRA
Description
This PR adds abilities to provide customized implementations of airflow task logging. It creates an abstraction for setting up, cleaning up and get task instance logs.
Tests
This change is primarily a refactor of logging logic. It is tested locally with custom logging implementations.
Commits
@aoen @saguziel