Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6fa6d1a
WIP: K8s config
uds5501 Jul 29, 2025
11c3f8e
Using fileTaskLogConfigs for storing task logs
uds5501 Jul 30, 2025
01dee63
Revert "Using fileTaskLogConfigs for storing task logs"
uds5501 Jul 30, 2025
45f1fda
Revert "WIP: K8s config"
uds5501 Jul 30, 2025
d34e193
WIP: Use delegation based task streamer
uds5501 Jul 30, 2025
12fa583
WIP: provide module bindings in k8s overlord module for delegate cons…
uds5501 Jul 30, 2025
99082a8
WIP: Enable multi level stream switching
uds5501 Jul 31, 2025
f6e225f
Complete switching log implementation
uds5501 Aug 1, 2025
61bf9dc
WIP: remove the embedded k8s test for now
uds5501 Aug 1, 2025
d8d22ae
Add StreamingTaskLogs to CLII
uds5501 Aug 1, 2025
493cf96
Add configuration error message for noop use case.
uds5501 Aug 1, 2025
04f9784
stream task status correctly
uds5501 Aug 4, 2025
aba0d14
Bind all extensions and refactor task log pushes
uds5501 Aug 4, 2025
a6a375c
make static checks happy
uds5501 Aug 4, 2025
297a713
add more tests
uds5501 Aug 4, 2025
ec7d715
Complete tests for indexing service task logs modules
uds5501 Aug 4, 2025
c1b788c
Complete addition of missing tests
uds5501 Aug 4, 2025
1784cec
Address review comments
uds5501 Aug 8, 2025
c30d8d3
make maven check happy
uds5501 Aug 8, 2025
e2e64cf
fix binder tests
uds5501 Aug 8, 2025
7002b47
Fix local module tests
uds5501 Aug 8, 2025
0d339e8
Remove check for log saves
uds5501 Aug 8, 2025
87cc328
missed removal
uds5501 Aug 8, 2025
53b18fe
missed removal - 2
uds5501 Aug 8, 2025
88aa31f
re add configuration for k8s task runner
uds5501 Aug 8, 2025
680182d
Revert "re add configuration for k8s task runner"
uds5501 Aug 8, 2025
ba273de
add indexing module to bind task logs
uds5501 Aug 8, 2025
5f1043d
address review comments.
uds5501 Aug 9, 2025
86aad85
accommodate nitpicks
uds5501 Aug 11, 2025
999d9c7
Merge branch 'master' of github.com:apache/druid into stopping_log_pu…
uds5501 Aug 11, 2025
e9e01fb
fix docker script
uds5501 Aug 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.google.inject.Provides;
import com.google.inject.multibindings.MapBinder;
import com.google.inject.name.Named;
import com.google.inject.name.Names;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.ConfigBuilder;
import org.apache.druid.discovery.NodeRole;
Expand All @@ -45,6 +46,8 @@
import org.apache.druid.guice.annotations.Smile;
import org.apache.druid.indexing.common.config.FileTaskLogsConfig;
import org.apache.druid.indexing.common.config.TaskConfig;
import org.apache.druid.indexing.common.tasklogs.ExternalLogStreamer;
import org.apache.druid.indexing.common.tasklogs.ExternalTaskLogs;
import org.apache.druid.indexing.common.tasklogs.FileTaskLogs;
import org.apache.druid.indexing.overlord.RemoteTaskRunnerFactory;
import org.apache.druid.indexing.overlord.TaskRunnerFactory;
Expand Down Expand Up @@ -290,13 +293,22 @@ public RunnerStrategy get()
private void configureTaskLogs(Binder binder)
{
PolyBind.createChoice(binder, "druid.indexer.logs.type", Key.get(TaskLogs.class), Key.get(FileTaskLogs.class));
PolyBind.createChoice(binder, "druid.indexer.logs.delegate.type", Key.get(TaskLogs.class, Names.named("delegate")), Key.get(FileTaskLogs.class));
JsonConfigProvider.bind(binder, "druid.indexer.logs", FileTaskLogsConfig.class);

final MapBinder<String, TaskLogs> taskLogBinder = Binders.taskLogsBinder(binder);
taskLogBinder.addBinding("noop").to(NoopTaskLogs.class).in(LazySingleton.class);
taskLogBinder.addBinding("file").to(FileTaskLogs.class).in(LazySingleton.class);
taskLogBinder.addBinding("external").to(ExternalTaskLogs.class).in(LazySingleton.class);

final MapBinder<String, TaskLogs> delegateTaskLogBinder = PolyBind.optionBinder(binder, Key.get(TaskLogs.class, Names.named("delegate")));
delegateTaskLogBinder.addBinding("noop").to(NoopTaskLogs.class).in(LazySingleton.class);
delegateTaskLogBinder.addBinding("file").to(FileTaskLogs.class).in(LazySingleton.class);

binder.bind(NoopTaskLogs.class).in(LazySingleton.class);
binder.bind(FileTaskLogs.class).in(LazySingleton.class);
binder.bind(ExternalTaskLogs.class).in(LazySingleton.class);
binder.bind(ExternalLogStreamer.class).in(LazySingleton.class);

binder.bind(TaskLogPusher.class).to(TaskLogs.class);
binder.bind(TaskLogKiller.class).to(TaskLogs.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.multibindings.MapBinder;
import com.google.inject.name.Names;
import org.apache.druid.indexing.common.config.FileTaskLogsConfig;
import org.apache.druid.indexing.common.tasklogs.ExternalLogStreamer;
import org.apache.druid.indexing.common.tasklogs.ExternalTaskLogs;
import org.apache.druid.indexing.common.tasklogs.FileTaskLogs;
import org.apache.druid.tasklogs.NoopTaskLogs;
import org.apache.druid.tasklogs.TaskLogKiller;
Expand All @@ -39,13 +42,22 @@ public class IndexingServiceTaskLogsModule implements Module
public void configure(Binder binder)
{
PolyBind.createChoice(binder, "druid.indexer.logs.type", Key.get(TaskLogs.class), Key.get(FileTaskLogs.class));
PolyBind.createChoice(binder, "druid.indexer.logs.delegate.type", Key.get(TaskLogs.class, Names.named("delegate")), Key.get(FileTaskLogs.class));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be a little confusing to have say these props set on the Overlord:

druid.indexer.logs.type=external
druid.indexer.logs.delegate.type=file

It is unclear why we need a delegate if we just wanted FileTaskLogs.

I think the intention is to direct task reports, status and payload to one place and logs to another.
So we need some kind of switching or composite task logs type, which would give us these props:

druid.indexer.logs.type=switching
druid.indexer.logs.switching.reportsType=file
druid.indexer.logs.switching.logsType=hdfs

With this, we just need a SwitchingTaskLogs implementation which takes in a reportsType and a logsType, both of which would be TaskLogs implementations.

@kfaraz kfaraz Jul 31, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To ensure that extensions register themselves correctly as a valid type for say druid.indexer.logs.switching.logsType, you would need to add a utility method in Binders, say bindTaskLogs and invoke it from all the relevant extension modules.

public static <T extends TaskLogs> void bindTaskLogs(Binder binder, String type, Class<T> clazz) {
    // bind to `druid.indexer.logs.type`
    // bind to `druid.indexer.logs.switching.reportsType`
    // bind to `druid.indexer.logs.switching.logsType`
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

druid.indexer.logs.switching.logsType=hdfs

The options I am trying to provide here is different implementations of logStorage vs logStreaming, maybe I could just extend this to druid.indexer.logs.switching.logs.storage and druid.indexer.logs.switching.streaming ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Yes, the same switching concept can be extended for that use case as well.

druid.indexer.logs.switching.streaming.logsPushType=noop
druid.indexer.logs.switching.streaming.logsStreamType=file

where logsPushType would bind to a TaskLogPusher object.
and logsStreamType would bind to a TaskLogStreamer object.

Would you still need to distinguish between reports and logs though?
Because I assume reports might need to be pushed to a different place.
If yes, you would still need a

druid.indexer.logs.switching.streaming.reportsType=noop/file/hdfs

@uds5501 uds5501 Jul 31, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds good, in my head I am looking to provide the flexibility just for log types for now. The delegated / reportType could take care of other task responsibilities. So finally, imo it should look like one of these.

Option 1 [asking for explicit types for each responsibility]

druid.indexer.logs.type=switching
druid.indexer.logs.switching.reportsType=file
druid.indexer.logs.switching.payloadManagerType=file
druid.indexer.logs.switching.logsType=switching
druid.indexer.logs.switching.logsType.logsPushType=noop
druid.indexer.logs.switching.logsType.logsStreamType=file

translating to

public class SwitchingTaskLogs implements TaskLogs
{
  private final TaskLogs reportDelegate;
  private final TaskLogs payloadDelegate;
  private final TaskLogs logDelegate;

  @Inject
  public SwitchingTaskLogs(
      @Named("report") TaskLogs reportDelegate,
      @Named("payload") TaskLogs payloadDelegate,
      @Named("log") TaskLogs logDelegate
   )
  {

    this.reportDelegate = reportDelegate;
    this.payloadDelegate = payloadDelegate;
    this.logDelegate = logDelegate;
  }
}

public class TaskLogManager implements TaskLogs  // this / some flavour of switching task log will be injected as switching log delegate to handle the task related responsibilities with an appropriate factory placed in front.
{
  private final TaskLogStreamer streamer;
  private final TaskLogPusher pusher; 

  @Inject
  public TaskLogManager(
      @Named("taskLogPusher") TaskLogs pusher,
      @Named("taskLogStreamer") TaskLogs streamer,
   )
  {
    this.pusher = pusher;
    this.streamer = streamer;
  }
}

or we could create one like

Option 2 [just keeping log handler separate while keeping the rest of the responsibilities with the delegate class]

druid.indexer.logs.type=switching
druid.indexer.logs.switching.delegate=file
druid.indexer.logs.switching.logsHandlerType=switching
druid.indexer.logs.switching.logHandler.logsPushType=noop
druid.indexer.logs.switching.logHandler.logsStreamType=file
public class SwitchingTaskLogs implements TaskLogs
{
  private final TaskLogs delegate;
  private final TaskLogs switchingLogDelegate; 

  @Inject
  public SwitchingTaskLogs(
      @Named("default") TaskLogs defaultDelegate,
      @Named("log") TaskLogs logDelegate,
   )
  {
    this.defaultDelegate = defaultDelegate;
    this.logDelegate = logDelegate;
  }
}

// the TaskLogManager stays the same.

I am personally leaning towards option 2 given the usecase in hand. what do you folks think?


EDIT: managed to make it work with a single SwitchingTaskLogs with factory. I've added the implementation in this PR for the same.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@uds5501 , I don't think we need two levels of a switching implementation. That would really complicate the configs.

You can provide some decent defaults

When druid.indexer.logs.type=switching

Property Default
druid.indexer.logs.switching.defaultType file
druid.indexer.logs.switching.logPushType use defaultType
druid.indexer.logs.switching.logStreamType use defaultType
druid.indexer.logs.switching.reportsType use defaultType

I don't think report and payload will ever need to be different.
If needed, we can add it in the future.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see, will use this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using this combination noew.

JsonConfigProvider.bind(binder, "druid.indexer.logs", FileTaskLogsConfig.class);

final MapBinder<String, TaskLogs> taskLogBinder = Binders.taskLogsBinder(binder);
taskLogBinder.addBinding("noop").to(NoopTaskLogs.class).in(LazySingleton.class);
taskLogBinder.addBinding("file").to(FileTaskLogs.class).in(LazySingleton.class);
taskLogBinder.addBinding("external").to(ExternalTaskLogs.class).in(LazySingleton.class);

final MapBinder<String, TaskLogs> delegateTaskLogBinder = PolyBind.optionBinder(binder, Key.get(TaskLogs.class, Names.named("delegate")));
delegateTaskLogBinder.addBinding("noop").to(NoopTaskLogs.class).in(LazySingleton.class);
delegateTaskLogBinder.addBinding("file").to(FileTaskLogs.class).in(LazySingleton.class);

binder.bind(NoopTaskLogs.class).in(LazySingleton.class);
binder.bind(FileTaskLogs.class).in(LazySingleton.class);
binder.bind(ExternalTaskLogs.class).in(LazySingleton.class);
binder.bind(ExternalLogStreamer.class).in(LazySingleton.class);

binder.bind(TaskLogPusher.class).to(TaskLogs.class);
binder.bind(TaskLogKiller.class).to(TaskLogs.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.druid.indexing.common.tasklogs;

import com.google.common.base.Optional;
import org.apache.druid.tasklogs.TaskLogStreamer;

import java.io.IOException;
import java.io.InputStream;

public class ExternalLogStreamer implements TaskLogStreamer
Comment thread
uds5501 marked this conversation as resolved.
Outdated
{
@Override
public Optional<InputStream> streamTaskLog(String taskid, long offset) throws IOException
{
return Optional.absent();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.druid.indexing.common.tasklogs;

import com.google.common.base.Optional;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import org.apache.druid.java.util.emitter.EmittingLogger;
import org.apache.druid.tasklogs.TaskLogs;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;

public class ExternalTaskLogs implements TaskLogs
Comment thread
uds5501 marked this conversation as resolved.
Outdated
{
private static final EmittingLogger log = new EmittingLogger(ExternalTaskLogs.class);
private final TaskLogs delegate;
private final ExternalLogStreamer logStreamer;

@Inject
public ExternalTaskLogs(@Named("delegate") TaskLogs delegate, ExternalLogStreamer logStreamer)
{
this.delegate = delegate;
this.logStreamer = logStreamer;
}

@Override
public Optional<InputStream> streamTaskLog(String taskid, long offset) throws IOException
{
return logStreamer.streamTaskLog(taskid, offset);
}

@Override
public Optional<InputStream> streamTaskReports(final String taskid) throws IOException
{
return delegate.streamTaskReports(taskid);
}

@Override
public Optional<InputStream> streamTaskStatus(final String taskid) throws IOException
{
return delegate.streamTaskReports(taskid);
}

@Override
public void pushTaskLog(String taskid, File logFile)
{
log.debug("Skipping task log push for task[%s]", taskid);
Comment thread
uds5501 marked this conversation as resolved.
Outdated
}

@Override
public void pushTaskPayload(String taskid, File taskPayloadFile) throws IOException
{
delegate.pushTaskPayload(taskid, taskPayloadFile);
}

@Override
public void killAll() throws IOException
{
delegate.killAll();
}

@Override
public void killOlderThan(long timestamp) throws IOException
{
delegate.killOlderThan(timestamp);
}

@Override
public void pushTaskReports(String taskid, File reportFile) throws IOException
{
delegate.pushTaskReports(taskid, reportFile);
}

@Override
public void pushTaskStatus(String taskid, File reportFile) throws IOException
{
delegate.pushTaskStatus(taskid, reportFile);
}

@Override
public Optional<InputStream> streamTaskPayload(String taskid) throws IOException
{
return delegate.streamTaskPayload(taskid);
}
}
Loading