[IGN-14073] 8.3 replace for HistoryManager::storeHistory

@Kurt.Larson I have a 8.1 module using the old HistoryManager to store some AuditRecordSFData with:

```

context.getHistoryManager()
        .storeHistory(ds.getName(), new AuditRecordSFData(ds.getName(), insertQuery,
                record.getTimestamp(), record.getOriginatingSystem(),
                record.getOriginatingContext(), record.getActor(), record.getActorHost(),
                record.getAction(),
                record.getActionTarget(),
                actionTargetEquipement,
                ActionTargetDesignation,
                actionValue,
                actionValueDetail,
                record.getStatusCode()));

```

```
public class AuditRecordSFData implements DatasourceData {

String query;
String datasource;
Object[] values;

public AuditRecordSFData(String datasource, String query, Object... values) {
    this.datasource = datasource;
    this.query = query;
    this.values = values;
}


	@Override
	public int getDataCount() {
		return 1;
	}

	@Override
	public HistoryFlavor getFlavor() {
        return DatasourceData.SQLTAG;
	}

	@Override
	public String getLoggerName() {
		return "Audit Log Splitter Store & Forward";
	}

    @Override
	public String getSignature() {
		return "SFQuery: " + datasource + " - " + query;
	}

	@Override
	public void storeToConnection(SRConnection conn) throws Exception {
		try{
			conn.runPrepUpdate(query, values);
		} catch (Exception e) {
			logger.error("storeToConnection - query = {} values = {}",query,values);
			throw e;
		}
	}
}

```

what is the right migration path ?

```
//import com.inductiveautomation.ignition.gateway.history.DatasourceData; //import //com.inductiveautomation.ignition.gateway.history.HistoryFlavor; import com.inductiveautomation.ignition.gateway.storeforward.deprecated.DatasourceData; import com.inductiveautomation.ignition.gateway.storeforward.deprecated.HistoryFlavor;
```

and context.getStoreAndForwardManager().storeData() ?

but in that case, how to obtain a storageId ?

Yeah, that's correct. The StorageKey is a 2 part key - 1) engineId and 2) sinkId.

So that would be the datasource name (which has an associated engine) and the audit profile name.

Here's an example:

var auditRecord = DefaultAuditRecord.newBuilder()
                .action(record.getAction())
                .actionTarget(record.getActionTarget())
                .actionValue(record.getActionValue())
                .actor(record.getActor())
                .actorHost(record.getActorHost())
                .originatingContext(record.getOriginatingContext())
                .originatingSystem(record.getOriginatingSystem())
                .statusCode(record.getStatusCode())
                .timestamp(record.getTimestamp())
                .build();
var persistentRecord = PersistentAuditRecord.wrap(auditRecord);
var storageKey = StorageKey.of("datasourceName", "auditProfileName");

context.getStoreAndForwardManager().storeData(storageKey, persistentRecord);

If you don't know the actual audit profile name, but you have the datasource name, then you could do the below to grab the identifiers for the audit profile associated with that datasource / engine. I should make this easier; there's an EngineManifest but it's not accessible from the StoreAndForwardManager. For now though:

 /**
 * Retrieves the set of storage keys for audit records associated with the specified store-and-forward engine.
 * This method filters pipeline information tied to the engine to identify storage keys
 * that correspond to audit records.
 *
 * @param engineId the unique identifier of the store-and-forward engine from which audit storage keys are to be retrieved
 * @return a set of {@link StorageKey} objects representing storage destinations for audit records within the specified engine
 */
public Set<StorageKey> retrieveAuditStorageKeys(String engineId) {
    var engineInformation = context.getStoreAndForwardManager().getEngineInformation(engineId, false);

    return engineInformation.stream()
        .map(EngineInformation::pipelineInformation)
        .flatMap(List::stream)
        .filter(pi -> PersistentAuditRecord.FLAVOR.equals(pi.flavor()))
        .map(PipelineInformation::storageKey)
        .collect(Collectors.toSet());
}

Relevant Docs

StoreAndForwardManager

/**
 * Stores the provided persistent data associated with a specific storage key.
 * This is used to persist data into a store-and-forward system.
 *
 * @param storageKey the key identifying the storage destination and sink
 * @param data       the persistent data to store
 * @param <T>        the type of data to store, which must implement {@link PersistentData}
 * @throws Exception if an error occurs while storing the data
 */
default <T extends PersistentData> void storeData(StorageKey storageKey, T data) throws Exception {
    storeData(PersistentDataBundle.of(storageKey, data));
}

/**
 * Gets descriptive {@link EngineInformation} about the specified engine that is registered with this manager.
 * Quarantined data info will only be included if the specific {@param includeQuarantinedInfo} is {@code true}.
 *
 * @param engineId the id of the engine to get information for
 * @param includeQuarantinedInfo if {@link QuarantinedDataInfo} results should be included
 * @return an optional containing the engine information if it exists
 */
Optional<EngineInformation> getEngineInformation(String engineId, boolean includeQuarantinedInfo);

StorageKey

/**
 * A composite key that identifies storage destinations and sinks within the store-and-forward system.
 *
 * <p>This key serves dual purposes:
 * <ul>
 *   <li>Registration key for data sinks within engines</li>
 *   <li>Routing key for associating data with storage destinations</li>
 * </ul>
 *
 * <p>All storage destinations require both an engine ID and a sink ID. The sink ID
 * can be the same as the engine ID to indicate the primary/default storage location.</p>
 *
 * @param engineId identifies the target engine
 * @param sinkId identifies the specific sink within the engine
 */
public record StorageKey(
    String engineId,
    String sinkId
)


/**
 * Creates a StorageKey where the same ID is used for both engine and sink.
 * This represents the primary/default storage location within the engine.
 *
 * @param id the identifier to use for both engine and sink
 * @return new StorageKey instance with the same ID for both components
 * @throws IllegalArgumentException if id is null or blank
 */
public static StorageKey repeated(String id) {
    return new StorageKey(id, id);
}

/**
 * Creates a StorageKey with separate engine and sink identifiers.
 * This allows routing data to specific named sinks within an engine.
 *
 * @param engineId the engine identifier
 * @param sinkId the sink identifier
 * @return new StorageKey instance with separate engine and sink IDs
 * @throws IllegalArgumentException if engineId or sinkId is null or blank
 */
public static StorageKey of(String engineId, String sinkId) {
    return new StorageKey(engineId, sinkId);
}
1 Like

Oh wait, I'll leave the previous post up for posterity, but I see now that you're using DatasourceData. DatasourceData.SQLTAG doesn't exist anymore...

There's a DataSink called DatasourceQuerySink that takes in QuerySFData that looks identical to what you have above in your AuditRecordSFData.

public QuerySFData(String query, Object[] values, String datasource) {
    this.query = query;
    this.values = values;
    this.datasource = datasource;
}

I'm going to move that into gateway-api so that you can leverage that. Then you won't need your AuditRecordSFData class. You will need to handle potential upgrades from the 8.1 DatasourceData, which could look something like this:

// ... include existing AuditRecordSFData structure

@Override
public PersistentData upgrade(String engineId) {
    return new QuerySFData(query, values, datasource)
}

Upgrade method on legacy HistoricalData:

/**
 * Attempts to upgrade the historical data to persistent data using the specified engine ID if necessary.
 *
 * @param engineId the engine ID to use for the upgrade if necessary
 * @return the upgraded persistent data
 */
PersistentData upgrade(String engineId);

And then since you're storing simple data as a query then you could just use StorageKey.repeated(datasourceName) when storing data to the StoreAndForwardManager. That is what the DatasourceQuerySink storageKey is:

public DatasourceQuerySink(GatewayContext context, String dataSource) {
    super(context, StorageKey.repeated(dataSource), QuerySFData.FLAVOR, 1);
}

Heh. I'm glad I held off on a custom historian I've been contemplating..... :sweat_smile:

Well hopefully creating a Historian is easier than before. :anxious_face_with_sweat:

Feel free to message me / start another post if/when you do. I'll try to get up some SDK examples for both Store & Forward and Historian.

1 Like

I'm pretty sure I'm going to cheat with a tag actor when it comes up again. :innocent:

But dodging that migration hassle was my point. Not a ding on the new historian API.

1 Like