Friday, February 27, 2015

MongoDB Write Operations

Write Operations Overview :

Write operations modify the data in mongodb. Write operations are atomic at document level.

If update operation include upsert:true, documents are inserted if the query condition does not match criteria.

db.people.update(
...    { name: "Andy" },
...    {
...       name: "Andy",
...       rating: 1,
...       score: 1
...    },
...    { upsert: true }
... )
WriteResult({
    "nMatched" : 0,
    "nUpserted" : 1,
    "nModified" : 0,
    "_id" : ObjectId("54f0b1051235b5c69441d9ea")
})
> db.people.update(    { name: "Andy" },    {       name: "Andy",       rating: 1,       score: 1    },    { upsert: true } )
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })


Write Concerns :

Unack : does not confirm whether error occured or not

Acknowledge : ensure that data is written and available for in-memory read but not persisted.

Journal : ensure that  data is written to disk.

What is journal : journal are binary content, journaling is process in which data is written to binary structure first in journal and then written to  data files. In case with clean shutdown, this files are removed. journal files starts with j_ in the journal directory

Replica Acknowledged : response confirms that data is written to primary as well as secondary.

Mongodb size of the document is 16 MB. when document increases more than this size, gridFS can be used. GridFS uses two collection, one for storing metadata and one for storing actual chunks.

Data Models design includes either having normalized data divided between collections or having single document with embedded data. having multiple collection poses for transctions related issues as mongodb have transaction commit at document level and no write operation can have more than one document involve.




MongoDB Query Plan and Distributed Queries

Mongodb query optimizer processes query and chooses the most efficient query plan for query. The results are buffered and query plan is cached, so next time when the query is fired cached query plan is used.

Query optimizer also evaluates query plan on timely basis based on the conditions like mongo restart, add or drop index, reIndex etc.

db.collection.getPlanCache() method provide interface to view query plan information and clear plan.

Distributed Queries : Sharded cluster allow you to partition data based on the shard key and mongos router based on the config metadata information will route the query to specific shard node only.

This is possible only if query includes shard key, in case if query does not include shard key, mongos router will route the query on all cluster.

Replica sets uses read preferences to determine how to route query. Read preferences can be per connection based or per operation based.

Default read preference are read on primary, nearest is on secodary with minimum n/w latency, secondaryOnly is read only through secondary and error if secondaries are not available. 

Secondary is when read from secondary and if all are not available read from primary.

Mongodb Installation and crud operation with index overview

Mongodb current version at the time of writing this blog is 2.8. It can be downloaded from

curl -O http://downloads.mongodb.org/osx/mongodb-osx-x86_64-2.6.8.tgz
 
or brew install mongodb on mac machines.
 
update PATH variable using 
 
export PATH=<mongodb-install-directory>/bin:$PATH 

and create data directory using

mkdir -p /data/db
 
now to run mongodb run command mongod or ./mongod from bin directory
 
mongo client - console based which can run through mongo or ./mongo
 
At this stage if you see prompt with mongo > means that you are connected to mongodb server
 
CRUD operations
 
Some understanding before we start on CRUD operations on console
 
1. show dbs will display all databases available
2. use test will connect to database test. If not existing at the time of inserting first
   document database will be created automatically.
3. mongo documents are json format, BSON is binary format type of json document
 
Inserting document in mongodb
 
db.students.insert({firstName:"paresh",lastName:"bhavsar",age:39,interest:["cricket","basket-ball","gardening"],address:{"zip":85298,"city":"phoenix"}})
 
Here, interest is array type and address is complex type element.

TO Retrieve document from collection
 
1. db.students.find({"firstName":"paresh"}) - search by first name
2. db.students.find({"address.city":"phoenix"}) - searching in complex structure
3. db.students.find({interest:{$all:["cricket","gardening"]}}) - search in array. all elements must match 

By default cursor are closed after 20 minutes this timeframe can be configured.

db.getServerStatus() command will provide information about server status.

_id is unique index for each collection in mongodb. you can not remove this index. All documents
inserted in mongodb will have default value of _id. 

To optimize the query performance, index needs to be created in mongodb.

At present as we have not created any index in the collection we can get following output for the query


 db.students.getIndexes()
[
 {
  "v" : 1,
  "key" : {
   "_id" : 1
  },
  "name" : "_id_",
  "ns" : "test.students"
 }
]

db.students.ensureIndex({firstName:1}) this will create single field index while below mentioned
will create compound index, as it is associated with multiple fields
 
db.students.ensureIndex({"firstName":1,"lastName":1})
 
now to check how many indexes are available we can use below mentioned command.
 
db.students.getIndexes()
[
 {
  "v" : 1,
  "key" : {
   "_id" : 1
  },
  "name" : "_id_",
  "ns" : "test.students"
 },
 {
  "v" : 1,
  "key" : {
   "firstName" : 1
  },
  "name" : "firstName_1",
  "ns" : "test.students"
 },
 {
  "v" : 1,
  "key" : {
   "firstName" : 1,
   "lastName" : 1
  },
  "name" : "firstName_1_lastName_1",
  "ns" : "test.students"
 }
]
 
 
 
As index is created we can findout explain command to check whether mongo is using indexes or not for query
 
db.students.find({firstName:"paresh"}).explain()
{
 "cursor" : "BtreeCursor firstName_1",
 "isMultiKey" : false,
 "n" : 1,
 "nscannedObjects" : 1,
 "nscanned" : 1,
 "nscannedObjectsAllPlans" : 2,
 "nscannedAllPlans" : 2,
 "scanAndOrder" : false,
 "indexOnly" : false,
 "nYields" : 0,
 "nChunkSkips" : 0,
 "millis" : 0,
 "indexBounds" : {
  "firstName" : [
   [
    "paresh",
    "paresh"
   ]
  ]
 },
 "server" : "Pareshs-MacBook-Pro.local:27017",
 "filterSet" : false
}

Here, index firstName_1 is used, for query with first name and last name it is mentioned below. 
 
db.students.find({firstName:"paresh","lastName":"bhavsar"}).explain()
{
 "cursor" : "BtreeCursor firstName_1_lastName_1",
 "isMultiKey" : false,
 "n" : 1,
 "nscannedObjects" : 1,
 "nscanned" : 1,
 "nscannedObjectsAllPlans" : 2,
 "nscannedAllPlans" : 2,
 "scanAndOrder" : false,
 "indexOnly" : false,
 "nYields" : 0,
 "nChunkSkips" : 0,
 "millis" : 0,
 "indexBounds" : {
  "firstName" : [
   [
    "paresh",
    "paresh"
   ]
  ],
  "lastName" : [
   [
    "bhavsar",
    "bhavsar"
   ]
  ]
 },
 "server" : "Pareshs-MacBook-Pro.local:27017",
 "filterSet" : false
} 
 
 
For queries which includes firstName and age indexes used are 
 
 db.students.find({firstName:"paresh","age":{$gt:30}}).explain()
{
 "cursor" : "BtreeCursor firstName_1_lastName_1",
 "isMultiKey" : false,
 "n" : 1,
 "nscannedObjects" : 1,
 "nscanned" : 1,
 "nscannedObjectsAllPlans" : 2,
 "nscannedAllPlans" : 2,
 "scanAndOrder" : false,
 "indexOnly" : false,
 "nYields" : 0,
 "nChunkSkips" : 0,
 "millis" : 0,
 "indexBounds" : {
  "firstName" : [
   [
    "paresh",
    "paresh"
   ]
  ],
  "lastName" : [
   [
    {
     "$minElement" : 1
    },
    {
     "$maxElement" : 1
    }
   ]
  ]
 },
 "server" : "Pareshs-MacBook-Pro.local:27017",
 "filterSet" : false
} 
 
 
 
Why is performance better, because index occupies less storage than the document and index information
is available in RAM and mostly sequential on disk.
 
If we are searching only thru ID field it uses IDCursor index to search documents
 
db.students.find({_id:1,"address.zip":85298}).explain()
{
 "cursor" : "BtreeCursor _id_",
 "isMultiKey" : false,
 "n" : 0,
 "nscannedObjects" : 0,
 "nscanned" : 0,
 "nscannedObjectsAllPlans" : 1,
 "nscannedAllPlans" : 1,
 "scanAndOrder" : false,
 "indexOnly" : false,
 "nYields" : 0,
 "nChunkSkips" : 0,
 "millis" : 0,
 "indexBounds" : {
  "_id" : [
   [
    1,
    1
   ]
  ]
 },
 "server" : "Pareshs-MacBook-Pro.local:27017",
 "filterSet" : false
}
 
 
 
IndexOnly Field : Covered Index

If returned fields in the query (called projection) are all covered with indexes, mongo will not
have to read from disk to retrieve the document and this give extremely fast, blazing fast
performance. This type of queries are called covered query.
 
> db.students.find({"firstName":"paresh"},{_id:0,"firstName":1}).explain()
 
{
 "cursor" : "BtreeCursor firstName_1",
 "isMultiKey" : false,
 "n" : 6,
 "nscannedObjects" : 0,
 "nscanned" : 6,
 "nscannedObjectsAllPlans" : 0,
 "nscannedAllPlans" : 12,
 "scanAndOrder" : false,
 "indexOnly" : true,
 "nYields" : 0,
 "nChunkSkips" : 0,
 "millis" : 0,
 "indexBounds" : {
  "firstName" : [
   [
    "paresh",
    "paresh"
   ]
  ]
 },
 "server" : "Pareshs-MacBook-Pro.local:27017",
 "filterSet" : false
} 

As we have index on lastName below mentioned query is displaying indexOnly value as true.

db.students.find({"firstName":"paresh"},{_id:0,"firstName":1,lastName:1}).explain()
{
 "cursor" : "BtreeCursor firstName_1_lastName_1",
 "isMultiKey" : false,
 "n" : 6,
 "nscannedObjects" : 0,
 "nscanned" : 6,
 "nscannedObjectsAllPlans" : 6,
 "nscannedAllPlans" : 12,
 "scanAndOrder" : false,
 "indexOnly" : true,
 "nYields" : 0,
 "nChunkSkips" : 0,
 "millis" : 0,
 "indexBounds" : {
  "firstName" : [
   [
    "paresh",
    "paresh"
   ]
  ],
  "lastName" : [
   [
    {
     "$minElement" : 1
    },
    {
     "$maxElement" : 1
    }
   ]
  ]
 },
 "server" : "Pareshs-MacBook-Pro.local:27017",
 "filterSet" : false
}





 
 
 
 


 


 
 

Tuesday, February 24, 2015

Certificates Using Java KeyTool and Portecle

Keystores stores certificates, using below mentioned command mysite-keystore.jks file will be created. There are two password to be provided one for keystore and second password for alias

keytool -genkey -alias mysite.com -keyalg RSA -keystore mysite-keystore.jks -keysize 2048



This will generate file named as mysite-keystore.jks file which will contain certificate information as provided.

Now, to list certificate information using keystore below mentioned command can be used.

keytool -list -v -keystore mysite-keystore.jks

This will list self signed certificate inside your keystore. To configure tomcat with selfsigned certificate below mentioned is server.xml file change for e.g.

<Connector port="7443" protocol="org.apache.coyote.http11.Http11Protocol"
               maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
               clientAuth="false" sslProtocol="TLS"
               keystoreFile="/home/ec2-user/certs/gruber-keystore"
                keystorePass="changeit"
  />

Certificates are signed by CA called certification authority, in which case we need to generate certificate signing request called csr. Keytool command can genereate csr request using below mentioned.

keytool -certreq -alias mysite.com -keystore mysite-keystore.jks -file mydomain.csr

This will generate mydomain.csr file which we need to send to CA and it will be signed by them.

CA provides certificates which includes root certificate and certificate chains. We can import those certificates using

keytool -import -trustcacerts -alias root -file Thawte.crt -keystore mydomain-keystore.jks

Portecle is very good tool for generating keystore, generating keypair, examining certificate and importing signed certificate.

More information on protecle can be found here. http://portecle.sourceforge.net/








Monday, February 23, 2015

Spring Boot (RESTful WS, https and swagger)

RESTful web services provides http endpoints. Http endpoints can have functions like POST, PUT, DELETE and GET.  Http methods can be mapped to operations like create, update, retrieve and delete. For example we can have Http GET endpoint to retrieve user information and http POST to create customer. PUT can be for update and DELETE for deleting customer.

Spring Boot provides very simple way to create Restful Webservice with embedded tomcat or jetty server. Below mentioned is dependency to be added for creating Restful WebService using Spring Boot.

       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>


We also require to add parent element inside pom.xml file as mentioned below.

   <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.10.RELEASE</version>
    </parent>


Now, RestEndpoint can be created by annotating class with @RestController as shown below.

@RestController
@RequestMapping("/users")

public class UserController {

 .....

}

@RestController will facilitate to create http endpoints for each function defined in UserController class. @RequestMapping will map /users URL to functions.

Functions can be declared as shown in below to create Http POST method.

 @RequestMapping(method = RequestMethod.POST)
 public CreateUserResponse createUser(@RequestBody User user) throws Exception {
        validateUser(user);
        String token = userService.createUser(user);
        return new CreateUserResponse(token);
    }


Here, method is annotated with POST method type and RequestMapping e.g. http://<host-name>:port/users ensure that it will call createUser function. @RequestBody will marshal json text to Java Object. For e.g. json request like {"firstName":"steven","lastName":"sharma"} will create java object as described below. Marshalling / Unmarshalling operation are provided by Jackson API.

class User {
    private String firstName;
    private String lastName;
    // getter and setter

}

To accept parameters from query string @RequestParam is used. so below function will be mapped to URL http://<localhost>:port/users/=?firstName=myname

@RequestMapping(method = RequestMethod.GET)
    public List<User> searchUser(@RequestParam("firstName") String firstName) throws Exception {
       return null;
    }


To accept parameter as a part of URL path for e.g.  URL http://<localhost>:port/users/firstName/myname

  @RequestMapping(value = "/firstName/{name}",method = RequestMethod.GET)
    public List<User> searchUser(@PathVariable("name") String name) throws Exception {
       return userService.getUsersByFirstName(name);
    }
 



Main function of the method is defined below.

@ComponentScan("basepackage")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}



mvn clean package
java -jar project.jar

Above commands will start tomcat as embedded server and generate rest endpoints as per discussed above.

Changing server port : 

Configuration parameters can be passed through yml file which is part of resource directory, To change port of tomcat which is embedded in spring-boot project. Add application.yml file in resources directory and add below mentioned configuration parameters, to ensure that rest endpoint will have port no. 9191 associated with.

server:
  port: 9191


Enabling Https :

Generating self signed certificate and private key using below mentioned command

keytool -genkey -alias aliasName -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore myapp.p12 -validity 365

This will create myapp.p12 file. Create Spring Bean as shown below from configuration file.

 @Bean
    public EmbeddedServletContainerCustomizer containerCustomizer() throws FileNotFoundException {
        final String absoluteKeystoreFile = ResourceUtils.getFile(keyStore).getAbsolutePath();

        final TomcatConnectorCustomizer customizer = new GruberTomcatConnectionCustomizer(
                absoluteKeystoreFile, "changeit", "PKCS12", "gruber",tomcatPort);

        return new EmbeddedServletContainerCustomizer() {

            @Override
            public void customize(ConfigurableEmbeddedServletContainer container) {
                if(container instanceof TomcatEmbeddedServletContainerFactory) {
                    TomcatEmbeddedServletContainerFactory containerFactory = (TomcatEmbeddedServletContainerFactory)          container;
                    containerFactory.addConnectorCustomizers(customizer);
                }
            };
        };
    }


Create below mentioned class

public class GruberTomcatConnectionCustomizer implements TomcatConnectorCustomizer {

        private String absoluteKeystoreFile;
        private String keystorePassword;
        private String keystoreType;
        private String keystoreAlias;
        private int tomcatPort;

        public GruberTomcatConnectionCustomizer(String absoluteKeystoreFile,
                String keystorePassword, String keystoreType, String keystoreAlias, int tomcatPort) {
            this.absoluteKeystoreFile = absoluteKeystoreFile;
            this.keystorePassword = keystorePassword;
            this.keystoreType = keystoreType;
            this.keystoreAlias = keystoreAlias.toLowerCase();
            this.tomcatPort = tomcatPort;

        }

        @Override
        public void customize(Connector connector) {
            connector.setPort(tomcatPort);
            connector.setSecure(true);
            connector.setScheme("https");

            connector.setAttribute("SSLEnabled", true);
            connector.setAttribute("sslProtocol", "TLS");
            connector.setAttribute("protocol", "org.apache.coyote.http11.Http11Protocol");
            connector.setAttribute("clientAuth", false);
            connector.setAttribute("keystoreFile", absoluteKeystoreFile);
            connector.setAttribute("keystoreType", keystoreType);
            connector.setAttribute("keystorePass", keystorePassword);
            connector.setAttribute("keystoreAlias", keystoreAlias);
            connector.setAttribute("keyPass", keystorePassword);
        }
 }


This configuration will start tomcat on secured port.

Here is configuration to be placed on application.yml file

tomcat-port: 7443

In case if you want to run tomcat with https and self-signed 

https://looksok.wordpress.com/2014/11/16/configure-sslhttps-on-tomcat-with-self-signed-certificate/












 



 



Monday, December 15, 2014

MongoDB Security

This blog describes details to understand the way mongo security work for client-server and between mongo instances in replica set environment. Securing data remains concern areas for solution architects and this blog will help developers to set up mongo environment enabling secuity. 

To implement security control for mongo data, all clients must be authenticated. Mongo authentication (username/password) is always validated with specific database, the way mongo stores users in system.users collection of admin database. Once authenticated before client execute any command authorization is verified to allow specific action on mongo instances based on role associated with. Authorization can be set based on pre-defined roles applicable over database or on individual collection.

For replica set environment, mongo instances communicate each other based on keyfile or X509 certificate. Same keyfile must be used on all instances of mongo instances and mongo instances are required to run with --keyfile option. Steps to run mongo instances with security enabled are mentioned below.

Create admin user with mongo built in role root. Below mentioned are steps to follow.

run mongd with --noauth option (e.g. mongod or mongod --noauth)
connect using mongo
db.createUser({"user":"superuser","pwd":"123456","roles":["root"]})
Now, run mongod without --auth option

connect to mongod instance using mongo command 
mongo -u superuser -p 123456 --authenticateDatabase admin

Important to understand that two same name users can be created provided both of them belongs to the difference database. So we can create two users with the same name provided we switch database using 'use databasename'

Below mentioned will not be authenticated as it mongo will try to authenticate user with test database while superuser belong to admin database and it must be authenticated using admin database.

mongo -u superuser -p 123456

Once connected as the role is root it can access any database.

Logically it is should not be possible to connect to mongo using 'mongo' command, but mongod will allow connection to be established with but not allowing any actions. for e..g. after connecting even 'show dbs' will not work.

Below mentioned is java code using mongo driver to connect to database.

 List<MongoCredential> credentials =  new ArrayList<>();     credentials.add(MongoCredential.createMongoCRCredential("superuser","admin","123456".toCharArray()));
DBCollection collection = new MongoClient(new ServerAddress("127.0.0.1",27017), credentials).getDB("tenantDB").getCollection("test");collection.insert(new BasicDBObject("x",1));

 In situation where we need to enable mongod instance with --auth option below mentioned are steps.

generate key file using 

openssl rand -base64 741 > mongodb-keyfile
chmod 600 mongodb-keyfile

mongod --replSet m101 --logpath "2.log" --dbpath /data/rs2 --port 27018 --smallfiles --oplogSize 64 --auth --keyFile mongodb-keyfile --fork
 

mongod --replSet m101 --logpath "2.log" --dbpath /data/rs2 --port 27018 --smallfiles --oplogSize 64 --auth --keyFile mongodb-keyfile --fork
 

mongod --replSet m101 --logpath "2.log" --dbpath /data/rs2 --port 27018 --smallfiles --oplogSize 64 --auth --keyFile mongodb-keyfile --fork

rs.initiate() from primary and adding members to replica set using 

rs.add("localhost:27018")
rs.add("localhost:27019")
rs.conf() and rs.status() will provide the status for the replica set. 

Any of the member to be added with replica set now must be supplied key file and it can run in --auth mode, else it will not be reachable by other members.
 
Reference :

http://docs.mongodb.org/manual/tutorial/deploy-replica-set-with-auth/







Thursday, December 11, 2014

MongDB ReplicaSet and Streaming

This blog is about providing information on mongodb replication and oplog streaming.

MongoDB instances (mongod) can be configured for replication. One instance will be working as primary while others will work as secondary. Primary instances are for reading and writing to mongo clusterl Secondary instances sync data written to primary using oplog (explain later in the same blog).

Below mentioned will created three mongod instance running on different ports.

Steps to create replica set …

mkdir -p /data/rs1 /data/rs2 /data/rs3
Create three mongod instances running on port no. 27017, 27018 and 27019
mongod --replSet m101 --logpath "1.log" --dbpath /data/rs1 --port 27017 --smallfiles --oplogSize 64 --fork

mongod --replSet m101 --logpath "2.log" --dbpath /data/rs2 --port 27018 --smallfiles --oplogSize 64 --fork

mongod --replSet m101 --logpath "3.log" --dbpath /data/rs3 --port 27019 --smallfiles --oplogSize 64 --fork
At this point all the instances are running separately, no replica is set.

Connect with one of the mongo instance

mongo —port 27017

Once you get mongo shell enter below configuration

config = { _id: "m101", members:[
          { _id : 0, host : "localhost:27017"},
          { _id : 1, host : "localhost:27018"},
          { _id : 2, host : "localhost:27019"} ]
};
rs.initiate(config);

Now, one of the instance will act as PRIMARY and remaining will be SECONDARY.


  • By default read / write operations are allowed only on the primary.To perform read operation on secondary one can enter slave.isOk() . Write operations are not permitted on secondary
  • Secondary are working as replica set (meaning write on primary will get sync with secondary in almost real time).
  • Secondary can be used for read load distribution which provides read operations horizontally scalable.

once replica is set, mongod instances will have local db which will have oplog.rs collection. oplog.rs collection is capped collection and it logs each operations (insert / update / delete) in any collection. for e.g. when we insert document in any of the collection in mongo, it is inserted in the oplog.rs.

For example below mentioned steps insert a document inserted in customer collection and its entry in oplog.rs collection.

It is possible to create tailable cursor on oplog.rs collection. (Note : Tailable cursor blocks the current thread on cursor.hasNext() method.)

Oplos will be rolled over once specified size of the oplog is reached deep

One of the implementation with my project we used oplog.rs to read document entry (insert / update / delete) for further processing. We also stored checkpoint       (oplog entry timestamp) so that when system is restarted it will process mongo documents from that point onwards. 

Program (java mongo driver) connected with one of the node will get all information about the replica set members. And if primary is down, application will transparently write to other mongod instance(which comes up as PRIMARY)

It is possible to add / remove members in the replica set without restarting mongo instances.

Now, let’s try to understanding sharding… Sharding add scalability to mongo architecture, shard instances are mongo instances and shard instance can have replica members to provide HA. Shard key can be created and there will be mongos instances which works as router to send read / write query to correct shard. Just to make aware that mongos communicate to mongod (PRIMARY) 

CRUD operations with mongo and oplog.rs corresponding entries

for e.g. if insert below mentioned statement we can find corrosponding entry in oplog.rs


m101:PRIMARY>use test;
m101:PRIMARY>db.customer.insert({“x”:1})
m101:PRIMARY>use local
m101:PRIMARY> db.oplog.rs.find().sort({"$natural":-1}).limit(1).pretty()
{
   "ts" : Timestamp(1418846623, 1),
   "h" : NumberLong("-5919409785401989933"),
   "v" : 2,
   "op" : "i",
   "ns" : "test.customer",
   "o" : {
      "_id" : ObjectId("5491e19f02d16dfe03bb04f6"),
      "x" : 1
   }
}


ns is namespace which is dbname.collectionname, o stands object that we are inserting in the namespace and op stands for type of operation we are performing in our case it is “i” stands for insert.


when we have update operation as mentioned below op will be type “u”


m101:PRIMARY> db.customer.update({"x":1},{"$set":{"y":1}});
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
m101:PRIMARY> use local
switched to db local
m101:PRIMARY> db.oplog.rs.find().sort({"$natural":-1}).limit(1).pretty()
{
   "ts" : Timestamp(1418847060, 1),
   "h" : NumberLong("-187714577821739110"),
   "v" : 2,
   "op" : "u",
   "ns" : "test.customer",
   "o2" : {
      "_id" : ObjectId("5491e19f02d16dfe03bb04f6")
   },
   "o" : {
      "$set" : {
          "y" : 1
      }
   }
}


in case when document is removed from any of the collection, we can have op value “d”


m101:PRIMARY> db.customer.remove({"x":1})
WriteResult({ "nRemoved" : 1 })
m101:PRIMARY> use local
switched to db local
m101:PRIMARY> db.oplog.rs.find().sort({"$natural":-1}).limit(1).pretty()
{
   "ts" : Timestamp(1418847272, 1),
   "h" : NumberLong("-849370824550739777"),
   "v" : 2,
   "op" : "d",
   "ns" : "test.customer",
   "b" : true,
   "o" : {
      "_id" : ObjectId("5491e19f02d16dfe03bb04f6")
   }
}


now let’s consider the scenario where we are updating existing value, there will not be any change in the oplog.rs entry, same as explained in the update entry.

Some of the useful links to further study