Repository: mlflow/mlflow

Issue:
[FR] Add timestamp parameter to mlflow.log_metric
### Willingness to contribute

Yes. I would be willing to contribute this feature with guidance from the MLflow community.

### Proposal Summary

The `log_metric` [method](https://github.com/mlflow/mlflow/blob/0deea32675d0c49df3276bcb10efd4a6ed8075b1/mlflow/tracking/client.py#L702) of the python `MlflowClient` class takes a `timestamp` parameter as input which can then be used for visualisations in the UI instead of step.

However, this parameter doesn't exist in the fluent `log_metric` [method](https://github.com/mlflow/mlflow/blob/0deea32675d0c49df3276bcb10efd4a6ed8075b1/mlflow/tracking/fluent.py#L732). It is set by default to be the current timestamp.


It would be an optional parameter, defaulting to the current timestamp to keep the default behaviour similar to the current one.

### Motivation

> #### What is the use case for this feature?
This can be useful in runs where you make predictions over multiple timestamps. For example for time series problems where the model predicts multiple steps.
> #### Why is this use case valuable to support for MLflow users in general?
Monitoring metrics over time is common use case of data science.
> #### Why is this use case valuable to support for your project(s) or organization?
We are working with time series model which make predictions over multiple steps and we want to easily monitor metrics for each of these steps and be able to clearly identify the step by its timestamp (in our case date).
> #### Why is it currently difficult to achieve this use case?
It can currently only be done by creating an MlflowClient, which is of course manageable but we lose the advantage of having the fluent `log_metric` method.

### Details

The method could look like this:

```python
def log_metric(
    key: str, value: float, step: Optional[int] = None, synchronous: bool = True, timestamp: Optional[int] = None,
) -> Optional[RunOperations]:
    run_id = _get_or_start_run().info.run_id
    return MlflowClient().log_metric(
        run_id, key, value, timestamp or get_current_time_millis(), step or 0, synchronous=synchronous
    )
```

### What component(s) does this bug affect?

- [ ] `area/artifacts`: Artifact stores and artifact logging
- [ ] `area/build`: Build and test infrastructure for MLflow
- [ ] `area/docs`: MLflow documentation pages
- [ ] `area/examples`: Example code
- [ ] `area/gateway`: AI Gateway service, Gateway client APIs, third-party Gateway integrations
- [ ] `area/model-registry`: Model Registry service, APIs, and the fluent client calls for Model Registry
- [ ] `area/models`: MLmodel format, model serialization/deserialization, flavors
- [ ] `area/recipes`: Recipes, Recipe APIs, Recipe configs, Recipe Templates
- [ ] `area/projects`: MLproject format, project running backends
- [ ] `area/scoring`: MLflow Model server, model deployment tools, Spark UDFs
- [ ] `area/server-infra`: MLflow Tracking server backend
- [X] `area/tracking`: Tracking Service, tracking client APIs, autologging

### What interface(s) does this bug affect?

- [X] `area/uiux`: Front-end, user experience, plotting, JavaScript, JavaScript dev server
- [ ] `area/docker`: Docker use across MLflow's components, such as MLflow Projects and MLflow Models
- [ ] `area/sqlalchemy`: Use of SQLAlchemy in the Tracking Service or Model Registry
- [ ] `area/windows`: Windows support

### What language(s) does this bug affect?

- [ ] `language/r`: R APIs and clients
- [ ] `language/java`: Java APIs and clients
- [ ] `language/new`: Proposals for new client languages

### What integration(s) does this bug affect?

- [ ] `integrations/azure`: Azure and Azure ML integrations
- [ ] `integrations/sagemaker`: SageMaker integrations
- [ ] `integrations/databricks`: Databricks integrations


Discussion:
@mbenoit29 Thank you for bringing the issue to our attention! 
There is absolutely no reason not having that for fluent API. Would you mind if I assign this to you?
@B-Step62 No problem, happy to submit a PR!
@mbenoit29 Awesome, thank you so much for your contribution!

Resolving patch:
diff --git a/mlflow/tracking/fluent.py b/mlflow/tracking/fluent.py
--- a/mlflow/tracking/fluent.py
+++ b/mlflow/tracking/fluent.py
@@ -730,7 +730,11 @@ def delete_tag(key: str) -> None:
 
 
 def log_metric(
-    key: str, value: float, step: Optional[int] = None, synchronous: bool = True
+    key: str,
+    value: float,
+    step: Optional[int] = None,
+    synchronous: bool = True,
+    timestamp: Optional[int] = None,
 ) -> Optional[RunOperations]:
     """
     Log a metric under the current run. If no run is active, this method will create
@@ -746,6 +750,7 @@ def log_metric(
                   All backend stores will support values up to length 5000, but some
                   may support larger values.
     :param step: Metric step (int). Defaults to zero if unspecified.
+    :param timestamp: Time when this metric was calculated. Defaults to the current system time.
     :param synchronous: *Experimental* If True, blocks until the metric is logged
                         successfully. If False, logs the metric asynchronously and
                         returns a future representing the logging operation.
@@ -769,7 +774,12 @@ def log_metric(
     """
     run_id = _get_or_start_run().info.run_id
     return MlflowClient().log_metric(
-        run_id, key, value, get_current_time_millis(), step or 0, synchronous=synchronous
+        run_id,
+        key,
+        value,
+        timestamp or get_current_time_millis(),
+        step or 0,
+        synchronous=synchronous,
     )
 
 


Test patch: