How to track document changes

The article is mainly aimed at specialists who have encountered the problem of “disappearing” documents in MongoDB and do not understand where to find the history of these documents, whether it was saved in the collection at all or was updated and deleted, and in what sequence all these actions occurred and at what time.

MongoDB is a popular NoSQL database widely used for storing large amounts of data. One of the key features of MongoDB is the Oplog mechanism (operational log), which allows you to track changes in collections. In this article, we will look at how to work with Oplog, search documents, convert timestamps, and output results in a readable format, which is extremely convenient for analysts.

Introduction to Oplog

Oplog is a log containing records of all operations that change the state of the database. These can be insert operations [i]updates [u] and deletions [d]. Oplog is especially useful for replicating data and tracking changes in real time.

Task

Suppose we have a collection of documents in MongoDB, and we want to track changes in the statuses of these documents. Our goal is to find documents with certain initial statuses, and then find subsequent changes to new statuses of these documents. We also want to convert MongoDB timestamps into a readable format and output the results.

Example of a ready-made query in Oplog

function convertNumberLongToISOString(numberLong) {
    var date = new Date(numberLong.toNumber());
    return date.toISOString();
}

function convertTimestampToMoscowTime(timestamp) {
    var date = new Date(timestamp.getHighBits() * 1000);
    var moscowOffset = 3 * 60 * 60 * 1000;
    var moscowTime = new Date(date.getTime() + moscowOffset);

    var day = ('0' + moscowTime.getDate()).slice(-2);
    var month = ('0' + (moscowTime.getMonth() + 1)).slice(-2);
    var year = moscowTime.getFullYear();
    var hours = ('0' + moscowTime.getHours()).slice(-2);
    var minutes = ('0' + moscowTime.getMinutes()).slice(-2);
    var seconds = ('0' + moscowTime.getSeconds()).slice(-2);

    return day + '-' + month + '-' + year + ' ' + hours + ':' + minutes + ':' + seconds;
}

var initialStatuses = ["PROCESS", "WELL_DONE"]; // может быть множество значений, перечисленных через запятую
var targetStatuses = ["ERROR"]; // может быть множество значений, перечисленных через запятую

var startDate = new Date('2020-03-01T00:00:00Z'); // проставляем нужную нам дату по Москве, начиная с которой будет осуществлятся поиск логов
var startTimestamp = Timestamp(Math.floor(startDate.getTime() / 1000), 0);

var initialDocuments = db.getCollection("oplog.rs").find({
    "ui": UUID("1234e321-a6fr-4egv-b2bf-5aedfv5rgv54"), // пишем значение из выполненного запроса в п.1
    "o.statusCode": { $in: initialStatuses },
    "ts": { $gte: startTimestamp }
}, {
     // тут перечисляем все поля из коллекции, которые хотим видеть в ответе запроса
     // перед каждым наименованием поля стоит o. - технически необходимо, сокращение от object
    "o.statusCode": 1,
    "o.cadId": 1,
    "o.pupsId": 1,
    "o.number": 1,
    "o.date": 1,
    "o.dateFast": 1,
    "o.refactor": 1,
    "o.rembo": 1,
    "o._id": 1,
    "op": 1, // тут не стоит o. , тк это поле относится уже к oplog-данным
    "ts": 1 // тут не стоит o. , тк это поле относится уже к oplog-данным
}).toArray();

if (initialDocuments.length > 0) {
    var documentIds = initialDocuments.map(function(doc) {
        return doc.o._id; // берем конкретный _id документа и смотрим переход по нужным нам статусам
    });

    var targetDocuments = db.getCollection("oplog.rs").find({
        "op": { $in: ["u", "i"] }, // ищем операции u и i (update - обновление и insert - новая), можно оставить только одну из них
        "ui": UUID("1234e321-a6fr-4egv-b2bf-5aedfv5rgv54"), // пишем значение из выполненного запроса в п.1
        "o._id": { $in: documentIds },
        "o.statusCode": { $in: targetStatuses },
        "ts": { $gte: startTimestamp }
    }, {
         // тут перечисляем все поля из коллекции, которые хотим видеть в ответе запроса
         // перед каждым наименованием поля стоит o. - технически необходимо, сокращение от object
       "o.statusCode": 1,
       "o.cadId": 1,
       "o.pupsId": 1,
       "o.number": 1,         
       "o.date": 1,
       "o.dateFast": 1,
       "o.refactor": 1,
       "o.rembo": 1,
       "o._id": 1,
       "op": 1, // тут не стоит o. , тк это поле относится уже к oplog-данным
       "ts": 1 // тут не стоит o. , тк это поле относится уже к oplog-данным
    }).toArray();

    var initialDocMap = {};
    initialDocuments.forEach(function(doc) {
        initialDocMap[doc.o._id] = doc;
    });

    var matchCount = 0;
    targetDocuments.forEach(function(doc) {
        if (matchCount >= 100) return; //  остановка вывода результатов после 100 совпадений, чтобы не грохнуть БД) но вы можете снять это ограничение или уменьшить

        var prevDoc = initialDocMap[doc.o._id];
        if (prevDoc && doc.ts.getHighBits() > prevDoc.ts.getHighBits()) {
            print("Document with pupsId" + prevDoc.o.pupsId + ":"); // тут пишем, что хотим видеть в заголовке найденных документов, в данном случае я хочу видеть в заголовке наименование компании
            printjson({ // перечисляем поля для красивого вывода, где можно преобразовать наименование поля из коллекции в более удобочитаемое
                _id: prevDoc.o._id, // слева пишем наименование поля из коллекции, а справа как хотим, чтобы отображалось в выводе в oplog
                refactor: prevDoc.o.refactor,
                rembo: prevDoc.o.rembo,
                cadId: prevDoc.o.cadId,
                pupsId: prevDoc.o.pupsId,
                statusCode: prevDoc.o.statusCode,
                number: prevDoc.o.number,
                dateFast: convertNumberLongToISOString(prevDoc.o.dateFast),
                date: prevDoc.o.date,
                op: prevDoc.op,
                ts: convertTimestampToMoscowTime(prevDoc.ts)
            });

            print("Document with pupsId" + doc.o.pupsId + ":");
            printjson({
                _id: prevDoc.o._id, // слева пишем наименование поля из коллекции, а справа как хотим, чтобы отображалось в выводе в oplog
                refactor: prevDoc.o.refactor,
                rembo: prevDoc.o.rembo,
                cadId: prevDoc.o.cadId,
                pupsId: prevDoc.o.pupsId,
                statusCode: prevDoc.o.statusCode,
                number: prevDoc.o.number,
                dateFast: convertNumberLongToISOString(prevDoc.o.dateFast),
                date: prevDoc.o.date,
                op: doc.op,
                ts: convertTimestampToMoscowTime(doc.ts)
            });

            matchCount++;
        }
    });

    if (matchCount === 0) {
        print("No matching documents found."); // пишем в свободной форме, какой текст вывести, если документов не нашлось
    }
} else {
    print("No documents found with initial statuses."); // пишем в свободной форме, какой текст вывести, если документов, с изначальным (initial) статусом не нашлось
}

// Поиск операций удаления
var deleteOperations = db.getCollection("oplog.rs").find({
    "op": "d"
}).limit(100).toArray(); // Лимит на выдачу документов

// Вывод операций удаления
deleteOperations.forEach(function(doc) {
    print("Delete operation:");
    printjson({
        _id: doc.o._id,
        ts: convertTimestampToMoscowTime(doc.ts),
        op: doc.op,
        ns: doc.ns,
        ui: doc.ui
    });
});

Example answer

Document with pupsId Ромашка:
{
  _id: '1234fgbf4d8d48aa8a2ca44565fds43j', // по этому _id мы нашли 3 записи обновления, которые произошли с этим документом после утсановленной нами даты в запросе 2020-03-01T00:00:00Z 
  refactor: 'sdgds435g',
  rembo: null,
  cadId: '444fff',
  pupsId: 'UBP9JN',
  statusCode: 'ERROR',
  number: '692343',
  dateFast: '2023-11-15T18:16:59.636Z',
  date: '15.11.2023',
  op: 'u', // также можно побаловаться запросом и при необходимости преобразовать "u" в "обновление"
  ts: '16-11-2023 00:19:02' // наш преобразованный NumberLong в удобочитаемом формате
}
Document with pupsId Ромашка:
{
  _id: '1234fgbf4d8d48aa8a2ca44565fds43j',
  refactor: 'sdfsdf454ff',
  rembo: null,
  cadId: '444fff',
  pupsId: 'UBP9JN',
  statusCode: 'WELL_DONE',
  number: '75324324',
  dateFast: '2023-11-16T15:02:04.655Z',
  date: '16.11.2023',
  op: 'u',
  ts: '16-11-2023 21:02:05'
}
Document with pupsId Ромашка:
{
  _id: '1234fgbf4d8d48aa8a2ca44565fds43j',
  refactor: 'sdfsdg4543fg',
  rembo: null,
  cadId: '444fff',
  pupsId: 'UBP9JN',
  statusCode: 'ERROR',
  number: '75234234',
  dateFast: '2023-11-16T15:02:05.655Z',
  date: '16.11.2023',
  op: 'u',
  ts: '16-11-2023 21:04:35'
}

Detailed steps to execute the query above

  1. Getting the UUID of a collection to query in Oplog.

  2. Preparing functions for converting timestamps.

  3. Search for initial documents with specific statuses.

  4. Search for subsequent changes to these documents.

  5. Output results in a readable format.

  6. Search for deletion operations [d].

Getting a Collection UUID for a Query in Oplog

Since oplog is stored in the local DB and is used for all collections within a server set at once, we need to get a specific UUID of the collection from which we want to get the data history for documents.

The query is entered in the query window of the selected collection.

db.getCollectionInfos()

Example answer

From this response we will need the value from the “uuid” field, which we will later insert into the “ui” field in the Oplog query window.

[
    {
        "name" : "your-collection-name",
        "type" : "collection",
        "options" : {

        },
        "info" : {
            "readOnly" : false,
            "uuid" : UUID("1234e321-a6fr-4egv-b2bf-5aedfv5rgv54") // необходимое значение
        },
        "idIndex" : {
            "v" : 2.0,
            "key" : {
                "_id" : 1.0
            },
            "name" : "_id_"
        }
    }
]

Preparing functions for converting timestamps

MongoDB stores timestamps in the format Timestamp And NumberLong. For ease of reading, we need to convert them to standard time formats.

Function to convert NumberLong to ISO string

function convertNumberLongToISOString(numberLong) {
    var date = new Date(numberLong.toNumber());
    return date.toISOString();
}

Function to convert Timestamp to Moscow time

function convertTimestampToMoscowTime(timestamp) {
    var date = new Date(timestamp.getHighBits() * 1000);
    var moscowOffset = 3 * 60 * 60 * 1000; // Москва на 3 часа впереди UTC
    var moscowTime = new Date(date.getTime() + moscowOffset);

    var day = ('0' + moscowTime.getDate()).slice(-2);
    var month = ('0' + (moscowTime.getMonth() + 1)).slice(-2);
    var year = moscowTime.getFullYear();
    var hours = ('0' + moscowTime.getHours()).slice(-2);
    var minutes = ('0' + moscowTime.getMinutes()).slice(-2);
    var seconds = ('0' + moscowTime.getSeconds()).slice(-2);

    return day + '-' + month + '-' + year + ' ' + hours + ':' + minutes + ':' + seconds;
}

Search for initial documents with specific statuses

Our task is to find documents with initial statuses ERRORstarting from a certain date.

In the example we use the field statusCodewhich is present in every document in our collection.

We perform a search by logic – initialStatuses – the status that was recorded first and targetStatuses – a status that updated the previous status.

To do this, we use the following query:

var initialStatuses = ["PROCESS", "WELL_DONE"]; // может быть множество значений, перечисленных через запятую
var startDate = new Date('2024-03-01T00:00:00Z'); // проставляем нужную нам дату по Москве
var startTimestamp = Timestamp(Math.floor(startDate.getTime() / 1000), 0);

var initialDocuments = db.getCollection("oplog.rs").find({
    "ui": UUID("1234e321-a6fr-4egv-b2bf-5aedfv5rgv54"), // пишем значение из выполненного запроса в п.1
    "o.statusCode": { $in: initialStatuses },
    "ts": { $gte: startTimestamp }
}, { // тут перечисляем все поля из коллекции, которые хотим видеть в ответе запроса
     // перед каждым наименованием поля стоит o. - технически необходимо, сокращение от object
    "o.statusCode": 1,
    "o.cadId": 1,
    "o.pupsId": 1,
    "o.number": 1,         
    "o.date": 1,
    "o.dateFast": 1,
    "o.refactor": 1,
    "o.rembo": 1,
    "o._id": 1,
    "op": 1,
    "ts": 1
}).toArray();

Search for subsequent changes to these documents

After receiving the initial documents, we look for their changes to target statuses DEBITED or DOCUMENT_DONE.

var targetStatuses = ["ERROR"];

if (initialDocuments.length > 0) {
    var documentIds = initialDocuments.map(function(doc) {
        return doc.o._id;
    });

    var targetDocuments = db.getCollection("oplog.rs").find({
        "op": { $in: ["u", "i"] }, // ищем операции u и i (update - обновление и insert - новая)
        "ui": UUID("1234e321-a6fr-4egv-b2bf-5aedfv5rgv54"), // пишем значение из выполненного запроса в п.1
        "o._id": { $in: documentIds },
        "o.statusCode": { $in: targetStatuses },
        "ts": { $gte: startTimestamp }
    }, {
       "o.statusCode": 1,
       "o.cadId": 1,
       "o.pupsId": 1,
       "o.number": 1,         
       "o.date": 1,
       "o.dateFast": 1,
       "o.refactor": 1,
       "o.rembo": 1,
       "o._id": 1,
       "op": 1,
       "ts": 1
    }).toArray();

Outputting results in a readable format

For convenience, we will create a map of the initial documents and then display the matched documents.

    var initialDocMap = {};
    initialDocuments.forEach(function(doc) {
        initialDocMap[doc.o._id] = doc;
    });

    var matchCount = 0;
    targetDocuments.forEach(function(doc) {
        if (matchCount >= 100) return;

        var prevDoc = initialDocMap[doc.o._id];
        if (prevDoc && doc.ts.getHighBits() > prevDoc.ts.getHighBits()) {
            print("Document with pupsId" + prevDoc.o.pupsId + ":");
            printjson({
                _id: prevDoc.o._id, // слева пишем наименование поля из коллекции, а справа как хотим, чтобы отображалось в выводе в oplog
                refactor: prevDoc.o.refactor,
                rembo: prevDoc.o.rembo,
                cadId: prevDoc.o.cadId,
                pupsId: prevDoc.o.pupsId,
                statusCode: prevDoc.o.statusCode,
                number: prevDoc.o.number,
                dateFast: convertNumberLongToISOString(prevDoc.o.dateFast),
                date: prevDoc.o.date,
                op: prevDoc.op,
                ts: convertTimestampToMoscowTime(prevDoc.ts)
            });

            print("Document with pupsId" + doc.o.pupsId + ":");
            printjson({
                _id: prevDoc.o._id, // слева пишем наименование поля из коллекции, а справа как хотим, чтобы отображалось в выводе в oplog
                refactor: prevDoc.o.refactor,
                rembo: prevDoc.o.rembo,
                cadId: prevDoc.o.cadId,
                pupsId: prevDoc.o.pupsId,
                statusCode: prevDoc.o.statusCode,
                number: prevDoc.o.number,
                dateFast: convertNumberLongToISOString(prevDoc.o.dateFast),
                date: prevDoc.o.date,
                op: doc.op,
                ts: convertTimestampToMoscowTime(doc.ts)
            });

            matchCount++;
        }
    });

    if (matchCount === 0) {
        print("No matching documents found.");
    }
} else {
    print("No documents found with initial statuses.");
}

Search for deletion operations [d]

We cannot insert an operation at the beginning of our query [d] deletions together with operations [u] (update) and [i] (input), because in the output oplog shows the ones we need _id And ts (timestamp – time) of a deleted document without the fields typical for documents in our collection.

// Поиск операций удаления
var deleteOperations = db.getCollection("oplog.rs").find({
    "op": "d"
}).limit(100).toArray(); // Лимит на выдачу документов

// Вывод операций удаления
deleteOperations.forEach(function(doc) {
    print("Delete operation:");
    printjson({
        _id: doc.o._id,
        ts: convertTimestampToMoscowTime(doc.ts),
        op: doc.op,
        ns: doc.ns,
        ui: doc.ui
    });
});

This script performs the following steps:

  1. Creates a map of source documents for quick access by ID.

  2. Iterates through each document from the target changes and checks if there is a corresponding source document with the same ID and a later timestamp.

  3. Outputs the original and modified document in a readable format, including converted timestamps.

Conclusion

In this article, we looked at how to work with MongoDB Oplog to track document changes. We learned how to search for documents with specific initial statuses, find their subsequent changes, and output the results in a readable format.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *